From e107296bcabdeee0ac394bb9fa1c53d00ab496c6 Mon Sep 17 00:00:00 2001 From: sergeych Date: Wed, 29 Apr 2026 12:59:24 +0300 Subject: [PATCH] Add receiver-stack function types --- docs/OOP.md | 25 ++ docs/ai_language_reference.md | 6 + docs/tutorial.md | 34 +++ .../kotlin/net/sergeych/lyng/CodeContext.kt | 7 +- .../kotlin/net/sergeych/lyng/Compiler.kt | 213 ++++++++++++++---- .../kotlin/net/sergeych/lyng/TypeDecl.kt | 1 + .../lyng/miniast/BuiltinDocRegistry.kt | 11 +- .../net/sergeych/lyng/miniast/MiniAst.kt | 1 + .../net/sergeych/lyng/obj/ObjTypeExpr.kt | 12 +- lynglib/src/commonTest/kotlin/MiniAstTest.kt | 23 ++ .../kotlin/net/sergeych/lyng/OptTest.kt | 128 +++++++++++ 11 files changed, 415 insertions(+), 46 deletions(-) diff --git a/docs/OOP.md b/docs/OOP.md index 172b91b..f16c2aa 100644 --- a/docs/OOP.md +++ b/docs/OOP.md @@ -454,6 +454,31 @@ Key rules and features: - For arbitrary receivers, use casts: `(expr as Type).member(...)` or `(expr as? Type)?.member(...)`. - Qualified access does not relax visibility. +### Receiver-stack lambdas + +Qualified `this@Type` is also used outside inheritance when a lambda has multiple visible receivers. +This is common in DSL-style builders. + +- `A & B` means one receiver value that implements both types. +- `context(A, B) C.()->R` means a receiver stack: + - primary `this` is `C` + - outer/context receivers are `A`, then `B` +- Unqualified lookup checks the primary receiver first. +- If the primary receiver does not define a member and several outer/context receivers do, Lyng reports a compile-time ambiguity. Use `this@Type` to select one explicitly. + +Example: + +```lyng +class Html { fun title() = "html" } +class Head { fun title() = "head" } +class Body + +val block: context(Html, Head) Body.()->String = { + // title() // compile-time ambiguity: Html vs Head + this@Html.title() +} +``` + - Field inheritance (`val`/`var`) and collisions - Instance storage is kept per declaring class, internally disambiguated; unqualified read/write resolves to the first match in the resolution order (leftmost base). - Qualified read/write (via `this@Type` or casts) targets the chosen ancestor’s storage. diff --git a/docs/ai_language_reference.md b/docs/ai_language_reference.md index 213ffe0..df15065 100644 --- a/docs/ai_language_reference.md +++ b/docs/ai_language_reference.md @@ -164,7 +164,12 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T - unions `A | B` - intersections `A & B` - function types `(A, B)->R` and receiver form `Receiver.(A)->R` + - receiver-stack function types via `context(A, B) Receiver.(P)->R` - variadics in function type via ellipsis (`T...`) +- `A & B` means one value implementing both types. +- `context(A, B) Receiver.(P)->R` is different: it declares an ordered implicit-receiver stack where `Receiver` is primary `this`, then `A`, then `B`. +- Nested receiver lambdas keep outer receivers in scope; unqualified lookup prefers the innermost receiver, and `this@Type` can select an outer/context receiver explicitly. +- If the primary receiver does not provide a member and multiple outer/context receivers do, the lookup is a compile-time ambiguity and must be disambiguated with `this@Type`. - Generics: - type params on classes/functions/type aliases - bounds via `:` with union/intersection expressions @@ -217,6 +222,7 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T - Disambiguation helpers are supported: - qualified this: `this@Base.member()` - cast view: `(obj as Base).member()` +- In nested receiver lambdas, `this@Type` can target any receiver visible through the receiver stack, not just inheritance ancestors. - On unknown receiver types, compiler allows only Object-safe members: - `toString`, `toInspectString`, `let`, `also`, `apply`, `run` - Other members require known receiver type or explicit cast. diff --git a/docs/tutorial.md b/docs/tutorial.md index 0e266c5..0fd936b 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -352,6 +352,40 @@ Sets `this` to the first argument and executes the block. Returns the value retu assertEquals(3, sum) >>> void +Receiver lambdas can also keep outer receivers in scope. The primary receiver wins for unqualified lookup, and `this@Type` +selects an outer receiver explicitly: + + class Html { fun lang() = "en" } + class Body { fun lang() = "body" } + + fun html(block: Html.()->String) = with(Html()) { block(this) } + fun body(block: Body.()->String) = with(Body()) { block(this) } + + val result = html { + body { + lang() + ":" + this@Html.lang() + } + } + assertEquals("body:en", result) + >>> void + +You can declare the same requirement in a function type: + + val block: context(Html) Body.()->String = { + lang() + ":" + this@Html.lang() + } + +If the primary receiver does not define a member and multiple outer/context receivers do, Lyng reports an ambiguity instead of picking one silently: + + class A { fun title() = "a" } + class B { fun title() = "b" } + class C + + val block: context(A, B) C.()->String = { + // title() // compile-time ambiguity + this@A.title() + } + ## run Executes a block after it returning the value passed by the block. for example, can be used with elvis operator: diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CodeContext.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CodeContext.kt index f4ceddd..0742f5a 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CodeContext.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CodeContext.kt @@ -22,13 +22,16 @@ sealed class CodeContext { class Function( val name: String, val implicitThisMembers: Boolean = false, - val implicitThisTypeName: String? = null, + val implicitReceiverTypeNames: List = emptyList(), val typeParams: Set = emptySet(), val typeParamDecls: List = emptyList(), /** True for static methods and top-level functions: they have no implicit `this`, * so class-body field initializers inside them should not inherit the class name. */ val noImplicitThis: Boolean = false - ): CodeContext() + ): CodeContext() { + val implicitThisTypeName: String? + get() = implicitReceiverTypeNames.firstOrNull() + } class ClassBody(val name: String, val isExtern: Boolean = false): CodeContext() { var typeParams: Set = emptySet() var typeParamDecls: List = emptyList() diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index e82a8d4..f9240a2 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -857,32 +857,83 @@ class Compiler( return signature?.tailBlockReceiverType ?: if (name == "flow") "FlowBuilder" else null } - private fun currentImplicitThisTypeName(): String? { + private fun mergeReceiverTypeNames(vararg groups: Iterable): List { + val result = mutableListOf() + for (group in groups) { + for (typeName in group) { + val normalized = typeName?.takeIf { it.isNotBlank() } ?: continue + if (!result.contains(normalized)) { + result += normalized + } + } + } + return result + } + + private fun typeDeclReceiverTypeNames(typeDecl: TypeDecl.Function?): List { + if (typeDecl == null) return emptyList() + + fun receiverTypeName(typeDecl: TypeDecl?): String? = when (typeDecl) { + is TypeDecl.Simple -> typeDecl.name + is TypeDecl.Generic -> typeDecl.name + else -> null + } + + return mergeReceiverTypeNames( + listOf(receiverTypeName(typeDecl.receiver)), + typeDecl.contextReceivers.map(::receiverTypeName) + ) + } + + private fun currentImplicitReceiverTypeNames(): List { + val result = mutableListOf() for (ctx in codeContexts.asReversed()) { when (ctx) { is CodeContext.Function -> { - if (ctx.implicitThisTypeName != null) return ctx.implicitThisTypeName + result.addAll( + ctx.implicitReceiverTypeNames.filter { typeName -> + typeName.isNotBlank() && !result.contains(typeName) + } + ) // A static method or top-level function explicitly has no implicit `this`. // Stop here — do not fall through to an enclosing ClassBody. - if (ctx.noImplicitThis) return null + if (ctx.noImplicitThis) return result } // Class field initializers are compiled directly under ClassBody with no wrapping // Function. Lambdas inside those initializers must still see `this` as the class. - is CodeContext.ClassBody -> return ctx.name + is CodeContext.ClassBody -> { + if (!result.contains(ctx.name)) { + result += ctx.name + } + return result + } else -> {} } } - return null + return result } - private fun implicitReceiverTypeForMember(name: String): String? { - for (ctx in codeContexts.asReversed()) { - val fn = ctx as? CodeContext.Function ?: continue - if (!fn.implicitThisMembers) continue - val typeName = fn.implicitThisTypeName ?: continue - if (hasImplicitThisMember(name, typeName)) return typeName + private fun currentImplicitThisTypeName(): String? { + return currentImplicitReceiverTypeNames().firstOrNull() + } + + private fun implicitReceiverTypeForMember(name: String, pos: Pos? = null): String? { + val visibleReceivers = currentImplicitReceiverTypeNames() + if (visibleReceivers.isEmpty()) return null + val matching = visibleReceivers.filter { hasImplicitThisMember(name, it) } + if (matching.isEmpty()) return null + + val primary = visibleReceivers.firstOrNull() + if (primary != null && matching.first() == primary) { + return primary } - return null + if (matching.size > 1 && pos != null) { + throw ScriptError( + pos, + "member '$name' is ambiguous between receivers ${matching.joinToString(", ")}; use this@Type to disambiguate" + ) + } + return matching.first() } private fun currentEnclosingClassName(): String? { @@ -1151,10 +1202,11 @@ class Compiler( val classCtx = codeContexts.lastOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody if (classCtx != null && classCtx.declaredMembers.contains(name)) { resolutionSink?.referenceMember(name, pos) - val ids = resolveMemberIds(name, pos, null) - return ImplicitThisMemberRef(name, pos, ids.fieldId, ids.methodId, currentImplicitThisTypeName()) + val implicitType = implicitReceiverTypeForMember(name, pos) ?: classCtx.name + val ids = resolveImplicitThisMemberIds(name, pos, implicitType) + return ImplicitThisMemberRef(name, pos, ids.fieldId, ids.methodId, implicitType) } - val implicitTypeFromFunc = implicitReceiverTypeForMember(name) + val implicitTypeFromFunc = implicitReceiverTypeForMember(name, pos) val hasImplicitClassMember = classCtx != null && hasImplicitThisMember(name, classCtx.name) if (implicitTypeFromFunc == null && !hasImplicitClassMember) { val modulePlan = moduleSlotPlan() @@ -1277,7 +1329,7 @@ class Compiler( if (hasImplicitThisMember(name, implicitType)) { resolutionSink?.referenceMember(name, pos, implicitType) val ids = resolveImplicitThisMemberIds(name, pos, implicitType) - val preferredType = if (currentImplicitThisTypeName() == null) null else implicitType + val preferredType = if (currentImplicitReceiverTypeNames().isEmpty()) null else implicitType return ImplicitThisMemberRef(name, pos, ids.fieldId, ids.methodId, preferredType) } } @@ -1687,6 +1739,7 @@ class Compiler( ) is TypeDecl.Function -> TypeDecl.Function( receiver = decl.receiver?.let { transform(it) }, + contextReceivers = decl.contextReceivers.map { transform(it) }, params = decl.params.map { transform(it) }, returnType = transform(decl.returnType), nullable = decl.isNullable @@ -3498,13 +3551,12 @@ class Compiler( implicitItType: TypeDecl? = null, expectedCallableType: TypeDecl.Function? = null ): ObjRef { - fun receiverTypeName(typeDecl: TypeDecl?): String? = when (typeDecl) { - is TypeDecl.Simple -> typeDecl.name - is TypeDecl.Generic -> typeDecl.name - else -> null - } - - val effectiveExpectedReceiverType = expectedReceiverType ?: receiverTypeName(expectedCallableType?.receiver) + val effectiveImplicitReceiverTypes = mergeReceiverTypeNames( + listOf(expectedReceiverType), + typeDeclReceiverTypeNames(expectedCallableType), + currentImplicitReceiverTypeNames() + ) + val effectiveExpectedReceiverType = effectiveImplicitReceiverTypes.firstOrNull() // lambda args are different: val startPos = cc.currentPos() val label = lastLabel @@ -3572,7 +3624,7 @@ class Compiler( CodeContext.Function( "", implicitThisMembers = true, - implicitThisTypeName = effectiveExpectedReceiverType + implicitReceiverTypeNames = effectiveImplicitReceiverTypes ) ) { val returnLabels = label?.let { setOf(it) } ?: emptySet() @@ -3834,6 +3886,7 @@ class Compiler( ) val lambdaTypeDecl = TypeDecl.Function( receiver = effectiveExpectedReceiverType?.let { TypeDecl.Simple(it, false) }, + contextReceivers = effectiveImplicitReceiverTypes.drop(1).map { TypeDecl.Simple(it, false) }, params = lambdaParamTypeDecls.toList(), returnType = inferredReturnDecl ?: returnClass?.let { TypeDecl.Simple(it.className, false) } ?: TypeDecl.TypeAny, nullable = false @@ -4247,9 +4300,10 @@ class Compiler( } is TypeDecl.Function -> { val receiver = type.receiver?.let { expandTypeAliases(it, pos, seen) } + val contextReceivers = type.contextReceivers.map { expandTypeAliases(it, pos, seen) } val params = type.params.map { expandTypeAliases(it, pos, seen) } val ret = expandTypeAliases(type.returnType, pos, seen) - TypeDecl.Function(receiver, params, ret, type.nullable) + TypeDecl.Function(receiver, contextReceivers, params, ret, type.nullable) } is TypeDecl.Ellipsis -> { val elem = expandTypeAliases(type.elementType, pos, seen) @@ -4336,9 +4390,10 @@ class Compiler( } is TypeDecl.Function -> { val receiver = type.receiver?.let { substituteTypeAliasTypeVars(it, bindings) } + val contextReceivers = type.contextReceivers.map { substituteTypeAliasTypeVars(it, bindings) } val params = type.params.map { substituteTypeAliasTypeVars(it, bindings) } val ret = substituteTypeAliasTypeVars(type.returnType, bindings) - TypeDecl.Function(receiver, params, ret, type.nullable) + TypeDecl.Function(receiver, contextReceivers, params, ret, type.nullable) } is TypeDecl.Ellipsis -> { val elem = substituteTypeAliasTypeVars(type.elementType, bindings) @@ -4480,6 +4535,35 @@ class Compiler( var receiverDecl: TypeDecl? = null var receiverMini: MiniTypeRef? = null + val contextReceivers = mutableListOf>() + + run { + val contextSaved = cc.savePos() + val contextToken = cc.peekNextNonWhitespace() + if (contextToken.type != Token.Type.ID || contextToken.value != "context") { + return@run + } + cc.nextNonWhitespace() + if (!cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true)) { + cc.restorePos(contextSaved) + return@run + } + cc.skipWsTokens() + if (cc.peekNextNonWhitespace().type != Token.Type.RPAREN) { + while (true) { + val parsed = parseTypeExpressionWithMini() + contextReceivers += parsed + val sep = cc.nextNonWhitespace() + when (sep.type) { + Token.Type.COMMA -> continue + Token.Type.RPAREN -> break + else -> sep.raiseSyntax("expected ',' or ')' in context receiver list") + } + } + } else { + cc.nextNonWhitespace() + } + } val first = cc.peekNextNonWhitespace() if (first.type == Token.Type.LPAREN) { @@ -4524,12 +4608,14 @@ class Compiler( val mini = MiniFunctionType( range = MiniRange(rangeStart, rangeEnd), receiver = normalizedReceiverMini, + contextReceivers = contextReceivers.map { it.second }, params = params.map { it.second }, returnType = retMini, nullable = isNullable ) val sem = TypeDecl.Function( receiver = normalizedReceiverDecl, + contextReceivers = contextReceivers.map { it.first }, params = params.map { it.first }, returnType = retDecl, nullable = isNullable @@ -5100,7 +5186,17 @@ class Compiler( TypeDecl.TypeNullableAny -> "Any?" is TypeDecl.Simple -> "S:${type.name}" is TypeDecl.Generic -> "G:${type.name}<${type.args.joinToString(",") { typeDeclKey(it) }}>" - is TypeDecl.Function -> "F:(${type.params.joinToString(",") { typeDeclKey(it) }})->${typeDeclKey(type.returnType)}" + is TypeDecl.Function -> buildString { + append("F:") + type.receiver?.let { append("recv=").append(typeDeclKey(it)).append(";") } + if (type.contextReceivers.isNotEmpty()) { + append("ctx=").append(type.contextReceivers.joinToString(",") { typeDeclKey(it) }).append(";") + } + append('(') + append(type.params.joinToString(",") { typeDeclKey(it) }) + append(")->") + append(typeDeclKey(type.returnType)) + } is TypeDecl.Ellipsis -> "E:${typeDeclKey(type.elementType)}" is TypeDecl.TypeVar -> "V:${type.name}" is TypeDecl.Union -> "U:${type.options.joinToString("|") { typeDeclKey(it) }}" @@ -5793,6 +5889,14 @@ class Compiler( memberType.receiver?.let { declaredReceiver -> receiverDecl?.let { collectTypeVarBindings(declaredReceiver, it, bindings) } } + val contextLimit = minOf(memberType.contextReceivers.size, currentImplicitReceiverTypeNames().size) + for (i in 0 until contextLimit) { + collectTypeVarBindings( + memberType.contextReceivers[i], + TypeDecl.Simple(currentImplicitReceiverTypeNames()[i], false), + bindings + ) + } val paramList = memberType.params val ellipsisIndex = paramList.indexOfFirst { it is TypeDecl.Ellipsis } @@ -5842,6 +5946,7 @@ class Compiler( if (explicitTypeArgs.isNullOrEmpty()) return val typeVars = LinkedHashSet() memberType.receiver?.let { collectTypeVarNamesInOrder(it, typeVars) } + memberType.contextReceivers.forEach { collectTypeVarNamesInOrder(it, typeVars) } memberType.params.forEach { collectTypeVarNamesInOrder(it, typeVars) } collectTypeVarNamesInOrder(memberType.returnType, typeVars) val names = typeVars.toList() @@ -5857,6 +5962,7 @@ class Compiler( is TypeDecl.Generic -> type.args.forEach { collectTypeVarNamesInOrder(it, out) } is TypeDecl.Function -> { type.receiver?.let { collectTypeVarNamesInOrder(it, out) } + type.contextReceivers.forEach { collectTypeVarNamesInOrder(it, out) } type.params.forEach { collectTypeVarNamesInOrder(it, out) } collectTypeVarNamesInOrder(type.returnType, out) } @@ -6721,10 +6827,17 @@ class Compiler( } } is TypeDecl.Function -> { - if (argType is TypeDecl.Function && paramType.params.size == argType.params.size) { + if ( + argType is TypeDecl.Function && + paramType.params.size == argType.params.size && + paramType.contextReceivers.size == argType.contextReceivers.size + ) { if (paramType.receiver != null && argType.receiver != null) { collectTypeVarBindings(paramType.receiver, argType.receiver, out) } + for (i in paramType.contextReceivers.indices) { + collectTypeVarBindings(paramType.contextReceivers[i], argType.contextReceivers[i], out) + } for (i in paramType.params.indices) { collectTypeVarBindings(paramType.params[i], argType.params[i], out) } @@ -7207,7 +7320,7 @@ class Compiler( (ctx as? CodeContext.Function)?.implicitThisMembers == true } if ((classContext || implicitThis) && extensionNames.contains(left.name)) { - val receiverTypeName = implicitReceiverTypeForMember(left.name) ?: implicitThisTypeName + val receiverTypeName = implicitReceiverTypeForMember(left.name, left.pos()) ?: implicitThisTypeName val ids = resolveImplicitThisMemberIds(left.name, left.pos(), receiverTypeName) ImplicitThisMethodCallRef( left.name, @@ -7233,7 +7346,7 @@ class Compiler( (ctx as? CodeContext.Function)?.implicitThisMembers == true } if ((classContext || implicitThis) && extensionNames.contains(left.name)) { - val receiverTypeName = implicitReceiverTypeForMember(left.name) ?: implicitThisTypeName + val receiverTypeName = implicitReceiverTypeForMember(left.name, left.pos()) ?: implicitThisTypeName val ids = resolveImplicitThisMemberIds(left.name, left.pos(), receiverTypeName) ImplicitThisMethodCallRef( left.name, @@ -7284,6 +7397,16 @@ class Compiler( } private fun expectedCallableArgumentType(target: ObjRef, argIndex: Int): TypeDecl.Function? { + lookupNamedFunctionDecl(target)?.let { decl -> + val params = decl.params.map { param -> + if (param.isEllipsis) TypeDecl.Ellipsis(param.type) else param.type + } + if (argIndex < params.size) { + return params[argIndex] as? TypeDecl.Function + } + val ellipsis = params.lastOrNull() as? TypeDecl.Ellipsis + return ellipsis?.elementType as? TypeDecl.Function + } val decl = (resolveReceiverTypeDecl(target) ?: seedTypeDeclFromRef(target)) as? TypeDecl.Function ?: return null val params = when (target) { @@ -7609,7 +7732,7 @@ class Compiler( CodeContext.Function( "", implicitThisMembers = true, - implicitThisTypeName = implicitThisType + implicitReceiverTypeNames = listOfNotNull(implicitThisType) ) ) { parseBlock() @@ -9728,7 +9851,7 @@ class Compiler( CodeContext.Function( name, implicitThisMembers = implicitThisMembers, - implicitThisTypeName = implicitThisTypeName, + implicitReceiverTypeNames = listOfNotNull(implicitThisTypeName), typeParams = typeParams, typeParamDecls = typeParamDecls, noImplicitThis = noImplicitThis @@ -10843,9 +10966,17 @@ class Compiler( } } - val initialExpression = if (setNull || isProperty) null - else parseStatement(true) - ?: throw ScriptError(effectiveEqToken!!.pos, "Expected initializer expression") + val initialExpression = if (setNull || isProperty) { + null + } else if (varTypeDecl is TypeDecl.Function && cc.peekNextNonWhitespace().type == Token.Type.LBRACE) { + val brace = cc.nextNonWhitespace() + ExpressionStatement( + parseLambdaExpression(expectedCallableType = varTypeDecl), + brace.pos + ) + } else { + parseStatement(true) ?: throw ScriptError(effectiveEqToken!!.pos, "Expected initializer expression") + } if (varTypeDecl == TypeDecl.TypeAny && initialExpression != null) { val inferred = inferTypeDeclFromInitializer(initialExpression) @@ -11071,7 +11202,7 @@ class Compiler( CodeContext.Function( "", implicitThisMembers = accessorImplicitThisMembers, - implicitThisTypeName = accessorImplicitThisTypeName + implicitReceiverTypeNames = listOfNotNull(accessorImplicitThisTypeName) ) ) { wrapFunctionBytecode(parseBlock(), "") @@ -11083,7 +11214,7 @@ class Compiler( CodeContext.Function( "", implicitThisMembers = accessorImplicitThisMembers, - implicitThisTypeName = accessorImplicitThisTypeName + implicitReceiverTypeNames = listOfNotNull(accessorImplicitThisTypeName) ) ) { val expr = parseExpression() @@ -11110,7 +11241,7 @@ class Compiler( CodeContext.Function( "", implicitThisMembers = accessorImplicitThisMembers, - implicitThisTypeName = accessorImplicitThisTypeName + implicitReceiverTypeNames = listOfNotNull(accessorImplicitThisTypeName) ) ) { wrapFunctionBytecode(parseBlockWithPredeclared(listOf(setArgName to true)), "") @@ -11123,7 +11254,7 @@ class Compiler( CodeContext.Function( "", implicitThisMembers = accessorImplicitThisMembers, - implicitThisTypeName = accessorImplicitThisTypeName + implicitReceiverTypeNames = listOfNotNull(accessorImplicitThisTypeName) ) ) { parseExpressionBlockWithPredeclared(listOf(setArgName to true)) @@ -11152,7 +11283,7 @@ class Compiler( CodeContext.Function( "", implicitThisMembers = accessorImplicitThisMembers, - implicitThisTypeName = accessorImplicitThisTypeName + implicitReceiverTypeNames = listOfNotNull(accessorImplicitThisTypeName) ) ) { parseBlockWithPredeclared(listOf(setArg.value to true)) @@ -11166,7 +11297,7 @@ class Compiler( CodeContext.Function( "", implicitThisMembers = accessorImplicitThisMembers, - implicitThisTypeName = accessorImplicitThisTypeName + implicitReceiverTypeNames = listOfNotNull(accessorImplicitThisTypeName) ) ) { parseExpressionBlockWithPredeclared(listOf(setArg.value to true)) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/TypeDecl.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/TypeDecl.kt index 5edd68c..34eb2a0 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/TypeDecl.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/TypeDecl.kt @@ -26,6 +26,7 @@ sealed class TypeDecl(val isNullable:Boolean = false) { // ?? data class Function( val receiver: TypeDecl?, + val contextReceivers: List = emptyList(), val params: List, val returnType: TypeDecl, val nullable: Boolean = false diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt index 1818eb1..34202d8 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt @@ -36,6 +36,7 @@ data class TypeNameDoc(val segments: List, override val nullable: Boolea data class TypeGenericDoc(val base: TypeNameDoc, val args: List, override val nullable: Boolean = false) : TypeDoc data class TypeFunctionDoc( val receiver: TypeDoc? = null, + val contextReceivers: List = emptyList(), val params: List, val returns: TypeDoc, override val nullable: Boolean = false @@ -45,8 +46,13 @@ data class TypeVarDoc(val name: String, override val nullable: Boolean = false) // Convenience builders fun type(name: String, nullable: Boolean = false) = TypeNameDoc(name.split('.'), nullable) fun typeVar(name: String, nullable: Boolean = false) = TypeVarDoc(name, nullable) -fun funType(params: List, returns: TypeDoc, receiver: TypeDoc? = null, nullable: Boolean = false) = - TypeFunctionDoc(receiver, params, returns, nullable) +fun funType( + params: List, + returns: TypeDoc, + receiver: TypeDoc? = null, + contextReceivers: List = emptyList(), + nullable: Boolean = false +) = TypeFunctionDoc(receiver, contextReceivers, params, returns, nullable) // ---------------- Registry ---------------- @@ -281,6 +287,7 @@ internal fun TypeDoc.toMiniTypeRef(): MiniTypeRef = when (this) { is TypeFunctionDoc -> MiniFunctionType( range = builtinRange(), receiver = this.receiver?.toMiniTypeRef(), + contextReceivers = this.contextReceivers.map { it.toMiniTypeRef() }, params = this.params.map { it.toMiniTypeRef() }, returnType = this.returns.toMiniTypeRef(), nullable = this.nullable diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/MiniAst.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/MiniAst.kt index 6757ae7..093ce60 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/MiniAst.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/MiniAst.kt @@ -139,6 +139,7 @@ data class MiniGenericType( data class MiniFunctionType( override val range: MiniRange, val receiver: MiniTypeRef?, + val contextReceivers: List, val params: List, val returnType: MiniTypeRef, val nullable: Boolean diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjTypeExpr.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjTypeExpr.kt index 89d063e..f2ebe70 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjTypeExpr.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjTypeExpr.kt @@ -206,7 +206,17 @@ private fun typeDeclKey(type: TypeDecl): String = when (type) { TypeDecl.TypeNullableAny -> "Any?" is TypeDecl.Simple -> "S:${type.name}" is TypeDecl.Generic -> "G:${type.name}<${type.args.joinToString(",") { typeDeclKey(it) }}>" - is TypeDecl.Function -> "F:(${type.params.joinToString(",") { typeDeclKey(it) }})->${typeDeclKey(type.returnType)}" + is TypeDecl.Function -> buildString { + append("F:") + type.receiver?.let { append("recv=").append(typeDeclKey(it)).append(";") } + if (type.contextReceivers.isNotEmpty()) { + append("ctx=").append(type.contextReceivers.joinToString(",") { typeDeclKey(it) }).append(";") + } + append('(') + append(type.params.joinToString(",") { typeDeclKey(it) }) + append(")->") + append(typeDeclKey(type.returnType)) + } is TypeDecl.Ellipsis -> "E:${typeDeclKey(type.elementType)}" is TypeDecl.TypeVar -> "V:${type.name}" is TypeDecl.Union -> "U:${type.options.joinToString("|") { typeDeclKey(it) }}" diff --git a/lynglib/src/commonTest/kotlin/MiniAstTest.kt b/lynglib/src/commonTest/kotlin/MiniAstTest.kt index d8774d1..6364386 100644 --- a/lynglib/src/commonTest/kotlin/MiniAstTest.kt +++ b/lynglib/src/commonTest/kotlin/MiniAstTest.kt @@ -473,6 +473,29 @@ class MiniAstTest { assertEquals("b", fn.params[1].name) } + @Test + fun miniAst_captures_context_receiver_function_type() = runTest { + val code = """ + val block: context(Html, Head) Body.()->String = { "ok" } + """.trimIndent() + val (_, sink) = compileWithMini(code) + val mini = sink.build() + assertNotNull(mini) + val vd = mini.declarations.filterIsInstance().firstOrNull { it.name == "block" } + assertNotNull(vd) + val type = vd.type as MiniFunctionType + val receiver = type.receiver as MiniTypeName + assertEquals(listOf("Body"), receiver.segments.map { it.name }) + assertEquals(2, type.contextReceivers.size) + val ctx0 = type.contextReceivers[0] as MiniTypeName + val ctx1 = type.contextReceivers[1] as MiniTypeName + assertEquals(listOf("Html"), ctx0.segments.map { it.name }) + assertEquals(listOf("Head"), ctx1.segments.map { it.name }) + assertTrue(type.params.isEmpty()) + val ret = type.returnType as MiniTypeName + assertEquals(listOf("String"), ret.segments.map { it.name }) + } + @Test fun miniAst_captures_dokka_tags() = runTest { val code = """ diff --git a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/OptTest.kt b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/OptTest.kt index 1cee9d1..cc8cf9c 100644 --- a/lynglib/src/commonTest/kotlin/net/sergeych/lyng/OptTest.kt +++ b/lynglib/src/commonTest/kotlin/net/sergeych/lyng/OptTest.kt @@ -154,4 +154,132 @@ class OptTest { assert( cnt == 4 ) """.trimIndent()) } + + @Test + fun testReceivers1() = runTest { + eval(""" + class RA { + fun a() { println("a") } + } + class RB { + fun b() { println("b") } + } + + fun ta( f: RA.()->Unit ) { + val instance = RA() + with(instance) { f(this) } + } + fun tb( f: RB.()->Unit ) { + val b = RB() + with(b) { f(this) } + } + ta { + a() + tb { + b() + // but important: a() must still be accessible + // because it is inner block, sort of closure: + a() + } + } + """.trimIndent()) + } + + @Test + fun testContextReceiverFunctionType() = runTest { + eval(""" + class RA { + fun value(): Int = 10 + } + class RB { + fun value(): Int = 20 + } + + fun ta(f: RA.()->Int): Int { + val instance = RA() + return with(instance) { f(this) } + } + + fun tb(f: context(RA) RB.()->Int): Int { + val instance = RB() + return with(instance) { f(this) } + } + + val result = ta { + val block: context(RA) RB.()->Int = { + value() + this@RA.value() + } + tb(block) + } + + assertEquals(30, result) + """.trimIndent()) + } + + @Test + fun testNestedReceiverQualifiedThis() = runTest { + eval(""" + class RA { + fun value(): Int = 1 + } + class RB { + fun value(): Int = 2 + } + + fun ta(f: RA.()->Int): Int { + val instance = RA() + return with(instance) { f(this) } + } + fun tb(f: RB.()->Int): Int { + val instance = RB() + return with(instance) { f(this) } + } + + val result = ta { + tb { + value() + this@RA.value() + } + } + + assertEquals(3, result) + """.trimIndent()) + } + + @Test + fun testReceiverAmbiguityRequiresQualifiedThis() = runTest { + val ex = assertFailsWith { + eval(""" + class RA { + fun shared(): Int = 10 + } + class RC { + fun shared(): Int = 30 + } + class RB + + fun ta(f: RA.()->Int): Int { + val instance = RA() + return with(instance) { f(this) } + } + fun tc(f: RC.()->Int): Int { + val instance = RC() + return with(instance) { f(this) } + } + fun tb(f: context(RA, RC) RB.()->Int): Int { + val instance = RB() + return with(instance) { f(this) } + } + + ta { + tc { + val block: context(RA, RC) RB.()->Int = { + shared() + } + tb(block) + } + } + """.trimIndent()) + } + assertContains(ex.message ?: "", "ambiguous between receivers RA, RC") + } }