From d3f23be7fe7703c07cb3e55138376359f71bc25d Mon Sep 17 00:00:00 2001 From: sergeych Date: Sun, 8 Jun 2025 18:09:55 +0400 Subject: [PATCH] OOP: structs with field access! --- docs/OOP.md | 36 +++++++-- .../net/sergeych/lyng/ArgsDeclaration.kt | 26 +++--- .../kotlin/net/sergeych/lyng/Compiler.kt | 80 ++++++++++++++----- .../kotlin/net/sergeych/lyng/Obj.kt | 3 +- .../kotlin/net/sergeych/lyng/ObjClass.kt | 42 +++++----- .../kotlin/net/sergeych/lyng/Script.kt | 2 + library/src/commonTest/kotlin/ScriptTest.kt | 38 +++++++-- 7 files changed, 155 insertions(+), 72 deletions(-) diff --git a/docs/OOP.md b/docs/OOP.md index 56bce41..d87d6f6 100644 --- a/docs/OOP.md +++ b/docs/OOP.md @@ -76,12 +76,36 @@ Regular methods are called on instances as usual `instance.method()`. The method The class is a some data record with named fields and fixed order, in fact. To define a class, just Provide a name and a record like this: - class Vec2(x,y) + // creating new class with main constructor + // with all fields public and mutable: -This way, you have created a _constructor_, so calling `Vec2( 10, 20 )` would create an _instane_ of `Vec2` class: + struct Point(x,y) + assert( Point is Class ) + + // now we can create instance + val p1 = Point(3,4) - class Vec2(x,y) - Vec2(10,20) - >> eee + // is is of the newly created type: + assert( p1 is Point ) -TBD \ No newline at end of file + // we can read and write its fields: + assert( p1.x == 3 ) + assert( p1.y == 4 ) + + p1.y++ + assert( p1.y == 5 ) + + >>> void + +Let's see in details. The statement `struct Point(x,y)` creates a struct, or public class, +with two field, which are mutable and publicly visible, because it is _struct_. `(x,y)` here +is the [argument list], same as when defining a function. All together creates a class with +a _constructor_ that requires two parameters for fields. So when creating it with +`Point(10, 20)` we say _calling Point constructor_ with these parameters. + +Such declaration is identical to `class Point(var x,var y)` which does exactly the same. + + +TBD + +[argument list](declaring_arguments.md) \ No newline at end of file diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt index 3d6a739..0e585af 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/ArgsDeclaration.kt @@ -4,15 +4,15 @@ package net.sergeych.lyng * List of argument declarations in the __definition__ of the lambda, class constructor, * function, etc. It is created by [Compiler.parseArgsDeclaration] */ -data class ArgsDeclaration(val args: List, val endTokenType: Token.Type) { +data class ArgsDeclaration(val params: List, val endTokenType: Token.Type) { init { - val i = args.count { it.isEllipsis } - if (i > 1) throw ScriptError(args[i].pos, "there can be only one argument") - val start = args.indexOfFirst { it.defaultValue != null } + val i = params.count { it.isEllipsis } + if (i > 1) throw ScriptError(params[i].pos, "there can be only one argument") + val start = params.indexOfFirst { it.defaultValue != null } if (start >= 0) - for (j in start + 1 until args.size) - if (args[j].defaultValue == null) throw ScriptError( - args[j].pos, + for (j in start + 1 until params.size) + if (params[j].defaultValue == null) throw ScriptError( + params[j].pos, "required argument can't follow default one" ) } @@ -31,8 +31,8 @@ data class ArgsDeclaration(val args: List, val endTokenType: Token.Type) { suspend fun processHead(index: Int): Int { var i = index - while (i != args.size) { - val a = args[i] + while (i != params.size) { + val a = params[i] if (a.isEllipsis) break val value = when { i < fromArgs.size -> fromArgs[i] @@ -46,10 +46,10 @@ data class ArgsDeclaration(val args: List, val endTokenType: Token.Type) { } suspend fun processTail(index: Int): Int { - var i = args.size - 1 + var i = params.size - 1 var j = fromArgs.size - 1 while (i > index) { - val a = args[i] + val a = params[i] if (a.isEllipsis) break val value = when { j >= index -> { @@ -66,14 +66,14 @@ data class ArgsDeclaration(val args: List, val endTokenType: Token.Type) { } fun processEllipsis(index: Int, toFromIndex: Int) { - val a = args[index] + val a = params[index] val l = if (index > toFromIndex) ObjList() else ObjList(fromArgs.values.subList(index, toFromIndex + 1).toMutableList()) assign(a, l) } val leftIndex = processHead(0) - if (leftIndex < args.size) { + if (leftIndex < params.size) { val end = processTail(leftIndex) processEllipsis(leftIndex, end) } else { diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index cef5cbd..2248af5 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -16,14 +16,14 @@ class Compiler( private fun parseScript(start: Pos, tokens: CompilerContext): Script { val statements = mutableListOf() - while (parseStatement(tokens,braceMeansLambda = true)?.also { + while (parseStatement(tokens, braceMeansLambda = true)?.also { statements += it } != null) {/**/ } return Script(start, statements) } - private fun parseStatement(cc: CompilerContext,braceMeansLambda: Boolean = false): Statement? { + private fun parseStatement(cc: CompilerContext, braceMeansLambda: Boolean = false): Statement? { while (true) { val t = cc.next() return when (t.type) { @@ -49,7 +49,7 @@ class Compiler( Token.Type.LBRACE -> { cc.previous() - if( braceMeansLambda ) + if (braceMeansLambda) parseExpression(cc) else parseBlock(cc) @@ -141,8 +141,10 @@ class Compiler( } } if (!isCall) { - operand = Accessor { context -> + operand = Accessor({ context -> left.getter(context).value.readField(context, next.value) + }) { cc, newValue -> + left.getter(cc).value.writeField(cc, next.value, newValue) } } } ?: throw ScriptError(t.pos, "Expecting expression before dot") @@ -354,7 +356,7 @@ class Compiler( } return Accessor { x -> - if( closure == null ) closure = x + if (closure == null) closure = x callStatement.asReadonly } } @@ -695,10 +697,16 @@ class Compiler( private fun parseClassDeclaration(cc: CompilerContext, isStruct: Boolean): Statement { val nameToken = cc.requireToken(Token.Type.ID) - val parsedArgs = parseArgsDeclaration(cc) + val constructorArgsDeclaration = + if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true)) + parseArgsDeclaration(cc) + else null - if( parsedArgs != null && parsedArgs.endTokenType != Token.Type.RPAREN) - throw ScriptError(nameToken.pos, "Bad class declaration: expected ')' at the end of the primary constructor") + if (constructorArgsDeclaration != null && constructorArgsDeclaration.endTokenType != Token.Type.RPAREN) + throw ScriptError( + nameToken.pos, + "Bad class declaration: expected ')' at the end of the primary constructor" + ) cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true) val t = cc.next() @@ -707,7 +715,10 @@ class Compiler( val bodyInit: Statement? = if (t.type == Token.Type.LBRACE) { // parse body TODO("parse body") - } else null + } else { + cc.previous() + null + } // create class val className = nameToken.value @@ -721,21 +732,45 @@ class Compiler( val constructorCode = statement { // constructor code is registered with class instance and is called over - // new `thisObj` already set by class to ObjInstance + // new `thisObj` already set by class to ObjInstance.instanceContext thisObj as ObjInstance + // the context now is a "class creation context", we must use its args to initialize // fields. Note that 'this' is already set by class -// parsedArgs?.let { pa -> -// pa.extractArgs { (def, value) -> -// val access = def.accessType ?: defaultAccess -// val visibility = def.visibility ?: defaultVisibility -// addItem(def.name, access.isMutable, value) -// } -// - thisObj + constructorArgsDeclaration?.let { cad -> + cad.assignToContext(this) + // note that accessors are created in ObjClass instance, not during instance + // initialization (not here) + } + thisObj + } + // inheritance must alter this code: + val newClass = ObjClass(className).apply { + instanceConstructor = constructorCode + constructorArgsDeclaration?.let { cad -> + // we need accessors for all fields: + for (f in cad.params) { + createField( + f.name, + statement { + val context = (thisObj as ObjInstance).instanceContext + println("called on $thisObj") + context[f.name]?.value ?: raiseError("field is not initialized: ${f.name}") + }, + true, +// (f.accessType ?: defaultAccess).isMutable, + ) + } + } + } + + return statement { + // the main statement should create custom ObjClass instance with field + // accessors, constructor registration, etc. + addItem(className, false, newClass) + newClass } - TODO() } @@ -1063,8 +1098,11 @@ class Compiler( throw ScriptError(t.pos, "Bad function definition: expected '(' after 'fn ${name}'") val argsDeclaration = parseArgsDeclaration(tokens) - if( argsDeclaration == null || argsDeclaration.endTokenType != Token.Type.RPAREN) - throw ScriptError(t.pos, "Bad function definition: expected valid argument declaration or () after 'fn ${name}'") + if (argsDeclaration == null || argsDeclaration.endTokenType != Token.Type.RPAREN) + throw ScriptError( + t.pos, + "Bad function definition: expected valid argument declaration or () after 'fn ${name}'" + ) // Here we should be at open body val fnStatements = parseBlock(tokens) diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt index f9e27d8..060a15c 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/Obj.kt @@ -170,8 +170,7 @@ open class Obj { val value = obj?.value return when (value) { is Statement -> { - // readonly property, important: call it on this - value.execute(context.copy(context.pos, newThisObj = this)).asReadonly + WithAccess(value.execute(context.copy(context.pos, newThisObj = this)), obj.isMutable) } // could be writable property naturally null -> ObjNull.asReadonly diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt index c5e92b5..3126c4e 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/ObjClass.kt @@ -4,14 +4,10 @@ val ObjClassType by lazy { ObjClass("Class") } class ObjClass( val className: String, - val constructorArgs: List = emptyList(), vararg val parents: ObjClass, ) : Obj() { - constructor( - className: String, - vararg parents: ObjClass, - ) : this(className, emptyList(), *parents) + var instanceConstructor: Statement? = null val allParentsSet: Set = parents.flatMap { listOf(it) + it.allParentsSet @@ -26,27 +22,28 @@ class ObjClass( override suspend fun compareTo(context: Context, other: Obj): Int = if (other === this) 0 else -1 -// private var initInstanceHandler: (suspend (Context, List) -> Obj)? = null + override suspend fun callOn(context: Context): Obj { + println("callOn $this constructing....") + println("on context: $context") + val instance = ObjInstance(this) + instance.instanceContext = context.copy(newThisObj = instance,args = context.args) + if (instanceConstructor != null) { + instanceConstructor!!.execute(instance.instanceContext) + } + return instance + } - // suspend fun newInstance(context: Context, vararg args: Obj): Obj = -// initInstanceHandler?.invoke(context, args.toList()) -// ?: context.raiseError("No initInstance handler for $this") -// -// fun buildInstance(f: suspend Context.(List) -> Obj) { -// if (initInstanceHandler != null) throw IllegalStateException("initInstance already set") -// initInstanceHandler = f -// } -// -// fun addParent(context: Context, parent: Obj) { -// val self = context.thisObj -// self.parentInstances.add(parent) -// } -// fun defaultInstance(): Obj = object : Obj() { override val objClass: ObjClass = this@ObjClass } - fun createField(name: String, initialValue: Obj, isMutable: Boolean = false, pos: Pos = Pos.builtIn) { + fun createField( + name: String, + initialValue: Obj, + isMutable: Boolean = false, + visibility: Compiler.Visibility = Compiler.Visibility.Public, + pos: Pos = Pos.builtIn + ) { if (name in members || allParentsSet.any { name in it.members } == true) throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes") members[name] = WithAccess(initialValue, isMutable) @@ -154,6 +151,7 @@ val ObjArray by lazy { } } -class ObjInstance(override val objClass: ObjClass): Obj() { +class ObjInstance(override val objClass: ObjClass) : Obj() { + internal lateinit var instanceContext: Context } diff --git a/library/src/commonMain/kotlin/net/sergeych/lyng/Script.kt b/library/src/commonMain/kotlin/net/sergeych/lyng/Script.kt index 3646cf1..f5cee42 100644 --- a/library/src/commonMain/kotlin/net/sergeych/lyng/Script.kt +++ b/library/src/commonMain/kotlin/net/sergeych/lyng/Script.kt @@ -156,6 +156,8 @@ class Script( // interfaces addConst("Iterable", ObjIterable) addConst("Array", ObjArray) + addConst("Class", ObjClassType) + addConst("Object", Obj().objClass) val pi = ObjReal(PI) addConst("π", pi) diff --git a/library/src/commonTest/kotlin/ScriptTest.kt b/library/src/commonTest/kotlin/ScriptTest.kt index f40d540..6910557 100644 --- a/library/src/commonTest/kotlin/ScriptTest.kt +++ b/library/src/commonTest/kotlin/ScriptTest.kt @@ -1383,14 +1383,36 @@ class ScriptTest { } @Test - fun simpleClassDelaration() = runTest { - eval( """ -// class Vec2(x,y) -// println(Vec2(1,2)::class) - println("---------------------") - println(Int::class) - """.trimIndent() - ) + fun testSimpleStruct() = runTest { + val c = Context() + c.eval(""" + struct Point(x,y) + assert( Point::class is Class ) + val p = Point(2,3) + assert(p is Point) + println(p) + println(p.x) + assert( p.x == 2 ) + assert( p.y == 3 ) + + val p2 = Point(p.x+1,p.y+1) + p.x = 0 + assertEquals( 0, p.x ) + """.trimIndent()) + } + @Test + fun testNonAssignalbeFieldInStruct() = runTest { + val c = Context() + c.eval(""" + struct Point(x,y) + val p = Point("2",3) + assert(p is Point) + assert( p.x == "2" ) + assert( p.y == 3 ) + + p.x = 0 + assertEquals( 0, p.x ) + """.trimIndent()) } } \ No newline at end of file