Require explicit extern members in extern class/object and update docs

This commit is contained in:
Sergey Chernov 2026-03-15 03:27:50 +03:00
parent e447c778ed
commit 979e6ea9b7
13 changed files with 105 additions and 53 deletions

View File

@ -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. 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
// Lyng side (in a module) // Lyng side (in a module)
class Counter { 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`. 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
// Kotlin side (binding) // Kotlin side (binding)
val moduleScope = Script.newScope() // or an existing module scope val moduleScope = Script.newScope() // or an existing module scope

View File

@ -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. - **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. - **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. See **Embedding Lyng** for full samples and usage details.

View File

@ -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). 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 ### 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. - **Deep inference**: The compiler analyzes types of symbols along the execution path and in many cases eliminates unnecessary casts or type specifications.

View File

@ -7727,6 +7727,7 @@ class Compiler(
val annotation = lastAnnotation val annotation = lastAnnotation
val parentContext = codeContexts.last() val parentContext = codeContexts.last()
val parentClassCtx = parentContext as? CodeContext.ClassBody
// Is extension? // Is extension?
if (looksLikeExtensionReceiver()) { if (looksLikeExtensionReceiver()) {
@ -7760,6 +7761,9 @@ class Compiler(
name = t.value name = t.value
nameStartPos = t.pos 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 extensionWrapperName = extTypeName?.let { extensionCallableName(it, name) }
val classCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody val classCtx = codeContexts.asReversed().firstOrNull { it is CodeContext.ClassBody } as? CodeContext.ClassBody
var memberMethodId = if (extTypeName == null) classCtx?.memberMethodIds?.get(name) else null var memberMethodId = if (extTypeName == null) classCtx?.memberMethodIds?.get(name) else null
@ -8782,6 +8786,10 @@ class Compiler(
name = nameToken.value name = nameToken.value
nameStartPos = nameToken.pos 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 receiverNormalization = normalizeReceiverTypeDecl(receiverTypeDecl, emptySet())
val implicitTypeParams = receiverNormalization.second val implicitTypeParams = receiverNormalization.second

View File

@ -24,46 +24,46 @@ package lyng.observable
extern class ChangeRejectionException : Exception extern class ChangeRejectionException : Exception
extern class Subscription { extern class Subscription {
fun cancel(): Void extern fun cancel(): Void
} }
extern class Observable<Change> { extern class Observable<Change> {
fun beforeChange(listener: (Change)->Void): Subscription extern fun beforeChange(listener: (Change)->Void): Subscription
fun onChange(listener: (Change)->Void): Subscription extern fun onChange(listener: (Change)->Void): Subscription
fun changes(): Flow<Change> extern fun changes(): Flow<Change>
} }
extern class ListChange<T> extern class ListChange<T>
extern class ListSet<T> : ListChange<T> { extern class ListSet<T> : ListChange<T> {
val index: Int extern val index: Int
val oldValue: Object extern val oldValue: Object
val newValue: Object extern val newValue: Object
} }
extern class ListInsert<T> : ListChange<T> { extern class ListInsert<T> : ListChange<T> {
val index: Int extern val index: Int
val values: List<T> extern val values: List<T>
} }
extern class ListRemove<T> : ListChange<T> { extern class ListRemove<T> : ListChange<T> {
val index: Int extern val index: Int
val oldValue: Object extern val oldValue: Object
} }
extern class ListClear<T> : ListChange<T> { extern class ListClear<T> : ListChange<T> {
val oldValues: List<T> extern val oldValues: List<T>
} }
extern class ListReorder<T> : ListChange<T> { extern class ListReorder<T> : ListChange<T> {
val oldValues: List<T> extern val oldValues: List<T>
val newValues: Object extern val newValues: Object
} }
extern class ObservableList<T> : List<T> { extern class ObservableList<T> : List<T> {
fun beforeChange(listener: (ListChange<T>)->Void): Subscription extern fun beforeChange(listener: (ListChange<T>)->Void): Subscription
fun onChange(listener: (ListChange<T>)->Void): Subscription extern fun onChange(listener: (ListChange<T>)->Void): Subscription
fun changes(): Flow<ListChange<T>> extern fun changes(): Flow<ListChange<T>>
} }
fun List<T>.observable(): ObservableList<T> { fun List<T>.observable(): ObservableList<T> {

View File

@ -164,7 +164,7 @@ class BindingTest {
val ms = Script.newScope() val ms = Script.newScope()
ms.eval(""" ms.eval("""
extern class A { extern class A {
fun get1(): String extern fun get1(): String
} }
extern fun getA(): A extern fun getA(): A
@ -184,4 +184,3 @@ class BindingTest {
} }
} }

View File

@ -231,7 +231,7 @@ class BridgeBindingTest {
val ms = Script.newScope() val ms = Script.newScope()
ms.eval(""" ms.eval("""
extern class A { extern class A {
val field: Int extern val field: Int
} }
fun test(a: A) = a.field fun test(a: A) = a.field

View File

@ -249,13 +249,13 @@ class MiniAstTest {
// Doc2 // Doc2
extern class C1 { extern class C1 {
// Doc3 // Doc3
fun m1() extern fun m1()
} }
// Doc4 // Doc4
extern object O1 { extern object O1 {
// Doc5 // Doc5
val v1: String extern val v1: String
} }
// Doc6 // Doc6

View File

@ -3080,11 +3080,11 @@ class ScriptTest {
""" """
extern fun hostFunction(a: Int, b: String): String extern fun hostFunction(a: Int, b: String): String
extern class HostClass(name: String) { extern class HostClass(name: String) {
fun doSomething(): Int extern fun doSomething(): Int
val status: String extern val status: String
} }
extern object HostObject { extern object HostObject {
fun getInstance(): HostClass extern fun getInstance(): HostClass
} }
extern enum HostEnum { extern enum HostEnum {
VALUE1, VALUE2 VALUE1, VALUE2
@ -3097,6 +3097,19 @@ class ScriptTest {
) )
} }
@Test
fun testExternClassMembersMustBeExplicitlyExtern() = runTest {
assertFailsWith<ScriptError> {
eval(
"""
extern class HostClass {
fun doSomething(): Int
}
""".trimIndent()
)
}
}
@Test @Test
fun testExternExtension() = runTest { fun testExternExtension() = runTest {
eval( eval(

View File

@ -209,7 +209,7 @@ class CompletionEngineLightTest {
fun inferredTypeFromMemberCall() = runBlocking { fun inferredTypeFromMemberCall() = runBlocking {
val code = """ val code = """
extern class MyClass { extern class MyClass {
fun getList(): List<String> extern fun getList(): List<String>
} }
extern val c: MyClass extern val c: MyClass
val x = c.getList() val x = c.getList()

View File

@ -8,22 +8,22 @@ extern class IllegalArgumentException
extern class NotImplementedException extern class NotImplementedException
extern class Delegate extern class Delegate
extern class Iterable<T> { extern class Iterable<T> {
fun iterator(): Iterator<T> extern fun iterator(): Iterator<T>
fun forEach(action: (T)->void): void extern fun forEach(action: (T)->void): void
fun map<R>(transform: (T)->R): List<R> extern fun map<R>(transform: (T)->R): List<R>
fun toList(): List<T> extern fun toList(): List<T>
fun toImmutableList(): ImmutableList<T> extern fun toImmutableList(): ImmutableList<T>
val toSet: Set<T> extern val toSet: Set<T>
val toImmutableSet: ImmutableSet<T> extern val toImmutableSet: ImmutableSet<T>
val toMap: Map<Object,Object> extern val toMap: Map<Object,Object>
val toImmutableMap: ImmutableMap<Object,Object> extern val toImmutableMap: ImmutableMap<Object,Object>
} }
extern class Iterator<T> { extern class Iterator<T> {
fun hasNext(): Bool extern fun hasNext(): Bool
fun next(): T extern fun next(): T
fun cancelIteration(): void extern fun cancelIteration(): void
fun toList(): List<T> extern fun toList(): List<T>
} }
// Host-provided iterator wrapper for Kotlin collections. // Host-provided iterator wrapper for Kotlin collections.
@ -33,47 +33,47 @@ class KotlinIterator<T> : Iterator<T> {
} }
extern class Collection<T> : Iterable<T> { extern class Collection<T> : Iterable<T> {
val size: Int extern val size: Int
} }
extern class Array<T> : Collection<T> { extern class Array<T> : Collection<T> {
} }
extern class ImmutableList<T> : Array<T> { extern class ImmutableList<T> : Array<T> {
fun toMutable(): List<T> extern fun toMutable(): List<T>
} }
extern class List<T> : Array<T> { extern class List<T> : Array<T> {
fun add(value: T, more...): void extern fun add(value: T, more...): void
fun toImmutable(): ImmutableList<T> extern fun toImmutable(): ImmutableList<T>
} }
extern class RingBuffer<T> : Iterable<T> { extern class RingBuffer<T> : Iterable<T> {
val size: Int extern val size: Int
fun first(): T extern fun first(): T
fun add(value: T): void extern fun add(value: T): void
} }
extern class Set<T> : Collection<T> { extern class Set<T> : Collection<T> {
fun toImmutable(): ImmutableSet<T> extern fun toImmutable(): ImmutableSet<T>
} }
extern class ImmutableSet<T> : Collection<T> { extern class ImmutableSet<T> : Collection<T> {
fun toMutable(): Set<T> extern fun toMutable(): Set<T>
} }
extern class Map<K,V> : Collection<MapEntry<K,V>> { extern class Map<K,V> : Collection<MapEntry<K,V>> {
fun toImmutable(): ImmutableMap<K,V> extern fun toImmutable(): ImmutableMap<K,V>
} }
extern class ImmutableMap<K,V> : Collection<MapEntry<K,V>> { extern class ImmutableMap<K,V> : Collection<MapEntry<K,V>> {
fun getOrNull(key: K): V? extern fun getOrNull(key: K): V?
fun toMutable(): Map<K,V> extern fun toMutable(): Map<K,V>
} }
extern class MapEntry<K,V> : Array<Object> { extern class MapEntry<K,V> : Array<Object> {
val key: K extern val key: K
val value: V extern val value: V
} }
// Built-in math helpers (implemented in host runtime). // Built-in math helpers (implemented in host runtime).

View File

@ -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: - Kotlin can store two opaque payloads:
- `instance.data` (per instance) - `instance.data` (per instance)
- `classData` (per class) - `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 ## 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`. - 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. - `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`. - 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() = ...`.

View File

@ -276,6 +276,7 @@ Map inference:
- `{ "a": 1, "b": "x" }` is `Map<String,Int|String>`. - `{ "a": 1, "b": "x" }` is `Map<String,Int|String>`.
- Empty map literal uses `{:}` (since `{}` is empty callable). - Empty map literal uses `{:}` (since `{}` is empty callable).
- `extern class Map<K=String,V=Object>` so `Map()` is `Map<String,Object>()` unless contextual type overrides. - `extern class Map<K=String,V=Object>` so `Map()` is `Map<String,Object>()` unless contextual type overrides.
- `extern class` / `extern object` are declaration-only; members in their bodies must be explicitly marked `extern`.
Flow typing: Flow typing:
- Compiler should narrow types based on control-flow (e.g., `if (x != null)` narrows `x` to non-null inside the branch). - Compiler should narrow types based on control-flow (e.g., `if (x != null)` narrows `x` to non-null inside the branch).