Add MiniAst documentation system and Markdown rendering for built-in and IDE documentation.
This commit is contained in:
parent
c52e132dcc
commit
067970b80c
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
39
lynglib/src/commonMain/kotlin/net/sergeych/lyng/Docs.kt
Normal file
39
lynglib/src/commonMain/kotlin/net/sergeych/lyng/Docs.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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())
|
||||
|
||||
|
||||
@ -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)
|
||||
@ -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"
|
||||
@ -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) {}
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}"
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user