Compare commits

...

2 Commits

Author SHA1 Message Date
50986fbac5 +namespaces with direct resolution
+national chars in ids
2025-05-18 11:59:45 +04:00
1f2afbbe38 +namespaces with direct resolution
+national chars in ids
2025-05-18 11:58:38 +04:00
10 changed files with 241 additions and 128 deletions

View File

@ -1,10 +1,15 @@
package net.sergeych.ling package net.sergeych.ling
data class Arguments(val list: List<Statement> ) { data class Arguments(val list: List<Obj> ) {
val size by list::size 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 { companion object {
val EMPTY = Arguments(emptyList()) val EMPTY = Arguments(emptyList())

View File

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

View File

@ -1,27 +1,25 @@
package net.sergeych.ling package net.sergeych.ling
import kotlin.math.pow //sealed class ObjType(name: String, val defaultValue: Obj? = null) {
//
sealed class ObjType(name: String, val defaultValue: Obj? = null) { // class Str : ObjType("string", ObjString(""))
// class Int : ObjType("real", ObjReal(0.0))
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.
* Descriptor for whatever object could be used as argument, return value, // */
* field, etc. //data class ObjDescriptor(
*/ // val type: ObjType,
data class ObjDescriptor( // val mutable: Boolean,
val type: ObjType, //)
val mutable: Boolean, //
) //data class MethodDescriptor(
// val args: Array<ObjDescriptor>,
data class MethodDescriptor( // val result: ObjDescriptor
val args: Array<ObjDescriptor>, //)
val result: ObjDescriptor
)
/* /*
Meta context contains map of symbols. Meta context contains map of symbols.
@ -84,6 +82,7 @@ class Compiler {
// try keyword statement // try keyword statement
parseKeywordStatement(t, tokens) parseKeywordStatement(t, tokens)
?: run { ?: run {
tokens.previous()
parseExpression(tokens) parseExpression(tokens)
} }
} }
@ -130,6 +129,15 @@ class Compiler {
val t = tokens.next() val t = tokens.next()
// todoL var? // todoL var?
return when (t.type) { 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(true,tokens) }??????
// "-" -> statement { parseNumber(false,tokens) } // "-" -> statement { parseNumber(false,tokens) }
// "~" -> statement(t.pos) { ObjInt( parseLong(tokens)) } // "~" -> statement(t.pos) { ObjInt( parseLong(tokens)) }
@ -155,17 +163,63 @@ class Compiler {
} }
fun parseVarAccess(id: Token, tokens: ListIterator<Token>,path: List<String> = emptyList()): Statement {
val nt = tokens.next()
fun parseLong(tokens: ListIterator<Token>): Long = fun resolve(context: Context): Context {
// todo: hex notation? var targetContext = context
getLong(tokens) for( n in path) {
val x = targetContext[n] ?: throw ScriptError(id.pos, "undefined symbol: $n")
fun getLong(tokens: ListIterator<Token>): Long { (x.value as? ObjNamespace )?.let { targetContext = it.context }
val t = tokens.next() ?: throw ScriptError(id.pos, "Invalid symbolic path (wrong type of ${x.name}: ${x.value}")
if (t.type != Token.Type.INT) throw ScriptError(t.pos, "expected number here") }
return t.value.toLong() 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<Statement>()
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<Token>): Obj { fun parseNumber(isPlus: Boolean, tokens: ListIterator<Token>): Obj {
val t = tokens.next() val t = tokens.next()
return when (t.type) { return when (t.type) {
@ -189,6 +243,7 @@ class Compiler {
* Parse keyword-starting statenment. * Parse keyword-starting statenment.
* @return parsed statement or null if, for example. [id] is not among keywords * @return parsed statement or null if, for example. [id] is not among keywords
*/ */
@Suppress("UNUSED_PARAMETER")
private fun parseKeywordStatement(id: Token, tokens: ListIterator<Token>): Statement? { private fun parseKeywordStatement(id: Token, tokens: ListIterator<Token>): Statement? {
return null return null
} }
@ -243,22 +298,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() suspend fun eval(code: String) = Compiler.compile(code).execute()

View File

@ -1,15 +1,13 @@
package net.sergeych.ling package net.sergeych.ling
import kotlin.math.PI
class Context( class Context(
val callerPos: Pos,
val parent: Context? = null, val parent: Context? = null,
val args: Arguments = Arguments.EMPTY val args: Arguments = Arguments.EMPTY
) { ) {
data class Item( data class Item(
val name: String, var value: Obj?, val name: String,
var value: Obj?,
val isMutable: Boolean = false val isMutable: Boolean = false
) )
@ -17,30 +15,51 @@ class Context(
operator fun get(name: String): Item? = objects[name] ?: parent?.get(name) 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, fun addItem(name: String, isMutable: Boolean, value: Obj?) {
object : Statement() { 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 <reified T> addFn(vararg names: String, crossinline fn: suspend Context.() -> T) {
val newFn = object : Statement() {
override val pos: Pos = Pos.builtIn override val pos: Pos = Pos.builtIn
override suspend fun execute(context: Context): Obj { override suspend fun execute(context: Context): Obj {
return try { return try {
context.fn() from(context.fn())
} catch (e: Exception) { } catch (e: Exception) {
raise(e.message ?: "unexpected error") raise(e.message ?: "unexpected error")
} }
} }
}
}) for (name in names) {
) addItem(
name,
} false,
newFn
val basicContext = Context(Pos.builtIn).apply { )
addFn("println") { }
require(args.size == 1)
println(args[0].execute(this).asStr.value)
Void
} }
addFn("π") { ObjReal(PI) }
inline fun <reified T> 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()
}
} }

View File

@ -9,13 +9,39 @@ sealed class Obj {
open val asStr: ObjString by lazy { open val asStr: ObjString by lazy {
if( this is ObjString) this else ObjString(this.toString()) if( this is ObjString) this else ObjString(this.toString())
} }
companion object {
inline fun <reified T> 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 @Serializable
@SerialName("void") @SerialName("void")
object Void: Obj() { object ObjVoid: Obj() {
override fun equals(other: Any?): Boolean { 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 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 @Serializable
@SerialName("real") @SerialName("real")
data class ObjReal(val value: Double): Obj(), Numeric { 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() { data class ObjBool(val value: Boolean): Obj() {
override val asStr by lazy { ObjString(value.toString()) } 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}"
}
}

View File

@ -55,11 +55,12 @@ private class Parser(fromPos: Pos) {
pos.back() pos.back()
decodeNumber(loadChars(digits), from) decodeNumber(loadChars(digits), from)
} }
in idFirstChars -> { else -> {
// it includes first char, so from current position if( ch.isLetter() || ch == '_' )
Token(ch + loadChars(idNextChars), from, Token.Type.ID) Token(ch + loadChars(idNextChars), from, Token.Type.ID)
else
raise("can't parse token")
} }
else -> raise("can't parse token")
} }
} }

View File

@ -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 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") 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 { val end: Boolean get() = line >= source.lines.size
source.lines[line] + "\n" + "-".repeat(column - 1) + "^\n"
}
companion object { companion object {
val builtIn = Pos(Source.builtIn, 0, 0) val builtIn = Pos(Source.builtIn, 0, 0)
@ -33,11 +31,6 @@ class MutablePos(private val from: Pos) {
val end: Boolean get() = val end: Boolean get() =
line == lines.size line == lines.size
fun reset(to: Pos) {
line = to.line
column = to.column
}
fun toPos(): Pos = Pos(from.source, line, column) fun toPos(): Pos = Pos(from.source, line, column)
fun advance(): Char? { fun advance(): Char? {

View File

@ -1,5 +1,8 @@
package net.sergeych.ling package net.sergeych.ling
import kotlin.math.PI
import kotlin.math.sin
class Script( class Script(
override val pos: Pos, override val pos: Pos,
private val statements: List<Statement> = emptyList(), private val statements: List<Statement> = emptyList(),
@ -7,7 +10,7 @@ class Script(
override suspend fun execute(context: Context): Obj { override suspend fun execute(context: Context): Obj {
// todo: run script // todo: run script
var lastResult: Obj = Void var lastResult: Obj = ObjVoid
for (s in statements) { for (s in statements) {
lastResult = s.execute(context) lastResult = s.execute(context)
} }
@ -17,6 +20,20 @@ class Script(
suspend fun execute() = execute(defaultContext) suspend fun execute() = execute(defaultContext)
companion object { 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")
}
}
} }
} }

View File

@ -12,6 +12,7 @@ fun Statement.raise(text: String): Nothing {
throw ScriptError(pos, text) throw ScriptError(pos, text)
} }
@Suppress("unused")
fun Statement.require(cond: Boolean, message: () -> String) { fun Statement.require(cond: Boolean, message: () -> String) {
if (!cond) raise(message()) if (!cond) raise(message())
} }
@ -21,19 +22,6 @@ fun statement(pos: Pos, f: suspend (Context) -> Obj): Statement = object : State
override suspend fun execute(context: Context): Obj = f(context) override suspend fun execute(context: Context): Obj = f(context)
} }
class IfStatement(
override val pos: Pos,
val cond: Statement, val ifTrue: Statement, val ifFalse: Statement?
) : Statement() {
override suspend fun execute(context: Context): Obj {
val c = cond.execute(context)
if (c !is ObjBool)
raise("if: condition must me boolean, got: $c")
return if (c.value) ifTrue.execute(context) else ifFalse?.execute(context) ?: Void
}
}
class LogicalAndStatement( class LogicalAndStatement(
override val pos: Pos, override val pos: Pos,
val left: Statement, val right: Statement val left: Statement, val right: Statement
@ -100,18 +88,7 @@ class AssignStatement(override val pos: Pos, val name: String, val value: Statem
override suspend fun execute(context: Context): Obj { override suspend fun execute(context: Context): Obj {
val variable = context[name] ?: raise("can't assign: variable does not exist: $name") val variable = context[name] ?: raise("can't assign: variable does not exist: $name")
variable.value = value.execute(context) 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")
}
}

View File

@ -2,28 +2,14 @@ package io.github.kotlin.fibonacci
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import net.sergeych.ling.* import net.sergeych.ling.*
import kotlin.math.PI
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertTrue
class ScriptTest { 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 @Test
fun parseNumbers() { fun parseNumbers() {
fun check(expected: String, type: Token.Type, row: Int, col: Int, src: String, offset: Int = 0) { 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(3.14), eval("3.14"))
assertEquals(ObjReal(314.0), eval("3.14e2")) 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<ObjReal>(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)
} }
} }