first OO features: x::class, x.method(1,2,3) builting Real.roundToInt
This commit is contained in:
parent
361c1d5b13
commit
d21544ca5d
57
docs/OOP.md
Normal file
57
docs/OOP.md
Normal 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)
|
@ -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 )
|
@ -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}"
|
||||
)
|
||||
)
|
||||
|
@ -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))
|
||||
|
@ -0,0 +1,9 @@
|
||||
package net.sergeych.ling
|
||||
|
||||
class IfScope(val isTrue: Boolean) {
|
||||
|
||||
fun otherwise(f: ()->Unit): Boolean {
|
||||
if( !isTrue ) f()
|
||||
return false
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
20
library/src/commonMain/kotlin/net/sergeych/ling/ObjClass.kt
Normal file
20
library/src/commonMain/kotlin/net/sergeych/ling/ObjClass.kt
Normal 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)
|
||||
// }
|
||||
}
|
@ -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()
|
||||
|
@ -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")
|
||||
|
@ -564,4 +564,14 @@ class ScriptTest {
|
||||
eval(src)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCallable1() = runTest {
|
||||
val src = """
|
||||
val callable = {
|
||||
println("called")
|
||||
}
|
||||
""".trimIndent()
|
||||
println(eval(src).toString())
|
||||
}
|
||||
|
||||
}
|
@ -187,4 +187,9 @@ class BookTest {
|
||||
fun testsFromAdvanced() = runTest {
|
||||
runDocTests("../docs/advanced_topics.md")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testsFromOOPrinciples() = runTest {
|
||||
runDocTests("../docs/OOP.md")
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user