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 87d8d19..a93f1f3 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ScriptRuntime.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ScriptRuntime.kt @@ -1,7 +1,7 @@ package io.github.alexzhirkevich.skriptie -import io.github.alexzhirkevich.skriptie.common.SyntaxError -import io.github.alexzhirkevich.skriptie.common.TypeError +import io.github.alexzhirkevich.skriptie.ecmascript.SyntaxError +import io.github.alexzhirkevich.skriptie.ecmascript.TypeError public enum class VariableType { diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/Function.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/Function.kt index c64b53e..b1c453f 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/Function.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/Function.kt @@ -6,6 +6,10 @@ import io.github.alexzhirkevich.skriptie.VariableType import io.github.alexzhirkevich.skriptie.argForNameOrIndex import io.github.alexzhirkevich.skriptie.ecmascript.ESAny import io.github.alexzhirkevich.skriptie.ecmascript.ESObject +import io.github.alexzhirkevich.skriptie.ecmascript.ESObjectBase +import io.github.alexzhirkevich.skriptie.ecmascript.SyntaxError +import io.github.alexzhirkevich.skriptie.ecmascript.TypeError +import io.github.alexzhirkevich.skriptie.ecmascript.unresolvedReference import io.github.alexzhirkevich.skriptie.invoke public class FunctionParam( @@ -23,11 +27,33 @@ internal interface Callable { operator fun invoke(args: List, context: ScriptRuntime) : Any? } + internal class Function( - val name : String, + override val name : String, val parameters : List, - private val body : Expression -) : Callable { + val body : Expression, + var thisRef : Any? = null, + val isClassMember : Boolean = false, + var isStatic : Boolean = false, + val extraVariables: Map> = emptyMap(), +) : ESObject by ESObjectBase(name), Callable, Named { + override val type: String + get() = "function" + + fun copy( + body: Expression = this.body, + extraVariables: Map> = emptyMap(), + ) : Function{ + return Function( + name = name, + parameters = parameters, + body = body, + thisRef = thisRef, + isClassMember = isClassMember, + extraVariables = this.extraVariables + extraVariables + ) + } + init { val varargs = parameters.count { it.isVararg } @@ -40,53 +66,56 @@ internal class Function( args: List, context: ScriptRuntime, ): Any? { - try { + return try { val arguments = buildMap { parameters.fastForEachIndexed { i, p -> - val value = if (p.isVararg){ + val value = if (p.isVararg) { args.drop(i).fastMap { it(context) } } else { - requireNotNull(args.argForNameOrIndex(i, p.name) ?: p.default) { - "'${p.name}' argument of '$name' function is missing" - }.invoke(context) + (args.argForNameOrIndex(i, p.name) ?: p.default) + ?.invoke(context) + ?: Unit } - this[p.name] = Pair( - VariableType.Local, - value - ) + this[p.name] = VariableType.Local to value + } + thisRef?.let { + this["this"] = VariableType.Const to it } } - return context.withScope(arguments, body::invoke) + context.withScope(arguments + extraVariables, body::invoke) } catch (ret: BlockReturn) { - return ret.value + ret.value } } } -internal fun OpFunctionExec( - name : String, - receiver : Expression?, - parameters : List, -) = Expression { ctx -> +internal class OpFunctionExec( + override val name : String, + val receiver : Expression?, + val parameters : List, +) : Expression, Named { + override fun invokeRaw(context: ScriptRuntime): Any? { + val res = receiver?.invoke(context) + val function = when { + res == null -> context[name] + res is Callable && name.isBlank() -> res + res is ESObject -> res[name] + res is ESAny -> { + return res.invoke(name, context, parameters) + } - val res = receiver?.invoke(ctx) - val function = when { - res == null -> ctx[name] - res is Callable && name.isBlank() -> res - res is ESObject -> res[name] - res is ESAny -> { - return@Expression res.invoke(name, ctx, parameters) + else -> null } - else -> null - } - if (function is Unit){ - unresolvedReference(name) - } - if (function !is Callable){ - throw TypeError("$name ($function) is not a function") + if (function is Unit) { + unresolvedReference(name) + } + if (function !is Callable) { + throw TypeError("$name ($function) is not a function") + } + return function.invoke( + args = parameters, + context = context, + ) } - function.invoke( - args = parameters, - context = ctx, - ) } + diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/Named.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/Named.kt new file mode 100644 index 0000000..168aa12 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/Named.kt @@ -0,0 +1,6 @@ +package io.github.alexzhirkevich.skriptie.common + +internal interface Named { + + val name : String +} \ No newline at end of file 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 095fce7..2d24ac3 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 @@ -5,12 +5,14 @@ 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.ecmascript.TypeError import io.github.alexzhirkevich.skriptie.invoke internal class OpAssign( val type : VariableType? = null, val variableName : String, val receiver : Expression?=null, + var isStatic : Boolean = false, val assignableValue : Expression, private val merge : ((Any?, Any?) -> Any?)? ) : Expression { diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpCompare.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpCompare.kt index 4747a5b..90f8b47 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpCompare.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpCompare.kt @@ -1,22 +1,24 @@ package io.github.alexzhirkevich.skriptie.common import io.github.alexzhirkevich.skriptie.Expression +import io.github.alexzhirkevich.skriptie.ScriptRuntime import io.github.alexzhirkevich.skriptie.invoke internal fun OpCompare( a : Expression, b : Expression, - comparator : (Comparable<*>, Comparable<*>) -> Any + comparator : (Comparable<*>, Comparable<*>, ScriptRuntime) -> Any ) = Expression { comparator( a(it) as Comparable<*>, - b(it) as Comparable<*> + b(it) as Comparable<*>, + it ) } -internal val OpGreaterComparator : (Comparable<*>, Comparable<*>) -> Boolean = { a, b -> +internal val OpGreaterComparator : (Comparable<*>, Comparable<*>, ScriptRuntime) -> Boolean = { a, b, _ -> if (a is Number && b is Number) { a.toDouble() > b.toDouble() @@ -25,7 +27,7 @@ internal val OpGreaterComparator : (Comparable<*>, Comparable<*>) -> Boolean = { } } -internal val OpLessComparator : (Comparable<*>, Comparable<*>) -> Boolean = { a, b -> +internal val OpLessComparator : (Comparable<*>, Comparable<*>, ScriptRuntime) -> Boolean = { a, b, _ -> if (a is Number && b is Number) { a.toDouble() < b.toDouble() } else { @@ -33,10 +35,11 @@ internal val OpLessComparator : (Comparable<*>, Comparable<*>) -> Boolean = { a, } } -internal val OpTypedEqualsComparator : (Comparable<*>, Comparable<*>) -> Boolean = { a, b -> - OpEqualsImpl(a, b, true) +internal val OpTypedEqualsComparator : (Comparable<*>, Comparable<*>, ScriptRuntime) -> Boolean = { a, b, r -> + OpEqualsImpl(a, b, true, r) } -internal val OpEqualsComparator : (Comparable<*>, Comparable<*>) -> Boolean = { a, b -> - OpEqualsImpl(a, b, false) -} +internal val OpEqualsComparator : (Comparable<*>, Comparable<*>,ScriptRuntime) -> Boolean = + { a, b, r -> + OpEqualsImpl(a, b, false, r) + } diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpEquals.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpEquals.kt index 2e4b7db..5b83532 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpEquals.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/OpEquals.kt @@ -1,6 +1,7 @@ package io.github.alexzhirkevich.skriptie.common import io.github.alexzhirkevich.skriptie.Expression +import io.github.alexzhirkevich.skriptie.ScriptRuntime import io.github.alexzhirkevich.skriptie.invoke import io.github.alexzhirkevich.skriptie.javascript.JsWrapper @@ -9,22 +10,27 @@ internal fun OpEquals( b : Expression, isTyped : Boolean ) = Expression { - OpEqualsImpl(a(it), b(it), isTyped) + OpEqualsImpl(a(it), b(it), isTyped, it) } -internal tailrec fun OpEqualsImpl(a : Any?, b : Any?, typed : Boolean) : Boolean { +internal fun OpEqualsImpl(a : Any?, b : Any?, typed : Boolean, runtime: ScriptRuntime) : Boolean { + + if (!typed) { + if (a is JsWrapper<*>) { + return OpEqualsImpl(a.value, b, typed, runtime) + } + + if (b is JsWrapper<*>) { + return OpEqualsImpl(a, b.value, typed, runtime) + } + } + return when { a == null || b == null -> a == b - a is Number && b is Number -> a.toDouble() == b.toDouble() - typed || a::class == b::class -> { - if (a is JsWrapper<*> && b is JsWrapper<*>) { - OpEqualsImpl(a.value, b.value, typed) - } else { - a == b - } - } - a is String && b is Number -> a.toDoubleOrNull() == b.toDouble() - b is String && a is Number -> b.toDoubleOrNull() == a.toDouble() + typed -> a::class == b::class && OpEqualsImpl(a, b, false, runtime) + a::class == b::class -> a == b + b is Number -> runtime.toNumber(a).toDouble() == b.toDouble() + a is Number -> runtime.toNumber(b).toDouble() == a.toDouble() else -> a.toString() == b.toString() } } \ No newline at end of file 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 32fce3d..ffc031b 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 @@ -4,6 +4,7 @@ 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.unresolvedReference import io.github.alexzhirkevich.skriptie.invoke import kotlin.jvm.JvmInline 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 4ad7ac6..4268d3f 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 @@ -2,6 +2,7 @@ package io.github.alexzhirkevich.skriptie.common import io.github.alexzhirkevich.skriptie.Expression import io.github.alexzhirkevich.skriptie.VariableType +import io.github.alexzhirkevich.skriptie.ecmascript.SyntaxError import io.github.alexzhirkevich.skriptie.invoke internal class ThrowableValue(val value : Any?) : Throwable(message = value?.toString()) { diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESClass.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESClass.kt new file mode 100644 index 0000000..0b97f39 --- /dev/null +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESClass.kt @@ -0,0 +1,156 @@ +package io.github.alexzhirkevich.skriptie.ecmascript + +import io.github.alexzhirkevich.skriptie.Expression +import io.github.alexzhirkevich.skriptie.ScriptRuntime +import io.github.alexzhirkevich.skriptie.VariableType +import io.github.alexzhirkevich.skriptie.common.Callable +import io.github.alexzhirkevich.skriptie.common.Function +import io.github.alexzhirkevich.skriptie.common.Named +import io.github.alexzhirkevich.skriptie.common.OpConstant +import io.github.alexzhirkevich.skriptie.common.fastForEach +import io.github.alexzhirkevich.skriptie.invoke + +internal interface ESClass : ESObject, Callable, Named { + + val functions : List + + val construct: Function? + + val extends : Expression? + + val constructorClass : Expression? + + val isInitialized : Boolean get() = true + + val static : List get() = emptyList() + + override val type: String + get() = "object" + + fun newInstance(args: List, context: ScriptRuntime) : Any? = + invoke(args, context) +} + +internal fun ESClass.superFunctions(runtime: ScriptRuntime) : List { + val extendsClass = extends?.invoke(runtime) as? ESClass + ?: return emptyList() + + return (extendsClass.superFunctions(runtime) + extendsClass.functions).associateBy { it.name }.values.toList() +} + +internal tailrec fun ESClass.instanceOf( + any: Any?, + runtime: ScriptRuntime, + extends: Expression? = this.extends +) : Boolean { + + if (constructorClass?.invoke(runtime) == any || any is ESObjectAccessor) { + return true + } + + val e = extends?.invoke(runtime)?.let { it as? ESClass? } ?: return false + + if (e == any) { + return true + } + + return instanceOf(any, runtime, e.extends) +} + +internal sealed interface StaticClassMember { + + fun assignTo(clazz : ESClass, runtime: ScriptRuntime) + + class Variable(val name : String, val init : Expression) : StaticClassMember { + override fun assignTo(clazz: ESClass, runtime: ScriptRuntime) { + clazz[name] = init(runtime) + } + } + + class Method(val function: Function) : StaticClassMember { + override fun assignTo(clazz: ESClass, runtime: ScriptRuntime) { + clazz[function.name] = function + } + } +} + +internal open class ESClassBase( + override val name : String, + final override val functions : List, + final override val construct: Function?, + final override val extends: Expression?, + final override val static: List +) : ESObjectBase(name), ESClass { + + private var isSuperInitialized = false + + override val isInitialized: Boolean get() = + extends == null || isSuperInitialized + + final override var constructorClass: Expression = OpConstant(this) + + override fun invoke(args: List, context: ScriptRuntime): Any? { + + val superConstructor = (extends?.invoke(context) as? ESClass)?.construct?.let { + it.copy( + body = { r -> + if (isSuperInitialized) { + throw ReferenceError("Super constructor may only be called once") + } else { + it.body(r).also { isSuperInitialized = true } + } + } + ) + } + if (superConstructor == null && extends != null) { + isSuperInitialized = true + } + + val clazz = ESClassBase( + name = name, + functions = (superFunctions(context) + functions).map(Function::copy), + construct = construct?.copy( + extraVariables = if (superConstructor != null){ + mapOf("super" to (VariableType.Const to superConstructor)) + } else emptyMap() + ), + extends = extends, + static = static, + ) + superConstructor?.thisRef = clazz + + clazz.constructorClass = constructorClass + clazz.functions.fastForEach { + clazz[it.name] = it.apply { thisRef=clazz } + } + clazz.construct?.thisRef = clazz + + clazz.construct?.invoke(args, context) + + return clazz + } + override val type: String + get() = "object" + + override fun toString(): String { + val properties = keys.joinToString( + prefix = " ", + postfix = " ", + separator = ", " + ) { + "$it: ${get(it)}" + } + return "$name {$properties}" + } +} + +internal fun ESClassInstantiation( + name: String, + args : List +) = Expression { + val clazz = it[name] + syntaxCheck(clazz is ESClass) { + "$name is not a constructor" + } + clazz.newInstance(args, it) +} \ No newline at end of file diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESFunction.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESFunction.kt index 1a62051..f79fdde 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESFunction.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESFunction.kt @@ -1,15 +1,24 @@ package io.github.alexzhirkevich.skriptie.ecmascript -import io.github.alexzhirkevich.skriptie.common.Callable +import io.github.alexzhirkevich.skriptie.Expression +import io.github.alexzhirkevich.skriptie.common.Function -internal interface ESFunction : ESObject, Callable { +internal interface ESFunction : ESClass { override val type: String get() = "function" } internal abstract class ESFunctionBase( - name : String + override val name : String ) : ESObjectBase(name), ESFunction { + + override val constructorClass: Expression? get() = null + + override val extends: Expression? get() = null + + override val construct: Function? + get() = null + override val type: String get() = "function" 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 bf0f888..fc26d46 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 @@ -9,6 +9,7 @@ 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 +import io.github.alexzhirkevich.skriptie.common.Named import io.github.alexzhirkevich.skriptie.common.OpAssign import io.github.alexzhirkevich.skriptie.common.OpAssignByIndex import io.github.alexzhirkevich.skriptie.common.OpBlock @@ -33,21 +34,19 @@ import io.github.alexzhirkevich.skriptie.common.OpNot 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 import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract -internal val EXPR_DEBUG_PRINT_ENABLED = false +internal val EXPR_DEBUG_PRINT_ENABLED = true internal enum class LogicalContext { And, Or, Compare } internal enum class BlockContext { - None, Loop, Function + None, Loop, Function, Class } internal class ESInterpreterImpl( @@ -60,8 +59,18 @@ internal class ESInterpreterImpl( private var ch: Char = ' ' fun interpret(): Script { - val block = parseBlock(scoped = false, context = emptyList()) - return Script { langContext.toKotlin(block(it)) } + val block = parseBlock(scoped = false, blockContext = emptyList()) + return Script { + try { + langContext.toKotlin(block(it)) + } catch (t: Throwable) { + if (t is ESAny) { + throw t + } else { + throw ESError(t.message, t) + } + } + } } private fun prepareNextChar() { @@ -154,20 +163,26 @@ internal class ESInterpreterImpl( context: Expression, blockContext: List, unaryOnly: Boolean = false, - isExpressionStart: Boolean = false + isExpressionStart: Boolean = false, + variableName : String? = null ): Expression { - var x = parseExpressionOp( - context, - blockContext = blockContext, - isExpressionStart = isExpressionStart - ) + var x = if (variableName == null) { + parseExpressionOp( + context, + blockContext = blockContext, + isExpressionStart = isExpressionStart + ) + } else { + OpGetVariable(variableName, receiver = null) + } if (EXPR_DEBUG_PRINT_ENABLED) { println("Parsing assignment for $x") } val checkAssignment = { - if (unaryOnly) - throw SyntaxError("Invalid left-hand side in assignment") + syntaxCheck(!unaryOnly){ + "Invalid left-hand side in assignment" + } } while (true) { @@ -231,7 +246,8 @@ internal class ESInterpreterImpl( println("making ternary operator: onTrue...") } - val onTrue = parseAssignment(globalContext, blockContext) + val bContext = blockContext.dropLastWhile { it == BlockContext.Class } + val onTrue = parseAssignment(globalContext, bContext) if (!eat(':')) { throw SyntaxError("Unexpected end of input") @@ -239,7 +255,7 @@ internal class ESInterpreterImpl( if (EXPR_DEBUG_PRINT_ENABLED) { println("making ternary operator: onFalse...") } - val onFalse = parseAssignment(globalContext, blockContext) + val onFalse = parseAssignment(globalContext, bContext) OpIfCondition( condition = x, @@ -310,8 +326,8 @@ internal class ESInterpreterImpl( eatSequence("<=") -> OpCompare( x, parseExpressionOp(globalContext, LogicalContext.Compare, blockContext) - ) { a, b -> - OpLessComparator(a, b) || OpEqualsComparator(a, b) + ) { a, b,r -> + OpLessComparator(a, b,r) || OpEqualsComparator(a, b,r) } eatSequence("<") -> OpCompare( @@ -323,8 +339,8 @@ internal class ESInterpreterImpl( eatSequence(">=") -> OpCompare( x, parseExpressionOp(globalContext, LogicalContext.Compare, blockContext) - ) { a, b -> - OpGreaterComparator(a, b) || OpEqualsComparator(a, b) + ) { a, b,r -> + OpGreaterComparator(a, b, r) || OpEqualsComparator(a, b,r) } eatSequence(">") -> OpCompare( @@ -409,12 +425,13 @@ internal class ESInterpreterImpl( private fun parseFactorOp( context: Expression, blockContext: List, - isExpressionStart: Boolean = false + isExpressionStart: Boolean = false, + allowContinueWithContext : Boolean = true ): Expression { val parsedOp = when { isExpressionStart && nextCharIs('{'::equals) -> - parseBlock(context = emptyList()) + parseBlock(blockContext = emptyList()) !isExpressionStart && eat('{') -> { if (EXPR_DEBUG_PRINT_ENABLED) { @@ -621,11 +638,12 @@ internal class ESInterpreterImpl( else -> throw SyntaxError("Unexpected token $ch at pos $pos") } - return parsedOp.finish(blockContext) + return parsedOp.finish(blockContext, allowContinueWithContext) } - private fun Expression.finish(blockContext: List): Expression { + private fun Expression.finish(blockContext: List, allowContinueWithContext: Boolean): Expression { return when { + !allowContinueWithContext -> this // inplace function invocation this is InterpretationContext && nextCharIs { it == '(' } -> { parseFunction( @@ -639,6 +657,22 @@ internal class ESInterpreterImpl( || nextCharIs('['::equals) -> parseFactorOp(this, blockContext) // continue with receiver + eatSequence("instanceof") -> { + val obj = parseFactorOp(globalContext, emptyList()) + + Expression { + val o = obj(it) + if (o !is ESObject) { + throw TypeError("Right-hand side of 'instanceof' is not an object ($obj)") + } + val thisObj = this(it) + if (thisObj is ESClass){ + thisObj.instanceOf(o, it) + } else { + false + } + } + } else -> this } } @@ -675,6 +709,22 @@ internal class ESInterpreterImpl( blockContext: List ): Expression { + if (blockContext.lastOrNull() == BlockContext.Class){ + if (func == "static") { + if (EXPR_DEBUG_PRINT_ENABLED){ + println("parsing static class member") + } + val v = parseAssignment(globalContext, blockContext = blockContext) + if (v is OpAssign){ + v.isStatic = true + } + if (v is OpConstant && v.value is Function){ + v.value.isStatic = true + } + return v + } + } + return when (func) { "var", "let", "const" -> parseVariable(func) "typeof" -> parseTypeof() @@ -684,6 +734,19 @@ internal class ESInterpreterImpl( "function" -> { OpConstant(parseFunctionDefinition(blockContext = blockContext)) } + "new" -> { + if (EXPR_DEBUG_PRINT_ENABLED){ + println("parsing 'new' class instantiation") + } + val decl = parseFactorOp(globalContext, emptyList(), allowContinueWithContext = false) + syntaxCheck(decl is OpFunctionExec) { + "$decl is not a constructor" + } + ESClassInstantiation(decl.name, decl.parameters) + } + "class" -> { + OpConstant(parseClassDefinition()) + } "for" -> parseForLoop(blockContext) @@ -694,7 +757,7 @@ internal class ESInterpreterImpl( OpWhileLoop( condition = parseWhileCondition(), - body = parseBlock(context = blockContext + BlockContext.Loop), + body = parseBlock(blockContext = blockContext + BlockContext.Loop), isFalse = langContext::isFalse ) } @@ -758,51 +821,73 @@ internal class ESInterpreterImpl( } return when (context) { - is InterpretationContext -> context.interpret(func, args) - ?: run { - if (args != null && func != null) { + is InterpretationContext -> { + val f = context.interpret(func, args) + when { + f != null -> f + func != null && blockContext.lastOrNull() == BlockContext.Class -> { + if (args != null) { + if (EXPR_DEBUG_PRINT_ENABLED) { + println("Parsing class method...") + } + OpConstant( + parseFunctionDefinition( + name = func, + args = args, + blockContext = emptyList() + ) + ) + } else { + parseAssignment( + context = globalContext, + blockContext = blockContext.dropLastWhile { it == BlockContext.Class }, + variableName = func + ) + } + } + args != null && func != null -> { if (EXPR_DEBUG_PRINT_ENABLED) { println("parsed call for defined function $func") } OpFunctionExec(func, null, args) - } else null - } - ?: run { - if (args == null && func != null) { + } + + args == null && func != null -> { if (EXPR_DEBUG_PRINT_ENABLED) { println("making GetVariable $func...") } OpGetVariable(name = func, receiver = null) - } else { - null } - } - ?: unresolvedReference( - ref = func ?: "null", - obj = context::class.simpleName - ?.substringAfter("Op") - ?.substringBefore("Context") - ) + else -> unresolvedReference( + ref = func ?: "null", + obj = context::class.simpleName + ?.substringAfter("Op") + ?.substringBefore("Context") + ) + } + } else -> { - kotlin.run { - if (args != null && func != null) { + when { + args != null && func != null -> { if (EXPR_DEBUG_PRINT_ENABLED) { println("parsed call for function $func with receiver $context") } - return@run OpFunctionExec( + OpFunctionExec( name = func, receiver = context, parameters = args ) } - if (args == null && func != null) { + + args == null && func != null -> { if (EXPR_DEBUG_PRINT_ENABLED) { println("making GetVariable $func with receiver $context... ") } - return@run OpGetVariable(name = func, receiver = context) + return OpGetVariable(name = func, receiver = context) } - unresolvedReference(func ?: "null") + + else -> unresolvedReference(func ?: "null") } } } @@ -876,7 +961,7 @@ internal class ESInterpreterImpl( println("making do/while loop") } - val body = parseBlock(context = blockContext + BlockContext.Loop) + val body = parseBlock(blockContext = blockContext + BlockContext.Loop) syntaxCheck(body is OpBlock) { "Invalid do/while syntax" @@ -903,10 +988,10 @@ internal class ESInterpreterImpl( blockContext = blockContext, ) - val onTrue = parseBlock(context = blockContext) + val onTrue = parseBlock(blockContext = blockContext) val onFalse = if (eatSequence("else")) { - parseBlock(context = blockContext) + parseBlock(blockContext = blockContext) } else null return OpIfCondition( @@ -933,7 +1018,7 @@ internal class ESInterpreterImpl( if (EXPR_DEBUG_PRINT_ENABLED) { println("making try") } - val tryBlock = parseBlock(requireBlock = true, context = blockContext) + val tryBlock = parseBlock(requireBlock = true, blockContext = blockContext) val catchBlock = if (eatSequence("catch")) { if (eat('(')) { @@ -948,15 +1033,15 @@ internal class ESInterpreterImpl( arg.name to parseBlock( scoped = false, requireBlock = true, - context = blockContext + blockContext = blockContext ) } else { - null to parseBlock(requireBlock = true, context = blockContext) + null to parseBlock(requireBlock = true, blockContext = blockContext) } } else null val finallyBlock = if (eatSequence("finally")) { - parseBlock(requireBlock = true, context = blockContext) + parseBlock(requireBlock = true, blockContext = blockContext) } else null return OpTryCatch( @@ -1012,7 +1097,7 @@ internal class ESInterpreterImpl( } } - val body = parseBlock(scoped = false, context = parentBlockContext + BlockContext.Loop) + val body = parseBlock(scoped = false, blockContext = parentBlockContext + BlockContext.Loop) return OpForLoop( assignment = assign, @@ -1031,7 +1116,7 @@ internal class ESInterpreterImpl( syntaxCheck(fArgs.size == args.size) { "Invalid arrow function" } - val lambda = parseBlock(context = blockContext + BlockContext.Function) + val lambda = parseBlock(blockContext = blockContext + BlockContext.Function) return Function( "", @@ -1040,8 +1125,59 @@ internal class ESInterpreterImpl( ) } + private fun parseClassDefinition() : ESClass { + if (EXPR_DEBUG_PRINT_ENABLED) { + println("parsing class...") + } + + val name = parseFactorOp(globalContext, emptyList()) + + syntaxCheck(name is OpGetVariable) { + "Invalid class declaration" + } + + val extends = if (eatSequence("extends")) { + parseFactorOp(globalContext, emptyList()) + } else null + + val static = mutableListOf() + + val body = parseBlock( + scoped = false, + requireBlock = true, + blockContext = listOf(BlockContext.Class), + static = static + ) as OpBlock + + val functions = body.expressions + .filterIsInstance() + .map { it.value } + .filterIsInstance() + + syntaxCheck(functions.size == body.expressions.size) { + "Invalid class body (${name.name})" + } + + val constructors = functions.filter { it.name == "constructor" }.toSet() + + syntaxCheck(constructors.size <= 1) { + "A class may only have one constructor" + } + + val clazz = ESClassBase( + name = name.name, + functions = functions - constructors, + construct = constructors.singleOrNull(), + extends = extends, + static = static + ) + + return clazz + } + private fun parseFunctionDefinition( name: String? = null, + args: List? = null, blockContext: List ): Function { @@ -1059,8 +1195,8 @@ internal class ESInterpreterImpl( println("making defined function $actualName") } - val args = parseFunctionArgs(actualName).let { args -> - args?.map { + val nArgs = (args ?: parseFunctionArgs(actualName))?.let { a -> + a.map { when (it) { is OpGetVariable -> FunctionParam(name = it.name, default = null) is OpAssign -> FunctionParam( @@ -1073,8 +1209,8 @@ internal class ESInterpreterImpl( } } - if (args == null) { - throw SyntaxError("Missing function args") + syntaxCheck(nArgs != null) { + "Missing function args" } syntaxCheck(nextCharIs('{'::equals)) { @@ -1084,13 +1220,14 @@ internal class ESInterpreterImpl( val block = parseBlock( scoped = false, - context = blockContext + BlockContext.Function + blockContext = blockContext + BlockContext.Function ) return Function( name = actualName, - parameters = args, - body = block + parameters = nArgs, + body = block, + isClassMember = args != null ) } @@ -1128,13 +1265,14 @@ internal class ESInterpreterImpl( private fun parseBlock( scoped: Boolean = true, requireBlock: Boolean = false, - context: List + blockContext: List, + static : MutableList? = null, ): Expression { var funcIndex = 0 val list = buildList { if (eat('{')) { while (!eat('}') && pos < expr.length) { - val expr = parseAssignment(globalContext, context, isExpressionStart = true) + val expr = parseAssignment(globalContext, blockContext, isExpressionStart = true) if (size == 0 && expr is OpGetVariable && eat(':')) { return parseObject( @@ -1148,19 +1286,41 @@ internal class ESInterpreterImpl( ) } - if (expr is OpConstant && expr.value is Function) { - add( - funcIndex++, - OpAssign( - type = VariableType.Local, - variableName = expr.value.name, - receiver = null, - assignableValue = expr, - merge = null + when { + expr is OpAssign && expr.isStatic -> { + static?.add(StaticClassMember.Variable(expr.variableName, expr.assignableValue)) + } + expr is OpConstant && expr.value is Function && expr.value.isStatic -> { + static?.add(StaticClassMember.Method(expr.value)) + } + expr is OpConstant && (expr.value is Function && !expr.value.isClassMember || expr.value is ESClass) -> { + val name = (expr.value as Named).name + + if (EXPR_DEBUG_PRINT_ENABLED){ + println("registering '$name' as class or top level function") + } + add( + index = funcIndex++, + element = OpAssign( + type = VariableType.Local, + variableName = name, + receiver = null, + assignableValue = expr, + merge = null + ) ) - ) - } else { - add(expr) + if (expr.value is ESClass) { + expr.value.static.forEach { s -> + add( + index = funcIndex++, + element = Expression { + s.assignTo(expr.value, it) + } + ) + } + } + } + else -> add(expr) } eat(';') eat(';') @@ -1169,7 +1329,7 @@ internal class ESInterpreterImpl( if (requireBlock) { throw SyntaxError("Unexpected token at $pos: block start was expected") } - add(parseAssignment(globalContext, context)) + add(parseAssignment(globalContext, blockContext)) } } return OpBlock(list, scoped) 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 a50c4a0..6a059ef 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 @@ -2,14 +2,17 @@ package io.github.alexzhirkevich.skriptie.ecmascript import io.github.alexzhirkevich.skriptie.Expression import io.github.alexzhirkevich.skriptie.ScriptRuntime +import io.github.alexzhirkevich.skriptie.common.Function import io.github.alexzhirkevich.skriptie.common.FunctionParam import io.github.alexzhirkevich.skriptie.common.OpConstant import io.github.alexzhirkevich.skriptie.common.defaults import io.github.alexzhirkevich.skriptie.invoke +import io.github.alexzhirkevich.skriptie.javascript.JsNumber +import io.github.alexzhirkevich.skriptie.javascript.JsNumberClass internal class ESNumber : ESFunctionBase("Number") { - val isFinite by func("number"){ + val isFinite by func("number") { val arg = it.getOrNull(0) ?: return@func false val num = toNumber(arg).toDouble() if (num.isNaN()) @@ -35,11 +38,6 @@ internal class ESNumber : ESFunctionBase("Number") { arg.toString().trim().trimParseInt(radix) } - val isNan by func("number") { - val arg = it.getOrNull(0) ?: return@func false - toNumber(arg).toDouble().isNaN() - } - val isSafeInteger by func("number") { val arg = it.getOrNull(0) ?: return@func false val num = toNumber(arg) @@ -58,6 +56,15 @@ internal class ESNumber : ESFunctionBase("Number") { num.toDoubleOrNull() ?: 0L } + val isNan by func("number") { + val arg = it.getOrNull(0) ?: return@func false + toNumber(arg).toDouble().isNaN() + } + + override val functions: List = listOf( + isFinite, isInteger, parseInt, isNan, isSafeInteger, parseFloat + ) + override fun get(variable: Any?): Any? { return when (variable) { "EPSILON" -> Double.MIN_VALUE @@ -72,9 +79,13 @@ internal class ESNumber : ESFunctionBase("Number") { } } - override fun invoke(args: List, context: ScriptRuntime): Any? { + override fun invoke(args: List, context: ScriptRuntime): Number { return context.toNumber(args.single().invoke(context)) } + + override fun newInstance(args: List, context: ScriptRuntime): JsNumberClass { + return JsNumberClass(JsNumber(invoke(args, context))) + } } private fun String.trimParseInt(radix : Int) : Long? { 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 b2d28d8..8616873 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 @@ -5,22 +5,25 @@ import io.github.alexzhirkevich.skriptie.ScriptRuntime import io.github.alexzhirkevich.skriptie.common.Callable import io.github.alexzhirkevich.skriptie.common.Function import io.github.alexzhirkevich.skriptie.common.FunctionParam -import io.github.alexzhirkevich.skriptie.common.unresolvedReference import kotlin.properties.PropertyDelegateProvider import kotlin.properties.ReadOnlyProperty public interface ESObject : ESAny { - public val keys : Set - public val entries : List> + 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? { + override fun invoke( + function: String, + context: ScriptRuntime, + arguments: List + ): Any? { val f = get(function) if (f !is Callable) { - when (f){ + when (f) { "toString" -> return toString() else -> unresolvedReference(function) } @@ -29,8 +32,9 @@ public interface ESObject : ESAny { } } + internal open class ESObjectBase( - internal val name : String, + open val name : String, private val map : MutableMap = mutableMapOf() ) : ESObject { @@ -121,7 +125,7 @@ internal fun func( vararg args: String, params: (String) -> FunctionParam = { FunctionParam(it) }, body: ScriptRuntime.(args: List) -> Any? -) : PropertyDelegateProvider> = func( +) : PropertyDelegateProvider> = func( args = args.map(params).toTypedArray(), body = body ) @@ -129,7 +133,7 @@ internal fun func( internal fun func( vararg args: FunctionParam, body: ScriptRuntime.(args: List) -> Any? -): PropertyDelegateProvider> = PropertyDelegateProvider { obj, prop -> +): PropertyDelegateProvider> = PropertyDelegateProvider { obj, prop -> obj.set(prop.name, Function( name = prop.name, parameters = args.toList(), @@ -141,7 +145,7 @@ internal fun func( )) ReadOnlyProperty { thisRef, property -> - thisRef[property.name] as Callable + thisRef[property.name] as Function } } 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 index 5f8f00f..945ac6b 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESObjectAccessor.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/ESObjectAccessor.kt @@ -2,20 +2,28 @@ package io.github.alexzhirkevich.skriptie.ecmascript import io.github.alexzhirkevich.skriptie.Expression import io.github.alexzhirkevich.skriptie.ScriptRuntime +import io.github.alexzhirkevich.skriptie.common.Function import io.github.alexzhirkevich.skriptie.invoke internal class ESObjectAccessor : ESFunctionBase("Object") { - init { - this.set("keys", "keys".func("o"){ - (it.firstOrNull() as? ESObject)?.keys ?: emptyList() - }) + private val fKeys = "keys".func("o"){ + (it.firstOrNull() as? ESObject)?.keys ?: emptyList() + } + + private val fEntries = "entries".func("o"){ + (it.firstOrNull() as? ESObject)?.entries ?: emptyList() + } - this.set("entries", "entries".func("o"){ - (it.firstOrNull() as? ESObject)?.entries ?: emptyList() - }) + init { + this["keys"] = fKeys + this["entries"] = fEntries } + override val functions: List = listOf( + fKeys, fEntries + ) + override fun invoke(args: List, context: ScriptRuntime): Any? { return args.single().invoke(context) as ESObject } 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 beaf063..736a9b6 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 @@ -6,7 +6,6 @@ import io.github.alexzhirkevich.skriptie.Expression import io.github.alexzhirkevich.skriptie.ScriptIO import io.github.alexzhirkevich.skriptie.ScriptRuntime import io.github.alexzhirkevich.skriptie.VariableType -import io.github.alexzhirkevich.skriptie.common.TypeError import kotlin.jvm.JvmInline public abstract class ESRuntime( @@ -33,12 +32,21 @@ public abstract class ESRuntime( set("Object", ESObjectAccessor(), VariableType.Const) set("Number", ESNumber(), VariableType.Const) set("globalThis", this, VariableType.Const) + set("this", this, VariableType.Const) set("Infinity", Double.POSITIVE_INFINITY, VariableType.Const) set("NaN", Double.NaN, VariableType.Const) set("undefined", Unit, VariableType.Const) } final override fun get(variable: Any?): Any? { + val v = getInternal(variable) + if (variable == "this" && v is ESClass && !v.isInitialized) { + throw ReferenceError("Must call super constructor in derived class before accessing 'this' or returning from derived constructor") + } + return v + } + + private fun getInternal(variable: Any?) : Any? { if (variable in this){ return super.get(variable) } diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/Errors.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/Errors.kt similarity index 64% rename from skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/Errors.kt rename to skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/Errors.kt index 314fc56..5683481 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/common/Errors.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/ecmascript/Errors.kt @@ -1,8 +1,6 @@ -package io.github.alexzhirkevich.skriptie.common +package io.github.alexzhirkevich.skriptie.ecmascript -import io.github.alexzhirkevich.skriptie.ecmascript.ESAny - -public sealed class SkriptieError(message : String?, cause : Throwable?) : Exception(message, cause), ESAny { +public open class ESError(message : String?, cause : Throwable?) : Exception(message, cause), ESAny { override fun get(variable: Any?): Any? { return when(variable){ "message" -> message @@ -13,11 +11,11 @@ public sealed class SkriptieError(message : String?, cause : Throwable?) : Excep } } -public class SyntaxError(message : String? = null, cause : Throwable? = null) : SkriptieError(message, cause) +public class SyntaxError(message : String? = null, cause : Throwable? = null) : ESError(message, cause) -public class TypeError(message : String? = null, cause : Throwable? = null) : SkriptieError(message, cause) +public class TypeError(message : String? = null, cause : Throwable? = null) : ESError(message, cause) -public class ReferenceError(message : String? = null, cause : Throwable? = null) : SkriptieError(message, cause) +public class ReferenceError(message : String? = null, cause : Throwable? = null) : ESError(message, cause) internal fun unresolvedReference(ref : String, obj : String? = null) : Nothing = if (obj != null) 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 index 1b4b2b4..6196f60 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JSLangContext.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JSLangContext.kt @@ -213,7 +213,9 @@ private fun jspos(v : Any?) : Any { private tailrec fun Any?.numberOrNull(withNaNs : Boolean = true) : Number? = when(this) { - null -> 0 + null -> 0L + true -> 1L + false -> 0L is JsString -> if (withNaNs) value.numberOrNull(withNaNs) else null is JsArray -> if (withNaNs) value.numberOrNull(withNaNs) else null is JsWrapper<*> -> value.numberOrNull() 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 8a62a5b..0542828 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 @@ -5,7 +5,7 @@ import io.github.alexzhirkevich.skriptie.ScriptRuntime import io.github.alexzhirkevich.skriptie.argAt import io.github.alexzhirkevich.skriptie.common.Callable import io.github.alexzhirkevich.skriptie.common.OpConstant -import io.github.alexzhirkevich.skriptie.common.TypeError +import io.github.alexzhirkevich.skriptie.ecmascript.TypeError import io.github.alexzhirkevich.skriptie.common.checkNotEmpty import io.github.alexzhirkevich.skriptie.common.fastFilter import io.github.alexzhirkevich.skriptie.common.fastMap 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 6c458f0..4faf91d 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 @@ -3,19 +3,49 @@ package io.github.alexzhirkevich.skriptie.javascript import io.github.alexzhirkevich.skriptie.Expression import io.github.alexzhirkevich.skriptie.ScriptRuntime import io.github.alexzhirkevich.skriptie.argAtOrNull -import io.github.alexzhirkevich.skriptie.common.unresolvedReference +import io.github.alexzhirkevich.skriptie.common.Function import io.github.alexzhirkevich.skriptie.ecmascript.ESAny +import io.github.alexzhirkevich.skriptie.ecmascript.ESClass +import io.github.alexzhirkevich.skriptie.ecmascript.ESObjectBase import io.github.alexzhirkevich.skriptie.ecmascript.checkArgsNotNull +import io.github.alexzhirkevich.skriptie.ecmascript.unresolvedReference import io.github.alexzhirkevich.skriptie.invoke import kotlin.jvm.JvmInline import kotlin.math.pow import kotlin.math.roundToInt import kotlin.math.roundToLong +internal class JsNumberClass( + val number: JsNumber +) : ESObjectBase("Number"), ESClass, JsWrapper by number, Comparable> by number { + + override fun invoke(args: List, context: ScriptRuntime): Any? = Unit + + override fun toString(): String { + return number.toString() + } + + override val name: String + get() = "Number" + + override fun get(variable: Any?): Any? { + return number[variable] ?: super.get(variable) + } + + override val functions: List get() = emptyList() + override val construct: Function? get() = null + + override val extends: Expression = Expression { + it["Number"] + } + + override val constructorClass: Expression? get() = extends +} + @JvmInline public value class JsNumber( override val value : Number -) : ESAny, JsWrapper, Comparable { +) : ESAny, JsWrapper, Comparable> { override val type: String get() = "number" @@ -48,7 +78,7 @@ public value class JsNumber( } } - override fun compareTo(other: JsNumber): Int { + override fun compareTo(other: JsWrapper): Int { return value.toDouble().compareTo(other.value.toDouble()) } } diff --git a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JsWrapper.kt b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JsWrapper.kt index c8157db..6132ec6 100644 --- a/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JsWrapper.kt +++ b/skriptie/src/commonMain/kotlin/io/github/alexzhirkevich/skriptie/javascript/JsWrapper.kt @@ -1,5 +1,5 @@ package io.github.alexzhirkevich.skriptie.javascript -internal interface JsWrapper { - val value: T +public interface JsWrapper { + public val value: T } \ No newline at end of file diff --git a/skriptie/src/commonTest/kotlin/js/ClassesTest.kt b/skriptie/src/commonTest/kotlin/js/ClassesTest.kt new file mode 100644 index 0000000..9c6c4d3 --- /dev/null +++ b/skriptie/src/commonTest/kotlin/js/ClassesTest.kt @@ -0,0 +1,295 @@ +package js + +import io.github.alexzhirkevich.skriptie.ecmascript.ESClass +import io.github.alexzhirkevich.skriptie.ecmascript.ESObject +import io.github.alexzhirkevich.skriptie.ecmascript.ReferenceError +import io.github.alexzhirkevich.skriptie.ecmascript.SyntaxError +import io.github.alexzhirkevich.skriptie.javascript.JSRuntime +import kotlin.test.Test +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +class ClassesTest { + + @Test + fun declaration() { + """ + class Test {} + + new Test() + """.trimIndent().eval().let { + assertTrue { it is ESClass } + } + + """ + class Test { + constructor(x){ + this.x = x + } + } + + let t = new Test(123) + t.x + """.trimIndent().eval().assertEqualsTo(123L) + } + + @Test + fun singleConstructor() { + assertFailsWith { + """ + class Test { + constructor(){} + constructor(x){ + this.x = x + } + } + """.trimIndent().eval() + } + } + + @Test + fun methods() { + """ + class Test { + method(x){ + return x + } + } + + let t = new Test() + t.method(123) + """.trimIndent().eval().assertEqualsTo(123L) + + """ + class Test { + method(){ return 1 } + method(){ return 2 } + } + + (new Test()).method() + """.trimIndent().eval().assertEqualsTo(2L) + } + + @Test + fun inlineMethodInvoke() { + """ + class Test { + method(x){ + return x + } + } + + new Test().method(123) + """.trimIndent().eval().assertEqualsTo(123L) + } + + @Test + fun inheritance() { + """ + class A { + a(){ return 'a'} + } + class B extends A {} + + new B().a() + """.trimIndent().eval().assertEqualsTo("a") + + assertTrue { + """ + class A extends Object { + a(){ return 'a'} + } + let a = new A() + """.trimIndent().eval() is ESObject + } + } + + @Test + fun instanceof() { + + assertTrue { + """ + class Test {} + let t = new Test() + t instanceof Test + """.trimIndent().eval() as Boolean + } + + assertTrue { + """ + class A {} + class B extends A {} + + new B() instanceof A + """.trimIndent().eval() as Boolean + } + + assertTrue { + """ + class Test {} + let t = new Test() + t instanceof Object + """.trimIndent().eval() as Boolean + } + + } + + @Test + fun superConstructorAndMethod() { + + val runtime = JSRuntime() + + """ + class Person { + constructor(name) { + this.name = name; + } + getDetails() { + return this.name + } + } + + class Employee extends Person { + constructor(name, company) { + super(name); + this.company = company; + } + } + + const emp1 = new Employee("John", "Unilever"); + """.trimIndent().eval(runtime) + + "emp1.getDetails()".eval(runtime).assertEqualsTo("John") + "emp1.name".eval(runtime).assertEqualsTo("John") + "emp1.company".eval(runtime).assertEqualsTo("Unilever") + } + + @Test + fun override() { + + """ + class A { + test() { + return 'A' + } + } + + class B extends A { + test() { + return 'B' + } + } + + new B().test() + """.trimIndent().eval().assertEqualsTo("B") + } + + @Test + fun static() { + + val runtime = JSRuntime() + """ + class A { + static test = 'static' + + static method(){ + return A.test + } + } + """.trimIndent().eval(runtime) + + "A.test".eval(runtime).assertEqualsTo("static") + "A.method()".eval(runtime).assertEqualsTo("static") + } + + @Test + fun staticInheritance() { + + val runtime = JSRuntime() + + """ + class A { + static test = 'static' + + static method(){ + return A.test + } + } + + class B extends A {} + """.trimIndent().eval(runtime) + + "B.test".eval(runtime).assertEqualsTo("static") + "B.method()".eval(runtime).assertEqualsTo("static") + } + + + @Test + fun doubleSuperCall() { + + assertFailsWith { + """ + class A { + constructor() { + } + } + + class B extends A { + constructor(x) { + super(); + super(); + this.x = x + } + } + + let b = new B() + + """.trimIndent().eval() + } + } + + @Test + fun missedSuperCall(){ + assertFailsWith { + """ + class A { + } + + class B extends A { + constructor(x) { + console.log(this) + this.x = x; + } + } + + const emp1 = new B(1); + """.trimIndent().eval() + } + } + @Test + fun notMatchingArgConstructor(){ + """ + class Test { + constructor(x){ + this.x = x + if (x !== undefined) + throw "error" + } + } + + let t = new Test() + t.x + """.trimIndent().eval().assertEqualsTo(Unit) + } + + @Test + fun number(){ + "1 instanceof Number".eval().assertEqualsTo(false) + "new Number(1)".eval().assertEqualsTo(1L) + "new Number('1')".eval().assertEqualsTo(1L) + "new Number(1) instanceof Number".eval().assertEqualsTo(true) + "new Number(1) === Number(1)".eval().assertEqualsTo(false) + "new Number('1') == Number(true)".eval().assertEqualsTo(true) + "new Number('2') > Number(true)".eval().assertEqualsTo(true) + } +} \ No newline at end of file diff --git a/skriptie/src/commonTest/kotlin/js/EsNumberTest.kt b/skriptie/src/commonTest/kotlin/js/EsNumberTest.kt index dda2288..e0dd6fa 100644 --- a/skriptie/src/commonTest/kotlin/js/EsNumberTest.kt +++ b/skriptie/src/commonTest/kotlin/js/EsNumberTest.kt @@ -33,10 +33,15 @@ class EsNumberTest { "Number(\"123.0\")".eval().assertEqualsTo(123.0) "Number(\"unicorn\")".eval().assertEqualsTo(Double.NaN) "Number(undefined)".eval().assertEqualsTo(Double.NaN) + "Number(true)".eval().assertEqualsTo(1L) + "Number(false)".eval().assertEqualsTo(0L) + "1 === Number(1)".eval().assertEqualsTo(true) + "1 == Number(1)".eval().assertEqualsTo(true) + "1 == true".eval().assertEqualsTo(true) + "0 == false".eval().assertEqualsTo(true) } @Test fun static_props(){ - "Number.MAX_SAFE_INTEGER".eval().assertEqualsTo(Long.MAX_VALUE) "Number.MIN_SAFE_INTEGER".eval().assertEqualsTo(Long.MIN_VALUE) "Number.MAX_VALUE".eval().assertEqualsTo(Double.MAX_VALUE) diff --git a/skriptie/src/commonTest/kotlin/js/FunctionsTest.kt b/skriptie/src/commonTest/kotlin/js/FunctionsTest.kt index f4c0787..b6ba2a6 100644 --- a/skriptie/src/commonTest/kotlin/js/FunctionsTest.kt +++ b/skriptie/src/commonTest/kotlin/js/FunctionsTest.kt @@ -1,7 +1,7 @@ package js -import io.github.alexzhirkevich.skriptie.common.ReferenceError -import io.github.alexzhirkevich.skriptie.common.TypeError +import io.github.alexzhirkevich.skriptie.ecmascript.ReferenceError +import io.github.alexzhirkevich.skriptie.ecmascript.TypeError import kotlin.test.Test import kotlin.test.assertFailsWith diff --git a/skriptie/src/commonTest/kotlin/js/JsTestUtil.kt b/skriptie/src/commonTest/kotlin/js/JsTestUtil.kt index 6fa269c..7c51eaf 100644 --- a/skriptie/src/commonTest/kotlin/js/JsTestUtil.kt +++ b/skriptie/src/commonTest/kotlin/js/JsTestUtil.kt @@ -3,6 +3,7 @@ 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.ScriptRuntime import io.github.alexzhirkevich.skriptie.ecmascript.ESInterpreter import io.github.alexzhirkevich.skriptie.invoke import io.github.alexzhirkevich.skriptie.javascript.JSLangContext @@ -16,8 +17,10 @@ internal fun Any?.assertEqualsTo(other : Double, tolerance: Double = 0.0001) { assertEquals(other, this as Double, tolerance) } -internal fun String.eval(io : ScriptIO = DefaultScriptIO) : Any? { - val runtime = JSRuntime(io) +internal fun String.eval(runtime: ScriptRuntime = JSRuntime()) : Any? { + return ScriptEngine(runtime, ESInterpreter(JSLangContext)).invoke(this) +} +internal fun String.eval(io : ScriptIO = DefaultScriptIO, runtime: ScriptRuntime = JSRuntime(io)) : Any? { 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 index de6be95..febf293 100644 --- a/skriptie/src/commonTest/kotlin/js/ObjectTest.kt +++ b/skriptie/src/commonTest/kotlin/js/ObjectTest.kt @@ -1,6 +1,7 @@ package js import io.github.alexzhirkevich.skriptie.ecmascript.ESObject +import io.github.alexzhirkevich.skriptie.javascript.JSRuntime import kotlin.test.Test class ObjectTest { @@ -28,6 +29,9 @@ class ObjectTest { @Test fun syntax(){ + + val runtime = JSRuntime() + """ let obj = { string : "string", @@ -35,9 +39,13 @@ class ObjectTest { 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") + """.trimIndent().eval(runtime) + + "typeof(obj.string)".eval(runtime).assertEqualsTo("string") + "typeof(obj.number)".eval(runtime).assertEqualsTo("number") + "typeof(obj.f)".eval(runtime).assertEqualsTo("function") + "typeof(obj.af)".eval(runtime).assertEqualsTo("function") + "typeof(obj.nothing)".eval(runtime).assertEqualsTo("undefined") } @Test diff --git a/skriptie/src/commonTest/kotlin/js/SyntaxTest.kt b/skriptie/src/commonTest/kotlin/js/SyntaxTest.kt index 970fc64..64c73d8 100644 --- a/skriptie/src/commonTest/kotlin/js/SyntaxTest.kt +++ b/skriptie/src/commonTest/kotlin/js/SyntaxTest.kt @@ -1,8 +1,8 @@ package js -import io.github.alexzhirkevich.skriptie.common.ReferenceError -import io.github.alexzhirkevich.skriptie.common.SyntaxError -import io.github.alexzhirkevich.skriptie.common.TypeError +import io.github.alexzhirkevich.skriptie.ecmascript.ReferenceError +import io.github.alexzhirkevich.skriptie.ecmascript.SyntaxError +import io.github.alexzhirkevich.skriptie.ecmascript.TypeError import kotlin.test.Test import kotlin.test.assertFailsWith