Fast-path compiled lambda calls with args

This commit is contained in:
Sergey Chernov 2026-04-21 16:04:04 +03:00
parent 97990f00ce
commit 3721ee8332
5 changed files with 40 additions and 83 deletions

View File

@ -21,3 +21,7 @@ import net.sergeych.lyng.obj.Obj
interface BytecodeCallable {
fun callOnFast(scope: Scope): Obj? = null
}
interface BytecodeArgCallable {
fun callWithArgsFast(scope: Scope, args: Arguments): Obj? = null
}

View File

@ -9510,79 +9510,11 @@ class Compiler(
val closureBox = FunctionClosureBox()
val captureSlots = capturePlan.captures.toList()
val fnBody = object : Statement(), BytecodeBodyProvider, BytecodeCallable {
val fnBody = object : Statement(), BytecodeBodyProvider {
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<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 {
scope.pos = start
// restore closure where the function was defined, and making a copy of it
// for local space. If there is no closure, we are in, say, class context where
// the closure is in the class initialization and we needn't more:
@ -9591,6 +9523,7 @@ class Compiler(
it.args = scope.args
}
} ?: scope
context.pos = start
// Capacity hint: parameters + declared locals + small overhead
val capacityHint = paramNames.size + fnLocalDecls + 4

View File

@ -3622,7 +3622,7 @@ class BytecodeCompiler(
if (!ref.isOptional) {
val args = compileCallArgsWithReceiver(receiver, emptyList(), false) ?: return null
val encodedCount = encodeCallArgCount(args) ?: return null
builder.emit(Opcode.CALL_SLOT, callee.slot, args.base, encodedCount, dst)
emitCallCompiled(callee, args.base, encodedCount, dst)
} else {
val nullSlot = allocSlot()
builder.emit(Opcode.CONST_NULL, nullSlot)
@ -3636,7 +3636,7 @@ class BytecodeCompiler(
)
val args = compileCallArgsWithReceiver(receiver, emptyList(), false) ?: return null
val encodedCount = encodeCallArgCount(args) ?: return null
builder.emit(Opcode.CALL_SLOT, callee.slot, args.base, encodedCount, dst)
emitCallCompiled(callee, args.base, encodedCount, dst)
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel)))
builder.mark(nullLabel)
builder.emit(Opcode.CONST_NULL, dst)
@ -4895,7 +4895,7 @@ class BytecodeCompiler(
val args = compileCallArgs(ref.args, ref.tailBlock, ref.explicitTypeArgs) ?: return null
val encodedCount = encodeCallArgCount(args) ?: return null
setPos(callPos)
builder.emit(Opcode.CALL_SLOT, calleeSlot, args.base, encodedCount, dst)
emitCallCompiled(CompiledValue(calleeSlot, SlotType.OBJ), args.base, encodedCount, dst)
} else {
val nullSlot = allocSlot()
builder.emit(Opcode.CONST_NULL, nullSlot)
@ -4911,7 +4911,7 @@ class BytecodeCompiler(
val args = compileCallArgs(ref.args, ref.tailBlock, ref.explicitTypeArgs) ?: return null
val encodedCount = encodeCallArgCount(args) ?: return null
setPos(callPos)
builder.emit(Opcode.CALL_SLOT, calleeSlot, args.base, encodedCount, dst)
emitCallCompiled(CompiledValue(calleeSlot, SlotType.OBJ), args.base, encodedCount, dst)
builder.emit(Opcode.JMP, listOf(CmdBuilder.Operand.LabelRef(endLabel)))
builder.mark(nullLabel)
builder.emit(Opcode.CONST_NULL, dst)

View File

@ -3290,7 +3290,10 @@ class CmdCallDirect(
frame.ensureScope().raiseIllegalState("bytecode runtime cannot call non-bytecode Statement")
}
}
val result = if (PerfFlags.SCOPE_POOL) {
val directFastResult = (callee as? BytecodeArgCallable)?.callWithArgsFast(frame.ensureScope(), args)
val result = if (directFastResult != null) {
directFastResult
} else if (PerfFlags.SCOPE_POOL) {
frame.ensureScope().withChildFrame(args) { child ->
(callee as? BytecodeCallable)?.callOnFast(child) ?: callee.callOn(child)
}
@ -3328,13 +3331,16 @@ class CmdCallSlot(
frame.ensureScope().raiseUnset(message)
}
val args = frame.buildArguments(argBase, argCount)
val scope = frame.ensureScope()
val directFastResult = (callee as? BytecodeArgCallable)?.callWithArgsFast(scope, args)
val canPool = PerfFlags.SCOPE_POOL && callee !is Statement
val result = if (canPool) {
val result = if (directFastResult != null) {
directFastResult
} else if (canPool) {
frame.ensureScope().withChildFrame(args) { child ->
(callee as? BytecodeCallable)?.callOnFast(child) ?: callee.callOn(child)
}
} else {
val scope = frame.ensureScope()
if (callee is Statement) {
val bytecodeBody = (callee as? BytecodeBodyProvider)?.bytecodeBody()
if (callee !is BytecodeStatement && callee !is BytecodeCallable && bytecodeBody == null) {
@ -3429,13 +3435,16 @@ class CmdListFillInt(
val scope = frame.ensureScope()
val result = ObjList(LongArray(size))
for (i in 0 until size) {
val args = Arguments(ObjInt.of(i.toLong()))
val value = if (callable is BytecodeLambdaCallable && callable.supportsImplicitIntFillFastPath()) {
callable.invokeImplicitIntArgFast(scope, i.toLong()) ?: callable.invokeImplicitIntArg(scope, i.toLong())
} else if (callable is BytecodeLambdaCallable && callable.supportsDirectInvokeFastPath()) {
callable.invokeWithArgsFast(scope, Arguments(ObjInt.of(i.toLong())))
?: callable.invokeWithArgs(scope, Arguments(ObjInt.of(i.toLong())))
} else if (callable is BytecodeArgCallable) {
callable.callWithArgsFast(scope, args) ?: run {
val child = scope.createChildScope(scope.pos, args = args)
(callable as? BytecodeCallable)?.callOnFast(child) ?: callable.callOn(child)
}
} else {
val child = scope.createChildScope(scope.pos, args = Arguments(ObjInt.of(i.toLong())))
val child = scope.createChildScope(scope.pos, args = args)
(callable as? BytecodeCallable)?.callOnFast(child) ?: callable.callOn(child)
}
val intValue = (value as? ObjInt)?.value ?: scope.raiseClassCastError("expected Int fill result")
@ -3980,7 +3989,7 @@ class BytecodeLambdaCallable(
private val preferredThisType: String?,
private val returnLabels: Set<String>,
override val pos: Pos,
) : Statement(), BytecodeCallable {
) : Statement(), BytecodeCallable, BytecodeArgCallable {
private val slotPlanByName: Map<String, Int> by lazy(LazyThreadSafetyMode.NONE) { fn.localSlotPlanByName() }
private val declaredLocalNames: Set<String> by lazy(LazyThreadSafetyMode.NONE) {
fn.constants
@ -4223,6 +4232,8 @@ class BytecodeLambdaCallable(
}
}
override fun callWithArgsFast(scope: Scope, args: Arguments): Obj? = invokeWithArgsFast(scope, args)
override fun callOnFast(scope: Scope): Obj? = invokeWithArgsFast(scope, scope.args)
override suspend fun execute(scope: Scope): Obj {

View File

@ -395,8 +395,17 @@ suspend fun Obj.getLyngExceptionMessageWithStackTrace(scope: Scope? = null,showD
var at = "unknown"
val stack = if (!trace.list.isEmpty()) {
val first = trace.list[0]
at = (first.readField(s, "at").value as ObjString).value
"\n" + trace.list.map { " at " + it.toString(s).value }.joinToString("\n")
suspend fun formatTraceEntry(entry: Obj): String {
return when (entry) {
is ObjString -> entry.value.removePrefix("#")
else -> entry.toString(s).value
}
}
at = when (first) {
is ObjString -> formatTraceEntry(first)
else -> (first.readField(s, "at").value as ObjString).value
}
"\n" + trace.list.map { " at " + formatTraceEntry(it) }.joinToString("\n")
} else {
val pos = s.pos
if (pos.source.fileName.isNotEmpty() && pos.currentLine.isNotEmpty()) {