Step 24A: bytecode capture tables for lambdas

This commit is contained in:
Sergey Chernov 2026-02-10 04:19:52 +03:00
parent 3ce9029162
commit 8f1c660f4e
7 changed files with 104 additions and 8 deletions

View File

@ -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] Force delegated locals into local slots (even module) and avoid scope-slot resolution.
- [x] Drop opcode/runtime support for `ASSIGN_SCOPE_SLOT`. - [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) ## Interpreter Removal (next)
- [x] Step 25: Replace Statement-based declaration calls in bytecode. - [ ] Step 25: Replace Statement-based declaration calls in bytecode.
- [x] Add bytecode const/op for class/enum/function declarations (no `Statement` objects in constants). - [ ] Add bytecode const/op for class/enum/function declarations (no `Statement` objects in constants).
- [x] Replace `emitStatementCall` usage for `ClassDeclStatement`, `FunctionDeclStatement`, `EnumDeclStatement`. - [ ] Replace `emitStatementCall` usage for `ClassDeclStatement`, `FunctionDeclStatement`, `EnumDeclStatement`.
- [x] Add JVM disasm coverage to ensure module init has no `CALL_SLOT` to `Callable@...` for declarations. - [ ] 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). - [ ] 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. - [ ] 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. - [ ] Remove `containsValueFnRef`/`forceScopeSlots` workaround once lambdas are bytecode.

View File

