diff --git a/lyng-idea/build.gradle.kts b/lyng-idea/build.gradle.kts index 9483735..25fd54b 100644 --- a/lyng-idea/build.gradle.kts +++ b/lyng-idea/build.gradle.kts @@ -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 { 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 e078316..f8a374d 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 @@ -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("", 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().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().firstOrNull { it.name == ident }?.let { return renderDeclDoc(it) } + // And classes + docs.filterIsInstance().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", "
") } + val raw = d.doc?.raw + val doc: String? = if (raw.isNullOrBlank()) null else MarkdownRenderer.render(raw) val sb = StringBuilder() sb.append("
").append(htmlEscape(title)).append("
") - if (!doc.isNullOrBlank()) sb.append("
").append(doc).append("
") + if (!doc.isNullOrBlank()) sb.append(styledMarkdown(doc!!)) return sb.toString() } @@ -131,11 +199,44 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { return "
${htmlEscape(title)}
" } + 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("
").append(htmlEscape(title)).append("
") + 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("
").append(htmlEscape(title)).append("
") + 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 blocks (case-insensitive, dotall) + val styleRegex = Regex("(?is)]*>.*?") + 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): String { + val sb = StringBuilder() + sb.append("
Overloads for ").append(htmlEscape(name)).append("
") + sb.append("
    ") + overloads.forEach { fn -> + sb.append("
  • ") + .append(htmlEscape("fun ${fn.name}${signatureOf(fn)}")) + .append("") + fn.doc?.summary?.let { sum -> sb.append(" — ").append(htmlEscape(sum)) } + sb.append("
  • ") + } + sb.append("
") + 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): Map { + val map = LinkedHashMap() + for (mod in importedModules) { + val docs = BuiltinDocRegistry.docsForModule(mod) + docs.filterIsInstance().forEach { cls -> + // Prefer the first occurrence; allow later duplicates to be ignored + map.putIfAbsent(cls.name, cls) + } + } + return map + } + + private fun resolveMemberWithInheritance(importedModules: List, className: String, member: String): Pair? { + val classes = aggregateClasses(importedModules) + fun dfs(name: String, visited: MutableSet): Pair? { + 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, member: String): Pair? { + 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 + } } diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/docs/MarkdownRenderer.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/docs/MarkdownRenderer.kt new file mode 100644 index 0000000..cde9528 --- /dev/null +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/docs/MarkdownRenderer.kt @@ -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
+ * - 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(256, 0.75f, true) { + override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): 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 + } +} diff --git a/lyngio/src/commonMain/kotlin/net/sergeych/lyng/io/fs/LyngFsModule.kt b/lyngio/src/commonMain/kotlin/net/sergeych/lyng/io/fs/LyngFsModule.kt index 9377889..fdc2a0a 100644 --- a/lyngio/src/commonMain/kotlin/net/sergeych/lyng/io/fs/LyngFsModule.kt +++ b/lyngio/src/commonMain/kotlin/net/sergeych/lyng/io/fs/LyngFsModule.kt @@ -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() 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() self.path.parent?.let { ObjPath( this@apply, self.secured, it) } ?: ObjNull } - addFn("segments") { + addFnDoc( + name = "segments", + doc = "List of path segments.", + // returns: List + returns = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.String"))), + moduleName = module.packageName + ) { val self = thisAs() 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 - 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(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(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().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().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(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(0)) @@ -262,7 +380,13 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) { } } // glob(pattern: String): List - 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().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 - 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 - 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, 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() (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() 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() 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() (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() 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() 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() 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 } } } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Docs.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Docs.kt new file mode 100644 index 0000000..e754f07 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Docs.kt @@ -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> = 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> = emptyMap()): DocString = DocString(text, null, tags) + } +} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt index ec49432..7439d55 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Scope.kt @@ -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()) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt new file mode 100644 index 0000000..ce465e7 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt @@ -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, override val nullable: Boolean = false) : TypeDoc +data class TypeGenericDoc(val base: TypeNameDoc, val args: List, override val nullable: Boolean = false) : TypeDoc +data class TypeFunctionDoc( + val receiver: TypeDoc? = null, + val params: List, + 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, returns: TypeDoc, receiver: TypeDoc? = null, nullable: Boolean = false) = + TypeFunctionDoc(receiver, params, returns, nullable) + +// ---------------- Registry ---------------- + +interface BuiltinDocSource { + fun docsForModule(moduleName: String): List +} + +object BuiltinDocRegistry : BuiltinDocSource { + // Simple storage; populated at init time; reads dominate afterwards. + private val modules: MutableMap> = mutableMapOf() + // Optional lazy suppliers to avoid hard init order coupling (e.g., stdlib docs) + private val lazySuppliers: MutableMap List> = 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 { + 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 = (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) { + // 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() + + fun funDoc( + name: String, + doc: String, + params: List = emptyList(), + returns: TypeDoc? = null, + tags: Map> = 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> = 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 = emptyList(), + tags: Map> = 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 = decls.toList() +} + +class ClassDocsBuilder internal constructor(private val className: String) { + private val members = mutableListOf() + + fun method( + name: String, + doc: String, + params: List = emptyList(), + returns: TypeDoc? = null, + isStatic: Boolean = false, + tags: Map> = 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> = 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 = members.toList() +} + +// ---------------- Helpers ---------------- + +private fun builtinRange() = MiniRange(Pos.builtIn, Pos.builtIn) + +private fun miniDoc(text: String, tags: Map>): 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 { + val decls = mutableListOf() + // 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) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/DocRegistrationHelpers.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/DocRegistrationHelpers.kt new file mode 100644 index 0000000..c8e24d8 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/DocRegistrationHelpers.kt @@ -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 Scope.addFnDoc( + vararg names: String, + doc: String, + params: List = emptyList(), + returns: TypeDoc? = null, + tags: Map> = 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> = emptyMap(), + moduleName: String? = null, + crossinline fn: suspend Scope.() -> Unit +) { + addFnDoc( + *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> = 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 = emptyList(), + returns: TypeDoc? = null, + isOpen: Boolean = false, + visibility: Visibility = Visibility.Public, + tags: Map> = 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> = 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 = emptyList(), + returns: TypeDoc? = null, + isOpen: Boolean = false, + tags: Map> = 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> = 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" diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/MiniAst.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/MiniAst.kt index 2b6ca65..0ce422b 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/MiniAst.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/MiniAst.kt @@ -130,7 +130,9 @@ data class MiniClassDecl( val ctorFields: List = emptyList(), val classFields: List = 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 = 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, + 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) {} diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRecord.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRecord.kt index 7496b32..7a79be1 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRecord.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRecord.kt @@ -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), diff --git a/lynglib/src/jvmTest/kotlin/SamplesTest.kt b/lynglib/src/jvmTest/kotlin/SamplesTest.kt index 73ae98d..9a834c6 100644 --- a/lynglib/src/jvmTest/kotlin/SamplesTest.kt +++ b/lynglib/src/jvmTest/kotlin/SamplesTest.kt @@ -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()) } } diff --git a/site/src/jsMain/kotlin/ReferencePage.kt b/site/src/jsMain/kotlin/ReferencePage.kt index 09c2bab..47efb28 100644 --- a/site/src/jsMain/kotlin/ReferencePage.kt +++ b/site/src/jsMain/kotlin/ReferencePage.kt @@ -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}" }