Compare commits

...

5 Commits

25 changed files with 547 additions and 157 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

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

View File

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

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

View File

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

View File

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

View File

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

View File

@ -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") })
}
}

View File

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

View File

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

View File

@ -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]. */

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

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

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

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

View File

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

View File

@ -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")
}
}

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

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

View File

@ -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() = ...`.

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

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.