From d21544ca5db39e2c5f470afb2b501fdbe8fb9650 Mon Sep 17 00:00:00 2001 From: sergeych Date: Wed, 28 May 2025 11:44:55 +0400 Subject: [PATCH] first OO features: x::class, x.method(1,2,3) builting Real.roundToInt --- docs/OOP.md | 57 +++++++++ .../kotlin/net/sergeych/ling/Arguments.kt | 8 +- .../kotlin/net/sergeych/ling/Compiler.kt | 65 +++++++++-- .../kotlin/net/sergeych/ling/Context.kt | 8 +- .../kotlin/net/sergeych/ling/IfScope.kt | 9 ++ .../kotlin/net/sergeych/ling/Obj.kt | 109 +++++++++--------- .../kotlin/net/sergeych/ling/ObjClass.kt | 20 ++++ .../kotlin/net/sergeych/ling/Parser.kt | 9 ++ .../kotlin/net/sergeych/ling/Script.kt | 2 + library/src/commonTest/kotlin/ScriptTest.kt | 10 ++ library/src/jvmTest/kotlin/BookTest.kt | 5 + 11 files changed, 231 insertions(+), 71 deletions(-) create mode 100644 docs/OOP.md create mode 100644 library/src/commonMain/kotlin/net/sergeych/ling/IfScope.kt create mode 100644 library/src/commonMain/kotlin/net/sergeych/ling/ObjClass.kt diff --git a/docs/OOP.md b/docs/OOP.md new file mode 100644 index 0000000..d7b8e3b --- /dev/null +++ b/docs/OOP.md @@ -0,0 +1,57 @@ +# OO implementation in Ling + +Basic principles: + +- Everything is an instance of some class +- Every class except Obj has at least one parent +- Obj has no parents and is the root of the hierarchy +- instance has member fields and member functions +- Every class has hclass members and class functions, or companion ones, are these of the base class. +- every class has _type_ which is an instances of ObjClass +- ObjClass sole parent is Obj +- ObjClass contains code for instance methods, class fields, hierarchy information. +- Class information is also scoped. +- We acoid imported classes duplication using packages and import caching, so the same imported module is the same object in all its classes. + +## Instances + +Result of executing of any expression or statement in the Ling is the object that +inherits `Obj`, but is not `Obj`. For example it could be Int, void, null, real, string, bool, etc. + +This means whatever expression returns or the variable holds, is the first-class +object, no differenes. For example: + + 1.67.roundToInt() + 1>>> 2 + +Here, instance method of the real object, created from literal `1.67` is called. + +## Instance class + +Everything can be classified, and classes could be tested for equivalence: + + 3.14::class + 1>>> Real + +Class is the object, naturally, with class: + + 3.14::class::class + 1>>> Class + +Classes can be compared: + + println(3.14::class == 2.21::class) + println(3.14::class == 1::class) + println(π::class) + >>> true + >>> false + >>> Real + >>> void + +### Methods in-depth + +Regular methods are called on instances as usual `instance.method()`. The method resolution order is + +1. this instance methods; +2. parents method: no guarantee but we enumerate parents in order of appearance; +3. possible extension methods (scoped) diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Arguments.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Arguments.kt index 77ff563..ee588cc 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Arguments.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Arguments.kt @@ -1,6 +1,6 @@ package net.sergeych.ling -data class Arguments(val callerPos: Pos,val list: List): Iterable { +data class Arguments(val list: List): Iterable { data class Info(val value: Obj,val pos: Pos) @@ -14,10 +14,12 @@ data class Arguments(val callerPos: Pos,val list: List): Iterable { } companion object { - val EMPTY = Arguments("".toSource().startPos,emptyList()) + val EMPTY = Arguments(emptyList()) } override fun iterator(): Iterator { return list.map { it.value }.iterator() } -} \ No newline at end of file +} + +fun List.toArguments() = Arguments(this ) \ 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 8c25d1c..cc11ec2 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Compiler.kt @@ -89,6 +89,17 @@ class CompilerContext(val tokens: List) : ListIterator by tokens.l } else true } + fun ifNextIs(typeId: Token.Type, f: (Token) -> Unit): IfScope { + val t = next() + return if (t.type == typeId) { + f(t) + IfScope(true) + } else { + previous() + IfScope(false) + } + } + } @@ -264,9 +275,30 @@ class Compiler { } Token.Type.DOT -> { - if (operand == null) - throw ScriptError(t.pos, "Expecting expression before dot") - continue + operand?.let { left -> + // dotcall: calling method on the operand, if next is ID, "(" + cc.ifNextIs(Token.Type.ID) { methodToken -> + cc.ifNextIs(Token.Type.LPAREN) { + // instance method call + val args = parseArgs(cc) + operand = Accessor { context -> + context.pos = methodToken.pos + val v = left.getter(context) + v.callInstanceMethod( + context, + methodToken.value, + args.toArguments() + ) + } + } + }.otherwise { + TODO("implement member access") + } + } ?: throw ScriptError(t.pos, "Expecting expression before dot") + } + + Token.Type.COLONCOLON -> { + operand = parseScopeOperator(operand,cc) } Token.Type.LPAREN -> { @@ -275,7 +307,6 @@ class Compiler { operand = parseFunctionCall( cc, left, - thisObj = null, ) } ?: run { // Expression in parentheses @@ -371,8 +402,20 @@ class Compiler { } } - fun parseFunctionCall(cc: CompilerContext, left: Accessor, thisObj: Statement?): Accessor { - // insofar, functions always return lvalue + private fun parseScopeOperator(operand: Accessor?, cc: CompilerContext): Accessor { + // implement global scope maybe? + if( operand == null ) throw ScriptError(cc.next().pos, "Expecting expression before ::") + val t = cc.next() + if( t.type != Token.Type.ID ) throw ScriptError(t.pos, "Expecting ID after ::") + return when(t.value) { + "class" -> Accessor { + operand.getter(it).objClass + } + else -> throw ScriptError(t.pos, "Unknown scope operation: ${t.value}") + } + } + + fun parseArgs(cc: CompilerContext): List { val args = mutableListOf() do { val t = cc.next() @@ -385,13 +428,19 @@ class Compiler { } } } while (t.type != Token.Type.RPAREN) + return args + } + + + fun parseFunctionCall(cc: CompilerContext, left: Accessor): Accessor { + // insofar, functions always return lvalue + val args = parseArgs(cc) return Accessor { context -> val v = left.getter(context) v.callOn(context.copy( context.pos, Arguments( - context.pos, args.map { Arguments.Info((it.value as Statement).execute(context), it.pos) } ), ) @@ -851,7 +900,7 @@ class Compiler { false, d.defaultValue?.execute(context) ?: throw ScriptError( - context.args.callerPos, + context.pos, "missing required argument #${1 + i}: ${d.name}" ) ) diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt index 27c210f..b2245b4 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Context.kt @@ -3,11 +3,12 @@ package net.sergeych.ling class Context( val parent: Context?, val args: Arguments = Arguments.EMPTY, - var pos: Pos = Pos.builtIn + var pos: Pos = Pos.builtIn, + val thisObj: Obj = ObjVoid ) { constructor( args: Arguments = Arguments.EMPTY, - pos: Pos = Pos.builtIn + pos: Pos = Pos.builtIn, ) : this(Script.defaultContext, args, pos) @@ -29,7 +30,8 @@ class Context( objects[name] ?: parent?.get(name) - fun copy(pos: Pos, args: Arguments = Arguments.EMPTY): Context = Context(this, args, pos) + fun copy(pos: Pos, args: Arguments = Arguments.EMPTY,newThisObj: Obj? = null): Context = + Context(this, args, pos, newThisObj ?: thisObj) fun addItem(name: String, isMutable: Boolean, value: Obj?) { objects.put(name, StoredObj(value, isMutable)) diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/IfScope.kt b/library/src/commonMain/kotlin/net/sergeych/ling/IfScope.kt new file mode 100644 index 0000000..4bbaaf2 --- /dev/null +++ b/library/src/commonMain/kotlin/net/sergeych/ling/IfScope.kt @@ -0,0 +1,9 @@ +package net.sergeych.ling + +class IfScope(val isTrue: Boolean) { + + fun otherwise(f: ()->Unit): Boolean { + if( !isTrue ) f() + return false + } +} \ 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 f9c004b..ab94e8a 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Obj.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.sync.withLock import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlin.math.floor +import kotlin.math.roundToLong typealias InstanceMethod = (Context, Obj) -> Obj @@ -19,50 +20,36 @@ data class Accessor( fun setter(pos: Pos) = setterOrNull ?: throw ScriptError(pos,"can't assign value") } -sealed class ClassDef( - val className: String -) { - val baseClasses: List get() = emptyList() - protected val instanceMembers: MutableMap> = mutableMapOf() - private val monitor = Mutex() - - - suspend fun addInstanceMethod( - context: Context, - name: String, - isOpen: Boolean = false, - body: Obj - ) { - monitor.withLock { - instanceMembers[name]?.let { - if (!it.isMutable) - context.raiseError("method $name is not open and can't be overridden") - it.value = body - } ?: instanceMembers.put(name, WithAccess(body, isOpen)) - } - } - - suspend fun getInstanceMethodOrNull(name: String): Obj? = - monitor.withLock { instanceMembers[name]?.value } - - suspend fun getInstanceMethod(context: Context, name: String): Obj = - getInstanceMethodOrNull(name) ?: context.raiseError("no method found: $name") - -// suspend fun callInstanceMethod(context: Context, name: String, self: Obj,args: Arguments): Obj { -// getInstanceMethod(context, name).invoke(context, self,args) -// } -} - - -object ObjClassDef : ClassDef("Obj") - sealed class Obj { - open val classDef: ClassDef = ObjClassDef var isFrozen: Boolean = false - protected val instanceMethods: Map> = mutableMapOf() private val monitor = Mutex() + // members: fields most often + internal val members = mutableMapOf>() + private val parentInstances = listOf() + + /** + * Get instance member traversing the hierarchy if needed. Its meaning is different for different objects. + */ + fun getInstanceMemberOrNull(name: String): Obj? { + members[name]?.let { return it.value } + parentInstances.forEach { parent -> parent.getInstanceMemberOrNull(name)?.let { return it } } + return null + } + + fun getInstanceMember(atPos: Pos, name: String): Obj = getInstanceMemberOrNull(name) + ?: throw ScriptError(atPos,"symbol doesn't exist: $name") + + suspend fun callInstanceMethod(context: Context, name: String,args: Arguments): Obj { + // instance _methods_ are our ObjClass instance: + // note that getInstanceMember traverses the hierarchy + return objClass.getInstanceMember(context.pos,name).invoke(context, this, args) + } + + + // methods that to override + open suspend fun compareTo(context: Context, other: Obj): Int { context.raiseNotImplemented() } @@ -71,7 +58,11 @@ sealed class Obj { if (this is ObjString) this else ObjString(this.toString()) } - open val definition: ClassDef = ObjClassDef + /** + * Class of the object: definition of member functions (top-level), etc. + * Note that using lazy allows to avoid endless recursion here + */ + open val objClass: ObjClass by lazy { ObjClass("Obj") } open fun plus(context: Context, other: Obj): Obj { context.raiseNotImplemented() @@ -106,8 +97,6 @@ sealed class Obj { if (isFrozen) context.raiseError("attempt to mutate frozen object") } - suspend fun getInstanceMember(context: Context, name: String): Obj? = definition.getInstanceMethodOrNull(name) - suspend fun sync(block: () -> T): T = monitor.withLock { block() } open suspend fun readField(context: Context, name: String): Obj { @@ -122,6 +111,13 @@ sealed class Obj { context.raiseNotImplemented() } + suspend fun invoke(context: Context, thisObj: Obj,args: Arguments): Obj = + callOn(context.copy(context.pos,args = args, newThisObj = thisObj)) + + suspend fun invoke(context: Context,atPos: Pos, thisObj: Obj,args: Arguments): Obj = + callOn(context.copy(atPos,args = args,newThisObj = thisObj)) + + companion object { inline fun from(obj: T): Obj { return when (obj) { @@ -206,8 +202,7 @@ fun Obj.toBool(): Boolean = (this as? ObjBool)?.value ?: throw IllegalArgumentException("cannot convert to boolean $this") -@Serializable -@SerialName("real") + data class ObjReal(val value: Double) : Obj(), Numeric { override val asStr by lazy { ObjString(value.toString()) } override val longValue: Long by lazy { floor(value).toLong() } @@ -221,10 +216,21 @@ data class ObjReal(val value: Double) : Obj(), Numeric { } override fun toString(): String = value.toString() + + override val objClass: ObjClass = type + + companion object { + val type: ObjClass = ObjClass("Real").apply { + members["roundToInt"] = WithAccess( + statement(Pos.builtIn) { + (it.thisObj as ObjReal).value.roundToLong().toObj() + }, + false + ) + } + } } -@Serializable -@SerialName("int") data class ObjInt(var value: Long) : Obj(), Numeric { override val asStr get() = ObjString(value.toString()) override val longValue get() = value @@ -285,14 +291,3 @@ open class ObjError(val context: Context, val message: String) : Obj() { } class ObjNullPointerError(context: Context) : ObjError(context, "object is null") - -class ObjClass(override val definition: ClassDef) : Obj() { - - override suspend fun compareTo(context: Context, other: Obj): Int { -// definition.callInstanceMethod(":compareTo", context, other)?.let { -// it(context, this) -// } - TODO("Not yet implemented") - } - -} \ No newline at end of file diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/ObjClass.kt b/library/src/commonMain/kotlin/net/sergeych/ling/ObjClass.kt new file mode 100644 index 0000000..51c4620 --- /dev/null +++ b/library/src/commonMain/kotlin/net/sergeych/ling/ObjClass.kt @@ -0,0 +1,20 @@ +package net.sergeych.ling + +val ObjClassType by lazy { ObjClass("Class") } + +class ObjClass( + val className: String +): Obj() { + + override val objClass: ObjClass by lazy { ObjClassType } + + override fun toString(): String = className + + override suspend fun compareTo(context: Context, other: Obj): Int = if( other === this ) 0 else -1 + +// val parents: List get() = emptyList() + +// suspend fun callInstanceMethod(context: Context, name: String, self: Obj,args: Arguments): Obj { +// getInstanceMethod(context, name).invoke(context, self,args) +// } +} \ 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 962efd0..2deb378 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Parser.kt @@ -133,6 +133,15 @@ private class Parser(fromPos: Pos) { } '\n' -> Token("\n", from, Token.Type.NEWLINE) + ':' -> { + if( currentChar == ':') { + advance() + Token("::", from, Token.Type.COLONCOLON) + } + else + Token(":", from, Token.Type.COLON) + } + '"' -> loadStringToken() in digitsSet -> { pos.back() diff --git a/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt b/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt index 0921a6f..b652e57 100644 --- a/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt +++ b/library/src/commonMain/kotlin/net/sergeych/ling/Script.kt @@ -47,6 +47,8 @@ class Script( sin(args.firstAndOnly().toDouble()) } val pi = ObjReal(PI) + val z = pi.objClass + println("PI class $z") addConst(pi, "π") getOrCreateNamespace("Math").also { ns -> ns.addConst(pi, "PI") diff --git a/library/src/commonTest/kotlin/ScriptTest.kt b/library/src/commonTest/kotlin/ScriptTest.kt index e28ec50..76b0d0b 100644 --- a/library/src/commonTest/kotlin/ScriptTest.kt +++ b/library/src/commonTest/kotlin/ScriptTest.kt @@ -564,4 +564,14 @@ class ScriptTest { eval(src) } + @Test + fun testCallable1() = runTest { + val src = """ + val callable = { + println("called") + } + """.trimIndent() + println(eval(src).toString()) + } + } \ No newline at end of file diff --git a/library/src/jvmTest/kotlin/BookTest.kt b/library/src/jvmTest/kotlin/BookTest.kt index 91be19f..46be28e 100644 --- a/library/src/jvmTest/kotlin/BookTest.kt +++ b/library/src/jvmTest/kotlin/BookTest.kt @@ -187,4 +187,9 @@ class BookTest { fun testsFromAdvanced() = runTest { runDocTests("../docs/advanced_topics.md") } + + @Test + fun testsFromOOPrinciples() = runTest { + runDocTests("../docs/OOP.md") + } } \ No newline at end of file