diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BytecodeCallable.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BytecodeCallable.kt index 3181726..bf650c5 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BytecodeCallable.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/BytecodeCallable.kt @@ -16,4 +16,8 @@ package net.sergeych.lyng -interface BytecodeCallable +import net.sergeych.lyng.obj.Obj + +interface BytecodeCallable { + fun callOnFast(scope: Scope): Obj? = null +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 6c28c14..a69589c 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -3553,11 +3553,82 @@ class Compiler( val ref = LambdaFnRef( valueFn = { closureScope -> val captureRecords = closureScope.captureRecords - val stmt = object : Statement(), BytecodeBodyProvider { + val stmt = object : Statement(), BytecodeBodyProvider, BytecodeCallable { override val pos: Pos = fnStatements.pos override fun bytecodeBody(): BytecodeStatement? = fnStatements as? BytecodeStatement + override fun callOnFast(scope: Scope): Obj? { + val context = scope.applyClosureForBytecode(closureScope, preferredThisType = expectedReceiverType).also { + it.args = scope.args + } + if (captureSlots.isNotEmpty()) { + if (captureRecords != null) { + context.captureRecords = captureRecords + context.captureNames = captureSlots.map { it.name } + } else { + val resolvedRecords = ArrayList(captureSlots.size) + val resolvedNames = ArrayList(captureSlots.size) + for (capture in captureSlots) { + val rec = resolveStableCaptureRecord( + closureScope, + capture.name, + context.currentClassCtx + ) ?: closureScope.raiseSymbolNotFound("symbol ${capture.name} not found") + resolvedRecords.add(freezeImmutableCaptureRecord(rec)) + resolvedNames.add(capture.name) + } + context.captureRecords = resolvedRecords + context.captureNames = resolvedNames + } + } + val bytecodeBody = fnStatements as? BytecodeStatement ?: return null + val bytecodeFn = bytecodeBody.bytecodeFunction() + if (!supportsDirectInvokeFastPath || !bytecodeFn.fastOnly) return null + val fastPreboundNames = if (argsDeclaration == null) { + setOf("it") + } else { + argsDeclaration.params.mapTo(LinkedHashSet()) { it.name } + } + val declaredNames = bytecodeFn.constants + .mapNotNull { it as? BytecodeConst.LocalDecl } + .mapTo(mutableSetOf()) { it.name } + if (!canFastSeedUndeclaredLocals(bytecodeFn, declaredNames, fastPreboundNames)) return null + if (argsDeclaration != null && !argsDeclaration.supportsFastFrameBinding(scope.args)) return null + val slotPlan = bytecodeFn.localSlotPlanByName() + val binder: (net.sergeych.lyng.bytecode.CmdFrame, Arguments) -> Unit = { frame, arguments -> + if (argsDeclaration == null) { + val l = arguments.list + val itValue: Obj = when (l.size) { + 0 -> ObjVoid + 1 -> l[0] + else -> ObjList(l.toMutableList()) + } + val itSlot = slotPlan["it"] + if (itSlot != null) { + when (itValue) { + is ObjInt -> frame.frame.setInt(itSlot, itValue.value) + is ObjReal -> frame.frame.setReal(itSlot, itValue.value) + is ObjBool -> frame.frame.setBool(itSlot, itValue.value) + else -> frame.frame.setObj(itSlot, itValue) + } + } + } else { + argsDeclaration.assignToFrameFast( + context, + arguments, + slotPlan, + frame.frame + ) + } + } + return try { + net.sergeych.lyng.bytecode.CmdVm().executeFastOnlyNoSuspend(bytecodeFn, context, scope.args, binder) + } catch (e: ReturnException) { + if (e.label == null || returnLabels.contains(e.label)) e.result else throw e + } + } + override suspend fun execute(scope: Scope): Obj { val context = scope.applyClosureForBytecode(closureScope, preferredThisType = expectedReceiverType).also { it.args = scope.args @@ -9439,9 +9510,76 @@ class Compiler( val closureBox = FunctionClosureBox() val captureSlots = capturePlan.captures.toList() - val fnBody = object : Statement(), BytecodeBodyProvider { + val fnBody = object : Statement(), BytecodeBodyProvider, BytecodeCallable { override val pos: Pos = start override fun bytecodeBody(): BytecodeStatement? = fnStatements as? BytecodeStatement + + override fun callOnFast(scope: Scope): Obj? { + scope.pos = start + val context = closureBox.closure?.let { closure -> + scope.applyClosureForBytecode(closure).also { + it.args = scope.args + } + } ?: scope + + val captureBase = closureBox.captureContext ?: closureBox.closure + val bytecodeBody = (fnStatements as? BytecodeStatement) ?: return null + val bytecodeFn = bytecodeBody.bytecodeFunction() + if (!bytecodeFn.fastOnly || !argsDeclaration.supportsFastFrameBinding(scope.args)) return null + val declaredNames = bytecodeFn.constants + .mapNotNull { it as? BytecodeConst.LocalDecl } + .mapTo(mutableSetOf()) { it.name } + val preboundNames = LinkedHashSet() + argsDeclaration.params.mapTo(preboundNames) { it.name } + mergedTypeParamDecls.mapTo(preboundNames) { it.name } + if (!canFastSeedUndeclaredLocals(bytecodeFn, declaredNames, preboundNames)) return null + val captureNames = captureNamesForBytecodeFunction( + bytecodeFn, + captureSlots.map { it.name } + ) + val prebuiltCaptures = closureBox.captureRecords + if (prebuiltCaptures != null && captureNames.isNotEmpty()) { + context.captureRecords = prebuiltCaptures + context.captureNames = captureNames + } else if (captureBase != null && captureNames.isNotEmpty()) { + val resolvedRecords = ArrayList(captureNames.size) + for (name in captureNames) { + val rec = resolveStableCaptureRecord( + captureBase, + name, + context.currentClassCtx + ) ?: captureBase.raiseSymbolNotFound("symbol $name not found") + resolvedRecords.add(freezeImmutableCaptureRecord(rec)) + } + context.captureRecords = resolvedRecords + context.captureNames = captureNames + } + val slotPlan = bytecodeFn.localSlotPlanByName() + val binder: (net.sergeych.lyng.bytecode.CmdFrame, Arguments) -> Unit = { frame, arguments -> + argsDeclaration.assignToFrameFast( + context, + arguments, + slotPlan, + frame.frame + ) + val typeBindings = bindTypeParamsAtRuntime(context, argsDeclaration, mergedTypeParamDecls) + if (typeBindings.isNotEmpty()) { + for ((name, bound) in typeBindings) { + val slot = slotPlan[name] ?: continue + frame.frame.setObj(slot, bound) + } + } + if (extTypeName != null) { + context.thisObj = scope.thisObj + } + } + return try { + net.sergeych.lyng.bytecode.CmdVm().executeFastOnlyNoSuspend(bytecodeFn, context, scope.args, binder) + } catch (e: ReturnException) { + if (e.label == null || e.label == name || e.label == outerLabel) e.result else throw e + } + } + override suspend fun execute(scope: Scope): Obj { scope.pos = start diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScopeFacade.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScopeFacade.kt index d264285..02fef09 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScopeFacade.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScopeFacade.kt @@ -66,7 +66,8 @@ internal class ScopeBridge(internal val scope: Scope) : ScopeFacade { override fun raiseIllegalState(message: String): Nothing = scope.raiseIllegalState(message) override fun raiseNotImplemented(what: String): Nothing = scope.raiseNotImplemented(what) override suspend fun call(callee: Obj, args: Arguments, newThisObj: Obj?): Obj { - return callee.callOn(scope.createChildScope(scope.pos, args = args, newThisObj = newThisObj)) + val child = scope.createChildScope(scope.pos, args = args, newThisObj = newThisObj) + return (callee as? BytecodeCallable)?.callOnFast(child) ?: callee.callOn(child) } override suspend fun toStringOf(obj: Obj, forInspect: Boolean): ObjString = obj.toString(scope, forInspect) override suspend fun inspect(obj: Obj): String = obj.inspect(scope) 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 e7f8ec9..123411b 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt @@ -58,6 +58,31 @@ class CmdVm { return execute(fn, scope0, Arguments.from(args)) } + fun executeFastOnlyNoSuspend( + fn: CmdFunction, + scope0: Scope, + args: Arguments, + binder: ((CmdFrame, Arguments) -> Unit)? = null + ): Obj { + require(fn.fastOnly) { "fast-only execution requested for non-fast function ${fn.name}" } + result = null + val frame = CmdFrame(this, fn, scope0, args.list) + frame.applyCaptureRecords() + binder?.invoke(frame, args) + val cmds = fn.cmds + try { + while (result == null) { + val cmd = cmds[frame.ip++] + if (!cmd.performFast(frame)) { + error("fast-only command not supported: ${cmd::class.simpleName}") + } + } + } catch (e: Throwable) { + throw frame.normalizeThrowableFast(e) + } + return result ?: ObjVoid + } + suspend fun executeFastOnly( fn: CmdFunction, scope0: Scope, @@ -3270,7 +3295,7 @@ class CmdCallDirect( } else { val scope = frame.ensureScope() if (callee is BytecodeLambdaCallable && callee.supportsDirectInvokeFastPath()) { - callee.invokeWithArgs(scope, args) + callee.invokeWithArgsFast(scope, args) ?: callee.invokeWithArgs(scope, args) } else { callee.callOn(scope.createChildScope(scope.pos, args = args)) } @@ -3312,7 +3337,7 @@ class CmdCallSlot( } } if (callee is BytecodeLambdaCallable && callee.supportsDirectInvokeFastPath()) { - callee.invokeWithArgs(scope, args) + callee.invokeWithArgsFast(scope, args) ?: callee.invokeWithArgs(scope, args) } else { callee.callOn(scope.createChildScope(scope.pos, args = args)) } @@ -3399,9 +3424,10 @@ class CmdListFillInt( val result = ObjList(LongArray(size)) for (i in 0 until size) { val value = if (callable is BytecodeLambdaCallable && callable.supportsImplicitIntFillFastPath()) { - callable.invokeImplicitIntArg(scope, i.toLong()) + callable.invokeImplicitIntArgFast(scope, i.toLong()) ?: callable.invokeImplicitIntArg(scope, i.toLong()) } else if (callable is BytecodeLambdaCallable && callable.supportsDirectInvokeFastPath()) { - callable.invokeWithArgs(scope, Arguments(ObjInt.of(i.toLong()))) + callable.invokeWithArgsFast(scope, Arguments(ObjInt.of(i.toLong()))) + ?: callable.invokeWithArgs(scope, Arguments(ObjInt.of(i.toLong()))) } else { callable.callOn(scope.createChildScope(scope.pos, args = Arguments(ObjInt.of(i.toLong())))) } @@ -4014,16 +4040,18 @@ class BytecodeLambdaCallable( fun supportsDirectInvokeFastPath(): Boolean = supportsDirectInvokeFastPath - private val supportsFastUndeclaredLocalInit: Boolean by lazy(LazyThreadSafetyMode.NONE) { - val parameterSlots = paramSlotPlan.values.toHashSet() - fn.localSlotNames.indices.all { localIndex -> - val name = fn.localSlotNames[localIndex] ?: return@all true - if (declaredLocalNames.contains(name)) return@all true - if (fn.localSlotCaptures.getOrNull(localIndex) == true) return@all true - parameterSlots.contains(fn.scopeSlotCount + localIndex) + private val fastPreboundLocalNames: Set by lazy(LazyThreadSafetyMode.NONE) { + if (argsDeclaration == null) { + setOf("it") + } else { + argsDeclaration.params.mapTo(LinkedHashSet()) { it.name } } } + private val supportsFastUndeclaredLocalInit: Boolean by lazy(LazyThreadSafetyMode.NONE) { + canFastSeedUndeclaredLocals(fn, declaredLocalNames, fastPreboundLocalNames) + } + private fun supportsFastOnlyVm(arguments: Arguments): Boolean { if (!supportsDirectInvokeFastPath || !fn.fastOnly) return false if (!supportsFastUndeclaredLocalInit) return false @@ -4139,6 +4167,21 @@ class BytecodeLambdaCallable( } } + fun invokeImplicitIntArgFast(scope: Scope, arg: Long): Obj? { + if (!supportsFastOnlyVm(Arguments.EMPTY)) return null + val context = buildContext(scope, Arguments.EMPTY) + val binder: (CmdFrame, Arguments) -> Unit = { frame, _ -> + slotPlanByName["it"]?.let { itSlot -> + frame.frame.setInt(itSlot, arg) + } + } + return try { + CmdVm().executeFastOnlyNoSuspend(fn, context, Arguments.EMPTY, binder) + } catch (e: ReturnException) { + if (e.label == null || returnLabels.contains(e.label)) e.result else throw e + } + } + suspend fun invokeWithArgs(scope: Scope, args: Arguments): Obj { val context = buildContext(scope, args) return try { @@ -4160,6 +4203,21 @@ class BytecodeLambdaCallable( } } + fun invokeWithArgsFast(scope: Scope, args: Arguments): Obj? { + if (!supportsFastOnlyVm(args)) return null + val context = buildContext(scope, args) + val binder: (CmdFrame, Arguments) -> Unit = { frame, arguments -> + bindArgumentsFast(frame, context, arguments) + } + return try { + CmdVm().executeFastOnlyNoSuspend(fn, context, args, binder) + } catch (e: ReturnException) { + if (e.label == null || returnLabels.contains(e.label)) e.result else throw e + } + } + + override fun callOnFast(scope: Scope): Obj? = invokeWithArgsFast(scope, scope.args) + override suspend fun execute(scope: Scope): Obj { return invokeWithArgs(scope, scope.args) } @@ -4611,6 +4669,19 @@ class CmdFrame( return ExecutionError(errorObject, pos, message, t) } + fun normalizeThrowableFast(t: Throwable): Throwable { + if (t is ExecutionError || t is ReturnException || t is LoopBreakContinueException) return t + val parentScope = ensureScope() + val pos = (t as? ScriptError)?.pos ?: currentErrorPos() ?: parentScope.pos + val throwScope = parentScope.createChildScope(pos = pos) + val message = when (t) { + is ScriptError -> t.errorMessage + else -> t.message ?: t.toString() + } + val errorObject = ObjUnknownException(throwScope, message) + return ExecutionError(errorObject, pos, message, t) + } + suspend fun handleException(t: Throwable): Boolean { val handler = tryStack.lastOrNull() ?: return false vmIterDebug { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/SeedLocals.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/SeedLocals.kt index d538517..2c78f8a 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/SeedLocals.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/SeedLocals.kt @@ -19,6 +19,22 @@ package net.sergeych.lyng.bytecode import net.sergeych.lyng.Scope import net.sergeych.lyng.obj.ObjRecord +internal fun canFastSeedUndeclaredLocals( + fn: CmdFunction, + declaredLocalNames: Set, + preboundLocalNames: Set +): Boolean { + if (fn.localSlotNames.isEmpty()) return true + for (i in fn.localSlotNames.indices) { + val name = fn.localSlotNames[i] ?: continue + if (declaredLocalNames.contains(name)) continue + if (fn.localSlotCaptures.getOrNull(i) == true) continue + if (preboundLocalNames.contains(name)) continue + return false + } + return true +} + internal suspend fun seedFrameLocalsFromScope(frame: CmdFrame, scope: Scope) { val localNames = frame.fn.localSlotNames if (localNames.isEmpty()) return diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt index b5bab0a..761dc52 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt @@ -24,6 +24,7 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonNull import kotlinx.serialization.serializer +import net.sergeych.lyng.BytecodeCallable import net.sergeych.lyng.* import net.sergeych.lyng.InteropOperator import net.sergeych.lyng.OperatorInteropRegistry @@ -728,12 +729,13 @@ open class Obj { return if (usePool) { scope.withChildFrame(args, newThisObj = thisObj) { child -> if (declaringClass != null) child.currentClassCtx = declaringClass - callOn(child) + (this as? BytecodeCallable)?.callOnFast(child) ?: callOn(child) } } else { - callOn(scope.createChildScope(scope.pos, args = args, newThisObj = thisObj).also { + val child = scope.createChildScope(scope.pos, args = args, newThisObj = thisObj).also { if (declaringClass != null) it.currentClassCtx = declaringClass - }) + } + (this as? BytecodeCallable)?.callOnFast(child) ?: callOn(child) } }