lynglib: added MiniAST support

lyngweb: better syntax highlighting and code editor
This commit is contained in:
sergeych committed 2025-11-24 00:38:17 +01:00
1 parent 28b961d339
commit ea0ecb1db3
24 files changed
+3095 -218

No files matched your search

@@ -0,0 +1,132 @@
/*
* 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.
*
*/
/*
* Mini binding tests for highlighting/editor features
*/
package net.sergeych.lyng
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.binding.Binder
import net.sergeych.lyng.miniast.MiniAstBuilder
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class BindingTest {
private suspend fun bind(code: String): net.sergeych.lyng.binding.BindingSnapshot {
val src = code.trimIndent()
val sink = MiniAstBuilder()
Compiler.compileWithMini(src, sink)
val mini = sink.build()
assertNotNull(mini, "MiniScript should be built")
return Binder.bind(src, mini!!)
}
@Test
fun binds_params_and_locals() = runTest {
val snap = bind(
"""
fun f(a:Int){
val x = 1
a + x
}
"""
)
// Expect at least one Param symbol "a" and one Val symbol "x"
val aIds = snap.symbols.filter { it.name == "a" }.map { it.id }
val xIds = snap.symbols.filter { it.name == "x" }.map { it.id }
assertTrue(aIds.isNotEmpty())
assertTrue(xIds.isNotEmpty())
// Both should have at least one reference across any symbol with that name
val aRefs = snap.references.count { it.symbolId in aIds }
val xRefs = snap.references.count { it.symbolId in xIds }
assertEquals(1, aRefs)
assertEquals(1, xRefs)
}
@Test
fun binds_top_level_val_usage() = runTest {
val snap = bind(
"""
val x = 1
x + 1
"""
)
val xSym = snap.symbols.firstOrNull { it.name == "x" }
assertNotNull(xSym)
// One reference usage to top-level x
val refs = snap.references.filter { it.symbolId == xSym!!.id }
assertEquals(1, refs.size)
}
@Test
fun shadowing_scopes() = runTest {
val snap = bind(
"""
val x = 1
fun f(){
val x = 2
x
}
"""
)
val allX = snap.symbols.filter { it.name == "x" }
// Expect at least two x symbols (top-level and local)
assertEquals(true, allX.size >= 2)
// The single reference inside f body should bind to the inner x (containerId != null)
val localXs = allX.filter { it.containerId != null }
assertEquals(true, localXs.isNotEmpty())
val localX = localXs.maxBy { it.declStart }
val refsToLocal = snap.references.count { it.symbolId == localX.id }
assertEquals(1, refsToLocal)
}
@Test
fun class_fields_basic() = runTest {
val snap = bind(
"""
class C {
val foo = 1
fun bar(){ foo }
}
"""
)
val fooField = snap.symbols.firstOrNull { it.name == "foo" }
assertNotNull(fooField)
// Should have at least one reference (usage in bar)
val refs = snap.references.count { it.symbolId == fooField!!.id }
assertEquals(1, refs)
}
@Test
fun ctor_params_as_fields() = runTest {
val snap = bind(
"""
class C(val x:Int){
fun f(){ x }
}
"""
)
val xField = snap.symbols.firstOrNull { it.name == "x" }
assertNotNull(xField)
val refs = snap.references.count { it.symbolId == xField!!.id }
assertEquals(1, refs)
}
}
@@ -0,0 +1,134 @@
/*
* 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.
*
*/
/*
* Mini-AST capture tests
*/
package net.sergeych.lyng
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.miniast.*
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class MiniAstTest {
private suspend fun compileWithMini(code: String): Pair<Script, net.sergeych.lyng.miniast.MiniAstBuilder> {
val sink = MiniAstBuilder()
val script = Compiler.compileWithMini(code.trimIndent(), sink)
return script to sink
}
@Test
fun miniAst_captures_import_segments() = runTest {
val code = """
import lyng.stdlib
val x = 1
"""
val (_, sink) = compileWithMini(code)
val mini = sink.build()
assertNotNull(mini)
val imps = mini!!.imports
assertTrue(imps.isNotEmpty(), "imports should be captured")
val first = imps.first()
val segNames = first.segments.map { it.name }
assertEquals(listOf("lyng", "stdlib"), segNames)
// Ensure ranges are valid and ordered
for (seg in first.segments) {
assertTrue(seg.range.start.line == first.range.start.line)
assertTrue(seg.range.start.column <= seg.range.end.column)
}
}
@Test
fun miniAst_captures_fun_docs_and_types() = runTest {
val code = """
// Summary: does foo
// details can be here
fun foo(a: Int, b: pkg.String?): Boolean {
true
}
"""
val (_, sink) = compileWithMini(code)
val mini = sink.build()
assertNotNull(mini)
val fn = mini!!.declarations.filterIsInstance<MiniFunDecl>().firstOrNull { it.name == "foo" }
assertNotNull(fn, "function decl should be captured")
// Doc
assertNotNull(fn.doc)
assertEquals("Summary: does foo", fn.doc!!.summary)
assertTrue(fn.doc!!.raw.contains("details"))
// Params
assertEquals(2, fn.params.size)
val p1 = fn.params[0]
val p2 = fn.params[1]
val t1 = p1.type as MiniTypeName
assertEquals(listOf("Int"), t1.segments.map { it.name })
assertEquals(false, t1.nullable)
val t2 = p2.type as MiniTypeName
assertEquals(listOf("pkg", "String"), t2.segments.map { it.name })
assertEquals(true, t2.nullable)
// Return type
val rt = fn.returnType as MiniTypeName
assertEquals(listOf("Boolean"), rt.segments.map { it.name })
assertEquals(false, rt.nullable)
}
@Test
fun miniAst_captures_val_type_and_doc() = runTest {
val code = """
// docs for x
val x: List<String> = ["a", "b"]
"""
val (_, sink) = compileWithMini(code)
val mini = sink.build()
assertNotNull(mini)
val vd = mini!!.declarations.filterIsInstance<net.sergeych.lyng.miniast.MiniValDecl>().firstOrNull { it.name == "x" }
assertNotNull(vd)
assertNotNull(vd.doc)
assertEquals("docs for x", vd.doc!!.summary)
val ty = vd.type
assertNotNull(ty)
val gen = ty as MiniGenericType
val base = gen.base as MiniTypeName
assertEquals(listOf("List"), base.segments.map { it.name })
assertEquals(1, gen.args.size)
val arg0 = gen.args[0] as MiniTypeName
assertEquals(listOf("String"), arg0.segments.map { it.name })
assertEquals(false, gen.nullable)
assertNotNull(vd.initRange)
}
@Test
fun miniAst_captures_class_bases_and_doc() = runTest {
val code = """
/** Class C docs */
class C: Base1, Base2 {}
"""
val (_, sink) = compileWithMini(code)
val mini = sink.build()
assertNotNull(mini)
val cd = mini!!.declarations.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == "C" }
assertNotNull(cd)
assertNotNull(cd.doc)
assertTrue(cd.doc!!.raw.contains("Class C docs"))
// Bases captured as plain names for now
assertEquals(listOf("Base1", "Base2"), cd.bases)
}
}
@@ -3544,4 +3544,25 @@ class ScriptTest {
""".trimIndent())
}
// @Ignore
// @Test
// fun interpolationTest() = runTest {
// eval($$$"""
//
// val foo = "bar"
// val buzz = ["foo", "bar"]
//
// // 1. simple interpolation
// assertEquals( "bar", "$foo" )
// assertEquals( "bar", "${foo}" )
//
// // 2. escaping the dollar sign
// assertEquals( "$", "\$foo"[0] )
// assertEquals( "foo, "\$foo"[1..] )
//
// // 3. interpolation with expression
// assertEquals( "foo!. bar?", "${buzz[0]+"!"}. ${buzz[1]+"?"}" )
// """.trimIndent())
// }
}