proposal: extending kotlin bridging ABI for generic externs

This commit is contained in:
Sergey Chernov 2026-03-15 04:22:36 +03:00
parent 2b13fe8053
commit 74eb8ff082
4 changed files with 184 additions and 49 deletions

View File

@ -21,3 +21,7 @@
- Create closure references only when a capture is detected; use a direct frame+slot reference (foreign slot ref) instead of scope slots.
- Keep Scope as a lazy reflection facade: resolve name -> slot only on demand for Kotlin interop (no eager name mapping on every call).
- Avoid PUSH_SCOPE/POP_SCOPE in bytecode for loops/functions unless dynamic name access or Kotlin reflection is requested.
## ABI proposal notes
- Runtime generic metadata for generic extern classes is tracked in `proposals/extern_generic_runtime_abi.md`.
- Keep this design `Obj`-centric: do not assume extern-class values are `ObjInstance`; collection must be enabled on `ObjClass`.

View File

@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych"
version = "1.5.0-SNAPSHOT"
version = "1.5.1-SNAPSHOT"
// Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below

View File

@ -0,0 +1,179 @@
# Proposal: Runtime Generic Metadata for Extern Classes (Obj-Centric ABI)
Status: Draft
Date: 2026-03-15
Owner: compiler/runtime/bridge
## Context
`extern class` declarations are currently allowed to be generic (e.g. `extern class Cell<T>`), and this is already used by stdlib (`Iterable<T>`, `List<T>`, `Map<K,V>`, etc.).
The open problem is exposing applied generic type arguments to Kotlin-side bindings at runtime.
Important constraint:
- Extern-class values are **not necessarily** `ObjInstance`.
- The only hard requirement is that values are `Obj`.
So the ABI design must be `Obj`-centric and must not assume any specific object implementation layout.
## Goals
- Keep `extern class<T>` allowed.
- Make runtime generic metadata collection explicit and opt-in.
- Support all `Obj` subclasses, including host/custom objects not backed by `ObjInstance`.
- Provide a stable Kotlin bridge API for reading applied type args.
- Keep default runtime overhead near zero for classes that do not need this feature.
## Non-goals (phase 1)
- Full reified method-generic metadata for every call site.
- Runtime enforcement of generic bounds from metadata alone.
- Backfilling metadata for pre-existing objects created without capture.
## Proposed ABI Shape
### 1) Opt-in flag on `ObjClass`
Add runtime policy to `ObjClass`:
```kotlin
enum class RuntimeGenericMode {
None,
CaptureClassTypeArgs
}
open class ObjClass(...) : Obj() {
open val runtimeGenericMode: RuntimeGenericMode = RuntimeGenericMode.None
}
```
Default remains `None`.
### 2) Per-object metadata storage owned by `ObjClass`
Store metadata keyed by `Obj` identity, not by instance internals:
```kotlin
open class ObjClass(...) : Obj() {
open fun setRuntimeTypeArgs(obj: Obj, args: List<RuntimeTypeRef>)
open fun getRuntimeTypeArgs(obj: Obj): List<RuntimeTypeRef>?
open fun clearRuntimeTypeArgs(obj: Obj) // optional
}
```
Default implementation:
- class-local identity map keyed by `Obj` references.
- no assumptions about object field layout.
- works for any `Obj` subtype.
Implementation note:
- JVM: weak identity map preferred to avoid leaks.
- JS/Wasm/Native: platform-appropriate identity map strategy; weak where available.
### 3) Type token format
Introduce compact runtime token:
```kotlin
sealed class RuntimeTypeRef {
data class Simple(val className: String, val nullable: Boolean = false) : RuntimeTypeRef()
data class Generic(val className: String, val args: List<RuntimeTypeRef>, val nullable: Boolean = false) : RuntimeTypeRef()
data class Union(val options: List<RuntimeTypeRef>, val nullable: Boolean = false) : RuntimeTypeRef()
data class Intersection(val options: List<RuntimeTypeRef>, val nullable: Boolean = false) : RuntimeTypeRef()
data class TypeVar(val name: String, val nullable: Boolean = false) : RuntimeTypeRef()
data class Unknown(val nullable: Boolean = false) : RuntimeTypeRef()
}
```
`Unknown` is required so arity is preserved even when inference cannot produce a concrete runtime type.
### 4) Compiler/runtime collection point
When constructing/obtaining a value of generic class `C<...>`:
- Resolve applied class type arguments (explicit or inferred).
- If target class has `runtimeGenericMode == CaptureClassTypeArgs`, call:
- `C.setRuntimeTypeArgs(resultObj, resolvedArgsAsRuntimeTypeRefs)`
- If mode is `None`, do nothing.
No assumption about concrete returned object type besides `Obj`.
### 5) Bridge accessor API
Add a stable helper on binding context/facade:
```kotlin
fun typeArgsOf(obj: Obj, asClass: ObjClass): List<RuntimeTypeRef>?
fun typeArgOf(obj: Obj, asClass: ObjClass, index: Int): RuntimeTypeRef?
```
Semantics:
- Reads metadata stored under `asClass`.
- Returns `null` when absent/not captured.
- Does not throw on non-`ObjInstance` objects.
This allows a host binding for `extern class Cell<T>` to inspect `T` safely.
## Source-Level Policy
No syntax change required in phase 1.
How to enable capture:
- Kotlin host sets `runtimeGenericMode` for classes that need it.
- Optionally add compiler directive later (future work), but not required for MVP.
## ABI Compatibility
- Backward compatible by default (`None` mode does not change behavior).
- Older runtimes can ignore metadata-related calls if feature not used.
- Optional capability flag can be introduced (`GENERIC_RUNTIME_ARGS`) for explicit runtime negotiation.
## Performance Considerations
- Zero overhead for classes with `None`.
- For capture-enabled classes: one map write per object creation/registration event.
- Memory cost proportional to number of live captured objects and type token size.
- Prefer weak-key maps where possible to minimize retention risk.
## Failure Modes and Handling
- If type args cannot be fully resolved at capture point:
- store `Unknown` tokens in unresolved positions.
- If an object is produced outside normal constructor flow:
- bridge can call `setRuntimeTypeArgs` manually if needed.
- If map strategy is unavailable on a platform:
- provide best-effort fallback map and document lifetime caveats.
## Phased Implementation Plan
### Phase 1 (MVP)
- Add `RuntimeGenericMode` on `ObjClass`.
- Add `RuntimeTypeRef`.
- Add `ObjClass` metadata storage/getters.
- Collect class-level generic args for opted-in extern classes.
- Add bridge getters and tests with non-`ObjInstance` `Obj` subtype.
### Phase 2
- Inheritance-aware views (querying args for parent extern classes if needed).
- Better token normalization/canonicalization.
### Phase 3
- Optional method-level generic call metadata (if a concrete use-case appears).
## Test Plan (minimum)
- `extern class Cell<T>` with capture enabled:
- `Cell<Int>()` exposes `Int`.
- inferred type args captured where available.
- Same class with capture disabled:
- no metadata returned.
- Non-`ObjInstance` host object path:
- metadata can be set/read via `ObjClass` API.
- Unknown/incomplete inference:
- returned args preserve arity via `Unknown`.
## Open Questions
- Exact cross-platform weak identity map abstraction location.
- Whether to add user-facing Lyng syntax for enabling capture, or keep host-only policy.
- Whether parent-generic projection should be materialized at capture time or computed lazily.

