Compare commits

..

No commits in common. "8e0442670da95f2f7ee6910352f78b9c9f0b808f" and "37d093817e337d6c8b8cb4541674299e9c2cbf31" have entirely different histories.

25 changed files with 157 additions and 547 deletions

View File

@ -21,7 +21,3 @@
- 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,12 +243,6 @@ 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 {
@ -257,22 +251,7 @@ class Counter {
}
```
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)
```
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`.
```kotlin
// Kotlin side (binding)
@ -342,7 +321,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 extern (explicitly, or implicitly via `extern object`) so the compiler emits ABI slots for Kotlin bindings.
- Members must be marked `extern` 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,7 +248,6 @@ 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 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.
- **Class bridge binding**: declare classes/members in Lyng (marked `extern`) and bind the implementations in Kotlin before the first instance is created.
See **Embedding Lyng** for full samples and usage details.

View File

@ -18,11 +18,6 @@ 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,13 +88,11 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
// Imports: each segment as namespace/path
mini?.imports?.forEach { imp ->
imp.segments.forEach { seg ->
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)
}
}
}
// Add annotation/label coloring using token highlighter
run {

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", "void",
"when", "in", "is", "break", "continue", "try", "catch", "finally",
"get", "set", "object", "enum", "init", "by", "step", "property", "constructor"
)

View File

