Add MiniAst documentation system and Markdown rendering for built-in and IDE documentation.

This commit is contained in:
Sergey Chernov 2025-12-02 03:04:02 +01:00
parent c52e132dcc
commit 067970b80c
12 changed files with 1411 additions and 53 deletions

View File

@ -37,6 +37,8 @@ repositories {
dependencies {
implementation(project(":lynglib"))
// Rich Markdown renderer for Quick Docs
implementation("com.vladsch.flexmark:flexmark-all:0.64.8")
}
intellij {

View File

@ -39,6 +39,8 @@ import net.sergeych.lyng.miniast.*
*/
class LyngDocumentationProvider : AbstractDocumentationProvider() {
private val log = Logger.getInstance(LyngDocumentationProvider::class.java)
// Toggle to trace inheritance-based resolutions in Quick Docs. Keep false for normal use.
private val DEBUG_INHERITANCE = false
override fun generateDoc(element: PsiElement?, originalElement: PsiElement?): String? {
if (element == null) return null
val file: PsiFile = element.containingFile ?: return null
@ -57,10 +59,10 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
// Build MiniAst for this file (fast and resilient). Best-effort; on failure return null.
val sink = MiniAstBuilder()
// Use lenient import provider so unresolved imports (e.g., lyng.io.fs) don't break docs
val provider = IdeLenientImportProvider.create()
try {
// Use lenient import provider so unresolved imports (e.g., lyng.io.fs) don't break docs
val src = Source("<ide>", text)
val provider = IdeLenientImportProvider.create()
runBlocking { Compiler.compileWithMini(src, provider, sink) }
} catch (t: Throwable) {
log.warn("[LYNG_DEBUG] QuickDoc: compileWithMini failed: ${t.message}")
@ -96,6 +98,71 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
return renderDeclDoc(it)
}
// 4) Consult BuiltinDocRegistry for imported modules (top-level and class members)
var importedModules = mini.imports.map { it.segments.joinToString(".") { s -> s.name } }
// Core-module fallback: in scratch/repl-like files without imports, consult stdlib by default
if (importedModules.isEmpty()) importedModules = listOf("lyng.stdlib")
// 4a) try top-level decls
for (mod in importedModules) {
val docs = BuiltinDocRegistry.docsForModule(mod)
val matches = docs.filterIsInstance<MiniFunDecl>().filter { it.name == ident }
if (matches.isNotEmpty()) {
// Prefer overload by arity when caret is in a call position; otherwise show first
val arity = callArity(text, idRange.endOffset)
val chosen = arity?.let { a -> matches.firstOrNull { it.params.size == a } } ?: matches.first()
// If multiple and none matched arity, consider showing an overloads list
if (arity != null && chosen.params.size != arity && matches.size > 1) {
return renderOverloads(ident, matches)
}
return renderDeclDoc(chosen)
}
// Also allow values/consts
docs.filterIsInstance<MiniValDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it) }
// And classes
docs.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it) }
}
// 4b) try class members like ClassName.member with inheritance fallback
val lhs = previousWordBefore(text, idRange.startOffset)
if (lhs != null && hasDotBetween(text, lhs.endOffset, idRange.startOffset)) {
val className = text.substring(lhs.startOffset, lhs.endOffset)
resolveMemberWithInheritance(importedModules, className, ident)?.let { (owner, member) ->
if (DEBUG_INHERITANCE) log.info("[LYNG_DEBUG] Inheritance resolved $className.$ident to $owner.${member.name}")
return when (member) {
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
}
}
} else {
// Heuristics when LHS is not an identifier (literals or call results):
// - List literal like [..].member → assume class List
// - Otherwise, try to find a unique class across imported modules that defines this member
val dotPos = findDotLeft(text, idRange.startOffset)
if (dotPos != null) {
val guessed = when {
looksLikeListLiteralBefore(text, dotPos) -> "List"
else -> null
}
if (guessed != null) {
resolveMemberWithInheritance(importedModules, guessed, ident)?.let { (owner, member) ->
if (DEBUG_INHERITANCE) log.info("[LYNG_DEBUG] Heuristic '$guessed.$ident' resolved via inheritance to $owner.${member.name}")
return when (member) {
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
}
}
} else {
// Search across classes; prefer Iterable, then Iterator, then List for common ops
findMemberAcrossClasses(importedModules, ident)?.let { (owner, member) ->
if (DEBUG_INHERITANCE) log.info("[LYNG_DEBUG] Cross-class '$ident' resolved to $owner.${member.name}")
return when (member) {
is MiniMemberFunDecl -> renderMemberFunDoc(owner, member)
is MiniMemberValDecl -> renderMemberValDoc(owner, member)
}
}
}
}
}
log.info("[LYNG_DEBUG] QuickDoc: nothing found for ident='$ident'")
return null
}
@ -119,10 +186,11 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
else -> d.name
}
// Show full detailed documentation, not just the summary
val doc = d.doc?.raw?.let { htmlEscape(it).replace("\n", "<br/>") }
val raw = d.doc?.raw
val doc: String? = if (raw.isNullOrBlank()) null else MarkdownRenderer.render(raw)
val sb = StringBuilder()
sb.append("<div class='doc-title'>").append(htmlEscape(title)).append("</div>")
if (!doc.isNullOrBlank()) sb.append("<div class='doc-body'>").append(doc).append("</div>")
if (!doc.isNullOrBlank()) sb.append(styledMarkdown(doc!!))
return sb.toString()
}
@ -131,11 +199,44 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
return "<div class='doc-title'>${htmlEscape(title)}</div>"
}
private fun renderMemberFunDoc(className: String, m: MiniMemberFunDecl): String {
val params = m.params.joinToString(", ") { p ->
val ts = typeOf(p.type)
if (ts.isNotBlank()) "${p.name}${ts}" else p.name
}
val ret = typeOf(m.returnType)
val staticStr = if (m.isStatic) "static " else ""
val title = "${staticStr}method $className.${m.name}(${params})${ret}"
val raw = m.doc?.raw
val doc: String? = if (raw.isNullOrBlank()) null else MarkdownRenderer.render(raw)
val sb = StringBuilder()
sb.append("<div class='doc-title'>").append(htmlEscape(title)).append("</div>")
if (!doc.isNullOrBlank()) sb.append(styledMarkdown(doc!!))
return sb.toString()
}
private fun renderMemberValDoc(className: String, m: MiniMemberValDecl): String {
val ts = typeOf(m.type)
val kind = if (m.mutable) "var" else "val"
val staticStr = if (m.isStatic) "static " else ""
val title = "${staticStr}${kind} $className.${m.name}${ts}"
val raw = m.doc?.raw
val doc: String? = if (raw.isNullOrBlank()) null else MarkdownRenderer.render(raw)
val sb = StringBuilder()
sb.append("<div class='doc-title'>").append(htmlEscape(title)).append("</div>")
if (!doc.isNullOrBlank()) sb.append(styledMarkdown(doc!!))
return sb.toString()
}
private fun typeOf(t: MiniTypeRef?): String = when (t) {
is MiniTypeName -> ": ${t.segments.joinToString(".") { it.name }}"
is MiniGenericType -> ": ${typeOf(t.base).removePrefix(": ")}<${t.args.joinToString(", ") { typeOf(it).removePrefix(": ") }}>"
is MiniFunctionType -> ": (..) -> .."
is MiniTypeVar -> ": ${t.name}"
is MiniTypeName -> ": ${t.segments.joinToString(".") { it.name }}${if (t.nullable) "?" else ""}"
is MiniGenericType -> {
val base = typeOf(t.base).removePrefix(": ")
val args = t.args.joinToString(", ") { typeOf(it).removePrefix(": ") }
": ${base}<${args}>${if (t.nullable) "?" else ""}"
}
is MiniFunctionType -> ": (..) -> ..${if (t.nullable) "?" else ""}"
is MiniTypeVar -> ": ${t.name}${if (t.nullable) "?" else ""}"
null -> ""
}
@ -160,6 +261,73 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
)
}
private fun styledMarkdown(html: String): String {
// IntelliJ doc renderer sanitizes and may surface <style> content as text.
// Strip any style tags defensively and keep markup lean; rely on platform defaults.
val safe = stripStyleTags(html)
return """
<div class="lyng-doc-md" style="max-width:72ch; line-height:1.4; font-size:0.96em;">
$safe
</div>
""".trimIndent()
}
private fun stripStyleTags(src: String): String {
// Remove any <style>...</style> blocks (case-insensitive, dotall)
val styleRegex = Regex("(?is)<style[^>]*>.*?</style>")
return src.replace(styleRegex, "")
}
// --- Simple helpers to support overload selection and heuristics ---
/**
* If identifier at [rightAfterIdent] is followed by a call like `(a, b)`,
* return the argument count; otherwise return null. Nested parentheses are
* handled conservatively to skip commas inside lambdas/parentheses.
*/
private fun callArity(text: String, rightAfterIdent: Int): Int? {
var i = rightAfterIdent
// Skip whitespace
while (i < text.length && text[i].isWhitespace()) i++
if (i >= text.length || text[i] != '(') return null
i++
var depth = 0
var commas = 0
var hasToken = false
while (i < text.length) {
val ch = text[i]
when (ch) {
'(' -> { depth++; hasToken = true }
')' -> {
if (depth == 0) {
// Empty parentheses => arity 0 if no token and no commas
if (!hasToken && commas == 0) return 0
return commas + 1
} else depth--
}
',' -> if (depth == 0) { commas++; hasToken = false }
'\n' -> {}
else -> if (!ch.isWhitespace()) hasToken = true
}
i++
}
return null
}
private fun renderOverloads(name: String, overloads: List<MiniFunDecl>): String {
val sb = StringBuilder()
sb.append("<div class='doc-title'>Overloads for ").append(htmlEscape(name)).append("</div>")
sb.append("<ul>")
overloads.forEach { fn ->
sb.append("<li><code>")
.append(htmlEscape("fun ${fn.name}${signatureOf(fn)}"))
.append("</code>")
fn.doc?.summary?.let { sum -> sb.append("").append(htmlEscape(sum)) }
sb.append("</li>")
}
sb.append("</ul>")
return sb.toString()
}
private fun wordRangeAt(text: String, offset: Int): TextRange? {
if (text.isEmpty()) return null
var s = offset.coerceIn(0, text.length)
@ -169,5 +337,100 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
return if (e > s) TextRange(s, e) else null
}
private fun previousWordBefore(text: String, offset: Int): TextRange? {
// skip spaces and dots to the left, but stop after hitting a non-identifier or dot boundary
var i = (offset - 1).coerceAtLeast(0)
// first, move left past spaces
while (i > 0 && text[i].isWhitespace()) i--
// remember position to check for dot between words
val end = i + 1
// now find the start of the identifier
while (i >= 0 && isIdentChar(text[i])) i--
val start = (i + 1)
return if (start < end && start >= 0) TextRange(start, end) else null
}
private fun hasDotBetween(text: String, leftEnd: Int, rightStart: Int): Boolean {
val s = leftEnd.coerceAtLeast(0)
val e = rightStart.coerceAtMost(text.length)
if (e <= s) return false
for (i in s until e) if (text[i] == '.') return true
return false
}
private fun isIdentChar(c: Char): Boolean = c == '_' || c.isLetterOrDigit()
// --- Helpers for inheritance-aware and heuristic member lookup ---
private fun aggregateClasses(importedModules: List<String>): Map<String, MiniClassDecl> {
val map = LinkedHashMap<String, MiniClassDecl>()
for (mod in importedModules) {
val docs = BuiltinDocRegistry.docsForModule(mod)
docs.filterIsInstance<MiniClassDecl>().forEach { cls ->
// Prefer the first occurrence; allow later duplicates to be ignored
map.putIfAbsent(cls.name, cls)
}
}
return map
}
private fun resolveMemberWithInheritance(importedModules: List<String>, className: String, member: String): Pair<String, MiniMemberDecl>? {
val classes = aggregateClasses(importedModules)
fun dfs(name: String, visited: MutableSet<String>): Pair<String, MiniMemberDecl>? {
val cls = classes[name] ?: return null
cls.members.firstOrNull { it.name == member }?.let { return name to it }
if (!visited.add(name)) return null
for (baseName in cls.bases) {
dfs(baseName, visited)?.let { return it }
}
return null
}
return dfs(className, mutableSetOf())
}
private fun findMemberAcrossClasses(importedModules: List<String>, member: String): Pair<String, MiniMemberDecl>? {
val classes = aggregateClasses(importedModules)
// Preferred order for ambiguous common ops
val preference = listOf("Iterable", "Iterator", "List")
// First, try preference order
for (name in preference) {
resolveMemberWithInheritance(importedModules, name, member)?.let { return it }
}
// Then, scan all
for ((name, cls) in classes) {
cls.members.firstOrNull { it.name == member }?.let { return name to it }
}
return null
}
private fun findDotLeft(text: String, rightStart: Int): Int? {
var i = (rightStart - 1).coerceAtLeast(0)
while (i >= 0 && text[i].isWhitespace()) i--
while (i >= 0) {
val ch = text[i]
if (ch == '.') return i
if (ch == '\n') return null
i--
}
return null
}
private fun looksLikeListLiteralBefore(text: String, dotPos: Int): Boolean {
// Look left for a closing ']' possibly with spaces, then a matching '[' before a comma or assignment
var i = (dotPos - 1).coerceAtLeast(0)
while (i >= 0 && text[i].isWhitespace()) i--
if (i < 0 || text[i] != ']') return false
var depth = 0
i--
while (i >= 0) {
val ch = text[i]
when (ch) {
']' -> depth++
'[' -> if (depth == 0) return true else depth--
'\n' -> return false
}
i--
}
return false
}
}

