diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CallSignature.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CallSignature.kt index 6f0fb06..3ab976c 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CallSignature.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CallSignature.kt @@ -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 + } +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 20d0428..55511a8 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -187,6 +187,7 @@ class Compiler( private val callableReturnTypeByScopeId: MutableMap> = mutableMapOf() private val callableReturnTypeByName: MutableMap = mutableMapOf() private val callableReturnTypeDeclByName: MutableMap = mutableMapOf() + private val callSignatureByName: MutableMap = mutableMapOf() private val lambdaReturnTypeByRef: MutableMap = mutableMapOf() private val exactLambdaRefByScopeId: MutableMap> = mutableMapOf() private val lambdaCaptureEntriesByRef: MutableMap> = @@ -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) } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/FunctionDeclStatement.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/FunctionDeclStatement.kt index b34de2c..111f4ea 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/FunctionDeclStatement.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/FunctionDeclStatement.kt @@ -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( 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 b0b7e8e..62565f8 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeCompiler.kt @@ -42,6 +42,7 @@ class BytecodeCompiler( private val enumEntriesByName: Map> = emptyMap(), private val callableReturnTypeByScopeId: Map> = emptyMap(), private val callableReturnTypeByName: Map = emptyMap(), + private val callSignatureByName: Map = emptyMap(), private val externCallableNames: Set = emptySet(), private val externBindingNames: Set = emptySet(), private val preparedModuleBindingNames: Set = 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>, 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() 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? { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt index 1ffa3ef..e6b35a0 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/BytecodeStatement.kt @@ -106,6 +106,7 @@ class BytecodeStatement private constructor( enumEntriesByName: Map> = emptyMap(), callableReturnTypeByScopeId: Map> = emptyMap(), callableReturnTypeByName: Map = emptyMap(), + callSignatureByName: Map = emptyMap(), externCallableNames: Set = emptySet(), externBindingNames: Set = emptySet(), preparedModuleBindingNames: Set = emptySet(), @@ -146,6 +147,7 @@ class BytecodeStatement private constructor( enumEntriesByName = enumEntriesByName, callableReturnTypeByScopeId = callableReturnTypeByScopeId, callableReturnTypeByName = callableReturnTypeByName, + callSignatureByName = callSignatureByName, externCallableNames = externCallableNames, externBindingNames = externBindingNames, preparedModuleBindingNames = preparedModuleBindingNames, diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/DocRegistrationHelpers.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/DocRegistrationHelpers.kt index 19f1207..92cb050 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/DocRegistrationHelpers.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/DocRegistrationHelpers.kt @@ -99,10 +99,11 @@ fun ObjClass.addFnDoc( visibility: Visibility = Visibility.Public, tags: Map> = 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> = 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) 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 0b064e8..d46a535 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/Obj.kt @@ -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()) } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt index 3d6efbb..ffba564 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt @@ -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 + ) } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjIterable.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjIterable.kt index 4b03471..86e03bf 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjIterable.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjIterable.kt @@ -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() 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(0) val result = mutableListOf() @@ -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(0) val result = mutableListOf() diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjMap.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjMap.kt index f174323..55a7dec 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjMap.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjMap.kt @@ -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 = 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(0) thisAs().map.getOrPut(key) { diff --git a/notes/non_suspending_call_optimization_plan.md b/notes/non_suspending_call_optimization_plan.md new file mode 100644 index 0000000..d01b9f2 --- /dev/null +++ b/notes/non_suspending_call_optimization_plan.md @@ -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.