Add non-suspending fast calls for bytecode callables

This commit is contained in:
Sergey Chernov 2026-04-21 11:35:20 +03:00
parent 33d170f525
commit a61b5a31be
6 changed files with 250 additions and 18 deletions

View File

@ -16,4 +16,8 @@
package net.sergeych.lyng package net.sergeych.lyng
interface BytecodeCallable import net.sergeych.lyng.obj.Obj
interface BytecodeCallable {
fun callOnFast(scope: Scope): Obj? = null
}

View File

@ -3553,11 +3553,82 @@ class Compiler(
val ref = LambdaFnRef( val ref = LambdaFnRef(
valueFn = { closureScope -> valueFn = { closureScope ->
val captureRecords = closureScope.captureRecords val captureRecords = closureScope.captureRecords
val stmt = object : Statement(), BytecodeBodyProvider { val stmt = object : Statement(), BytecodeBodyProvider, BytecodeCallable {
override val pos: Pos = fnStatements.pos override val pos: Pos = fnStatements.pos
override fun bytecodeBody(): BytecodeStatement? = fnStatements as? BytecodeStatement 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<ObjRecord>(captureSlots.size)
val resolvedNames = ArrayList<String>(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 { override suspend fun execute(scope: Scope): Obj {
val context = scope.applyClosureForBytecode(closureScope, preferredThisType = expectedReceiverType).also { val context = scope.applyClosureForBytecode(closureScope, preferredThisType = expectedReceiverType).also {
it.args = scope.args it.args = scope.args
@ -9439,9 +9510,76 @@ class Compiler(
val closureBox = FunctionClosureBox() val closureBox = FunctionClosureBox()
val captureSlots = capturePlan.captures.toList() val captureSlots = capturePlan.captures.toList()
val fnBody = object : Statement(), BytecodeBodyProvider { val fnBody = object : Statement(), BytecodeBodyProvider, BytecodeCallable {
override val pos: Pos = start override val pos: Pos = start
override fun bytecodeBody(): BytecodeStatement? = fnStatements as? BytecodeStatement 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<String>()
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<ObjRecord>(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 { override suspend fun execute(scope: Scope): Obj {
scope.pos = start scope.pos = start

View File

@ -66,7 +66,8 @@ internal class ScopeBridge(internal val scope: Scope) : ScopeFacade {
override fun raiseIllegalState(message: String): Nothing = scope.raiseIllegalState(message) override fun raiseIllegalState(message: String): Nothing = scope.raiseIllegalState(message)
override fun raiseNotImplemented(what: String): Nothing = scope.raiseNotImplemented(what) override fun raiseNotImplemented(what: String): Nothing = scope.raiseNotImplemented(what)
override suspend fun call(callee: Obj, args: Arguments, newThisObj: Obj?): Obj { 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 toStringOf(obj: Obj, forInspect: Boolean): ObjString = obj.toString(scope, forInspect)
override suspend fun inspect(obj: Obj): String = obj.inspect(scope) override suspend fun inspect(obj: Obj): String = obj.inspect(scope)

View File

@ -58,6 +58,31 @@ class CmdVm {
return execute(fn, scope0, Arguments.from(args)) 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( suspend fun executeFastOnly(
fn: CmdFunction, fn: CmdFunction,
scope0: Scope, scope0: Scope,
@ -3270,7 +3295,7 @@ class CmdCallDirect(
} else { } else {
val scope = frame.ensureScope() val scope = frame.ensureScope()
if (callee is BytecodeLambdaCallable && callee.supportsDirectInvokeFastPath()) { if (callee is BytecodeLambdaCallable && callee.supportsDirectInvokeFastPath()) {
callee.invokeWithArgs(scope, args) callee.invokeWithArgsFast(scope, args) ?: callee.invokeWithArgs(scope, args)
} else { } else {
callee.callOn(scope.createChildScope(scope.pos, args = args)) callee.callOn(scope.createChildScope(scope.pos, args = args))
} }
@ -3312,7 +3337,7 @@ class CmdCallSlot(
} }
} }
if (callee is BytecodeLambdaCallable && callee.supportsDirectInvokeFastPath()) { if (callee is BytecodeLambdaCallable && callee.supportsDirectInvokeFastPath()) {
callee.invokeWithArgs(scope, args) callee.invokeWithArgsFast(scope, args) ?: callee.invokeWithArgs(scope, args)
} else { } else {
callee.callOn(scope.createChildScope(scope.pos, args = args)) callee.callOn(scope.createChildScope(scope.pos, args = args))
} }
@ -3399,9 +3424,10 @@ class CmdListFillInt(
val result = ObjList(LongArray(size)) val result = ObjList(LongArray(size))
for (i in 0 until size) { for (i in 0 until size) {
val value = if (callable is BytecodeLambdaCallable && callable.supportsImplicitIntFillFastPath()) { 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()) { } 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 { } else {
callable.callOn(scope.createChildScope(scope.pos, args = Arguments(ObjInt.of(i.toLong())))) callable.callOn(scope.createChildScope(scope.pos, args = Arguments(ObjInt.of(i.toLong()))))
} }
@ -4014,16 +4040,18 @@ class BytecodeLambdaCallable(
fun supportsDirectInvokeFastPath(): Boolean = supportsDirectInvokeFastPath fun supportsDirectInvokeFastPath(): Boolean = supportsDirectInvokeFastPath
private val supportsFastUndeclaredLocalInit: Boolean by lazy(LazyThreadSafetyMode.NONE) { private val fastPreboundLocalNames: Set<String> by lazy(LazyThreadSafetyMode.NONE) {
val parameterSlots = paramSlotPlan.values.toHashSet() if (argsDeclaration == null) {
fn.localSlotNames.indices.all { localIndex -> setOf("it")
val name = fn.localSlotNames[localIndex] ?: return@all true } else {
if (declaredLocalNames.contains(name)) return@all true argsDeclaration.params.mapTo(LinkedHashSet()) { it.name }
if (fn.localSlotCaptures.getOrNull(localIndex) == true) return@all true
parameterSlots.contains(fn.scopeSlotCount + localIndex)
} }
} }
private val supportsFastUndeclaredLocalInit: Boolean by lazy(LazyThreadSafetyMode.NONE) {
canFastSeedUndeclaredLocals(fn, declaredLocalNames, fastPreboundLocalNames)
}
private fun supportsFastOnlyVm(arguments: Arguments): Boolean { private fun supportsFastOnlyVm(arguments: Arguments): Boolean {
if (!supportsDirectInvokeFastPath || !fn.fastOnly) return false if (!supportsDirectInvokeFastPath || !fn.fastOnly) return false
if (!supportsFastUndeclaredLocalInit) 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 { suspend fun invokeWithArgs(scope: Scope, args: Arguments): Obj {
val context = buildContext(scope, args) val context = buildContext(scope, args)
return try { 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 { override suspend fun execute(scope: Scope): Obj {
return invokeWithArgs(scope, scope.args) return invokeWithArgs(scope, scope.args)
} }
@ -4611,6 +4669,19 @@ class CmdFrame(
return ExecutionError(errorObject, pos, message, t) 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 { suspend fun handleException(t: Throwable): Boolean {
val handler = tryStack.lastOrNull() ?: return false val handler = tryStack.lastOrNull() ?: return false
vmIterDebug { vmIterDebug {

View File

@ -19,6 +19,22 @@ package net.sergeych.lyng.bytecode
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.obj.ObjRecord import net.sergeych.lyng.obj.ObjRecord
internal fun canFastSeedUndeclaredLocals(
fn: CmdFunction,
declaredLocalNames: Set<String>,
preboundLocalNames: Set<String>
): 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) { internal suspend fun seedFrameLocalsFromScope(frame: CmdFrame, scope: Scope) {
val localNames = frame.fn.localSlotNames val localNames = frame.fn.localSlotNames
if (localNames.isEmpty()) return if (localNames.isEmpty()) return

View File

@ -24,6 +24,7 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.serializer import kotlinx.serialization.serializer
import net.sergeych.lyng.BytecodeCallable
import net.sergeych.lyng.* import net.sergeych.lyng.*
import net.sergeych.lyng.InteropOperator import net.sergeych.lyng.InteropOperator
import net.sergeych.lyng.OperatorInteropRegistry import net.sergeych.lyng.OperatorInteropRegistry
@ -728,12 +729,13 @@ open class Obj {
return if (usePool) { return if (usePool) {
scope.withChildFrame(args, newThisObj = thisObj) { child -> scope.withChildFrame(args, newThisObj = thisObj) { child ->
if (declaringClass != null) child.currentClassCtx = declaringClass if (declaringClass != null) child.currentClassCtx = declaringClass
callOn(child) (this as? BytecodeCallable)?.callOnFast(child) ?: callOn(child)
} }
} else { } 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 if (declaringClass != null) it.currentClassCtx = declaringClass
}) }
(this as? BytecodeCallable)?.callOnFast(child) ?: callOn(child)
} }
} }