Implement init blocks for instance initialization, refactor class initialization logic to ensure correct inheritance and constructor chaining, update documentation and add tests.
This commit is contained in:
parent
35cc8fa930
commit
76a1804dc1
28
docs/OOP.md
28
docs/OOP.md
@ -42,6 +42,34 @@ a _constructor_ that requires two parameters for fields. So when creating it wit
|
||||
Form now on `Point` is a class, it's type is `Class`, and we can create instances with it as in the
|
||||
example above.
|
||||
|
||||
## Instance initialization: init block
|
||||
|
||||
In addition to the primary constructor arguments, you can provide an `init` block that runs on each instance creation. This is useful for more complex initializations, side effects, or setting up fields that depend on multiple constructor parameters.
|
||||
|
||||
class Point(val x, val y) {
|
||||
var magnitude
|
||||
|
||||
init {
|
||||
magnitude = Math.sqrt(x*x + y*y)
|
||||
}
|
||||
}
|
||||
|
||||
Key features of `init` blocks:
|
||||
- **Scope**: They have full access to `this` members and all primary constructor parameters.
|
||||
- **Order**: In a single-inheritance scenario, `init` blocks run immediately after the instance fields are prepared but before the primary constructor body logic.
|
||||
- **Multiple blocks**: You can have multiple `init` blocks; they will be executed in the order they appear in the class body.
|
||||
|
||||
### Initialization in Multiple Inheritance
|
||||
|
||||
In cases of multiple inheritance, `init` blocks are executed following the constructor chaining rule:
|
||||
1. All ancestors are initialized first, following the inheritance hierarchy (diamond-safe: each ancestor is initialized exactly once).
|
||||
2. The `init` blocks of each class are executed after its parents have been fully initialized.
|
||||
3. For a hierarchy `class D : B, C`, the initialization order is: `B`'s chain, then `C`'s chain (skipping common ancestors with `B`), and finally `D`'s own `init` blocks.
|
||||
|
||||
### Initialization during Deserialization
|
||||
|
||||
When an object is restored from a serialized form (e.g., using `Lynon`), `init` blocks are **re-executed**. This ensures that transient state or derived fields are correctly recalculated upon restoration. However, primary constructors are **not** re-called during deserialization; only the `init` blocks and field initializers are executed to restore the instance state.
|
||||
|
||||
Class point has a _method_, or a _member function_ `length()` that uses its _fields_ `x` and `y` to
|
||||
calculate the magnitude. Length is called
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
group = "net.sergeych"
|
||||
version = "1.0.8-SNAPSHOT"
|
||||
version = "1.0.9-SNAPSHOT"
|
||||
|
||||
// Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below
|
||||
|
||||
|
||||
@ -1353,6 +1353,23 @@ class Compiler(
|
||||
parseClassDeclaration()
|
||||
}
|
||||
|
||||
"init" -> {
|
||||
if (codeContexts.lastOrNull() is CodeContext.ClassBody) {
|
||||
val block = parseBlock()
|
||||
lastParsedBlockRange?.let { range ->
|
||||
miniSink?.onInitDecl(MiniInitDecl(MiniRange(id.pos, range.end), id.pos))
|
||||
}
|
||||
val initStmt = statement(id.pos) { scp ->
|
||||
block.execute(scp)
|
||||
ObjVoid
|
||||
}
|
||||
statement {
|
||||
currentClassCtx?.instanceInitializers?.add(initStmt)
|
||||
ObjVoid
|
||||
}
|
||||
} else null
|
||||
}
|
||||
|
||||
"enum" -> {
|
||||
pendingDeclStart = id.pos
|
||||
pendingDeclDoc = consumePendingDoc()
|
||||
@ -1754,7 +1771,7 @@ class Compiler(
|
||||
val constructorArgsDeclaration =
|
||||
if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true))
|
||||
parseArgsDeclaration(isClassDeclaration = true)
|
||||
else null
|
||||
else ArgsDeclaration(emptyList(), Token.Type.RPAREN)
|
||||
|
||||
if (constructorArgsDeclaration != null && constructorArgsDeclaration.endTokenType != Token.Type.RPAREN)
|
||||
throw ScriptError(
|
||||
@ -1791,7 +1808,9 @@ class Compiler(
|
||||
if (next.type == Token.Type.LBRACE) {
|
||||
// parse body
|
||||
val bodyStart = next.pos
|
||||
val st = parseScript()
|
||||
val st = withLocalNames(constructorArgsDeclaration?.params?.map { it.name }?.toSet() ?: emptySet()) {
|
||||
parseScript()
|
||||
}
|
||||
val rbTok = cc.next()
|
||||
if (rbTok.type != Token.Type.RBRACE) throw ScriptError(rbTok.pos, "unbalanced braces in class body")
|
||||
classBodyRange = MiniRange(bodyStart, rbTok.pos)
|
||||
|
||||
@ -220,6 +220,16 @@ class Script(
|
||||
ObjDynamic.create(this, requireOnlyArg())
|
||||
}
|
||||
|
||||
val root = this
|
||||
val mathClass = ObjClass("Math").apply {
|
||||
addFn("sqrt") {
|
||||
ObjReal(sqrt(args.firstAndOnly().toDouble()))
|
||||
}
|
||||
}
|
||||
addItem("Math", false, ObjInstance(mathClass).apply {
|
||||
instanceScope = Scope(root, thisObj = this)
|
||||
})
|
||||
|
||||
addFn("require") {
|
||||
val condition = requiredArg<ObjBool>(0)
|
||||
if (!condition.value) {
|
||||
|
||||
@ -183,6 +183,7 @@ object CompletionEngineLight {
|
||||
val ci = CompletionItem(name, Kind.Field, typeText = typeOf((chosen as MiniMemberValDecl).type))
|
||||
if (ci.name.startsWith(prefix, true)) out += ci
|
||||
}
|
||||
is MiniInitDecl -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -210,6 +211,7 @@ object CompletionEngineLight {
|
||||
if (ci.name.startsWith(prefix, true)) out += ci
|
||||
already.add(name)
|
||||
}
|
||||
is MiniInitDecl -> {}
|
||||
}
|
||||
} else {
|
||||
// Fallback: emit simple method name without detailed types
|
||||
@ -302,6 +304,7 @@ object CompletionEngineLight {
|
||||
val ret = when (member) {
|
||||
is MiniMemberFunDecl -> member.returnType
|
||||
is MiniMemberValDecl -> member.type
|
||||
is MiniInitDecl -> null
|
||||
}
|
||||
return simpleClassNameOf(ret)
|
||||
}
|
||||
@ -362,6 +365,7 @@ object CompletionEngineLight {
|
||||
val ret = when (member) {
|
||||
is MiniMemberFunDecl -> member.returnType
|
||||
is MiniMemberValDecl -> member.type
|
||||
is MiniInitDecl -> null
|
||||
}
|
||||
return simpleClassNameOf(ret)
|
||||
}
|
||||
|
||||
@ -183,6 +183,15 @@ data class MiniMemberValDecl(
|
||||
override val isStatic: Boolean = false,
|
||||
) : MiniMemberDecl
|
||||
|
||||
data class MiniInitDecl(
|
||||
override val range: MiniRange,
|
||||
override val nameStart: Pos,
|
||||
) : MiniMemberDecl {
|
||||
override val name: String get() = "init"
|
||||
override val doc: MiniDoc? get() = null
|
||||
override val isStatic: Boolean get() = false
|
||||
}
|
||||
|
||||
// Streaming sink to collect mini-AST during parsing. Implementations may assemble a tree or process events.
|
||||
interface MiniAstSink {
|
||||
fun onScriptStart(start: Pos) {}
|
||||
@ -193,6 +202,7 @@ interface MiniAstSink {
|
||||
fun onImport(node: MiniImport) {}
|
||||
fun onFunDecl(node: MiniFunDecl) {}
|
||||
fun onValDecl(node: MiniValDecl) {}
|
||||
fun onInitDecl(node: MiniInitDecl) {}
|
||||
fun onClassDecl(node: MiniClassDecl) {}
|
||||
|
||||
fun onBlock(node: MiniBlock) {}
|
||||
|
||||
@ -200,6 +200,16 @@ open class ObjClass(
|
||||
override suspend fun compareTo(scope: Scope, other: Obj): Int = if (other === this) 0 else -1
|
||||
|
||||
override suspend fun callOn(scope: Scope): Obj {
|
||||
val instance = createInstance(scope)
|
||||
initializeInstance(instance, scope.args, runConstructors = true)
|
||||
return instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance of this class and initialize its [ObjInstance.instanceScope] with
|
||||
* methods. Does NOT run initializers or constructors.
|
||||
*/
|
||||
internal fun createInstance(scope: Scope): ObjInstance {
|
||||
val instance = ObjInstance(this)
|
||||
// Avoid capturing a transient (pooled) call frame as the parent of the instance scope.
|
||||
// Bind instance scope to the caller's parent chain directly so name resolution (e.g., stdlib like sqrt)
|
||||
@ -223,14 +233,38 @@ open class ObjClass(
|
||||
}
|
||||
}
|
||||
}
|
||||
// Constructor chaining MVP: initialize base classes left-to-right, then this class.
|
||||
// Ensure each ancestor is initialized at most once (diamond-safe).
|
||||
val visited = hashSetOf<ObjClass>()
|
||||
return instance
|
||||
}
|
||||
|
||||
suspend fun initClass(c: ObjClass, argsForThis: Arguments?, isRoot: Boolean = false) {
|
||||
/**
|
||||
* Run initializers and optionally constructors for the given [instance].
|
||||
* Handles Multiple Inheritance correctly (diamond-safe).
|
||||
*/
|
||||
internal suspend fun initializeInstance(
|
||||
instance: ObjInstance,
|
||||
args: Arguments?,
|
||||
runConstructors: Boolean
|
||||
) {
|
||||
val visited = hashSetOf<ObjClass>()
|
||||
initClassInternal(instance, visited, this, args, isRoot = true, runConstructors = runConstructors)
|
||||
}
|
||||
|
||||
private suspend fun initClassInternal(
|
||||
instance: ObjInstance,
|
||||
visited: MutableSet<ObjClass>,
|
||||
c: ObjClass,
|
||||
argsForThis: Arguments?,
|
||||
@Suppress("UNUSED_PARAMETER") isRoot: Boolean = false,
|
||||
runConstructors: Boolean = true
|
||||
) {
|
||||
if (!visited.add(c)) return
|
||||
// For the most-derived class, bind its constructor params BEFORE parents so base arg thunks can see them
|
||||
if (isRoot) {
|
||||
|
||||
// Bind constructor parameters (both mangled and unmangled)
|
||||
// These are needed for:
|
||||
// 1) base constructor argument evaluation (if called from a derived class)
|
||||
// 2) this class's field initializers and `init` blocks
|
||||
// 3) this class's constructor body
|
||||
// 4) `compareTo` and other structural operations
|
||||
c.constructorMeta?.let { meta ->
|
||||
val argsHere = argsForThis ?: Arguments.EMPTY
|
||||
// Assign constructor params into instance scope (unmangled)
|
||||
@ -242,12 +276,12 @@ open class ObjClass(
|
||||
if (rec != null) {
|
||||
val mangled = "${c.className}::${p.name}"
|
||||
// Always point the mangled name to the current record to keep writes consistent
|
||||
// across re-bindings (e.g., second pass before ctor)
|
||||
// across re-bindings
|
||||
instance.instanceScope.objects[mangled] = rec
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize direct parents first, in order
|
||||
for (p in c.directParents) {
|
||||
val raw = c.directParentArgs[p]?.toArguments(instance.instanceScope, false)
|
||||
@ -255,8 +289,25 @@ open class ObjClass(
|
||||
val need = p.constructorMeta?.params?.size ?: 0
|
||||
if (need == 0) Arguments.EMPTY else Arguments(raw.list.take(need), tailBlockMode = false)
|
||||
} else Arguments.EMPTY
|
||||
initClass(p, limited)
|
||||
initClassInternal(instance, visited, p, limited, false, runConstructors)
|
||||
}
|
||||
|
||||
// Re-bind this class's parameters right before running its initializers and constructor.
|
||||
// This ensures that unmangled names in the instance scope correctly refer to THIS class's
|
||||
// parameters even if they were shadowed/overwritten by parent class initialization.
|
||||
c.constructorMeta?.let { meta ->
|
||||
val argsHere = argsForThis ?: Arguments.EMPTY
|
||||
meta.assignToContext(instance.instanceScope, argsHere)
|
||||
// Re-sync mangled names to point to the fresh records to keep them consistent
|
||||
for (p in meta.params) {
|
||||
val rec = instance.instanceScope.objects[p.name]
|
||||
if (rec != null) {
|
||||
val mangled = "${c.className}::${p.name}"
|
||||
instance.instanceScope.objects[mangled] = rec
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Execute per-instance initializers collected from class body for this class
|
||||
if (c.instanceInitializers.isNotEmpty()) {
|
||||
val savedCtx = instance.instanceScope.currentClassCtx
|
||||
@ -270,28 +321,13 @@ open class ObjClass(
|
||||
}
|
||||
}
|
||||
// Then run this class' constructor, if any
|
||||
if (runConstructors) {
|
||||
c.instanceConstructor?.let { ctor ->
|
||||
// Bind this class's constructor parameters into the instance scope now, right before ctor
|
||||
c.constructorMeta?.let { meta ->
|
||||
val argsHere = argsForThis ?: Arguments.EMPTY
|
||||
meta.assignToContext(instance.instanceScope, argsHere)
|
||||
// Ensure mangled aliases exist for qualified access starting from this class
|
||||
for (p in meta.params) {
|
||||
val rec = instance.instanceScope.objects[p.name]
|
||||
if (rec != null) {
|
||||
val mangled = "${c.className}::${p.name}"
|
||||
// Overwrite to ensure alias refers to the latest ObjRecord after re-binding
|
||||
instance.instanceScope.objects[mangled] = rec
|
||||
}
|
||||
}
|
||||
}
|
||||
val execScope = instance.instanceScope.createChildScope(args = argsForThis ?: Arguments.EMPTY, newThisObj = instance)
|
||||
val execScope =
|
||||
instance.instanceScope.createChildScope(args = argsForThis ?: Arguments.EMPTY, newThisObj = instance)
|
||||
ctor.execute(execScope)
|
||||
}
|
||||
}
|
||||
|
||||
initClass(this, instance.instanceScope.args, isRoot = true)
|
||||
return instance
|
||||
}
|
||||
|
||||
suspend fun callWithArgs(scope: Scope, vararg plainArgs: Obj): Obj {
|
||||
|
||||
@ -33,7 +33,9 @@ class ObjInstanceClass(val name: String, vararg parents: ObjClass) : ObjClass(na
|
||||
if (args.size > actualSize)
|
||||
scope.raiseIllegalArgument("constructor $name has only $actualSize but serialized version has ${args.size}")
|
||||
val newScope = scope.createChildScope(args = Arguments(args))
|
||||
return (callOn(newScope) as ObjInstance).apply {
|
||||
val instance = createInstance(newScope)
|
||||
initializeInstance(instance, newScope.args, runConstructors = false)
|
||||
return instance.apply {
|
||||
deserializeStateVars(scope, decoder)
|
||||
invokeInstanceMethod(scope, "onDeserialized") { ObjVoid }
|
||||
}
|
||||
|
||||
@ -186,4 +186,87 @@ class OOTest {
|
||||
assertEquals(15, cc.foo.bar(10,2,3))
|
||||
""")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testClassInitialization() = runTest {
|
||||
eval("""
|
||||
var countInstances = 0
|
||||
class Point(val x: Int, val y: Int) {
|
||||
println("Class initializer is called 1")
|
||||
var magnitude
|
||||
|
||||
/*
|
||||
init {} section optionally provide initialization code that is called on each instance creation.
|
||||
it should have the same access to this.* and constructor parameters as any other member
|
||||
function.
|
||||
*/
|
||||
init {
|
||||
countInstances++
|
||||
magnitude = Math.sqrt(x*x + y*y)
|
||||
}
|
||||
}
|
||||
|
||||
val p = Point(1, 2)
|
||||
assertEquals(1, countInstances)
|
||||
assertEquals(p, Point(1,2) )
|
||||
assertEquals(2, countInstances)
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMIInitialization() = runTest {
|
||||
eval("""
|
||||
var order = []
|
||||
class A {
|
||||
init { order.add("A") }
|
||||
}
|
||||
class B : A {
|
||||
init { order.add("B") }
|
||||
}
|
||||
class C {
|
||||
init { order.add("C") }
|
||||
}
|
||||
class D : B, C {
|
||||
init { order.add("D") }
|
||||
}
|
||||
D()
|
||||
assertEquals(["A", "B", "C", "D"], order)
|
||||
""")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMIDiamondInitialization() = runTest {
|
||||
eval("""
|
||||
var order = []
|
||||
class A {
|
||||
init { order.add("A") }
|
||||
}
|
||||
class B : A {
|
||||
init { order.add("B") }
|
||||
}
|
||||
class C : A {
|
||||
init { order.add("C") }
|
||||
}
|
||||
class D : B, C {
|
||||
init { order.add("D") }
|
||||
}
|
||||
D()
|
||||
assertEquals(["A", "B", "C", "D"], order)
|
||||
""")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInitBlockInDeserialization() = runTest {
|
||||
eval("""
|
||||
import lyng.serialization
|
||||
var count = 0
|
||||
class A {
|
||||
init { count++ }
|
||||
}
|
||||
val a1 = A()
|
||||
val coded = Lynon.encode(a1)
|
||||
val a2 = Lynon.decode(coded)
|
||||
assertEquals(2, count)
|
||||
""")
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user