+namespaces with direct resolution

+national chars in ids
This commit is contained in:
Sergey Chernov 2025-05-18 11:55:57 +04:00
parent b220299fc5
commit 1f2afbbe38
10 changed files with 240 additions and 116 deletions

View File

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

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
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<ObjDescriptor>,
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<ObjDescriptor>,
// 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<Token>,path: List<String> = emptyList()): Statement {
val nt = tokens.next()
fun parseLong(tokens: ListIterator<Token>): Long =
// todo: hex notation?
getLong(tokens)
fun getLong(tokens: ListIterator<Token>): 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<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 {
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()

View File

@ -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 <reified T> 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 <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()
}
}
val basicContext = Context(Pos.builtIn).apply {
addFn("println") {
require(args.size == 1)
println(args[0].execute(this).asStr.value)
Void
}
addFn("π") { ObjReal(PI) }
}

View File

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

View File

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

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

View File

@ -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<Statement> = 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")
}
}
}
}

View File

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

View File

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