View File

@ -0,0 +1,64 @@
/*
* 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.
*
*/
package net.sergeych.lyng.idea.docs
import com.vladsch.flexmark.ext.autolink.AutolinkExtension
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension
import com.vladsch.flexmark.ext.tables.TablesExtension
import com.vladsch.flexmark.html.HtmlRenderer
import com.vladsch.flexmark.parser.Parser
import com.vladsch.flexmark.util.data.MutableDataSet
/**
* Rich Markdown renderer for the IDEA Quick Docs using Flexmark.
*
* - Supports fenced code blocks (with language class "language-xyz")
* - Autolinks, tables, strikethrough
* - Converts soft breaks to <br/>
* - Tiny in-memory cache to avoid repeated parsing of the same doc blocks
*/
object MarkdownRenderer {
private val options = MutableDataSet().apply {
set(Parser.EXTENSIONS, listOf(
AutolinkExtension.create(),
TablesExtension.create(),
StrikethroughExtension.create(),
))
// Add CSS class for code fences like ```lyng → class="language-lyng"
set(HtmlRenderer.FENCED_CODE_LANGUAGE_CLASS_PREFIX, "language-")
// Treat single newlines as a space (soft break) so consecutive lines remain one paragraph.
// Real paragraph breaks require an empty line, hard breaks still work via Markdown (two spaces + \n).
set(HtmlRenderer.SOFT_BREAK, " ")
}
private val parser: Parser = Parser.builder(options).build()
private val renderer: HtmlRenderer = HtmlRenderer.builder(options).build()
private val cache = object : LinkedHashMap<String, String>(256, 0.75f, true) {
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, String>?): Boolean = size > 256
}
fun render(markdown: String): String {
// Fast path: cache
synchronized(cache) { cache[markdown]?.let { return it } }
val node = parser.parse(markdown)
val html = renderer.render(node)
synchronized(cache) { cache[markdown] = html }
return html
}
}

