diff --git a/docs/whats_new.md b/docs/whats_new.md index e56548f..cac1ca5 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -9,6 +9,7 @@ For a programmer-focused migration summary across 1.5.x, see `docs/whats_new_1_5 - `1.5.4` is the stabilization release for the 1.5 feature set. - The 1.5 line now brings together richer ranges and loops, interpolation, math modules, immutable and observable collections, richer `lyngio`, and much better CLI/IDE support. - `1.5.4` specifically fixes user-visible issues around decimal arithmetic, mixed numeric flows, list behavior, and observable list hooks. +- `1.5.4` also fixes extension-member registration for named singleton `object` declarations, so `fun X.foo()` and `val X.bar` now work as expected. - The docs, homepage samples, and release metadata now point at the current stable version. ## User Highlights Across 1.5.x @@ -303,6 +304,23 @@ object Config { Config.show() ``` +Named singleton objects can also be used as extension receivers: + +```lyng +object X { + fun base() = "base" +} + +fun X.decorate(value): String { + this.base() + ":" + value.toString() +} + +val X.tag get() = this.base() + ":tag" + +assertEquals("base:42", X.decorate(42)) +assertEquals("base:tag", X.tag) +``` + ### Nested Declarations and Lifted Enums You can now declare classes, objects, enums, and type aliases inside another class. These nested declarations live in the class namespace (no outer instance capture) and are accessed with a qualifier. diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/FunctionDeclStatement.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/FunctionDeclStatement.kt index 5a6366e..804c42b 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/FunctionDeclStatement.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/FunctionDeclStatement.kt @@ -85,8 +85,7 @@ internal suspend fun executeFunctionDecl( } if (spec.extTypeName != null) { - val type = scope[spec.extTypeName]?.value ?: scope.raiseSymbolNotFound("class ${spec.extTypeName} not found") - if (type !is ObjClass) scope.raiseClassCastError("${spec.extTypeName} is not the class instance") + val type = scope.resolveExtensionReceiverClass(spec.extTypeName) scope.addExtension( type, spec.name, @@ -167,8 +166,7 @@ internal suspend fun executeFunctionDecl( val compiledFnBody = annotatedFnBody spec.extTypeName?.let { typeName -> - val type = scope[typeName]?.value ?: scope.raiseSymbolNotFound("class $typeName not found") - if (type !is ObjClass) scope.raiseClassCastError("$typeName is not the class instance") + val type = scope.resolveExtensionReceiverClass(typeName) if (spec.isStatic) { type.createClassField( spec.name, diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt index f1688ee..fd2719c 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt @@ -1004,4 +1004,13 @@ open class Scope( return rec.value as? net.sergeych.lyng.obj.ObjClass ?: raiseClassCastError("Expected class $name, got ${rec.value.objClass.className}") } + + internal fun resolveExtensionReceiverClass(name: String): net.sergeych.lyng.obj.ObjClass { + val value = get(name)?.value ?: raiseSymbolNotFound("class $name not found") + return when (value) { + is net.sergeych.lyng.obj.ObjClass -> value + is net.sergeych.lyng.obj.ObjInstance -> value.objClass + else -> raiseClassCastError("$name is not the class instance") + } + } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt index b29a49e..ee193ff 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt @@ -3094,11 +3094,7 @@ class CmdDeclExtProperty(internal val constId: Int, internal val slot: Int) : Cm override suspend fun perform(frame: CmdFrame) { val decl = frame.fn.constants[constId] as? BytecodeConst.ExtensionPropertyDecl ?: error("DECL_EXT_PROPERTY expects ExtensionPropertyDecl at $constId") - val type = frame.ensureScope()[decl.extTypeName]?.value - ?: frame.ensureScope().raiseSymbolNotFound("class ${decl.extTypeName} not found") - if (type !is ObjClass) { - frame.ensureScope().raiseClassCastError("${decl.extTypeName} is not the class instance") - } + val type = frame.ensureScope().resolveExtensionReceiverClass(decl.extTypeName) frame.ensureScope().addExtension( type, decl.property.name, diff --git a/lynglib/src/commonTest/kotlin/OOTest.kt b/lynglib/src/commonTest/kotlin/OOTest.kt index d845ce3..9c4a3d1 100644 --- a/lynglib/src/commonTest/kotlin/OOTest.kt +++ b/lynglib/src/commonTest/kotlin/OOTest.kt @@ -384,6 +384,32 @@ class OOTest { } } + @Test + fun testObjectSingletonSupportsExtensions() = runTest { + val scope = Script.newScope() + scope.eval( + """ + object X { + fun base() = "base" + } + + fun X.decorate(value): String { + this.base() + ":" + value.toString() + } + val X.tag get() = this.base() + ":tag" + + assertEquals("base", X.base()) + assertEquals("base:42", X.decorate(42)) + assertEquals("base:ok", X.decorate("ok")) + assertEquals("base:tag", X.tag) + + // Wrapper names should be generated for singleton-object receivers too. + assertEquals("base:17", __ext__X__decorate(X, 17)) + assertEquals("base:tag", __ext_get__X__tag(X)) + """.trimIndent() + ) + } + @Test fun testExtensionsAreScopeIsolated() = runTest { val scope1 = Script.newScope()