comparison operators + some optimizations

This commit is contained in:
Sergey Chernov 2025-05-19 11:16:46 +04:00
parent 07a4c4c8d0
commit 95e68d6e2a
7 changed files with 187 additions and 36 deletions

View File

@ -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<Map<String, Operator>> = (0..< lastLevel).map { l ->
val byLevel: List<Map<Token.Type, Operator>> = (0..<lastLevel).map { l ->
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("<eval>", code))
}
}
suspend fun eval(code: String) = Compiler.compile(code).execute()
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))
)
}
}

View File

@ -5,14 +5,36 @@ import kotlinx.serialization.Serializable
import kotlin.math.floor
@Serializable
sealed class Obj {
sealed class Obj : Comparable<Obj> {
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 <reified T> 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 <reified T> 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")
}
}

View File

@ -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()

View File

@ -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 {

View File

@ -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)
}

View File

@ -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 {

View File

@ -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()}
}
}