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] Create captures only when detected; do not allocate scope slots.
|
||||||
- [x] Add disassembler output for capture tables.
|
- [x] Add disassembler output for capture tables.
|
||||||
- [x] JVM tests must be green before commit.
|
- [x] JVM tests must be green before commit.
|
||||||
- [ ] Step 24B: Frame-slot captures in bytecode runtime.
|
- [x] Step 24B: Frame-slot captures in bytecode runtime.
|
||||||
- [ ] Build lambdas from bytecode + capture table (no capture names).
|
- [x] Build lambdas from bytecode + capture table (no capture names).
|
||||||
- [ ] Read captured values via `FrameSlotRef` only.
|
- [x] Read captured values via `FrameSlotRef` only.
|
||||||
- [ ] Forbid `resolveCaptureRecord` in bytecode paths; keep only in interpreter.
|
- [x] Forbid `resolveCaptureRecord` in bytecode paths; keep only in interpreter.
|
||||||
- [ ] JVM tests must be green before commit.
|
- [x] JVM tests must be green before commit.
|
||||||
- [ ] Step 24C: Remove scope local mirroring in bytecode execution.
|
- [ ] Step 24C: Remove scope local mirroring in bytecode execution.
|
||||||
- [ ] Remove/disable any bytecode runtime code that writes locals into Scope for execution.
|
- [ ] Remove/disable any bytecode runtime code that writes locals into Scope for execution.
|
||||||
- [ ] Keep Scope creation only for reflection/Kotlin interop paths.
|
- [ ] Keep Scope creation only for reflection/Kotlin interop paths.
|
||||||
|
|||||||
@ -2880,22 +2880,31 @@ class Compiler(
|
|||||||
val context = scope.applyClosure(closureScope, preferredThisType = expectedReceiverType)
|
val context = scope.applyClosure(closureScope, preferredThisType = expectedReceiverType)
|
||||||
if (paramSlotPlanSnapshot.isNotEmpty()) context.applySlotPlan(paramSlotPlanSnapshot)
|
if (paramSlotPlanSnapshot.isNotEmpty()) context.applySlotPlan(paramSlotPlanSnapshot)
|
||||||
if (captureSlots.isNotEmpty()) {
|
if (captureSlots.isNotEmpty()) {
|
||||||
val moduleScope = if (context is ApplyScope) {
|
val captureRecords = closureScope.captureRecords
|
||||||
var s: Scope? = closureScope
|
if (captureRecords != null) {
|
||||||
while (s != null && s !is ModuleScope) {
|
for (i in captureSlots.indices) {
|
||||||
s = s.parent
|
val rec = captureRecords.getOrNull(i)
|
||||||
|
?: closureScope.raiseSymbolNotFound("capture ${captureSlots[i].name} not found")
|
||||||
|
context.updateSlotFor(captureSlots[i].name, rec)
|
||||||
}
|
}
|
||||||
s as? ModuleScope
|
|
||||||
} else {
|
} else {
|
||||||
null
|
val moduleScope = if (context is ApplyScope) {
|
||||||
}
|
var s: Scope? = closureScope
|
||||||
for (capture in captureSlots) {
|
while (s != null && s !is ModuleScope) {
|
||||||
if (moduleScope != null && moduleScope.getLocalRecordDirect(capture.name) != null) {
|
s = s.parent
|
||||||
continue
|
}
|
||||||
|
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
|
// 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.
|
// Enabled by default for child scopes; module/class scopes can ignore it.
|
||||||
private val slots: MutableList<ObjRecord> = mutableListOf()
|
private val slots: MutableList<ObjRecord> = mutableListOf()
|
||||||
private val nameToSlot: MutableMap<String, Int> = mutableMapOf()
|
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).
|
* Auxiliary per-frame map of local bindings (locals declared in this frame).
|
||||||
* This helps resolving locals across suspension when slot ownership isn't
|
* This helps resolving locals across suspension when slot ownership isn't
|
||||||
|
|||||||
@ -608,16 +608,41 @@ class BytecodeCompiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun compileValueFnRef(ref: ValueFnRef): CompiledValue? {
|
private fun compileValueFnRef(ref: ValueFnRef): CompiledValue? {
|
||||||
lambdaCaptureEntriesByRef[ref]?.let { captures ->
|
val captureTableId = lambdaCaptureEntriesByRef[ref]?.let { captures ->
|
||||||
builder.addConst(BytecodeConst.CaptureTable(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()
|
val slot = allocSlot()
|
||||||
builder.emit(Opcode.MAKE_VALUE_FN, id, slot)
|
builder.emit(Opcode.MAKE_VALUE_FN, id, slot)
|
||||||
updateSlotType(slot, SlotType.OBJ)
|
updateSlotType(slot, SlotType.OBJ)
|
||||||
return CompiledValue(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? {
|
private fun compileEvalRef(ref: ObjRef): CompiledValue? {
|
||||||
val refInfo = when (ref) {
|
val refInfo = when (ref) {
|
||||||
is BinaryOpRef -> "BinaryOpRef(${ref.op})"
|
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 Ref(val value: net.sergeych.lyng.obj.ObjRef) : BytecodeConst()
|
||||||
data class StatementVal(val statement: net.sergeych.lyng.Statement) : BytecodeConst()
|
data class StatementVal(val statement: net.sergeych.lyng.Statement) : BytecodeConst()
|
||||||
data class ListLiteralPlan(val spreads: List<Boolean>) : 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 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 CaptureTable(val entries: List<BytecodeCaptureEntry>) : BytecodeConst()
|
||||||
data class ExtensionPropertyDecl(
|
data class ExtensionPropertyDecl(
|
||||||
val extTypeName: String,
|
val extTypeName: String,
|
||||||
val property: ObjProperty,
|
val property: ObjProperty,
|
||||||
|
|||||||
@ -61,7 +61,7 @@ object CmdDisassembler {
|
|||||||
"[]"
|
"[]"
|
||||||
} else {
|
} else {
|
||||||
table.entries.joinToString(prefix = "[", postfix = "]") { entry ->
|
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')
|
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
|
val valueFn = frame.fn.constants.getOrNull(id) as? BytecodeConst.ValueFn
|
||||||
?: error("MAKE_VALUE_FN expects ValueFn at $id")
|
?: 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()) {
|
if (frame.fn.localSlotNames.isNotEmpty()) {
|
||||||
frame.syncScopeToFrame()
|
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 {
|
private fun shouldSyncLocalCaptures(captures: List<String>): Boolean {
|
||||||
if (captures.isEmpty()) return false
|
if (captures.isEmpty()) return false
|
||||||
val localNames = fn.localSlotNames
|
val localNames = fn.localSlotNames
|
||||||
|
|||||||
@ -23,3 +23,10 @@ data class LambdaCaptureEntry(
|
|||||||
val ownerScopeId: Int,
|
val ownerScopeId: Int,
|
||||||
val ownerSlotId: 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