From d8598fdae1256ee782349f42e5fdf75b7e884bc2 Mon Sep 17 00:00:00 2001 From: Zhirkevich Alexander Y Date: Thu, 5 Sep 2024 17:23:48 +0300 Subject: [PATCH] wip --- .../skriptie/InterpretationContext.kt | 5 + .../alexzhirkevich/skriptie/ScriptEngine.kt | 5 +- .../alexzhirkevich/skriptie/ScriptRuntime.kt | 30 +- .../alexzhirkevich/skriptie/common/Errors.kt | 13 +- .../skriptie/common/OpAssign.kt | 32 +- .../skriptie/common/OpAssignByIndex.kt | 26 +- .../skriptie/common/OpGetVariable.kt | 18 + .../skriptie/common/OpIfCondition.kt | 21 +- .../alexzhirkevich/skriptie/common/OpIndex.kt | 8 +- .../skriptie/common/OpTryCatch.kt | 53 +- .../skriptie/common/ScriptUtil.kt | 28 +- .../skriptie/ecmascript/ESAny.kt | 2 +- .../ecmascript/ESInterpretationContext.kt | 2 +- .../skriptie/ecmascript/ESInterpreter.kt | 13 +- .../skriptie/ecmascript/ESInterpreterImpl.kt | 516 +++++++++++------- .../skriptie/ecmascript/ESNumber.kt | 3 +- .../skriptie/ecmascript/ESObject.kt | 41 +- .../skriptie/ecmascript/ESObjectAccessor.kt | 22 + .../skriptie/ecmascript/ESRuntime.kt | 8 +- .../skriptie/javascript/JSLangContext.kt | 245 +++++++++ .../skriptie/javascript/JSRuntime.kt | 265 +-------- .../skriptie/javascript/JsArray.kt | 18 +- .../skriptie/javascript/JsNumber.kt | 4 +- .../skriptie/javascript/JsString.kt | 4 +- .../src/commonTest/kotlin/js/FunctionsTest.kt | 11 + .../src/commonTest/kotlin/js/JsArrayTest.kt | 19 +- .../src/commonTest/kotlin/js/JsTestUtil.kt | 12 +- .../src/commonTest/kotlin/js/ObjectTest.kt | 70 +++ .../src/commonTest/kotlin/js/SyntaxTest.kt | 66 ++- 29 files changed, 1004 insertions(+), 556 deletions(-) create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESObjectAccessor.kt create mode 100644 skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JSLangContext.kt create mode 100644 skriptie/src/commonTest/kotlin/js/ObjectTest.kt diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/InterpretationContext.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/InterpretationContext.kt index 64af8b70..80de0f5e 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/InterpretationContext.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/InterpretationContext.kt @@ -1,5 +1,10 @@ package io.github.alexzhirkevich.skriptie +public object DummyInterpretationContext : InterpretationContext { + override fun interpret(callable: String?, args: List?): Expression? { + return null + } +} public interface InterpretationContext : Expression { diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ScriptEngine.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ScriptEngine.kt index 56926117..f593c80f 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ScriptEngine.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ScriptEngine.kt @@ -14,9 +14,8 @@ public fun ScriptEngine.invoke(script: String) : Any? { } public fun ScriptEngine( - context: ScriptRuntime, + runtime: ScriptRuntime, interpreter: ScriptInterpreter ): ScriptEngine = object : ScriptEngine, ScriptInterpreter by interpreter { - override val runtime: ScriptRuntime - get() = context + override val runtime: ScriptRuntime get() = runtime } \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ScriptRuntime.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ScriptRuntime.kt index 0d4e009e..87d8d19c 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ScriptRuntime.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ScriptRuntime.kt @@ -2,7 +2,6 @@ package io.github.alexzhirkevich.skriptie import io.github.alexzhirkevich.skriptie.common.SyntaxError import io.github.alexzhirkevich.skriptie.common.TypeError -import io.github.alexzhirkevich.skriptie.common.unresolvedReference public enum class VariableType { @@ -15,11 +14,11 @@ public interface ScriptRuntime : LangContext { public val comparator : Comparator - public operator fun contains(variable: String): Boolean + public operator fun contains(variable: Any?): Boolean - public operator fun get(variable: String): Any? + public operator fun get(variable: Any?): Any? - public fun set(variable: String, value: Any?, type: VariableType?) + public fun set(variable: Any?, value: Any?, type: VariableType?) public fun withScope( extraVariables: Map> = emptyMap(), @@ -29,7 +28,7 @@ public interface ScriptRuntime : LangContext { public fun reset() } -private class BlockScriptContext( +private class ScopedRuntime( private val parent : ScriptRuntime ) : DefaultRuntime(), LangContext by parent { @@ -39,7 +38,7 @@ private class BlockScriptContext( override val comparator: Comparator get() = parent.comparator - override fun get(variable: String): Any? { + override fun get(variable: Any?): Any? { return if (variable in variables) { super.get(variable) } else { @@ -47,11 +46,11 @@ private class BlockScriptContext( } } - override fun contains(variable: String): Boolean { + override fun contains(variable: Any?): Boolean { return super.contains(variable) || parent.contains(variable) } - override fun set(variable: String, value: Any?, type: VariableType?) { + override fun set(variable: Any?, value: Any?, type: VariableType?) { when { type == VariableType.Global -> parent.set(variable, value, type) type != null || variable in variables -> super.set(variable, value, type) @@ -62,30 +61,27 @@ private class BlockScriptContext( public abstract class DefaultRuntime : ScriptRuntime { - protected val variables: MutableMap> = mutableMapOf() + protected val variables: MutableMap> = mutableMapOf() private val child by lazy { - BlockScriptContext(this) + ScopedRuntime(this) } - override fun contains(variable: String): Boolean { + override fun contains(variable: Any?): Boolean { return variable in variables } - override fun set(variable: String, value: Any?, type: VariableType?) { - if (type == null && variable !in variables) { - unresolvedReference(variable) - } + override fun set(variable: Any?, value: Any?, type: VariableType?) { if (type != null && variable in variables) { throw SyntaxError("Identifier '$variable' is already declared") } if (type == null && variables[variable]?.first == VariableType.Const) { throw TypeError("Assignment to constant variable ('$variable')") } - variables[variable] = (type ?: variables[variable]?.first)!! to value + variables[variable] = (type ?: variables[variable]?.first ?: VariableType.Global) to value } - override fun get(variable: String): Any? { + override fun get(variable: Any?): Any? { return if (contains(variable)) variables[variable]?.second else Unit diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/Errors.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/Errors.kt index 83fec9ab..314fc567 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/Errors.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/Errors.kt @@ -1,6 +1,17 @@ package io.github.alexzhirkevich.skriptie.common -public sealed class SkriptieError(message : String?, cause : Throwable?) : Exception(message, cause) +import io.github.alexzhirkevich.skriptie.ecmascript.ESAny + +public sealed class SkriptieError(message : String?, cause : Throwable?) : Exception(message, cause), ESAny { + override fun get(variable: Any?): Any? { + return when(variable){ + "message" -> message + "stack" -> stackTraceToString() + "name" -> this::class.simpleName + else -> Unit + } + } +} public class SyntaxError(message : String? = null, cause : Throwable? = null) : SkriptieError(message, cause) diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpAssign.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpAssign.kt index d1cc3b4b..095fce7e 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpAssign.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpAssign.kt @@ -3,19 +3,30 @@ package io.github.alexzhirkevich.skriptie.common import io.github.alexzhirkevich.skriptie.Expression import io.github.alexzhirkevich.skriptie.ScriptRuntime import io.github.alexzhirkevich.skriptie.VariableType +import io.github.alexzhirkevich.skriptie.ecmascript.ESAny +import io.github.alexzhirkevich.skriptie.ecmascript.ESObject import io.github.alexzhirkevich.skriptie.invoke internal class OpAssign( val type : VariableType? = null, val variableName : String, + val receiver : Expression?=null, val assignableValue : Expression, private val merge : ((Any?, Any?) -> Any?)? ) : Expression { override fun invokeRaw(context: ScriptRuntime): Any? { val v = assignableValue.invoke(context) - - val current = context.get(variableName) + val r = receiver?.invoke(context) + + val current = if (receiver == null) { + context[variableName] + } else { + when (r){ + is ESAny -> r[variableName] + else -> null + } + } check(merge == null || current != null) { "Cant modify $variableName as it is undefined" @@ -25,11 +36,18 @@ internal class OpAssign( merge.invoke(current, v) } else v - context.set( - variable = variableName, - value = value, - type = type - ) + if (receiver == null) { + context.set( + variable = variableName, + value = value, + type = type + ) + } else { + when (r) { + is ESObject -> r[variableName] = value + else -> throw TypeError("Cannot set properties of ${if (r == Unit) "undefined" else r} (setting '$variableName')") + } + } return value } diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpAssignByIndex.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpAssignByIndex.kt index 1bf12f82..a9bce1cf 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpAssignByIndex.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpAssignByIndex.kt @@ -3,6 +3,7 @@ package io.github.alexzhirkevich.skriptie.common import io.github.alexzhirkevich.skriptie.Expression import io.github.alexzhirkevich.skriptie.ScriptRuntime import io.github.alexzhirkevich.skriptie.VariableType +import io.github.alexzhirkevich.skriptie.ecmascript.ESObject import io.github.alexzhirkevich.skriptie.invoke import io.github.alexzhirkevich.skriptie.javascript.JsArray @@ -26,17 +27,20 @@ internal class OpAssignByIndex( context.set(variableName, mutableListOf(), scope) return invoke(context) } else { - val i = context.toNumber(index.invoke(context)) - check(!i.toDouble().isNaN()) { - "Unexpected index: $i" - } - val index = i.toInt() + val idx = index(context) return when (current) { is JsArray-> { + val i = context.toNumber(idx) + + check(!i.toDouble().isNaN()) { + "Unexpected index: $i" + } + val index = i.toInt() + while (current.value.lastIndex < index) { current.value.add(Unit) } @@ -50,6 +54,18 @@ internal class OpAssignByIndex( } current.value[index] } + is ESObject -> { + val idxNorm = when (idx){ + is CharSequence -> idx.toString() + else -> idx + } + + if (idxNorm in current && merge != null){ + current[idxNorm] = merge.invoke(current[idxNorm], v) + } else { + current[idxNorm] = v + } + } else -> error("Can't assign '$current' by index ($index)") } } diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpGetVariable.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpGetVariable.kt index 259d354e..32fce3d4 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpGetVariable.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpGetVariable.kt @@ -14,6 +14,24 @@ internal value class OpConstant(val value: Any?) : Expression { } } +private object UNINITIALIZED + +internal class OpLazy( + private val init : (ScriptRuntime) -> Any? +) : Expression { + + private var value : Any? = UNINITIALIZED + + override fun invokeRaw(context: ScriptRuntime): Any? { + + if (value is UNINITIALIZED){ + value = init(context) + } + + return value + } +} + internal class OpGetVariable( val name : String, val receiver : Expression?, diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpIfCondition.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpIfCondition.kt index 35421978..eaf9da15 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpIfCondition.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpIfCondition.kt @@ -4,18 +4,19 @@ import io.github.alexzhirkevich.skriptie.Expression import io.github.alexzhirkevich.skriptie.invoke -internal fun OpIfCondition( - condition : Expression = OpConstant(true), +internal fun OpIfCondition( + condition : Expression, onTrue : Expression? = null, - onFalse : Expression? = null + onFalse : Expression? = null, + expressible : Boolean = false ) = Expression { - val expr = if (condition(it) as Boolean){ - onTrue - } else { - onFalse - } + val expr = if (it.isFalse(condition(it))) onFalse else onTrue - expr?.invoke(it) + val res = expr?.invoke(it) - Unit + if (expressible) { + res + } else { + Unit + } } \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpIndex.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpIndex.kt index 6050ec6a..eda0cf7f 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpIndex.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpIndex.kt @@ -9,16 +9,16 @@ internal class OpIndex( val index : Expression, ) : Expression { - override fun invokeRaw(context: ScriptRuntime): Any { + override fun invokeRaw(context: ScriptRuntime): Any? { return invoke(context, variable, index) } companion object { - fun invoke(context: ScriptRuntime, variable : Expression, index : Expression) : Any{ + fun invoke(context: ScriptRuntime, variable : Expression, index : Expression) : Any? { val v = checkNotEmpty(variable(context)) - val idx = (index(context).let(context::toNumber)).toInt() + val idx = index(context) - return v.valueAtIndexOrUnit(idx) + return v.valueAtIndexOrUnit(idx, context.toNumber(idx).toInt()) } } } \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpTryCatch.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpTryCatch.kt index 12d85e39..4ad7ac60 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpTryCatch.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpTryCatch.kt @@ -4,58 +4,53 @@ import io.github.alexzhirkevich.skriptie.Expression import io.github.alexzhirkevich.skriptie.VariableType import io.github.alexzhirkevich.skriptie.invoke -internal fun OpTryCatch( +internal class ThrowableValue(val value : Any?) : Throwable(message = value?.toString()) { + override fun toString(): String { + return value.toString() + " (thrown)" + } +} + +internal fun OpTryCatch( tryBlock : Expression, catchVariableName : String?, catchBlock : Expression?, finallyBlock : Expression?, ) = when { - catchBlock != null && finallyBlock != null -> - TryCatchFinally(tryBlock, catchVariableName, catchBlock, finallyBlock) + catchBlock != null -> + TryCatchFinally( + tryBlock = tryBlock, + catchVariableName = catchVariableName, + catchBlock = catchBlock, + finallyBlock = finallyBlock + ) - catchBlock != null -> TryCatch(tryBlock, catchVariableName, catchBlock) - finallyBlock != null -> TryFinally(tryBlock, finallyBlock) - else -> error("SyntaxError: Missing catch or finally after try") + finallyBlock != null -> TryFinally( + tryBlock = tryBlock, + finallyBlock = finallyBlock + ) + else -> throw SyntaxError("Missing catch or finally after try") } -private fun TryCatchFinally( +private fun TryCatchFinally( tryBlock : Expression, catchVariableName : String?, catchBlock : Expression, - finallyBlock : Expression, + finallyBlock : Expression? = null, ) = Expression { try { tryBlock(it) } catch (t: Throwable) { if (catchVariableName != null) { + val throwable = if (t is ThrowableValue) t.value else t it.withScope( - extraVariables = mapOf(catchVariableName to (VariableType.Const to t)), + extraVariables = mapOf(catchVariableName to (VariableType.Local to throwable)), block = catchBlock::invoke ) } 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( - extraVariables = mapOf(catchVariableName to (VariableType.Const to t)), - block = catchBlock::invoke - ) - } else { - catchBlock(it) - } + finallyBlock?.invoke(it) } } diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/ScriptUtil.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/ScriptUtil.kt index c36e8cc6..91fb6be7 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/ScriptUtil.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/ScriptUtil.kt @@ -2,6 +2,7 @@ package io.github.alexzhirkevich.skriptie.common import io.github.alexzhirkevich.skriptie.Expression import io.github.alexzhirkevich.skriptie.ScriptRuntime +import io.github.alexzhirkevich.skriptie.ecmascript.ESAny import io.github.alexzhirkevich.skriptie.invoke import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract @@ -51,13 +52,34 @@ internal fun Any.valueAtIndexOrUnit(index : Int) : Any { } } - is List<*> -> this.getOrElse(index) { Unit } - is Array<*> -> this.getOrElse(index) { Unit } - is CharSequence -> this.getOrNull(index) ?: Unit + is List<*> -> getOrElse(index) { Unit } + is Array<*> -> getOrElse(index) { Unit } + is CharSequence -> getOrNull(index) ?: Unit else -> Unit }!! } +internal fun Any.valueAtIndexOrUnit(index : Any?, numberIndex : Int) : Any { + val indexNorm = when (index){ + is CharSequence -> index.toString() + else -> index + } + return when (this) { + is Map<*, *> -> { + if (indexNorm in this) { + get(indexNorm) + } else { + Unit + } + } + + is List<*> -> getOrElse(numberIndex) { Unit } + is Array<*> -> getOrElse(numberIndex) { Unit } + is CharSequence -> getOrNull(numberIndex) ?: Unit + is ESAny -> get(indexNorm) + else -> Unit + }!! +} @Suppress("BanInlineOptIn") @OptIn(ExperimentalContracts::class) diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESAny.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESAny.kt index 91bc2089..e9801a99 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESAny.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESAny.kt @@ -7,7 +7,7 @@ public interface ESAny { public val type : String get() = "object" - public operator fun get(variable: String): Any? + public operator fun get(variable: Any?): Any? public operator fun invoke( function: String, diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESInterpretationContext.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESInterpretationContext.kt index ae9f7e99..bdb23d9e 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESInterpretationContext.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESInterpretationContext.kt @@ -3,7 +3,7 @@ package io.github.alexzhirkevich.skriptie.ecmascript import io.github.alexzhirkevich.skriptie.Expression import io.github.alexzhirkevich.skriptie.InterpretationContext -public open class EcmascriptInterpretationContext( +public open class ESInterpretationContext( public val namedArgumentsEnabled : Boolean = false ) : InterpretationContext { override fun interpret(callable: String?, args: List?): Expression? { diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESInterpreter.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESInterpreter.kt index 81a022d6..8232b5ed 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESInterpreter.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESInterpreter.kt @@ -5,11 +5,16 @@ import io.github.alexzhirkevich.skriptie.LangContext import io.github.alexzhirkevich.skriptie.Script import io.github.alexzhirkevich.skriptie.ScriptInterpreter -public class EcmascriptInterpreter( - private val interpretationContext : InterpretationContext, - private val langContext: LangContext +public class ESInterpreter( + private val langContext: LangContext, + private val interpretationContext : InterpretationContext = ESInterpretationContext(false), ) : ScriptInterpreter { + override fun interpret(script: String): Script { - return EcmascriptInterpreterImpl(script, langContext, interpretationContext).interpret() + return ESInterpreterImpl( + expr = script, + langContext = langContext, + globalContext = interpretationContext + ).interpret() } } diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESInterpreterImpl.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESInterpreterImpl.kt index 01aa81fc..bf0f888d 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESInterpreterImpl.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESInterpreterImpl.kt @@ -5,6 +5,7 @@ import io.github.alexzhirkevich.skriptie.InterpretationContext import io.github.alexzhirkevich.skriptie.LangContext import io.github.alexzhirkevich.skriptie.Script import io.github.alexzhirkevich.skriptie.VariableType +import io.github.alexzhirkevich.skriptie.common.Callable import io.github.alexzhirkevich.skriptie.common.Delegate import io.github.alexzhirkevich.skriptie.common.Function import io.github.alexzhirkevich.skriptie.common.FunctionParam @@ -33,6 +34,7 @@ import io.github.alexzhirkevich.skriptie.common.OpReturn import io.github.alexzhirkevich.skriptie.common.OpTryCatch import io.github.alexzhirkevich.skriptie.common.OpWhileLoop import io.github.alexzhirkevich.skriptie.common.SyntaxError +import io.github.alexzhirkevich.skriptie.common.ThrowableValue import io.github.alexzhirkevich.skriptie.common.unresolvedReference import io.github.alexzhirkevich.skriptie.invoke import io.github.alexzhirkevich.skriptie.isAssignable @@ -40,7 +42,6 @@ import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract internal val EXPR_DEBUG_PRINT_ENABLED = false - internal enum class LogicalContext { And, Or, Compare } @@ -49,8 +50,7 @@ internal enum class BlockContext { None, Loop, Function } - -internal class EcmascriptInterpreterImpl( +internal class ESInterpreterImpl( expr : String, private val langContext: LangContext, private val globalContext : InterpretationContext, @@ -153,9 +153,14 @@ internal class EcmascriptInterpreterImpl( private fun parseAssignment( context: Expression, blockContext: List, - unaryOnly : Boolean = false + unaryOnly: Boolean = false, + isExpressionStart: Boolean = false ): Expression { - var x = parseExpressionOp(context, blockContext = blockContext) + var x = parseExpressionOp( + context, + blockContext = blockContext, + isExpressionStart = isExpressionStart + ) if (EXPR_DEBUG_PRINT_ENABLED) { println("Parsing assignment for $x") } @@ -198,9 +203,10 @@ internal class EcmascriptInterpreterImpl( checkAssignment() parseAssignmentValue(x, null) } + eatSequence("++") -> { - check(x.isAssignable()) { - "Not assignable" + syntaxCheck(x.isAssignable()) { + "Value is not assignable" } OpIncDecAssign( variable = x, @@ -210,8 +216,8 @@ internal class EcmascriptInterpreterImpl( } eatSequence("--") -> { - check(x.isAssignable()) { - "Not assignable" + syntaxCheck(x.isAssignable()) { + "Value is not assignable" } OpIncDecAssign( variable = x, @@ -221,13 +227,13 @@ internal class EcmascriptInterpreterImpl( } eat('?') -> { - if (EXPR_DEBUG_PRINT_ENABLED){ + if (EXPR_DEBUG_PRINT_ENABLED) { println("making ternary operator: onTrue...") } val onTrue = parseAssignment(globalContext, blockContext) - if (!eat(':')){ + if (!eat(':')) { throw SyntaxError("Unexpected end of input") } if (EXPR_DEBUG_PRINT_ENABLED) { @@ -264,7 +270,8 @@ internal class EcmascriptInterpreterImpl( x is OpGetVariable -> OpAssign( variableName = x.name, - assignableValue = parseAssignment(globalContext, emptyList()), + receiver = x.receiver, + assignableValue = parseAssignment(globalContext, emptyList(),), type = x.assignmentType, merge = merge ).also { @@ -279,9 +286,10 @@ internal class EcmascriptInterpreterImpl( private fun parseExpressionOp( context: Expression, logicalContext: LogicalContext? = null, - blockContext: List + blockContext: List, + isExpressionStart: Boolean = false ): Expression { - var x = parseTermOp(context, blockContext) + var x = parseTermOp(context, blockContext, isExpressionStart) while (true) { prepareNextChar() x = when { @@ -366,8 +374,12 @@ internal class EcmascriptInterpreterImpl( } } - private fun parseTermOp(context: Expression, blockContext: List): Expression { - var x = parseFactorOp(context, blockContext) + private fun parseTermOp( + context: Expression, + blockContext: List, + isExpressionStart: Boolean = false + ): Expression { + var x = parseFactorOp(context, blockContext, isExpressionStart) while (true) { prepareNextChar() x = when { @@ -394,16 +406,29 @@ internal class EcmascriptInterpreterImpl( } } - private fun parseFactorOp(context: Expression, blockContext: List): Expression { + private fun parseFactorOp( + context: Expression, + blockContext: List, + isExpressionStart: Boolean = false + ): Expression { val parsedOp = when { - nextCharIs('{'::equals) -> parseBlock(context = emptyList()) + isExpressionStart && nextCharIs('{'::equals) -> + parseBlock(context = emptyList()) + + !isExpressionStart && eat('{') -> { + if (EXPR_DEBUG_PRINT_ENABLED) { + println("making object") + } - context === globalContext && eatSequence("++") -> { + parseObject() + } + + eatSequence("++") -> { val start = pos val variable = parseFactorOp(globalContext, blockContext) require(variable.isAssignable()) { - "Unexpected '++' as $start" + "Unexpected '++' at $start" } OpIncDecAssign( variable = variable, @@ -412,11 +437,11 @@ internal class EcmascriptInterpreterImpl( ) } - context === globalContext && eatSequence("--") -> { + eatSequence("--") -> { val start = pos val variable = parseFactorOp(globalContext, blockContext) require(variable.isAssignable()) { - "Unexpected '--' as $start" + "Unexpected '--' at $start" } OpIncDecAssign( variable = variable, @@ -425,26 +450,26 @@ internal class EcmascriptInterpreterImpl( ) } - context === globalContext && eat('+') -> + eat('+') -> Delegate(parseFactorOp(context, blockContext), langContext::pos) - context === globalContext && eat('-') -> + eat('-') -> Delegate(parseFactorOp(context, blockContext), langContext::neg) - context === globalContext && !nextSequenceIs("!=") && eat('!') -> + !nextSequenceIs("!=") && eat('!') -> OpNot(parseExpressionOp(context, blockContext = blockContext), langContext::isFalse) - context === globalContext && eat('(') -> { + eat('(') -> { val exprs = buildList { - if (eat(')')){ + if (eat(')')) { return@buildList } do { add(parseAssignment(context, blockContext = blockContext)) } while (eat(',')) - check(eat(')')) { - "Bad expression: Missing ')'" + if (!eat(')')) { + throw SyntaxError("Missing ')'") } } @@ -452,13 +477,13 @@ internal class EcmascriptInterpreterImpl( if (eatSequence("=>")) { OpConstant(parseArrowFunction(exprs, blockContext)) } else { - exprs.getOrElse(0){ + exprs.getOrElse(0) { throw SyntaxError("Unexpected token ')'") } } } - context === globalContext && nextCharIs { it.isDigit() || it == '.' } -> { + nextCharIs { it.isDigit() || it == '.' } -> { if (EXPR_DEBUG_PRINT_ENABLED) { print("making const number... ") } @@ -476,14 +501,14 @@ internal class EcmascriptInterpreterImpl( } NumberFormat.Hex.prefix -> { - check(numberFormat == NumberFormat.Dec && !isFloat) { + syntaxCheck(numberFormat == NumberFormat.Dec && !isFloat) { "Invalid number at pos $startPos" } numberFormat = NumberFormat.Hex } NumberFormat.Oct.prefix -> { - check(numberFormat == NumberFormat.Dec && !isFloat) { + syntaxCheck(numberFormat == NumberFormat.Dec && !isFloat) { "Invalid number at pos $startPos" } numberFormat = NumberFormat.Oct @@ -493,7 +518,7 @@ internal class EcmascriptInterpreterImpl( if (numberFormat == NumberFormat.Hex) { continue } - check(numberFormat == NumberFormat.Dec && !isFloat) { + syntaxCheck(numberFormat == NumberFormat.Dec && !isFloat) { "Invalid number at pos $startPos" } numberFormat = NumberFormat.Bin @@ -526,7 +551,7 @@ internal class EcmascriptInterpreterImpl( OpConstant(langContext.fromKotlin(num)) } - context === globalContext && nextCharIs('\''::equals) || nextCharIs('"'::equals) -> { + nextCharIs('\''::equals) || nextCharIs('"'::equals) -> { if (EXPR_DEBUG_PRINT_ENABLED) { print("making const string... ") } @@ -550,8 +575,8 @@ internal class EcmascriptInterpreterImpl( variable = context, index = parseExpressionOp(globalContext, blockContext = blockContext) ).also { - require(eat(']')) { - "Bad expression: Missing ']'" + syntaxCheck(eat(']')) { + "Missing ']'" } } } @@ -567,8 +592,8 @@ internal class EcmascriptInterpreterImpl( } add(parseExpressionOp(context, blockContext = blockContext)) } while (eat(',')) - require(eat(']')) { - "Bad expression: missing ]" + syntaxCheck(eat(']')) { + "Missing ]" } } OpMakeArray(arrayArgs) @@ -644,7 +669,6 @@ internal class EcmascriptInterpreterImpl( } - private fun parseFunction( context: Expression, func: String?, @@ -652,60 +676,8 @@ internal class EcmascriptInterpreterImpl( ): Expression { return when (func) { - "var", "let", "const" -> { - val scope = when (func) { - "var" -> VariableType.Global - "let" -> VariableType.Local - else -> VariableType.Const - } - - val start = pos - - when (val expr = parseAssignment(globalContext, emptyList())) { - is OpAssign -> { - OpAssign( - type = scope, - variableName = expr.variableName, - assignableValue = expr.assignableValue, - merge = null - ) - } - - is OpGetVariable -> { - OpAssign( - type = scope, - variableName = expr.name, - assignableValue = OpConstant(Unit), - merge = null - ) - } - - else -> throw SyntaxError( - "Unexpected identifier '${ - this.expr.substring(start).substringBefore(' ').trim() - }'" - ) - } - } - - "typeof" -> { - val expr = parseAssignment( - context = globalContext, - blockContext = emptyList(), - unaryOnly = true - ) - Expression { - when (val v = expr(it)) { - null -> "object" - Unit -> "undefined" - true, false -> "boolean" - - is ESAny -> v.type - else -> v::class.simpleName - } - } - } - + "var", "let", "const" -> parseVariable(func) + "typeof" -> parseTypeof() "null" -> OpConstant(null) "true" -> OpConstant(true) "false" -> OpConstant(false) @@ -713,12 +685,7 @@ internal class EcmascriptInterpreterImpl( OpConstant(parseFunctionDefinition(blockContext = blockContext)) } - "for" -> { - if (EXPR_DEBUG_PRINT_ENABLED) { - println("making for loop") - } - parseForLoop(blockContext) - } + "for" -> parseForLoop(blockContext) "while" -> { if (EXPR_DEBUG_PRINT_ENABLED) { @@ -732,91 +699,57 @@ internal class EcmascriptInterpreterImpl( ) } - "do" -> { + "do" -> parseDoWhile(blockContext) + "if" -> parseIf(blockContext) + "continue" -> { if (EXPR_DEBUG_PRINT_ENABLED) { - println("making do/while loop") + println("parsing loop continue") } - val body = parseBlock(context = blockContext + BlockContext.Loop) - - check(body is OpBlock) { - "Invalid do/while syntax" + syntaxCheck(BlockContext.Loop in blockContext){ + "Illegal continue statement: no surrounding iteration statement" } - check(eatSequence("while")) { - "Missing while condition in do/while block" - } - val condition = parseWhileCondition() - - OpDoWhileLoop( - condition = condition, - body = body, - isFalse = langContext::isFalse - ) + OpContinue() } - "if" -> { - + "break" -> { if (EXPR_DEBUG_PRINT_ENABLED) { - print("parsing if...") - } - - val condition = parseExpressionOp(globalContext, blockContext = blockContext) - - val onTrue = parseBlock(context = blockContext) - - val onFalse = if (eatSequence("else")) { - parseBlock(context = blockContext) - } else null - - OpIfCondition( - condition = condition, - onTrue = onTrue, - onFalse = onFalse - ) - } - - "continue" -> { - if (BlockContext.Loop in blockContext) { - if (EXPR_DEBUG_PRINT_ENABLED) { - println("parsing loop continue") - } - OpContinue() - } else { - throw SyntaxError("Illegal continue statement: no surrounding iteration statement") + println("parsing loop break") } - } - - "break" -> { - if (BlockContext.Loop in blockContext) { - if (EXPR_DEBUG_PRINT_ENABLED) { - println("parsing loop break") - } - OpBreak() - } else { - throw SyntaxError("Illegal break statement") + syntaxCheck(BlockContext.Loop in blockContext){ + "Illegal break statement" } + OpBreak() } "return" -> { - if (BlockContext.Function in blockContext) { - val expr = parseExpressionOp(globalContext, blockContext = blockContext) - if (EXPR_DEBUG_PRINT_ENABLED) { - println("making return with $expr") - } - OpReturn(expr) - } else { - throw SyntaxError("Illegal return statement") + if (EXPR_DEBUG_PRINT_ENABLED) { + println("making return") + } + syntaxCheck(BlockContext.Function in blockContext) { + "Illegal return statement" } + val expr = parseExpressionOp( + context = globalContext, + blockContext = blockContext, + ) + OpReturn(expr) } - - "try" -> { + "throw" -> { if (EXPR_DEBUG_PRINT_ENABLED) { - println("making try $expr") + println("parsing throw") + } + val throwable = parseAssignment(globalContext, blockContext) + + Expression { + val t = throwable(it) + throw if (t is Throwable) t else ThrowableValue(t) } - parseTryCatch(blockContext) } + "try" -> parseTryCatch(blockContext) + else -> { val args = parseFunctionArgs(func) @@ -877,29 +810,142 @@ internal class EcmascriptInterpreterImpl( } } + private fun parseVariable(type: String) :Expression { + val scope = when (type) { + "var" -> VariableType.Global + "let" -> VariableType.Local + else -> VariableType.Const + } + + val start = pos + + return when (val expr = parseAssignment(globalContext, emptyList())) { + is OpAssign -> { + OpAssign( + type = scope, + variableName = expr.variableName, + assignableValue = expr.assignableValue, + merge = null + ) + } + + is OpGetVariable -> { + OpAssign( + type = scope, + variableName = expr.name, + assignableValue = OpConstant(Unit), + merge = null + ) + } + + else -> throw SyntaxError( + "Unexpected identifier '${ + this.expr.substring(start).substringBefore(' ').trim() + }'" + ) + } + } + + private fun parseTypeof() : Expression { + val isArg = eat('(') + val expr = parseAssignment( + context = globalContext, + blockContext = emptyList(), + unaryOnly = true + ) + if (isArg) { + syntaxCheck(eat(')')) { + "Missing )" + } + } + return Expression { + when (val v = expr(it)) { + null -> "object" + Unit -> "undefined" + true, false -> "boolean" + + is ESAny -> v.type + is Callable -> "function" + else -> v::class.simpleName + } + } + } + + private fun parseDoWhile(blockContext: List) : Expression { + if (EXPR_DEBUG_PRINT_ENABLED) { + println("making do/while loop") + } + + val body = parseBlock(context = blockContext + BlockContext.Loop) + + syntaxCheck(body is OpBlock) { + "Invalid do/while syntax" + } + + syntaxCheck(eatSequence("while")) { + "Missing while condition in do/while block" + } + val condition = parseWhileCondition() + + return OpDoWhileLoop( + condition = condition, + body = body, + isFalse = langContext::isFalse + ) + } + private fun parseIf(blockContext: List) : Expression { + if (EXPR_DEBUG_PRINT_ENABLED) { + print("parsing if...") + } + + val condition = parseExpressionOp( + context = globalContext, + blockContext = blockContext, + ) + + val onTrue = parseBlock(context = blockContext) + + val onFalse = if (eatSequence("else")) { + parseBlock(context = blockContext) + } else null + + return OpIfCondition( + condition = condition, + onTrue = onTrue, + onFalse = onFalse + ) + } + private fun parseWhileCondition(): Expression { - check(eat('(')) { + syntaxCheck(eat('(')) { "Missing while loop condition" } - val condition = parseExpressionOp(globalContext, blockContext = emptyList()) + val condition = parseExpressionOp(globalContext, blockContext = emptyList(),) - check(eat(')')) { + syntaxCheck(eat(')')) { "Missing closing ')' in loop condition" } return condition } private fun parseTryCatch(blockContext: List): Expression { + if (EXPR_DEBUG_PRINT_ENABLED) { + println("making try") + } val tryBlock = parseBlock(requireBlock = true, context = blockContext) val catchBlock = if (eatSequence("catch")) { if (eat('(')) { val start = pos - while (!eat(')') && pos < expr.length) { - //nothing + val arg = parseFactorOp(globalContext, emptyList()) + syntaxCheck(arg is OpGetVariable){ + "Invalid syntax at $start" } - expr.substring(start, pos).trim() to parseBlock( + syntaxCheck(eat(')')){ + "Invalid syntax at $pos" + } + arg.name to parseBlock( scoped = false, requireBlock = true, context = blockContext @@ -922,21 +968,48 @@ internal class EcmascriptInterpreterImpl( } private fun parseForLoop(parentBlockContext: List): Expression { - check(eat('(')) - val assign = if (eat(';')) null else parseAssignment(globalContext, emptyList()) - check(assign is OpAssign?) + if (EXPR_DEBUG_PRINT_ENABLED) { + println("making for loop") + } + + syntaxCheck(eat('(')) { + "Invalid for loop" + } + + val assign = if (eat(';')) null + else parseAssignment( + context = globalContext, + blockContext = emptyList(), + ) + syntaxCheck(assign is OpAssign?) { + "Invalid for loop" + } if (assign != null) { - check(eat(';')) + syntaxCheck(eat(';')) { + "Invalid for loop" + } } - val comparison = if (eat(';')) null else parseAssignment(globalContext, emptyList()) + val comparison = if (eat(';')) null + else parseAssignment( + context = globalContext, + blockContext = emptyList(), + ) if (comparison != null) { - check(eat(';')) + syntaxCheck(eat(';')) { + "Invalid for loop" + } } - val increment = if (eat(')')) null else parseAssignment(globalContext, emptyList()) + val increment = if (eat(')')) null + else parseAssignment( + context = globalContext, + blockContext = emptyList(), + ) if (increment != null) { - check(eat(')')) + syntaxCheck(eat(')')) { + "Invalid for loop" + } } val body = parseBlock(scoped = false, context = parentBlockContext + BlockContext.Loop) @@ -953,9 +1026,11 @@ internal class EcmascriptInterpreterImpl( private fun parseArrowFunction( args: List, blockContext: List - ) : Function { + ): Function { val fArgs = args.filterIsInstance() - check(fArgs.size == args.size) + syntaxCheck(fArgs.size == args.size) { + "Invalid arrow function" + } val lambda = parseBlock(context = blockContext + BlockContext.Function) return Function( @@ -993,17 +1068,16 @@ internal class EcmascriptInterpreterImpl( default = it.assignableValue ) - else -> error("Invalid function declaration at $start") + else -> throw SyntaxError("Invalid function declaration at $start") } } } - checkNotNull(args) { - "Missing function args" + if (args == null) { + throw SyntaxError("Missing function args") } - - check(nextCharIs('{'::equals)) { + syntaxCheck(nextCharIs('{'::equals)) { "Missing function body at $pos" } @@ -1020,6 +1094,37 @@ internal class EcmascriptInterpreterImpl( ) } + private fun parseObject(extraFields: Map = emptyMap()): Expression { + val props = buildMap { + while (!eat('}')) { + + val start = pos + val name = parseTermOp(globalContext, emptyList()) + syntaxCheck(name is OpGetVariable) { + "Invalid syntax at $start" + } + + syntaxCheck(eat(':')) { + "Invalid syntax at $pos" + } + + if (EXPR_DEBUG_PRINT_ENABLED) { + println("making object property ${name.name}") + } + + this[name.name] = parseExpressionOp(globalContext, null, emptyList()) + eat(',') + } + } + extraFields + return Expression { r -> + Object("") { + props.forEach { + it.key eq it.value.invoke(r) + } + } + } + } + private fun parseBlock( scoped: Boolean = true, requireBlock: Boolean = false, @@ -1029,10 +1134,31 @@ internal class EcmascriptInterpreterImpl( val list = buildList { if (eat('{')) { while (!eat('}') && pos < expr.length) { - val expr = parseAssignment(globalContext, context) + val expr = parseAssignment(globalContext, context, isExpressionStart = true) + + if (size == 0 && expr is OpGetVariable && eat(':')) { + return parseObject( + mapOf( + expr.name to parseExpressionOp( + globalContext, + null, + emptyList() + ) + ) + ) + } if (expr is OpConstant && expr.value is Function) { - add(funcIndex++, OpAssign(VariableType.Local, expr.value.name, expr, null)) + add( + funcIndex++, + OpAssign( + type = VariableType.Local, + variableName = expr.value.name, + receiver = null, + assignableValue = expr, + merge = null + ) + ) } else { add(expr) } @@ -1090,6 +1216,18 @@ private enum class NumberFormat( Bin(2, "01", 'b') } +@OptIn(ExperimentalContracts::class) +public inline fun syntaxCheck(value: Boolean, lazyMessage: () -> Any) { + contract { + returns() implies value + } + + if (!value) { + val message = lazyMessage() + throw SyntaxError(message.toString()) + } +} + private val NumberFormatIndicators = NumberFormat.entries.mapNotNull { it.prefix } private val reservedKeywords = setOf( diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESNumber.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESNumber.kt index b008b9bb..a50c4a0b 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESNumber.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESNumber.kt @@ -58,7 +58,7 @@ internal class ESNumber : ESFunctionBase("Number") { num.toDoubleOrNull() ?: 0L } - override fun get(variable: String): Any? { + override fun get(variable: Any?): Any? { return when (variable) { "EPSILON" -> Double.MIN_VALUE "length" -> Double.MIN_VALUE @@ -78,7 +78,6 @@ internal class ESNumber : ESFunctionBase("Number") { } private fun String.trimParseInt(radix : Int) : Long? { - println("$radix ${drop(2)}") return when (radix) { 0 -> if (startsWith("0x",true)){ diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESObject.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESObject.kt index ff432a3a..b2d28d82 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESObject.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESObject.kt @@ -10,13 +10,20 @@ import kotlin.properties.PropertyDelegateProvider import kotlin.properties.ReadOnlyProperty public interface ESObject : ESAny { - public operator fun set(variable: String, value: Any?) - public operator fun contains(variable: String): Boolean + + public val keys : Set + public val entries : List> + + public operator fun set(variable: Any?, value: Any?) + public operator fun contains(variable: Any?): Boolean override fun invoke(function: String, context: ScriptRuntime, arguments: List): Any? { val f = get(function) if (f !is Callable) { - unresolvedReference(function) + when (f){ + "toString" -> return toString() + else -> unresolvedReference(function) + } } return f.invoke(arguments, context) } @@ -24,28 +31,25 @@ public interface ESObject : ESAny { internal open class ESObjectBase( internal val name : String, - private val map : MutableMap = mutableMapOf() + private val map : MutableMap = mutableMapOf() ) : ESObject { - init { - if ("toString" !in map){ - map["toString"] = Function( - name = "toString", - parameters = emptyList(), - body = { toString() }) - } - } + override val keys: Set + get() = map.keys.map { it.toString() }.toSet() + + override val entries: List> + get() = map.entries.map { listOf(it.key, it.value) } - override fun get(variable: String): Any? { + override fun get(variable: Any?): Any? { return if (variable in map) map[variable] else Unit } - override fun set(variable: String, value: Any?) { + override fun set(variable: Any?, value: Any?) { map[variable] = value } - override fun contains(variable: String): Boolean = variable in map + override fun contains(variable: Any?): Boolean = variable in map override fun toString(): String { return if (name.isNotBlank()){ @@ -82,6 +86,7 @@ private class ObjectScopeImpl( name: String, val o : ESObject = ESObjectBase(name) ) : ObjectScope { + override fun String.func( vararg args: FunctionParam, body: ScriptRuntime.(args: List) -> Any? @@ -98,7 +103,7 @@ private class ObjectScopeImpl( } override fun String.eq(value: Any?) { - o[this] = value + o.set(this, value) } } @@ -125,7 +130,7 @@ internal fun func( vararg args: FunctionParam, body: ScriptRuntime.(args: List) -> Any? ): PropertyDelegateProvider> = PropertyDelegateProvider { obj, prop -> - obj[prop.name] = Function( + obj.set(prop.name, Function( name = prop.name, parameters = args.toList(), body = { @@ -133,7 +138,7 @@ internal fun func( body(args.map { get(it.name) }) } } - ) + )) ReadOnlyProperty { thisRef, property -> thisRef[property.name] as Callable diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESObjectAccessor.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESObjectAccessor.kt new file mode 100644 index 00000000..5f8f00f9 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESObjectAccessor.kt @@ -0,0 +1,22 @@ +package io.github.alexzhirkevich.skriptie.ecmascript + +import io.github.alexzhirkevich.skriptie.Expression +import io.github.alexzhirkevich.skriptie.ScriptRuntime +import io.github.alexzhirkevich.skriptie.invoke + +internal class ESObjectAccessor : ESFunctionBase("Object") { + + init { + this.set("keys", "keys".func("o"){ + (it.firstOrNull() as? ESObject)?.keys ?: emptyList() + }) + + this.set("entries", "entries".func("o"){ + (it.firstOrNull() as? ESObject)?.entries ?: emptyList() + }) + } + + override fun invoke(args: List, context: ScriptRuntime): Any? { + return args.single().invoke(context) as ESObject + } +} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESRuntime.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESRuntime.kt index 3c5a88a8..beaf0633 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESRuntime.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESRuntime.kt @@ -17,6 +17,9 @@ public abstract class ESRuntime( ESComparator(this) } + override val keys: Set get() = emptySet() + override val entries: List> get() = emptyList() + init { init() } @@ -27,6 +30,7 @@ public abstract class ESRuntime( } private fun init() { + set("Object", ESObjectAccessor(), VariableType.Const) set("Number", ESNumber(), VariableType.Const) set("globalThis", this, VariableType.Const) set("Infinity", Double.POSITIVE_INFINITY, VariableType.Const) @@ -34,7 +38,7 @@ public abstract class ESRuntime( set("undefined", Unit, VariableType.Const) } - final override fun get(variable: String): Any? { + final override fun get(variable: Any?): Any? { if (variable in this){ return super.get(variable) } @@ -48,7 +52,7 @@ public abstract class ESRuntime( return super.get(variable) } - final override fun set(variable: String, value: Any?) { + final override fun set(variable: Any?, value: Any?) { set(variable, value, VariableType.Local) } diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JSLangContext.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JSLangContext.kt new file mode 100644 index 00000000..1b4b2b41 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JSLangContext.kt @@ -0,0 +1,245 @@ +package io.github.alexzhirkevich.skriptie.javascript + +import io.github.alexzhirkevich.skriptie.LangContext +import io.github.alexzhirkevich.skriptie.common.fastMap +import kotlin.math.absoluteValue + +public object JSLangContext : LangContext { + + override fun isFalse(a: Any?): Boolean { + return a == null + || a == false + || a is Unit + || a is Number && a.toDouble().let { it == 0.0 || it.isNaN() } + || a is CharSequence && a.isEmpty() + || (a as? JsWrapper<*>)?.value?.let(::isFalse) == true + } + + override fun sum(a: Any?, b: Any?): Any? { + return jssum( + a?.numberOrThis(false), + b?.numberOrThis(false) + ) + } + + override fun sub(a: Any?, b: Any?): Any? { + return jssub(a?.numberOrThis(), b?.numberOrThis()) + } + + override fun mul(a: Any?, b: Any?): Any? { + return jsmul(a?.numberOrThis(), b?.numberOrThis()) + } + + override fun div(a: Any?, b: Any?): Any? { + return jsdiv(a?.numberOrThis(), b?.numberOrThis()) + } + + override fun mod(a: Any?, b: Any?): Any? { + return jsmod(a?.numberOrThis(), b?.numberOrThis()) + } + + override fun inc(a: Any?): Any? { + return jsinc(a?.numberOrThis()) + } + + override fun dec(a: Any?): Any? { + return jsdec(a?.numberOrThis()) + } + + override fun neg(a: Any?): Any? { + return jsneg(a?.numberOrThis()) + } + + override fun pos(a: Any?): Any? { + return jspos(a?.numberOrThis()) + } + + override fun toNumber(a: Any?, strict: Boolean): Number { + return a.numberOrNull(withNaNs = !strict) ?: Double.NaN + } + + override fun fromKotlin(a: Any?): Any? { + return when (a) { + is JsWrapper<*> -> a + is Number -> JsNumber(a) + is UByte -> JsNumber(a.toLong()) + is UShort -> JsNumber(a.toLong()) + is UInt -> JsNumber(a.toLong()) + is ULong -> JsNumber(a.toLong()) + is Collection<*> -> JsArray(a.map(::fromKotlin).toMutableList()) + is CharSequence -> JsString(a.toString()) + else -> a + } + } + + override fun toKotlin(a: Any?): Any? { + return when (a) { + is JsArray -> a.value.fastMap(::toKotlin) + is JsWrapper<*> -> toKotlin(a.value) + else -> a + } + } +} + +private fun jssum(a : Any?, b : Any?) : Any? { + val a = if (a is List<*>) + a.joinToString(",") + else a + val b = if (b is List<*>) + b.joinToString(",") + else b + return when { + a == null && b == null -> 0L + a == null && b is Number || a is Number && b == null -> a ?: b + b is Unit || a is Unit -> Double.NaN + a is Long && b is Long -> a + b + a is Number && b is Number -> a.toDouble() + b.toDouble() + else -> a.toString() + b.toString() + } +} + +private fun jssub(a : Any?, b : Any?) : Any? { + return when { + a is Unit || b is Unit -> Double.NaN + a is Number && b is Unit || a is Unit && b is Number -> Double.NaN + a is Long? && b is Long? -> (a ?: 0L) - (b ?: 0L) + a is Double? && b is Double? ->(a ?: 0.0) - (b ?: 0.0) + a is Number? && b is Number? ->(a?.toDouble() ?: 0.0) - (b?.toDouble() ?: 0.0) + else -> Double.NaN + } +} + +private fun jsmul(a : Any?, b : Any?) : Any? { + return when { + a == Unit || b == Unit -> Double.NaN + a == null || b == null -> 0L + a is Long && b is Long -> a*b + a is Double && b is Double -> a*b + a is Long && b is Long -> 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 } + } + else -> Double.NaN + } +} + +private fun jsdiv(a : Any?, b : Any?) : Any { + return when { + a is Unit || b is Unit + || (a == null && b == null) + || ((a as? Number)?.toDouble() == 0.0 && b == null) + || ((b as? Number)?.toDouble() == 0.0 && a == null) + || ((a as? CharSequence)?.toString()?.toDoubleOrNull() == 0.0 && b == null) + || ((b as? CharSequence)?.toString()?.toDoubleOrNull() == 0.0 && a == null) -> Double.NaN + a == null -> 0L + b == null || (b as? Number)?.toDouble() == 0.0 -> Double.POSITIVE_INFINITY + a is Long && b is Long -> when { + 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 } + } + + else -> Double.NaN + } +} + +private fun jsmod(a : Any?, b : Any?) : Any { + return when { + b == null || a == Unit || b == Unit -> Double.NaN + (b as? Number)?.toDouble()?.absoluteValue?.let { it < Double.MIN_VALUE } == true -> Double.NaN + a == null -> 0L + a is Long && b is Long -> a % b + a is Number && b is Number -> a.toDouble() % b.toDouble() + else -> Double.NaN + } +} + + +private fun jsinc(v : Any?) : Any { + return when (v) { + null -> 1L + is Long -> v + 1 + is Double -> v + 1 + is Number -> v.toDouble() + 1 + else -> Double.NaN + } +} + +private fun jsdec(v : Any?) : Any { + return when (v) { + null -> -1L + is Long -> v - 1 + is Double -> v - 1 + is Number -> v.toDouble() - 1 + else -> Double.NaN + } +} + +private fun jsneg(v : Any?) : Any { + return when (v) { + null -> -0 + is Long -> -v + is Number -> -v.toDouble() + is List<*> -> { + v as List + v.fastMap { -it.toDouble() } + } + + else -> Double.NaN + } +} + +private fun jspos(v : Any?) : Any { + return when (v) { + null -> 0 + is Number -> v + else -> Double.NaN + } +} + + +private tailrec fun Any?.numberOrNull(withNaNs : Boolean = true) : Number? = when(this) { + null -> 0 + is JsString -> if (withNaNs) value.numberOrNull(withNaNs) else null + is JsArray -> if (withNaNs) value.numberOrNull(withNaNs) else null + is JsWrapper<*> -> value.numberOrNull() + is Byte -> toLong() + is UByte -> toLong() + is Short -> toLong() + is UShort -> toLong() + is Int -> toLong() + is UInt -> toLong() + is ULong -> toLong() + is Float -> toDouble() + is Long -> this + is Double -> this + is String -> if (withNaNs) { + val t = trim() + t.toLongOrNull() ?: t.toDoubleOrNull() + } else null + is List<*> -> { + if (withNaNs) { + singleOrNull()?.numberOrNull(withNaNs) + } else{ + null + } + } + else -> null +} + +private fun Any?.numberOrThis(withMagic : Boolean = true) : Any? = numberOrNull(withMagic) ?: this + diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JSRuntime.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JSRuntime.kt index 437ba867..c55e7d7f 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JSRuntime.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JSRuntime.kt @@ -1,18 +1,31 @@ package io.github.alexzhirkevich.skriptie.javascript import io.github.alexzhirkevich.skriptie.DefaultScriptIO +import io.github.alexzhirkevich.skriptie.LangContext +import io.github.alexzhirkevich.skriptie.ScriptEngine import io.github.alexzhirkevich.skriptie.ScriptIO import io.github.alexzhirkevich.skriptie.VariableType -import io.github.alexzhirkevich.skriptie.common.fastMap +import io.github.alexzhirkevich.skriptie.ecmascript.ESInterpreter import io.github.alexzhirkevich.skriptie.ecmascript.ESNumber import io.github.alexzhirkevich.skriptie.ecmascript.ESObject import io.github.alexzhirkevich.skriptie.ecmascript.ESRuntime import io.github.alexzhirkevich.skriptie.ecmascript.init -import kotlin.math.absoluteValue +import io.github.alexzhirkevich.skriptie.invoke + +/** + * Invoke JavaScript code. + * + * Unlike Kotlin/JS, [script] is not required to be compile-time constant + * */ +public fun js(script : String) : Any? { + return ScriptEngine(JSRuntime(), JSInterpreter).invoke(script) +} + +private val JSInterpreter = ESInterpreter(JSLangContext) public open class JSRuntime( io: ScriptIO = DefaultScriptIO -) : ESRuntime(io = io) { +) : ESRuntime(io = io), LangContext by JSLangContext { init { recreate() @@ -23,80 +36,6 @@ public open class JSRuntime( recreate() } - override fun isFalse(a: Any?): Boolean { - return a == null - || a == false - || a is Unit - || a is Number && a.toDouble().let { it == 0.0 || it.isNaN() } - || a is CharSequence && a.isEmpty() - || (a as? JsWrapper<*>)?.value?.let(::isFalse) == true - } - - override fun sum(a: Any?, b: Any?): Any? { - return jssum( - a?.numberOrThis(false), - b?.numberOrThis(false) - ) - } - - override fun sub(a: Any?, b: Any?): Any? { - return jssub(a?.numberOrThis(), b?.numberOrThis()) - } - - override fun mul(a: Any?, b: Any?): Any? { - return jsmul(a?.numberOrThis(), b?.numberOrThis()) - } - - override fun div(a: Any?, b: Any?): Any? { - return jsdiv(a?.numberOrThis(), b?.numberOrThis()) - } - - override fun mod(a: Any?, b: Any?): Any? { - return jsmod(a?.numberOrThis(), b?.numberOrThis()) - } - - override fun inc(a: Any?): Any? { - return jsinc(a?.numberOrThis()) - } - - override fun dec(a: Any?): Any? { - return jsdec(a?.numberOrThis()) - } - - override fun neg(a: Any?): Any? { - return jsneg(a?.numberOrThis()) - } - - override fun pos(a: Any?): Any? { - return jspos(a?.numberOrThis()) - } - - override fun toNumber(a: Any?, strict: Boolean): Number { - return a.numberOrNull(withNaNs = !strict) ?: Double.NaN - } - - override fun fromKotlin(a: Any?): Any? { - return when (a) { - is JsWrapper<*> -> a - is Number -> JsNumber(a) - is UByte -> JsNumber(a.toLong()) - is UShort -> JsNumber(a.toLong()) - is UInt -> JsNumber(a.toLong()) - is ULong -> JsNumber(a.toLong()) - is List<*> -> JsArray(a.fastMap(::fromKotlin).toMutableList()) - is CharSequence -> JsString(a.toString()) - else -> a - } - } - - override fun toKotlin(a: Any?): Any? { - return when (a) { - is JsArray -> a.value.fastMap(::toKotlin) - is JsWrapper<*> -> toKotlin(a.value) - else -> a - } - } - private fun recreate() { set("Math", JsMath(), VariableType.Const) set("console", JsConsole(), VariableType.Const) @@ -105,174 +44,14 @@ public open class JSRuntime( val number = get("Number") as ESNumber val globalThis = get("globalThis") as ESObject - globalThis["parseInt"] = number.parseInt - globalThis["parseFloat"] = number.parseFloat - globalThis["isFinite"] = number.isFinite - globalThis["isNan"] = number.isNan - globalThis["isInteger"] = number.isInteger - globalThis["isSafeInteger"] = number.isSafeInteger + globalThis.set("parseInt", number.parseInt) + globalThis.set("parseFloat", number.parseFloat) + globalThis.set("isFinite", number.isFinite) + globalThis.set("isNan", number.isNan) + globalThis.set("isInteger", number.isInteger) + globalThis.set("isSafeInteger", number.isSafeInteger) } } } -private fun jssum(a : Any?, b : Any?) : Any? { - val a = if (a is List<*>) - a.joinToString(",") - else a - val b = if (b is List<*>) - b.joinToString(",") - else b - return when { - a == null && b == null -> 0L - a == null && b is Number || a is Number && b == null -> a ?: b - b is Unit || a is Unit -> Double.NaN - a is Long && b is Long -> a + b - a is Number && b is Number -> a.toDouble() + b.toDouble() - else -> a.toString() + b.toString() - } -} - -private fun jssub(a : Any?, b : Any?) : Any? { - return when { - a is Unit || b is Unit -> Double.NaN - a is Number && b is Unit || a is Unit && b is Number -> Double.NaN - a is Long? && b is Long? -> (a ?: 0L) - (b ?: 0L) - a is Double? && b is Double? ->(a ?: 0.0) - (b ?: 0.0) - a is Number? && b is Number? ->(a?.toDouble() ?: 0.0) - (b?.toDouble() ?: 0.0) - else -> Double.NaN - } -} - -private fun jsmul(a : Any?, b : Any?) : Any? { - return when { - a == Unit || b == Unit -> Double.NaN - a == null || b == null -> 0L - a is Long && b is Long -> a*b - a is Double && b is Double -> a*b - a is Long && b is Long -> 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 } - } - else -> Double.NaN - } -} - -private fun jsdiv(a : Any?, b : Any?) : Any { - return when { - a is Unit || b is Unit - || (a == null && b == null) - || ((a as? Number)?.toDouble() == 0.0 && b == null) - || ((b as? Number)?.toDouble() == 0.0 && a == null) - || ((a as? CharSequence)?.toString()?.toDoubleOrNull() == 0.0 && b == null) - || ((b as? CharSequence)?.toString()?.toDoubleOrNull() == 0.0 && a == null) -> Double.NaN - a == null -> 0L - b == null || (b as? Number)?.toDouble() == 0.0 -> Double.POSITIVE_INFINITY - a is Long && b is Long -> when { - 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 } - } - - else -> Double.NaN - } -} - -private fun jsmod(a : Any?, b : Any?) : Any { - return when { - b == null || a == Unit || b == Unit -> Double.NaN - (b as? Number)?.toDouble()?.absoluteValue?.let { it < Double.MIN_VALUE } == true -> Double.NaN - a == null -> 0L - a is Long && b is Long -> a % b - a is Number && b is Number -> a.toDouble() % b.toDouble() - else -> Double.NaN - } -} - - -private fun jsinc(v : Any?) : Any { - return when (v) { - null -> 1L - is Long -> v + 1 - is Double -> v + 1 - is Number -> v.toDouble() + 1 - else -> Double.NaN - } -} - -private fun jsdec(v : Any?) : Any { - return when (v) { - null -> -1L - is Long -> v - 1 - is Double -> v - 1 - is Number -> v.toDouble() - 1 - else -> Double.NaN - } -} - -private fun jsneg(v : Any?) : Any { - return when (v) { - null -> -0 - is Long -> -v - is Number -> -v.toDouble() - is List<*> -> { - v as List - v.fastMap { -it.toDouble() } - } - - else -> Double.NaN - } -} - -private fun jspos(v : Any?) : Any { - return when (v) { - null -> 0 - is Number -> v - else -> Double.NaN - } -} - - -private tailrec fun Any?.numberOrNull(withNaNs : Boolean = true) : Number? = when(this) { - null -> 0 - is JsString -> if (withNaNs) value.numberOrNull(withNaNs) else null - is JsArray -> if (withNaNs) value.numberOrNull(withNaNs) else null - is JsWrapper<*> -> value.numberOrNull() - is Byte -> toLong() - is UByte -> toLong() - is Short -> toLong() - is UShort -> toLong() - is Int -> toLong() - is UInt -> toLong() - is ULong -> toLong() - is Float -> toDouble() - is Long -> this - is Double -> this - is String -> if (withNaNs) { - val t = trim() - t.toLongOrNull() ?: t.toDoubleOrNull() - } else null - is List<*> -> { - if (withNaNs) { - singleOrNull()?.numberOrNull(withNaNs) - } else{ - null - } - } - else -> null -} -private fun Any?.numberOrThis(withMagic : Boolean = true) : Any? = numberOrNull(withMagic) ?: this diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JsArray.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JsArray.kt index e47c6986..8a62a5ba 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JsArray.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JsArray.kt @@ -9,6 +9,7 @@ import io.github.alexzhirkevich.skriptie.common.TypeError import io.github.alexzhirkevich.skriptie.common.checkNotEmpty import io.github.alexzhirkevich.skriptie.common.fastFilter import io.github.alexzhirkevich.skriptie.common.fastMap +import io.github.alexzhirkevich.skriptie.common.valueAtIndexOrUnit import io.github.alexzhirkevich.skriptie.ecmascript.ESAny import io.github.alexzhirkevich.skriptie.ecmascript.checkArgs import io.github.alexzhirkevich.skriptie.invoke @@ -17,15 +18,19 @@ import kotlin.jvm.JvmInline @JvmInline internal value class JsArray( override val value : MutableList -) : ESAny, JsWrapper> { +) : ESAny, JsWrapper>, MutableList by value { - override fun get(variable: String): Any { + override fun get(variable: Any?): Any { return when (variable){ "length" -> value.size else -> Unit } } + override fun toString(): String { + return value.joinToString(separator = ",") + } + override fun invoke( function: String, context: ScriptRuntime, @@ -118,6 +123,15 @@ internal value class JsArray( value.slice(start.. { + val idx = arguments[0].invoke(context).let(context::toNumber).toInt() + value.valueAtIndexOrUnit(idx) + } + "includes" -> { + val v = arguments[0].invoke(context) + val fromIndex = arguments.getOrNull(1)?.let(context::toNumber)?.toInt() ?: 0 + value.indexOf(v) > fromIndex + } else -> super.invoke(function, context, arguments) } } diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JsNumber.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JsNumber.kt index 0eecf5e7..6c458f0a 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JsNumber.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JsNumber.kt @@ -19,8 +19,8 @@ public value class JsNumber( override val type: String get() = "number" - override fun get(variable: String): Any? { - unresolvedReference(variable) + override fun get(variable: Any?): Any? { + unresolvedReference(variable.toString()) } override fun toString(): String { diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JsString.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JsString.kt index 0aba6e71..a4722444 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JsString.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JsString.kt @@ -15,7 +15,7 @@ import kotlin.jvm.JvmInline @JvmInline internal value class JsString( override val value : String -) : ESAny, JsWrapper, Comparable { +) : ESAny, JsWrapper, Comparable, CharSequence by value { override val type: String get() = "string" @@ -25,7 +25,7 @@ internal value class JsString( return value } - override fun get(variable: String): Any { + override fun get(variable: Any?): Any { return when(variable){ "length" -> value.length else -> Unit diff --git a/skriptie/src/commonTest/kotlin/js/FunctionsTest.kt b/skriptie/src/commonTest/kotlin/js/FunctionsTest.kt index fb29dcc0..f4c0787b 100644 --- a/skriptie/src/commonTest/kotlin/js/FunctionsTest.kt +++ b/skriptie/src/commonTest/kotlin/js/FunctionsTest.kt @@ -138,4 +138,15 @@ class FunctionsTest { x """.trimIndent().eval().assertEqualsTo(1L) } + + @Test + fun recursion(){ + """ + function fib(n) { + return n < 2 ? n : fib(n - 1) + fib(n - 2); + } + + fib(7) + """.trimIndent().eval().assertEqualsTo(13L) + } } \ No newline at end of file diff --git a/skriptie/src/commonTest/kotlin/js/JsArrayTest.kt b/skriptie/src/commonTest/kotlin/js/JsArrayTest.kt index ac7e2073..b10507ad 100644 --- a/skriptie/src/commonTest/kotlin/js/JsArrayTest.kt +++ b/skriptie/src/commonTest/kotlin/js/JsArrayTest.kt @@ -1,6 +1,7 @@ package js import kotlin.test.Test +import kotlin.test.assertFalse import kotlin.test.assertTrue class JsArrayTest { @@ -53,7 +54,14 @@ class JsArrayTest { assertTrue { """ let arr = [1,2,3,4] - arr.some(v => v ===2) + arr.some(v => v === 2) + """.trimIndent().eval() as Boolean + } + + assertFalse { + """ + let arr = [1,2,3,4] + arr.some(v => v === 5) """.trimIndent().eval() as Boolean } } @@ -66,4 +74,13 @@ class JsArrayTest { arr """.trimIndent().eval().assertEqualsTo(listOf(2L,8L, 66L)) } + + @Test + fun at(){ + "[66,2,8].at(0)".eval().assertEqualsTo(66L) + "[66,2,8].at(3)".eval().assertEqualsTo(Unit) + + "[66,2,8][1]".eval().assertEqualsTo(2L) + "[66,2,8][3]".eval().assertEqualsTo(Unit) + } } \ No newline at end of file diff --git a/skriptie/src/commonTest/kotlin/js/JsTestUtil.kt b/skriptie/src/commonTest/kotlin/js/JsTestUtil.kt index 48e7594c..6fa269cd 100644 --- a/skriptie/src/commonTest/kotlin/js/JsTestUtil.kt +++ b/skriptie/src/commonTest/kotlin/js/JsTestUtil.kt @@ -3,9 +3,9 @@ package js import io.github.alexzhirkevich.skriptie.DefaultScriptIO import io.github.alexzhirkevich.skriptie.ScriptEngine import io.github.alexzhirkevich.skriptie.ScriptIO -import io.github.alexzhirkevich.skriptie.ecmascript.EcmascriptInterpreter +import io.github.alexzhirkevich.skriptie.ecmascript.ESInterpreter import io.github.alexzhirkevich.skriptie.invoke -import io.github.alexzhirkevich.skriptie.javascript.JSGlobalContext +import io.github.alexzhirkevich.skriptie.javascript.JSLangContext import io.github.alexzhirkevich.skriptie.javascript.JSRuntime import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -19,11 +19,5 @@ internal fun Any?.assertEqualsTo(other : Double, tolerance: Double = 0.0001) { internal fun String.eval(io : ScriptIO = DefaultScriptIO) : Any? { val runtime = JSRuntime(io) - return ScriptEngine( - runtime, - EcmascriptInterpreter( - JSGlobalContext(false), - runtime - ) - ).invoke(this) + return ScriptEngine(runtime, ESInterpreter(JSLangContext)).invoke(this) } diff --git a/skriptie/src/commonTest/kotlin/js/ObjectTest.kt b/skriptie/src/commonTest/kotlin/js/ObjectTest.kt new file mode 100644 index 00000000..de6be953 --- /dev/null +++ b/skriptie/src/commonTest/kotlin/js/ObjectTest.kt @@ -0,0 +1,70 @@ +package js + +import io.github.alexzhirkevich.skriptie.ecmascript.ESObject +import kotlin.test.Test + +class ObjectTest { + + @Test + fun context(){ + + val obj = "{ name : 'test'}".eval() as ESObject + + obj["name"].toString().assertEqualsTo("test") + + + "typeof {}".eval().assertEqualsTo("object") + "let x = {}; typeof x".eval().assertEqualsTo("object") + "let x = ({}); typeof x".eval().assertEqualsTo("object") + "let x = Object({}); typeof x".eval().assertEqualsTo("object") + "let x = 1; if ({}) { x = 2 }; x".eval().assertEqualsTo(2L) + """ + function test(x) { + return x + } + typeof test({}) + """.trimIndent().eval().assertEqualsTo("object") + } + + @Test + fun syntax(){ + """ + let obj = { + string : "string", + number : 123, + f : function() { }, + af : () => {} + } + + typeof(obj.string) + ' ' + typeof(obj.number) + ' ' + typeof(obj.f) + ' ' + typeof(obj.af) + ' ' + typeof obj.nothing + """.trimIndent().eval().assertEqualsTo("string number function function undefined") + } + + @Test + fun getters(){ + "let obj = { name : 'string' }; obj['name']".eval().assertEqualsTo("string") + "let obj = { name : 'string' }; obj.name".eval().assertEqualsTo("string") + } + + @Test + fun setters(){ + "let obj = {}; obj['name'] = 213; obj.name".eval().assertEqualsTo(213L) + "let obj = {}; obj.name = 213; obj.name".eval().assertEqualsTo(213L) + } + + @Test + fun global_object(){ + "typeof Object".eval().assertEqualsTo("function") + + "Object.keys({ name : 'test' })".eval().assertEqualsTo(listOf("name")) + "Object.keys({ name : 'test', x : 1 })".eval().assertEqualsTo(listOf("name","x")) + ("Object.keys(1)".eval() as List<*>).size.assertEqualsTo(0) + + "Object.entries({ name : 'test' })".eval() + .assertEqualsTo(listOf(listOf("name", "test"))) + "Object.entries({ name : 'test', x : 1 })".eval() + .assertEqualsTo(listOf(listOf("name", "test"), listOf("x", 1L))) + ("Object.entries(1)".eval() as List<*>).size.assertEqualsTo(0) + + } +} \ No newline at end of file diff --git a/skriptie/src/commonTest/kotlin/js/SyntaxTest.kt b/skriptie/src/commonTest/kotlin/js/SyntaxTest.kt index f2c59be9..970fc643 100644 --- a/skriptie/src/commonTest/kotlin/js/SyntaxTest.kt +++ b/skriptie/src/commonTest/kotlin/js/SyntaxTest.kt @@ -141,9 +141,73 @@ class SyntaxTest { } @Test - fun typeOf(){ + fun typeOf() { "typeof(1)".eval().assertEqualsTo("number") "typeof(null)".eval().assertEqualsTo("object") "typeof(undefined)".eval().assertEqualsTo("undefined") + "typeof('str')".eval().assertEqualsTo("string") + "typeof('str') + 123".eval().assertEqualsTo("string123") + + "typeof 1".eval().assertEqualsTo("number") + "typeof null".eval().assertEqualsTo("object") + "typeof undefined".eval().assertEqualsTo("undefined") + + "typeof 1===1".eval().assertEqualsTo("boolean") + + "let x = 1; typeof x++".eval().assertEqualsTo("number") + assertFailsWith { + "let x = 1; typeof x = 2".eval() + } + assertFailsWith { + "let x = 1; typeof x += 2".eval() + } + } + + @Test + fun tryCatch(){ + """ + let error = undefined + try { + let x = null + x.test = 1 + } catch(x) { + error = x.message + } + error + """.trimIndent().eval().assertEqualsTo("Cannot set properties of null (setting 'test')") + + """ + let error = undefined + try { + throw 'test' + } catch(x) { + error = x + } + error + """.trimIndent().eval().assertEqualsTo("test") + + """ + let a = 1 + try { + throw 'test' + } catch(x) { + a++ + } finally { + a++ + } + a + """.trimIndent().eval().assertEqualsTo(3L) + + """ + let a = 1 + try { + + } catch(x) { + a++ + } finally { + a++ + } + a + """.trimIndent().eval().assertEqualsTo(2L) } } \ No newline at end of file