proposal: extending kotlin bridging ABI for generic externs
This commit is contained in:
parent
2b13fe8053
commit
74eb8ff082
@ -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`.
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
179
proposals/extern_generic_runtime_abi.md
Normal file
179
proposals/extern_generic_runtime_abi.md
Normal 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.
|
||||
|
||||
@ -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.
|
||||
Loading…
x
Reference in New Issue
Block a user