From 979e6ea9b755417c40eb30212697dd6826d1a3c7 Mon Sep 17 00:00:00 2001 From: sergeych Date: Sun, 15 Mar 2026 03:27:50 +0300 Subject: [PATCH] Require explicit extern members in extern class/object and update docs --- docs/embedding.md | 21 ++++++++ docs/whats_new.md | 1 + docs/whats_new_1_5.md | 5 ++ .../kotlin/net/sergeych/lyng/Compiler.kt | 8 +++ .../lyng/stdlib_included/observable_lyng.kt | 34 ++++++------ lynglib/src/commonTest/kotlin/BindingTest.kt | 3 +- .../commonTest/kotlin/BridgeBindingTest.kt | 2 +- lynglib/src/commonTest/kotlin/MiniAstTest.kt | 4 +- lynglib/src/commonTest/kotlin/ScriptTest.kt | 19 +++++-- .../lyng/miniast/CompletionEngineLightTest.kt | 2 +- lynglib/stdlib/lyng/root.lyng | 54 +++++++++---------- notes/kotlin_bridge_binding.md | 4 ++ notes/new_lyng_type_system_spec.md | 1 + 13 files changed, 105 insertions(+), 53 deletions(-) diff --git a/docs/embedding.md b/docs/embedding.md index 3ff3685..119d89d 100644 --- a/docs/embedding.md +++ b/docs/embedding.md @@ -243,6 +243,12 @@ For extensions and libraries, the **preferred** workflow is Lyng‑first: declar This keeps Lyng semantics (visibility, overrides, type checks) in Lyng, while Kotlin supplies the behavior. +Pure extern declarations use the simplified rule set: +- `extern class` / `extern object` are declaration-only ABI surfaces. +- Every member in their body must be explicitly marked `extern`. +- Plain Lyng member implementations inside `extern class` / `extern object` are not allowed. +- Put Lyng behavior into regular classes or extension methods. + ```lyng // Lyng side (in a module) class Counter { @@ -253,6 +259,21 @@ class Counter { Note: members must be marked `extern` so the compiler emits the ABI slots that Kotlin bindings attach to. This applies to functions and properties bound via `addFun` / `addVal` / `addVar`. +Example of pure extern class declaration: + +```lyng +extern class HostCounter { + extern var value: Int + extern fun inc(by: Int): Int +} +``` + +If you need Lyng-side convenience behavior, add it as an extension: + +```lyng +fun HostCounter.bump() = inc(1) +``` + ```kotlin // Kotlin side (binding) val moduleScope = Script.newScope() // or an existing module scope diff --git a/docs/whats_new.md b/docs/whats_new.md index 17d3f81..0608495 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -249,5 +249,6 @@ Lyng now provides a public Kotlin reflection bridge and a Lyng‑first class bin - **Bridge resolver**: explicit handles for values, vars, and callables with predictable lookup rules. - **Class bridge binding**: declare classes/members in Lyng (marked `extern`) and bind the implementations in Kotlin before the first instance is created. +- **Extern declaration rule**: `extern class` / `extern object` are declaration-only; all members in their bodies must be explicitly marked `extern`. See **Embedding Lyng** for full samples and usage details. diff --git a/docs/whats_new_1_5.md b/docs/whats_new_1_5.md index 0e7abb1..27935e0 100644 --- a/docs/whats_new_1_5.md +++ b/docs/whats_new_1_5.md @@ -18,6 +18,11 @@ In particular, it means no slow and flaky runtime lookups. Once compiled, code g The API is fixed and will be kept with further Lyng core changes. It is now the recommended way to write Lyng extensions in Kotlin. It is much simpler and more elegant than the internal one. See [Kotlin Bridge Binding](../notes/kotlin_bridge_binding.md). +Extern declaration clarification: +- `extern class` / `extern object` are pure extern surfaces. +- Members inside them must be explicitly marked `extern`. +- Lyng method/property bodies for these declarations should be implemented as extensions instead. + ### Smart types system - **Deep inference**: The compiler analyzes types of symbols along the execution path and in many cases eliminates unnecessary casts or type specifications. diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index e85d1ab..d8a116a 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -7727,6 +7727,7 @@ class Compiler( val annotation = lastAnnotation val parentContext = codeContexts.last() + val parentClassCtx = parentContext as? CodeContext.ClassBody // Is extension? if (looksLikeExtensionReceiver()) { @@ -7760,6 +7761,9 @@ class Compiler( name = t.value nameStartPos = t.pos } + if (parentClassCtx?.isExtern == true && !isExtern) { + throw ScriptError(nameStartPos, "members of extern classes/objects must be marked extern") + } 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 @@ -8782,6 +8786,10 @@ class Compiler( name = nameToken.value nameStartPos = nameToken.pos } + val parentClassCtx = codeContexts.lastOrNull() as? CodeContext.ClassBody + if (parentClassCtx?.isExtern == true && !isExtern) { + throw ScriptError(nameStartPos, "members of extern classes/objects must be marked extern") + } val receiverNormalization = normalizeReceiverTypeDecl(receiverTypeDecl, emptySet()) val implicitTypeParams = receiverNormalization.second diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/stdlib_included/observable_lyng.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/stdlib_included/observable_lyng.kt index 03776b7..cb84c23 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/stdlib_included/observable_lyng.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/stdlib_included/observable_lyng.kt @@ -24,46 +24,46 @@ package lyng.observable extern class ChangeRejectionException : Exception extern class Subscription { - fun cancel(): Void + extern fun cancel(): Void } extern class Observable { - fun beforeChange(listener: (Change)->Void): Subscription - fun onChange(listener: (Change)->Void): Subscription - fun changes(): Flow + extern fun beforeChange(listener: (Change)->Void): Subscription + extern fun onChange(listener: (Change)->Void): Subscription + extern fun changes(): Flow } extern class ListChange extern class ListSet : ListChange { - val index: Int - val oldValue: Object - val newValue: Object + extern val index: Int + extern val oldValue: Object + extern val newValue: Object } extern class ListInsert : ListChange { - val index: Int - val values: List + extern val index: Int + extern val values: List } extern class ListRemove : ListChange { - val index: Int - val oldValue: Object + extern val index: Int + extern val oldValue: Object } extern class ListClear : ListChange { - val oldValues: List + extern val oldValues: List } extern class ListReorder : ListChange { - val oldValues: List - val newValues: Object + extern val oldValues: List + extern val newValues: Object } extern class ObservableList : List { - fun beforeChange(listener: (ListChange)->Void): Subscription - fun onChange(listener: (ListChange)->Void): Subscription - fun changes(): Flow> + extern fun beforeChange(listener: (ListChange)->Void): Subscription + extern fun onChange(listener: (ListChange)->Void): Subscription + extern fun changes(): Flow> } fun List.observable(): ObservableList { diff --git a/lynglib/src/commonTest/kotlin/BindingTest.kt b/lynglib/src/commonTest/kotlin/BindingTest.kt index 81e3f6d..79a7de7 100644 --- a/lynglib/src/commonTest/kotlin/BindingTest.kt +++ b/lynglib/src/commonTest/kotlin/BindingTest.kt @@ -164,7 +164,7 @@ class BindingTest { val ms = Script.newScope() ms.eval(""" extern class A { - fun get1(): String + extern fun get1(): String } extern fun getA(): A @@ -184,4 +184,3 @@ class BindingTest { } } - diff --git a/lynglib/src/commonTest/kotlin/BridgeBindingTest.kt b/lynglib/src/commonTest/kotlin/BridgeBindingTest.kt index 660eb1f..126c271 100644 --- a/lynglib/src/commonTest/kotlin/BridgeBindingTest.kt +++ b/lynglib/src/commonTest/kotlin/BridgeBindingTest.kt @@ -231,7 +231,7 @@ class BridgeBindingTest { val ms = Script.newScope() ms.eval(""" extern class A { - val field: Int + extern val field: Int } fun test(a: A) = a.field diff --git a/lynglib/src/commonTest/kotlin/MiniAstTest.kt b/lynglib/src/commonTest/kotlin/MiniAstTest.kt index 29c6dd6..d8774d1 100644 --- a/lynglib/src/commonTest/kotlin/MiniAstTest.kt +++ b/lynglib/src/commonTest/kotlin/MiniAstTest.kt @@ -249,13 +249,13 @@ class MiniAstTest { // Doc2 extern class C1 { // Doc3 - fun m1() + extern fun m1() } // Doc4 extern object O1 { // Doc5 - val v1: String + extern val v1: String } // Doc6 diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index c1b5ab6..a82950a 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -3080,11 +3080,11 @@ class ScriptTest { """ extern fun hostFunction(a: Int, b: String): String extern class HostClass(name: String) { - fun doSomething(): Int - val status: String + extern fun doSomething(): Int + extern val status: String } extern object HostObject { - fun getInstance(): HostClass + extern fun getInstance(): HostClass } extern enum HostEnum { VALUE1, VALUE2 @@ -3097,6 +3097,19 @@ class ScriptTest { ) } + @Test + fun testExternClassMembersMustBeExplicitlyExtern() = runTest { + assertFailsWith { + eval( + """ + extern class HostClass { + fun doSomething(): Int + } + """.trimIndent() + ) + } + } + @Test fun testExternExtension() = runTest { eval( diff --git a/lynglib/src/jvmTest/kotlin/net/sergeych/lyng/miniast/CompletionEngineLightTest.kt b/lynglib/src/jvmTest/kotlin/net/sergeych/lyng/miniast/CompletionEngineLightTest.kt index 2d2b84d..4ec8965 100644 --- a/lynglib/src/jvmTest/kotlin/net/sergeych/lyng/miniast/CompletionEngineLightTest.kt +++ b/lynglib/src/jvmTest/kotlin/net/sergeych/lyng/miniast/CompletionEngineLightTest.kt @@ -209,7 +209,7 @@ class CompletionEngineLightTest { fun inferredTypeFromMemberCall() = runBlocking { val code = """ extern class MyClass { - fun getList(): List + extern fun getList(): List } extern val c: MyClass val x = c.getList() diff --git a/lynglib/stdlib/lyng/root.lyng b/lynglib/stdlib/lyng/root.lyng index 9c5ce87..16b6c51 100644 --- a/lynglib/stdlib/lyng/root.lyng +++ b/lynglib/stdlib/lyng/root.lyng @@ -8,22 +8,22 @@ extern class IllegalArgumentException extern class NotImplementedException extern class Delegate extern class Iterable { - fun iterator(): Iterator - fun forEach(action: (T)->void): void - fun map(transform: (T)->R): List - fun toList(): List - fun toImmutableList(): ImmutableList - val toSet: Set - val toImmutableSet: ImmutableSet - val toMap: Map - val toImmutableMap: ImmutableMap + extern fun iterator(): Iterator + extern fun forEach(action: (T)->void): void + extern fun map(transform: (T)->R): List + extern fun toList(): List + extern fun toImmutableList(): ImmutableList + extern val toSet: Set + extern val toImmutableSet: ImmutableSet + extern val toMap: Map + extern val toImmutableMap: ImmutableMap } extern class Iterator { - fun hasNext(): Bool - fun next(): T - fun cancelIteration(): void - fun toList(): List + extern fun hasNext(): Bool + extern fun next(): T + extern fun cancelIteration(): void + extern fun toList(): List } // Host-provided iterator wrapper for Kotlin collections. @@ -33,47 +33,47 @@ class KotlinIterator : Iterator { } extern class Collection : Iterable { - val size: Int + extern val size: Int } extern class Array : Collection { } extern class ImmutableList : Array { - fun toMutable(): List + extern fun toMutable(): List } extern class List : Array { - fun add(value: T, more...): void - fun toImmutable(): ImmutableList + extern fun add(value: T, more...): void + extern fun toImmutable(): ImmutableList } extern class RingBuffer : Iterable { - val size: Int - fun first(): T - fun add(value: T): void + extern val size: Int + extern fun first(): T + extern fun add(value: T): void } extern class Set : Collection { - fun toImmutable(): ImmutableSet + extern fun toImmutable(): ImmutableSet } extern class ImmutableSet : Collection { - fun toMutable(): Set + extern fun toMutable(): Set } extern class Map : Collection> { - fun toImmutable(): ImmutableMap + extern fun toImmutable(): ImmutableMap } extern class ImmutableMap : Collection> { - fun getOrNull(key: K): V? - fun toMutable(): Map + extern fun getOrNull(key: K): V? + extern fun toMutable(): Map } extern class MapEntry : Array { - val key: K - val value: V + extern val key: K + extern val value: V } // Built-in math helpers (implemented in host runtime). diff --git a/notes/kotlin_bridge_binding.md b/notes/kotlin_bridge_binding.md index 0c0c695..5973c92 100644 --- a/notes/kotlin_bridge_binding.md +++ b/notes/kotlin_bridge_binding.md @@ -12,6 +12,9 @@ This note describes the Lyng-first workflow where a class is declared in Lyng an - Kotlin can store two opaque payloads: - `instance.data` (per instance) - `classData` (per class) +- `extern class` / `extern object` are pure extern surfaces: + - all members in their bodies must be explicitly `extern`; + - Lyng member bodies inside extern classes/objects are not supported. ## Lyng: declare extern members @@ -70,3 +73,4 @@ LyngClassBridge.bind(className = "Foo", module = "bridge.mod", importManager = i - Use `init { ... }` / `initWithInstance { ... }` with `ScopeFacade` receiver; access instance via `thisObj`. - `classData` and `instance.data` are Kotlin-only payloads and do not appear in Lyng reflection. - Binding after the first instance of a class is created throws a `ScriptError`. +- If you need Lyng-side helpers for an extern type, add them as extensions, e.g. `fun Foo.helper() = ...`. diff --git a/notes/new_lyng_type_system_spec.md b/notes/new_lyng_type_system_spec.md index 277abdb..e417e6f 100644 --- a/notes/new_lyng_type_system_spec.md +++ b/notes/new_lyng_type_system_spec.md @@ -276,6 +276,7 @@ Map inference: - `{ "a": 1, "b": "x" }` is `Map`. - Empty map literal uses `{:}` (since `{}` is empty callable). - `extern class Map` so `Map()` is `Map()` unless contextual type overrides. +- `extern class` / `extern object` are declaration-only; members in their bodies must be explicitly marked `extern`. Flow typing: - Compiler should narrow types based on control-flow (e.g., `if (x != null)` narrows `x` to non-null inside the branch).