From 8f1c660f4e0606a318d7166c5a69768ec41659c4 Mon Sep 17 00:00:00 2001 From: sergeych Date: Tue, 10 Feb 2026 04:19:52 +0300 Subject: [PATCH] Step 24A: bytecode capture tables for lambdas --- bytecode_migration_plan.md | 33 ++++++++++++++++--- .../kotlin/net/sergeych/lyng/Compiler.kt | 27 +++++++++++++-- .../lyng/bytecode/BytecodeCompiler.kt | 4 +++ .../sergeych/lyng/bytecode/BytecodeConst.kt | 1 + .../lyng/bytecode/BytecodeStatement.kt | 5 ++- .../sergeych/lyng/bytecode/CmdDisassembler.kt | 17 ++++++++++ .../lyng/bytecode/LambdaCaptureEntry.kt | 25 ++++++++++++++ 7 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/LambdaCaptureEntry.kt diff --git a/bytecode_migration_plan.md b/bytecode_migration_plan.md index df29fd0..b7f5cfd 100644 --- a/bytecode_migration_plan.md +++ b/bytecode_migration_plan.md @@ -85,12 +85,37 @@ Goal: migrate the compiler so all values live in frames/bytecode, keeping JVM te - [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. +- [ ] Step 24B: Frame-slot captures in bytecode runtime. + - [ ] Build lambdas from bytecode + capture table (no capture names). + - [ ] Read captured values via `FrameSlotRef` only. + - [ ] Forbid `resolveCaptureRecord` in bytecode paths; keep only in interpreter. + - [ ] JVM tests must be green before commit. +- [ ] Step 24C: Remove scope local mirroring in bytecode execution. + - [ ] Remove/disable any bytecode runtime code that writes locals into Scope for execution. + - [ ] Keep Scope creation only for reflection/Kotlin interop paths. + - [ ] JVM tests must be green before commit. +- [ ] Step 24D: Eliminate `ClosureScope` usage on bytecode execution paths. + - [ ] Avoid `ClosureScope` in bytecode-related call paths (Block/Lambda/ObjDynamic/ObjProperty). + - [ ] Keep interpreter path using `ClosureScope` until interpreter removal. + - [ ] JVM tests must be green before commit. +- [ ] Step 24E: Isolate interpreter-only capture logic. + - [ ] Mark `resolveCaptureRecord` paths as interpreter-only. + - [ ] Guard or delete any bytecode path that tries to sync captures into scopes. + - [ ] JVM tests must be green before commit. + ## Interpreter Removal (next) -- [x] Step 25: Replace Statement-based declaration calls in bytecode. - - [x] Add bytecode const/op for class/enum/function declarations (no `Statement` objects in constants). - - [x] Replace `emitStatementCall` usage for `ClassDeclStatement`, `FunctionDeclStatement`, `EnumDeclStatement`. - - [x] Add JVM disasm coverage to ensure module init has no `CALL_SLOT` to `Callable@...` for declarations. +- [ ] Step 25: Replace Statement-based declaration calls in bytecode. + - [ ] Add bytecode const/op for class/enum/function declarations (no `Statement` objects in constants). + - [ ] Replace `emitStatementCall` usage for `ClassDeclStatement`, `FunctionDeclStatement`, `EnumDeclStatement`. + - [ ] 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). - [ ] Compile lambda bodies to bytecode and emit an opcode to create a callable from bytecode + capture plan. - [ ] Remove `containsValueFnRef`/`forceScopeSlots` workaround once lambdas are bytecode. diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 1488b48..827d9f5 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -168,6 +168,8 @@ class Compiler( private val callableReturnTypeByScopeId: MutableMap> = mutableMapOf() private val callableReturnTypeByName: MutableMap = mutableMapOf() private val lambdaReturnTypeByRef: MutableMap = mutableMapOf() + private val lambdaCaptureEntriesByRef: MutableMap> = + mutableMapOf() private val classFieldTypesByName: MutableMap> = mutableMapOf() private val classScopeMembersByClassName: MutableMap> = mutableMapOf() private val classScopeCallableMembersByClassName: MutableMap> = mutableMapOf() @@ -1561,7 +1563,8 @@ class Compiler( classFieldTypesByName = classFieldTypesByName, enumEntriesByName = enumEntriesByName, callableReturnTypeByScopeId = callableReturnTypeByScopeId, - callableReturnTypeByName = callableReturnTypeByName + callableReturnTypeByName = callableReturnTypeByName, + lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef ) as BytecodeStatement unwrapped to bytecodeStmt.bytecodeFunction() } else { @@ -1857,7 +1860,8 @@ class Compiler( classFieldTypesByName = classFieldTypesByName, enumEntriesByName = enumEntriesByName, callableReturnTypeByScopeId = callableReturnTypeByScopeId, - callableReturnTypeByName = callableReturnTypeByName + callableReturnTypeByName = callableReturnTypeByName, + lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef ) } @@ -1892,7 +1896,8 @@ class Compiler( classFieldTypesByName = classFieldTypesByName, enumEntriesByName = enumEntriesByName, callableReturnTypeByScopeId = callableReturnTypeByScopeId, - callableReturnTypeByName = callableReturnTypeByName + callableReturnTypeByName = callableReturnTypeByName, + lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef ) } @@ -2929,6 +2934,22 @@ class Compiler( if (returnClass != null) { lambdaReturnTypeByRef[ref] = returnClass } + val moduleScopeId = moduleSlotPlan()?.id + val captureEntries = captureSlots.map { capture -> + val owner = capturePlan.captureOwners[capture.name] + ?: error("Missing capture owner for ${capture.name}") + val kind = if (moduleScopeId != null && owner.scopeId == moduleScopeId) { + net.sergeych.lyng.bytecode.CaptureOwnerFrameKind.MODULE + } else { + net.sergeych.lyng.bytecode.CaptureOwnerFrameKind.LOCAL + } + net.sergeych.lyng.bytecode.LambdaCaptureEntry( + ownerKind = kind, + ownerScopeId = owner.scopeId, + ownerSlotId = owner.slot + ) + } + lambdaCaptureEntriesByRef[ref] = captureEntries return ref } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt index 67fd030..bc0ec0f 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -33,6 +33,7 @@ class BytecodeCompiler( private val enumEntriesByName: Map> = emptyMap(), private val callableReturnTypeByScopeId: Map> = emptyMap(), private val callableReturnTypeByName: Map = emptyMap(), + private val lambdaCaptureEntriesByRef: Map> = emptyMap(), ) { private var builder = CmdBuilder() private var nextSlot = 0 @@ -607,6 +608,9 @@ class BytecodeCompiler( } private fun compileValueFnRef(ref: ValueFnRef): CompiledValue? { + lambdaCaptureEntriesByRef[ref]?.let { captures -> + builder.addConst(BytecodeConst.CaptureTable(captures)) + } val id = builder.addConst(BytecodeConst.ValueFn(ref.valueFn())) val slot = allocSlot() builder.emit(Opcode.MAKE_VALUE_FN, id, slot) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeConst.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeConst.kt index c51c2ee..ce3b84b 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeConst.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeConst.kt @@ -36,6 +36,7 @@ sealed class BytecodeConst { data class ValueFn(val fn: suspend (net.sergeych.lyng.Scope) -> net.sergeych.lyng.obj.ObjRecord) : BytecodeConst() data class DeclExec(val executable: net.sergeych.lyng.DeclExecutable) : BytecodeConst() data class SlotPlan(val plan: Map, val captures: List = emptyList()) : BytecodeConst() + data class CaptureTable(val entries: List) : BytecodeConst() data class ExtensionPropertyDecl( val extTypeName: String, val property: ObjProperty, diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt index 6bf1bf9..b816f61 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt @@ -20,6 +20,7 @@ package net.sergeych.lyng.bytecode import net.sergeych.lyng.* import net.sergeych.lyng.obj.Obj import net.sergeych.lyng.obj.ObjClass +import net.sergeych.lyng.obj.ValueFnRef class BytecodeStatement private constructor( val original: Statement, @@ -50,6 +51,7 @@ class BytecodeStatement private constructor( enumEntriesByName: Map> = emptyMap(), callableReturnTypeByScopeId: Map> = emptyMap(), callableReturnTypeByName: Map = emptyMap(), + lambdaCaptureEntriesByRef: Map> = emptyMap(), ): Statement { if (statement is BytecodeStatement) return statement val hasUnsupported = containsUnsupportedStatement(statement) @@ -73,7 +75,8 @@ class BytecodeStatement private constructor( classFieldTypesByName = classFieldTypesByName, enumEntriesByName = enumEntriesByName, callableReturnTypeByScopeId = callableReturnTypeByScopeId, - callableReturnTypeByName = callableReturnTypeByName + callableReturnTypeByName = callableReturnTypeByName, + lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef ) val compiled = compiler.compileStatement(nameHint, statement) val fn = compiled ?: throw BytecodeCompileException( diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt index d308be0..f85db1a 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt @@ -50,6 +50,23 @@ object CmdDisassembler { } out.append('\n') } + val captureConsts = fn.constants.withIndex().mapNotNull { (idx, constVal) -> + val table = constVal as? BytecodeConst.CaptureTable ?: return@mapNotNull null + idx to table + } + if (captureConsts.isNotEmpty()) { + out.append("consts:\n") + for ((idx, table) in captureConsts) { + val entries = if (table.entries.isEmpty()) { + "[]" + } else { + table.entries.joinToString(prefix = "[", postfix = "]") { entry -> + "${entry.ownerKind}#${entry.ownerScopeId}:${entry.ownerSlotId}" + } + } + out.append("k").append(idx).append(" CAPTURE_TABLE ").append(entries).append('\n') + } + } return out.toString() } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/LambdaCaptureEntry.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/LambdaCaptureEntry.kt new file mode 100644 index 0000000..e3d74b9 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/LambdaCaptureEntry.kt @@ -0,0 +1,25 @@ +/* + * 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.bytecode + +enum class CaptureOwnerFrameKind { MODULE, LOCAL } + +data class LambdaCaptureEntry( + val ownerKind: CaptureOwnerFrameKind, + val ownerScopeId: Int, + val ownerSlotId: Int, +)