@ -168,6 +168,8 @@ class Compiler(
private val callableReturnTypeByScopeId: MutableMap<Int, MutableMap<Int, ObjClass>> = mutableMapOf() private val callableReturnTypeByScopeId: MutableMap<Int, MutableMap<Int, ObjClass>> = mutableMapOf()
private val callableReturnTypeByName: MutableMap<String, ObjClass> = mutableMapOf() private val callableReturnTypeByName: MutableMap<String, ObjClass> = mutableMapOf()
private val lambdaReturnTypeByRef: MutableMap<ObjRef, ObjClass> = mutableMapOf() private val lambdaReturnTypeByRef: MutableMap<ObjRef, ObjClass> = mutableMapOf()
private val lambdaCaptureEntriesByRef: MutableMap<ValueFnRef, List<net.sergeych.lyng.bytecode.LambdaCaptureEntry>> =
mutableMapOf()
private val classFieldTypesByName: MutableMap<String, MutableMap<String, ObjClass>> = mutableMapOf() private val classFieldTypesByName: MutableMap<String, MutableMap<String, ObjClass>> = mutableMapOf()
private val classScopeMembersByClassName: MutableMap<String, MutableSet<String>> = mutableMapOf() private val classScopeMembersByClassName: MutableMap<String, MutableSet<String>> = mutableMapOf()
private val classScopeCallableMembersByClassName: MutableMap<String, MutableSet<String>> = mutableMapOf() private val classScopeCallableMembersByClassName: MutableMap<String, MutableSet<String>> = mutableMapOf()
@ -1561,7 +1563,8 @@ class Compiler(
classFieldTypesByName = classFieldTypesByName, classFieldTypesByName = classFieldTypesByName,
enumEntriesByName = enumEntriesByName, enumEntriesByName = enumEntriesByName,
callableReturnTypeByScopeId = callableReturnTypeByScopeId, callableReturnTypeByScopeId = callableReturnTypeByScopeId,
callableReturnTypeByName = callableReturnTypeByName callableReturnTypeByName = callableReturnTypeByName,
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
) as BytecodeStatement ) as BytecodeStatement
unwrapped to bytecodeStmt.bytecodeFunction() unwrapped to bytecodeStmt.bytecodeFunction()
} else { } else {
@ -1857,7 +1860,8 @@ class Compiler(
classFieldTypesByName = classFieldTypesByName, classFieldTypesByName = classFieldTypesByName,
enumEntriesByName = enumEntriesByName, enumEntriesByName = enumEntriesByName,
callableReturnTypeByScopeId = callableReturnTypeByScopeId, callableReturnTypeByScopeId = callableReturnTypeByScopeId,
callableReturnTypeByName = callableReturnTypeByName callableReturnTypeByName = callableReturnTypeByName,
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
) )
} }
@ -1892,7 +1896,8 @@ class Compiler(
classFieldTypesByName = classFieldTypesByName, classFieldTypesByName = classFieldTypesByName,
enumEntriesByName = enumEntriesByName, enumEntriesByName = enumEntriesByName,
callableReturnTypeByScopeId = callableReturnTypeByScopeId, callableReturnTypeByScopeId = callableReturnTypeByScopeId,
callableReturnTypeByName = callableReturnTypeByName callableReturnTypeByName = callableReturnTypeByName,
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
) )
} }
@ -2929,6 +2934,22 @@ class Compiler(
if (returnClass != null) { if (returnClass != null) {
lambdaReturnTypeByRef[ref] = returnClass 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 return ref
} }

View File

@ -33,6 +33,7 @@ class BytecodeCompiler(
private val enumEntriesByName: Map<String, List<String>> = emptyMap(), private val enumEntriesByName: Map<String, List<String>> = emptyMap(),
private val callableReturnTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(), private val callableReturnTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(),
private val callableReturnTypeByName: Map<String, ObjClass> = emptyMap(), private val callableReturnTypeByName: Map<String, ObjClass> = emptyMap(),
private val lambdaCaptureEntriesByRef: Map<ValueFnRef, List<LambdaCaptureEntry>> = emptyMap(),
) { ) {
private var builder = CmdBuilder() private var builder = CmdBuilder()
private var nextSlot = 0 private var nextSlot = 0
@ -607,6 +608,9 @@ class BytecodeCompiler(
} }
private fun compileValueFnRef(ref: ValueFnRef): CompiledValue? { private fun compileValueFnRef(ref: ValueFnRef): CompiledValue? {
lambdaCaptureEntriesByRef[ref]?.let { captures ->
builder.addConst(BytecodeConst.CaptureTable(captures))
}
val id = builder.addConst(BytecodeConst.ValueFn(ref.valueFn())) val id = builder.addConst(BytecodeConst.ValueFn(ref.valueFn()))
val slot = allocSlot() val slot = allocSlot()
builder.emit(Opcode.MAKE_VALUE_FN, id, slot) builder.emit(Opcode.MAKE_VALUE_FN, id, slot)

View File

@ -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 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 DeclExec(val executable: net.sergeych.lyng.DeclExecutable) : BytecodeConst()
data class SlotPlan(val plan: Map<String, Int>, val captures: List<String> = emptyList()) : BytecodeConst() data class SlotPlan(val plan: Map<String, Int>, val captures: List<String> = emptyList()) : BytecodeConst()
data class CaptureTable(val entries: List<LambdaCaptureEntry>) : BytecodeConst()
data class ExtensionPropertyDecl( data class ExtensionPropertyDecl(
val extTypeName: String, val extTypeName: String,
val property: ObjProperty, val property: ObjProperty,

View File

@ -20,6 +20,7 @@ package net.sergeych.lyng.bytecode
import net.sergeych.lyng.* import net.sergeych.lyng.*
import net.sergeych.lyng.obj.Obj import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjClass import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.ValueFnRef
class BytecodeStatement private constructor( class BytecodeStatement private constructor(
val original: Statement, val original: Statement,
@ -50,6 +51,7 @@ class BytecodeStatement private constructor(
enumEntriesByName: Map<String, List<String>> = emptyMap(), enumEntriesByName: Map<String, List<String>> = emptyMap(),
callableReturnTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(), callableReturnTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(),
callableReturnTypeByName: Map<String, ObjClass> = emptyMap(), callableReturnTypeByName: Map<String, ObjClass> = emptyMap(),
lambdaCaptureEntriesByRef: Map<ValueFnRef, List<LambdaCaptureEntry>> = emptyMap(),
): Statement { ): Statement {
if (statement is BytecodeStatement) return statement if (statement is BytecodeStatement) return statement
val hasUnsupported = containsUnsupportedStatement(statement) val hasUnsupported = containsUnsupportedStatement(statement)
@ -73,7 +75,8 @@ class BytecodeStatement private constructor(
classFieldTypesByName = classFieldTypesByName, classFieldTypesByName = classFieldTypesByName,
enumEntriesByName = enumEntriesByName, enumEntriesByName = enumEntriesByName,
callableReturnTypeByScopeId = callableReturnTypeByScopeId, callableReturnTypeByScopeId = callableReturnTypeByScopeId,
callableReturnTypeByName = callableReturnTypeByName callableReturnTypeByName = callableReturnTypeByName,
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
) )
val compiled = compiler.compileStatement(nameHint, statement) val compiled = compiler.compileStatement(nameHint, statement)
val fn = compiled ?: throw BytecodeCompileException( val fn = compiled ?: throw BytecodeCompileException(

View File

@ -50,6 +50,23 @@ object CmdDisassembler {
} }
out.append('\n') 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() return out.toString()
} }

View File

@ -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,
)