View File

@ -1,48 +0,0 @@
> under cosntruction
> note to AI: ignore this file for now
The problem is, sometimes we want to implement an interface enhancing existing classes. For example, we want ti implement Delegate for Map. We can add extension methods to Map that do the work, but we can add Delegate to the inheritance chain.
The problem is not trivial: while adding interfaces in other languages is easy, adding the while
class with a state to existing one should be done carefully.
Proposed syntax:
```lyng
extend Map with Delegate {
fun getValue(thisRef, key) = this[key]
fun setValue(thisRef, key, value) = this[key] = value
}
```
And now we can use Map as a Delegate:
```lyng
val map = { foo: 1. bar: 2 }
val foo by map
assertEquals(1, foo)
```
The syntax is similar to the one used for inheritance. But while Delegate has no state and it is actually simple. Much harder task is ti implement some class with state (trait):
```lyng
// the class we will use as a trait must have on constructor parameters
// or only parameters with default values
class MyTraitClass(initValue=100) {
private var field
fun traitField get() = field + initValue
set(value) { field = value }
}
extend Map with MyTraitClass
assertEquals(100, Map().traitField)
val m = Map()
m.traitField = 1000
assertEquals(1100,m.traitField)
```
We limit extension to module scope level, e.g., not in functions, not in classes, but at the "global level", probably ModuleScope.
The course of action could be:
- when constructing a class instance, compiler search in the ModuleScope extensions for it, and if found, add them to MI parent list to the end in the order of appearance in code (e.g. random ;)), them construct the instance as usual.