Step 26A: bytecode lambda callables
This commit is contained in:
parent
99ca15d20f
commit
bde32ca7b5
@ -121,9 +121,9 @@ Goal: migrate the compiler so all values live in frames/bytecode, keeping JVM te
|
|||||||
- [x] Replace `emitStatementCall` usage for `FunctionDeclStatement`.
|
- [x] Replace `emitStatementCall` usage for `FunctionDeclStatement`.
|
||||||
- [x] Add JVM disasm coverage to ensure module init has no `CALL_SLOT` to `Callable@...` for declarations.
|
- [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).
|
- [ ] 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.
|
- [x] 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.
|
||||||
- [ ] Add JVM tests for captured locals and delegated locals inside lambdas on the bytecode path.
|
- [x] Add JVM tests for captured locals and delegated locals inside lambdas on the bytecode path.
|
||||||
- [ ] Step 27: Remove interpreter opcodes and constants from bytecode runtime.
|
- [ ] Step 27: Remove interpreter opcodes and constants from bytecode runtime.
|
||||||
- [ ] Delete `BytecodeConst.ValueFn`, `CmdMakeValueFn`, and `MAKE_VALUE_FN`.
|
- [ ] Delete `BytecodeConst.ValueFn`, `CmdMakeValueFn`, and `MAKE_VALUE_FN`.
|
||||||
- [ ] Delete `BytecodeConst.StatementVal`, `CmdEvalStmt`, and `EVAL_STMT`.
|
- [ ] Delete `BytecodeConst.StatementVal`, `CmdEvalStmt`, and `EVAL_STMT`.
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
interface BytecodeCallable
|
||||||
@ -2918,7 +2918,9 @@ class Compiler(
|
|||||||
} else {
|
} else {
|
||||||
body
|
body
|
||||||
}
|
}
|
||||||
val ref = ValueFnRef { closureScope ->
|
val bytecodeFn = (fnStatements as? BytecodeStatement)?.bytecodeFunction()
|
||||||
|
val ref = LambdaFnRef(
|
||||||
|
valueFn = { closureScope ->
|
||||||
val captureRecords = closureScope.captureRecords
|
val captureRecords = closureScope.captureRecords
|
||||||
val stmt = object : Statement(), BytecodeBodyProvider {
|
val stmt = object : Statement(), BytecodeBodyProvider {
|
||||||
override val pos: Pos = fnStatements.pos
|
override val pos: Pos = fnStatements.pos
|
||||||
@ -3009,7 +3011,15 @@ class Compiler(
|
|||||||
stmt
|
stmt
|
||||||
}
|
}
|
||||||
callable.asReadonly
|
callable.asReadonly
|
||||||
}
|
},
|
||||||
|
bytecodeFn = bytecodeFn,
|
||||||
|
paramSlotPlan = paramSlotPlanSnapshot,
|
||||||
|
argsDeclaration = argsDeclaration,
|
||||||
|
preferredThisType = expectedReceiverType,
|
||||||
|
wrapAsExtensionCallable = wrapAsExtensionCallable,
|
||||||
|
returnLabels = returnLabels,
|
||||||
|
pos = startPos
|
||||||
|
)
|
||||||
if (returnClass != null) {
|
if (returnClass != null) {
|
||||||
lambdaReturnTypeByRef[ref] = returnClass
|
lambdaReturnTypeByRef[ref] = returnClass
|
||||||
}
|
}
|
||||||
|
|||||||
@ -623,6 +623,42 @@ class BytecodeCompiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun compileValueFnRef(ref: ValueFnRef): CompiledValue? {
|
private fun compileValueFnRef(ref: ValueFnRef): CompiledValue? {
|
||||||
|
if (ref is LambdaFnRef && ref.bytecodeFn != null) {
|
||||||
|
val captures = lambdaCaptureEntriesByRef[ref].orEmpty()
|
||||||
|
val captureTableId = if (captures.isEmpty()) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
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 captureNames = captures.map { it.ownerName }
|
||||||
|
val id = builder.addConst(
|
||||||
|
BytecodeConst.LambdaFn(
|
||||||
|
fn = ref.bytecodeFn,
|
||||||
|
captureTableId = captureTableId,
|
||||||
|
captureNames = captureNames,
|
||||||
|
paramSlotPlan = ref.paramSlotPlan,
|
||||||
|
argsDeclaration = ref.argsDeclaration,
|
||||||
|
preferredThisType = ref.preferredThisType,
|
||||||
|
wrapAsExtensionCallable = ref.wrapAsExtensionCallable,
|
||||||
|
returnLabels = ref.returnLabels,
|
||||||
|
pos = ref.pos
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val slot = allocSlot()
|
||||||
|
builder.emit(Opcode.MAKE_LAMBDA_FN, id, slot)
|
||||||
|
updateSlotType(slot, SlotType.OBJ)
|
||||||
|
return CompiledValue(slot, SlotType.OBJ)
|
||||||
|
}
|
||||||
|
|
||||||
val captureTableId = lambdaCaptureEntriesByRef[ref]?.let { captures ->
|
val captureTableId = lambdaCaptureEntriesByRef[ref]?.let { captures ->
|
||||||
if (captures.isEmpty()) return@let null
|
if (captures.isEmpty()) return@let null
|
||||||
val resolved = captures.map { entry ->
|
val resolved = captures.map { entry ->
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package net.sergeych.lyng.bytecode
|
package net.sergeych.lyng.bytecode
|
||||||
|
|
||||||
|
import net.sergeych.lyng.ArgsDeclaration
|
||||||
import net.sergeych.lyng.Pos
|
import net.sergeych.lyng.Pos
|
||||||
import net.sergeych.lyng.Visibility
|
import net.sergeych.lyng.Visibility
|
||||||
import net.sergeych.lyng.obj.ListLiteralRef
|
import net.sergeych.lyng.obj.ListLiteralRef
|
||||||
@ -37,6 +38,17 @@ sealed class BytecodeConst {
|
|||||||
val fn: suspend (net.sergeych.lyng.Scope) -> net.sergeych.lyng.obj.ObjRecord,
|
val fn: suspend (net.sergeych.lyng.Scope) -> net.sergeych.lyng.obj.ObjRecord,
|
||||||
val captureTableId: Int? = null,
|
val captureTableId: Int? = null,
|
||||||
) : BytecodeConst()
|
) : BytecodeConst()
|
||||||
|
data class LambdaFn(
|
||||||
|
val fn: CmdFunction,
|
||||||
|
val captureTableId: Int?,
|
||||||
|
val captureNames: List<String>,
|
||||||
|
val paramSlotPlan: Map<String, Int>,
|
||||||
|
val argsDeclaration: ArgsDeclaration?,
|
||||||
|
val preferredThisType: String?,
|
||||||
|
val wrapAsExtensionCallable: Boolean,
|
||||||
|
val returnLabels: Set<String>,
|
||||||
|
val pos: Pos,
|
||||||
|
) : BytecodeConst()
|
||||||
data class DeclExec(val executable: net.sergeych.lyng.DeclExecutable) : BytecodeConst()
|
data class DeclExec(val executable: net.sergeych.lyng.DeclExecutable) : BytecodeConst()
|
||||||
data class EnumDecl(
|
data class EnumDecl(
|
||||||
val declaredName: String,
|
val declaredName: String,
|
||||||
|
|||||||
@ -154,7 +154,7 @@ class CmdBuilder {
|
|||||||
Opcode.CONST_NULL ->
|
Opcode.CONST_NULL ->
|
||||||
listOf(OperandKind.SLOT)
|
listOf(OperandKind.SLOT)
|
||||||
Opcode.CONST_OBJ, Opcode.CONST_INT, Opcode.CONST_REAL, Opcode.CONST_BOOL,
|
Opcode.CONST_OBJ, Opcode.CONST_INT, Opcode.CONST_REAL, Opcode.CONST_BOOL,
|
||||||
Opcode.MAKE_VALUE_FN ->
|
Opcode.MAKE_VALUE_FN, Opcode.MAKE_LAMBDA_FN ->
|
||||||
listOf(OperandKind.CONST, OperandKind.SLOT)
|
listOf(OperandKind.CONST, OperandKind.SLOT)
|
||||||
Opcode.PUSH_SCOPE, Opcode.PUSH_SLOT_PLAN ->
|
Opcode.PUSH_SCOPE, Opcode.PUSH_SLOT_PLAN ->
|
||||||
listOf(OperandKind.CONST)
|
listOf(OperandKind.CONST)
|
||||||
@ -255,6 +255,7 @@ class CmdBuilder {
|
|||||||
Opcode.CONST_BOOL -> CmdConstBool(operands[0], operands[1])
|
Opcode.CONST_BOOL -> CmdConstBool(operands[0], operands[1])
|
||||||
Opcode.CONST_NULL -> CmdConstNull(operands[0])
|
Opcode.CONST_NULL -> CmdConstNull(operands[0])
|
||||||
Opcode.MAKE_VALUE_FN -> CmdMakeValueFn(operands[0], operands[1])
|
Opcode.MAKE_VALUE_FN -> CmdMakeValueFn(operands[0], operands[1])
|
||||||
|
Opcode.MAKE_LAMBDA_FN -> CmdMakeLambda(operands[0], operands[1])
|
||||||
Opcode.BOX_OBJ -> CmdBoxObj(operands[0], operands[1])
|
Opcode.BOX_OBJ -> CmdBoxObj(operands[0], operands[1])
|
||||||
Opcode.OBJ_TO_BOOL -> CmdObjToBool(operands[0], operands[1])
|
Opcode.OBJ_TO_BOOL -> CmdObjToBool(operands[0], operands[1])
|
||||||
Opcode.RANGE_INT_BOUNDS -> CmdRangeIntBounds(operands[0], operands[1], operands[2], operands[3])
|
Opcode.RANGE_INT_BOUNDS -> CmdRangeIntBounds(operands[0], operands[1], operands[2], operands[3])
|
||||||
|
|||||||
@ -87,6 +87,7 @@ object CmdDisassembler {
|
|||||||
is CmdLoadThisVariant -> Opcode.LOAD_THIS_VARIANT to intArrayOf(cmd.typeId, cmd.dst)
|
is CmdLoadThisVariant -> Opcode.LOAD_THIS_VARIANT to intArrayOf(cmd.typeId, cmd.dst)
|
||||||
is CmdConstNull -> Opcode.CONST_NULL to intArrayOf(cmd.dst)
|
is CmdConstNull -> Opcode.CONST_NULL to intArrayOf(cmd.dst)
|
||||||
is CmdMakeValueFn -> Opcode.MAKE_VALUE_FN to intArrayOf(cmd.id, cmd.dst)
|
is CmdMakeValueFn -> Opcode.MAKE_VALUE_FN to intArrayOf(cmd.id, cmd.dst)
|
||||||
|
is CmdMakeLambda -> Opcode.MAKE_LAMBDA_FN to intArrayOf(cmd.id, cmd.dst)
|
||||||
is CmdBoxObj -> Opcode.BOX_OBJ to intArrayOf(cmd.src, cmd.dst)
|
is CmdBoxObj -> Opcode.BOX_OBJ to intArrayOf(cmd.src, cmd.dst)
|
||||||
is CmdObjToBool -> Opcode.OBJ_TO_BOOL to intArrayOf(cmd.src, cmd.dst)
|
is CmdObjToBool -> Opcode.OBJ_TO_BOOL to intArrayOf(cmd.src, cmd.dst)
|
||||||
is CmdCheckIs -> Opcode.CHECK_IS to intArrayOf(cmd.objSlot, cmd.typeSlot, cmd.dst)
|
is CmdCheckIs -> Opcode.CHECK_IS to intArrayOf(cmd.objSlot, cmd.typeSlot, cmd.dst)
|
||||||
@ -280,7 +281,7 @@ object CmdDisassembler {
|
|||||||
Opcode.CONST_NULL ->
|
Opcode.CONST_NULL ->
|
||||||
listOf(OperandKind.SLOT)
|
listOf(OperandKind.SLOT)
|
||||||
Opcode.CONST_OBJ, Opcode.CONST_INT, Opcode.CONST_REAL, Opcode.CONST_BOOL,
|
Opcode.CONST_OBJ, Opcode.CONST_INT, Opcode.CONST_REAL, Opcode.CONST_BOOL,
|
||||||
Opcode.MAKE_VALUE_FN ->
|
Opcode.MAKE_VALUE_FN, Opcode.MAKE_LAMBDA_FN ->
|
||||||
listOf(OperandKind.CONST, OperandKind.SLOT)
|
listOf(OperandKind.CONST, OperandKind.SLOT)
|
||||||
Opcode.PUSH_SCOPE, Opcode.PUSH_SLOT_PLAN ->
|
Opcode.PUSH_SCOPE, Opcode.PUSH_SLOT_PLAN ->
|
||||||
listOf(OperandKind.CONST)
|
listOf(OperandKind.CONST)
|
||||||
|
|||||||
@ -1505,7 +1505,7 @@ class CmdCallSlot(
|
|||||||
} else {
|
} else {
|
||||||
// Pooling for Statement-based callables (lambdas) can still alter closure semantics; keep safe path for now.
|
// Pooling for Statement-based callables (lambdas) can still alter closure semantics; keep safe path for now.
|
||||||
val scope = frame.ensureScope()
|
val scope = frame.ensureScope()
|
||||||
if (callee is Statement && callee !is BytecodeStatement) {
|
if (callee is Statement && callee !is BytecodeStatement && callee !is BytecodeCallable) {
|
||||||
frame.syncFrameToScope(useRefs = true)
|
frame.syncFrameToScope(useRefs = true)
|
||||||
}
|
}
|
||||||
callee.callOn(scope.createChildScope(scope.pos, args = args))
|
callee.callOn(scope.createChildScope(scope.pos, args = args))
|
||||||
@ -1892,6 +1892,74 @@ class CmdMakeValueFn(internal val id: Int, internal val dst: Int) : Cmd() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CmdMakeLambda(internal val id: Int, internal val dst: Int) : Cmd() {
|
||||||
|
override suspend fun perform(frame: CmdFrame) {
|
||||||
|
val lambdaConst = frame.fn.constants.getOrNull(id) as? BytecodeConst.LambdaFn
|
||||||
|
?: error("MAKE_LAMBDA_FN expects LambdaFn at $id")
|
||||||
|
val scope = frame.ensureScope()
|
||||||
|
val captureRecords = lambdaConst.captureTableId?.let { frame.buildCaptureRecords(it) }
|
||||||
|
val stmt = BytecodeLambdaCallable(
|
||||||
|
fn = lambdaConst.fn,
|
||||||
|
closureScope = scope,
|
||||||
|
captureRecords = captureRecords,
|
||||||
|
captureNames = lambdaConst.captureNames,
|
||||||
|
paramSlotPlan = lambdaConst.paramSlotPlan,
|
||||||
|
argsDeclaration = lambdaConst.argsDeclaration,
|
||||||
|
preferredThisType = lambdaConst.preferredThisType,
|
||||||
|
returnLabels = lambdaConst.returnLabels,
|
||||||
|
pos = lambdaConst.pos
|
||||||
|
)
|
||||||
|
val callable: Obj = if (lambdaConst.wrapAsExtensionCallable) {
|
||||||
|
ObjExtensionMethodCallable("<lambda>", stmt)
|
||||||
|
} else {
|
||||||
|
stmt
|
||||||
|
}
|
||||||
|
frame.storeObjResult(dst, callable.asReadonly.value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BytecodeLambdaCallable(
|
||||||
|
private val fn: CmdFunction,
|
||||||
|
private val closureScope: Scope,
|
||||||
|
private val captureRecords: List<ObjRecord>?,
|
||||||
|
private val captureNames: List<String>,
|
||||||
|
private val paramSlotPlan: Map<String, Int>,
|
||||||
|
private val argsDeclaration: ArgsDeclaration?,
|
||||||
|
private val preferredThisType: String?,
|
||||||
|
private val returnLabels: Set<String>,
|
||||||
|
override val pos: Pos,
|
||||||
|
) : Statement(), BytecodeCallable {
|
||||||
|
override suspend fun execute(scope: Scope): Obj {
|
||||||
|
val context = scope.applyClosureForBytecode(closureScope, preferredThisType).also {
|
||||||
|
it.args = scope.args
|
||||||
|
}
|
||||||
|
if (paramSlotPlan.isNotEmpty()) context.applySlotPlan(paramSlotPlan)
|
||||||
|
if (captureRecords != null) {
|
||||||
|
context.captureRecords = captureRecords
|
||||||
|
context.captureNames = captureNames
|
||||||
|
} else if (captureNames.isNotEmpty()) {
|
||||||
|
closureScope.raiseIllegalState("bytecode lambda capture records missing")
|
||||||
|
}
|
||||||
|
if (argsDeclaration == null) {
|
||||||
|
val l = scope.args.list
|
||||||
|
val itValue: Obj = when (l.size) {
|
||||||
|
0 -> ObjVoid
|
||||||
|
1 -> l[0]
|
||||||
|
else -> ObjList(l.toMutableList())
|
||||||
|
}
|
||||||
|
context.addItem("it", false, itValue, recordType = ObjRecord.Type.Argument)
|
||||||
|
} else {
|
||||||
|
argsDeclaration.assignToContext(context, scope.args, defaultAccessType = AccessType.Val)
|
||||||
|
}
|
||||||
|
return try {
|
||||||
|
CmdVm().execute(fn, context, scope.args.list)
|
||||||
|
} catch (e: ReturnException) {
|
||||||
|
if (e.label == null || returnLabels.contains(e.label)) e.result else throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class CmdIterPush(internal val iterSlot: Int) : Cmd() {
|
class CmdIterPush(internal val iterSlot: Int) : Cmd() {
|
||||||
override suspend fun perform(frame: CmdFrame) {
|
override suspend fun perform(frame: CmdFrame) {
|
||||||
frame.pushIterator(frame.slotToObj(iterSlot))
|
frame.pushIterator(frame.slotToObj(iterSlot))
|
||||||
|
|||||||
@ -42,6 +42,7 @@ enum class Opcode(val code: Int) {
|
|||||||
CHECK_IS(0x15),
|
CHECK_IS(0x15),
|
||||||
ASSERT_IS(0x16),
|
ASSERT_IS(0x16),
|
||||||
MAKE_QUALIFIED_VIEW(0x17),
|
MAKE_QUALIFIED_VIEW(0x17),
|
||||||
|
MAKE_LAMBDA_FN(0x18),
|
||||||
|
|
||||||
ADD_INT(0x20),
|
ADD_INT(0x20),
|
||||||
SUB_INT(0x21),
|
SUB_INT(0x21),
|
||||||
|
|||||||
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* 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.obj
|
||||||
|
|
||||||
|
import net.sergeych.lyng.ArgsDeclaration
|
||||||
|
import net.sergeych.lyng.Pos
|
||||||
|
import net.sergeych.lyng.Scope
|
||||||
|
import net.sergeych.lyng.bytecode.CmdFunction
|
||||||
|
|
||||||
|
class LambdaFnRef(
|
||||||
|
valueFn: suspend (Scope) -> ObjRecord,
|
||||||
|
val bytecodeFn: CmdFunction?,
|
||||||
|
val paramSlotPlan: Map<String, Int>,
|
||||||
|
val argsDeclaration: ArgsDeclaration?,
|
||||||
|
val preferredThisType: String?,
|
||||||
|
val wrapAsExtensionCallable: Boolean,
|
||||||
|
val returnLabels: Set<String>,
|
||||||
|
val pos: Pos,
|
||||||
|
) : ValueFnRef(valueFn)
|
||||||
@ -72,7 +72,7 @@ sealed interface ObjRef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Runtime-computed read-only reference backed by a lambda. */
|
/** Runtime-computed read-only reference backed by a lambda. */
|
||||||
class ValueFnRef(private val fn: suspend (Scope) -> ObjRecord) : ObjRef {
|
open class ValueFnRef(private val fn: suspend (Scope) -> ObjRecord) : ObjRef {
|
||||||
internal fun valueFn(): suspend (Scope) -> ObjRecord = fn
|
internal fun valueFn(): suspend (Scope) -> ObjRecord = fn
|
||||||
|
|
||||||
override suspend fun get(scope: Scope): ObjRecord = fn(scope)
|
override suspend fun get(scope: Scope): ObjRecord = fn(scope)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user