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.
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

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.
- **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.

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).
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.

View File

@ -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

View File

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

View File

@ -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 {
}
}

View File

@ -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

View File

@ -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

View File

@ -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<ScriptError> {
eval(
"""
extern class HostClass {
fun doSomething(): Int
}
""".trimIndent()
)
}
}
@Test
fun testExternExtension() = runTest {
eval(

View File

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

View File

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

View File

@ -276,6 +276,7 @@ Map inference:
- `{ "a": 1, "b": "x" }` is `Map<String,Int|String>`.
- 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` / `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).