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:
sergeych committed 2025-12-21 18:14:51 +01:00
1 parent 35cc8fa930
commit 76a1804dc1
9 files changed
+260 -68

No files matched your search

+1 -1
View File
@@ -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,75 +233,101 @@ 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) {
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) {
c.constructorMeta?.let { meta ->
val argsHere = argsForThis ?: Arguments.EMPTY
// Assign constructor params into instance scope (unmangled)
meta.assignToContext(instance.instanceScope, argsHere)
// Also expose them under MI-mangled storage keys `${Class}::name` so qualified views can access them
// and so that base-class casts like `(obj as Base).field` work.
for (p in meta.params) {
val rec = instance.instanceScope.objects[p.name]
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)
instance.instanceScope.objects[mangled] = rec
}
}
/**
* 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
// 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)
meta.assignToContext(instance.instanceScope, argsHere)
// Also expose them under MI-mangled storage keys `${Class}::name` so qualified views can access them
// and so that base-class casts like `(obj as Base).field` work.
for (p in meta.params) {
val rec = instance.instanceScope.objects[p.name]
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
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)
val limited = if (raw != null) {
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)
}
// Execute per-instance initializers collected from class body for this class
if (c.instanceInitializers.isNotEmpty()) {
val savedCtx = instance.instanceScope.currentClassCtx
instance.instanceScope.currentClassCtx = c
try {
for (initStmt in c.instanceInitializers) {
initStmt.execute(instance.instanceScope)
}
} finally {
instance.instanceScope.currentClassCtx = savedCtx
}
}
// Then run this class' constructor, if any
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)
ctor.execute(execScope)
}
}
initClass(this, instance.instanceScope.args, isRoot = true)
return instance
// Initialize direct parents first, in order
for (p in c.directParents) {
val raw = c.directParentArgs[p]?.toArguments(instance.instanceScope, false)
val limited = if (raw != null) {
val need = p.constructorMeta?.params?.size ?: 0
if (need == 0) Arguments.EMPTY else Arguments(raw.list.take(need), tailBlockMode = false)
} else Arguments.EMPTY
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
instance.instanceScope.currentClassCtx = c
try {
for (initStmt in c.instanceInitializers) {
initStmt.execute(instance.instanceScope)
}
} finally {
instance.instanceScope.currentClassCtx = savedCtx
}
}
// Then run this class' constructor, if any
if (runConstructors) {
c.instanceConstructor?.let { ctor ->
val execScope =
instance.instanceScope.createChildScope(args = argsForThis ?: Arguments.EMPTY, newThisObj = instance)
ctor.execute(execScope)
}
}
}
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 }
}
+83
View File
@@ -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)
""")
}
}