From 0732202c806e9a9887c857862aa8383e688248b8 Mon Sep 17 00:00:00 2001 From: sergeych Date: Mon, 22 Dec 2025 14:55:53 +0100 Subject: [PATCH] improved vairable tracking, fixed plugin to wirk with 1.0.10, fixed lambda comparison --- .gitignore | 1 + docs/OOP.md | 6 + docs/declaring_arguments.md | 2 +- docs/tutorial.md | 13 ++ .../completion/LyngCompletionContributor.kt | 43 ++++++ .../idea/docs/LyngDocumentationProvider.kt | 28 +++- .../kotlin/net/sergeych/lyng/Compiler.kt | 16 ++- .../lyng/miniast/CompletionEngineLight.kt | 35 +++++ .../kotlin/net/sergeych/lyng/obj/ObjInt.kt | 4 + .../kotlin/net/sergeych/lyng/obj/ObjRef.kt | 21 +++ .../kotlin/net/sergeych/lyng/statements.kt | 3 +- lynglib/src/commonTest/kotlin/OOTest.kt | 122 ++++++++++++++---- 12 files changed, 266 insertions(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index b5f10f1..562c9a4 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ xcuserdata /kotlin-js-store/wasm/yarn.lock /distributables /.output.txt +/build.log diff --git a/docs/OOP.md b/docs/OOP.md index 9895a9b..979f7dd 100644 --- a/docs/OOP.md +++ b/docs/OOP.md @@ -81,8 +81,14 @@ statements discussed later, there could be default values, ellipsis, etc. class Point(x=0,y=0) val p = Point() assert( p.x == 0 && p.y == 0 ) + + // Named arguments in constructor calls use colon syntax: + val p2 = Point(y: 10, x: 5) + assert( p2.x == 5 && p2.y == 10 ) >>> void +Note that unlike **Kotlin**, which uses `=` for named arguments, Lyng uses `:` to avoid ambiguity with assignment expressions. + ## Methods Functions defined inside a class body are methods, and unless declared diff --git a/docs/declaring_arguments.md b/docs/declaring_arguments.md index 2995c00..18d52c9 100644 --- a/docs/declaring_arguments.md +++ b/docs/declaring_arguments.md @@ -123,7 +123,7 @@ Rules: - A named argument cannot reassign a parameter already set positionally. - If the last parameter has already been assigned by a named argument (or named splat), a trailing block is not allowed and results in an error. -Why `:` and not `=` at call sites? In Lyng, `=` is an expression (assignment), so we use `:` to avoid ambiguity. Declarations continue to use `:` for types, while call sites use `as` / `as?` for type operations. +Why `:` and not `=` at call sites? In Lyng, `=` is an expression (assignment), so we use `:` to avoid ambiguity. This is a key difference from **Kotlin**, which uses `=` for named arguments. Declarations in Lyng continue to use `:` for types, while call sites use `as` / `as?` for type operations. ## Named splats (map splats) diff --git a/docs/tutorial.md b/docs/tutorial.md index 7eb6130..475056e 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -439,6 +439,19 @@ It is possible to define also vararg using ellipsis: See the [arguments reference](declaring_arguments.md) for more details. +## Named arguments + +When calling functions, you can use named arguments with the colon syntax `name: value`. This is particularly useful when you have many parameters with default values. + +```lyng + fun test(a="foo", b="bar", c="bazz") { [a, b, c] } + + assertEquals(["foo", "b", "bazz"], test(b: "b")) + assertEquals(["a", "bar", "c"], test("a", c: "c")) +``` + +**Note for Kotlin users:** Lyng uses `:` instead of `=` for named arguments at call sites. This is because in Lyng, `=` is an expression that returns the assigned value, and using it in an argument list would create ambiguity. + ## Closures Each __block has an isolated context that can be accessed from closures__. For example: diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/completion/LyngCompletionContributor.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/completion/LyngCompletionContributor.kt index bada8e6..4dec6e0 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/completion/LyngCompletionContributor.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/completion/LyngCompletionContributor.kt @@ -1,3 +1,20 @@ +/* + * Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + /* * Lightweight BASIC completion for Lyng, MVP version. * Uses MiniAst (best-effort) + BuiltinDocRegistry to suggest symbols. @@ -192,6 +209,7 @@ class LyngCompletionContributor : CompletionContributor() { emit(builder) existing.add(name) } + is MiniInitDecl -> {} } } else { // Fallback: emit simple method name without detailed types @@ -329,6 +347,7 @@ class LyngCompletionContributor : CompletionContributor() { when (m) { is MiniMemberFunDecl -> if (!m.isStatic) continue is MiniMemberValDecl -> if (!m.isStatic) continue + is MiniInitDecl -> continue } } val list = target.getOrPut(m.name) { mutableListOf() } @@ -404,6 +423,7 @@ class LyngCompletionContributor : CompletionContributor() { .withTypeText(typeOf((chosen as MiniMemberValDecl).type), true) emit(builder) } + is MiniInitDecl -> {} } } } @@ -454,6 +474,7 @@ class LyngCompletionContributor : CompletionContributor() { emit(builder) already.add(name) } + is MiniInitDecl -> {} } } else { // Synthetic fallback: method without detailed params/types to improve UX in absence of docs @@ -504,6 +525,7 @@ class LyngCompletionContributor : CompletionContributor() { already.add(name) continue } + is MiniInitDecl -> {} } } // Fallback: emit without detailed types if we couldn't resolve @@ -613,6 +635,7 @@ class LyngCompletionContributor : CompletionContributor() { val rt = when (m) { is MiniMemberFunDecl -> m.returnType is MiniMemberValDecl -> m.type + is MiniInitDecl -> null } simpleClassNameOf(rt) } @@ -715,6 +738,24 @@ class LyngCompletionContributor : CompletionContributor() { if (hasDigits) { return if (isHex) "Int" else if (hasDot || hasExp) "Real" else "Int" } + + // 3) this@Type or as Type + val identRange = TextCtx.wordRangeAt(text, i + 1) + if (identRange != null) { + val ident = text.substring(identRange.startOffset, identRange.endOffset) + // if it's "as Type", we want Type + var k2 = TextCtx.prevNonWs(text, identRange.startOffset - 1) + if (k2 >= 1 && text[k2] == 's' && text[k2 - 1] == 'a' && (k2 - 1 == 0 || !text[k2 - 2].isLetterOrDigit())) { + return ident + } + // if it's "this@Type", we want Type + if (k2 >= 0 && text[k2] == '@') { + val k3 = TextCtx.prevNonWs(text, k2 - 1) + if (k3 >= 3 && text.substring(k3 - 3, k3 + 1) == "this") { + return ident + } + } + } } return null } @@ -761,6 +802,7 @@ class LyngCompletionContributor : CompletionContributor() { val returnType = when (member) { is MiniMemberFunDecl -> member.returnType is MiniMemberValDecl -> member.type + is MiniInitDecl -> null } return simpleClassNameOf(returnType) } @@ -840,6 +882,7 @@ class LyngCompletionContributor : CompletionContributor() { val returnType = when (member) { is MiniMemberFunDecl -> member.returnType is MiniMemberValDecl -> member.type + is MiniInitDecl -> null } return simpleClassNameOf(returnType) } diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/docs/LyngDocumentationProvider.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/docs/LyngDocumentationProvider.kt index 496b2a6..57566c6 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/docs/LyngDocumentationProvider.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/docs/LyngDocumentationProvider.kt @@ -154,7 +154,27 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { } else null } else null } - else -> DocLookupUtils.guessClassFromCallBefore(text, dotPos, importedModules) + else -> { + val guessed = DocLookupUtils.guessClassFromCallBefore(text, dotPos, importedModules) + if (guessed != null) guessed + else { + // handle this@Type or as Type + val i2 = TextCtx.prevNonWs(text, dotPos - 1) + if (i2 >= 0) { + val identRange = TextCtx.wordRangeAt(text, i2 + 1) + if (identRange != null) { + val id = text.substring(identRange.startOffset, identRange.endOffset) + val k = TextCtx.prevNonWs(text, identRange.startOffset - 1) + if (k >= 1 && text[k] == 's' && text[k-1] == 'a' && (k-1 == 0 || !text[k-2].isLetterOrDigit())) { + id + } else if (k >= 0 && text[k] == '@') { + val k2 = TextCtx.prevNonWs(text, k - 1) + if (k2 >= 3 && text.substring(k2 - 3, k2 + 1) == "this") id else null + } else null + } else null + } else null + } + } } if (DEBUG_LOG) log.info("[LYNG_DEBUG] QuickDoc: memberCtx dotPos=${dotPos} chBeforeDot='${if (dotPos>0) text[dotPos-1] else ' '}' classGuess=${className} imports=${importedModules}") if (className != null) { @@ -163,6 +183,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { return when (member) { is MiniMemberFunDecl -> renderMemberFunDoc(owner, member) is MiniMemberValDecl -> renderMemberValDoc(owner, member) + is MiniInitDecl -> null } } log.info("[LYNG_DEBUG] QuickDoc: resolve failed for ${className}.${ident}") @@ -224,6 +245,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { return when (member) { is MiniMemberFunDecl -> renderMemberFunDoc(owner, member) is MiniMemberValDecl -> renderMemberValDoc(owner, member) + is MiniInitDecl -> null } } } else { @@ -242,6 +264,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { return when (member) { is MiniMemberFunDecl -> renderMemberFunDoc(owner, member) is MiniMemberValDecl -> renderMemberValDoc(owner, member) + is MiniInitDecl -> null } } } else { @@ -254,6 +277,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { return when (member) { is MiniMemberFunDecl -> renderMemberFunDoc(owner, member) is MiniMemberValDecl -> renderMemberValDoc(owner, member) + is MiniInitDecl -> null } } } @@ -268,6 +292,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { return when (m) { is MiniMemberFunDecl -> renderMemberFunDoc("String", m) is MiniMemberValDecl -> renderMemberValDoc("String", m) + is MiniInitDecl -> null } } } @@ -277,6 +302,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { return when (member) { is MiniMemberFunDecl -> renderMemberFunDoc(owner, member) is MiniMemberValDecl -> renderMemberValDoc(owner, member) + is MiniInitDecl -> null } } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 4166b8d..e5f80f1 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -2569,7 +2569,21 @@ class Compiler( val pattern = ListLiteralRef(entries) // Register all names in the pattern - pattern.forEachVariable { name -> declareLocalName(name) } + pattern.forEachVariableWithPos { name, namePos -> + declareLocalName(name) + val declRange = MiniRange(namePos, namePos) + val node = MiniValDecl( + range = declRange, + name = name, + mutable = isMutable, + type = null, + initRange = null, + doc = pendingDeclDoc, + nameStart = namePos + ) + miniSink?.onValDecl(node) + } + pendingDeclDoc = null val eqToken = cc.next() if (eqToken.type != Token.Type.ASSIGN) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/CompletionEngineLight.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/CompletionEngineLight.kt index 4928eda..ddc50d4 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/CompletionEngineLight.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/CompletionEngineLight.kt @@ -1,3 +1,20 @@ +/* + * Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + /* * Pure-Kotlin, PSI-free completion engine used for isolated tests and non-IDE harnesses. * Mirrors the IntelliJ MVP logic: MiniAst + BuiltinDocRegistry + lenient imports. @@ -270,6 +287,24 @@ object CompletionEngineLight { break } if (hasDigits) return if (hasDot || hasExp) "Real" else "Int" + + // 3) this@Type or as Type + val identRange = wordRangeAt(text, i + 1) + if (identRange != null) { + val ident = text.substring(identRange.first, identRange.second) + // if it's "as Type", we want Type + var k = prevNonWs(text, identRange.first - 1) + if (k >= 1 && text[k] == 's' && text[k - 1] == 'a' && (k - 1 == 0 || !text[k - 2].isLetterOrDigit())) { + return ident + } + // if it's "this@Type", we want Type + if (k >= 0 && text[k] == '@') { + val k2 = prevNonWs(text, k - 1) + if (k2 >= 3 && text.substring(k2 - 3, k2 + 1) == "this") { + return ident + } + } + } } return null } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInt.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInt.kt index 5c395e8..58fd67d 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInt.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInt.kt @@ -180,6 +180,10 @@ class ObjInt(var value: Long, override val isConst: Boolean = false) : Obj(), Nu LynonType.IntSigned -> ObjInt(decoder.unpackSigned()) else -> scope.raiseIllegalState("illegal type code for Int: $lynonType") } + }.apply { + addFn("toInt") { + thisObj + } } } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt index 0b5a1f8..5bf9529 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt @@ -42,6 +42,14 @@ sealed interface ObjRef { * Used for declaring local variables in destructuring. */ fun forEachVariable(block: (String) -> Unit) {} + + /** + * Calls [block] for each variable name that this reference targets for writing, + * including its source position if available. + */ + fun forEachVariableWithPos(block: (String, Pos) -> Unit) { + forEachVariable { block(it, Pos.UNKNOWN) } + } } /** Runtime-computed read-only reference backed by a lambda. */ @@ -1225,6 +1233,10 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef { override fun forEachVariable(block: (String) -> Unit) { block(name) } + + override fun forEachVariableWithPos(block: (String, Pos) -> Unit) { + block(name, atPos) + } // Per-frame slot cache to avoid repeated name lookups private var cachedFrameId: Long = 0L private var cachedSlot: Int = -1 @@ -1632,6 +1644,15 @@ class ListLiteralRef(private val entries: List) : ObjRef { } } + override fun forEachVariableWithPos(block: (String, Pos) -> Unit) { + for (e in entries) { + when (e) { + is ListEntry.Element -> e.ref.forEachVariableWithPos(block) + is ListEntry.Spread -> e.ref.forEachVariableWithPos(block) + } + } + } + override suspend fun get(scope: Scope): ObjRecord { // Heuristic capacity hint: count element entries; spreads handled opportunistically val elemCount = entries.count { it is ListEntry.Element } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt index 4b8ced0..afdb48e 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/statements.kt @@ -47,7 +47,8 @@ abstract class Statement( override suspend fun compareTo(scope: Scope, other: Obj): Int { if( other == ObjNull || other == ObjVoid ) return 1 - throw UnsupportedOperationException("not comparable") + if( other === this ) return 0 + return -1 } override suspend fun callOn(scope: Scope): Obj { diff --git a/lynglib/src/commonTest/kotlin/OOTest.kt b/lynglib/src/commonTest/kotlin/OOTest.kt index 716d048..0a7fc9b 100644 --- a/lynglib/src/commonTest/kotlin/OOTest.kt +++ b/lynglib/src/commonTest/kotlin/OOTest.kt @@ -16,13 +16,18 @@ */ import kotlinx.coroutines.test.runTest +import net.sergeych.lyng.Script import net.sergeych.lyng.eval +import net.sergeych.lyng.obj.ObjInstance +import net.sergeych.lyng.obj.ObjList import kotlin.test.Test +import kotlin.test.assertEquals class OOTest { @Test fun testClassProps() = runTest { - eval(""" + eval( + """ import lyng.time class Point(x,y) { @@ -35,11 +40,14 @@ class OOTest { assertEquals(Point(0,0), Point.origin) assertEquals(Point(1,2), Point.center) - """.trimIndent()) + """.trimIndent() + ) } + @Test fun testClassMethods() = runTest { - eval(""" + eval( + """ import lyng.time class Point(x,y) { @@ -58,12 +66,14 @@ class OOTest { assertEquals(null, Point.getData() ) Point.setData("foo") assertEquals( "foo!", Point.getData() ) - """.trimIndent()) + """.trimIndent() + ) } @Test fun testDynamicGet() = runTest { - eval(""" + eval( + """ val accessor = dynamic { get { name -> if( name == "foo" ) "bar" else null @@ -74,12 +84,14 @@ class OOTest { assertEquals("bar", accessor.foo) assertEquals(null, accessor.bar) - """.trimIndent()) + """.trimIndent() + ) } @Test fun testDelegateSet() = runTest { - eval(""" + eval( + """ var setValueForBar = null val accessor = dynamic { get { name -> @@ -104,12 +116,14 @@ class OOTest { assertThrows { accessor.bad = "!23" } - """.trimIndent()) + """.trimIndent() + ) } @Test fun testDynamicIndexAccess() = runTest { - eval(""" + eval( + """ val store = Map() val accessor = dynamic { get { name -> @@ -124,23 +138,27 @@ class OOTest { accessor["foo"] = "bar" assertEquals("bar", accessor["foo"]) assertEquals("bar", accessor.foo) - """.trimIndent()) + """.trimIndent() + ) } @Test fun testMultilineConstructor() = runTest { - eval(""" + eval( + """ class Point( x, y ) assertEquals(Point(1,2), Point(1,2) ) - """.trimIndent()) + """.trimIndent() + ) } @Test fun testDynamicClass() = runTest { - eval(""" + eval( + """ fun getContract(contractName) { dynamic { @@ -150,14 +168,16 @@ class OOTest { } } getContract("foo").bar - """) + """ + ) } @Test fun testDynamicClassReturn2() = runTest { // todo: should work without extra parenthesis // see below - eval(""" + eval( + """ fun getContract(contractName) { println("1") @@ -184,12 +204,14 @@ class OOTest { assertEquals(6, x(1,2,3)) // v HERE v assertEquals(15, cc.foo.bar(10,2,3)) - """) + """ + ) } @Test fun testClassInitialization() = runTest { - eval(""" + eval( + """ var countInstances = 0 class Point(val x: Int, val y: Int) { println("Class initializer is called 1") @@ -210,12 +232,14 @@ class OOTest { assertEquals(1, countInstances) assertEquals(p, Point(1,2) ) assertEquals(2, countInstances) - """.trimIndent()) + """.trimIndent() + ) } @Test fun testMIInitialization() = runTest { - eval(""" + eval( + """ var order = [] class A { init { order.add("A") } @@ -231,12 +255,14 @@ class OOTest { } D() assertEquals(["A", "B", "C", "D"], order) - """) + """ + ) } @Test fun testMIDiamondInitialization() = runTest { - eval(""" + eval( + """ var order = [] class A { init { order.add("A") } @@ -252,12 +278,14 @@ class OOTest { } D() assertEquals(["A", "B", "C", "D"], order) - """) + """ + ) } @Test fun testInitBlockInDeserialization() = runTest { - eval(""" + eval( + """ import lyng.serialization var count = 0 class A { @@ -267,6 +295,52 @@ class OOTest { val coded = Lynon.encode(a1) val a2 = Lynon.decode(coded) assertEquals(2, count) - """) + """ + ) + } + + @Test + fun testDefaultCompare() = runTest { + eval( + """ + class Point(val x: Int, val y: Int) + + assertEquals(Point(1,2), Point(1,2) ) + assert( Point(1,2) != Point(2,1) ) + assert( Point(1,2) == Point(1,2) ) + + """.trimIndent() + ) + } + + @Test + fun testConstructorCallsWithNamedParams() = runTest { + val scope = Script.newScope() + val list = scope.eval( + """ + import lyng.time + + class BarRequest( + id, + vaultId, userAddress, isDepositRequest, grossWeight, fineness, notes="", + createdAt = Instant.now().truncateToSecond(), + updatedAt = Instant.now().truncateToSecond() + ) { + // unrelated for comparison + static val stateNames = [1, 2, 3] + + val cell = cached { Cell[id] } + } + assertEquals( 5,5.toInt()) + val b1 = BarRequest(1, "v1", "u1", true, 1000, 999) + val b2 = BarRequest(1, "v1", "u1", true, 1000, 999, createdAt: b1.createdAt, updatedAt: b1.updatedAt) + assertEquals(b1, b2) + assertEquals( 0, b1 <=> b2) + [b1, b2] + """.trimIndent() + ) as ObjList + val b1 = list.list[0] as ObjInstance + val b2 = list.list[1] as ObjInstance + assertEquals(0, b1.compareTo(scope, b2)) } } \ No newline at end of file