From 408543d1918f6d8ede3c6b8b22fb422bf02c290a Mon Sep 17 00:00:00 2001 From: sergeych Date: Sun, 18 May 2025 12:49:45 +0400 Subject: [PATCH] +var and val --- .../kotlin/net/sergeych/ling/Compiler.kt | 94 +++++++++++++------ .../kotlin/net/sergeych/ling/Context.kt | 7 +- .../kotlin/net/sergeych/ling/Obj.kt | 1 + .../kotlin/net/sergeych/ling/Parser.kt | 9 +- .../kotlin/net/sergeych/ling/Script.kt | 2 +- .../kotlin/net/sergeych/ling/statements.kt | 2 + library/src/commonTest/kotlin/ScriptTest.kt | 31 +++++- 7 files changed, 108 insertions(+), 38 deletions(-) diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt index c0086fb..bd097d8 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt @@ -61,38 +61,42 @@ class Compiler { } private fun parseStatement(tokens: ListIterator): Statement? { - val t = tokens.next() - return when (t.type) { - Token.Type.ID -> { - // could be keyword, assignment or just the expression - val next = tokens.next() - if (next.type == Token.Type.EQ) { - // this _is_ assignment statement - AssignStatement( - t.pos, t.value, - parseExpression(tokens) ?: throw ScriptError( - t.pos, - "Expecting expression for assignment operator" + while(true) { + val t = tokens.next() + return when (t.type) { + Token.Type.ID -> { + // could be keyword, assignment or just the expression + val next = tokens.next() + if (next.type == Token.Type.ASSIGN) { + // this _is_ assignment statement + return AssignStatement( + t.pos, t.value, + parseExpression(tokens) ?: throw ScriptError( + t.pos, + "Expecting expression for assignment operator" + ) ) - ) - } - // not assignment, maybe keyword statement: - // get back the token which is not '=': - tokens.previous() - // try keyword statement - parseKeywordStatement(t, tokens) - ?: run { - tokens.previous() - parseExpression(tokens) } - } + // not assignment, maybe keyword statement: + // get back the token which is not '=': + tokens.previous() + // try keyword statement + parseKeywordStatement(t, tokens) + ?: run { + tokens.previous() + parseExpression(tokens) + } + } - Token.Type.EOF -> null + Token.Type.SEMICOLON -> continue - else -> { - // could be expression - tokens.previous() - parseExpression(tokens) + Token.Type.EOF -> null + + else -> { + // could be expression + tokens.previous() + parseExpression(tokens) + } } } } @@ -243,9 +247,37 @@ class Compiler { * Parse keyword-starting statenment. * @return parsed statement or null if, for example. [id] is not among keywords */ - @Suppress("UNUSED_PARAMETER") - private fun parseKeywordStatement(id: Token, tokens: ListIterator): Statement? { - return null + private fun parseKeywordStatement(id: Token, tokens: ListIterator): Statement? + = when (id.value) { + "val" -> parseVarDeclaration(id.value, false, tokens) + "var" -> parseVarDeclaration(id.value, true, tokens) + else -> null + } + + private fun parseVarDeclaration(kind: String, mutable: Boolean, tokens: ListIterator): Statement { + val nameToken = tokens.next() + if( nameToken.type != Token.Type.ID) + throw ScriptError(nameToken.pos, "Expected identifier after '$kind'") + val name = nameToken.value + val eqToken = tokens.next() + var setNull = false + if( eqToken.type != Token.Type.ASSIGN) { + if( !mutable ) + throw ScriptError(eqToken.pos, "Expected initializator: '=' after '$kind ${name}'") + else { + tokens.previous() + setNull = true + } + } + val initialExpression = if( setNull ) null else parseExpression(tokens) + ?: throw ScriptError(eqToken.pos, "Expected initializer expression") + return statement(nameToken.pos) { context -> + if( context.containsLocal(name) ) + throw ScriptError(nameToken.pos, "Variable $name is already defined") + val initValue = initialExpression?.execute(context) ?: ObjNull + context.addItem(name, mutable, initValue) + ObjVoid + } } // fun parseStatement(parser: Parser): Statement? = diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt index 66da831..0291ec5 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt @@ -1,7 +1,7 @@ package net.sergeych.ling class Context( - val parent: Context? = null, + val parent: Context? = Script.defaultContext.copy(), val args: Arguments = Arguments.EMPTY ) { @@ -58,6 +58,11 @@ class Context( } } + suspend fun eval(code: String): Obj = + Compiler().compile(code.toSource()).execute(this) + + fun containsLocal(name: String): Boolean = name in objects + companion object { operator fun invoke() = Context() } diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt index df98b20..eab4fe1 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt @@ -69,6 +69,7 @@ fun Obj.toLong(): Long = ?: (this as? ObjString)?.value?.toLong() ?: throw IllegalArgumentException("cannot convert to double $this") +fun Obj.toInt(): Int = toLong().toInt() @Serializable diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt index 1ef3a38..c40b255 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt @@ -41,7 +41,14 @@ private class Parser(fromPos: Pos) { ']' -> Token("]", from, Token.Type.RBRACKET) ',' -> Token(",", from, Token.Type.COMMA) ';' -> Token(";", from, Token.Type.SEMICOLON) - '=' -> Token("=", from, Token.Type.ASSIGN) + '=' -> { + if( pos.currentChar == '=') { + advance() + Token("==", from, Token.Type.EQ) + } + else + Token("=", from, Token.Type.ASSIGN) + } '+' -> Token("+", from, Token.Type.PLUS) '-' -> Token("-", from, Token.Type.MINUS) '*' -> Token("*", from, Token.Type.STAR) diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt index 4370d3b..30760b6 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt @@ -20,7 +20,7 @@ class Script( suspend fun execute() = execute(defaultContext) companion object { - val defaultContext: Context = Context().apply { + val defaultContext: Context = Context(null).apply { addFn("println") { require(args.size == 1) println(args[0].asStr.value) diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/statements.kt b/library/src/commonMain/kotlin/net/sergeych/ling/statements.kt index b58171c..194db4d 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/statements.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/statements.kt @@ -87,6 +87,8 @@ class MinusStatement( 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") variable.value = value.execute(context) return ObjVoid } diff --git a/library/src/commonTest/kotlin/ScriptTest.kt b/library/src/commonTest/kotlin/ScriptTest.kt index d509a7e..cfc094c 100644 --- a/library/src/commonTest/kotlin/ScriptTest.kt +++ b/library/src/commonTest/kotlin/ScriptTest.kt @@ -3,10 +3,7 @@ package io.github.kotlin.fibonacci import kotlinx.coroutines.test.runTest import net.sergeych.ling.* import kotlin.math.PI -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertIs -import kotlin.test.assertTrue +import kotlin.test.* class ScriptTest { @@ -109,4 +106,30 @@ class ScriptTest { assertTrue(eval("sin(π)").toDouble() - 1 < 0.000001) } + @Test + fun varsAndConsts() = runTest { + val context = Context() + assertEquals(ObjVoid,context.eval(""" + val a = 17 + var b = 3 + """.trimIndent())) + assertEquals(17, context.eval("a").toInt()) + assertEquals(20, context.eval("b + a").toInt()) + assertFailsWith { + context.eval("a = 10") + } + assertEquals(10, context.eval("b = a - 3 - 4; b").toInt()) + assertEquals(10, context.eval("b").toInt()) + } + +// @Test +// fun functionTest() = runTest { +// val context = Context() +// context.eval(""" +// fun foo(a, b) { +// a + b +// } +// """.trimIndent()) +// } + } \ No newline at end of file