Drive higher-order lambda inlining from call metadata

This commit is contained in:
Sergey Chernov 2026-04-21 20:51:16 +03:00
parent f4ab2ebab4
commit fbb5688696
11 changed files with 401 additions and 121 deletions

View File

@ -20,5 +20,30 @@ package net.sergeych.lyng
* Compile-time call metadata for known functions. Used to select lambda receiver semantics.
*/
data class CallSignature(
val tailBlockReceiverType: String? = null
val tailBlockReceiverType: String? = null,
val inlineHigherOrder: HigherOrderInline? = null
) {
data class HigherOrderInline(
val kind: Kind,
val result: ResultMode,
val argCount: Int = 1,
val lambdaArgIndex: Int = 0
)
enum class Kind {
UNARY_ARGUMENT,
RECEIVER,
ITERABLE,
MAP_GET_OR_PUT
}
enum class ResultMode {
BLOCK_RESULT,
RETURN_RECEIVER,
FOR_EACH,
MAP,
FILTER,
MAP_NOT_NULL,
ASSOCIATE_BY
}
}

View File

@ -187,6 +187,7 @@ class Compiler(
private val callableReturnTypeByScopeId: MutableMap<Int, MutableMap<Int, ObjClass>> = mutableMapOf()
private val callableReturnTypeByName: MutableMap<String, ObjClass> = mutableMapOf()
private val callableReturnTypeDeclByName: MutableMap<String, TypeDecl> = mutableMapOf()
private val callSignatureByName: MutableMap<String, CallSignature> = mutableMapOf()
private val lambdaReturnTypeByRef: MutableMap<ObjRef, ObjClass> = mutableMapOf()
private val exactLambdaRefByScopeId: MutableMap<Int, MutableMap<Int, LambdaFnRef>> = mutableMapOf()
private val lambdaCaptureEntriesByRef: MutableMap<ValueFnRef, List<net.sergeych.lyng.bytecode.LambdaCaptureEntry>> =
@ -265,6 +266,11 @@ class Compiler(
if (plan.slots.containsKey(name)) continue
declareSlotNameIn(plan, name, record.isMutable, record.type == ObjRecord.Type.Delegated)
scopeSeedNames.add(name)
record.callSignature?.let { signature ->
if (!callSignatureByName.containsKey(name)) {
callSignatureByName[name] = signature
}
}
if (record.typeDecl != null && nameTypeDecl[name] == null) {
nameTypeDecl[name] = record.typeDecl
if (nameObjClass[name] == null) {
@ -291,6 +297,11 @@ class Compiler(
)
scopeSeedNames.add(getterName)
}
record.callSignature?.let { signature ->
if (!callSignatureByName.containsKey(getterName)) {
callSignatureByName[getterName] = signature
}
}
val prop = record.value as? ObjProperty
if (prop?.setter != null) {
val setterName = extensionPropertySetterName(cls.className, name)
@ -316,6 +327,11 @@ class Compiler(
)
scopeSeedNames.add(callableName)
}
record.callSignature?.let { signature ->
if (!callSignatureByName.containsKey(callableName)) {
callSignatureByName[callableName] = signature
}
}
}
}
}
@ -347,6 +363,11 @@ class Compiler(
record.type == ObjRecord.Type.Delegated
)
scopeSeedNames.add(name)
record.callSignature?.let { signature ->
if (!callSignatureByName.containsKey(name)) {
callSignatureByName[name] = signature
}
}
if (record.typeDecl != null && nameTypeDecl[name] == null) {
nameTypeDecl[name] = record.typeDecl
if (nameObjClass[name] == null) {
@ -762,6 +783,29 @@ class Compiler(
?: importManager.rootScope.getLocalRecordDirect(name)?.callSignature
}
private fun declaredCallSignature(name: String, extTypeName: String?, actualExtern: Boolean): CallSignature? {
val imported = if (actualExtern) {
importManager.rootScope.getLocalRecordDirect(name)?.callSignature
} else {
null
}
val inferred = when {
extTypeName == "Iterable" && name == "filter" -> CallSignature(
inlineHigherOrder = CallSignature.HigherOrderInline(
kind = CallSignature.Kind.ITERABLE,
result = CallSignature.ResultMode.FILTER
)
)
else -> null
}
return when {
imported == null -> inferred
inferred == null -> imported
imported.inlineHigherOrder != null -> imported
else -> imported.copy(inlineHigherOrder = inferred.inlineHigherOrder)
}
}
internal data class MemberIds(val fieldId: Int?, val methodId: Int?)
private fun resolveMemberIds(name: String, pos: Pos, qualifier: String? = null): MemberIds {
@ -1429,6 +1473,11 @@ class Compiler(
}
private fun seedImportTypeMetadata(name: String, record: ObjRecord) {
record.callSignature?.let { signature ->
if (!callSignatureByName.containsKey(name)) {
callSignatureByName[name] = signature
}
}
if (record.typeDecl != null && nameTypeDecl[name] == null) {
nameTypeDecl[name] = record.typeDecl
}
@ -1904,6 +1953,7 @@ class Compiler(
enumEntriesByName = enumEntriesByName,
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
callableReturnTypeByName = callableReturnTypeByName,
callSignatureByName = callSignatureByName,
externBindingNames = externBindingNames,
preparedModuleBindingNames = importBindings.keys,
scopeRefPosByName = moduleReferencePosByName,
@ -2260,6 +2310,7 @@ class Compiler(
enumEntriesByName = enumEntriesByName,
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
callableReturnTypeByName = callableReturnTypeByName,
callSignatureByName = callSignatureByName,
externCallableNames = externCallableNames,
externBindingNames = externBindingNames,
preparedModuleBindingNames = importBindings.keys,
@ -2294,6 +2345,7 @@ class Compiler(
enumEntriesByName = enumEntriesByName,
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
callableReturnTypeByName = callableReturnTypeByName,
callSignatureByName = callSignatureByName,
externCallableNames = externCallableNames,
externBindingNames = externBindingNames,
preparedModuleBindingNames = importBindings.keys,
@ -2353,6 +2405,7 @@ class Compiler(
enumEntriesByName = enumEntriesByName,
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
callableReturnTypeByName = callableReturnTypeByName,
callSignatureByName = callSignatureByName,
externCallableNames = externCallableNames,
externBindingNames = externBindingNames,
preparedModuleBindingNames = importBindings.keys,
@ -9208,7 +9261,7 @@ class Compiler(
val extensionWrapperName = extTypeName?.let { extensionCallableName(it, name) }
val classCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody
var memberMethodId = if (extTypeName == null) classCtx?.memberMethodIds?.get(name) else null
val externCallSignature = if (actualExtern) importManager.rootScope.getLocalRecordDirect(name)?.callSignature else null
val externCallSignature = declaredCallSignature(name, extTypeName, actualExtern)
val declKind = if (parentContext is CodeContext.ClassBody) SymbolKind.MEMBER else SymbolKind.FUNCTION
resolutionSink?.declareSymbol(name, declKind, isMutable = false, pos = nameStartPos, isOverride = isOverride)
@ -9230,7 +9283,9 @@ class Compiler(
}
if (extensionWrapperName != null) {
declareLocalName(extensionWrapperName, isMutable = false)
externCallSignature?.let { callSignatureByName[extensionWrapperName] = it }
}
externCallSignature?.let { callSignatureByName[name] = it }
if (actualExtern && declKind != SymbolKind.MEMBER) {
externCallableNames.add(name)
}

View File

@ -118,7 +118,14 @@ internal suspend fun executeFunctionDecl(
scope.addExtension(
type,
spec.name,
ObjRecord(ObjUnset, isMutable = false, visibility = spec.visibility, declaringClass = null, type = ObjRecord.Type.Delegated).apply {
ObjRecord(
ObjUnset,
isMutable = false,
visibility = spec.visibility,
declaringClass = null,
type = ObjRecord.Type.Delegated,
callSignature = spec.externCallSignature
).apply {
delegate = finalDelegate
}
)
@ -135,7 +142,8 @@ internal suspend fun executeFunctionDecl(
null,
spec.startPos,
isTransient = spec.isTransient,
type = ObjRecord.Type.Delegated
type = ObjRecord.Type.Delegated,
callSignature = spec.externCallSignature
).apply {
delegate = finalDelegate
}
@ -204,6 +212,7 @@ internal suspend fun executeFunctionDecl(
visibility = spec.visibility,
pos = spec.startPos,
type = ObjRecord.Type.Fun,
callSignature = spec.externCallSignature,
)
} else {
scope.addExtension(
@ -215,6 +224,7 @@ internal suspend fun executeFunctionDecl(
visibility = spec.visibility,
declaringClass = null,
type = ObjRecord.Type.Fun,
callSignature = spec.externCallSignature,
typeDecl = spec.typeDecl
)
)
@ -227,6 +237,7 @@ internal suspend fun executeFunctionDecl(
wrapper,
spec.visibility,
recordType = ObjRecord.Type.Fun,
callSignature = spec.externCallSignature,
typeDecl = spec.typeDecl
)
} ?: run {
@ -245,7 +256,8 @@ internal suspend fun executeFunctionDecl(
isOverride = spec.isOverride,
type = ObjRecord.Type.Fun,
methodId = spec.memberMethodId,
typeDecl = spec.typeDecl
typeDecl = spec.typeDecl,
callSignature = spec.externCallSignature
)
val memberValue = cls.members[spec.name]?.value ?: compiledFnBody
scope.addItem(

View File

@ -42,6 +42,7 @@ class BytecodeCompiler(
private val enumEntriesByName: Map<String, List<String>> = emptyMap(),
private val callableReturnTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(),
private val callableReturnTypeByName: Map<String, ObjClass> = emptyMap(),
private val callSignatureByName: Map<String, CallSignature> = emptyMap(),
private val externCallableNames: Set<String> = emptySet(),
private val externBindingNames: Set<String> = emptySet(),
private val preparedModuleBindingNames: Set<String> = emptySet(),
@ -4980,22 +4981,22 @@ class BytecodeCompiler(
}
private fun compileInlineHigherOrderMethodCall(ref: MethodCallRef): CompiledValue? {
val spec = inlineHigherOrderMethodSpec(ref.name) ?: return null
val spec = inlineHigherOrderMethodSpec(ref) ?: return null
if (ref.args.size != spec.argCount || ref.args.any { it.isSplat || it.name != null }) return null
if (!ref.explicitTypeArgs.isNullOrEmpty()) return null
val lambdaRef = extractExactLambdaRef(ref.args[spec.lambdaArgIndex].value) ?: return null
val inlineRef = lambdaRef.inlineBodyRef ?: return null
return when (spec.kind) {
InlineHigherOrderMethodKind.UNARY_ARGUMENT -> {
CallSignature.Kind.UNARY_ARGUMENT -> {
if (!isMethodInlineSafe(lambdaRef, inlineRef, allowReceiverRefs = false, allowCaptures = true)) return null
val receiver = compileRefWithFallback(ref.receiver, null, refPosOrCurrent(ref.receiver)) ?: return null
val receiverObj = ensureObjSlot(receiver)
compileOptionalInlineMethod(ref.isOptional, receiverObj) {
val bindings = prepareInlineLambdaBindingsFromValues(lambdaRef, listOf(receiver)) ?: return@compileOptionalInlineMethod null
when (spec.result) {
InlineHigherOrderResultMode.BLOCK_RESULT ->
CallSignature.ResultMode.BLOCK_RESULT ->
compileInlineLambdaBody(lambdaRef, inlineRef, bindings)
InlineHigherOrderResultMode.RETURN_RECEIVER -> {
CallSignature.ResultMode.RETURN_RECEIVER -> {
compileInlineLambdaBody(lambdaRef, inlineRef, bindings) ?: return@compileOptionalInlineMethod null
CompiledValue(receiverObj.slot, SlotType.OBJ)
}
@ -5003,7 +5004,7 @@ class BytecodeCompiler(
}
}
}
InlineHigherOrderMethodKind.RECEIVER -> {
CallSignature.Kind.RECEIVER -> {
val receiverInfo = receiverInlineInfo(lambdaRef) ?: return null
if (!isMethodInlineSafe(lambdaRef, inlineRef, allowReceiverRefs = true, allowCaptures = true)) return null
val receiver = compileRefWithFallback(ref.receiver, null, refPosOrCurrent(ref.receiver)) ?: return null
@ -5012,7 +5013,7 @@ class BytecodeCompiler(
compileInlineReceiverLambdaInvocation(receiverObj, lambdaRef, spec.result, receiverInfo)
}
}
InlineHigherOrderMethodKind.ITERABLE -> {
CallSignature.Kind.ITERABLE -> {
if (!isMethodInlineSafe(lambdaRef, inlineRef, allowReceiverRefs = false, allowCaptures = true)) return null
val receiver = compileRefWithFallback(ref.receiver, null, refPosOrCurrent(ref.receiver)) ?: return null
val receiverObj = ensureObjSlot(receiver)
@ -5020,7 +5021,7 @@ class BytecodeCompiler(
compileInlineIterableLambdaLoop(receiverObj, ref, lambdaRef, inlineRef, spec.result)
}
}
InlineHigherOrderMethodKind.MAP_GET_OR_PUT -> {
CallSignature.Kind.MAP_GET_OR_PUT -> {
val receiverClass = resolveReceiverClass(ref.receiver) ?: return null
if (receiverClass != ObjMap.type) return null
if (!isMethodInlineSafe(lambdaRef, inlineRef, allowReceiverRefs = false, allowCaptures = true)) return null
@ -5131,81 +5132,20 @@ class BytecodeCompiler(
return slot
}
private enum class InlineHigherOrderMethodKind {
UNARY_ARGUMENT,
RECEIVER,
ITERABLE,
MAP_GET_OR_PUT
}
private enum class InlineHigherOrderResultMode {
BLOCK_RESULT,
RETURN_RECEIVER,
FOR_EACH,
MAP,
FILTER,
MAP_NOT_NULL,
ASSOCIATE_BY
}
private data class InlineHigherOrderMethodSpec(
val kind: InlineHigherOrderMethodKind,
val result: InlineHigherOrderResultMode,
val argCount: Int = 1,
val lambdaArgIndex: Int = 0
)
private data class InlineReceiverInfo(
val explicitBindings: List<Pair<String, Int>>,
val thisTypeName: String?
)
private fun inlineHigherOrderMethodSpec(name: String): InlineHigherOrderMethodSpec? {
return when (name) {
"let" -> InlineHigherOrderMethodSpec(
InlineHigherOrderMethodKind.UNARY_ARGUMENT,
InlineHigherOrderResultMode.BLOCK_RESULT
)
"also" -> InlineHigherOrderMethodSpec(
InlineHigherOrderMethodKind.UNARY_ARGUMENT,
InlineHigherOrderResultMode.RETURN_RECEIVER
)
"apply" -> InlineHigherOrderMethodSpec(
InlineHigherOrderMethodKind.RECEIVER,
InlineHigherOrderResultMode.RETURN_RECEIVER
)
"run" -> InlineHigherOrderMethodSpec(
InlineHigherOrderMethodKind.RECEIVER,
InlineHigherOrderResultMode.BLOCK_RESULT
)
"forEach" -> InlineHigherOrderMethodSpec(
InlineHigherOrderMethodKind.ITERABLE,
InlineHigherOrderResultMode.FOR_EACH
)
"map" -> InlineHigherOrderMethodSpec(
InlineHigherOrderMethodKind.ITERABLE,
InlineHigherOrderResultMode.MAP
)
"filter" -> InlineHigherOrderMethodSpec(
InlineHigherOrderMethodKind.ITERABLE,
InlineHigherOrderResultMode.FILTER
)
"mapNotNull" -> InlineHigherOrderMethodSpec(
InlineHigherOrderMethodKind.ITERABLE,
InlineHigherOrderResultMode.MAP_NOT_NULL
)
"associateBy" -> InlineHigherOrderMethodSpec(
InlineHigherOrderMethodKind.ITERABLE,
InlineHigherOrderResultMode.ASSOCIATE_BY
)
"getOrPut" -> InlineHigherOrderMethodSpec(
InlineHigherOrderMethodKind.MAP_GET_OR_PUT,
InlineHigherOrderResultMode.BLOCK_RESULT,
argCount = 2,
lambdaArgIndex = 1
)
else -> null
}
private fun inlineHigherOrderMethodSpec(ref: MethodCallRef): CallSignature.HigherOrderInline? {
resolveReceiverClass(ref.receiver)
?.resolveInstanceMember(ref.name)
?.record
?.callSignature
?.inlineHigherOrder
?.let { return it }
val receiverClass = resolveReceiverClass(ref.receiver) ?: return null
return extensionCallableSignature(receiverClass, ref.name)?.inlineHigherOrder
}
private fun compileOptionalInlineMethod(
@ -5329,7 +5269,7 @@ class BytecodeCompiler(
private fun compileInlineReceiverLambdaInvocation(
receiverObj: CompiledValue,
lambdaRef: LambdaFnRef,
behavior: InlineHigherOrderResultMode,
behavior: CallSignature.ResultMode,
receiverInfo: InlineReceiverInfo
): CompiledValue? {
val inlineRef = lambdaRef.inlineBodyRef ?: return null
@ -5338,9 +5278,9 @@ class BytecodeCompiler(
inlineThisBindings.addLast(previousBinding)
return try {
when (behavior) {
InlineHigherOrderResultMode.BLOCK_RESULT ->
CallSignature.ResultMode.BLOCK_RESULT ->
compileInlineLambdaBody(lambdaRef, inlineRef, receiverInfo.explicitBindings)
InlineHigherOrderResultMode.RETURN_RECEIVER -> {
CallSignature.ResultMode.RETURN_RECEIVER -> {
compileInlineLambdaBody(lambdaRef, inlineRef, receiverInfo.explicitBindings) ?: return null
CompiledValue(receiverSlot, SlotType.OBJ)
}
@ -5372,7 +5312,7 @@ class BytecodeCompiler(
ref: MethodCallRef,
lambdaRef: LambdaFnRef,
inlineRef: ObjRef,
behavior: InlineHigherOrderResultMode
behavior: CallSignature.ResultMode
): CompiledValue? {
val iterableMethods = ObjIterable.instanceMethodIdMap(includeAbstract = true)
val iteratorMethodId = iterableMethods["iterator"]
@ -5388,17 +5328,17 @@ class BytecodeCompiler(
builder.emit(Opcode.ITER_PUSH, iterSlot)
val result = when (behavior) {
InlineHigherOrderResultMode.FOR_EACH -> CompiledValue(ensureVoidSlot(), SlotType.OBJ)
InlineHigherOrderResultMode.MAP,
InlineHigherOrderResultMode.FILTER,
InlineHigherOrderResultMode.MAP_NOT_NULL -> createEmptyMutableList() ?: return null
InlineHigherOrderResultMode.ASSOCIATE_BY -> createEmptyMutableMap() ?: return null
CallSignature.ResultMode.FOR_EACH -> CompiledValue(ensureVoidSlot(), SlotType.OBJ)
CallSignature.ResultMode.MAP,
CallSignature.ResultMode.FILTER,
CallSignature.ResultMode.MAP_NOT_NULL -> createEmptyMutableList() ?: return null
CallSignature.ResultMode.ASSOCIATE_BY -> createEmptyMutableMap() ?: return null
else -> return null
}
if (behavior == InlineHigherOrderResultMode.FILTER) {
if (behavior == CallSignature.ResultMode.FILTER) {
listElementClassFromReceiverRef(ref.receiver)?.let { listElementClassBySlot[result.slot] = it }
}
if (behavior == InlineHigherOrderResultMode.MAP) {
if (behavior == CallSignature.ResultMode.MAP) {
lambdaRef.inferredReturnClass?.let { listElementClassBySlot[result.slot] = it }
}
@ -5418,14 +5358,14 @@ class BytecodeCompiler(
val nextObj = ensureObjSlot(CompiledValue(nextSlot, SlotType.UNKNOWN))
val bindings = prepareInlineLambdaBindingsFromValues(lambdaRef, listOf(nextObj)) ?: return null
when (behavior) {
InlineHigherOrderResultMode.FOR_EACH -> {
CallSignature.ResultMode.FOR_EACH -> {
compileInlineLambdaBody(lambdaRef, inlineRef, bindings) ?: return null
}
InlineHigherOrderResultMode.MAP -> {
CallSignature.ResultMode.MAP -> {
val mapped = compileInlineLambdaBody(lambdaRef, inlineRef, bindings) ?: return null
appendToList(result, mapped) ?: return null
}
InlineHigherOrderResultMode.FILTER -> {
CallSignature.ResultMode.FILTER -> {
val predicate = compileInlineLambdaBody(lambdaRef, inlineRef, bindings) ?: return null
val predicateBool = compileValueAsBool(predicate)
val skipLabel = builder.label()
@ -5436,7 +5376,7 @@ class BytecodeCompiler(
appendToList(result, nextObj) ?: return null
builder.mark(skipLabel)
}
InlineHigherOrderResultMode.MAP_NOT_NULL -> {
CallSignature.ResultMode.MAP_NOT_NULL -> {
val mapped = compileInlineLambdaBody(lambdaRef, inlineRef, bindings) ?: return null
val mappedObj = ensureObjSlot(mapped)
val nullSlot = allocSlot()
@ -5451,7 +5391,7 @@ class BytecodeCompiler(
appendToList(result, mappedObj) ?: return null
builder.mark(skipLabel)
}
InlineHigherOrderResultMode.ASSOCIATE_BY -> {
CallSignature.ResultMode.ASSOCIATE_BY -> {
val key = compileInlineLambdaBody(lambdaRef, inlineRef, bindings) ?: return null
appendToMap(result, key, nextObj)
}
@ -5986,11 +5926,11 @@ class BytecodeCompiler(
return names
}
private fun resolveExtensionSlotByReceiverNames(
private fun resolveExtensionWrapperNameByReceiverNames(
receiverClass: ObjClass,
memberName: String,
wrapperName: (String, String) -> String
): CompiledValue? {
): String? {
for (receiverName in extensionReceiverTypeNames(receiverClass)) {
val candidate = wrapperName(receiverName, memberName)
if (allowedScopeNames != null &&
@ -5999,15 +5939,15 @@ class BytecodeCompiler(
) {
continue
}
resolveDirectNameSlot(candidate)?.let { return it }
if (resolveDirectNameSlot(candidate) != null) return candidate
}
return null
}
private fun resolveUniqueExtensionWrapperSlot(
private fun resolveUniqueExtensionWrapperName(
memberName: String,
wrapperPrefix: String
): CompiledValue? {
): String? {
val suffix = "__$memberName"
val candidates = LinkedHashSet<String>()
for (name in localSlotIndexByName.keys) {
@ -6020,8 +5960,31 @@ class BytecodeCompiler(
candidates.add(name)
}
}
if (candidates.size != 1) return null
return resolveDirectNameSlot(candidates.first())
return candidates.singleOrNull()
}
private fun resolveExtensionSlotByReceiverNames(
receiverClass: ObjClass,
memberName: String,
wrapperName: (String, String) -> String
): CompiledValue? {
val resolvedName = resolveExtensionWrapperNameByReceiverNames(receiverClass, memberName, wrapperName) ?: return null
return resolveDirectNameSlot(resolvedName)
}
private fun resolveUniqueExtensionWrapperSlot(
memberName: String,
wrapperPrefix: String
): CompiledValue? {
val resolvedName = resolveUniqueExtensionWrapperName(memberName, wrapperPrefix) ?: return null
return resolveDirectNameSlot(resolvedName)
}
private fun extensionCallableSignature(receiverClass: ObjClass, memberName: String): CallSignature? {
val wrapperName = resolveExtensionWrapperNameByReceiverNames(receiverClass, memberName, ::extensionCallableName)
?: resolveUniqueExtensionWrapperName(memberName, "__ext__")
?: return null
return callSignatureByName[wrapperName]
}
private fun resolveExtensionCallableSlot(receiverClass: ObjClass, memberName: String): CompiledValue? {

View File

@ -106,6 +106,7 @@ class BytecodeStatement private constructor(
enumEntriesByName: Map<String, List<String>> = emptyMap(),
callableReturnTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(),
callableReturnTypeByName: Map<String, ObjClass> = emptyMap(),
callSignatureByName: Map<String, CallSignature> = emptyMap(),
externCallableNames: Set<String> = emptySet(),
externBindingNames: Set<String> = emptySet(),
preparedModuleBindingNames: Set<String> = emptySet(),
@ -146,6 +147,7 @@ class BytecodeStatement private constructor(
enumEntriesByName = enumEntriesByName,
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
callableReturnTypeByName = callableReturnTypeByName,
callSignatureByName = callSignatureByName,
externCallableNames = externCallableNames,
externBindingNames = externBindingNames,
preparedModuleBindingNames = preparedModuleBindingNames,

View File

@ -99,10 +99,11 @@ fun ObjClass.addFnDoc(
visibility: Visibility = Visibility.Public,
tags: Map<String, List<String>> = emptyMap(),
moduleName: String? = null,
callSignature: net.sergeych.lyng.CallSignature? = null,
code: suspend ScopeFacade.() -> Obj
) {
// Register runtime method
addFn(name, isOpen, visibility, code = code)
addFn(name, isOpen, visibility, callSignature = callSignature, code = code)
// Register docs for the member under this class
BuiltinDocRegistry.module(moduleName ?: ownerModuleNameFromClassOrUnknown()) {
classDoc(this@addFnDoc.className, doc = "") {
@ -137,9 +138,10 @@ fun ObjClass.addClassFnDoc(
isOpen: Boolean = false,
tags: Map<String, List<String>> = emptyMap(),
moduleName: String? = null,
callSignature: net.sergeych.lyng.CallSignature? = null,
code: suspend ScopeFacade.() -> Obj
) {
addClassFn(name, isOpen, code)
addClassFn(name, isOpen, callSignature, code)
BuiltinDocRegistry.module(moduleName ?: ownerModuleNameFromClassOrUnknown()) {
classDoc(this@addClassFnDoc.className, doc = "") {
method(name = name, doc = doc, params = params, returns = returns, isStatic = true, tags = tags)

View File

@ -828,7 +828,13 @@ open class Obj {
name = "let",
doc = "Calls the specified function block with `this` value as its argument and returns its result.",
params = listOf(ParamDoc("block")),
moduleName = "lyng.stdlib"
moduleName = "lyng.stdlib",
callSignature = CallSignature(
inlineHigherOrder = CallSignature.HigherOrderInline(
kind = CallSignature.Kind.UNARY_ARGUMENT,
result = CallSignature.ResultMode.BLOCK_RESULT
)
)
) {
call(args.firstAndOnly(), Arguments(thisObj))
}
@ -836,7 +842,13 @@ open class Obj {
name = "apply",
doc = "Calls the specified function block with `this` value as its receiver and returns `this` value.",
params = listOf(ParamDoc("block")),
moduleName = "lyng.stdlib"
moduleName = "lyng.stdlib",
callSignature = CallSignature(
inlineHigherOrder = CallSignature.HigherOrderInline(
kind = CallSignature.Kind.RECEIVER,
result = CallSignature.ResultMode.RETURN_RECEIVER
)
)
) {
val body = args.firstAndOnly()
val scope = requireScope()
@ -852,7 +864,13 @@ open class Obj {
name = "also",
doc = "Calls the specified function block with `this` value as its argument and returns `this` value.",
params = listOf(ParamDoc("block")),
moduleName = "lyng.stdlib"
moduleName = "lyng.stdlib",
callSignature = CallSignature(
inlineHigherOrder = CallSignature.HigherOrderInline(
kind = CallSignature.Kind.UNARY_ARGUMENT,
result = CallSignature.ResultMode.RETURN_RECEIVER
)
)
) {
call(args.firstAndOnly(), Arguments(thisObj))
thisObj
@ -861,7 +879,13 @@ open class Obj {
name = "run",
doc = "Calls the specified function block with `this` value as its receiver and returns its result.",
params = listOf(ParamDoc("block")),
moduleName = "lyng.stdlib"
moduleName = "lyng.stdlib",
callSignature = CallSignature(
inlineHigherOrder = CallSignature.HigherOrderInline(
kind = CallSignature.Kind.RECEIVER,
result = CallSignature.ResultMode.BLOCK_RESULT
)
)
) {
call(args.firstAndOnly())
}

View File

@ -849,6 +849,7 @@ open class ObjClass(
fieldId: Int? = null,
methodId: Int? = null,
typeDecl: net.sergeych.lyng.TypeDecl? = null,
callSignature: net.sergeych.lyng.CallSignature? = null,
): ObjRecord {
// Validation of override rules: only for non-system declarations
var existing: ObjRecord? = null
@ -949,6 +950,7 @@ open class ObjClass(
isOverride = isOverride,
isTransient = isTransient,
type = type,
callSignature = callSignature,
typeDecl = typeDecl,
memberName = name,
fieldId = effectiveFieldId,
@ -975,7 +977,8 @@ open class ObjClass(
isTransient: Boolean = false,
type: ObjRecord.Type = ObjRecord.Type.Field,
fieldId: Int? = null,
methodId: Int? = null
methodId: Int? = null,
callSignature: net.sergeych.lyng.CallSignature? = null
): ObjRecord {
initClassScope()
val existing = classScope!!.objects[name]
@ -1016,6 +1019,7 @@ open class ObjClass(
writeVisibility,
recordType = type,
isTransient = isTransient,
callSignature = callSignature,
fieldId = effectiveFieldId,
methodId = effectiveMethodId
)
@ -1035,6 +1039,7 @@ open class ObjClass(
isOverride: Boolean = false,
pos: Pos = Pos.builtIn,
methodId: Int? = null,
callSignature: net.sergeych.lyng.CallSignature? = null,
code: (suspend net.sergeych.lyng.ScopeFacade.() -> Obj)? = null
) {
val stmt = code?.let { ObjExternCallable.fromBridge { it() } } ?: ObjNull
@ -1042,7 +1047,8 @@ open class ObjClass(
name, stmt, isMutable, visibility, writeVisibility, pos, declaringClass,
isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride,
type = ObjRecord.Type.Fun,
methodId = methodId
methodId = methodId,
callSignature = callSignature
)
}
@ -1074,8 +1080,19 @@ open class ObjClass(
}
fun addClassConst(name: String, value: Obj) = createClassField(name, value)
fun addClassFn(name: String, isOpen: Boolean = false, code: suspend net.sergeych.lyng.ScopeFacade.() -> Obj) {
createClassField(name, ObjExternCallable.fromBridge { code() }, isOpen, type = ObjRecord.Type.Fun)
fun addClassFn(
name: String,
isOpen: Boolean = false,
callSignature: net.sergeych.lyng.CallSignature? = null,
code: suspend net.sergeych.lyng.ScopeFacade.() -> Obj
) {
createClassField(
name,
ObjExternCallable.fromBridge { code() },
isOpen,
type = ObjRecord.Type.Fun,
callSignature = callSignature
)
}

View File

@ -18,6 +18,7 @@
package net.sergeych.lyng.obj
import net.sergeych.lyng.Arguments
import net.sergeych.lyng.CallSignature
import net.sergeych.lyng.miniast.ParamDoc
import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.addPropertyDoc
@ -189,7 +190,13 @@ val ObjIterable by lazy {
doc = "Build a map from elements using the lambda result as key.",
params = listOf(ParamDoc("keySelector")),
returns = type("lyng.Map"),
moduleName = "lyng.stdlib"
moduleName = "lyng.stdlib",
callSignature = CallSignature(
inlineHigherOrder = CallSignature.HigherOrderInline(
kind = CallSignature.Kind.ITERABLE,
result = CallSignature.ResultMode.ASSOCIATE_BY
)
)
) {
val association = requireOnlyArg<Obj>()
val result = ObjMap()
@ -204,7 +211,13 @@ val ObjIterable by lazy {
doc = "Apply the lambda to each element in iteration order.",
params = listOf(ParamDoc("action")),
isOpen = true,
moduleName = "lyng.stdlib"
moduleName = "lyng.stdlib",
callSignature = CallSignature(
inlineHigherOrder = CallSignature.HigherOrderInline(
kind = CallSignature.Kind.ITERABLE,
result = CallSignature.ResultMode.FOR_EACH
)
)
) {
val scope = requireScope()
val it = thisObj.invokeInstanceMethod(scope, "iterator")
@ -222,7 +235,13 @@ val ObjIterable by lazy {
params = listOf(ParamDoc("transform")),
returns = type("lyng.List"),
isOpen = true,
moduleName = "lyng.stdlib"
moduleName = "lyng.stdlib",
callSignature = CallSignature(
inlineHigherOrder = CallSignature.HigherOrderInline(
kind = CallSignature.Kind.ITERABLE,
result = CallSignature.ResultMode.MAP
)
)
) {
val fn = requiredArg<Obj>(0)
val result = mutableListOf<Obj>()
@ -238,7 +257,13 @@ val ObjIterable by lazy {
params = listOf(ParamDoc("transform")),
returns = type("lyng.List"),
isOpen = true,
moduleName = "lyng.stdlib"
moduleName = "lyng.stdlib",
callSignature = CallSignature(
inlineHigherOrder = CallSignature.HigherOrderInline(
kind = CallSignature.Kind.ITERABLE,
result = CallSignature.ResultMode.MAP_NOT_NULL
)
)
) {
val fn = requiredArg<Obj>(0)
val result = mutableListOf<Obj>()

View File

@ -19,6 +19,7 @@ package net.sergeych.lyng.obj
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import net.sergeych.lyng.CallSignature
import net.sergeych.lyng.Scope
import net.sergeych.lyng.miniast.*
import net.sergeych.lynon.LynonDecoder
@ -262,7 +263,15 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
doc = "Get value by key or compute, store, and return the default from a lambda.",
params = listOf(ParamDoc("key"), ParamDoc("default")),
returns = type("lyng.Any"),
moduleName = "lyng.stdlib"
moduleName = "lyng.stdlib",
callSignature = CallSignature(
inlineHigherOrder = CallSignature.HigherOrderInline(
kind = CallSignature.Kind.MAP_GET_OR_PUT,
result = CallSignature.ResultMode.BLOCK_RESULT,
argCount = 2,
lambdaArgIndex = 1
)
)
) {
val key = requiredArg<Obj>(0)
thisAs<ObjMap>().map.getOrPut(key) {

View File

@ -0,0 +1,146 @@
# Non-Suspending Call Optimization Plan
## Current state
Completed in the current phase:
- Higher-order lambda inlining is now metadata-driven through `CallSignature`.
- Built-in member methods (`let`, `also`, `apply`, `run`, `forEach`, `map`, `mapNotNull`, `associateBy`, `getOrPut`) publish inline metadata at declaration sites.
- Lyng extension wrappers now preserve and expose `callSignature`, so extension methods such as `Iterable.filter` use the same inlining path as built-in members.
- `BytecodeCompiler` no longer relies on a backend hardcoded name table for these higher-order inlining cases.
- JVM tests are green after the metadata move.
Primary motivation remains unchanged: suspend call overhead is still significant, and lambda inlining only removes part of it.
## Constraints
- Keep source positions, stack traces, and throw-site reporting correct.
- Do not reintroduce one-off special cases tied to specific stdlib method names.
- Prefer declaration metadata and reusable compiler/runtime mechanisms.
- Preserve Kotlin Multiplatform compatibility in `commonMain`.
- Avoid changing public language semantics just to optimize the runtime path.
## Why this is the next step
Lambda inlining helps when the callee body is directly available at the call site.
The next large remaining cost is calling compiled functions through suspend entry points even when the generated body never suspends.
That suggests a second optimization track:
1. detect bytecode callables that are safe to execute through a non-suspending fast path;
2. route direct calls to that path when the caller can prove it is safe;
3. keep the suspend path as the fallback for correctness.
## Proposed phases
### Phase 1: Define "non-suspending compiled callable"
Add explicit metadata on compiled functions / lambdas indicating whether their bytecode body may suspend.
Requirements:
- Computed once during bytecode generation.
- Conservative: false negatives are acceptable; false positives are not.
- Must account for:
- direct suspend-capable call opcodes;
- flow / coroutine constructs;
- delegated runtime helpers that may suspend;
- nested lambda creation if invocation may suspend.
Likely implementation direction:
- store `maySuspend` or `fastOnly`-adjacent metadata on `CmdFunction` or the callable wrapper;
- derive it from emitted bytecode opcodes and embedded lambda constants.
### Phase 2: Add a direct non-suspending invoke path
For bytecode callables proven non-suspending, add an execution entry point that avoids suspend machinery for ordinary direct calls.
Requirements:
- Reuse as much of the existing fast frame setup as possible.
- Keep exception translation and source mapping identical to the suspend path.
- Do not depend on JVM-only tricks.
Potential direction:
- extend `BytecodeCallable` with a capability query or richer fast-call API;
- let call sites choose among:
- inline body
- non-suspending compiled call
- existing suspend call
### Phase 3: Teach bytecode call sites to use it
Apply the new path only where the callee is known precisely.
Initial targets:
- direct lambda invocation where exact lambda ref is known but inlining is not possible;
- direct local function calls where the binding resolves to a compiled callable;
- extension wrapper calls where wrapper binding is known and non-suspending.
Do not start with dynamic dispatch or reflective calls.
### Phase 4: Validate behavioral fidelity
Must explicitly verify:
- thrown exceptions still report the same Lyng source positions;
- stack traces remain useful enough for debugging;
- optional calls / null propagation are unchanged;
- captures and implicit `this` still bind correctly.
### Phase 5: Measure before broadening
Benchmark after each widening step, especially:
- `OptTest.testAddToArray`
- iterable pipeline samples using `filter` / `map`
- direct lambda call microbenchmarks
- closure-heavy samples with captures
## Open technical questions
1. Where should non-suspending capability live?
- `CmdFunction`
- `BytecodeStatement`
- callable wrapper object
- `CallSignature`-adjacent metadata
2. Should the compiler emit a separate opcode for known non-suspending compiled calls, or should runtime dispatch pick the fast path from a normal call opcode?
3. Can we preserve the current error/stack behavior if we bypass suspend wrappers entirely, or do we need a thin compatibility layer?
4. Should capture-free and capture-heavy compiled lambdas share the same direct-call mechanism, or should captured callables stay on the safer path initially?
## Suggested order of execution
1. Add conservative `maySuspend` analysis for compiled bytecode functions.
2. Expose a non-suspending direct-call capability on compiled callables.
3. Use it for exact direct lambda calls first.
4. Extend to exact local function calls.
5. Re-measure.
6. Only then consider broader dispatch sites.
## Validation checklist
- `./gradlew :lynglib:compileKotlinJvm --console=plain`
- `./gradlew :lynglib:jvmTest --tests net.sergeych.lyng.OptTest.testAddToArray --console=plain`
- `./gradlew :lynglib:jvmTest --tests StdlibTest.testIterableFilter --tests CompilerVmReviewRegressionTest --console=plain`
- `./gradlew :lynglib:jvmTest --console=plain`
## Notes from the completed phase
Relevant current commits before this follow-up work:
- `3be2892` Use fast compiled callbacks in dynamic and flow helpers
- `1d5caaa` Broaden lambda method inlining with captures
- `0c3242c` Generalize higher-order lambda inlining
- `f4ab2eb` Extend lambda inlining to getOrPut and implicit it calls
Current working tree phase adds:
- metadata-driven higher-order inlining through member and extension signatures;
- extension wrapper signature propagation;
- removal of the compiler-side higher-order name table fallback.