Step 24B: frame-slot captures for bytecode lambdas
This commit is contained in:
parent
8f1c660f4e
commit
6c0b86f6e6
@ -92,11 +92,11 @@ Goal: migrate the compiler so all values live in frames/bytecode, keeping JVM te
|
||||
- [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.
|
||||
- [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.
|
||||
- [ ] 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.
|
||||
|
||||
@ -2880,22 +2880,31 @@ class Compiler(
|
||||
val context = scope.applyClosure(closureScope, preferredThisType = expectedReceiverType)
|
||||
if (paramSlotPlanSnapshot.isNotEmpty()) context.applySlotPlan(paramSlotPlanSnapshot)
|
||||
if (captureSlots.isNotEmpty()) {
|
||||
val moduleScope = if (context is ApplyScope) {
|
||||
var s: Scope? = closureScope
|
||||
while (s != null && s !is ModuleScope) {
|
||||
s = s.parent
|
||||
val captureRecords = closureScope.captureRecords
|
||||
if (captureRecords != null) {
|
||||
for (i in captureSlots.indices) {
|
||||
val rec = captureRecords.getOrNull(i)
|
||||
?: closureScope.raiseSymbolNotFound("capture ${captureSlots[i].name} not found")
|
||||
context.updateSlotFor(captureSlots[i].name, rec)
|
||||
}
|
||||
s as? ModuleScope
|
||||
} else {
|
||||
null
|
||||
}
|
||||
for (capture in captureSlots) {
|
||||
if (moduleScope != null && moduleScope.getLocalRecordDirect(capture.name) != null) {
|
||||
continue
|
||||
val moduleScope = if (context is ApplyScope) {
|
||||
var s: Scope? = closureScope
|
||||
while (s != null && s !is ModuleScope) {
|
||||
s = s.parent
|
||||
}
|
||||
s as? ModuleScope
|
||||
} else {
|
||||
null
|
||||
}
|
||||
for (capture in captureSlots) {
|
||||
if (moduleScope != null && moduleScope.getLocalRecordDirect(capture.name) != null) {
|
||||
continue
|
||||
}
|
||||
val rec = closureScope.resolveCaptureRecord(capture.name)
|
||||
?: closureScope.raiseSymbolNotFound("symbol ${capture.name} not found")
|
||||
context.updateSlotFor(capture.name, rec)
|
||||
}
|
||||
val rec = closureScope.resolveCaptureRecord(capture.name)
|
||||
?: closureScope.raiseSymbolNotFound("symbol ${capture.name} not found")
|
||||
context.updateSlotFor(capture.name, rec)
|
||||
}
|
||||
}
|
||||
// Execute lambda body in a closure-aware context. Blocks inside the lambda
|
||||
|
||||
@ -59,6 +59,7 @@ open class Scope(
|
||||
// Enabled by default for child scopes; module/class scopes can ignore it.
|
||||
private val slots: MutableList<ObjRecord> = mutableListOf()
|
||||
private val nameToSlot: MutableMap<String, Int> = mutableMapOf()
|
||||
internal var captureRecords: List<ObjRecord>? = null
|
||||
/**
|
||||
* Auxiliary per-frame map of local bindings (locals declared in this frame).
|
||||
* This helps resolving locals across suspension when slot ownership isn't
|
||||
|
||||
@ -608,16 +608,41 @@ class BytecodeCompiler(
|
||||
}
|
||||
|
||||
private fun compileValueFnRef(ref: ValueFnRef): CompiledValue? {
|
||||
lambdaCaptureEntriesByRef[ref]?.let { captures ->
|
||||
builder.addConst(BytecodeConst.CaptureTable(captures))
|
||||
val captureTableId = lambdaCaptureEntriesByRef[ref]?.let { captures ->
|
||||
if (captures.isEmpty()) return@let null
|
||||
val resolved = captures.map { entry ->
|
||||
val slotIndex = resolveCaptureSlot(entry)
|
||||
BytecodeCaptureEntry(
|
||||
ownerKind = entry.ownerKind,
|
||||
ownerScopeId = entry.ownerScopeId,
|
||||
ownerSlotId = entry.ownerSlotId,
|
||||
slotIndex = slotIndex
|
||||
)
|
||||
}
|
||||
builder.addConst(BytecodeConst.CaptureTable(resolved))
|
||||
}
|
||||
val id = builder.addConst(BytecodeConst.ValueFn(ref.valueFn()))
|
||||
val id = builder.addConst(BytecodeConst.ValueFn(ref.valueFn(), captureTableId))
|
||||
val slot = allocSlot()
|
||||
builder.emit(Opcode.MAKE_VALUE_FN, id, slot)
|
||||
updateSlotType(slot, SlotType.OBJ)
|
||||
return CompiledValue(slot, SlotType.OBJ)
|
||||
}
|
||||
|
||||
private fun resolveCaptureSlot(entry: LambdaCaptureEntry): Int {
|
||||
val key = ScopeSlotKey(entry.ownerScopeId, entry.ownerSlotId)
|
||||
return when (entry.ownerKind) {
|
||||
CaptureOwnerFrameKind.MODULE -> {
|
||||
scopeSlotMap[key]
|
||||
?: throw BytecodeCompileException("Missing module capture slot for ${entry.ownerScopeId}:${entry.ownerSlotId}", Pos.builtIn)
|
||||
}
|
||||
CaptureOwnerFrameKind.LOCAL -> {
|
||||
val localIndex = localSlotIndexByKey[key]
|
||||
?: throw BytecodeCompileException("Missing local capture slot for ${entry.ownerScopeId}:${entry.ownerSlotId}", Pos.builtIn)
|
||||
scopeSlotCount + localIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun compileEvalRef(ref: ObjRef): CompiledValue? {
|
||||
val refInfo = when (ref) {
|
||||
is BinaryOpRef -> "BinaryOpRef(${ref.op})"
|
||||
|
||||
@ -33,10 +33,13 @@ sealed class BytecodeConst {
|
||||
data class Ref(val value: net.sergeych.lyng.obj.ObjRef) : BytecodeConst()
|
||||
data class StatementVal(val statement: net.sergeych.lyng.Statement) : BytecodeConst()
|
||||
data class ListLiteralPlan(val spreads: List<Boolean>) : 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,
|
||||
val captureTableId: Int? = null,
|
||||
) : 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 CaptureTable(val entries: List<LambdaCaptureEntry>) : BytecodeConst()
|
||||
data class CaptureTable(val entries: List<BytecodeCaptureEntry>) : BytecodeConst()
|
||||
data class ExtensionPropertyDecl(
|
||||
val extTypeName: String,
|
||||
val property: ObjProperty,
|
||||
|
||||
@ -61,7 +61,7 @@ object CmdDisassembler {
|
||||
"[]"
|
||||
} else {
|
||||
table.entries.joinToString(prefix = "[", postfix = "]") { entry ->
|
||||
"${entry.ownerKind}#${entry.ownerScopeId}:${entry.ownerSlotId}"
|
||||
"${entry.ownerKind}#${entry.ownerScopeId}:${entry.ownerSlotId}@s${entry.slotIndex}"
|
||||
}
|
||||
}
|
||||
out.append("k").append(idx).append(" CAPTURE_TABLE ").append(entries).append('\n')
|
||||
|
||||
@ -1869,7 +1869,12 @@ class CmdMakeValueFn(internal val id: Int, internal val dst: Int) : Cmd() {
|
||||
}
|
||||
val valueFn = frame.fn.constants.getOrNull(id) as? BytecodeConst.ValueFn
|
||||
?: error("MAKE_VALUE_FN expects ValueFn at $id")
|
||||
val result = valueFn.fn(frame.ensureScope()).value
|
||||
val scope = frame.ensureScope()
|
||||
val previousCaptures = scope.captureRecords
|
||||
val captureRecords = valueFn.captureTableId?.let { frame.buildCaptureRecords(it) }
|
||||
scope.captureRecords = captureRecords
|
||||
val result = valueFn.fn(scope).value
|
||||
scope.captureRecords = previousCaptures
|
||||
if (frame.fn.localSlotNames.isNotEmpty()) {
|
||||
frame.syncScopeToFrame()
|
||||
}
|
||||
@ -1944,6 +1949,37 @@ class CmdFrame(
|
||||
}
|
||||
}
|
||||
|
||||
internal fun buildCaptureRecords(captureTableId: Int): List<ObjRecord> {
|
||||
val table = fn.constants.getOrNull(captureTableId) as? BytecodeConst.CaptureTable
|
||||
?: error("Capture table $captureTableId missing")
|
||||
return table.entries.map { entry ->
|
||||
when (entry.ownerKind) {
|
||||
CaptureOwnerFrameKind.LOCAL -> {
|
||||
val localIndex = entry.slotIndex - fn.scopeSlotCount
|
||||
if (localIndex < 0) {
|
||||
error("Invalid local capture slot ${entry.slotIndex}")
|
||||
}
|
||||
val isMutable = fn.localSlotMutables.getOrNull(localIndex) ?: false
|
||||
val isDelegated = fn.localSlotDelegated.getOrNull(localIndex) ?: false
|
||||
if (isDelegated) {
|
||||
val delegate = frame.getObj(localIndex)
|
||||
ObjRecord(ObjNull, isMutable, type = ObjRecord.Type.Delegated).also {
|
||||
it.delegate = delegate
|
||||
}
|
||||
} else {
|
||||
ObjRecord(FrameSlotRef(frame, localIndex), isMutable)
|
||||
}
|
||||
}
|
||||
CaptureOwnerFrameKind.MODULE -> {
|
||||
val slot = entry.slotIndex
|
||||
val target = scopeTarget(slot)
|
||||
val index = fn.scopeSlotIndices[slot]
|
||||
target.getSlotRecord(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldSyncLocalCaptures(captures: List<String>): Boolean {
|
||||
if (captures.isEmpty()) return false
|
||||
val localNames = fn.localSlotNames
|
||||
|
||||
@ -23,3 +23,10 @@ data class LambdaCaptureEntry(
|
||||
val ownerScopeId: Int,
|
||||
val ownerSlotId: Int,
|
||||
)
|
||||
|
||||
data class BytecodeCaptureEntry(
|
||||
val ownerKind: CaptureOwnerFrameKind,
|
||||
val ownerScopeId: Int,
|
||||
val ownerSlotId: Int,
|
||||
val slotIndex: Int,
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user