From 95e68d6e2a244939cd86b1a54ff141e62fc68eac Mon Sep 17 00:00:00 2001 From: sergeych Date: Mon, 19 May 2025 11:16:46 +0400 Subject: [PATCH] comparison operators + some optimizations --- .../kotlin/net/sergeych/ling/Compiler.kt | 65 ++++++++++----- .../kotlin/net/sergeych/ling/Obj.kt | 79 ++++++++++++++++--- .../kotlin/net/sergeych/ling/Parser.kt | 27 ++++++- .../kotlin/net/sergeych/ling/Script.kt | 2 +- .../kotlin/net/sergeych/ling/Token.kt | 8 +- .../kotlin/net/sergeych/ling/statements.kt | 11 ++- library/src/commonTest/kotlin/ScriptTest.kt | 31 ++++++++ 7 files changed, 187 insertions(+), 36 deletions(-) diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt index 86baa3a..a41ebcf 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt @@ -115,7 +115,7 @@ class Compiler { while (true) { val opToken = tokens.next() - val op = byLevel[level][opToken.value] + val op = byLevel[level][opToken.type] if (op == null) { tokens.previous() break @@ -142,17 +142,21 @@ class Compiler { when (t.value) { "void" -> statement(t.pos, true) { ObjVoid } "null" -> statement(t.pos, true) { ObjNull } + "true" -> statement(t.pos, true) { ObjBool(true) } + "false" -> statement(t.pos, true) { ObjBool(false) } else -> parseVarAccess(t, tokens) } } + Token.Type.LPAREN -> { // ( subexpr ) parseExpression(tokens)?.also { val tl = tokens.next() - if( tl.type != Token.Type.RPAREN ) + if (tl.type != Token.Type.RPAREN) throw ScriptError(t.pos, "unbalanced parenthesis: no ')' for it") } } + Token.Type.PLUS -> { val n = parseNumber(true, tokens) statement(t.pos, true) { n } @@ -213,7 +217,8 @@ class Compiler { } while (t.type != Token.Type.RPAREN) statement(id.pos) { context -> - val v = resolve(context).get(id.value) ?: throw ScriptError(id.pos, "Undefined function: ${id.value}") + val v = + resolve(context).get(id.value) ?: throw ScriptError(id.pos, "Undefined function: ${id.value}") (v.value as? Statement)?.execute( context.copy( Arguments( @@ -325,7 +330,10 @@ class Compiler { d.name, false, d.defaultValue?.execute(context) - ?: throw ScriptError(context.args.callerPos, "missing required argument #${1+i}: ${d.name}") + ?: throw ScriptError( + context.args.callerPos, + "missing required argument #${1 + i}: ${d.name}" + ) ) } @@ -387,40 +395,59 @@ class Compiler { // } // } + data class Operator( + val tokenType: Token.Type, + val priority: Int, val arity: Int, + val generate: (Pos, Statement, Statement) -> Statement + ) + companion object { - data class Operator( - val name: String, - val priority: Int, val arity: Int, - val generate: (Pos, Statement, Statement) -> Statement - ) val allOps = listOf( - Operator("||", 0, 2) { pos, a, b -> LogicalOrStatement(pos, a, b) }, - Operator("&&", 1, 2) { pos, a, b -> LogicalAndStatement(pos, a, b) }, + Operator(Token.Type.OR, 0, 2) { pos, a, b -> LogicalOrStatement(pos, a, b) }, + Operator(Token.Type.AND, 1, 2) { pos, a, b -> LogicalAndStatement(pos, a, b) }, // bitwise or 2 // bitwise and 3 // equality/ne 4 + LogicalOp(Token.Type.EQ, 4) { a, b -> a == b }, + LogicalOp(Token.Type.NEQ, 4) { a, b -> a != b }, // relational <=,... 5 + LogicalOp(Token.Type.LTE, 5) { a, b -> a <= b }, + LogicalOp(Token.Type.LT, 5) { a, b -> a < b }, + LogicalOp(Token.Type.GTE, 5) { a, b -> a >= b }, + LogicalOp(Token.Type.GT, 5) { a, b -> a > b }, // shuttle <=> 6 // bitshhifts 7 - Operator("+", 8, 2) { pos, a, b -> + Operator(Token.Type.PLUS, 8, 2) { pos, a, b -> PlusStatement(pos, a, b) }, - Operator("-", 8, 2) { pos, a, b -> + Operator(Token.Type.MINUS, 8, 2) { pos, a, b -> MinusStatement(pos, a, b) }, - Operator("*", 9, 2) { pos, a, b -> MulStatement(pos, a, b) }, - Operator("/", 9, 2) { pos, a, b -> DivStatement(pos, a, b) }, - Operator("%", 9, 2) { pos, a, b -> ModStatement(pos, a, b) }, + Operator(Token.Type.STAR, 9, 2) { pos, a, b -> MulStatement(pos, a, b) }, + Operator(Token.Type.SLASH, 9, 2) { pos, a, b -> DivStatement(pos, a, b) }, + Operator(Token.Type.PERCENT, 9, 2) { pos, a, b -> ModStatement(pos, a, b) }, ) val lastLevel = 10 - val byLevel: List> = (0..< lastLevel).map { l -> + val byLevel: List> = (0.. allOps.filter { it.priority == l } - .map { it.name to it }.toMap() + .map { it.tokenType to it }.toMap() } fun compile(code: String): Script = Compiler().compile(Source("", code)) } } -suspend fun eval(code: String) = Compiler.compile(code).execute() \ No newline at end of file +suspend fun eval(code: String) = Compiler.compile(code).execute() + +fun LogicalOp(tokenType: Token.Type, priority: Int, f: (Obj, Obj) -> Boolean) = Compiler.Operator( + tokenType, + priority, + 2 +) { pos, a, b -> + statement(pos) { + ObjBool( + f(a.execute(it), b.execute(it)) + ) + } +} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt index 1202b23..3f80167 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt @@ -5,14 +5,36 @@ import kotlinx.serialization.Serializable import kotlin.math.floor @Serializable -sealed class Obj { +sealed class Obj : Comparable { open val asStr: ObjString by lazy { - if( this is ObjString) this else ObjString(this.toString()) + if (this is ObjString) this else ObjString(this.toString()) + } + + open val type: Type = Type.Any + + @Suppress("unused") + enum class Type { + @SerialName("Void") + Void, + @SerialName("Null") + Null, + @SerialName("String") + String, + @SerialName("Int") + Int, + @SerialName("Real") + Real, + @SerialName("Bool") + Bool, + @SerialName("Fn") + Fn, + @SerialName("Any") + Any, } companion object { inline fun from(obj: T): Obj { - return when(obj) { + return when (obj) { is Obj -> obj is Double -> ObjReal(obj) is Float -> ObjReal(obj.toDouble()) @@ -34,15 +56,23 @@ inline fun T.toObj(): Obj = Obj.from(this) @Serializable @SerialName("void") -object ObjVoid: Obj() { +object ObjVoid : Obj() { override fun equals(other: Any?): Boolean { return other is ObjVoid || other is Unit } + + override fun compareTo(other: Obj): Int { + return if (other === this) 0 else -1 + } } @Serializable @SerialName("null") -object ObjNull: Obj() { +object ObjNull : Obj() { + override fun compareTo(other: Obj): Int { + return if (other === this) 0 else -1 + } + override fun equals(other: Any?): Boolean { return other is ObjNull || other == null } @@ -50,7 +80,13 @@ object ObjNull: Obj() { @Serializable @SerialName("string") -data class ObjString(val value: String): Obj() { +data class ObjString(val value: String) : Obj() { + + override fun compareTo(other: Obj): Int { + if (other !is ObjString) throw IllegalArgumentException("cannot compare string with $other") + return this.value.compareTo(other.value) + } + override fun toString(): String = value } @@ -74,35 +110,58 @@ fun Obj.toLong(): Long = fun Obj.toInt(): Int = toLong().toInt() +fun Obj.toBool(): Boolean = (this as? ObjBool)?.value ?: throw IllegalArgumentException("cannot convert to boolean ${this.type}:$this") + @Serializable @SerialName("real") -data class ObjReal(val value: Double): Obj(), Numeric { +data class ObjReal(val value: Double) : Obj(), Numeric { override val asStr by lazy { ObjString(value.toString()) } override val longValue: Long by lazy { floor(value).toLong() } override val doubleValue: Double by lazy { value } override val toObjInt: ObjInt by lazy { ObjInt(longValue) } override val toObjReal: ObjReal by lazy { ObjReal(value) } + + override fun compareTo(other: Obj): Int { + if( other !is Numeric) throw IllegalArgumentException("cannot compare $this with $other") + return value.compareTo(other.doubleValue) + } + } @Serializable @SerialName("int") -data class ObjInt(val value: Long): Obj(), Numeric { +data class ObjInt(val value: Long) : Obj(), Numeric { override val asStr by lazy { ObjString(value.toString()) } override val longValue: Long by lazy { value } override val doubleValue: Double by lazy { value.toDouble() } override val toObjInt: ObjInt by lazy { ObjInt(value) } override val toObjReal: ObjReal by lazy { ObjReal(doubleValue) } + + override fun compareTo(other: Obj): Int { + if( other !is Numeric) throw IllegalArgumentException("cannot compare $this with $other") + return value.compareTo(other.doubleValue) + } } @Serializable @SerialName("bool") -data class ObjBool(val value: Boolean): Obj() { +data class ObjBool(val value: Boolean) : Obj() { override val asStr by lazy { ObjString(value.toString()) } + + override fun compareTo(other: Obj): Int { + if( other !is ObjBool) throw IllegalArgumentException("cannot compare $this with $other") + return value.compareTo(other.value) + } + } -data class ObjNamespace(val name: String,val context: Context): Obj() { +data class ObjNamespace(val name: String, val context: Context) : Obj() { override fun toString(): String { return "namespace ${name}" } + + override fun compareTo(other: Obj): Int { + throw IllegalArgumentException("cannot compare namespaces") + } } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt index 5b8813d..0a2fdfd 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt @@ -55,9 +55,30 @@ private class Parser(fromPos: Pos) { '/' -> Token("/", from, Token.Type.SLASH) '%' -> Token("%", from, Token.Type.PERCENT) '.' -> Token(".", from, Token.Type.DOT) - '<' -> Token("<", from, Token.Type.LT) - '>' -> Token(">", from, Token.Type.GT) - '!' -> Token("!", from, Token.Type.NOT) + '<' -> { + if(currentChar == '=') { + advance() + Token("<=", from, Token.Type.LTE) + } + else + Token("<", from, Token.Type.LT) + } + '>' -> { + if( currentChar == '=') { + advance() + Token(">=", from, Token.Type.GTE) + } + else + Token(">", from, Token.Type.GT) + } + '!' -> { + if( currentChar == '=') { + advance() + Token("!=", from, Token.Type.NEQ) + } + else + Token("!", from, Token.Type.NOT) + } '|' -> { if (currentChar == '|') { advance() diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt index e30c129..af9b645 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt @@ -16,7 +16,7 @@ class Script( return lastResult } - suspend fun execute() = execute(defaultContext) + suspend fun execute() = execute(defaultContext.copy()) companion object { val defaultContext: Context = Context(null).apply { diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt index b02558d..f2bde45 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt @@ -1,12 +1,16 @@ package net.sergeych.ling data class Token(val value: String, val pos: Pos, val type: Type) { + @Suppress("unused") enum class Type { ID, INT, REAL, HEX, STRING, LPAREN, RPAREN, LBRACE, RBRACE, LBRACKET, RBRACKET, COMMA, - SEMICOLON, COLON, EQ, PLUS, MINUS, STAR, SLASH, ASSIGN, EQEQ, NEQ, LT, LTEQ, GT, - GTEQ, AND, BITAND, OR, BITOR, NOT, DOT, ARROW, QUESTION, COLONCOLON, PERCENT, + SEMICOLON, COLON, + PLUS, MINUS, STAR, SLASH, ASSIGN, + EQ, NEQ, LT, LTE, GT, GTE, + AND, BITAND, OR, BITOR, NOT, DOT, ARROW, QUESTION, COLONCOLON, PERCENT, EOF, } + companion object { // fun eof(parser: Parser) = Token("", parser.currentPos, Type.EOF) } diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/statements.kt b/library/src/commonMain/kotlin/net/sergeych/ling/statements.kt index 8e9a06f..36a4f15 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/statements.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/statements.kt @@ -3,11 +3,20 @@ package net.sergeych.ling fun String.toSource(name: String = "eval"): Source = Source(name, this) +@Suppress("unused") abstract class Statement( - @Suppress("unused") val isStaticConst: Boolean = false + val isStaticConst: Boolean = false, + val isConst: Boolean = false, + val returnType: Type = Type.Any ) : Obj() { + abstract val pos: Pos abstract suspend fun execute(context: Context): Obj + + override fun compareTo(other: Obj): Int { + throw UnsupportedOperationException("not comparable") + } + } fun Statement.raise(text: String): Nothing { diff --git a/library/src/commonTest/kotlin/ScriptTest.kt b/library/src/commonTest/kotlin/ScriptTest.kt index a4fb708..da8415b 100644 --- a/library/src/commonTest/kotlin/ScriptTest.kt +++ b/library/src/commonTest/kotlin/ScriptTest.kt @@ -197,5 +197,36 @@ class ScriptTest { assertEquals(24, eval("(2 + 3) * 5 -1").toInt()) } + @Test + fun testEqNeq() = runTest { + assertEquals(ObjBool(true), eval("val x = 2; x == 2")) + assertEquals(ObjBool(false), eval("val x = 3; x == 2")) + assertEquals(ObjBool(true), eval("val x = 3; x != 2")) + assertEquals(ObjBool(false), eval("val x = 3; x != 3")) + + assertTrue { eval("1 == 1").toBool() } + assertTrue { eval("true == true").toBool() } + assertTrue { eval("true != false").toBool() } + assertFalse { eval("true == false").toBool() } + assertFalse { eval("false != false").toBool() } + + assertTrue { eval("2 == 2 && 3 != 4").toBool() } + } + + @Test + fun testGtLt() = runTest { + assertTrue { eval("3 > 2").toBool() } + assertFalse { eval("3 > 3").toBool() } + assertTrue { eval("3 >= 2").toBool() } + assertFalse { eval("3 >= 4").toBool() } + assertFalse { eval("3 < 2").toBool() } + assertFalse { eval("3 <= 2").toBool() } + assertTrue { eval("3 <= 3").toBool()} + assertTrue { eval("3 <= 4").toBool()} + assertTrue { eval("3 < 4").toBool()} + assertFalse { eval("4 < 3").toBool()} + assertFalse { eval("4 <= 3").toBool()} + } + } \ No newline at end of file