diff --git a/bytecode_migration_plan.md b/bytecode_migration_plan.md index 2dd5862..f768eab 100644 --- a/bytecode_migration_plan.md +++ b/bytecode_migration_plan.md @@ -1,175 +1,7 @@ -# Bytecode Migration Plan +# Bytecode Migration Plan (Archived) -Goal: migrate the compiler so all values live in frames/bytecode, keeping JVM tests green after each step. +Status: completed. -## Steps - -- [x] Step 1: Module/import slots seeded into module frame; bytecode module resolution works across closures. -- [x] Step 2: Allow implicit/qualified `this` member refs to compile to bytecode. - - [x] Enable bytecode for `ImplicitThisMethodCallRef`, `QualifiedThisMethodSlotCallRef`, `QualifiedThisFieldSlotRef`. - - [x] Keep unsupported cases blocked: `ClassScopeMemberRef`, dynamic receivers, delegated members. - - [x] JVM tests must be green before commit. -- [x] Step 3: Bytecode support for `try/catch/finally`. - - [x] Implement bytecode emission for try/catch and finally blocks. - - [x] Preserve existing error/stack semantics. - - [x] JVM tests must be green before commit. - -## Remaining Migration (prioritized) - -- [x] Step 4: Allow bytecode wrapping for supported declaration statements. - - [x] Enable `DestructuringVarDeclStatement` and `ExtensionPropertyDeclStatement` in `containsUnsupportedForBytecode`. - - [x] Keep JVM tests green before commit. -- [x] Step 5: Enable bytecode for delegated var declarations. - - [x] Revisit `containsDelegatedRefs` guard for `DelegatedVarDeclStatement`. - - [x] Ensure delegate binding uses explicit `Statement` objects (no inline suspend lambdas). - - [x] Keep JVM tests green before commit. -- [x] Step 6: Map literal spread in bytecode. - - [x] Replace `MapLiteralEntry.Spread` bytecode exception with runtime `putAll`/merge logic. -- [x] Step 7: Class-scope member refs in bytecode. - - [x] Support `ClassScopeMemberRef` without scope-map fallback. -- [x] Step 8: ObjDynamic member access in bytecode. - - [x] Allow dynamic receiver field/method lookup without falling back to interpreter. -- [x] Step 9: Module-level bytecode execution. - - [x] Compile `Script` bodies to bytecode instead of interpreting at module scope. - - [x] Keep import/module slot seeding in frame-only flow. -- [x] Step 10: Bytecode for declaration statements in module scripts. - - [x] Support `ClassDeclStatement`, `FunctionDeclStatement`, `EnumDeclStatement` in bytecode compilation. - - [x] Keep a mixed execution path for declarations (module bytecode calls statement bodies via `CALL_SLOT`). - - [x] Ensure module object member refs compile as instance access (not class-scope). -- [x] Step 11: Destructuring assignment bytecode. - - [x] Handle `[a, b] = expr` (AssignRef target `ListLiteralRef`) without interpreter fallback. -- [x] Step 12: Optional member assign-ops and inc/dec in bytecode. - - [x] Support `a?.b += 1` and `a?.b++` for `FieldRef` targets. - - [x] Fix post-inc return value for object slots stored in scope frames. - - [x] Handle optional receivers for member assign-ops and inc/dec without evaluating operands on null. - - [x] Support class-scope and index optional inc/dec paths in bytecode. -- [x] Step 13: Qualified `this` value refs in bytecode. - - [x] Compile `QualifiedThisRef` (`this@Type`) via `LOAD_THIS_VARIANT`. - - [x] Add a JVM test that evaluates `this@Type` as a value inside nested classes. -- [x] Step 14: Fast local ref reads in bytecode. - - [x] Support `FastLocalVarRef` reads with the same slot resolution as `LocalVarRef`. - - [x] If `BoundLocalVarRef` is still emitted, map it to a direct slot read instead of failing. - - [x] Add a JVM test that exercises fast-local reads in a bytecode-compiled function. -- [x] Step 15: Class-scope `?=` in bytecode. - - [x] Handle `C.x ?= v` and `C?.x ?= v` for class-scope members without falling back. - - [x] Add a JVM test for class-scope `?=` on static vars. -- [x] Step 16: Remove dead `ToBoolStatement`. - - [x] Confirm no parser/compiler paths construct `ToBoolStatement` and delete it plus interpreter hooks. - - [x] Keep JVM tests green after removal. -- [x] Step 17: Callable property calls in bytecode. - - [x] Support `CallRef` where the target is a `FieldRef` (e.g., `(obj.fn)()`), keeping compile-time resolution. - - [x] Add a JVM test for a callable property call compiled to bytecode. -- [x] Step 18: Delegated member access in bytecode. - - [x] Remove `containsDelegatedRefs` guard once bytecode emits delegated get/set/call correctly. - - [x] Add JVM coverage for delegated member get/set/call in bytecode. -- [x] Step 19: Unknown receiver member access in bytecode. - - [x] Reject Object/unknown receiver member calls without explicit cast or Dynamic. - - [x] Add union-member dispatch with ordered type checks and runtime mismatch error. - - [x] Add JVM tests for unknown receiver and union member access. -- [x] Step 20: Bytecode support for `NopStatement`. - - [x] Allow `NopStatement` in `containsUnsupportedForBytecode`. - - [x] Emit `ObjVoid` directly in bytecode for `NopStatement` in statement/value contexts. - - [x] Add a JVM test that exercises a code path returning `NopStatement` in bytecode (e.g., static class member decl in class body). -- [x] Step 21: Union mismatch path in bytecode. - - [x] Replace `UnionTypeMismatchStatement` branch with a bytecode-compilable throw path (no custom `StatementRef` that blocks bytecode). - - [x] Add a JVM test that forces the union mismatch at runtime and asserts the error message. -- [x] Step 22: Delegated local slots in bytecode. - - [x] Support reads/writes/assign-ops/inc/dec for delegated locals (`LocalSlotRef.isDelegated`) in `BytecodeCompiler`. - - [x] Remove `containsDelegatedRefs` guard once delegated locals are bytecode-safe. - - [x] Add JVM tests that use delegated locals inside bytecode-compiled functions. -- [x] Step 23: Refactor delegated locals to keep delegate objects in frame slots. - - [x] Add bytecode ops to bind/get/set delegated locals without scope storage. - - [x] Store delegated locals in frame slots and compile get/set/assign ops with new ops. - - [x] Preserve reflection facade by syncing delegated locals into scope only when needed. -- [x] Step 24: Remove `ASSIGN_SCOPE_SLOT` now that delegated locals are always frame-backed. - - [x] Force delegated locals into local slots (even module) and avoid scope-slot resolution. - - [x] Drop opcode/runtime support for `ASSIGN_SCOPE_SLOT`. - -## Frame-Only Execution (new, before interpreter removal) - -- [x] Step 24A: Bytecode capture tables for lambdas (compiler only). - - [x] Emit per-lambda capture tables containing (ownerFrameKind, ownerSlotId). - - [x] Create captures only when detected; do not allocate scope slots. - - [x] Add disassembler output for capture tables. - - [x] JVM tests must be green before commit. -- [x] Step 24B: Frame-slot captures in bytecode runtime. - - [x] Build lambdas from bytecode + capture table (no capture names). - - [x] Read captured values via `FrameSlotRef` only. - - [x] Forbid `resolveCaptureRecord` in bytecode paths; keep only in interpreter. - - [x] JVM tests must be green before commit. -- [x] Step 24C: Remove scope local mirroring in bytecode execution. - - [x] Remove/disable any bytecode runtime code that writes locals into Scope for execution. - - [x] Keep Scope creation only for reflection/Kotlin interop paths. - - [x] JVM tests must be green before commit. -- [x] Step 24D: Eliminate `ClosureScope` usage on bytecode execution paths. - - [x] Avoid `ClosureScope` in bytecode-related call paths (Block/Lambda/ObjDynamic/ObjProperty). - - [x] Keep interpreter path using `ClosureScope` until interpreter removal. - - [x] JVM tests must be green before commit. -- [x] Step 24E: Isolate interpreter-only capture logic. - - [x] Mark `resolveCaptureRecord` paths as interpreter-only. - - [x] Guard or delete any bytecode path that tries to sync captures into scopes. - - [x] JVM tests must be green before commit. - -## Interpreter Removal (next) - -- [ ] Step 25: Replace Statement-based declaration calls in bytecode. - - [x] Add bytecode const/op for enum 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`. - - [x] Replace `emitStatementCall` usage for `ClassDeclStatement`. - - [x] Replace `emitStatementCall` usage for `FunctionDeclStatement`. - - [x] 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). - - [x] Compile lambda bodies to bytecode and emit an opcode to create a callable from bytecode + capture plan. - - [x] Remove `containsValueFnRef` helper now that lambdas are bytecode-backed. - - [x] Remove `forceScopeSlots` branches once no bytecode paths depend on scope slots. - - [x] Add JVM tests for captured locals and delegated locals inside lambdas on the bytecode path. -- [x] Step 27: Remove interpreter opcodes and constants from bytecode runtime. - - [x] Delete `BytecodeConst.ValueFn`, `CmdMakeValueFn`, and `MAKE_VALUE_FN`. - - [x] Delete `BytecodeConst.StatementVal`, `CmdEvalStmt`, and `EVAL_STMT`. - - [x] Add bytecode-backed `::class` via `ClassOperatorRef` + `GET_OBJ_CLASS` to avoid ValueFn for class operator. - - [x] Add a bytecode fallback reporter hook for lambdas to locate remaining non-bytecode cases. - - [x] Remove `emitStatementCall`/`emitStatementEval` once unused. -- [ ] Step 28: Scope as facade only. - - [x] Audit bytecode execution paths for `Statement.execute` usage and remove remaining calls. - - [x] Keep scope sync only for reflection/Kotlin interop, not for execution. - - [x] Replace bytecode entry seeding from Scope with frame-only arg/local binding. - -## Interpreter Removal v2 (2026-02-12) - -Goal: remove interpreter execution paths entirely without regressing semantics; all runtime execution must be bytecode + frame slots. - -- [ ] Step A1: Interpreter path audit (map all remaining runtime `Statement.execute` and scope-lookup paths). - - [x] Audit results (current hotspots): - - `ClassDeclStatement.executeClassDecl`: executes class body/init via `spec.bodyInit?.execute` and `spec.initScope` (Statement list). - - `FunctionDeclStatement.executeFunctionDecl`: class-body delegated function init uses `Statement.execute` in initializer thunk. - - `EnumDeclStatement.execute`: direct execution path exists (bytecode also emits DECL_ENUM). - - `BlockStatement.execute`: creates child scope, applies slot plan/captures, then executes `Script` (which may interpret). - - `Script.execute`: interpreter loop when `moduleBytecode` is null or disabled. - - `ObjClass.addFn/addProperty` wrappers use `ObjNativeCallable` calling into `Statement.execute` for declared bodies. - - Object expressions: class body executed via `executeClassDecl` path (same as above). - - Extension wrappers: `ObjExtensionMethodCallable` uses callable that executes `Statement`. - -- [ ] Step A2: Bytecode-backed class + init. - - Replace class-body and instance init execution with bytecode functions (per-class + per-instance). - - Remove all class init `Statement.execute` calls. - - [x] Introduce `ClassStaticFieldInitStatement` + bytecode ops `DECL_CLASS_FIELD`/`DECL_CLASS_DELEGATED` for static class field init. - -- [ ] Step A3: Bytecode-safe delegated properties/functions and object expressions. - - Use explicit `object : Statement()` where needed. - - No inline suspend lambdas in hot paths. - - Remove interpreter fallbacks. - -- [ ] Step A4: Bytecode for all blocks/lambdas (including class bodies). - - Compile non-module blocks/lambdas to bytecode; eliminate interpreter gate flags. - -- [ ] Step A5: Delete interpreter execution path and dead code. - - Remove interpreter ops/constants and any runtime name-lookup fallbacks. - - Full test suite green. - -## Notes - -- Keep imports bound to module frame slots; no scope map writes for imports. -- Avoid inline suspend lambdas in compiler hot paths; use explicit `object : Statement()`. -- Do not reintroduce bytecode fallback opcodes; all symbol resolution remains compile-time only. +Historical reference: +- `notes/archive/bytecode_migration_plan.md` (full plan) +- `notes/archive/bytecode_migration_plan_completed.md` (summary) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BytecodeExec.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BytecodeExec.kt new file mode 100644 index 0000000..b7d5765 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BytecodeExec.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2026 Sergey S. Chernov + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sergeych.lyng + +import net.sergeych.lyng.bytecode.BytecodeStatement +import net.sergeych.lyng.bytecode.CmdVm +import net.sergeych.lyng.bytecode.seedFrameLocalsFromScope +import net.sergeych.lyng.obj.Obj + +internal suspend fun executeBytecodeWithSeed(scope: Scope, stmt: Statement, label: String): Obj { + val bytecode = when (stmt) { + is BytecodeStatement -> stmt + is BytecodeBodyProvider -> stmt.bytecodeBody() + else -> null + } ?: scope.raiseIllegalState("$label requires bytecode statement") + scope.pos = bytecode.pos + return CmdVm().execute(bytecode.bytecodeFunction(), scope, scope.args) { frame, _ -> + seedFrameLocalsFromScope(frame, scope) + } +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClassDeclStatement.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClassDeclStatement.kt index afa14b7..c64a1a5 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClassDeclStatement.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ClassDeclStatement.kt @@ -74,7 +74,7 @@ internal suspend fun executeClassDecl( newClass.classScope = classScope classScope.addConst("object", newClass) - spec.bodyInit?.let { requireBytecodeBody(scope, it, "object body init").execute(classScope) } + spec.bodyInit?.let { executeBytecodeWithSeed(classScope, it, "object body init") } val instance = newClass.callOn(scope.createChildScope(Arguments.EMPTY)) if (spec.declaredName != null) { @@ -140,7 +140,17 @@ internal suspend fun executeClassDecl( } } - spec.declaredName?.let { scope.addItem(it, false, newClass) } + spec.declaredName?.let { name -> + scope.addItem(name, false, newClass) + val module = scope as? ModuleScope + val frame = module?.moduleFrame + if (module != null && frame != null) { + val idx = module.moduleFrameLocalSlotNames.indexOf(name) + if (idx >= 0) { + frame.setObj(idx, newClass) + } + } + } val classScope = scope.createChildScope(newThisObj = newClass) if (!bodyCaptureRecords.isNullOrEmpty() && !bodyCaptureNames.isNullOrEmpty()) { classScope.captureRecords = bodyCaptureRecords @@ -148,10 +158,10 @@ internal suspend fun executeClassDecl( } classScope.currentClassCtx = newClass newClass.classScope = classScope - spec.bodyInit?.let { requireBytecodeBody(scope, it, "class body init").execute(classScope) } + spec.bodyInit?.let { executeBytecodeWithSeed(classScope, it, "class body init") } if (spec.initScope.isNotEmpty()) { for (s in spec.initScope) { - requireBytecodeBody(scope, s, "class init").execute(classScope) + executeBytecodeWithSeed(classScope, s, "class init") } } newClass.checkAbstractSatisfaction(spec.startPos) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 0086174..9cf8f39 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -19,6 +19,7 @@ package net.sergeych.lyng import net.sergeych.lyng.Compiler.Companion.compile import net.sergeych.lyng.bytecode.BytecodeStatement +import net.sergeych.lyng.bytecode.ForcedLocalSlotInfo import net.sergeych.lyng.bytecode.CmdListLiteral import net.sergeych.lyng.bytecode.CmdMakeRange import net.sergeych.lyng.bytecode.CmdRangeIntBounds @@ -133,15 +134,15 @@ class Compiler( if (plan.slots.containsKey(name)) return plan.slots[name] = SlotEntry(plan.nextIndex, isMutable, isDelegated) plan.nextIndex += 1 + if (!seedingSlotPlan && plan == moduleSlotPlan()) { + moduleDeclaredNames.add(name) + } } private fun declareSlotNameIn(plan: SlotPlan, name: String, isMutable: Boolean, isDelegated: Boolean) { if (plan.slots.containsKey(name)) return plan.slots[name] = SlotEntry(plan.nextIndex, isMutable, isDelegated) plan.nextIndex += 1 - if (!seedingSlotPlan && plan == moduleSlotPlan()) { - moduleDeclaredNames.add(name) - } } private fun declareSlotNameAt( @@ -187,6 +188,20 @@ class Compiler( private val moduleDeclaredNames: MutableSet = mutableSetOf() private var seedingSlotPlan: Boolean = false + private fun moduleForcedLocalSlotInfo(): Map { + val plan = moduleSlotPlan() ?: return emptyMap() + if (plan.slots.isEmpty()) return emptyMap() + val result = LinkedHashMap(plan.slots.size) + for ((name, entry) in plan.slots) { + result[name] = ForcedLocalSlotInfo( + index = entry.index, + isMutable = entry.isMutable, + isDelegated = entry.isDelegated + ) + } + return result + } + private fun seedSlotPlanFromScope(scope: Scope, includeParents: Boolean = false) { val plan = moduleSlotPlan() ?: return seedingSlotPlan = true @@ -963,15 +978,29 @@ class Compiler( resolutionSink?.reference(name, pos) return ref } - val ref = LocalSlotRef( - name, - moduleLoc.slot, - moduleLoc.scopeId, - moduleLoc.isMutable, - moduleLoc.isDelegated, - pos, - strictSlotRefs - ) + val ref = if (capturePlanStack.isEmpty() && moduleLoc.depth > 0) { + LocalSlotRef( + name, + moduleLoc.slot, + moduleLoc.scopeId, + moduleLoc.isMutable, + moduleLoc.isDelegated, + pos, + strictSlotRefs, + captureOwnerScopeId = moduleLoc.scopeId, + captureOwnerSlot = moduleLoc.slot + ) + } else { + LocalSlotRef( + name, + moduleLoc.slot, + moduleLoc.scopeId, + moduleLoc.isMutable, + moduleLoc.isDelegated, + pos, + strictSlotRefs + ) + } resolutionSink?.reference(name, pos) return ref } @@ -983,11 +1012,13 @@ class Compiler( } else { null } - if (seedSlotIndex != null) { + val seedSlotFree = seedSlotIndex != null && + modulePlan.slots.values.none { it.index == seedSlotIndex } + if (seedSlotFree) { declareSlotNameAt( modulePlan, name, - seedSlotIndex, + seedSlotIndex!!, sourceRecord.isMutable, sourceRecord.type == ObjRecord.Type.Delegated ) @@ -1003,15 +1034,33 @@ class Compiler( registerImportBinding(name, resolved.binding, pos) val slot = lookupSlotLocation(name) if (slot != null) { - val ref = LocalSlotRef( - name, - slot.slot, - slot.scopeId, - slot.isMutable, - slot.isDelegated, - pos, - strictSlotRefs - ) + captureLocalRef(name, slot, pos)?.let { ref -> + resolutionSink?.reference(name, pos) + return ref + } + val ref = if (capturePlanStack.isEmpty() && slot.depth > 0) { + LocalSlotRef( + name, + slot.slot, + slot.scopeId, + slot.isMutable, + slot.isDelegated, + pos, + strictSlotRefs, + captureOwnerScopeId = slot.scopeId, + captureOwnerSlot = slot.slot + ) + } else { + LocalSlotRef( + name, + slot.slot, + slot.scopeId, + slot.isMutable, + slot.isDelegated, + pos, + strictSlotRefs + ) + } resolutionSink?.reference(name, pos) return ref } @@ -1202,6 +1251,7 @@ class Compiler( private fun registerImportBinding(name: String, binding: ImportBinding, pos: Pos) { val existing = importBindings[name] ?: run { importBindings[name] = binding + scopeSeedNames.add(name) return } if (!sameImportBinding(existing, binding)) { @@ -1569,6 +1619,11 @@ class Compiler( } while (true) val modulePlan = if (needsSlotPlan) slotPlanIndices(slotPlanStack.last()) else emptyMap() + val forcedLocalInfo = if (useScopeSlots) emptyMap() else moduleForcedLocalSlotInfo() + val forcedLocalScopeId = if (useScopeSlots) null else moduleSlotPlan()?.id + val allowedScopeNames = if (useScopeSlots) modulePlan.keys else null + val scopeSlotNameSet = if (useScopeSlots) scopeSeedNames else null + val moduleScopeId = if (useScopeSlots) null else moduleSlotPlan()?.id val isModuleScript = codeContexts.lastOrNull() is CodeContext.Module && resolutionScriptDepth == 1 val wrapScriptBytecode = compileBytecode && isModuleScript val (finalStatements, moduleBytecode) = if (wrapScriptBytecode) { @@ -1578,9 +1633,11 @@ class Compiler( block, "