From bde32ca7b5d8db1fe93fc8352d9db13d8f7801b3 Mon Sep 17 00:00:00 2001 From: sergeych Date: Wed, 11 Feb 2026 20:50:56 +0300 Subject: [PATCH] Step 26A: bytecode lambda callables --- bytecode_migration_plan.md | 4 +- .../net/sergeych/lyng/BytecodeCallable.kt | 19 +++++ .../kotlin/net/sergeych/lyng/Compiler.kt | 14 +++- .../lyng/bytecode/BytecodeCompiler.kt | 36 ++++++++++ .../sergeych/lyng/bytecode/BytecodeConst.kt | 12 ++++ .../net/sergeych/lyng/bytecode/CmdBuilder.kt | 3 +- .../sergeych/lyng/bytecode/CmdDisassembler.kt | 3 +- .../net/sergeych/lyng/bytecode/CmdRuntime.kt | 70 ++++++++++++++++++- .../net/sergeych/lyng/bytecode/Opcode.kt | 1 + .../net/sergeych/lyng/obj/LambdaFnRef.kt | 33 +++++++++ .../kotlin/net/sergeych/lyng/obj/ObjRef.kt | 2 +- 11 files changed, 189 insertions(+), 8 deletions(-) create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/BytecodeCallable.kt create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/LambdaFnRef.kt diff --git a/bytecode_migration_plan.md b/bytecode_migration_plan.md index 5afaf5b..2e02408 100644 --- a/bytecode_migration_plan.md +++ b/bytecode_migration_plan.md @@ -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] 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. + - [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. - - [ ] 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. - [ ] Delete `BytecodeConst.ValueFn`, `CmdMakeValueFn`, and `MAKE_VALUE_FN`. - [ ] Delete `BytecodeConst.StatementVal`, `CmdEvalStmt`, and `EVAL_STMT`. diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BytecodeCallable.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BytecodeCallable.kt new file mode 100644 index 0000000..3181726 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BytecodeCallable.kt @@ -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 diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 48c4340..b695b98 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -2918,7 +2918,9 @@ class Compiler( } else { body } - val ref = ValueFnRef { closureScope -> + val bytecodeFn = (fnStatements as? BytecodeStatement)?.bytecodeFunction() + val ref = LambdaFnRef( + valueFn = { closureScope -> val captureRecords = closureScope.captureRecords val stmt = object : Statement(), BytecodeBodyProvider { override val pos: Pos = fnStatements.pos @@ -3009,7 +3011,15 @@ class Compiler( stmt } callable.asReadonly - } + }, + bytecodeFn = bytecodeFn, + paramSlotPlan = paramSlotPlanSnapshot, + argsDeclaration = argsDeclaration, + preferredThisType = expectedReceiverType, + wrapAsExtensionCallable = wrapAsExtensionCallable, + returnLabels = returnLabels, + pos = startPos + ) if (returnClass != null) { lambdaReturnTypeByRef[ref] = returnClass } 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 f045065..a45d795 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -623,6 +623,42 @@ class BytecodeCompiler( } 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 -> if (captures.isEmpty()) return@let null val resolved = captures.map { entry -> 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 32f13d2..61a9952 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeConst.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeConst.kt @@ -16,6 +16,7 @@ package net.sergeych.lyng.bytecode +import net.sergeych.lyng.ArgsDeclaration import net.sergeych.lyng.Pos import net.sergeych.lyng.Visibility 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 captureTableId: Int? = null, ) : BytecodeConst() + data class LambdaFn( + val fn: CmdFunction, + val captureTableId: Int?, + val captureNames: List, + val paramSlotPlan: Map, + val argsDeclaration: ArgsDeclaration?, + val preferredThisType: String?, + val wrapAsExtensionCallable: Boolean, + val returnLabels: Set, + val pos: Pos, + ) : BytecodeConst() data class DeclExec(val executable: net.sergeych.lyng.DeclExecutable) : BytecodeConst() data class EnumDecl( val declaredName: String, diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt index 95dff74..5034b5e 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdBuilder.kt @@ -154,7 +154,7 @@ class CmdBuilder { Opcode.CONST_NULL -> listOf(OperandKind.SLOT) 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) Opcode.PUSH_SCOPE, Opcode.PUSH_SLOT_PLAN -> listOf(OperandKind.CONST) @@ -255,6 +255,7 @@ class CmdBuilder { Opcode.CONST_BOOL -> CmdConstBool(operands[0], operands[1]) Opcode.CONST_NULL -> CmdConstNull(operands[0]) 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.OBJ_TO_BOOL -> CmdObjToBool(operands[0], operands[1]) Opcode.RANGE_INT_BOUNDS -> CmdRangeIntBounds(operands[0], operands[1], operands[2], operands[3]) 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 b453a46..f380838 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdDisassembler.kt @@ -87,6 +87,7 @@ object CmdDisassembler { is CmdLoadThisVariant -> Opcode.LOAD_THIS_VARIANT to intArrayOf(cmd.typeId, cmd.dst) is CmdConstNull -> Opcode.CONST_NULL to intArrayOf(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 CmdObjToBool -> Opcode.OBJ_TO_BOOL to intArrayOf(cmd.src, cmd.dst) is CmdCheckIs -> Opcode.CHECK_IS to intArrayOf(cmd.objSlot, cmd.typeSlot, cmd.dst) @@ -280,7 +281,7 @@ object CmdDisassembler { Opcode.CONST_NULL -> listOf(OperandKind.SLOT) 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) Opcode.PUSH_SCOPE, Opcode.PUSH_SLOT_PLAN -> listOf(OperandKind.CONST) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt index ee03378..bfcf070 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt @@ -1505,7 +1505,7 @@ class CmdCallSlot( } else { // Pooling for Statement-based callables (lambdas) can still alter closure semantics; keep safe path for now. 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) } 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("", stmt) + } else { + stmt + } + frame.storeObjResult(dst, callable.asReadonly.value) + return + } +} + +class BytecodeLambdaCallable( + private val fn: CmdFunction, + private val closureScope: Scope, + private val captureRecords: List?, + private val captureNames: List, + private val paramSlotPlan: Map, + private val argsDeclaration: ArgsDeclaration?, + private val preferredThisType: String?, + private val returnLabels: Set, + 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() { override suspend fun perform(frame: CmdFrame) { frame.pushIterator(frame.slotToObj(iterSlot)) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt index 25e8434..35913d1 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/Opcode.kt @@ -42,6 +42,7 @@ enum class Opcode(val code: Int) { CHECK_IS(0x15), ASSERT_IS(0x16), MAKE_QUALIFIED_VIEW(0x17), + MAKE_LAMBDA_FN(0x18), ADD_INT(0x20), SUB_INT(0x21), diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/LambdaFnRef.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/LambdaFnRef.kt new file mode 100644 index 0000000..57f3297 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/LambdaFnRef.kt @@ -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, + val argsDeclaration: ArgsDeclaration?, + val preferredThisType: String?, + val wrapAsExtensionCallable: Boolean, + val returnLabels: Set, + val pos: Pos, +) : ValueFnRef(valueFn) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt index bd123c0..bdc9e9b 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt @@ -72,7 +72,7 @@ sealed interface ObjRef { } /** 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 override suspend fun get(scope: Scope): ObjRecord = fn(scope)