OOP: structs with field access!

This commit is contained in:
Sergey Chernov 2025-06-08 18:09:55 +04:00
parent 397dcaae92
commit d3f23be7fe
7 changed files with 155 additions and 72 deletions

View File

@ -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, 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: 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)
// is is of the newly created type:
assert( p1 is Point )
// 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.
class Vec2(x,y)
Vec2(10,20)
>> eee
TBD TBD
[argument list](declaring_arguments.md)

View File

@ -4,15 +4,15 @@ package net.sergeych.lyng
* List of argument declarations in the __definition__ of the lambda, class constructor, * List of argument declarations in the __definition__ of the lambda, class constructor,
* function, etc. It is created by [Compiler.parseArgsDeclaration] * function, etc. It is created by [Compiler.parseArgsDeclaration]
*/ */
data class ArgsDeclaration(val args: List<Item>, val endTokenType: Token.Type) { data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type) {
init { init {
val i = args.count { it.isEllipsis } val i = params.count { it.isEllipsis }
if (i > 1) throw ScriptError(args[i].pos, "there can be only one argument") if (i > 1) throw ScriptError(params[i].pos, "there can be only one argument")
val start = args.indexOfFirst { it.defaultValue != null } val start = params.indexOfFirst { it.defaultValue != null }
if (start >= 0) if (start >= 0)
for (j in start + 1 until args.size) for (j in start + 1 until params.size)
if (args[j].defaultValue == null) throw ScriptError( if (params[j].defaultValue == null) throw ScriptError(
args[j].pos, params[j].pos,
"required argument can't follow default one" "required argument can't follow default one"
) )
} }
@ -31,8 +31,8 @@ data class ArgsDeclaration(val args: List<Item>, val endTokenType: Token.Type) {
suspend fun processHead(index: Int): Int { suspend fun processHead(index: Int): Int {
var i = index var i = index
while (i != args.size) { while (i != params.size) {
val a = args[i] val a = params[i]
if (a.isEllipsis) break if (a.isEllipsis) break
val value = when { val value = when {
i < fromArgs.size -> fromArgs[i] i < fromArgs.size -> fromArgs[i]
@ -46,10 +46,10 @@ data class ArgsDeclaration(val args: List<Item>, val endTokenType: Token.Type) {
} }
suspend fun processTail(index: Int): Int { suspend fun processTail(index: Int): Int {
var i = args.size - 1 var i = params.size - 1
var j = fromArgs.size - 1 var j = fromArgs.size - 1
while (i > index) { while (i > index) {
val a = args[i] val a = params[i]
if (a.isEllipsis) break if (a.isEllipsis) break
val value = when { val value = when {
j >= index -> { j >= index -> {
@ -66,14 +66,14 @@ data class ArgsDeclaration(val args: List<Item>, val endTokenType: Token.Type) {
} }
fun processEllipsis(index: Int, toFromIndex: Int) { fun processEllipsis(index: Int, toFromIndex: Int) {
val a = args[index] val a = params[index]
val l = if (index > toFromIndex) ObjList() val l = if (index > toFromIndex) ObjList()
else ObjList(fromArgs.values.subList(index, toFromIndex + 1).toMutableList()) else ObjList(fromArgs.values.subList(index, toFromIndex + 1).toMutableList())
assign(a, l) assign(a, l)
} }
val leftIndex = processHead(0) val leftIndex = processHead(0)
if (leftIndex < args.size) { if (leftIndex < params.size) {
val end = processTail(leftIndex) val end = processTail(leftIndex)
processEllipsis(leftIndex, end) processEllipsis(leftIndex, end)
} else { } else {

View File

@ -141,8 +141,10 @@ class Compiler(
} }
} }
if (!isCall) { if (!isCall) {
operand = Accessor { context -> operand = Accessor({ context ->
left.getter(context).value.readField(context, next.value) 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") } ?: throw ScriptError(t.pos, "Expecting expression before dot")
@ -695,10 +697,16 @@ class Compiler(
private fun parseClassDeclaration(cc: CompilerContext, isStruct: Boolean): Statement { private fun parseClassDeclaration(cc: CompilerContext, isStruct: Boolean): Statement {
val nameToken = cc.requireToken(Token.Type.ID) 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) if (constructorArgsDeclaration != null && constructorArgsDeclaration.endTokenType != Token.Type.RPAREN)
throw ScriptError(nameToken.pos, "Bad class declaration: expected ')' at the end of the primary constructor") throw ScriptError(
nameToken.pos,
"Bad class declaration: expected ')' at the end of the primary constructor"
)
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true) cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
val t = cc.next() val t = cc.next()
@ -707,7 +715,10 @@ class Compiler(
val bodyInit: Statement? = if (t.type == Token.Type.LBRACE) { val bodyInit: Statement? = if (t.type == Token.Type.LBRACE) {
// parse body // parse body
TODO("parse body") TODO("parse body")
} else null } else {
cc.previous()
null
}
// create class // create class
val className = nameToken.value val className = nameToken.value
@ -721,21 +732,45 @@ class Compiler(
val constructorCode = statement { val constructorCode = statement {
// constructor code is registered with class instance and is called over // 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 thisObj as ObjInstance
// the context now is a "class creation context", we must use its args to initialize // the context now is a "class creation context", we must use its args to initialize
// fields. Note that 'this' is already set by class // fields. Note that 'this' is already set by class
// parsedArgs?.let { pa -> constructorArgsDeclaration?.let { cad ->
// pa.extractArgs { (def, value) -> cad.assignToContext(this)
// val access = def.accessType ?: defaultAccess // note that accessors are created in ObjClass instance, not during instance
// val visibility = def.visibility ?: defaultVisibility // initialization (not here)
// addItem(def.name, access.isMutable, value) }
// }
// thisObj
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()
} }
@ -1064,7 +1099,10 @@ class Compiler(
val argsDeclaration = parseArgsDeclaration(tokens) val argsDeclaration = parseArgsDeclaration(tokens)
if (argsDeclaration == null || argsDeclaration.endTokenType != Token.Type.RPAREN) if (argsDeclaration == null || argsDeclaration.endTokenType != Token.Type.RPAREN)
throw ScriptError(t.pos, "Bad function definition: expected valid argument declaration or () after 'fn ${name}'") throw ScriptError(
t.pos,
"Bad function definition: expected valid argument declaration or () after 'fn ${name}'"
)
// Here we should be at open body // Here we should be at open body
val fnStatements = parseBlock(tokens) val fnStatements = parseBlock(tokens)

View File

@ -170,8 +170,7 @@ open class Obj {
val value = obj?.value val value = obj?.value
return when (value) { return when (value) {
is Statement -> { is Statement -> {
// readonly property, important: call it on this WithAccess(value.execute(context.copy(context.pos, newThisObj = this)), obj.isMutable)
value.execute(context.copy(context.pos, newThisObj = this)).asReadonly
} }
// could be writable property naturally // could be writable property naturally
null -> ObjNull.asReadonly null -> ObjNull.asReadonly

View File

@ -4,14 +4,10 @@ val ObjClassType by lazy { ObjClass("Class") }
class ObjClass( class ObjClass(
val className: String, val className: String,
val constructorArgs: List<ArgsDeclaration.Item> = emptyList(),
vararg val parents: ObjClass, vararg val parents: ObjClass,
) : Obj() { ) : Obj() {
constructor(
className: String,
vararg parents: ObjClass,
) : this(className, emptyList(), *parents)
var instanceConstructor: Statement? = null
val allParentsSet: Set<ObjClass> = parents.flatMap { val allParentsSet: Set<ObjClass> = parents.flatMap {
listOf(it) + it.allParentsSet 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 override suspend fun compareTo(context: Context, other: Obj): Int = if (other === this) 0 else -1
// private var initInstanceHandler: (suspend (Context, List<Obj>) -> 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>) -> 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() { fun defaultInstance(): Obj = object : Obj() {
override val objClass: ObjClass = this@ObjClass 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) 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") throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
members[name] = WithAccess(initialValue, isMutable) members[name] = WithAccess(initialValue, isMutable)
@ -156,4 +153,5 @@ val ObjArray by lazy {
class ObjInstance(override val objClass: ObjClass) : Obj() { class ObjInstance(override val objClass: ObjClass) : Obj() {
internal lateinit var instanceContext: Context
} }

View File

@ -156,6 +156,8 @@ class Script(
// interfaces // interfaces
addConst("Iterable", ObjIterable) addConst("Iterable", ObjIterable)
addConst("Array", ObjArray) addConst("Array", ObjArray)
addConst("Class", ObjClassType)
addConst("Object", Obj().objClass)
val pi = ObjReal(PI) val pi = ObjReal(PI)
addConst("π", pi) addConst("π", pi)

View File

@ -1383,14 +1383,36 @@ class ScriptTest {
} }
@Test @Test
fun simpleClassDelaration() = runTest { fun testSimpleStruct() = runTest {
eval( """ val c = Context()
// class Vec2(x,y) c.eval("""
// println(Vec2(1,2)::class) struct Point(x,y)
println("---------------------") assert( Point::class is Class )
println(Int::class) val p = Point(2,3)
""".trimIndent() 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())
} }
} }