Compare commits
5 Commits
37d093817e
...
8e0442670d
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e0442670d | |||
| 74eb8ff082 | |||
| 2b13fe8053 | |||
| 979e6ea9b7 | |||
| e447c778ed |
@ -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`.
|
||||
|
||||
@ -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 is implicitly extern (you may still write `extern`, but it is redundant).
|
||||
- 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 {
|
||||
@ -251,7 +257,22 @@ 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 of `extern class` / `extern object` are treated as extern by default, so the compiler emits 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 {
|
||||
var value: Int
|
||||
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)
|
||||
@ -321,7 +342,7 @@ Notes:
|
||||
|
||||
- Required order: declare/eval Lyng object in the module first, then call `bindObject(...)`.
|
||||
This is the pattern covered by `BridgeBindingTest.testExternObjectBinding`.
|
||||
- Members must be marked `extern` so the compiler emits ABI slots for Kotlin bindings.
|
||||
- Members must be extern (explicitly, or implicitly via `extern object`) so the compiler emits ABI slots for Kotlin bindings.
|
||||
- You can also bind by name/module via `LyngObjectBridge.bind(...)`.
|
||||
|
||||
Minimal `extern fun` example:
|
||||
|
||||
@ -248,6 +248,7 @@ The `Obj.getLyngExceptionMessageWithStackTrace()` extension method has been adde
|
||||
Lyng now provides a public Kotlin reflection bridge and a Lyng‑first class binding workflow. This is the **preferred** way to write Kotlin extensions and library integrations:
|
||||
|
||||
- **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 extern surfaces in Lyng (`extern` members, or members inside `extern class/object`) 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 are implicitly extern.
|
||||
|
||||
See **Embedding Lyng** for full samples and usage details.
|
||||
|
||||
@ -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 are implicitly extern (`extern` on a member is optional/redundant).
|
||||
- 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.
|
||||
|
||||
@ -88,9 +88,11 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
// Imports: each segment as namespace/path
|
||||
mini?.imports?.forEach { imp ->
|
||||
imp.segments.forEach { seg ->
|
||||
val start = analysis.source.offsetOf(seg.range.start)
|
||||
val end = analysis.source.offsetOf(seg.range.end)
|
||||
putRange(start, end, LyngHighlighterColors.NAMESPACE)
|
||||
if (seg.range.start.source === analysis.source && seg.range.end.source === analysis.source) {
|
||||
val start = analysis.source.offsetOf(seg.range.start)
|
||||
val end = analysis.source.offsetOf(seg.range.end)
|
||||
putRange(start, end, LyngHighlighterColors.NAMESPACE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -35,7 +35,7 @@ class LyngLexer : LexerBase() {
|
||||
"fun", "val", "var", "class", "interface", "type", "import", "as",
|
||||
"abstract", "closed", "override", "static", "extern", "open", "private", "protected",
|
||||
"if", "else", "for", "while", "return", "true", "false", "null",
|
||||
"when", "in", "is", "break", "continue", "try", "catch", "finally",
|
||||
"when", "in", "is", "break", "continue", "try", "catch", "finally", "void",
|
||||
"get", "set", "object", "enum", "init", "by", "step", "property", "constructor"
|
||||
)
|
||||
|
||||
|
||||
@ -26,23 +26,16 @@ import com.intellij.psi.search.FilenameIndex
|
||||
import com.intellij.psi.search.GlobalSearchScope
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.binding.BindingSnapshot
|
||||
import net.sergeych.lyng.miniast.BuiltinDocRegistry
|
||||
import net.sergeych.lyng.miniast.DocLookupUtils
|
||||
import net.sergeych.lyng.miniast.MiniEnumDecl
|
||||
import net.sergeych.lyng.miniast.MiniRange
|
||||
import net.sergeych.lyng.miniast.MiniScript
|
||||
import net.sergeych.lyng.tools.IdeLenientImportProvider
|
||||
import net.sergeych.lyng.tools.LyngAnalysisRequest
|
||||
import net.sergeych.lyng.tools.LyngAnalysisResult
|
||||
import net.sergeych.lyng.tools.LyngDiagnostic
|
||||
import net.sergeych.lyng.tools.LyngLanguageTools
|
||||
import net.sergeych.lyng.idea.LyngFileType
|
||||
import net.sergeych.lyng.miniast.*
|
||||
import net.sergeych.lyng.tools.*
|
||||
|
||||
object LyngAstManager {
|
||||
private val MINI_KEY = Key.create<MiniScript>("lyng.mini.cache")
|
||||
private val BINDING_KEY = Key.create<BindingSnapshot>("lyng.binding.cache")
|
||||
private val STAMP_KEY = Key.create<Long>("lyng.mini.cache.stamp")
|
||||
private val ANALYSIS_KEY = Key.create<LyngAnalysisResult>("lyng.analysis.cache")
|
||||
private val implicitBuiltinNames = setOf("void")
|
||||
|
||||
fun getMiniAst(file: PsiFile): MiniScript? = runReadAction {
|
||||
getAnalysis(file)?.mini
|
||||
@ -217,7 +210,7 @@ object LyngAstManager {
|
||||
val msg = diag.message
|
||||
if (msg.startsWith("unresolved name: ")) {
|
||||
val name = msg.removePrefix("unresolved name: ").trim()
|
||||
name in declaredTopLevel || name in builtinTopLevel
|
||||
name in declaredTopLevel || name in builtinTopLevel || name in implicitBuiltinNames
|
||||
} else if (msg.startsWith("unresolved member: ")) {
|
||||
val name = msg.removePrefix("unresolved member: ").trim()
|
||||
val range = diag.range
|
||||
|
||||
@ -124,4 +124,16 @@ class LyngDefinitionFilesTest : BasePlatformTestCase() {
|
||||
assertTrue("Should not report unresolved name for Declared", messages.none { it.contains("unresolved name: Declared") })
|
||||
assertTrue("Should not report unresolved member for greet", messages.none { it.contains("unresolved member: greet") })
|
||||
}
|
||||
|
||||
fun test_DiagnosticsDoNotReportVoidAsUnresolvedName() {
|
||||
val code = """
|
||||
fun f(): void {
|
||||
return void
|
||||
}
|
||||
""".trimIndent()
|
||||
myFixture.configureByText("main.lyng", code)
|
||||
val analysis = LyngAstManager.getAnalysis(myFixture.file)
|
||||
val messages = analysis?.diagnostics?.map { it.message } ?: emptyList()
|
||||
assertTrue("Should not report unresolved name for void, got=$messages", messages.none { it.contains("unresolved name: void") })
|
||||
}
|
||||
}
|
||||
|
||||
@ -176,6 +176,8 @@ class Compiler(
|
||||
private val lambdaCaptureEntriesByRef: MutableMap<ValueFnRef, List<net.sergeych.lyng.bytecode.LambdaCaptureEntry>> =
|
||||
mutableMapOf()
|
||||
private val classFieldTypesByName: MutableMap<String, MutableMap<String, ObjClass>> = mutableMapOf()
|
||||
private val classMethodReturnTypeByName: MutableMap<String, MutableMap<String, ObjClass>> = mutableMapOf()
|
||||
private val classMethodReturnTypeDeclByName: MutableMap<String, MutableMap<String, TypeDecl>> = mutableMapOf()
|
||||
private val classScopeMembersByClassName: MutableMap<String, MutableSet<String>> = mutableMapOf()
|
||||
private val classScopeCallableMembersByClassName: MutableMap<String, MutableSet<String>> = mutableMapOf()
|
||||
private val encodedPayloadTypeByScopeId: MutableMap<Int, MutableMap<Int, ObjClass>> = mutableMapOf()
|
||||
@ -2614,7 +2616,34 @@ class Compiler(
|
||||
}
|
||||
|
||||
Token.Type.NOT -> {
|
||||
if (operand != null) throw ScriptError(t.pos, "unexpected operator not '!' ")
|
||||
if (operand != null) {
|
||||
val save = cc.savePos()
|
||||
val next = cc.next()
|
||||
if (next.type == Token.Type.NOT) {
|
||||
val operandRef = operand
|
||||
val receiverClass = resolveReceiverClassForMember(operandRef)
|
||||
val inferredType = resolveReceiverTypeDecl(operandRef)
|
||||
?: receiverClass?.let { TypeDecl.Simple(it.className, false) }
|
||||
if (inferredType == null) {
|
||||
operand = operandRef
|
||||
continue
|
||||
}
|
||||
if (inferredType == TypeDecl.TypeAny || inferredType == TypeDecl.TypeNullableAny) {
|
||||
operand = operandRef
|
||||
continue
|
||||
}
|
||||
val nonNullType = makeTypeDeclNonNullable(inferredType)
|
||||
operand = CastRef(
|
||||
operandRef,
|
||||
TypeDeclRef(nonNullType, t.pos),
|
||||
isNullable = false,
|
||||
atPos = t.pos
|
||||
)
|
||||
continue
|
||||
}
|
||||
cc.restorePos(save)
|
||||
throw ScriptError(t.pos, "unexpected operator not '!' ")
|
||||
}
|
||||
val op = parseTerm() ?: throw ScriptError(t.pos, "Expecting expression")
|
||||
operand = UnaryOpRef(UnaryOp.NOT, op)
|
||||
}
|
||||
@ -3821,6 +3850,21 @@ class Compiler(
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeTypeDeclNonNullable(type: TypeDecl): TypeDecl {
|
||||
if (!type.isNullable) return type
|
||||
return when (type) {
|
||||
TypeDecl.TypeAny -> type
|
||||
TypeDecl.TypeNullableAny -> TypeDecl.TypeAny
|
||||
is TypeDecl.Function -> type.copy(nullable = false)
|
||||
is TypeDecl.Ellipsis -> type.copy(nullable = false)
|
||||
is TypeDecl.TypeVar -> type.copy(nullable = false)
|
||||
is TypeDecl.Union -> type.copy(nullable = false)
|
||||
is TypeDecl.Intersection -> type.copy(nullable = false)
|
||||
is TypeDecl.Simple -> TypeDecl.Simple(type.name, false)
|
||||
is TypeDecl.Generic -> TypeDecl.Generic(type.name, type.args, false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeMiniTypeNullable(type: MiniTypeRef): MiniTypeRef {
|
||||
return when (type) {
|
||||
is MiniTypeName -> type.copy(nullable = true)
|
||||
@ -4491,6 +4535,48 @@ class Compiler(
|
||||
is TypeDecl.Intersection -> "I:${type.options.joinToString("&") { typeDeclKey(it) }}"
|
||||
}
|
||||
|
||||
private fun classMethodReturnTypeDecl(targetClass: ObjClass?, name: String): TypeDecl? {
|
||||
if (targetClass == null) return null
|
||||
if (targetClass == ObjDynamic.type) return TypeDecl.TypeAny
|
||||
val member = targetClass.getInstanceMemberOrNull(name, includeAbstract = true)
|
||||
val declaringName = member?.declaringClass?.className
|
||||
if (declaringName != null) {
|
||||
classMethodReturnTypeDeclByName[declaringName]?.get(name)?.let { return it }
|
||||
classMethodReturnTypeByName[declaringName]?.get(name)?.let {
|
||||
return TypeDecl.Simple(it.className, false)
|
||||
}
|
||||
}
|
||||
classMethodReturnTypeDeclByName[targetClass.className]?.get(name)?.let { return it }
|
||||
classMethodReturnTypeByName[targetClass.className]?.get(name)?.let {
|
||||
return TypeDecl.Simple(it.className, false)
|
||||
}
|
||||
member?.typeDecl?.let { declaredType ->
|
||||
if (declaredType is TypeDecl.Function) return declaredType.returnType
|
||||
return declaredType
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun classMethodReturnClass(targetClass: ObjClass?, name: String): ObjClass? {
|
||||
if (targetClass == null) return null
|
||||
if (targetClass == ObjDynamic.type) return ObjDynamic.type
|
||||
classMethodReturnTypeDecl(targetClass, name)?.let { declared ->
|
||||
resolveTypeDeclObjClass(declared)?.let { return it }
|
||||
if (declared is TypeDecl.TypeVar) return Obj.rootObjectType
|
||||
}
|
||||
val member = targetClass.getInstanceMemberOrNull(name, includeAbstract = true)
|
||||
val declaringName = member?.declaringClass?.className
|
||||
if (declaringName != null) {
|
||||
classMethodReturnTypeByName[declaringName]?.get(name)?.let { return it }
|
||||
}
|
||||
classMethodReturnTypeByName[targetClass.className]?.get(name)?.let { return it }
|
||||
val declaredType = member?.typeDecl
|
||||
if (declaredType is TypeDecl.Function) {
|
||||
resolveTypeDeclObjClass(declaredType.returnType)?.let { return it }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun inferObjClassFromRef(ref: ObjRef): ObjClass? = when (ref) {
|
||||
is ConstRef -> ref.constValue as? ObjClass ?: ref.constValue.objClass
|
||||
is LocalVarRef -> nameObjClass[ref.name] ?: resolveClassByName(ref.name)
|
||||
@ -4511,6 +4597,11 @@ class Compiler(
|
||||
is RangeRef -> ObjRange.type
|
||||
is ClassOperatorRef -> ObjClassType
|
||||
is CastRef -> resolveTypeRefClass(ref.castTypeRef())
|
||||
is IndexRef -> {
|
||||
val targetClass = resolveReceiverClassForMember(ref.targetRef)
|
||||
classMethodReturnClass(targetClass, "getAt")
|
||||
?: inferFieldReturnClass(targetClass, "getAt")
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
@ -4547,6 +4638,12 @@ class Compiler(
|
||||
else -> TypeDecl.TypeVar("${typeDeclName(targetDecl)}.${ref.name}", false)
|
||||
}
|
||||
}
|
||||
is IndexRef -> {
|
||||
val targetDecl = resolveReceiverTypeDecl(ref.targetRef)
|
||||
inferMethodCallReturnTypeDecl("getAt", targetDecl)?.let { return it }
|
||||
val targetClass = resolveReceiverClassForMember(ref.targetRef)
|
||||
classMethodReturnTypeDecl(targetClass, "getAt")
|
||||
}
|
||||
is MethodCallRef -> methodReturnTypeDeclByRef[ref]
|
||||
is CallRef -> callReturnTypeDeclByRef[ref]
|
||||
is StatementRef -> (ref.statement as? ExpressionStatement)?.let { resolveReceiverTypeDecl(it.ref) }
|
||||
@ -4616,6 +4713,12 @@ class Compiler(
|
||||
val targetClass = resolveReceiverClassForMember(ref.target)
|
||||
inferFieldReturnClass(targetClass, ref.name)
|
||||
}
|
||||
is IndexRef -> {
|
||||
val targetClass = resolveReceiverClassForMember(ref.targetRef)
|
||||
classMethodReturnClass(targetClass, "getAt")
|
||||
?: inferFieldReturnClass(targetClass, "getAt")
|
||||
?: inferMethodCallReturnClass("getAt")
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@ -5102,6 +5205,7 @@ class Compiler(
|
||||
}
|
||||
|
||||
private fun resolveTypeRefClass(ref: ObjRef): ObjClass? = when (ref) {
|
||||
is TypeDeclRef -> resolveTypeDeclObjClass(ref.decl())
|
||||
is ConstRef -> ref.constValue as? ObjClass
|
||||
is LocalSlotRef -> resolveTypeDeclObjClass(TypeDecl.Simple(ref.name, false)) ?: nameObjClass[ref.name]
|
||||
is LocalVarRef -> resolveTypeDeclObjClass(TypeDecl.Simple(ref.name, false)) ?: nameObjClass[ref.name]
|
||||
@ -7885,6 +7989,23 @@ class Compiler(
|
||||
val rawFnStatements = parsedFnStatements?.let { unwrapBytecodeDeep(it) }
|
||||
val inferredReturnClass = returnTypeDecl?.let { resolveTypeDeclObjClass(it) }
|
||||
?: inferReturnClassFromStatement(rawFnStatements)
|
||||
if (declKind == SymbolKind.MEMBER && extTypeName == null) {
|
||||
val ownerClassName = (parentContext as? CodeContext.ClassBody)?.name
|
||||
if (ownerClassName != null) {
|
||||
val returnDecl = returnTypeDecl
|
||||
?: inferredReturnClass?.let { TypeDecl.Simple(it.className, false) }
|
||||
if (returnDecl != null) {
|
||||
classMethodReturnTypeDeclByName
|
||||
.getOrPut(ownerClassName) { mutableMapOf() }[name] = returnDecl
|
||||
resolveTypeDeclObjClass(returnDecl)?.let { returnClass ->
|
||||
classMethodReturnTypeByName
|
||||
.getOrPut(ownerClassName) { mutableMapOf() }[name] = returnClass
|
||||
classFieldTypesByName
|
||||
.getOrPut(ownerClassName) { mutableMapOf() }[name] = returnClass
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (declKind != SymbolKind.MEMBER && inferredReturnClass != null) {
|
||||
callableReturnTypeByName[name] = inferredReturnClass
|
||||
val slotLoc = lookupSlotLocation(name, includeModule = true)
|
||||
@ -8661,7 +8782,6 @@ class Compiler(
|
||||
name = nameToken.value
|
||||
nameStartPos = nameToken.pos
|
||||
}
|
||||
|
||||
val receiverNormalization = normalizeReceiverTypeDecl(receiverTypeDecl, emptySet())
|
||||
val implicitTypeParams = receiverNormalization.second
|
||||
if (implicitTypeParams.isNotEmpty()) pendingTypeParamStack.add(implicitTypeParams)
|
||||
|
||||
@ -51,8 +51,9 @@ interface BridgeInstanceContext {
|
||||
* Use [LyngClassBridge.bind] to obtain a binder and register implementations.
|
||||
* Bindings must happen before the first instance of the class is created.
|
||||
*
|
||||
* Important: members you bind here must be declared as `extern` in Lyng so the
|
||||
* compiler emits the ABI slots that Kotlin bindings attach to.
|
||||
* Important: members you bind here must be extern in Lyng (explicitly, or
|
||||
* implicitly by being inside `extern class` / `extern object`) so the compiler
|
||||
* emits the ABI slots that Kotlin bindings attach to.
|
||||
*/
|
||||
interface ClassBridgeBinder {
|
||||
/** Arbitrary Kotlin-side data attached to the class. */
|
||||
@ -70,7 +71,7 @@ interface ClassBridgeBinder {
|
||||
replaceWith = ReplaceWith("initWithInstance { block(this, thisObj) }")
|
||||
)
|
||||
fun initWithInstance(block: suspend (ScopeFacade, Obj) -> Unit)
|
||||
/** Bind a Lyng function/member to a Kotlin implementation (requires `extern` in Lyng). */
|
||||
/** Bind a Lyng function/member to a Kotlin implementation (requires extern member in Lyng). */
|
||||
fun addFun(name: String, impl: suspend ScopeFacade.() -> Obj)
|
||||
/**
|
||||
* Legacy addFun form.
|
||||
@ -81,7 +82,7 @@ interface ClassBridgeBinder {
|
||||
replaceWith = ReplaceWith("addFun(name) { impl(this, thisObj, args) }")
|
||||
)
|
||||
fun addFun(name: String, impl: suspend (ScopeFacade, Obj, Arguments) -> Obj)
|
||||
/** Bind a read-only member (val/property getter) declared as `extern`. */
|
||||
/** Bind a read-only member (val/property getter) declared extern in Lyng. */
|
||||
fun addVal(name: String, impl: suspend ScopeFacade.() -> Obj)
|
||||
/**
|
||||
* Legacy addVal form.
|
||||
@ -92,7 +93,7 @@ interface ClassBridgeBinder {
|
||||
replaceWith = ReplaceWith("addVal(name) { impl(this, thisObj) }")
|
||||
)
|
||||
fun addVal(name: String, impl: suspend (ScopeFacade, Obj) -> Obj)
|
||||
/** Bind a mutable member (var/property getter/setter) declared as `extern`. */
|
||||
/** Bind a mutable member (var/property getter/setter) declared extern in Lyng. */
|
||||
fun addVar(
|
||||
name: String,
|
||||
get: suspend ScopeFacade.() -> Obj,
|
||||
@ -119,8 +120,9 @@ interface ClassBridgeBinder {
|
||||
* Entry point for Kotlin bindings to declared Lyng classes.
|
||||
*
|
||||
* The workflow is Lyng-first: declare the class and its members in Lyng,
|
||||
* then bind the implementations from Kotlin. Bound members must be marked
|
||||
* `extern` so the compiler emits the ABI slots for Kotlin to attach to.
|
||||
* then bind the implementations from Kotlin. Bound members must be extern
|
||||
* (explicitly or by enclosing `extern class` / `extern object`) so the compiler
|
||||
* emits the ABI slots for Kotlin to attach to.
|
||||
*/
|
||||
object LyngClassBridge {
|
||||
/**
|
||||
@ -212,7 +214,7 @@ object LyngObjectBridge {
|
||||
/**
|
||||
* Sugar for [LyngClassBridge.bind] on a module scope.
|
||||
*
|
||||
* Bound members must be declared as `extern` in Lyng.
|
||||
* Bound members must be extern in Lyng (explicitly or via enclosing extern class/object).
|
||||
*/
|
||||
suspend fun ModuleScope.bind(
|
||||
className: String,
|
||||
@ -222,7 +224,7 @@ suspend fun ModuleScope.bind(
|
||||
/**
|
||||
* Sugar for [LyngObjectBridge.bind] on a module scope.
|
||||
*
|
||||
* Bound members must be declared as `extern` in Lyng.
|
||||
* Bound members must be extern in Lyng (explicitly or via enclosing extern class/object).
|
||||
*/
|
||||
suspend fun ModuleScope.bindObject(
|
||||
objectName: String,
|
||||
|
||||
@ -46,7 +46,7 @@ private val fallbackKeywordIds = setOf(
|
||||
"private", "protected", "static", "open", "extern", "init", "get", "set", "by", "step",
|
||||
// control flow and misc
|
||||
"if", "else", "when", "while", "do", "for", "try", "catch", "finally",
|
||||
"throw", "return", "break", "continue", "this", "null", "true", "false", "unset"
|
||||
"throw", "return", "break", "continue", "this", "null", "true", "false", "unset", "void"
|
||||
)
|
||||
|
||||
/** Maps lexer token type (and sometimes value) to a [HighlightKind]. */
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -179,6 +179,7 @@ object LyngLanguageTools {
|
||||
val source = analysis.source
|
||||
val out = ArrayList<LyngSemanticSpan>(128)
|
||||
val covered = HashSet<Pair<Int, Int>>()
|
||||
fun isCurrentSource(pos: Pos): Boolean = pos.source === source
|
||||
|
||||
fun addRange(start: Int, end: Int, kind: LyngSemanticKind) {
|
||||
if (start < 0 || end <= start || end > analysis.text.length) return
|
||||
@ -187,6 +188,7 @@ object LyngLanguageTools {
|
||||
}
|
||||
|
||||
fun addName(pos: Pos, name: String, kind: LyngSemanticKind) {
|
||||
if (!isCurrentSource(pos)) return
|
||||
val s = source.offsetOf(pos)
|
||||
addRange(s, s + name.length, kind)
|
||||
}
|
||||
@ -206,7 +208,9 @@ object LyngLanguageTools {
|
||||
addTypeSegments(t.returnType)
|
||||
}
|
||||
is MiniTypeVar -> {
|
||||
addRange(source.offsetOf(t.range.start), source.offsetOf(t.range.end), LyngSemanticKind.TypeRef)
|
||||
if (isCurrentSource(t.range.start) && isCurrentSource(t.range.end)) {
|
||||
addRange(source.offsetOf(t.range.start), source.offsetOf(t.range.end), LyngSemanticKind.TypeRef)
|
||||
}
|
||||
}
|
||||
is MiniTypeUnion -> {
|
||||
t.options.forEach { addTypeSegments(it) }
|
||||
@ -262,7 +266,9 @@ object LyngLanguageTools {
|
||||
|
||||
mini.imports.forEach { imp ->
|
||||
imp.segments.forEach { seg ->
|
||||
addRange(source.offsetOf(seg.range.start), source.offsetOf(seg.range.end), LyngSemanticKind.TypeRef)
|
||||
if (isCurrentSource(seg.range.start) && isCurrentSource(seg.range.end)) {
|
||||
addRange(source.offsetOf(seg.range.start), source.offsetOf(seg.range.end), LyngSemanticKind.TypeRef)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -30,8 +30,8 @@ import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.encodeToJsonElement
|
||||
import net.sergeych.lyng.*
|
||||
import net.sergeych.lyng.obj.*
|
||||
import net.sergeych.lyng.thisAs
|
||||
import net.sergeych.lyng.pacman.InlineSourcesImportProvider
|
||||
import net.sergeych.lyng.thisAs
|
||||
import net.sergeych.mp_tools.globalDefer
|
||||
import net.sergeych.tools.bm
|
||||
import kotlin.test.*
|
||||
@ -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
|
||||
|
||||
@ -565,19 +565,6 @@ class TypesTest {
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
// @Test
|
||||
// fun testNullableGenericTypes() = runTest {
|
||||
// eval("""
|
||||
// fun t<T>(): String =
|
||||
// when(T) {
|
||||
// is Object -> "%s is Object"(T::class.name)
|
||||
// else -> throw "It should not happen"
|
||||
// }
|
||||
// assert( Int is Object)
|
||||
// assertEquals( t<Int>(), "Class is Object")
|
||||
// """.trimIndent())
|
||||
// }
|
||||
|
||||
@Test fun testIndexer() = runTest {
|
||||
eval("""
|
||||
class Greeter {
|
||||
@ -596,6 +583,63 @@ class TypesTest {
|
||||
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test fun testIndexer2() = runTest {
|
||||
eval("""
|
||||
class Foo(bar)
|
||||
|
||||
class Greeter {
|
||||
override fun getAt(name): Foo = Foo("Hello, %s!"(name))
|
||||
}
|
||||
assertEquals("Hello, Bob!",Greeter()["Bob"].bar)
|
||||
val g = Greeter()
|
||||
assertEquals("Hello, Bob!",g["Bob"].bar)
|
||||
|
||||
// it should work with objects too:
|
||||
object Polite {
|
||||
override fun getAt(name): Foo? = Foo("How do you do, %s?"(name))
|
||||
}
|
||||
|
||||
assertEquals("How do you do, Bob?",Polite["Bob"].bar)
|
||||
assertEquals("How do you do, Bob?",Polite["Bob"]?.bar)
|
||||
assertEquals("How do you do, Bob?",Polite["Bob"]!!.bar)
|
||||
|
||||
class Greeter2 {
|
||||
override fun getAt(name): Foo? = Foo("How do you do, %s?"(name))
|
||||
}
|
||||
val g2 = Greeter2()
|
||||
assertEquals("How do you do, Bob?",g2["Bob"]?.bar)
|
||||
val g2v: Foo = g2["Bob"]!!
|
||||
assertEquals("How do you do, Bob?",g2v.bar)
|
||||
assertEquals("How do you do, Bob?",Greeter2()["Bob"].bar)
|
||||
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExternGenerics() = runTest {
|
||||
eval("""
|
||||
extern fun f<T>(x: T): T
|
||||
extern class Cell<T> {
|
||||
var value: T
|
||||
}
|
||||
""")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExternClassMemberInitializerStillFailsWithoutExplicitExtern() = runTest {
|
||||
val e = assertFailsWith<ScriptError> {
|
||||
eval(
|
||||
"""
|
||||
extern class Cell<T> {
|
||||
var value: T = 1
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
assertTrue(e.message?.contains("extern variable value cannot have an initializer or delegate") == true)
|
||||
}
|
||||
|
||||
// @Test fun nonTrivialOperatorsTest() = runTest {
|
||||
// val s = Script.newScope()
|
||||
// s.eval("""
|
||||
|
||||
@ -18,8 +18,9 @@
|
||||
package net.sergeych.lyng.tools
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.lyng.miniast.MiniClassDecl
|
||||
import net.sergeych.lyng.miniast.MiniMemberTypeAliasDecl
|
||||
import net.sergeych.lyng.Pos
|
||||
import net.sergeych.lyng.Source
|
||||
import net.sergeych.lyng.miniast.*
|
||||
import net.sergeych.lyng.stdlib_included.rootLyng
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
@ -127,4 +128,48 @@ class LyngLanguageToolsTest {
|
||||
val dis = LyngLanguageTools.disassembleSymbol(code, "add")
|
||||
assertTrue(!dis.contains("not a compiled body"), "Disassembly should be produced, got: $dis")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun languageTools_semanticHighlights_ignore_foreign_sources() {
|
||||
val localSource = Source("local.lyng", "val x = 1")
|
||||
val foreignSource = Source("defs.lyng.d", "val y = 2")
|
||||
val localStart = Pos(localSource, 0, 0)
|
||||
val foreignStart = Pos(foreignSource, 0, 0)
|
||||
|
||||
val mini = MiniScript(
|
||||
range = MiniRange(localStart, localStart),
|
||||
declarations = mutableListOf(
|
||||
MiniValDecl(
|
||||
range = MiniRange(foreignStart, foreignStart),
|
||||
name = "y",
|
||||
mutable = false,
|
||||
type = null,
|
||||
initRange = null,
|
||||
doc = null,
|
||||
nameStart = foreignStart
|
||||
)
|
||||
),
|
||||
imports = mutableListOf(
|
||||
MiniImport(
|
||||
range = MiniRange(foreignStart, foreignStart),
|
||||
segments = listOf(
|
||||
MiniImport.Segment("defs", MiniRange(foreignStart, foreignStart))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val analysis = LyngAnalysisResult(
|
||||
source = localSource,
|
||||
text = localSource.text,
|
||||
mini = mini,
|
||||
binding = null,
|
||||
resolution = null,
|
||||
importedModules = emptyList(),
|
||||
diagnostics = emptyList(),
|
||||
lexicalHighlights = emptyList()
|
||||
)
|
||||
|
||||
val spans = LyngLanguageTools.semanticHighlights(analysis)
|
||||
assertTrue(spans.isEmpty(), "Semantic spans should ignore positions from foreign sources, got $spans")
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
package lyng.stdlib
|
||||
|
||||
extern fun flow(builder: FlowBuilder.()->Void): Flow
|
||||
extern fun flow(builder: FlowBuilder.()->void): Flow
|
||||
|
||||
/* Built-in exception type. */
|
||||
extern class Exception
|
||||
@ -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).
|
||||
@ -337,17 +337,17 @@ override fun List<T>.toString() {
|
||||
}
|
||||
|
||||
/* Sort list in-place by key selector. */
|
||||
fun List<T>.sortBy<R>(predicate: (T)->R): Void {
|
||||
fun List<T>.sortBy<R>(predicate: (T)->R): void {
|
||||
sortWith { a, b -> predicate(a) <=> predicate(b) }
|
||||
}
|
||||
|
||||
/* Sort list in-place by natural order. */
|
||||
fun List<T>.sort(): Void {
|
||||
fun List<T>.sort(): void {
|
||||
sortWith { a, b -> a <=> b }
|
||||
}
|
||||
|
||||
/* Print this exception and its stack trace to standard output. */
|
||||
fun Exception.printStackTrace(): Void {
|
||||
fun Exception.printStackTrace(): void {
|
||||
println(this)
|
||||
for( entry in stackTrace ) {
|
||||
println("\tat "+entry.toString())
|
||||
@ -357,7 +357,7 @@ fun Exception.printStackTrace(): Void {
|
||||
/* Compile this string into a regular expression. */
|
||||
val String.re: Regex get() = Regex(this)
|
||||
|
||||
fun TODO(message: Object?=null): Void {
|
||||
fun TODO(message: Object?=null): void {
|
||||
throw "not implemented"
|
||||
}
|
||||
|
||||
@ -376,12 +376,12 @@ enum DelegateAccess {
|
||||
Implementing this interface is optional as Lyng uses dynamic dispatch,
|
||||
but it is recommended for documentation and clarity.
|
||||
*/
|
||||
interface Delegate<T,ThisRefType=Void> {
|
||||
interface Delegate<T,ThisRefType=void> {
|
||||
/* Called when a delegated 'val' or 'var' is read. */
|
||||
fun getValue(thisRef: ThisRefType, name: String): T = TODO("delegate getter is not implemented")
|
||||
|
||||
/* Called when a delegated 'var' is written. */
|
||||
fun setValue(thisRef: ThisRefType, name: String, newValue: T): Void = TODO("delegate setter is not implemented")
|
||||
fun setValue(thisRef: ThisRefType, name: String, newValue: T): void = TODO("delegate setter is not implemented")
|
||||
|
||||
/* Called when a delegated function is invoked. */
|
||||
fun invoke(thisRef: ThisRefType, name: String, args...): Object = TODO("delegate invoke is not implemented")
|
||||
|
||||
@ -4,7 +4,7 @@ This note describes the Lyng-first workflow where a class is declared in Lyng an
|
||||
|
||||
## Overview
|
||||
|
||||
- Lyng code declares a class and marks members as `extern`.
|
||||
- Lyng code declares a class and marks members as `extern` (or puts them inside `extern class`/`extern object`, where member `extern` is implicit).
|
||||
- Kotlin binds implementations with `LyngClassBridge.bind(...)`.
|
||||
- Binding must happen **before the first instance is created**.
|
||||
- `bind(className, module, importManager)` requires `module` to resolve class names; use
|
||||
@ -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 are implicitly extern (`extern` is optional/redundant);
|
||||
- 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() = ...`.
|
||||
|
||||
@ -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 are implicitly extern.
|
||||
|
||||
Flow typing:
|
||||
- Compiler should narrow types based on control-flow (e.g., `if (x != null)` narrows `x` to non-null inside the branch).
|
||||
|
||||
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