From 1f2afbbe386b1efda2e891b370e4267829d62347 Mon Sep 17 00:00:00 2001 From: sergeych Date: Sun, 18 May 2025 11:55:57 +0400 Subject: [PATCH] +namespaces with direct resolution +national chars in ids --- .../kotlin/net/sergeych/ling/Arguments.kt | 9 +- .../net/sergeych/ling/BuildDoubleFromParts.kt | 19 +++ .../kotlin/net/sergeych/ling/Compiler.kt | 132 +++++++++++------- .../kotlin/net/sergeych/ling/Context.kt | 57 +++++--- .../kotlin/net/sergeych/ling/Obj.kt | 49 ++++++- .../kotlin/net/sergeych/ling/Parser.kt | 9 +- .../kotlin/net/sergeych/ling/Pos.kt | 11 +- .../kotlin/net/sergeych/ling/Script.kt | 21 ++- .../kotlin/net/sergeych/ling/statements.kt | 15 +- library/src/commonTest/kotlin/ScriptTest.kt | 34 ++--- 10 files changed, 240 insertions(+), 116 deletions(-) create mode 100644 library/src/commonMain/kotlin/net/sergeych/ling/BuildDoubleFromParts.kt diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Arguments.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Arguments.kt index bcaaf00..e4e03ae 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Arguments.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Arguments.kt @@ -1,10 +1,15 @@ package net.sergeych.ling -data class Arguments(val list: List ) { +data class Arguments(val list: List ) { val size by list::size - operator fun get(index: Int): Statement = list[index] + operator fun get(index: Int): Obj = list[index] + + fun firstAndOnly(): Obj { + if( list.size != 1 ) throw IllegalArgumentException("Expected one argument, got ${list.size}") + return list.first() + } companion object { val EMPTY = Arguments(emptyList()) diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/BuildDoubleFromParts.kt b/library/src/commonMain/kotlin/net/sergeych/ling/BuildDoubleFromParts.kt new file mode 100644 index 0000000..edcc0cf --- /dev/null +++ b/library/src/commonMain/kotlin/net/sergeych/ling/BuildDoubleFromParts.kt @@ -0,0 +1,19 @@ +package net.sergeych.ling + +//fun buildDoubleFromParts( +// integerPart: Long, +// decimalPart: Long, +// exponent: Int +//): Double { +// // Handle zero decimal case efficiently +// val numDecimalDigits = if (decimalPart == 0L) 0 else decimalPart.toString().length +// +// // Calculate decimal multiplier (10^-digits) +// val decimalMultiplier = 10.0.pow(-numDecimalDigits) +// +// // Combine integer and decimal parts +// val baseValue = integerPart.toDouble() + decimalPart.toDouble() * decimalMultiplier +// +// // Apply exponent +// return baseValue * 10.0.pow(exponent) +//} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt index a852ed9..991d150 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt @@ -1,27 +1,25 @@ package net.sergeych.ling -import kotlin.math.pow - -sealed class ObjType(name: String, val defaultValue: Obj? = null) { - - class Str : ObjType("string", ObjString("")) - class Int : ObjType("real", ObjReal(0.0)) - -} - -/** - * Descriptor for whatever object could be used as argument, return value, - * field, etc. - */ -data class ObjDescriptor( - val type: ObjType, - val mutable: Boolean, -) - -data class MethodDescriptor( - val args: Array, - val result: ObjDescriptor -) +//sealed class ObjType(name: String, val defaultValue: Obj? = null) { +// +// class Str : ObjType("string", ObjString("")) +// class Int : ObjType("real", ObjReal(0.0)) +// +//} +// +///** +// * Descriptor for whatever object could be used as argument, return value, +// * field, etc. +// */ +//data class ObjDescriptor( +// val type: ObjType, +// val mutable: Boolean, +//) +// +//data class MethodDescriptor( +// val args: Array, +// val result: ObjDescriptor +//) /* Meta context contains map of symbols. @@ -84,6 +82,7 @@ class Compiler { // try keyword statement parseKeywordStatement(t, tokens) ?: run { + tokens.previous() parseExpression(tokens) } } @@ -130,6 +129,15 @@ class Compiler { val t = tokens.next() // todoL var? return when (t.type) { + Token.Type.ID -> { + parseVarAccess(t, tokens) + } + // 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)) } @@ -155,17 +163,63 @@ class Compiler { } + fun parseVarAccess(id: Token, tokens: ListIterator,path: List = emptyList()): Statement { + val nt = tokens.next() - fun parseLong(tokens: ListIterator): Long = - // todo: hex notation? - getLong(tokens) - - fun getLong(tokens: ListIterator): Long { - val t = tokens.next() - if (t.type != Token.Type.INT) throw ScriptError(t.pos, "expected number here") - return t.value.toLong() + fun resolve(context: Context): Context { + var targetContext = context + for( n in path) { + val x = targetContext[n] ?: throw ScriptError(id.pos, "undefined symbol: $n") + (x.value as? ObjNamespace )?.let { targetContext = it.context } + ?: throw ScriptError(id.pos, "Invalid symbolic path (wrong type of ${x.name}: ${x.value}") + } + return targetContext + } + return when(nt.type) { + Token.Type.DOT -> { + // selector + val t = tokens.next() + if( t.type== Token.Type.ID) { + parseVarAccess(t,tokens,path+id.value) + } + else + throw ScriptError(t.pos,"Expected identifier after '.'") + } + Token.Type.LPAREN -> { + // Load arg list + val args = mutableListOf() + do { + val t = tokens.next() + when(t.type) { + Token.Type.RPAREN, Token.Type.COMMA -> {} + else -> { + tokens.previous() + parseExpression(tokens)?.let { args += it } + ?: throw ScriptError(t.pos, "Expecting arguments list") + } + } + } while (t.type != Token.Type.RPAREN) + statement(id.pos) { context -> + val v = resolve(context).get(id.value) ?: throw ScriptError(id.pos, "Undefined variable: $id") + (v.value as? Statement)?.execute(context.copy(Arguments(args.map { it.execute(context) }))) + ?: throw ScriptError(id.pos, "Variable $id is not callable ($id)") + } + } + Token.Type.LBRACKET -> { + TODO("indexing") + } + else -> { + // just access the var + tokens.previous() + statement(id.pos) { + val v = resolve(it).get(id.value) ?: throw ScriptError(id.pos, "Undefined variable: $id") + v.value ?: throw ScriptError(id.pos, "Variable $id is not initialized") + } + } + } } + fun parseNumber(isPlus: Boolean, tokens: ListIterator): Obj { val t = tokens.next() return when (t.type) { @@ -243,22 +297,4 @@ class Compiler { } } -fun buildDoubleFromParts( - integerPart: Long, - decimalPart: Long, - exponent: Int -): Double { - // Handle zero decimal case efficiently - val numDecimalDigits = if (decimalPart == 0L) 0 else decimalPart.toString().length - - // Calculate decimal multiplier (10^-digits) - val decimalMultiplier = 10.0.pow(-numDecimalDigits) - - // Combine integer and decimal parts - val baseValue = integerPart.toDouble() + decimalPart.toDouble() * decimalMultiplier - - // Apply exponent - return baseValue * 10.0.pow(exponent) -} - suspend fun eval(code: String) = Compiler.compile(code).execute() \ No newline at end of file diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt index 4ba7d37..66da831 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt @@ -1,15 +1,13 @@ package net.sergeych.ling -import kotlin.math.PI - class Context( - val callerPos: Pos, val parent: Context? = null, val args: Arguments = Arguments.EMPTY ) { data class Item( - val name: String, var value: Obj?, + val name: String, + var value: Obj?, val isMutable: Boolean = false ) @@ -17,30 +15,51 @@ class Context( operator fun get(name: String): Item? = objects[name] ?: parent?.get(name) - fun copy(from: Pos, args: Arguments = Arguments.EMPTY): Context = Context(from, this, args) + fun copy(args: Arguments = Arguments.EMPTY): Context = Context(this, args) - fun addFn(name: String, fn: suspend Context.() -> Obj) = objects.put(name, Item(name, - object : Statement() { + fun addItem(name: String, isMutable: Boolean, value: Obj?) { + objects.put(name, Item(name, value, isMutable)) + } + + fun getOrCreateNamespace(name: String) = + (objects.getOrPut(name) { Item(name, ObjNamespace(name,copy()), isMutable = false) }.value as ObjNamespace) + .context + + inline fun addFn(vararg names: String, crossinline fn: suspend Context.() -> T) { + val newFn = object : Statement() { override val pos: Pos = Pos.builtIn override suspend fun execute(context: Context): Obj { return try { - context.fn() + from(context.fn()) + } catch (e: Exception) { raise(e.message ?: "unexpected error") } } + } + for (name in names) { + addItem( + name, + false, + newFn + ) + } + } - }) - ) + inline fun addConst(value: T,vararg names: String) { + val obj = Obj.from(value) + for (name in names) { + addItem( + name, + false, + obj + ) + } + } + + companion object { + operator fun invoke() = Context() + } } - -val basicContext = Context(Pos.builtIn).apply { - addFn("println") { - require(args.size == 1) - println(args[0].execute(this).asStr.value) - Void - } - addFn("π") { ObjReal(PI) } -} \ 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 5f2ed5e..df98b20 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt @@ -9,13 +9,39 @@ sealed class Obj { open val asStr: ObjString by lazy { if( this is ObjString) this else ObjString(this.toString()) } + + companion object { + inline fun from(obj: T): Obj { + return when(obj) { + is Obj -> obj + is Double -> ObjReal(obj) + is Float -> ObjReal(obj.toDouble()) + is Int -> ObjInt(obj.toLong()) + is Long -> ObjInt(obj) + is String -> ObjString(obj) + is CharSequence -> ObjString(obj.toString()) + is Boolean -> ObjBool(obj) + Unit -> ObjVoid + null -> ObjNull + else -> throw IllegalArgumentException("cannot convert to Obj: $obj") + } + } + } } @Serializable @SerialName("void") -object Void: Obj() { +object ObjVoid: Obj() { override fun equals(other: Any?): Boolean { - return other is Void || other is Unit + return other is ObjVoid || other is Unit + } +} + +@Serializable +@SerialName("null") +object ObjNull: Obj() { + override fun equals(other: Any?): Boolean { + return other is ObjNull || other == null } } @@ -32,6 +58,19 @@ interface Numeric { val toObjReal: ObjReal } +fun Obj.toDouble(): Double = + (this as? Numeric)?.doubleValue + ?: (this as? ObjString)?.value?.toDouble() + ?: throw IllegalArgumentException("cannot convert to double $this") + +@Suppress("unused") +fun Obj.toLong(): Long = + (this as? Numeric)?.longValue + ?: (this as? ObjString)?.value?.toLong() + ?: throw IllegalArgumentException("cannot convert to double $this") + + + @Serializable @SerialName("real") data class ObjReal(val value: Double): Obj(), Numeric { @@ -57,3 +96,9 @@ data class ObjInt(val value: Long): Obj(), Numeric { data class ObjBool(val value: Boolean): Obj() { override val asStr by lazy { ObjString(value.toString()) } } + +data class ObjNamespace(val name: String,val context: Context): Obj() { + override fun toString(): String { + return "namespace ${name}" + } +} \ 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 c3a3035..1ef3a38 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt @@ -55,11 +55,12 @@ private class Parser(fromPos: Pos) { pos.back() decodeNumber(loadChars(digits), from) } - in idFirstChars -> { - // it includes first char, so from current position - Token(ch + loadChars(idNextChars), from, Token.Type.ID) + else -> { + if( ch.isLetter() || ch == '_' ) + Token(ch + loadChars(idNextChars), from, Token.Type.ID) + else + raise("can't parse token") } - else -> raise("can't parse token") } } diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Pos.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Pos.kt index 54c6654..cefdbc8 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Pos.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Pos.kt @@ -10,11 +10,9 @@ data class Pos(val source: Source, val line: Int, val column: Int) { else if( line > 0) Pos(source, line-1, source.lines[line-1].length - 1) else throw IllegalStateException("can't go back from line 0, column 0") - val currentLine: String get() = source.lines[line] + val currentLine: String get() = if( end ) "EOF" else source.lines[line] - val showSource: String by lazy { - source.lines[line] + "\n" + "-".repeat(column - 1) + "^\n" - } + val end: Boolean get() = line >= source.lines.size companion object { val builtIn = Pos(Source.builtIn, 0, 0) @@ -33,11 +31,6 @@ class MutablePos(private val from: Pos) { val end: Boolean get() = line == lines.size - fun reset(to: Pos) { - line = to.line - column = to.column - } - fun toPos(): Pos = Pos(from.source, line, column) fun advance(): Char? { diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt index c497498..4370d3b 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt @@ -1,5 +1,8 @@ package net.sergeych.ling +import kotlin.math.PI +import kotlin.math.sin + class Script( override val pos: Pos, private val statements: List = emptyList(), @@ -7,7 +10,7 @@ class Script( override suspend fun execute(context: Context): Obj { // todo: run script - var lastResult: Obj = Void + var lastResult: Obj = ObjVoid for (s in statements) { lastResult = s.execute(context) } @@ -17,6 +20,20 @@ class Script( suspend fun execute() = execute(defaultContext) companion object { - val defaultContext: Context = Context(Pos.builtIn) + val defaultContext: Context = Context().apply { + addFn("println") { + require(args.size == 1) + println(args[0].asStr.value) + ObjVoid + } + addFn("sin") { + sin(args.firstAndOnly().toDouble()) + } + val pi = ObjReal(PI) + addConst(pi, "π") + getOrCreateNamespace("Math").also { ns -> + ns.addConst(pi, "PI") + } + } } } \ No newline at end of file diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/statements.kt b/library/src/commonMain/kotlin/net/sergeych/ling/statements.kt index 5a233de..265976e 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/statements.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/statements.kt @@ -30,7 +30,7 @@ class IfStatement( if (c !is ObjBool) raise("if: condition must me boolean, got: $c") - return if (c.value) ifTrue.execute(context) else ifFalse?.execute(context) ?: Void + return if (c.value) ifTrue.execute(context) else ifFalse?.execute(context) ?: ObjVoid } } @@ -100,18 +100,7 @@ class AssignStatement(override val pos: Pos, val name: String, val value: Statem override suspend fun execute(context: Context): Obj { val variable = context[name] ?: raise("can't assign: variable does not exist: $name") variable.value = value.execute(context) - return Void + return ObjVoid } } -class CallStatement( - override val pos: Pos, - val name: String, - val args: Arguments = Arguments.EMPTY -) : Statement() { - override suspend fun execute(context: Context): Obj { - val callee = context[name] ?: raise("Call: unknown name: $name") - return (callee.value as? Statement)?.execute(context.copy(pos, args)) - ?: raise("Call: not a callable object: $callee") - } -} \ No newline at end of file diff --git a/library/src/commonTest/kotlin/ScriptTest.kt b/library/src/commonTest/kotlin/ScriptTest.kt index 4e6f141..d509a7e 100644 --- a/library/src/commonTest/kotlin/ScriptTest.kt +++ b/library/src/commonTest/kotlin/ScriptTest.kt @@ -2,28 +2,14 @@ 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 class ScriptTest { - @Test - fun level0() = runTest { - val s = Script( - Pos.builtIn, - listOf( - CallStatement( - Pos.builtIn, "println", - Arguments(listOf(CallStatement(Pos.builtIn, "π", Arguments.EMPTY))) - ) - ) - ) - s.execute(basicContext) - } - - fun parseFirst(str: String): Token = - parseLing(str.toSource()).firstOrNull()!! - @Test fun parseNumbers() { fun check(expected: String, type: Token.Type, row: Int, col: Int, src: String, offset: Int = 0) { @@ -107,6 +93,20 @@ class ScriptTest { // assertEquals(ObjReal(3.14), eval("3.14")) assertEquals(ObjReal(314.0), eval("3.14e2")) + assertEquals(ObjReal(314.0), eval("100 3.14e2")) + assertEquals(ObjReal(314.0), eval("100\n 3.14e2")) + } + + @Test + fun compileBuiltinCalls() = runTest { +// println(eval("π")) + val pi = eval("Math.PI") + assertIs(pi) + assertTrue(pi.value - PI < 0.000001) + assertTrue(eval("Math.PI+1").toDouble() - PI - 1.0 < 0.000001) + + assertTrue(eval("sin(Math.PI)").toDouble() - 1 < 0.000001) + assertTrue(eval("sin(π)").toDouble() - 1 < 0.000001) } } \ No newline at end of file