@ -26,16 +26,23 @@ 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
@ -210,7 +217,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 implicitBuiltinNames
name in declaredTopLevel || name in builtinTopLevel
} else if (msg.startsWith("unresolved member: ")) {
val name = msg.removePrefix("unresolved member: ").trim()
val range = diag.range

View File

@ -124,16 +124,4 @@ 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,8 +176,6 @@ 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()
@ -2616,34 +2614,7 @@ class Compiler(
}
Token.Type.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 '!' ")
}
if (operand != null) throw ScriptError(t.pos, "unexpected operator not '!' ")
val op = parseTerm() ?: throw ScriptError(t.pos, "Expecting expression")
operand = UnaryOpRef(UnaryOp.NOT, op)
}
@ -3850,21 +3821,6 @@ 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)
@ -4535,48 +4491,6 @@ 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)
@ -4597,11 +4511,6 @@ 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
}
@ -4638,12 +4547,6 @@ 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) }
@ -4713,12 +4616,6 @@ 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
}
}
@ -5205,7 +5102,6 @@ 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]
@ -7989,23 +7885,6 @@ 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)
@ -8782,6 +8661,7 @@ 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,9 +51,8 @@ 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 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.
* Important: members you bind here must be declared as `extern` in Lyng so the
* compiler emits the ABI slots that Kotlin bindings attach to.
*/
interface ClassBridgeBinder {
/** Arbitrary Kotlin-side data attached to the class. */
@ -71,7 +70,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 member in Lyng). */
/** Bind a Lyng function/member to a Kotlin implementation (requires `extern` in Lyng). */
fun addFun(name: String, impl: suspend ScopeFacade.() -> Obj)
/**
* Legacy addFun form.
@ -82,7 +81,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 extern in Lyng. */
/** Bind a read-only member (val/property getter) declared as `extern`. */
fun addVal(name: String, impl: suspend ScopeFacade.() -> Obj)
/**
* Legacy addVal form.
@ -93,7 +92,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 extern in Lyng. */
/** Bind a mutable member (var/property getter/setter) declared as `extern`. */
fun addVar(
name: String,
get: suspend ScopeFacade.() -> Obj,
@ -120,9 +119,8 @@ 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 extern
* (explicitly or by enclosing `extern class` / `extern object`) so the compiler
* emits the ABI slots for Kotlin to attach to.
* then bind the implementations from Kotlin. Bound members must be marked
* `extern` so the compiler emits the ABI slots for Kotlin to attach to.
*/
object LyngClassBridge {
/**
@ -214,7 +212,7 @@ object LyngObjectBridge {
/**
* Sugar for [LyngClassBridge.bind] on a module scope.
*
* Bound members must be extern in Lyng (explicitly or via enclosing extern class/object).
* Bound members must be declared as `extern` in Lyng.
*/
suspend fun ModuleScope.bind(
className: String,
@ -224,7 +222,7 @@ suspend fun ModuleScope.bind(
/**
* Sugar for [LyngObjectBridge.bind] on a module scope.
*
* Bound members must be extern in Lyng (explicitly or via enclosing extern class/object).
* Bound members must be declared as `extern` in Lyng.
*/
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", "void"
"throw", "return", "break", "continue", "this", "null", "true", "false", "unset"
)
/** 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 {
extern fun cancel(): Void
fun cancel(): Void
}
extern class Observable<Change> {
extern fun beforeChange(listener: (Change)->Void): Subscription
extern fun onChange(listener: (Change)->Void): Subscription
extern fun changes(): Flow<Change>
fun beforeChange(listener: (Change)->Void): Subscription
fun onChange(listener: (Change)->Void): Subscription
fun changes(): Flow<Change>
}
extern class ListChange<T>
extern class ListSet<T> : ListChange<T> {
extern val index: Int
extern val oldValue: Object
extern val newValue: Object
val index: Int
val oldValue: Object
val newValue: Object
}
extern class ListInsert<T> : ListChange<T> {
extern val index: Int
extern val values: List<T>
val index: Int
val values: List<T>
}
extern class ListRemove<T> : ListChange<T> {
extern val index: Int
extern val oldValue: Object
val index: Int
val oldValue: Object
}
extern class ListClear<T> : ListChange<T> {
extern val oldValues: List<T>
val oldValues: List<T>
}
extern class ListReorder<T> : ListChange<T> {
extern val oldValues: List<T>
extern val newValues: Object
val oldValues: List<T>
val newValues: Object
}
extern class ObservableList<T> : List<T> {
extern fun beforeChange(listener: (ListChange<T>)->Void): Subscription
extern fun onChange(listener: (ListChange<T>)->Void): Subscription
extern fun changes(): Flow<ListChange<T>>
fun beforeChange(listener: (ListChange<T>)->Void): Subscription
fun onChange(listener: (ListChange<T>)->Void): Subscription
fun changes(): Flow<ListChange<T>>
}
fun List<T>.observable(): ObservableList<T> {

View File

@ -179,7 +179,6 @@ 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
@ -188,7 +187,6 @@ 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)
}
@ -208,10 +206,8 @@ object LyngLanguageTools {
addTypeSegments(t.returnType)
}
is MiniTypeVar -> {
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) }
}
@ -266,11 +262,9 @@ object LyngLanguageTools {
mini.imports.forEach { imp ->
imp.segments.forEach { seg ->
if (isCurrentSource(seg.range.start) && isCurrentSource(seg.range.end)) {
addRange(source.offsetOf(seg.range.start), source.offsetOf(seg.range.end), LyngSemanticKind.TypeRef)
}
}
}
fun addParams(params: List<MiniParam>) {
params.forEach { p -> addName(p.nameStart, p.name, LyngSemanticKind.Parameter) }

View File

@ -164,7 +164,7 @@ class BindingTest {
val ms = Script.newScope()
ms.eval("""
extern class A {
extern fun get1(): String
fun get1(): String
}
extern fun getA(): A
@ -184,3 +184,4 @@ class BindingTest {
}
}

View File

@ -231,7 +231,7 @@ class BridgeBindingTest {
val ms = Script.newScope()
ms.eval("""
extern class A {
extern val field: Int
val field: Int
}
fun test(a: A) = a.field

View File

@ -249,13 +249,13 @@ class MiniAstTest {
// Doc2
extern class C1 {
// Doc3
extern fun m1()
fun m1()
}
// Doc4
extern object O1 {
// Doc5
extern val v1: String
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.pacman.InlineSourcesImportProvider
import net.sergeych.lyng.thisAs
import net.sergeych.lyng.pacman.InlineSourcesImportProvider
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) {
extern fun doSomething(): Int
extern val status: String
fun doSomething(): Int
val status: String
}
extern object HostObject {
extern fun getInstance(): HostClass
fun getInstance(): HostClass
}
extern enum HostEnum {
VALUE1, VALUE2

View File

@ -565,6 +565,19 @@ 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 {
@ -583,63 +596,6 @@ 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,9 +18,8 @@
package net.sergeych.lyng.tools
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.Pos
import net.sergeych.lyng.Source
import net.sergeych.lyng.miniast.*
import net.sergeych.lyng.miniast.MiniClassDecl
import net.sergeych.lyng.miniast.MiniMemberTypeAliasDecl
import net.sergeych.lyng.stdlib_included.rootLyng
import kotlin.test.Test
import kotlin.test.assertEquals
@ -128,48 +127,4 @@ 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 {
extern fun getList(): List<String>
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> {
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>
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 class Iterator<T> {
extern fun hasNext(): Bool
extern fun next(): T
extern fun cancelIteration(): void
extern fun toList(): List<T>
fun hasNext(): Bool
fun next(): T
fun cancelIteration(): Void
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> {
extern val size: Int
val size: Int
}
extern class Array<T> : Collection<T> {
}
extern class ImmutableList<T> : Array<T> {
extern fun toMutable(): List<T>
fun toMutable(): List<T>
}
extern class List<T> : Array<T> {
extern fun add(value: T, more...): void
extern fun toImmutable(): ImmutableList<T>
fun add(value: T, more...): Void
fun toImmutable(): ImmutableList<T>
}
extern class RingBuffer<T> : Iterable<T> {
extern val size: Int
extern fun first(): T
extern fun add(value: T): void
val size: Int
fun first(): T
fun add(value: T): Void
}
extern class Set<T> : Collection<T> {
extern fun toImmutable(): ImmutableSet<T>
fun toImmutable(): ImmutableSet<T>
}
extern class ImmutableSet<T> : Collection<T> {
extern fun toMutable(): Set<T>
fun toMutable(): Set<T>
}
extern class Map<K,V> : Collection<MapEntry<K,V>> {
extern fun toImmutable(): ImmutableMap<K,V>
fun toImmutable(): ImmutableMap<K,V>
}
extern class ImmutableMap<K,V> : Collection<MapEntry<K,V>> {
extern fun getOrNull(key: K): V?
extern fun toMutable(): Map<K,V>
fun getOrNull(key: K): V?
fun toMutable(): Map<K,V>
}
extern class MapEntry<K,V> : Array<Object> {
extern val key: K
extern val value: V
val key: K
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` (or puts them inside `extern class`/`extern object`, where member `extern` is implicit).
- Lyng code declares a class and marks members as `extern`.
- 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,9 +12,6 @@ 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
@ -73,4 +70,3 @@ 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,7 +276,6 @@ 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

@ -1,179 +0,0 @@
# 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

@ -0,0 +1,48 @@
> 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.