Step 25C: bytecode class declarations

This commit is contained in:
Sergey Chernov 2026-02-10 08:30:10 +03:00
parent b32a937636
commit 4e08339756
10 changed files with 201 additions and 133 deletions

View File

@ -114,10 +114,10 @@ Goal: migrate the compiler so all values live in frames/bytecode, keeping JVM te
- [ ] Step 25: Replace Statement-based declaration calls in bytecode.
- [x] Add bytecode const/op for enum declarations (no `Statement` objects in constants).
- [ ] Add bytecode const/op for class declarations (no `Statement` objects in constants).
- [x] Add bytecode const/op for class declarations (no `Statement` objects in constants).
- [x] Add bytecode const/op for function declarations (no `Statement` objects in constants).
- [x] Replace `emitStatementCall` usage for `EnumDeclStatement`.
- [ ] Replace `emitStatementCall` usage for `ClassDeclStatement`.
- [x] Replace `emitStatementCall` usage for `ClassDeclStatement`.
- [x] Replace `emitStatementCall` usage for `FunctionDeclStatement`.
- [ ] Add JVM disasm coverage to ensure module init has no `CALL_SLOT` to `Callable@...` for declarations.
- [ ] Step 26: Bytecode-backed lambdas (remove `ValueFnRef` runtime execution).

View File

@ -17,20 +17,147 @@
package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.ObjException
import net.sergeych.lyng.obj.ObjInstance
import net.sergeych.lyng.obj.ObjInstanceClass
import net.sergeych.lyng.obj.ObjNull
import net.sergeych.lyng.obj.ObjRecord
data class ClassDeclBaseSpec(
val name: String,
val args: List<ParsedArgument>?
)
data class ClassDeclSpec(
val declaredName: String?,
val className: String,
val typeName: String,
val startPos: Pos,
val isExtern: Boolean,
val isAbstract: Boolean,
val isObject: Boolean,
val isAnonymous: Boolean,
val baseSpecs: List<ClassDeclBaseSpec>,
val constructorArgs: ArgsDeclaration?,
val constructorFieldIds: Map<String, Int>?,
val bodyInit: Statement?,
val initScope: List<Statement>,
)
internal suspend fun executeClassDecl(scope: Scope, spec: ClassDeclSpec): Obj {
if (spec.isObject) {
val parentClasses = spec.baseSpecs.map { baseSpec ->
val rec = scope[baseSpec.name] ?: throw ScriptError(spec.startPos, "unknown base class: ${baseSpec.name}")
(rec.value as? ObjClass) ?: throw ScriptError(spec.startPos, "${baseSpec.name} is not a class")
}
val newClass = ObjInstanceClass(spec.className, *parentClasses.toTypedArray())
newClass.isAnonymous = spec.isAnonymous
newClass.constructorMeta = ArgsDeclaration(emptyList(), Token.Type.RPAREN)
for (i in parentClasses.indices) {
val argsList = spec.baseSpecs[i].args
if (argsList != null) newClass.directParentArgs[parentClasses[i]] = argsList
}
val classScope = scope.createChildScope(newThisObj = newClass)
classScope.currentClassCtx = newClass
newClass.classScope = classScope
classScope.addConst("object", newClass)
spec.bodyInit?.execute(classScope)
val instance = newClass.callOn(scope.createChildScope(Arguments.EMPTY))
if (spec.declaredName != null) {
scope.addItem(spec.declaredName, false, instance)
}
return instance
}
if (spec.isExtern) {
val rec = scope[spec.className]
val existing = rec?.value as? ObjClass
val resolved = if (existing != null) {
existing
} else if (spec.className.contains('.')) {
scope.resolveQualifiedIdentifier(spec.className) as? ObjClass
} else {
null
}
val stub = resolved ?: ObjInstanceClass(spec.className).apply { this.isAbstract = true }
spec.declaredName?.let { scope.addItem(it, false, stub) }
return stub
}
val parentClasses = spec.baseSpecs.map { baseSpec ->
val rec = scope[baseSpec.name]
val cls = rec?.value as? ObjClass
if (cls != null) return@map cls
if (baseSpec.name == "Exception") return@map ObjException.Root
if (rec == null) throw ScriptError(spec.startPos, "unknown base class: ${baseSpec.name}")
throw ScriptError(spec.startPos, "${baseSpec.name} is not a class")
}
val constructorCode = object : Statement() {
override val pos: Pos = spec.startPos
override suspend fun execute(scope: Scope): Obj {
val instance = scope.thisObj as ObjInstance
return instance
}
}
val newClass = ObjInstanceClass(spec.className, *parentClasses.toTypedArray()).also {
it.isAbstract = spec.isAbstract
it.instanceConstructor = constructorCode
it.constructorMeta = spec.constructorArgs
for (i in parentClasses.indices) {
val argsList = spec.baseSpecs[i].args
if (argsList != null) it.directParentArgs[parentClasses[i]] = argsList
}
spec.constructorArgs?.params?.forEach { p ->
if (p.accessType != null) {
it.createField(
p.name,
ObjNull,
isMutable = p.accessType == AccessType.Var,
visibility = p.visibility ?: Visibility.Public,
declaringClass = it,
pos = Pos.builtIn,
isTransient = p.isTransient,
type = ObjRecord.Type.ConstructorField,
fieldId = spec.constructorFieldIds?.get(p.name)
)
}
}
}
spec.declaredName?.let { scope.addItem(it, false, newClass) }
val classScope = scope.createChildScope(newThisObj = newClass)
classScope.currentClassCtx = newClass
newClass.classScope = classScope
spec.bodyInit?.execute(classScope)
if (spec.initScope.isNotEmpty()) {
for (s in spec.initScope) {
s.execute(classScope)
}
}
newClass.checkAbstractSatisfaction(spec.startPos)
return newClass
}
class ClassDeclStatement(
val executable: DeclExecutable,
private val startPos: Pos,
val declaredName: String?,
val spec: ClassDeclSpec,
) : Statement() {
override val pos: Pos = startPos
override val pos: Pos = spec.startPos
val declaredName: String? get() = spec.declaredName
val typeName: String get() = spec.typeName
override suspend fun execute(scope: Scope): Obj {
return executable.execute(scope)
return executeClassDecl(scope, spec)
}
override suspend fun callOn(scope: Scope): Obj {
val target = scope.parent ?: scope
return executable.execute(target)
return executeClassDecl(target, spec)
}
}