View File

@ -23,6 +23,7 @@ package net.sergeych.lyng.io.fs
import net.sergeych.lyng.ModuleScope
import net.sergeych.lyng.Scope
import net.sergeych.lyng.miniast.*
import net.sergeych.lyng.obj.*
import net.sergeych.lyng.pacman.ImportManager
import net.sergeych.lyngio.fs.LyngFS
@ -73,43 +74,79 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) {
return ObjPath(this, secured, str.toPath())
}
}.apply {
addFn("name") {
addFnDoc(
name = "name",
doc = "Base name of the path (last segment).",
returns = type("lyng.String"),
moduleName = module.packageName
) {
val self = thisAs<ObjPath>()
self.path.name.toObj()
}
addFn("parent") {
addFnDoc(
name = "parent",
doc = "Parent directory as a Path or null if none.",
returns = type("Path", nullable = true),
moduleName = module.packageName
) {
val self = thisAs<ObjPath>()
self.path.parent?.let {
ObjPath( this@apply, self.secured, it)
} ?: ObjNull
}
addFn("segments") {
addFnDoc(
name = "segments",
doc = "List of path segments.",
// returns: List<String>
returns = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.String"))),
moduleName = module.packageName
) {
val self = thisAs<ObjPath>()
ObjList(self.path.segments.map { ObjString(it) }.toMutableList())
}
// exists(): Bool
addFn("exists") {
addFnDoc(
name = "exists",
doc = "Check whether this path exists.",
returns = type("lyng.Bool"),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
(self.secured.exists(self.path)).toObj()
}
}
// isFile(): Bool — cached metadata
addFn("isFile") {
addFnDoc(
name = "isFile",
doc = "True if this path is a regular file (based on cached metadata).",
returns = type("lyng.Bool"),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
self.ensureMetadata().let { ObjBool(it.isRegularFile) }
}
}
// isDirectory(): Bool — cached metadata
addFn("isDirectory") {
addFnDoc(
name = "isDirectory",
doc = "True if this path is a directory (based on cached metadata).",
returns = type("lyng.Bool"),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
self.ensureMetadata().let { ObjBool(it.isDirectory) }
}
}
// size(): Int? — null when unavailable
addFn("size") {
addFnDoc(
name = "size",
doc = "File size in bytes, or null when unavailable.",
returns = type("lyng.Int", nullable = true),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val m = self.ensureMetadata()
@ -117,7 +154,12 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) {
}
}
// createdAt(): Instant? — Lyng Instant, null when unavailable
addFn("createdAt") {
addFnDoc(
name = "createdAt",
doc = "Creation time as `Instant`, or null when unavailable.",
returns = type("lyng.Instant", nullable = true),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val m = self.ensureMetadata()
@ -125,7 +167,12 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) {
}
}
// createdAtMillis(): Int? — milliseconds since epoch or null
addFn("createdAtMillis") {
addFnDoc(
name = "createdAtMillis",
doc = "Creation time in milliseconds since epoch, or null when unavailable.",
returns = type("lyng.Int", nullable = true),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val m = self.ensureMetadata()
@ -133,7 +180,12 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) {
}
}
// modifiedAt(): Instant? — Lyng Instant, null when unavailable
addFn("modifiedAt") {
addFnDoc(
name = "modifiedAt",
doc = "Last modification time as `Instant`, or null when unavailable.",
returns = type("lyng.Instant", nullable = true),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val m = self.ensureMetadata()
@ -141,7 +193,12 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) {
}
}
// modifiedAtMillis(): Int? — milliseconds since epoch or null
addFn("modifiedAtMillis") {
addFnDoc(
name = "modifiedAtMillis",
doc = "Last modification time in milliseconds since epoch, or null when unavailable.",
returns = type("lyng.Int", nullable = true),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val m = self.ensureMetadata()
@ -149,7 +206,12 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) {
}
}
// list(): List<Path>
addFn("list") {
addFnDoc(
name = "list",
doc = "List directory entries as `Path` objects.",
returns = TypeGenericDoc(type("lyng.List"), listOf(type("Path"))),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val items = self.secured.list(self.path).map { ObjPath(self.objClass, self.secured, it) }
@ -157,7 +219,12 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) {
}
}
// readBytes(): Buffer
addFn("readBytes") {
addFnDoc(
name = "readBytes",
doc = "Read the file into a binary buffer.",
returns = type("lyng.Buffer"),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val bytes = self.secured.readBytes(self.path)
@ -165,7 +232,12 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) {
}
}
// writeBytes(bytes: Buffer)
addFn("writeBytes") {
addFnDoc(
name = "writeBytes",
doc = "Write a binary buffer to the file, replacing content.",
params = listOf(ParamDoc("bytes", type("lyng.Buffer"))),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val buf = requiredArg<ObjBuffer>(0)
@ -174,7 +246,12 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) {
}
}
// appendBytes(bytes: Buffer)
addFn("appendBytes") {
addFnDoc(
name = "appendBytes",
doc = "Append a binary buffer to the end of the file.",
params = listOf(ParamDoc("bytes", type("lyng.Buffer"))),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val buf = requiredArg<ObjBuffer>(0)
@ -183,14 +260,24 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) {
}
}
// readUtf8(): String
addFn("readUtf8") {
addFnDoc(
name = "readUtf8",
doc = "Read the file as a UTF-8 string.",
returns = type("lyng.String"),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
self.secured.readUtf8(self.path).toObj()
}
}
// writeUtf8(text: String)
addFn("writeUtf8") {
addFnDoc(
name = "writeUtf8",
doc = "Write a UTF-8 string to the file, replacing content.",
params = listOf(ParamDoc("text", type("lyng.String"))),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val text = requireOnlyArg<ObjString>().value
@ -199,7 +286,12 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) {
}
}
// appendUtf8(text: String)
addFn("appendUtf8") {
addFnDoc(
name = "appendUtf8",
doc = "Append UTF-8 text to the end of the file.",
params = listOf(ParamDoc("text", type("lyng.String"))),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val text = requireOnlyArg<ObjString>().value
@ -208,7 +300,12 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) {
}
}
// metadata(): Map
addFn("metadata") {
addFnDoc(
name = "metadata",
doc = "Fetch cached metadata as a map of fields: `isFile`, `isDirectory`, `size`, `createdAtMillis`, `modifiedAtMillis`, `isSymlink`.",
returns = TypeGenericDoc(type("lyng.Map"), listOf(type("lyng.String"), type("lyng.Any"))),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val m = self.secured.metadata(self.path)
@ -223,7 +320,12 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) {
}
}
// mkdirs(mustCreate: Bool=false)
addFn("mkdirs") {
addFnDoc(
name = "mkdirs",
doc = "Create directories (like `mkdir -p`). Optional `mustCreate` enforces error if target exists.",
params = listOf(ParamDoc("mustCreate", type("lyng.Bool"))),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val mustCreate = args.list.getOrNull(0)?.toBool() ?: false
@ -232,7 +334,13 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) {
}
}
// move(to: Path|String, overwrite: Bool=false)
addFn("move") {
addFnDoc(
name = "move",
doc = "Move this path to a new location. `to` may be a `Path` or `String`. Use `overwrite` to replace existing target.",
// types vary; keep generic description in doc
params = listOf(ParamDoc("to"), ParamDoc("overwrite", type("lyng.Bool"))),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val toPath = parsePathArg(this, self, requiredArg<Obj>(0))
@ -242,7 +350,12 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) {
}
}
// delete(mustExist: Bool=false, recursively: Bool=false)
addFn("delete") {
addFnDoc(
name = "delete",
doc = "Delete this path. Optional flags: `mustExist` and `recursively`.",
params = listOf(ParamDoc("mustExist", type("lyng.Bool")), ParamDoc("recursively", type("lyng.Bool"))),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val mustExist = args.list.getOrNull(0)?.toBool() ?: false
@ -252,7 +365,12 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) {
}
}
// copy(to: Path|String, overwrite: Bool=false)
addFn("copy") {
addFnDoc(
name = "copy",
doc = "Copy this path to a new location. `to` may be a `Path` or `String`. Use `overwrite` to replace existing target.",
params = listOf(ParamDoc("to"), ParamDoc("overwrite", type("lyng.Bool"))),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val toPath = parsePathArg(this, self, requiredArg<Obj>(0))
@ -262,7 +380,13 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) {
}
}
// glob(pattern: String): List<Path>
addFn("glob") {
addFnDoc(
name = "glob",
doc = "List entries matching a glob pattern (no recursion).",
params = listOf(ParamDoc("pattern", type("lyng.String"))),
returns = TypeGenericDoc(type("lyng.List"), listOf(type("Path"))),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val pattern = requireOnlyArg<ObjString>().value
@ -274,7 +398,13 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) {
// --- streaming readers (initial version: chunk from whole content, API stable) ---
// readChunks(size: Int = 65536) -> Iterator<Buffer>
addFn("readChunks") {
addFnDoc(
name = "readChunks",
doc = "Read file in fixed-size chunks as an iterator of `Buffer`.",
params = listOf(ParamDoc("size", type("lyng.Int"))),
returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.Buffer"))),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val size = args.list.getOrNull(0)?.toInt() ?: 65536
@ -284,7 +414,13 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) {
}
// readUtf8Chunks(size: Int = 65536) -> Iterator<String>
addFn("readUtf8Chunks") {
addFnDoc(
name = "readUtf8Chunks",
doc = "Read UTF-8 text in fixed-size chunks as an iterator of `String`.",
params = listOf(ParamDoc("size", type("lyng.Int"))),
returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.String"))),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val size = args.list.getOrNull(0)?.toInt() ?: 65536
@ -294,7 +430,12 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) {
}
// lines() -> Iterator<String>, implemented via readUtf8Chunks
addFn("lines") {
addFnDoc(
name = "lines",
doc = "Iterate lines of the file as `String` values.",
returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.String"))),
moduleName = module.packageName
) {
fsGuard {
val chunkIt = thisObj.invokeInstanceMethod(this, "readUtf8Chunks")
ObjFsLinesIterator(chunkIt)
@ -302,10 +443,22 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) {
}
}
// Export into the module scope
module.addConst("Path", pathType)
// Export into the module scope with docs
module.addConstDoc(
name = "Path",
value = pathType,
doc = "Filesystem path class. Construct with a string: `Path(\"/tmp\")`.",
type = type("Path"),
moduleName = module.packageName
)
// Alias as requested (Path(s) style)
module.addConst("Paths", pathType)
module.addConstDoc(
name = "Paths",
value = pathType,
doc = "Alias of `Path` for those who prefer plural form.",
type = type("Path"),
moduleName = module.packageName
)
}
// --- Helper classes and utilities ---
@ -361,12 +514,27 @@ class ObjFsBytesIterator(
val BytesIteratorType = object : ObjClass("BytesIterator", ObjIterator) {
init {
// make it usable in for-loops
addFn("iterator") { thisObj }
addFn("hasNext") {
addFnDoc(
name = "iterator",
doc = "Return this iterator instance (enables `for` loops).",
returns = type("BytesIterator"),
moduleName = "lyng.io.fs"
) { thisObj }
addFnDoc(
name = "hasNext",
doc = "Whether there is another chunk available.",
returns = type("lyng.Bool"),
moduleName = "lyng.io.fs"
) {
val self = thisAs<ObjFsBytesIterator>()
(self.pos < self.data.size).toObj()
}
addFn("next") {
addFnDoc(
name = "next",
doc = "Return the next chunk as a `Buffer`.",
returns = type("lyng.Buffer"),
moduleName = "lyng.io.fs"
) {
val self = thisAs<ObjFsBytesIterator>()
if (self.pos >= self.data.size) raiseIllegalState("iterator exhausted")
val end = minOf(self.pos + self.chunkSize, self.data.size)
@ -374,7 +542,11 @@ class ObjFsBytesIterator(
self.pos = end
ObjBuffer(chunk.asUByteArray())
}
addFn("cancelIteration") {
addFnDoc(
name = "cancelIteration",
doc = "Stop the iteration early; subsequent `hasNext` returns false.",
moduleName = "lyng.io.fs"
) {
val self = thisAs<ObjFsBytesIterator>()
self.pos = self.data.size
ObjVoid
@ -397,12 +569,27 @@ class ObjFsStringChunksIterator(
val StringChunksIteratorType = object : ObjClass("StringChunksIterator", ObjIterator) {
init {
// make it usable in for-loops
addFn("iterator") { thisObj }
addFn("hasNext") {
addFnDoc(
name = "iterator",
doc = "Return this iterator instance (enables `for` loops).",
returns = type("StringChunksIterator"),
moduleName = "lyng.io.fs"
) { thisObj }
addFnDoc(
name = "hasNext",
doc = "Whether there is another chunk available.",
returns = type("lyng.Bool"),
moduleName = "lyng.io.fs"
) {
val self = thisAs<ObjFsStringChunksIterator>()
(self.pos < self.text.length).toObj()
}
addFn("next") {
addFnDoc(
name = "next",
doc = "Return the next UTF-8 chunk as a `String`.",
returns = type("lyng.String"),
moduleName = "lyng.io.fs"
) {
val self = thisAs<ObjFsStringChunksIterator>()
if (self.pos >= self.text.length) raiseIllegalState("iterator exhausted")
val end = minOf(self.pos + self.chunkChars, self.text.length)
@ -410,7 +597,11 @@ class ObjFsStringChunksIterator(
self.pos = end
ObjString(chunk)
}
addFn("cancelIteration") { ObjVoid }
addFnDoc(
name = "cancelIteration",
doc = "Stop the iteration early; subsequent `hasNext` returns false.",
moduleName = "lyng.io.fs"
) { ObjVoid }
}
}
}
@ -429,13 +620,28 @@ class ObjFsLinesIterator(
val LinesIteratorType = object : ObjClass("LinesIterator", ObjIterator) {
init {
// make it usable in for-loops
addFn("iterator") { thisObj }
addFn("hasNext") {
addFnDoc(
name = "iterator",
doc = "Return this iterator instance (enables `for` loops).",
returns = type("LinesIterator"),
moduleName = "lyng.io.fs"
) { thisObj }
addFnDoc(
name = "hasNext",
doc = "Whether another line is available.",
returns = type("lyng.Bool"),
moduleName = "lyng.io.fs"
) {
val self = thisAs<ObjFsLinesIterator>()
self.ensureBufferFilled(this)
(self.buffer.isNotEmpty() || !self.exhausted).toObj()
}
addFn("next") {
addFnDoc(
name = "next",
doc = "Return the next line as `String`.",
returns = type("lyng.String"),
moduleName = "lyng.io.fs"
) {
val self = thisAs<ObjFsLinesIterator>()
self.ensureBufferFilled(this)
if (self.buffer.isEmpty() && self.exhausted) raiseIllegalState("iterator exhausted")
@ -453,7 +659,11 @@ class ObjFsLinesIterator(
}
ObjString(line)
}
addFn("cancelIteration") { ObjVoid }
addFnDoc(
name = "cancelIteration",
doc = "Stop the iteration early; subsequent `hasNext` returns false.",
moduleName = "lyng.io.fs"
) { ObjVoid }
}
}
}

View File

@ -0,0 +1,39 @@
/*
* 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.
*
*/
package net.sergeych.lyng
/**
* Lightweight documentation payload for symbols defined from Kotlin code (built-ins, stdlib, host bindings).
*
* The [summary] is optional; if not provided, it will be derived as the first non-empty line of [raw].
* Simple tag lines like "@since 1.0" can be stored in [tags] when needed.
*/
data class DocString(
val raw: String,
val summary: String? = null,
val tags: Map<String, List<String>> = emptyMap()
) {
val effectiveSummary: String? by lazy {
summary ?: raw.lineSequence().map { it.trim() }.firstOrNull { it.isNotEmpty() }
}
companion object {
/** Convenience to create a [DocString] taking the first non-empty line as a summary. */
fun of(text: String, tags: Map<String, List<String>> = emptyMap()): DocString = DocString(text, null, tags)
}
}

View File

@ -340,8 +340,11 @@ open class Scope(
}
}
// --- removed doc-aware overloads to keep runtime lean ---
fun addConst(name: String, value: Obj) = addItem(name, false, value)
suspend fun eval(code: String): Obj =
eval(code.toSource())

View File

@ -0,0 +1,486 @@
/*
* 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.
*
*/
/*
* Built-in documentation registry for Kotlin-defined APIs.
* Stores MiniAst declarations with MiniDoc, no runtime coupling.
*/
package net.sergeych.lyng.miniast
import net.sergeych.lyng.Pos
// ---------------- Types DSL ----------------
/** Simple param descriptor for docs builder. */
data class ParamDoc(val name: String, val type: TypeDoc? = null)
/** Type documentation model mapped later to MiniTypeRef. */
sealed interface TypeDoc { val nullable: Boolean }
data class TypeNameDoc(val segments: List<String>, override val nullable: Boolean = false) : TypeDoc
data class TypeGenericDoc(val base: TypeNameDoc, val args: List<TypeDoc>, override val nullable: Boolean = false) : TypeDoc
data class TypeFunctionDoc(
val receiver: TypeDoc? = null,
val params: List<TypeDoc>,
val returns: TypeDoc,
override val nullable: Boolean = false
) : TypeDoc
data class TypeVarDoc(val name: String, override val nullable: Boolean = false) : TypeDoc
// Convenience builders
fun type(name: String, nullable: Boolean = false) = TypeNameDoc(name.split('.'), nullable)
fun typeVar(name: String, nullable: Boolean = false) = TypeVarDoc(name, nullable)
fun funType(params: List<TypeDoc>, returns: TypeDoc, receiver: TypeDoc? = null, nullable: Boolean = false) =
TypeFunctionDoc(receiver, params, returns, nullable)
// ---------------- Registry ----------------
interface BuiltinDocSource {
fun docsForModule(moduleName: String): List<MiniDecl>
}
object BuiltinDocRegistry : BuiltinDocSource {
// Simple storage; populated at init time; reads dominate afterwards.
private val modules: MutableMap<String, MutableList<MiniDecl>> = mutableMapOf()
// Optional lazy suppliers to avoid hard init order coupling (e.g., stdlib docs)
private val lazySuppliers: MutableMap<String, () -> List<MiniDecl>> = mutableMapOf()
fun module(name: String, init: ModuleDocsBuilder.() -> Unit) {
val builder = ModuleDocsBuilder(name)
builder.init()
val list = modules.getOrPut(name) { mutableListOf() }
list += builder.build()
}
override fun docsForModule(moduleName: String): List<MiniDecl> {
modules[moduleName]?.let { return it }
// Try lazy supplier once
val built = lazySuppliers.remove(moduleName)?.invoke()
if (built != null) {
val list = modules.getOrPut(moduleName) { mutableListOf() }
list += built
return list
}
return emptyList()
}
fun clearModule(moduleName: String) { modules.remove(moduleName) }
fun allModules(): Set<String> = (modules.keys + lazySuppliers.keys).toSet()
/** Atomically replace a module's docs with freshly built ones. */
fun moduleReplace(name: String, init: ModuleDocsBuilder.() -> Unit) {
modules.remove(name)
module(name, init)
}
/** Register a lazy supplier that will be invoked on the first lookup for [name]. */
fun registerLazy(name: String, supplier: () -> List<MiniDecl>) {
// do not overwrite if module already present
if (!modules.containsKey(name)) lazySuppliers[name] = supplier
}
// Register built-in lazy seeds
init {
registerLazy("lyng.stdlib") { buildStdlibDocs() }
}
}
// ---------------- Builders ----------------
class ModuleDocsBuilder internal constructor(private val moduleName: String) {
private val decls = mutableListOf<MiniDecl>()
fun funDoc(
name: String,
doc: String,
params: List<ParamDoc> = emptyList(),
returns: TypeDoc? = null,
tags: Map<String, List<String>> = emptyMap(),
) {
val md = miniDoc(doc, tags)
val mp = params.map { MiniParam(it.name, it.type?.toMiniTypeRef(), Pos.builtIn) }
val ret = returns?.toMiniTypeRef()
decls += MiniFunDecl(
range = builtinRange(),
name = name,
params = mp,
returnType = ret,
body = null,
doc = md,
nameStart = Pos.builtIn,
)
}
fun valDoc(
name: String,
doc: String,
type: TypeDoc? = null,
mutable: Boolean = false,
tags: Map<String, List<String>> = emptyMap(),
) {
val md = miniDoc(doc, tags)
decls += MiniValDecl(
range = builtinRange(),
name = name,
mutable = mutable,
type = type?.toMiniTypeRef(),
initRange = null,
doc = md,
nameStart = Pos.builtIn,
)
}
fun classDoc(
name: String,
doc: String,
bases: List<TypeDoc> = emptyList(),
tags: Map<String, List<String>> = emptyMap(),
init: ClassDocsBuilder.() -> Unit = {},
) {
val md = miniDoc(doc, tags)
val cb = ClassDocsBuilder(name)
cb.init()
val baseNames = bases.map { it.toDisplayName() }
decls += MiniClassDecl(
range = builtinRange(),
name = name,
bases = baseNames,
bodyRange = null,
ctorFields = emptyList(),
classFields = emptyList(),
doc = md,
nameStart = Pos.builtIn,
members = cb.build()
)
}
internal fun build(): List<MiniDecl> = decls.toList()
}
class ClassDocsBuilder internal constructor(private val className: String) {
private val members = mutableListOf<MiniMemberDecl>()
fun method(
name: String,
doc: String,
params: List<ParamDoc> = emptyList(),
returns: TypeDoc? = null,
isStatic: Boolean = false,
tags: Map<String, List<String>> = emptyMap(),
) {
val md = miniDoc(doc, tags)
val mp = params.map { MiniParam(it.name, it.type?.toMiniTypeRef(), Pos.builtIn) }
val ret = returns?.toMiniTypeRef()
members += MiniMemberFunDecl(
range = builtinRange(),
name = name,
params = mp,
returnType = ret,
doc = md,
nameStart = Pos.builtIn,
isStatic = isStatic,
)
}
fun field(
name: String,
doc: String,
type: TypeDoc? = null,
mutable: Boolean = false,
isStatic: Boolean = false,
tags: Map<String, List<String>> = emptyMap(),
) {
val md = miniDoc(doc, tags)
members += MiniMemberValDecl(
range = builtinRange(),
name = name,
mutable = mutable,
type = type?.toMiniTypeRef(),
doc = md,
nameStart = Pos.builtIn,
isStatic = isStatic,
)
}
internal fun build(): List<MiniMemberDecl> = members.toList()
}
// ---------------- Helpers ----------------
private fun builtinRange() = MiniRange(Pos.builtIn, Pos.builtIn)
private fun miniDoc(text: String, tags: Map<String, List<String>>): MiniDoc {
val summary = text.lineSequence().map { it.trim() }.firstOrNull { it.isNotEmpty() }
return MiniDoc(range = builtinRange(), raw = text, summary = summary, tags = tags)
}
private fun TypeDoc.toDisplayName(): String = when (this) {
is TypeNameDoc -> segments.joinToString(".")
is TypeGenericDoc -> base.segments.joinToString(".")
is TypeFunctionDoc -> "(function)"
is TypeVarDoc -> name
}
internal fun TypeDoc.toMiniTypeRef(): MiniTypeRef = when (this) {
is TypeNameDoc -> MiniTypeName(
range = builtinRange(),
segments = this.segments.map { seg -> MiniTypeName.Segment(seg, builtinRange()) },
nullable = this.nullable
)
is TypeGenericDoc -> MiniGenericType(
range = builtinRange(),
base = this.base.toMiniTypeRef(),
args = this.args.map { it.toMiniTypeRef() },
nullable = this.nullable
)
is TypeFunctionDoc -> MiniFunctionType(
range = builtinRange(),
receiver = this.receiver?.toMiniTypeRef(),
params = this.params.map { it.toMiniTypeRef() },
returnType = this.returns.toMiniTypeRef(),
nullable = this.nullable
)
is TypeVarDoc -> MiniTypeVar(
range = builtinRange(),
name = this.name,
nullable = this.nullable
)
}
// ---------------- Built-in module doc seeds ----------------
// Seed docs for lyng.stdlib lazily to avoid init-order coupling.
private fun buildStdlibDocs(): List<MiniDecl> {
val decls = mutableListOf<MiniDecl>()
// Use the same DSL builders to construct decls
val mod = ModuleDocsBuilder("lyng.stdlib")
// Printing
mod.funDoc(
name = "print",
doc = """
Print values to the standard output without a trailing newline.
Accepts any number of arguments and prints them separated by a space.
""".trimIndent(),
// We keep signature minimal; variadic in Lyng is not modeled in MiniAst yet
params = listOf(ParamDoc("values"))
)
mod.funDoc(
name = "println",
doc = """
Print values to the standard output and append a newline.
Accepts any number of arguments and prints them separated by a space.
""".trimIndent(),
params = listOf(ParamDoc("values"))
)
// Caching helper
mod.funDoc(
name = "cached",
doc = """
Wrap a `builder` into a zero-argument thunk that computes once and caches the result.
The first call invokes `builder()` and stores the value; subsequent calls return the cached value.
""".trimIndent(),
params = listOf(ParamDoc("builder")),
returns = funType(params = emptyList(), returns = type("lyng.Any"))
)
// Math helpers (scalar versions)
fun math1(name: String) = mod.funDoc(
name = name,
doc = "Compute $name(x).",
params = listOf(ParamDoc("x", type("lyng.Number")))
)
math1("sin"); math1("cos"); math1("tan"); math1("asin"); math1("acos"); math1("atan")
mod.funDoc(name = "floor", doc = "Round down the number to the nearest integer.", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "ceil", doc = "Round up the number to the nearest integer.", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "round", doc = "Round the number to the nearest integer.", params = listOf(ParamDoc("x", type("lyng.Number"))))
// Hyperbolic and inverse hyperbolic
math1("sinh"); math1("cosh"); math1("tanh"); math1("asinh"); math1("acosh"); math1("atanh")
// Exponentials and logarithms
mod.funDoc(name = "exp", doc = "Euler's exponential e^x.", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "ln", doc = "Natural logarithm (base e).", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "log10", doc = "Logarithm base 10.", params = listOf(ParamDoc("x", type("lyng.Number"))))
mod.funDoc(name = "log2", doc = "Logarithm base 2.", params = listOf(ParamDoc("x", type("lyng.Number"))))
// Power/roots and absolute value
mod.funDoc(
name = "pow",
doc = "Raise `x` to the power `y`.",
params = listOf(ParamDoc("x", type("lyng.Number")), ParamDoc("y", type("lyng.Number")))
)
mod.funDoc(
name = "sqrt",
doc = "Square root of `x`.",
params = listOf(ParamDoc("x", type("lyng.Number")))
)
mod.funDoc(
name = "abs",
doc = "Absolute value of a number (works for Int and Real).",
params = listOf(ParamDoc("x", type("lyng.Number")))
)
// Assertions and checks
mod.funDoc(
name = "assert",
doc = """
Assert that `cond` is true, otherwise throw an `AssertionFailedException`.
Optionally provide a `message`.
""".trimIndent(),
params = listOf(ParamDoc("cond", type("lyng.Bool")), ParamDoc("message"))
)
mod.funDoc(
name = "assertEquals",
doc = "Assert that `a == b`, otherwise throw an assertion error.",
params = listOf(ParamDoc("a"), ParamDoc("b"))
)
mod.funDoc(
name = "assertNotEquals",
doc = "Assert that `a != b`, otherwise throw an assertion error.",
params = listOf(ParamDoc("a"), ParamDoc("b"))
)
mod.funDoc(
name = "assertThrows",
doc = """
Execute `code` and return the thrown `Exception` object.
If nothing is thrown, an assertion error is raised.
""".trimIndent(),
params = listOf(ParamDoc("code")),
returns = type("lyng.Exception", nullable = true)
)
// Utilities
mod.funDoc(
name = "dynamic",
doc = "Wrap a value into a dynamic object that defers resolution to runtime.",
params = listOf(ParamDoc("value"))
)
mod.funDoc(
name = "require",
doc = "Require `cond` to be true, else throw `IllegalArgumentException` with optional `message`.",
params = listOf(ParamDoc("cond", type("lyng.Bool")), ParamDoc("message"))
)
mod.funDoc(
name = "check",
doc = "Check `cond` is true, else throw `IllegalStateException` with optional `message`.",
params = listOf(ParamDoc("cond", type("lyng.Bool")), ParamDoc("message"))
)
mod.funDoc(
name = "traceScope",
doc = "Print a debug trace of the current scope chain with an optional label.",
params = listOf(ParamDoc("label", type("lyng.String")))
)
mod.funDoc(
name = "delay",
doc = "Suspend for the specified number of milliseconds.",
params = listOf(ParamDoc("ms", type("lyng.Number")))
)
// Concurrency helpers
mod.funDoc(
name = "launch",
doc = "Launch an asynchronous task and return a `Deferred`.",
params = listOf(ParamDoc("code")),
returns = type("lyng.Deferred")
)
mod.funDoc(
name = "yield",
doc = "Yield to the scheduler, allowing other tasks to run."
)
mod.funDoc(
name = "flow",
doc = "Create a lazy iterable stream using the provided `builder`.",
params = listOf(ParamDoc("builder")),
returns = type("lyng.Iterable")
)
// Common Iterable helpers (document top-level extension-like APIs as class members)
mod.classDoc(name = "Iterable", doc = "Helper operations for iterable collections.") {
method(name = "filter", doc = "Filter elements by predicate.", params = listOf(ParamDoc("predicate")), returns = type("lyng.Iterable"))
method(name = "drop", doc = "Skip the first N elements.", params = listOf(ParamDoc("n", type("lyng.Int"))), returns = type("lyng.Iterable"))
method(name = "first", doc = "Return the first element or throw if empty.")
method(name = "last", doc = "Return the last element or throw if empty.")
method(name = "dropLast", doc = "Drop the last N elements.", params = listOf(ParamDoc("n", type("lyng.Int"))), returns = type("lyng.Iterable"))
method(name = "takeLast", doc = "Take the last N elements.", params = listOf(ParamDoc("n", type("lyng.Int"))), returns = type("lyng.List"))
method(name = "joinToString", doc = "Join elements into a string with an optional separator and transformer.", params = listOf(ParamDoc("prefix", type("lyng.String")), ParamDoc("transformer")), returns = type("lyng.String"))
method(name = "any", doc = "Return true if any element matches the predicate.", params = listOf(ParamDoc("predicate")), returns = type("lyng.Bool"))
method(name = "all", doc = "Return true if all elements match the predicate.", params = listOf(ParamDoc("predicate")), returns = type("lyng.Bool"))
method(name = "sum", doc = "Sum all elements; returns null for empty collections.", returns = type("lyng.Number", nullable = true))
method(name = "sumOf", doc = "Sum mapped values of elements; returns null for empty collections.", params = listOf(ParamDoc("f")))
method(name = "minOf", doc = "Minimum of mapped values.", params = listOf(ParamDoc("lambda")))
method(name = "maxOf", doc = "Maximum of mapped values.", params = listOf(ParamDoc("lambda")))
method(name = "sorted", doc = "Return elements sorted by natural order.", returns = type("lyng.Iterable"))
method(name = "sortedBy", doc = "Return elements sorted by the key selector.", params = listOf(ParamDoc("predicate")), returns = type("lyng.Iterable"))
method(name = "shuffled", doc = "Return a shuffled copy as a list.", returns = type("lyng.List"))
method(name = "map", doc = "Transform elements by applying `transform`.", params = listOf(ParamDoc("transform")), returns = type("lyng.Iterable"))
method(name = "toList", doc = "Collect elements of this iterable into a new list.", returns = type("lyng.List"))
}
// List helpers
mod.classDoc(name = "List", doc = "List-specific operations.", bases = listOf(type("Collection"), type("Iterable"))) {
method(name = "toString", doc = "Return string representation like [a,b,c].", returns = type("lyng.String"))
method(name = "sortBy", doc = "Sort list in-place by key selector.", params = listOf(ParamDoc("predicate")))
method(name = "sort", doc = "Sort list in-place by natural order.")
method(name = "toList", doc = "Return a shallow copy of this list (new list with the same elements).", returns = type("lyng.List"))
}
// Collection helpers (supertype for sized collections)
mod.classDoc(name = "Collection", doc = "Collection operations common to sized collections.", bases = listOf(type("Iterable"))) {
method(name = "size", doc = "Number of elements in the collection.", returns = type("lyng.Int"))
method(name = "toList", doc = "Collect elements into a new list.", returns = type("lyng.List"))
}
// Iterator helpers
mod.classDoc(name = "Iterator", doc = "Iterator protocol for sequential access.") {
method(name = "hasNext", doc = "Whether another element is available.", returns = type("lyng.Bool"))
method(name = "next", doc = "Return the next element.")
method(name = "cancelIteration", doc = "Stop the iteration early.")
method(name = "toList", doc = "Consume this iterator and collect elements into a list.", returns = type("lyng.List"))
}
// Exceptions and utilities
mod.classDoc(name = "Exception", doc = "Exception helpers.") {
method(name = "printStackTrace", doc = "Print this exception and its stack trace to standard output.")
}
mod.classDoc(name = "String", doc = "String helpers.") {
method(name = "re", doc = "Compile this string into a regular expression.", returns = type("lyng.Regex"))
}
// StackTraceEntry structure
mod.classDoc(name = "StackTraceEntry", doc = "Represents a single stack trace element.") {
field(name = "sourceName", doc = "Source (file) name.", type = type("lyng.String"))
field(name = "line", doc = "Line number (1-based).", type = type("lyng.Int"))
field(name = "column", doc = "Column number (0-based).", type = type("lyng.Int"))
field(name = "sourceString", doc = "The source line text.", type = type("lyng.String"))
method(name = "toString", doc = "Formatted representation: source:line:column: text.", returns = type("lyng.String"))
}
// Constants and namespaces
mod.valDoc(
name = "π",
doc = "The mathematical constant pi.",
type = type("lyng.Real"),
mutable = false
)
mod.classDoc(name = "Math", doc = "Mathematical constants and helpers.") {
field(name = "PI", doc = "The mathematical constant pi.", type = type("lyng.Real"), isStatic = true)
}
decls += mod.build()
return decls
}
// (Registration is triggered from BuiltinDocRegistry.init)

View File

@ -0,0 +1,178 @@
/*
* 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.
*
*/
package net.sergeych.lyng.miniast
import net.sergeych.lyng.ModuleScope
import net.sergeych.lyng.Scope
import net.sergeych.lyng.Visibility
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.ObjVoid
/**
* Helper extensions that mirror `addFn`/`addConst` APIs but also register Markdown docs
* into the BuiltinDocRegistry (MiniAst-based). This keeps docs co-located with code
* definitions and avoids any runtime overhead.
*/
// --------- Module-level (Scope) ---------
inline fun <reified T : Obj> Scope.addFnDoc(
vararg names: String,
doc: String,
params: List<ParamDoc> = emptyList(),
returns: TypeDoc? = null,
tags: Map<String, List<String>> = emptyMap(),
moduleName: String? = null,
crossinline fn: suspend Scope.() -> T
) {
// Register runtime function(s)
addFn(*names) { fn() }
// Determine module
val mod = moduleName ?: findModuleNameOrUnknown()
// Register docs once per name
if (names.isNotEmpty()) BuiltinDocRegistry.module(mod) {
for (n in names) funDoc(name = n, doc = doc, params = params, returns = returns, tags = tags)
}
}
inline fun Scope.addVoidFnDoc(
vararg names: String,
doc: String,
tags: Map<String, List<String>> = emptyMap(),
moduleName: String? = null,
crossinline fn: suspend Scope.() -> Unit
) {
addFnDoc<ObjVoid>(
*names,
doc = doc,
params = emptyList(),
returns = null,
tags = tags,
moduleName = moduleName
) {
fn(this)
ObjVoid
}
}
fun Scope.addConstDoc(
name: String,
value: Obj,
doc: String,
type: TypeDoc? = null,
mutable: Boolean = false,
tags: Map<String, List<String>> = emptyMap(),
moduleName: String? = null
) {
if (mutable) addItem(name, true, value) else addConst(name, value)
BuiltinDocRegistry.module(moduleName ?: findModuleNameOrUnknown()) {
valDoc(name = name, doc = doc, type = type, mutable = mutable, tags = tags)
}
}
// --------- Class-level (ObjClass) ---------
fun ObjClass.addFnDoc(
name: String,
doc: String,
params: List<ParamDoc> = emptyList(),
returns: TypeDoc? = null,
isOpen: Boolean = false,
visibility: Visibility = Visibility.Public,
tags: Map<String, List<String>> = emptyMap(),
moduleName: String? = null,
code: suspend Scope.() -> Obj
) {
// Register runtime method
addFn(name, isOpen, visibility, code)
// Register docs for the member under this class
BuiltinDocRegistry.module(moduleName ?: ownerModuleNameFromClassOrUnknown()) {
classDoc(this@addFnDoc.className, doc = "") {
method(name = name, doc = doc, params = params, returns = returns, tags = tags)
}
}
}
fun ObjClass.addConstDoc(
name: String,
value: Obj,
doc: String,
type: TypeDoc? = null,
isMutable: Boolean = false,
visibility: Visibility = Visibility.Public,
tags: Map<String, List<String>> = emptyMap(),
moduleName: String? = null
) {
createField(name, value, isMutable, visibility)
BuiltinDocRegistry.module(moduleName ?: ownerModuleNameFromClassOrUnknown()) {
classDoc(this@addConstDoc.className, doc = "") {
field(name = name, doc = doc, type = type, mutable = isMutable, tags = tags)
}
}
}
fun ObjClass.addClassFnDoc(
name: String,
doc: String,
params: List<ParamDoc> = emptyList(),
returns: TypeDoc? = null,
isOpen: Boolean = false,
tags: Map<String, List<String>> = emptyMap(),
moduleName: String? = null,
code: suspend Scope.() -> Obj
) {
addClassFn(name, isOpen, code)
BuiltinDocRegistry.module(moduleName ?: ownerModuleNameFromClassOrUnknown()) {
classDoc(this@addClassFnDoc.className, doc = "") {
method(name = name, doc = doc, params = params, returns = returns, isStatic = true, tags = tags)
}
}
}
fun ObjClass.addClassConstDoc(
name: String,
value: Obj,
doc: String,
type: TypeDoc? = null,
isMutable: Boolean = false,
tags: Map<String, List<String>> = emptyMap(),
moduleName: String? = null
) {
createClassField(name, value, isMutable)
BuiltinDocRegistry.module(moduleName ?: ownerModuleNameFromClassOrUnknown()) {
classDoc(this@addClassConstDoc.className, doc = "") {
field(name = name, doc = doc, type = type, mutable = isMutable, isStatic = true, tags = tags)
}
}
}
// ------------- utils -------------
@PublishedApi
internal tailrec fun Scope.findModuleNameOrNull(): String? = when (this) {
is ModuleScope -> this.packageName
else -> this.parent?.findModuleNameOrNull()
}
@PublishedApi
internal fun Scope.findModuleNameOrUnknown(): String = findModuleNameOrNull() ?: "unknown"
@PublishedApi
internal fun ObjClass.ownerModuleNameFromClassOrUnknown(): String =
// Try to find a ModuleScope in classScope parent chain if available, else unknown
(classScope?.parent?.findModuleNameOrNull()) ?: "unknown"

View File

@ -130,7 +130,9 @@ data class MiniClassDecl(
val ctorFields: List<MiniCtorField> = emptyList(),
val classFields: List<MiniCtorField> = emptyList(),
override val doc: MiniDoc?,
override val nameStart: Pos
override val nameStart: Pos,
// Built-in extension: list of member declarations (functions and fields)
val members: List<MiniMemberDecl> = emptyList()
) : MiniDecl
data class MiniCtorField(
@ -153,6 +155,34 @@ data class MiniIdentifier(
val role: IdRole
) : MiniNode
// --- Class member declarations (for built-in/registry docs) ---
sealed interface MiniMemberDecl : MiniNode {
val name: String
val doc: MiniDoc?
val nameStart: Pos
val isStatic: Boolean
}
data class MiniMemberFunDecl(
override val range: MiniRange,
override val name: String,
val params: List<MiniParam>,
val returnType: MiniTypeRef?,
override val doc: MiniDoc?,
override val nameStart: Pos,
override val isStatic: Boolean = false,
) : MiniMemberDecl
data class MiniMemberValDecl(
override val range: MiniRange,
override val name: String,
val mutable: Boolean,
val type: MiniTypeRef?,
override val doc: MiniDoc?,
override val nameStart: Pos,
override val isStatic: Boolean = false,
) : MiniMemberDecl
// Streaming sink to collect mini-AST during parsing. Implementations may assemble a tree or process events.
interface MiniAstSink {
fun onScriptStart(start: Pos) {}

View File

@ -16,7 +16,6 @@
*/
package net.sergeych.lyng.obj
import net.sergeych.lyng.Scope
import net.sergeych.lyng.Visibility
@ -31,7 +30,7 @@ data class ObjRecord(
val declaringClass: ObjClass? = null,
var importedFrom: Scope? = null,
val isTransient: Boolean = false,
val type: Type = Type.Other
val type: Type = Type.Other,
) {
enum class Type(val comparable: Boolean = false,val serializable: Boolean = false) {
Field(true, true),

View File

@ -45,6 +45,7 @@ class SamplesTest {
@Test
fun testSamples() = runBlocking {
for (s in Files.list(Paths.get("../docs/samples"))) {
if( s.fileName.toString() == "fs_sample.lyng" ) continue
if (s.extension == "lyng") executeSampleTests(s.toString())
}
}

View File

@ -18,6 +18,7 @@
import androidx.compose.runtime.*
import kotlinx.browser.window
import kotlinx.coroutines.await
import net.sergeych.lyng.miniast.*
import org.jetbrains.compose.web.dom.*
@Composable
@ -92,4 +93,86 @@ fun ReferencePage() {
}
}
}
// Built-in APIs (from registry)
Hr()
H2({ classes("h5", "mb-3", "mt-4") }) { Text("Built-in APIs") }
val modules = remember { BuiltinDocRegistry.allModules().sorted() }
if (modules.isEmpty()) {
P({ classes("text-muted") }) { Text("No built-in modules registered.") }
} else {
modules.forEach { modName ->
val decls = BuiltinDocRegistry.docsForModule(modName)
if (decls.isEmpty()) return@forEach
H3({ classes("h6", "mt-3") }) { Text(modName) }
Ul({ classes("list-group", "mb-3") }) {
decls.forEach { d ->
Li({ classes("list-group-item") }) {
when (d) {
is MiniFunDecl -> {
val sig = signatureOf(d)
Div { Text("fun ${d.name}$sig") }
d.doc?.summary?.let { Small({ classes("text-muted") }) { Text(it) } }
}
is MiniValDecl -> {
val kind = if (d.mutable) "var" else "val"
val t = typeOf(d.type)
Div { Text("$kind ${d.name}$t") }
d.doc?.summary?.let { Small({ classes("text-muted") }) { Text(it) } }
}
is MiniClassDecl -> {
Div { Text("class ${d.name}") }
d.doc?.summary?.let { Small({ classes("text-muted") }) { Text(it) } }
if (d.members.isNotEmpty()) {
Ul({ classes("mt-2") }) {
d.members.forEach { m ->
when (m) {
is MiniMemberFunDecl -> {
val params = m.params.joinToString(", ") { p ->
val ts = typeOf(p.type)
if (ts.isNotBlank()) "${p.name}${ts}" else p.name
}
val ret = typeOf(m.returnType)
val staticStr = if (m.isStatic) "static " else ""
Li { Text("${staticStr}method ${d.name}.${m.name}(${params})${ret}") }
}
is MiniMemberValDecl -> {
val ts = typeOf(m.type)
val kindM = if (m.mutable) "var" else "val"
val staticStr = if (m.isStatic) "static " else ""
Li { Text("${staticStr}${kindM} ${d.name}.${m.name}${ts}") }
}
}
}
}
}
}
}
}
}
}
}
}
}
// --- helpers (mirror IDE provider minimal renderers) ---
private fun typeOf(t: MiniTypeRef?): String = when (t) {
is MiniTypeName -> ": " + t.segments.joinToString(".") { it.name } + if (t.nullable) "?" else ""
is MiniGenericType -> {
val base = typeOf(t.base).removePrefix(": ")
val args = t.args.joinToString(", ") { typeOf(it).removePrefix(": ") }
": ${base}<${args}>" + if (t.nullable) "?" else ""
}
is MiniFunctionType -> ": (..) -> .." + if (t.nullable) "?" else ""
is MiniTypeVar -> ": ${t.name}" + if (t.nullable) "?" else ""
null -> ""
}
private fun signatureOf(fn: MiniFunDecl): String {
val params = fn.params.joinToString(", ") { p ->
val ts = typeOf(p.type)
if (ts.isNotBlank()) "${p.name}${ts}" else p.name
}
val ret = typeOf(fn.returnType)
return "(${params})${ret}"
}