From 3dd98131e7f2d2585d9b75b43d8ae6dc1b670a3e Mon Sep 17 00:00:00 2001 From: sergeych Date: Sun, 18 May 2025 18:32:59 +0400 Subject: [PATCH] parentheses, more operators, some docs --- README.md | 2 + docs/math.md | 56 +++++++++++ .../kotlin/net/sergeych/ling/Compiler.kt | 53 +++++----- .../kotlin/net/sergeych/ling/Obj.kt | 3 + .../kotlin/net/sergeych/ling/Parser.kt | 55 +++++++---- .../kotlin/net/sergeych/ling/Script.kt | 18 +++- .../kotlin/net/sergeych/ling/Token.kt | 3 +- .../kotlin/net/sergeych/ling/statements.kt | 99 +++++++++++++++++-- library/src/commonTest/kotlin/ScriptTest.kt | 35 +++++++ 9 files changed, 266 insertions(+), 58 deletions(-) create mode 100644 docs/math.md diff --git a/README.md b/README.md index 731827d..a2f4d32 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ in the form of multiplatform library. +__current state of implementation and docs__: [docs/math.md]. + ## Why? Designed to add scripting to kotlin multiplatform application in easy and efficient way. This is attempt to achieve what Lua is for C/++. diff --git a/docs/math.md b/docs/math.md new file mode 100644 index 0000000..f0228d1 --- /dev/null +++ b/docs/math.md @@ -0,0 +1,56 @@ +# Operators + +## Precedence + +Same as in C++. + +| Priority | Operations | +|:----------------:|--------------------------------------| +| **Highest**
0 | power, not, calls, indexing, dot,... | +| 1 | `%` `*` `/` | +| 2 | `+` `-` | +| 3 | bit shifts (NI) | +| 4 | `<=>` (NI) | +| 5 | `<=` `>=` `<` `>` (NI) | +| 6 | `==` `!=` (NI) | +| 7 | `&` (NI) | +| 9 | `\|` (NI) | +| 10 | `&&` | +| 11
lowest | `\|\|` | + +- (NI) stands for not yet implemented. + +## Operators + +`+ - * / % `: if both operand is `Int`, calculates as int. Otherwise, as real. + +## Round and range + +The following functions return its argument if it is `Int`, +or transformed `Real` otherwise. + +| name | description | +|----------|--------------------------------------------------------| +| floor(x) | Computes the largest integer value not greater than x | +| ceil(x) | Computes the least integer value value not less than x | +| round(x) | Rounds x | +| | | +| | | + +## Scientific functions + +| name | meaning | +|---------------------|---------| +| `sin(x:Real): Real` | sine | +| | | +| | | +| | | + +## Scientific constant + +| name | meaning | +|----------------------|--------------| +| `Math.PI: Real` or π | 3.1415926... | +| | | +| | | +| | | diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt index dc94254..86baa3a 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt @@ -139,32 +139,34 @@ class Compiler { // todoL var? return when (t.type) { Token.Type.ID -> { - parseVarAccess(t, tokens) + when (t.value) { + "void" -> statement(t.pos, true) { ObjVoid } + "null" -> statement(t.pos, true) { ObjNull } + else -> parseVarAccess(t, tokens) + } + } + Token.Type.LPAREN -> { + // ( subexpr ) + parseExpression(tokens)?.also { + val tl = tokens.next() + if( tl.type != Token.Type.RPAREN ) + throw ScriptError(t.pos, "unbalanced parenthesis: no ')' for it") + } } - // todoL: check if it's a function call - // todoL: check if it's a field access - // todoL: check if it's a var - // todoL: check if it's a const - // todoL: check if it's a type - -// "+" -> statement { parseNumber(true,tokens) }?????? -// "-" -> statement { parseNumber(false,tokens) } -// "~" -> statement(t.pos) { ObjInt( parseLong(tokens)) } - Token.Type.PLUS -> { val n = parseNumber(true, tokens) - statement(t.pos) { n } + statement(t.pos, true) { n } } Token.Type.MINUS -> { val n = parseNumber(false, tokens) - statement(t.pos) { n } + statement(t.pos, true) { n } } Token.Type.INT, Token.Type.REAL, Token.Type.HEX -> { tokens.previous() val n = parseNumber(true, tokens) - statement(t.pos) { n } + statement(t.pos, true) { n } } else -> null @@ -211,7 +213,7 @@ class Compiler { } while (t.type != Token.Type.RPAREN) statement(id.pos) { context -> - val v = resolve(context).get(id.value) ?: throw ScriptError(id.pos, "Undefined variable: $id") + val v = resolve(context).get(id.value) ?: throw ScriptError(id.pos, "Undefined function: ${id.value}") (v.value as? Statement)?.execute( context.copy( Arguments( @@ -232,7 +234,7 @@ class Compiler { // just access the var tokens.previous() statement(id.pos) { - val v = resolve(it).get(id.value) ?: throw ScriptError(id.pos, "Undefined variable: $id") + val v = resolve(it).get(id.value) ?: throw ScriptError(id.pos, "Undefined variable: ${id.value}") v.value ?: throw ScriptError(id.pos, "Variable $id is not initialized") } } @@ -393,27 +395,26 @@ class Compiler { ) val allOps = listOf( -// Operator("||", 0, 2) { pos, a, b -> LogicalOrStatement(pos, a, b) }) - Operator("&&", 1, 2) { pos, a, b -> - LogicalAndStatement(pos, a, b) - }, + Operator("||", 0, 2) { pos, a, b -> LogicalOrStatement(pos, a, b) }, + Operator("&&", 1, 2) { pos, a, b -> LogicalAndStatement(pos, a, b) }, // bitwise or 2 // bitwise and 3 // equality/ne 4 // relational <=,... 5 // shuttle <=> 6 // bitshhifts 7 - // + - : 7 - Operator("+", 7, 2) { pos, a, b -> + Operator("+", 8, 2) { pos, a, b -> PlusStatement(pos, a, b) }, - Operator("-", 7, 2) { pos, a, b -> + Operator("-", 8, 2) { pos, a, b -> MinusStatement(pos, a, b) }, - // * / %: 8 + 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) }, ) - val lastLevel = 9 - val byLevel: List> = (0.. + val lastLevel = 10 + val byLevel: List> = (0..< lastLevel).map { l -> allOps.filter { it.priority == l } .map { it.name to it }.toMap() } diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt index eab4fe1..1202b23 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt @@ -29,6 +29,9 @@ sealed class Obj { } } +@Suppress("unused") +inline fun T.toObj(): Obj = Obj.from(this) + @Serializable @SerialName("void") object ObjVoid: Obj() { diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt index 3604583..5b8813d 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt @@ -13,7 +13,7 @@ fun parseLing(source: Source): List { do { val t = p.nextToken() tokens += t - } while(t.type != Token.Type.EOF) + } while (t.type != Token.Type.EOF) return tokens } @@ -42,28 +42,45 @@ private class Parser(fromPos: Pos) { ',' -> Token(",", from, Token.Type.COMMA) ';' -> Token(";", from, Token.Type.SEMICOLON) '=' -> { - if( pos.currentChar == '=') { + if (pos.currentChar == '=') { advance() Token("==", from, Token.Type.EQ) - } - else + } else Token("=", from, Token.Type.ASSIGN) } + '+' -> Token("+", from, Token.Type.PLUS) '-' -> Token("-", from, Token.Type.MINUS) '*' -> Token("*", from, Token.Type.STAR) '/' -> 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.OR) + } else + Token("|", from, Token.Type.BITOR) + } + '&' -> { + if (currentChar == '&') { + advance() + Token("&&", from, Token.Type.AND) + } else + Token("&", from, Token.Type.BITAND) + } + '"' -> loadStringToken() in digits -> { pos.back() decodeNumber(loadChars(digits), from) } + else -> { - if( ch.isLetter() || ch == '_' ) + if (ch.isLetter() || ch == '_') Token(ch + loadChars(idNextChars), from, Token.Type.ID) else raise("can't parse token") @@ -72,46 +89,43 @@ private class Parser(fromPos: Pos) { } private fun decodeNumber(p1: String, start: Pos): Token = - if( pos.end ) + if (pos.end) Token(p1, start, Token.Type.INT) - else if( currentChar == '.' ) { + else if (currentChar == '.') { // could be decimal advance() - if( currentChar in digits ) { + if (currentChar in digits) { // decimal part val p2 = loadChars(digits) // with exponent? - if( currentChar == 'e' || currentChar == 'E') { + if (currentChar == 'e' || currentChar == 'E') { advance() var negative = false - if(currentChar == '+' ) + if (currentChar == '+') advance() - else if(currentChar == '-') { + else if (currentChar == '-') { negative = true advance() } var p3 = loadChars(digits) - if( negative ) p3 = "-$p3" + if (negative) p3 = "-$p3" Token("$p1.${p2}e$p3", start, Token.Type.REAL) - } - else { + } else { // no exponent Token("$p1.$p2", start, Token.Type.REAL) } - } - else { + } else { // not decimal // something like 10.times, method call on integer number pos.back() Token(p1, start, Token.Type.INT) } - } - else { + } else { // could be integer, also hex: if (currentChar == 'x' && p1 == "0") { advance() Token(loadChars(hexDigits), start, Token.Type.HEX).also { - if( currentChar.isLetter() ) + if (currentChar.isLetter()) raise("invalid hex literal") } } else { @@ -130,7 +144,7 @@ private class Parser(fromPos: Pos) { val sb = StringBuilder() while (currentChar != '"') { - if( pos.end ) raise("unterminated string") + if (pos.end) raise("unterminated string") when (currentChar) { '\\' -> { advance() ?: raise("unterminated string") @@ -142,6 +156,7 @@ private class Parser(fromPos: Pos) { else -> sb.append('\\').append(currentChar) } } + else -> { sb.append(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 30760b6..e30c129 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt @@ -1,7 +1,6 @@ package net.sergeych.ling -import kotlin.math.PI -import kotlin.math.sin +import kotlin.math.* class Script( override val pos: Pos, @@ -26,6 +25,21 @@ class Script( println(args[0].asStr.value) ObjVoid } + addFn("floor") { + val x = args.firstAndOnly() + if( x is ObjInt ) x + else ObjReal(floor(x.toDouble())) + } + addFn("ceil") { + val x = args.firstAndOnly() + if( x is ObjInt ) x + else ObjReal(ceil(x.toDouble())) + } + addFn("round") { + val x = args.firstAndOnly() + if( x is ObjInt ) x + else ObjReal(round(x.toDouble())) + } addFn("sin") { sin(args.firstAndOnly().toDouble()) } diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt index 7ee07c5..b02558d 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Token.kt @@ -4,7 +4,8 @@ data class Token(val value: String, val pos: Pos, val type: Type) { 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, OR, NOT, DOT, ARROW, QUESTION, COLONCOLON, EOF, + GTEQ, 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 194db4d..8e9a06f 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/statements.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/statements.kt @@ -3,7 +3,9 @@ package net.sergeych.ling fun String.toSource(name: String = "eval"): Source = Source(name, this) -abstract class Statement : Obj() { +abstract class Statement( + @Suppress("unused") val isStaticConst: Boolean = false +) : Obj() { abstract val pos: Pos abstract suspend fun execute(context: Context): Obj } @@ -17,10 +19,11 @@ fun Statement.require(cond: Boolean, message: () -> String) { if (!cond) raise(message()) } -fun statement(pos: Pos, f: suspend (Context) -> Obj): Statement = object : Statement() { - override val pos: Pos = pos - override suspend fun execute(context: Context): Obj = f(context) -} +fun statement(pos: Pos, isStaticConst: Boolean = false, f: suspend (Context) -> Obj): Statement = + object : Statement(isStaticConst) { + override val pos: Pos = pos + override suspend fun execute(context: Context): Obj = f(context) + } class LogicalAndStatement( override val pos: Pos, @@ -29,15 +32,31 @@ class LogicalAndStatement( override suspend fun execute(context: Context): Obj { val l = left.execute(context).let { - (it as? ObjBool) ?: raise("logical and: left operand is not boolean: $it") + (it as? ObjBool) ?: raise("left operand is not boolean: $it") } val r = right.execute(context).let { - (it as? ObjBool) ?: raise("logical and: right operand is not boolean: $it") + (it as? ObjBool) ?: raise("right operand is not boolean: $it") } return ObjBool(l.value && r.value) } } +class LogicalOrStatement( + override val pos: Pos, + val left: Statement, val right: Statement +) : Statement() { + override suspend fun execute(context: Context): Obj { + + val l = left.execute(context).let { + (it as? ObjBool) ?: raise("left operand is not boolean: $it") + } + val r = right.execute(context).let { + (it as? ObjBool) ?: raise("right operand is not boolean: $it") + } + return ObjBool(l.value || r.value) + } +} + class PlusStatement( override val pos: Pos, val left: Statement, val right: Statement @@ -84,11 +103,73 @@ class MinusStatement( } } +class MulStatement( + override val pos: Pos, + val left: Statement, val right: Statement +) : Statement() { + override suspend fun execute(context: Context): Obj { + val l = left.execute(context) + if (l !is Numeric) + raise("left operand is not number: $l") + + val r = right.execute(context) + if (r !is Numeric) + raise("right operand is not number: $r") + + return if (l is ObjInt && r is ObjInt) + ObjInt(l.longValue * r.longValue) + else + ObjReal(l.doubleValue * r.doubleValue) + } +} + +class DivStatement( + override val pos: Pos, + val left: Statement, val right: Statement +) : Statement() { + override suspend fun execute(context: Context): Obj { + val l = left.execute(context) + if (l !is Numeric) + raise("left operand is not number: $l") + + val r = right.execute(context) + if (r !is Numeric) + raise("right operand is not number: $r") + + return if (l is ObjInt && r is ObjInt) + ObjInt(l.longValue / r.longValue) + else + ObjReal(l.doubleValue / r.doubleValue) + } +} + +class ModStatement( + override val pos: Pos, + val left: Statement, val right: Statement +) : Statement() { + override suspend fun execute(context: Context): Obj { + val l = left.execute(context) + if (l !is Numeric) + raise("left operand is not number: $l") + + val r = right.execute(context) + if (r !is Numeric) + raise("right operand is not number: $r") + + return if (l is ObjInt && r is ObjInt) + ObjInt(l.longValue % r.longValue) + else + ObjReal(l.doubleValue % r.doubleValue) + } +} + + + class AssignStatement(override val pos: Pos, val name: String, val value: Statement) : Statement() { override suspend fun execute(context: Context): Obj { val variable = context[name] ?: raise("can't assign: variable does not exist: $name") - if( !variable.isMutable ) - throw ScriptError(pos,"can't reassign val $name") + if (!variable.isMutable) + throw ScriptError(pos, "can't reassign val $name") variable.value = value.execute(context) return ObjVoid } diff --git a/library/src/commonTest/kotlin/ScriptTest.kt b/library/src/commonTest/kotlin/ScriptTest.kt index c952095..a4fb708 100644 --- a/library/src/commonTest/kotlin/ScriptTest.kt +++ b/library/src/commonTest/kotlin/ScriptTest.kt @@ -163,4 +163,39 @@ class ScriptTest { assertEquals(37, context.eval("foo(3,14)").toInt()) } + @Test + fun nullAndVoidTest() = runTest { + val context = Context() + assertEquals(ObjVoid,context.eval("void")) + assertEquals(ObjNull,context.eval("null")) + } + + @Test + fun testArithmeticOperators() = runTest { + assertEquals(2, eval("5/2").toInt()) + assertEquals(2.5, eval("5.0/2").toDouble()) + assertEquals(2.5, eval("5/2.0").toDouble()) + assertEquals(2.5, eval("5.0/2.0").toDouble()) + + assertEquals(1, eval("5%2").toInt()) + assertEquals(1.0, eval("5.0%2").toDouble()) + + assertEquals(77, eval("11 * 7").toInt()) + + assertEquals(2.0, eval("floor(5.0/2)").toDouble()) + assertEquals(3, eval("ceil(5.0/2)").toInt()) + + assertEquals(2.0, eval("round(4.7/2)").toDouble()) + assertEquals(3.0, eval("round(5.1/2)").toDouble()) + } + + @Test + fun testArithmeticParenthesis() = runTest { + assertEquals(17, eval("2 + 3 * 5").toInt()) + assertEquals(17, eval("2 + (3 * 5)").toInt()) + assertEquals(25, eval("(2 + 3) * 5").toInt()) + assertEquals(24, eval("(2 + 3) * 5 -1").toInt()) + } + + } \ No newline at end of file