View File

@ -5959,38 +5959,22 @@ class Compiler(
val initScope = popInitScope()
val declStatement = object : Statement() {
override val pos: Pos = startPos
override suspend fun execute(scope: Scope): Obj {
val parentClasses = baseSpecs.map { baseSpec ->
val rec = scope[baseSpec.name] ?: throw ScriptError(startPos, "unknown base class: ${baseSpec.name}")
(rec.value as? ObjClass) ?: throw ScriptError(startPos, "${baseSpec.name} is not a class")
}
val newClass = ObjInstanceClass(className, *parentClasses.toTypedArray())
newClass.isAnonymous = nameToken == null
newClass.constructorMeta = ArgsDeclaration(emptyList(), Token.Type.RPAREN)
for (i in parentClasses.indices) {
val argsList = baseSpecs[i].args
// In object, we evaluate parent args once at creation time
if (argsList != null) newClass.directParentArgs[parentClasses[i]] = argsList
}
val classScope = scope.createChildScope(newThisObj = newClass)
classScope.currentClassCtx = newClass
newClass.classScope = classScope
classScope.addConst("object", newClass)
bodyInit?.execute(classScope)
// Create instance (singleton)
val instance = newClass.callOn(scope.createChildScope(Arguments.EMPTY))
if (declaredName != null)
scope.addItem(declaredName, false, instance)
return instance
}
}
return ClassDeclStatement(StatementDeclExecutable(declStatement), startPos, className)
val spec = ClassDeclSpec(
declaredName = declaredName,
className = className,
typeName = className,
startPos = startPos,
isExtern = false,
isAbstract = false,
isObject = true,
isAnonymous = nameToken == null,
baseSpecs = baseSpecs.map { ClassDeclBaseSpec(it.name, it.args) },
constructorArgs = null,
constructorFieldIds = null,
bodyInit = bodyInit,
initScope = emptyList()
)
return ClassDeclStatement(spec)
}
private suspend fun parseClassDeclaration(isAbstract: Boolean = false, isExtern: Boolean = false): Statement {
@ -6313,91 +6297,23 @@ class Compiler(
// create instance constructor
// create custom objClass with all fields and instance constructor
val constructorCode = object : Statement() {
override val pos: Pos = startPos
override suspend fun execute(scope: Scope): Obj {
// constructor code is registered with class instance and is called over
// new `thisObj` already set by class to ObjInstance.instanceContext
val instance = scope.thisObj as ObjInstance
// Constructor parameters have been assigned to instance scope by ObjClass.callOn before
// invoking parent/child constructors.
// IMPORTANT: do not execute class body here; class body was executed once in the class scope
// to register methods and prepare initializers. Instance constructor should be empty unless
// we later add explicit constructor body syntax.
return instance
}
}
val classInfo = compileClassInfos[className]
val classDeclStatement = object : Statement() {
override val pos: Pos = startPos
override suspend fun execute(scope: Scope): Obj {
// the main statement should create custom ObjClass instance with field
// accessors, constructor registration, etc.
if (isExtern) {
val rec = scope[className]
val existing = rec?.value as? ObjClass
val resolved = existing ?: resolveClassByName(className)
val stub = resolved ?: ObjInstanceClass(className).apply { this.isAbstract = true }
scope.addItem(declaredName, false, stub)
return stub
}
// Resolve parent classes by name at execution time
val parentClasses = baseSpecs.map { baseSpec ->
val rec = scope[baseSpec.name]
val cls = rec?.value as? ObjClass
if (cls != null) return@map cls
if (baseSpec.name == "Exception") return@map ObjException.Root
if (rec == null) throw ScriptError(nameToken.pos, "unknown base class: ${baseSpec.name}")
throw ScriptError(nameToken.pos, "${baseSpec.name} is not a class")
}
val newClass = ObjInstanceClass(className, *parentClasses.toTypedArray()).also {
it.isAbstract = isAbstract
it.instanceConstructor = constructorCode
it.constructorMeta = constructorArgsDeclaration
// Attach per-parent constructor args (thunks) if provided
for (i in parentClasses.indices) {
val argsList = baseSpecs[i].args
if (argsList != null) it.directParentArgs[parentClasses[i]] = argsList
}
// Register constructor fields in the class members
constructorArgsDeclaration?.params?.forEach { p ->
if (p.accessType != null) {
it.createField(
p.name, ObjNull,
isMutable = p.accessType == AccessType.Var,
visibility = p.visibility ?: Visibility.Public,
declaringClass = it,
// Constructor fields are not currently supporting override/closed in parser
// but we should pass Pos.builtIn to skip validation for now if needed,
// or p.pos to allow it.
pos = Pos.builtIn,
isTransient = p.isTransient,
type = ObjRecord.Type.ConstructorField,
fieldId = classInfo?.fieldIds?.get(p.name)
val spec = ClassDeclSpec(
declaredName = declaredName,
className = className,
typeName = className,
startPos = startPos,
isExtern = isExtern,
isAbstract = isAbstract,
isObject = false,
isAnonymous = false,
baseSpecs = baseSpecs.map { ClassDeclBaseSpec(it.name, it.args) },
constructorArgs = constructorArgsDeclaration,
constructorFieldIds = classInfo?.fieldIds,
bodyInit = bodyInit,
initScope = initScope
)
}
}
}
scope.addItem(declaredName, false, newClass)
// Prepare class scope for class-scope members (static) and future registrations
val classScope = scope.createChildScope(newThisObj = newClass)
// Set lexical class context for visibility tagging inside class body
classScope.currentClassCtx = newClass
newClass.classScope = classScope
// Execute class body once in class scope to register instance methods and prepare instance field initializers
bodyInit?.execute(classScope)
if (initScope.isNotEmpty()) {
for (s in initScope)
s.execute(classScope)
}
newClass.checkAbstractSatisfaction(nameToken.pos)
// Debug summary: list registered instance methods and class-scope functions for this class
return newClass
}
}
ClassDeclStatement(StatementDeclExecutable(classDeclStatement), startPos, qualifiedName)
ClassDeclStatement(spec)
}
}
@ -7224,7 +7140,7 @@ class Compiler(
}
}
if (unwrapped is ClassDeclStatement) {
unwrapped.declaredName?.let { return resolveClassByName(it) }
return resolveClassByName(unwrapped.typeName)
}
val directRef = unwrapDirectRef(unwrapped)
return when (directRef) {
@ -7233,7 +7149,7 @@ class Compiler(
is RangeRef -> ObjRange.type
is StatementRef -> {
val decl = directRef.statement as? ClassDeclStatement
decl?.declaredName?.let { resolveClassByName(it) }
decl?.let { resolveClassByName(it.typeName) }
}
is ValueFnRef -> lambdaReturnTypeByRef[directRef]
is CastRef -> resolveTypeRefClass(directRef.castTypeRef())

View File

@ -161,7 +161,7 @@ class BytecodeCompiler(
)
}
is net.sergeych.lyng.ClassDeclStatement -> {
val value = emitStatementCall(stmt)
val value = emitDeclClass(stmt)
builder.emit(Opcode.RET, value.slot)
val localCount = maxOf(nextSlot, value.slot + 1) - scopeSlotCount
builder.build(
@ -4108,7 +4108,6 @@ class BytecodeCompiler(
private fun emitDeclExec(stmt: Statement): CompiledValue {
val executable = when (stmt) {
is net.sergeych.lyng.ClassDeclStatement -> stmt.executable
else -> throw BytecodeCompileException(
"Bytecode compile error: unsupported declaration ${stmt::class.simpleName}",
stmt.pos
@ -4144,6 +4143,14 @@ class BytecodeCompiler(
return CompiledValue(dst, SlotType.OBJ)
}
private fun emitDeclClass(stmt: net.sergeych.lyng.ClassDeclStatement): CompiledValue {
val constId = builder.addConst(BytecodeConst.ClassDecl(stmt.spec))
val dst = allocSlot()
builder.emit(Opcode.DECL_CLASS, constId, dst)
updateSlotType(dst, SlotType.OBJ)
return CompiledValue(dst, SlotType.OBJ)
}
private fun compileStatementValueOrFallback(stmt: Statement, needResult: Boolean = true): CompiledValue? {
val target = if (stmt is BytecodeStatement) stmt.original else stmt
setPos(target.pos)
@ -4177,7 +4184,7 @@ class BytecodeCompiler(
is DelegatedVarDeclStatement -> emitDelegatedVarDecl(target)
is DestructuringVarDeclStatement -> emitDestructuringVarDecl(target)
is net.sergeych.lyng.ExtensionPropertyDeclStatement -> emitExtensionPropertyDecl(target)
is net.sergeych.lyng.ClassDeclStatement -> emitDeclExec(target)
is net.sergeych.lyng.ClassDeclStatement -> emitDeclClass(target)
is net.sergeych.lyng.FunctionDeclStatement -> emitDeclFunction(target)
is net.sergeych.lyng.EnumDeclStatement -> emitDeclEnum(target)
is net.sergeych.lyng.TryStatement -> emitTry(target, true)
@ -4210,7 +4217,7 @@ class BytecodeCompiler(
is VarDeclStatement -> emitVarDecl(target)
is DelegatedVarDeclStatement -> emitDelegatedVarDecl(target)
is IfStatement -> compileIfStatement(target)
is net.sergeych.lyng.ClassDeclStatement -> emitDeclExec(target)
is net.sergeych.lyng.ClassDeclStatement -> emitDeclClass(target)
is net.sergeych.lyng.FunctionDeclStatement -> emitDeclFunction(target)
is net.sergeych.lyng.EnumDeclStatement -> emitDeclEnum(target)
is net.sergeych.lyng.ForInStatement -> {

View File

@ -47,6 +47,9 @@ sealed class BytecodeConst {
data class FunctionDecl(
val spec: net.sergeych.lyng.FunctionDeclSpec,
) : BytecodeConst()
data class ClassDecl(
val spec: net.sergeych.lyng.ClassDeclSpec,
) : BytecodeConst()
data class SlotPlan(val plan: Map<String, Int>, val captures: List<String> = emptyList()) : BytecodeConst()
data class CaptureTable(val entries: List<BytecodeCaptureEntry>) : BytecodeConst()
data class ExtensionPropertyDecl(

View File

@ -157,7 +157,7 @@ class CmdBuilder {
Opcode.PUSH_TRY ->
listOf(OperandKind.SLOT, OperandKind.IP, OperandKind.IP)
Opcode.DECL_LOCAL, Opcode.DECL_EXT_PROPERTY, Opcode.DECL_DELEGATED, Opcode.DECL_DESTRUCTURE,
Opcode.DECL_EXEC, Opcode.DECL_ENUM, Opcode.DECL_FUNCTION,
Opcode.DECL_EXEC, Opcode.DECL_ENUM, Opcode.DECL_FUNCTION, Opcode.DECL_CLASS,
Opcode.ASSIGN_DESTRUCTURE ->
listOf(OperandKind.CONST, OperandKind.SLOT)
Opcode.ADD_INT, Opcode.SUB_INT, Opcode.MUL_INT, Opcode.DIV_INT, Opcode.MOD_INT,
@ -413,6 +413,7 @@ class CmdBuilder {
Opcode.DECL_EXEC -> CmdDeclExec(operands[0], operands[1])
Opcode.DECL_ENUM -> CmdDeclEnum(operands[0], operands[1])
Opcode.DECL_FUNCTION -> CmdDeclFunction(operands[0], operands[1])
Opcode.DECL_CLASS -> CmdDeclClass(operands[0], operands[1])
Opcode.DECL_EXT_PROPERTY -> CmdDeclExtProperty(operands[0], operands[1])
Opcode.CALL_DIRECT -> CmdCallDirect(operands[0], operands[1], operands[2], operands[3])
Opcode.ASSIGN_DESTRUCTURE -> CmdAssignDestructure(operands[0], operands[1])

View File

@ -214,6 +214,7 @@ object CmdDisassembler {
is CmdDeclExec -> Opcode.DECL_EXEC to intArrayOf(cmd.constId, cmd.slot)
is CmdDeclEnum -> Opcode.DECL_ENUM to intArrayOf(cmd.constId, cmd.slot)
is CmdDeclFunction -> Opcode.DECL_FUNCTION to intArrayOf(cmd.constId, cmd.slot)
is CmdDeclClass -> Opcode.DECL_CLASS to intArrayOf(cmd.constId, cmd.slot)
is CmdDeclExtProperty -> Opcode.DECL_EXT_PROPERTY to intArrayOf(cmd.constId, cmd.slot)
is CmdCallDirect -> Opcode.CALL_DIRECT to intArrayOf(cmd.id, cmd.argBase, cmd.argCount, cmd.dst)
is CmdAssignDestructure -> Opcode.ASSIGN_DESTRUCTURE to intArrayOf(cmd.constId, cmd.slot)
@ -285,7 +286,7 @@ object CmdDisassembler {
Opcode.PUSH_TRY ->
listOf(OperandKind.SLOT, OperandKind.IP, OperandKind.IP)
Opcode.DECL_LOCAL, Opcode.DECL_EXT_PROPERTY, Opcode.DECL_DELEGATED, Opcode.DECL_DESTRUCTURE,
Opcode.DECL_EXEC, Opcode.DECL_ENUM, Opcode.DECL_FUNCTION,
Opcode.DECL_EXEC, Opcode.DECL_ENUM, Opcode.DECL_FUNCTION, Opcode.DECL_CLASS,
Opcode.ASSIGN_DESTRUCTURE ->
listOf(OperandKind.CONST, OperandKind.SLOT)
Opcode.ADD_INT, Opcode.SUB_INT, Opcode.MUL_INT, Opcode.DIV_INT, Opcode.MOD_INT,

View File

@ -1354,6 +1354,16 @@ class CmdDeclFunction(internal val constId: Int, internal val slot: Int) : Cmd()
}
}
class CmdDeclClass(internal val constId: Int, internal val slot: Int) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
val decl = frame.fn.constants[constId] as? BytecodeConst.ClassDecl
?: error("DECL_CLASS expects ClassDecl at $constId")
val result = executeClassDecl(frame.ensureScope(), decl.spec)
frame.storeObjResult(slot, result)
return
}
}
class CmdDeclDestructure(internal val constId: Int, internal val slot: Int) : Cmd() {
override suspend fun perform(frame: CmdFrame) {
val decl = frame.fn.constants[constId] as? BytecodeConst.DestructureDecl

View File

@ -167,6 +167,7 @@ enum class Opcode(val code: Int) {
DELEGATED_SET_LOCAL(0xC3),
BIND_DELEGATE_LOCAL(0xC4),
DECL_FUNCTION(0xC5),
DECL_CLASS(0xC6),
;
companion object {

View File

@ -260,7 +260,9 @@ class BytecodeRecentOpsTest {
assertNotNull(moduleFn, "module bytecode missing")
val disasm = CmdDisassembler.disassemble(moduleFn)
assertTrue(!disasm.contains("CALL_SLOT"), disasm)
assertTrue(disasm.contains("DECL_EXEC"), disasm)
assertTrue(disasm.contains("DECL_CLASS"), disasm)
assertTrue(disasm.contains("DECL_FUNCTION"), disasm)
assertTrue(disasm.contains("DECL_ENUM"), disasm)
}
@Test