first OO features: x::class, x.method(1,2,3) builting Real.roundToInt

This commit is contained in:
Sergey Chernov 2025-05-28 11:44:55 +04:00
parent 361c1d5b13
commit d21544ca5d
11 changed files with 231 additions and 71 deletions

57
docs/OOP.md Normal file
View File

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

View File

@ -1,6 +1,6 @@
package net.sergeych.ling
data class Arguments(val callerPos: Pos,val list: List<Info>): Iterable<Obj> {
data class Arguments(val list: List<Info>): Iterable<Obj> {
data class Info(val value: Obj,val pos: Pos)
@ -14,10 +14,12 @@ data class Arguments(val callerPos: Pos,val list: List<Info>): Iterable<Obj> {
}
companion object {
val EMPTY = Arguments("".toSource().startPos,emptyList())
val EMPTY = Arguments(emptyList())
}
override fun iterator(): Iterator<Obj> {
return list.map { it.value }.iterator()
}
}
fun List<Arguments.Info>.toArguments() = Arguments(this )

View File

@ -89,6 +89,17 @@ class CompilerContext(val tokens: List<Token>) : ListIterator<Token> 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<Arguments.Info> {
val args = mutableListOf<Arguments.Info>()
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}"
)
)

View File

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

View File

@ -0,0 +1,9 @@
package net.sergeych.ling
class IfScope(val isTrue: Boolean) {
fun otherwise(f: ()->Unit): Boolean {
if( !isTrue ) f()
return false
}
}

View File

@ -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<ClassDef> get() = emptyList()
protected val instanceMembers: MutableMap<String, WithAccess<Obj>> = 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<String, WithAccess<InstanceMethod>> = mutableMapOf()
private val monitor = Mutex()
// members: fields most often
internal val members = mutableMapOf<String, WithAccess<Obj>>()
private val parentInstances = listOf<Obj>()
/**
* 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 <T> 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 <reified T> 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")
}
}

View File

@ -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<ObjClass> get() = emptyList()
// suspend fun callInstanceMethod(context: Context, name: String, self: Obj,args: Arguments): Obj {
// getInstanceMethod(context, name).invoke(context, self,args)
// }
}

View File

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

View File

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

View File

@ -564,4 +564,14 @@ class ScriptTest {
eval(src)
}
@Test
fun testCallable1() = runTest {
val src = """
val callable = {
println("called")
}
""".trimIndent()
println(eval(src).toString())
}
}

View File

@ -187,4 +187,9 @@ class BookTest {
fun testsFromAdvanced() = runTest {
runDocTests("../docs/advanced_topics.md")
}
@Test
fun testsFromOOPrinciples() = runTest {
runDocTests("../docs/OOP.md")
}
}