Compare commits

..

2 Commits

33 changed files with 1472 additions and 559 deletions

View File

@ -17,7 +17,7 @@
plugins { plugins {
kotlin("jvm") kotlin("jvm")
id("org.jetbrains.intellij") version "1.17.3" id("org.jetbrains.intellij") version "1.17.4"
} }
group = "net.sergeych.lyng" group = "net.sergeych.lyng"
@ -52,7 +52,7 @@ dependencies {
intellij { intellij {
type.set("IC") type.set("IC")
// Build against a modern baseline. Install range is controlled by since/until below. // Build against a modern baseline. Install range is controlled by since/until below.
version.set("2024.3.1") version.set("2024.1.6")
// We manage <idea-version> ourselves in plugin.xml to keep it open-ended (no upper cap) // We manage <idea-version> ourselves in plugin.xml to keep it open-ended (no upper cap)
updateSinceUntilBuild.set(false) updateSinceUntilBuild.set(false)
// Include only available bundled plugins for this IDE build // Include only available bundled plugins for this IDE build

View File

@ -167,7 +167,11 @@ class LyngCompletionContributor : CompletionContributor() {
.withIcon(AllIcons.Nodes.Field) .withIcon(AllIcons.Nodes.Field)
.let { b -> if (!ci.typeText.isNullOrBlank()) b.withTypeText(ci.typeText, true) else b } .let { b -> if (!ci.typeText.isNullOrBlank()) b.withTypeText(ci.typeText, true) else b }
} }
emit(builder) if (ci.priority != 0.0) {
emit(PrioritizedLookupElement.withPriority(builder, ci.priority))
} else {
emit(builder)
}
} }
// In member context, ensure stdlib extension-like methods (e.g., String.re) are present // In member context, ensure stdlib extension-like methods (e.g., String.re) are present
if (memberDotPos != null) { if (memberDotPos != null) {
@ -401,7 +405,7 @@ class LyngCompletionContributor : CompletionContributor() {
} }
supplementPreferredBases(className) supplementPreferredBases(className)
fun emitGroup(map: LinkedHashMap<String, MutableList<MiniMemberDecl>>) { fun emitGroup(map: LinkedHashMap<String, MutableList<MiniMemberDecl>>, groupPriority: Double) {
val keys = map.keys.sortedBy { it.lowercase() } val keys = map.keys.sortedBy { it.lowercase() }
for (name in keys) { for (name in keys) {
val list = map[name] ?: continue val list = map[name] ?: continue
@ -428,7 +432,11 @@ class LyngCompletionContributor : CompletionContributor() {
.withTailText(tail, true) .withTailText(tail, true)
.withTypeText(ret, true) .withTypeText(ret, true)
.withInsertHandler(ParenInsertHandler) .withInsertHandler(ParenInsertHandler)
emit(builder) if (groupPriority != 0.0) {
emit(PrioritizedLookupElement.withPriority(builder, groupPriority))
} else {
emit(builder)
}
} }
is MiniMemberValDecl -> { is MiniMemberValDecl -> {
val icon = if (rep.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field val icon = if (rep.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field
@ -439,7 +447,11 @@ class LyngCompletionContributor : CompletionContributor() {
val builder = LookupElementBuilder.create(name) val builder = LookupElementBuilder.create(name)
.withIcon(icon) .withIcon(icon)
.withTypeText(typeOf(chosen.type), true) .withTypeText(typeOf(chosen.type), true)
emit(builder) if (groupPriority != 0.0) {
emit(PrioritizedLookupElement.withPriority(builder, groupPriority))
} else {
emit(builder)
}
} }
is MiniInitDecl -> {} is MiniInitDecl -> {}
} }
@ -447,8 +459,8 @@ class LyngCompletionContributor : CompletionContributor() {
} }
// Emit what we have first // Emit what we have first
emitGroup(directMap) emitGroup(directMap, 100.0)
emitGroup(inheritedMap) emitGroup(inheritedMap, 0.0)
// If suggestions are suspiciously sparse for known container classes, // If suggestions are suspiciously sparse for known container classes,
// try to conservatively supplement using a curated list resolved via docs registry. // try to conservatively supplement using a curated list resolved via docs registry.
@ -509,7 +521,7 @@ class LyngCompletionContributor : CompletionContributor() {
.withInsertHandler(ParenInsertHandler) .withInsertHandler(ParenInsertHandler)
} }
} }
emit(builder) emit(PrioritizedLookupElement.withPriority(builder, 50.0))
already.add(name) already.add(name)
} else { } else {
// Synthetic fallback: method without detailed params/types to improve UX in absence of docs // Synthetic fallback: method without detailed params/types to improve UX in absence of docs
@ -523,7 +535,7 @@ class LyngCompletionContributor : CompletionContributor() {
.withTailText("()", true) .withTailText("()", true)
.withInsertHandler(ParenInsertHandler) .withInsertHandler(ParenInsertHandler)
} }
emit(builder) emit(PrioritizedLookupElement.withPriority(builder, 50.0))
already.add(name) already.add(name)
} }
} }
@ -576,7 +588,7 @@ class LyngCompletionContributor : CompletionContributor() {
.withInsertHandler(ParenInsertHandler) .withInsertHandler(ParenInsertHandler)
} }
} }
emit(builder) emit(PrioritizedLookupElement.withPriority(builder, 50.0))
already.add(name) already.add(name)
continue continue
} }
@ -585,7 +597,7 @@ class LyngCompletionContributor : CompletionContributor() {
.withIcon(AllIcons.Nodes.Method) .withIcon(AllIcons.Nodes.Method)
.withTailText("()", true) .withTailText("()", true)
.withInsertHandler(ParenInsertHandler) .withInsertHandler(ParenInsertHandler)
emit(builder) emit(PrioritizedLookupElement.withPriority(builder, 50.0))
already.add(name) already.add(name)
} }
} }

View File

@ -461,25 +461,39 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
private fun ensureExternalDocsRegistered() { @Suppress("UNUSED_EXPRESSION") externalDocsLoaded } private fun ensureExternalDocsRegistered() { @Suppress("UNUSED_EXPRESSION") externalDocsLoaded }
private fun tryLoadExternalDocs(): Boolean { private fun tryLoadExternalDocs(): Boolean {
return try { var anyLoaded = false
try {
// Try known registrars; ignore failures if module is absent // Try known registrars; ignore failures if module is absent
val cls = Class.forName("net.sergeych.lyngio.docs.FsBuiltinDocs") val cls = Class.forName("net.sergeych.lyngio.docs.FsBuiltinDocs")
val m = cls.getMethod("ensure") val m = cls.getMethod("ensure")
m.invoke(null) m.invoke(null)
log.info("[LYNG_DEBUG] QuickDoc: external docs loaded: net.sergeych.lyngio.docs.FsBuiltinDocs.ensure() OK") log.info("[LYNG_DEBUG] QuickDoc: external docs loaded: net.sergeych.lyngio.docs.FsBuiltinDocs.ensure() OK")
true anyLoaded = true
} catch (_: Throwable) { } catch (_: Throwable) {}
try {
val cls = Class.forName("net.sergeych.lyngio.docs.ProcessBuiltinDocs")
val m = cls.getMethod("ensure")
m.invoke(null)
log.info("[LYNG_DEBUG] QuickDoc: external docs loaded: net.sergeych.lyngio.docs.ProcessBuiltinDocs.ensure() OK")
anyLoaded = true
} catch (_: Throwable) {}
if (!anyLoaded) {
// Seed a minimal plugin-local fallback so Path docs still work without lyngio // Seed a minimal plugin-local fallback so Path docs still work without lyngio
val seeded = try { val seeded = try {
FsDocsFallback.ensureOnce() FsDocsFallback.ensureOnce()
ProcessDocsFallback.ensureOnce()
true
} catch (_: Throwable) { false } } catch (_: Throwable) { false }
if (seeded) { if (seeded) {
log.info("[LYNG_DEBUG] QuickDoc: external docs NOT found; seeded plugin fallback for lyng.io.fs") log.info("[LYNG_DEBUG] QuickDoc: external docs NOT found; seeded plugin fallbacks")
} else { } else {
log.info("[LYNG_DEBUG] QuickDoc: external docs NOT found (lyngio absent on classpath)") log.info("[LYNG_DEBUG] QuickDoc: external docs NOT found (lyngio absent on classpath)")
} }
seeded return seeded
} }
return true
} }
override fun getCustomDocumentationElement( override fun getCustomDocumentationElement(

View File

@ -0,0 +1,69 @@
/*
* Copyright 2026 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.
*
*/
/*
* Minimal fallback docs seeding for `lyng.io.process` used only inside the IDEA plugin
* when external docs module (lyngio) is not present on the classpath.
*/
package net.sergeych.lyng.idea.docs
import net.sergeych.lyng.miniast.BuiltinDocRegistry
import net.sergeych.lyng.miniast.ParamDoc
import net.sergeych.lyng.miniast.type
internal object ProcessDocsFallback {
@Volatile
private var seeded = false
fun ensureOnce(): Boolean {
if (seeded) return true
synchronized(this) {
if (seeded) return true
BuiltinDocRegistry.module("lyng.io.process") {
classDoc(name = "Process", doc = "Process execution and control.") {
method(
name = "execute",
doc = "Execute a process with arguments.",
params = listOf(ParamDoc("executable", type("lyng.String")), ParamDoc("args", type("lyng.List"))),
returns = type("RunningProcess"),
isStatic = true
)
method(
name = "shell",
doc = "Execute a command via system shell.",
params = listOf(ParamDoc("command", type("lyng.String"))),
returns = type("RunningProcess"),
isStatic = true
)
}
classDoc(name = "RunningProcess", doc = "Handle to a running process.") {
method(name = "stdout", doc = "Get standard output stream as a Flow of lines.", returns = type("lyng.Flow"))
method(name = "stderr", doc = "Get standard error stream as a Flow of lines.", returns = type("lyng.Flow"))
method(name = "waitFor", doc = "Wait for the process to exit.", returns = type("lyng.Int"))
method(name = "signal", doc = "Send a signal to the process.", params = listOf(ParamDoc("signal", type("lyng.String"))))
method(name = "destroy", doc = "Forcefully terminate the process.")
}
valDoc(name = "Process", doc = "Process execution and control.", type = type("Process"))
valDoc(name = "RunningProcess", doc = "Handle to a running process.", type = type("RunningProcess"))
}
seeded = true
return true
}
}
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -54,6 +54,8 @@ class LyngSpellcheckingStrategy : SpellcheckingStrategy() {
} catch (_: Throwable) { false } } catch (_: Throwable) { false }
override fun getTokenizer(element: PsiElement): Tokenizer<*> { override fun getTokenizer(element: PsiElement): Tokenizer<*> {
if (element is com.intellij.psi.PsiFile) return EMPTY_TOKENIZER
val hasGrazie = grazieInstalled() val hasGrazie = grazieInstalled()
val hasGrazieApi = grazieApiAvailable() val hasGrazieApi = grazieApiAvailable()
val settings = LyngFormatterSettings.getInstance(element.project) val settings = LyngFormatterSettings.getInstance(element.project)
@ -63,27 +65,40 @@ class LyngSpellcheckingStrategy : SpellcheckingStrategy() {
} }
val file = element.containingFile ?: return EMPTY_TOKENIZER val file = element.containingFile ?: return EMPTY_TOKENIZER
val index = LyngSpellIndex.getUpToDate(file) ?: run { val et = element.node?.elementType
// Suspend legacy spellcheck until MiniAst-based index is ready val index = LyngSpellIndex.getUpToDate(file)
return EMPTY_TOKENIZER
}
val elRange = element.textRange ?: return EMPTY_TOKENIZER
fun overlaps(list: List<TextRange>) = list.any { it.intersects(elRange) }
// Decide responsibility per settings // Decide responsibility per settings
// If Grazie is present but its public API is not available (IC-243), do NOT delegate to it. // If Grazie is present but its public API is not available (IC-243), do NOT delegate to it.
val preferGrazie = hasGrazie && hasGrazieApi && settings.preferGrazieForCommentsAndLiterals val preferGrazie = hasGrazie && hasGrazieApi && settings.preferGrazieForCommentsAndLiterals
val grazieIds = hasGrazie && hasGrazieApi && settings.grazieChecksIdentifiers val grazieIds = hasGrazie && hasGrazieApi && settings.grazieChecksIdentifiers
if (index == null) {
// Index not ready: fall back to Lexer-based token types.
// Identifiers are safe because LyngLexer separates keywords from identifiers.
if (et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.IDENTIFIER) {
return if (grazieIds) EMPTY_TOKENIZER else IDENTIFIER_TOKENIZER
}
if (et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.LINE_COMMENT || et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.BLOCK_COMMENT) {
return if (preferGrazie) EMPTY_TOKENIZER else COMMENT_TEXT_TOKENIZER
}
if (et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.STRING && settings.spellCheckStringLiterals) {
return if (preferGrazie) EMPTY_TOKENIZER else STRING_WITH_PRINTF_EXCLUDES
}
return EMPTY_TOKENIZER
}
val elRange = element.textRange ?: return EMPTY_TOKENIZER
fun overlaps(list: List<TextRange>) = list.any { it.intersects(elRange) }
// Identifiers: only if range is within identifiers index and not delegated to Grazie // Identifiers: only if range is within identifiers index and not delegated to Grazie
if (overlaps(index.identifiers) && !grazieIds) return IDENTIFIER_TOKENIZER if (et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.IDENTIFIER && overlaps(index.identifiers) && !grazieIds) return IDENTIFIER_TOKENIZER
// Comments: only if not delegated to Grazie and overlapping indexed comments // Comments: only if not delegated to Grazie and overlapping indexed comments
if (!preferGrazie && overlaps(index.comments)) return COMMENT_TEXT_TOKENIZER if ((et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.LINE_COMMENT || et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.BLOCK_COMMENT) && overlaps(index.comments) && !preferGrazie) return COMMENT_TEXT_TOKENIZER
// Strings: only if not delegated to Grazie, literals checking enabled, and overlapping indexed strings // Strings: only if not delegated to Grazie, literals checking enabled, and overlapping indexed strings
if (!preferGrazie && settings.spellCheckStringLiterals && overlaps(index.strings)) return STRING_WITH_PRINTF_EXCLUDES if (et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.STRING && settings.spellCheckStringLiterals && overlaps(index.strings) && !preferGrazie) return STRING_WITH_PRINTF_EXCLUDES
return EMPTY_TOKENIZER return EMPTY_TOKENIZER
} }
@ -93,7 +108,7 @@ class LyngSpellcheckingStrategy : SpellcheckingStrategy() {
} }
private object IDENTIFIER_TOKENIZER : Tokenizer<PsiElement>() { private object IDENTIFIER_TOKENIZER : Tokenizer<PsiElement>() {
private val splitter = PlainTextSplitter.getInstance() private val splitter = com.intellij.spellchecker.inspections.IdentifierSplitter.getInstance()
override fun tokenize(element: PsiElement, consumer: TokenConsumer) { override fun tokenize(element: PsiElement, consumer: TokenConsumer) {
val text = element.text val text = element.text
if (text.isNullOrEmpty()) return if (text.isNullOrEmpty()) return

View File

@ -1,3 +1,20 @@
/*
* Copyright 2026 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.
*
*/
/* /*
* Ensure external/bundled docs are registered in BuiltinDocRegistry * Ensure external/bundled docs are registered in BuiltinDocRegistry
* so completion/quickdoc can resolve things like lyng.io.fs.Path. * so completion/quickdoc can resolve things like lyng.io.fs.Path.
@ -6,6 +23,7 @@ package net.sergeych.lyng.idea.util
import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.diagnostic.Logger
import net.sergeych.lyng.idea.docs.FsDocsFallback import net.sergeych.lyng.idea.docs.FsDocsFallback
import net.sergeych.lyng.idea.docs.ProcessDocsFallback
object DocsBootstrap { object DocsBootstrap {
private val log = Logger.getInstance(DocsBootstrap::class.java) private val log = Logger.getInstance(DocsBootstrap::class.java)
@ -20,20 +38,32 @@ object DocsBootstrap {
} }
} }
private fun tryLoadExternal(): Boolean = try { private fun tryLoadExternal(): Boolean {
val cls = Class.forName("net.sergeych.lyngio.docs.FsBuiltinDocs") var anyLoaded = false
val m = cls.getMethod("ensure") try {
m.invoke(null) val cls = Class.forName("net.sergeych.lyngio.docs.FsBuiltinDocs")
log.info("[LYNG_DEBUG] DocsBootstrap: external docs loaded: net.sergeych.lyngio.docs.FsBuiltinDocs.ensure() OK") val m = cls.getMethod("ensure")
true m.invoke(null)
} catch (_: Throwable) { log.info("[LYNG_DEBUG] DocsBootstrap: external docs loaded: net.sergeych.lyngio.docs.FsBuiltinDocs.ensure() OK")
false anyLoaded = true
} catch (_: Throwable) {}
try {
val cls = Class.forName("net.sergeych.lyngio.docs.ProcessBuiltinDocs")
val m = cls.getMethod("ensure")
m.invoke(null)
log.info("[LYNG_DEBUG] DocsBootstrap: external docs loaded: net.sergeych.lyngio.docs.ProcessBuiltinDocs.ensure() OK")
anyLoaded = true
} catch (_: Throwable) {}
return anyLoaded
} }
private fun trySeedFallback(): Boolean = try { private fun trySeedFallback(): Boolean = try {
val seeded = FsDocsFallback.ensureOnce() val seededFs = FsDocsFallback.ensureOnce()
val seededProcess = ProcessDocsFallback.ensureOnce()
val seeded = seededFs || seededProcess
if (seeded) { if (seeded) {
log.info("[LYNG_DEBUG] DocsBootstrap: external docs not found; seeded plugin fallback for lyng.io.fs") log.info("[LYNG_DEBUG] DocsBootstrap: external docs not found; seeded plugin fallback for lyng.io.fs/process")
} else { } else {
log.info("[LYNG_DEBUG] DocsBootstrap: external docs not found; no fallback seeded") log.info("[LYNG_DEBUG] DocsBootstrap: external docs not found; no fallback seeded")
} }

View File

@ -16,8 +16,8 @@
--> -->
<idea-plugin> <idea-plugin>
<!-- Open-ended compatibility: 2024.3+ (build 243 and newer) --> <!-- Open-ended compatibility: 2024.1+ (build 241 and newer) -->
<idea-version since-build="243"/> <idea-version since-build="241"/>
<id>net.sergeych.lyng.idea</id> <id>net.sergeych.lyng.idea</id>
<name>Lyng</name> <name>Lyng</name>
<vendor email="real.sergeych@gmail.com">Sergey Chernov</vendor> <vendor email="real.sergeych@gmail.com">Sergey Chernov</vendor>

View File

@ -1,3 +1,20 @@
/*
* Copyright 2026 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.completion package net.sergeych.lyng.idea.completion
import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.testFramework.fixtures.BasePlatformTestCase
@ -118,4 +135,41 @@ class LyngCompletionMemberTest : BasePlatformTestCase() {
// Heuristic: we expect more than a couple of items (not just size/toList) // Heuristic: we expect more than a couple of items (not just size/toList)
assertTrue("Too few member suggestions after list literal: $items", items.size >= 3) assertTrue("Too few member suggestions after list literal: $items", items.size >= 3)
} }
fun test_ProcessModule_Completion() {
val code = """
import lyng.io.process
Process.<caret>
""".trimIndent()
val imported = listOf("lyng.io.process")
ensureDocs(imported)
val items = complete(code)
assertTrue("Should contain 'execute'", items.contains("execute"))
assertTrue("Should contain 'shell'", items.contains("shell"))
}
fun test_RunningProcess_Completion() {
val code = """
import lyng.io.process
val p = Process.shell("ls")
p.<caret>
""".trimIndent()
val imported = listOf("lyng.io.process")
ensureDocs(imported)
val items = complete(code)
assertTrue("Should contain 'stdout'", items.contains("stdout"))
assertTrue("Should contain 'waitFor'", items.contains("waitFor"))
assertTrue("Should contain 'signal'", items.contains("signal"))
}
fun test_RegistryDirect() {
DocsBootstrap.ensure()
val docs = BuiltinDocRegistry.docsForModule("lyng.io.process")
assertTrue("Docs for lyng.io.process should not be empty", docs.isNotEmpty())
val processClass = docs.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == "Process" }
assertNotNull("Should contain Process class", processClass)
assertTrue("Process should have members", processClass!!.members.isNotEmpty())
}
} }

View File

@ -32,6 +32,7 @@ group = "net.sergeych"
version = "0.0.1-SNAPSHOT" version = "0.0.1-SNAPSHOT"
kotlin { kotlin {
jvmToolchain(17)
jvm() jvm()
androidTarget { androidTarget {
publishLibraryVariants("release") publishLibraryVariants("release")

View File

@ -39,43 +39,174 @@ object FsBuiltinDocs {
name = "Path", name = "Path",
doc = "Filesystem path class. Construct with a string: `Path(\"/tmp\")`." doc = "Filesystem path class. Construct with a string: `Path(\"/tmp\")`."
) { ) {
// Common instance methods (subset sufficient for Quick Docs) method(
name = "name",
doc = "Base name of the path (last segment).",
returns = type("lyng.String")
)
method(
name = "parent",
doc = "Parent directory as a Path or null if none.",
returns = type("Path", nullable = true)
)
method(
name = "segments",
doc = "List of path segments.",
returns = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.String")))
)
method( method(
name = "exists", name = "exists",
doc = "Whether the path exists on the filesystem.", doc = "Check whether this path exists on the filesystem.",
returns = type("lyng.Bool") returns = type("lyng.Bool")
) )
method( method(
name = "isFile", name = "isFile",
doc = "Whether the path exists and is a file.", doc = "True if this path is a regular file (based on cached metadata).",
returns = type("lyng.Bool") returns = type("lyng.Bool")
) )
method( method(
name = "isDir", name = "isDirectory",
doc = "Whether the path exists and is a directory.", doc = "True if this path is a directory (based on cached metadata).",
returns = type("lyng.Bool") returns = type("lyng.Bool")
) )
method(
name = "size",
doc = "File size in bytes, or null when unavailable.",
returns = type("lyng.Int", nullable = true)
)
method(
name = "createdAt",
doc = "Creation time as `Instant`, or null when unavailable.",
returns = type("lyng.Instant", nullable = true)
)
method(
name = "createdAtMillis",
doc = "Creation time in milliseconds since epoch, or null when unavailable.",
returns = type("lyng.Int", nullable = true)
)
method(
name = "modifiedAt",
doc = "Last modification time as `Instant`, or null when unavailable.",
returns = type("lyng.Instant", nullable = true)
)
method(
name = "modifiedAtMillis",
doc = "Last modification time in milliseconds since epoch, or null when unavailable.",
returns = type("lyng.Int", nullable = true)
)
method(
name = "list",
doc = "List directory entries as `Path` objects.",
returns = TypeGenericDoc(type("lyng.List"), listOf(type("Path")))
)
method(
name = "readBytes",
doc = "Read the entire file into a binary buffer.",
returns = type("lyng.Buffer")
)
method(
name = "writeBytes",
doc = "Write a binary buffer to the file, replacing content.",
params = listOf(ParamDoc("bytes", type("lyng.Buffer")))
)
method(
name = "appendBytes",
doc = "Append a binary buffer to the end of the file.",
params = listOf(ParamDoc("bytes", type("lyng.Buffer")))
)
method( method(
name = "readUtf8", name = "readUtf8",
doc = "Read the entire file as UTF-8 string.", doc = "Read the entire file as a UTF-8 string.",
returns = type("lyng.String") returns = type("lyng.String")
) )
method( method(
name = "writeUtf8", name = "writeUtf8",
doc = "Write UTF-8 string to the file (overwrite).", doc = "Write a UTF-8 string to the file, replacing content.",
params = listOf(ParamDoc("text", type("lyng.String"))) params = listOf(ParamDoc("text", type("lyng.String")))
) )
method( method(
name = "bytes", name = "appendUtf8",
doc = "Iterate file content as `Buffer` chunks.", doc = "Append UTF-8 text to the end of the file.",
params = listOf(ParamDoc("text", type("lyng.String")))
)
method(
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")))
)
method(
name = "mkdirs",
doc = "Create directories (like `mkdir -p`). If `mustCreate` is true and the path already exists, the call fails.",
params = listOf(ParamDoc("mustCreate", type("lyng.Bool")))
)
method(
name = "move",
doc = "Move this path to a new location. `to` may be a `Path` or `String`.",
params = listOf(ParamDoc("to"), ParamDoc("overwrite", type("lyng.Bool")))
)
method(
name = "delete",
doc = "Delete this path. `recursively=true` removes directories with their contents.",
params = listOf(ParamDoc("mustExist", type("lyng.Bool")), ParamDoc("recursively", type("lyng.Bool")))
)
method(
name = "copy",
doc = "Copy this path to a new location. `to` may be a `Path` or `String`.",
params = listOf(ParamDoc("to"), ParamDoc("overwrite", type("lyng.Bool")))
)
method(
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")))
)
method(
name = "readChunks",
doc = "Read file in fixed-size chunks as an iterator of `Buffer`.",
params = listOf(ParamDoc("size", type("lyng.Int"))), params = listOf(ParamDoc("size", type("lyng.Int"))),
returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.Buffer"))) returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.Buffer")))
) )
method( method(
name = "lines", name = "readUtf8Chunks",
doc = "Iterate file as lines of text.", 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"))) returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.String")))
) )
method(
name = "lines",
doc = "Iterate lines of the file as `String` values.",
returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.String")))
)
}
classDoc(
name = "BytesIterator",
doc = "Iterator over binary chunks."
) {
method("iterator", "Return this iterator instance.", returns = type("BytesIterator"))
method("hasNext", "Whether there is another chunk available.", returns = type("lyng.Bool"))
method("next", "Return the next chunk as a `Buffer`.", returns = type("lyng.Buffer"))
method("cancelIteration", "Stop the iteration early.")
}
classDoc(
name = "StringChunksIterator",
doc = "Iterator over UTF-8 text chunks."
) {
method("iterator", "Return this iterator instance.", returns = type("StringChunksIterator"))
method("hasNext", "Whether there is another chunk available.", returns = type("lyng.Bool"))
method("next", "Return the next UTF-8 chunk as a `String`.", returns = type("lyng.String"))
method("cancelIteration", "Stop the iteration early.")
}
classDoc(
name = "LinesIterator",
doc = "Iterator that yields lines of text."
) {
method("iterator", "Return this iterator instance.", returns = type("LinesIterator"))
method("hasNext", "Whether another line is available.", returns = type("lyng.Bool"))
method("next", "Return the next line as `String`.", returns = type("lyng.String"))
method("cancelIteration", "Stop the iteration early.")
} }
// Top-level exported constants // Top-level exported constants

View File

@ -0,0 +1,117 @@
/*
* Copyright 2026 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.lyngio.docs
import net.sergeych.lyng.miniast.BuiltinDocRegistry
import net.sergeych.lyng.miniast.ParamDoc
import net.sergeych.lyng.miniast.type
object ProcessBuiltinDocs {
private var registered = false
fun ensure() {
if (registered) return
BuiltinDocRegistry.module("lyng.io.process") {
classDoc(
name = "Process",
doc = "Process execution and control."
) {
method(
name = "execute",
doc = "Execute a process with arguments.",
params = listOf(ParamDoc("executable", type("lyng.String")), ParamDoc("args", type("lyng.List"))),
returns = type("RunningProcess"),
isStatic = true
)
method(
name = "shell",
doc = "Execute a command via system shell.",
params = listOf(ParamDoc("command", type("lyng.String"))),
returns = type("RunningProcess"),
isStatic = true
)
}
classDoc(
name = "Platform",
doc = "Platform information."
) {
method(
name = "details",
doc = "Get platform core details.",
returns = type("lyng.Map"),
isStatic = true
)
method(
name = "isSupported",
doc = "Check if processes are supported on this platform.",
returns = type("lyng.Bool"),
isStatic = true
)
}
classDoc(
name = "RunningProcess",
doc = "Handle to a running process."
) {
method(
name = "stdout",
doc = "Get standard output stream as a Flow of lines.",
returns = type("lyng.Flow")
)
method(
name = "stderr",
doc = "Get standard error stream as a Flow of lines.",
returns = type("lyng.Flow")
)
method(
name = "signal",
doc = "Send a signal to the process (e.g. 'SIGINT', 'SIGTERM', 'SIGKILL').",
params = listOf(ParamDoc("signal", type("lyng.String")))
)
method(
name = "waitFor",
doc = "Wait for the process to exit and return its exit code.",
returns = type("lyng.Int")
)
method(
name = "destroy",
doc = "Forcefully terminate the process."
)
}
// Top-level exported constants
valDoc(
name = "Process",
doc = "Process execution and control.",
type = type("Process")
)
valDoc(
name = "Platform",
doc = "Platform information.",
type = type("Platform")
)
valDoc(
name = "RunningProcess",
doc = "Handle to a running process.",
type = type("RunningProcess")
)
}
registered = true
}
}

View File

@ -46,6 +46,7 @@ buildkonfig {
} }
kotlin { kotlin {
jvmToolchain(17)
jvm() jvm()
androidTarget { androidTarget {
publishLibraryVariants("release") publishLibraryVariants("release")

View File

@ -51,6 +51,32 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
defaultVisibility: Visibility = Visibility.Public, defaultVisibility: Visibility = Visibility.Public,
declaringClass: net.sergeych.lyng.obj.ObjClass? = scope.currentClassCtx declaringClass: net.sergeych.lyng.obj.ObjClass? = scope.currentClassCtx
) { ) {
// Fast path for simple positional-only calls with no ellipsis and no defaults
if (arguments.named.isEmpty() && !arguments.tailBlockMode) {
var hasComplex = false
for (p in params) {
if (p.isEllipsis || p.defaultValue != null) {
hasComplex = true
break
}
}
if (!hasComplex) {
if (arguments.list.size != params.size)
scope.raiseIllegalArgument("expected ${params.size} arguments, got ${arguments.list.size}")
for (i in params.indices) {
val a = params[i]
val value = arguments.list[i]
scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable,
value.byValueCopy(),
a.visibility ?: defaultVisibility,
recordType = ObjRecord.Type.Argument,
declaringClass = declaringClass)
}
return
}
}
fun assign(a: Item, value: Obj) { fun assign(a: Item, value: Obj) {
scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable, scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable,
value.byValueCopy(), value.byValueCopy(),

View File

@ -169,9 +169,9 @@ class Compiler(
miniSink?.onScriptStart(start) miniSink?.onScriptStart(start)
do { do {
val t = cc.current() val t = cc.current()
if (t.type == Token.Type.NEWLINE || t.type == Token.Type.SINLGE_LINE_COMMENT || t.type == Token.Type.MULTILINE_COMMENT) { if (t.type == Token.Type.NEWLINE || t.type == Token.Type.SINGLE_LINE_COMMENT || t.type == Token.Type.MULTILINE_COMMENT) {
when (t.type) { when (t.type) {
Token.Type.SINLGE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> pushPendingDocToken(t) Token.Type.SINGLE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> pushPendingDocToken(t)
Token.Type.NEWLINE -> { Token.Type.NEWLINE -> {
// A standalone newline not immediately following a comment resets doc buffer // A standalone newline not immediately following a comment resets doc buffer
if (!prevWasComment) clearPendingDoc() else prevWasComment = false if (!prevWasComment) clearPendingDoc() else prevWasComment = false
@ -316,7 +316,7 @@ class Compiler(
} }
Token.Type.LABEL -> continue Token.Type.LABEL -> continue
Token.Type.SINLGE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> continue Token.Type.SINGLE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> continue
Token.Type.NEWLINE -> continue Token.Type.NEWLINE -> continue
@ -407,7 +407,7 @@ class Compiler(
val t = cc.next() val t = cc.next()
val startPos = t.pos val startPos = t.pos
when (t.type) { when (t.type) {
// Token.Type.NEWLINE, Token.Type.SINLGE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT-> { // Token.Type.NEWLINE, Token.Type.SINGLE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT-> {
// continue // continue
// } // }
@ -606,7 +606,7 @@ class Compiler(
// to skip in parseExpression: // to skip in parseExpression:
val current = cc.current() val current = cc.current()
val right = val right =
if (current.type == Token.Type.NEWLINE || current.type == Token.Type.SINLGE_LINE_COMMENT) if (current.type == Token.Type.NEWLINE || current.type == Token.Type.SINGLE_LINE_COMMENT)
null null
else else
parseExpression() parseExpression()
@ -887,7 +887,7 @@ class Compiler(
} }
Token.Type.NEWLINE -> {} Token.Type.NEWLINE -> {}
Token.Type.MULTILINE_COMMENT, Token.Type.SINLGE_LINE_COMMENT -> {} Token.Type.MULTILINE_COMMENT, Token.Type.SINGLE_LINE_COMMENT -> {}
Token.Type.ID -> { Token.Type.ID -> {
// visibility // visibility
@ -2734,7 +2734,8 @@ class Compiler(
doc = declDocLocal, doc = declDocLocal,
nameStart = nameStartPos, nameStart = nameStartPos,
receiver = receiverMini, receiver = receiverMini,
isExtern = actualExtern isExtern = actualExtern,
isStatic = isStatic
) )
miniSink?.onFunDecl(node) miniSink?.onFunDecl(node)
pendingDeclDoc = null pendingDeclDoc = null
@ -2941,10 +2942,11 @@ class Compiler(
doc = declDocLocal, doc = declDocLocal,
nameStart = nameStartPos, nameStart = nameStartPos,
receiver = receiverMini, receiver = receiverMini,
isExtern = actualExtern isExtern = actualExtern,
isStatic = isStatic
) )
miniSink?.onFunDecl(node)
miniSink?.onExitFunction(cc.currentPos()) miniSink?.onExitFunction(cc.currentPos())
miniSink?.onFunDecl(node)
} }
} }
@ -3002,7 +3004,8 @@ class Compiler(
initRange = null, initRange = null,
doc = pendingDeclDoc, doc = pendingDeclDoc,
nameStart = namePos, nameStart = namePos,
isExtern = actualExtern isExtern = actualExtern,
isStatic = false
) )
miniSink?.onValDecl(node) miniSink?.onValDecl(node)
} }
@ -3194,7 +3197,8 @@ class Compiler(
doc = pendingDeclDoc, doc = pendingDeclDoc,
nameStart = nameStartPos, nameStart = nameStartPos,
receiver = receiverMini, receiver = receiverMini,
isExtern = actualExtern isExtern = actualExtern,
isStatic = isStatic
) )
miniSink?.onValDecl(node) miniSink?.onValDecl(node)
pendingDeclDoc = null pendingDeclDoc = null

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -238,7 +238,7 @@ class CompilerContext(val tokens: List<Token>) {
} }
} }
Token.Type.MULTILINE_COMMENT, Token.Type.SINLGE_LINE_COMMENT, Token.Type.NEWLINE -> {} Token.Type.MULTILINE_COMMENT, Token.Type.SINGLE_LINE_COMMENT, Token.Type.NEWLINE -> {}
else -> { else -> {
restorePos(pos); return false restorePos(pos); return false
} }
@ -265,6 +265,6 @@ class CompilerContext(val tokens: List<Token>) {
} }
companion object { companion object {
val wstokens = setOf(Token.Type.NEWLINE, Token.Type.MULTILINE_COMMENT, Token.Type.SINLGE_LINE_COMMENT) val wstokens = setOf(Token.Type.NEWLINE, Token.Type.MULTILINE_COMMENT, Token.Type.SINGLE_LINE_COMMENT)
} }
} }

View File

@ -131,7 +131,7 @@ private class Parser(fromPos: Pos) {
pos.advance() pos.advance()
val body = loadToEndOfLine() val body = loadToEndOfLine()
// Include the leading '//' and do not trim; keep exact lexeme (excluding preceding codepoint) // Include the leading '//' and do not trim; keep exact lexeme (excluding preceding codepoint)
Token("//" + body, from, Token.Type.SINLGE_LINE_COMMENT) Token("//" + body, from, Token.Type.SINGLE_LINE_COMMENT)
} }
'*' -> { '*' -> {

View File

@ -70,9 +70,8 @@ open class Scope(
internal fun findExtension(receiverClass: ObjClass, name: String): ObjRecord? { internal fun findExtension(receiverClass: ObjClass, name: String): ObjRecord? {
var s: Scope? = this var s: Scope? = this
val visited = HashSet<Long>(4) var hops = 0
while (s != null) { while (s != null && hops++ < 1024) {
if (!visited.add(s.frameId)) break
// Proximity rule: check all extensions in the current scope before going to parent. // Proximity rule: check all extensions in the current scope before going to parent.
// Priority within scope: more specific class in MRO wins. // Priority within scope: more specific class in MRO wins.
for (cls in receiverClass.mro) { for (cls in receiverClass.mro) {
@ -108,7 +107,7 @@ open class Scope(
*/ */
internal fun tryGetLocalRecord(s: Scope, name: String, caller: net.sergeych.lyng.obj.ObjClass?): ObjRecord? { internal fun tryGetLocalRecord(s: Scope, name: String, caller: net.sergeych.lyng.obj.ObjClass?): ObjRecord? {
caller?.let { ctx -> caller?.let { ctx ->
s.objects["${ctx.className}::$name"]?.let { rec -> s.objects[ctx.mangledName(name)]?.let { rec ->
if (rec.visibility == Visibility.Private) return rec if (rec.visibility == Visibility.Private) return rec
} }
} }
@ -116,7 +115,7 @@ open class Scope(
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec
} }
caller?.let { ctx -> caller?.let { ctx ->
s.localBindings["${ctx.className}::$name"]?.let { rec -> s.localBindings[ctx.mangledName(name)]?.let { rec ->
if (rec.visibility == Visibility.Private) return rec if (rec.visibility == Visibility.Private) return rec
} }
} }
@ -132,11 +131,10 @@ open class Scope(
internal fun chainLookupIgnoreClosure(name: String, followClosure: Boolean = false, caller: net.sergeych.lyng.obj.ObjClass? = null): ObjRecord? { internal fun chainLookupIgnoreClosure(name: String, followClosure: Boolean = false, caller: net.sergeych.lyng.obj.ObjClass? = null): ObjRecord? {
var s: Scope? = this var s: Scope? = this
// use frameId to detect unexpected structural cycles in the parent chain // use hop counter to detect unexpected structural cycles in the parent chain
val visited = HashSet<Long>(4) var hops = 0
val effectiveCaller = caller ?: currentClassCtx val effectiveCaller = caller ?: currentClassCtx
while (s != null) { while (s != null && hops++ < 1024) {
if (!visited.add(s.frameId)) return null
tryGetLocalRecord(s, name, effectiveCaller)?.let { return it } tryGetLocalRecord(s, name, effectiveCaller)?.let { return it }
s = if (followClosure && s is ClosureScope) s.closureScope else s.parent s = if (followClosure && s is ClosureScope) s.closureScope else s.parent
} }
@ -155,9 +153,8 @@ open class Scope(
tryGetLocalRecord(this, name, currentClassCtx)?.let { return it } tryGetLocalRecord(this, name, currentClassCtx)?.let { return it }
// 2) walk parents for plain locals/bindings only // 2) walk parents for plain locals/bindings only
var s = parent var s = parent
val visited = HashSet<Long>(4) var hops = 0
while (s != null) { while (s != null && hops++ < 1024) {
if (!visited.add(s.frameId)) return null
tryGetLocalRecord(s, name, currentClassCtx)?.let { return it } tryGetLocalRecord(s, name, currentClassCtx)?.let { return it }
s = s.parent s = s.parent
} }
@ -182,9 +179,8 @@ open class Scope(
*/ */
internal fun chainLookupWithMembers(name: String, caller: net.sergeych.lyng.obj.ObjClass? = currentClassCtx, followClosure: Boolean = false): ObjRecord? { internal fun chainLookupWithMembers(name: String, caller: net.sergeych.lyng.obj.ObjClass? = currentClassCtx, followClosure: Boolean = false): ObjRecord? {
var s: Scope? = this var s: Scope? = this
val visited = HashSet<Long>(4) var hops = 0
while (s != null) { while (s != null && hops++ < 1024) {
if (!visited.add(s.frameId)) return null
tryGetLocalRecord(s, name, caller)?.let { return it } tryGetLocalRecord(s, name, caller)?.let { return it }
for (cls in s.thisObj.objClass.mro) { for (cls in s.thisObj.objClass.mro) {
s.extensions[cls]?.get(name)?.let { return it } s.extensions[cls]?.get(name)?.let { return it }
@ -396,6 +392,20 @@ open class Scope(
nameToSlot[name]?.let { slots[it] = record } nameToSlot[name]?.let { slots[it] = record }
} }
/**
* Clear all references and maps to prevent memory leaks when pooled.
*/
fun scrub() {
this.parent = null
this.skipScopeCreation = false
this.currentClassCtx = null
objects.clear()
slots.clear()
nameToSlot.clear()
localBindings.clear()
extensions.clear()
}
/** /**
* Reset this scope instance so it can be safely reused as a fresh child frame. * Reset this scope instance so it can be safely reused as a fresh child frame.
* Clears locals and slots, assigns new frameId, and sets parent/args/pos/thisObj. * Clears locals and slots, assigns new frameId, and sets parent/args/pos/thisObj.
@ -414,7 +424,6 @@ open class Scope(
nameToSlot.clear() nameToSlot.clear()
localBindings.clear() localBindings.clear()
extensions.clear() extensions.clear()
this.currentClassCtx = parent?.currentClassCtx
// Now safe to validate and re-parent // Now safe to validate and re-parent
ensureNoCycle(parent) ensureNoCycle(parent)
this.parent = parent this.parent = parent
@ -534,11 +543,15 @@ open class Scope(
} }
} }
// Map to a slot for fast local access (ensure consistency) // Map to a slot for fast local access (ensure consistency)
val idx = getSlotIndexOf(name) if (nameToSlot.isEmpty()) {
if (idx == null) {
allocateSlotFor(name, rec) allocateSlotFor(name, rec)
} else { } else {
slots[idx] = rec val idx = nameToSlot[name]
if (idx == null) {
allocateSlotFor(name, rec)
} else {
slots[idx] = rec
}
} }
return rec return rec
} }

View File

@ -22,7 +22,7 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
throw ScriptError(pos, text) throw ScriptError(pos, text)
} }
val isComment: Boolean by lazy { type == Type.SINLGE_LINE_COMMENT || type == Type.MULTILINE_COMMENT } val isComment: Boolean by lazy { type == Type.SINGLE_LINE_COMMENT || type == Type.MULTILINE_COMMENT }
fun isId(text: String) = fun isId(text: String) =
type == Type.ID && value == text type == Type.ID && value == text
@ -41,7 +41,7 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
SHUTTLE, SHUTTLE,
AND, BITAND, OR, BITOR, BITXOR, NOT, BITNOT, DOT, ARROW, EQARROW, QUESTION, COLONCOLON, AND, BITAND, OR, BITOR, BITXOR, NOT, BITNOT, DOT, ARROW, EQARROW, QUESTION, COLONCOLON,
SHL, SHR, SHL, SHR,
SINLGE_LINE_COMMENT, MULTILINE_COMMENT, SINGLE_LINE_COMMENT, MULTILINE_COMMENT,
LABEL, ATLABEL, // label@ at@label LABEL, ATLABEL, // label@ at@label
// type-checking/casting // type-checking/casting
AS, ASNULL, OBJECT, AS, ASNULL, OBJECT,

View File

@ -67,7 +67,7 @@ private fun kindOf(type: Type, value: String): HighlightKind? = when (type) {
Type.REGEX -> HighlightKind.Regex Type.REGEX -> HighlightKind.Regex
// comments // comments
Type.SINLGE_LINE_COMMENT, Type.MULTILINE_COMMENT -> HighlightKind.Comment Type.SINGLE_LINE_COMMENT, Type.MULTILINE_COMMENT -> HighlightKind.Comment
// punctuation // punctuation
Type.LPAREN, Type.RPAREN, Type.LBRACE, Type.RBRACE, Type.LBRACKET, Type.RBRACKET, Type.LPAREN, Type.RPAREN, Type.LBRACE, Type.RBRACE, Type.LBRACKET, Type.RBRACKET,

View File

@ -148,6 +148,8 @@ class ModuleDocsBuilder internal constructor(private val moduleName: String) {
body = null, body = null,
doc = md, doc = md,
nameStart = Pos.builtIn, nameStart = Pos.builtIn,
isExtern = false,
isStatic = false
) )
} }
@ -167,6 +169,8 @@ class ModuleDocsBuilder internal constructor(private val moduleName: String) {
initRange = null, initRange = null,
doc = md, doc = md,
nameStart = Pos.builtIn, nameStart = Pos.builtIn,
isExtern = false,
isStatic = false
) )
} }

View File

@ -33,6 +33,7 @@ data class CompletionItem(
val kind: Kind, val kind: Kind,
val tailText: String? = null, val tailText: String? = null,
val typeText: String? = null, val typeText: String? = null,
val priority: Double = 0.0,
) )
enum class Kind { Function, Class_, Enum, Value, Method, Field } enum class Kind { Function, Class_, Enum, Value, Method, Field }
@ -107,7 +108,7 @@ object CompletionEngineLight {
val locals = DocLookupUtils.extractLocalsAt(text, caret) val locals = DocLookupUtils.extractLocalsAt(text, caret)
for (name in locals) { for (name in locals) {
if (name.startsWith(prefix, true)) { if (name.startsWith(prefix, true)) {
out.add(CompletionItem(name, Kind.Value)) out.add(CompletionItem(name, Kind.Value, priority = 150.0))
} }
} }
@ -168,17 +169,17 @@ object CompletionEngineLight {
when (node) { when (node) {
is MiniFunDecl -> { is MiniFunDecl -> {
for (p in node.params) { for (p in node.params) {
add(CompletionItem(p.name, Kind.Value, typeText = typeOf(p.type))) add(CompletionItem(p.name, Kind.Value, typeText = typeOf(p.type), priority = 200.0))
} }
} }
is MiniClassDecl -> { is MiniClassDecl -> {
// Propose constructor parameters (ctorFields) // Propose constructor parameters (ctorFields)
for (p in node.ctorFields) { for (p in node.ctorFields) {
add(CompletionItem(p.name, if (p.mutable) Kind.Value else Kind.Field, typeText = typeOf(p.type))) add(CompletionItem(p.name, if (p.mutable) Kind.Value else Kind.Field, typeText = typeOf(p.type), priority = 200.0))
} }
// Propose class-level fields // Propose class-level fields
for (p in node.classFields) { for (p in node.classFields) {
add(CompletionItem(p.name, if (p.mutable) Kind.Value else Kind.Field, typeText = typeOf(p.type))) add(CompletionItem(p.name, if (p.mutable) Kind.Value else Kind.Field, typeText = typeOf(p.type), priority = 100.0))
} }
// Process members (methods/fields) // Process members (methods/fields)
for (m in node.members) { for (m in node.members) {
@ -189,10 +190,10 @@ object CompletionEngineLight {
when (m) { when (m) {
is MiniMemberFunDecl -> { is MiniMemberFunDecl -> {
val params = m.params.joinToString(", ") { it.name } val params = m.params.joinToString(", ") { it.name }
add(CompletionItem(m.name, Kind.Method, tailText = "(${params})", typeText = typeOf(m.returnType))) add(CompletionItem(m.name, Kind.Method, tailText = "(${params})", typeText = typeOf(m.returnType), priority = 100.0))
} }
is MiniMemberValDecl -> { is MiniMemberValDecl -> {
add(CompletionItem(m.name, if (m.mutable) Kind.Value else Kind.Field, typeText = typeOf(m.type))) add(CompletionItem(m.name, if (m.mutable) Kind.Value else Kind.Field, typeText = typeOf(m.type), priority = 100.0))
} }
is MiniInitDecl -> {} is MiniInitDecl -> {}
} }
@ -200,7 +201,7 @@ object CompletionEngineLight {
} }
is MiniMemberFunDecl -> { is MiniMemberFunDecl -> {
for (p in node.params) { for (p in node.params) {
add(CompletionItem(p.name, Kind.Value, typeText = typeOf(p.type))) add(CompletionItem(p.name, Kind.Value, typeText = typeOf(p.type), priority = 200.0))
} }
} }
else -> {} else -> {}
@ -250,7 +251,7 @@ object CompletionEngineLight {
"Array" -> listOf("Collection", "Iterable").forEach { if (visited.add(it)) addMembersOf(it, false) } "Array" -> listOf("Collection", "Iterable").forEach { if (visited.add(it)) addMembersOf(it, false) }
} }
fun emitGroup(map: LinkedHashMap<String, MutableList<MiniMemberDecl>>) { fun emitGroup(map: LinkedHashMap<String, MutableList<MiniMemberDecl>>, groupPriority: Double) {
for (name in map.keys.sortedBy { it.lowercase() }) { for (name in map.keys.sortedBy { it.lowercase() }) {
val variants = map[name] ?: continue val variants = map[name] ?: continue
// Prefer a method with a known return type; else any method; else first variant // Prefer a method with a known return type; else any method; else first variant
@ -265,7 +266,7 @@ object CompletionEngineLight {
val params = rep.params.joinToString(", ") { it.name } val params = rep.params.joinToString(", ") { it.name }
val extra = variants.count { it is MiniMemberFunDecl } - 1 val extra = variants.count { it is MiniMemberFunDecl } - 1
val ov = if (extra > 0) " (+$extra overloads)" else "" val ov = if (extra > 0) " (+$extra overloads)" else ""
val ci = CompletionItem(name, Kind.Method, tailText = "(${params})$ov", typeText = typeOf(rep.returnType)) val ci = CompletionItem(name, Kind.Method, tailText = "(${params})$ov", typeText = typeOf(rep.returnType), priority = groupPriority)
if (ci.name.startsWith(prefix, true)) out += ci if (ci.name.startsWith(prefix, true)) out += ci
} }
is MiniMemberValDecl -> { is MiniMemberValDecl -> {
@ -273,7 +274,7 @@ object CompletionEngineLight {
val chosen = variants.asSequence() val chosen = variants.asSequence()
.filterIsInstance<MiniMemberValDecl>() .filterIsInstance<MiniMemberValDecl>()
.firstOrNull { it.type != null } ?: rep .firstOrNull { it.type != null } ?: rep
val ci = CompletionItem(name, Kind.Field, typeText = typeOf(chosen.type)) val ci = CompletionItem(name, Kind.Field, typeText = typeOf(chosen.type), priority = groupPriority)
if (ci.name.startsWith(prefix, true)) out += ci if (ci.name.startsWith(prefix, true)) out += ci
} }
is MiniInitDecl -> {} is MiniInitDecl -> {}
@ -281,8 +282,8 @@ object CompletionEngineLight {
} }
} }
emitGroup(directMap) emitGroup(directMap, 100.0)
emitGroup(inheritedMap) emitGroup(inheritedMap, 0.0)
// Supplement with extension members (both stdlib and local) // Supplement with extension members (both stdlib and local)
run { run {
@ -296,22 +297,22 @@ object CompletionEngineLight {
val ci = when (m) { val ci = when (m) {
is MiniMemberFunDecl -> { is MiniMemberFunDecl -> {
val params = m.params.joinToString(", ") { it.name } val params = m.params.joinToString(", ") { it.name }
CompletionItem(name, Kind.Method, tailText = "(${params})", typeText = typeOf(m.returnType)) CompletionItem(name, Kind.Method, tailText = "(${params})", typeText = typeOf(m.returnType), priority = 50.0)
} }
is MiniFunDecl -> { is MiniFunDecl -> {
val params = m.params.joinToString(", ") { it.name } val params = m.params.joinToString(", ") { it.name }
CompletionItem(name, Kind.Method, tailText = "(${params})", typeText = typeOf(m.returnType)) CompletionItem(name, Kind.Method, tailText = "(${params})", typeText = typeOf(m.returnType), priority = 50.0)
} }
is MiniMemberValDecl -> CompletionItem(name, Kind.Field, typeText = typeOf(m.type)) is MiniMemberValDecl -> CompletionItem(name, Kind.Field, typeText = typeOf(m.type), priority = 50.0)
is MiniValDecl -> CompletionItem(name, Kind.Field, typeText = typeOf(m.type)) is MiniValDecl -> CompletionItem(name, Kind.Field, typeText = typeOf(m.type), priority = 50.0)
else -> CompletionItem(name, Kind.Method, tailText = "()", typeText = null) else -> CompletionItem(name, Kind.Method, tailText = "()", typeText = null, priority = 50.0)
} }
if (ci.name.startsWith(prefix, true)) { if (ci.name.startsWith(prefix, true)) {
out += ci out += ci
already.add(name) already.add(name)
} }
} else { } else {
val ci = CompletionItem(name, Kind.Method, tailText = "()", typeText = null) val ci = CompletionItem(name, Kind.Method, tailText = "()", typeText = null, priority = 50.0)
if (ci.name.startsWith(prefix, true)) { if (ci.name.startsWith(prefix, true)) {
out += ci out += ci
already.add(name) already.add(name)

View File

@ -87,6 +87,7 @@ sealed interface MiniNamedDecl : MiniNode {
// Start position of the declaration name identifier in source; end can be derived as start + name.length // Start position of the declaration name identifier in source; end can be derived as start + name.length
val nameStart: Pos val nameStart: Pos
val isExtern: Boolean val isExtern: Boolean
val isStatic: Boolean
} }
sealed interface MiniDecl : MiniNamedDecl sealed interface MiniDecl : MiniNamedDecl
@ -114,7 +115,8 @@ data class MiniFunDecl(
override val doc: MiniDoc?, override val doc: MiniDoc?,
override val nameStart: Pos, override val nameStart: Pos,
val receiver: MiniTypeRef? = null, val receiver: MiniTypeRef? = null,
override val isExtern: Boolean = false override val isExtern: Boolean = false,
override val isStatic: Boolean = false,
) : MiniDecl ) : MiniDecl
data class MiniValDecl( data class MiniValDecl(
@ -126,7 +128,8 @@ data class MiniValDecl(
override val doc: MiniDoc?, override val doc: MiniDoc?,
override val nameStart: Pos, override val nameStart: Pos,
val receiver: MiniTypeRef? = null, val receiver: MiniTypeRef? = null,
override val isExtern: Boolean = false override val isExtern: Boolean = false,
override val isStatic: Boolean = false,
) : MiniDecl ) : MiniDecl
data class MiniClassDecl( data class MiniClassDecl(
@ -141,6 +144,7 @@ data class MiniClassDecl(
// Built-in extension: list of member declarations (functions and fields) // Built-in extension: list of member declarations (functions and fields)
val members: List<MiniMemberDecl> = emptyList(), val members: List<MiniMemberDecl> = emptyList(),
override val isExtern: Boolean = false, override val isExtern: Boolean = false,
override val isStatic: Boolean = false,
val isObject: Boolean = false val isObject: Boolean = false
) : MiniDecl ) : MiniDecl
@ -150,7 +154,8 @@ data class MiniEnumDecl(
val entries: List<String>, val entries: List<String>,
override val doc: MiniDoc?, override val doc: MiniDoc?,
override val nameStart: Pos, override val nameStart: Pos,
override val isExtern: Boolean = false override val isExtern: Boolean = false,
override val isStatic: Boolean = false,
) : MiniDecl ) : MiniDecl
data class MiniCtorField( data class MiniCtorField(
@ -175,7 +180,7 @@ data class MiniIdentifier(
// --- Class member declarations (for built-in/registry docs) --- // --- Class member declarations (for built-in/registry docs) ---
sealed interface MiniMemberDecl : MiniNamedDecl { sealed interface MiniMemberDecl : MiniNamedDecl {
val isStatic: Boolean override val isStatic: Boolean
} }
data class MiniMemberFunDecl( data class MiniMemberFunDecl(
@ -319,7 +324,7 @@ class MiniAstBuilder : MiniAstSink {
returnType = attach.returnType, returnType = attach.returnType,
doc = attach.doc, doc = attach.doc,
nameStart = attach.nameStart, nameStart = attach.nameStart,
isStatic = false, // TODO: track static if needed isStatic = attach.isStatic,
isExtern = attach.isExtern, isExtern = attach.isExtern,
body = attach.body body = attach.body
) )
@ -359,7 +364,7 @@ class MiniAstBuilder : MiniAstSink {
initRange = attach.initRange, initRange = attach.initRange,
doc = attach.doc, doc = attach.doc,
nameStart = attach.nameStart, nameStart = attach.nameStart,
isStatic = false, // TODO: track static if needed isStatic = attach.isStatic,
isExtern = attach.isExtern isExtern = attach.isExtern
) )
// Duplicates for vals are rare but possible if Compiler calls it twice // Duplicates for vals are rare but possible if Compiler calls it twice

View File

@ -106,6 +106,34 @@ open class ObjClass(
val classId: Long = ClassIdGen.nextId() val classId: Long = ClassIdGen.nextId()
var layoutVersion: Int = 0 var layoutVersion: Int = 0
private val mangledNameCache = mutableMapOf<String, String>()
fun mangledName(name: String): String = mangledNameCache.getOrPut(name) { "$className::$name" }
/**
* Map of public member names to their effective storage keys in instanceScope.objects.
* This is pre-calculated to avoid MRO traversal and string concatenation during common access.
*/
val publicMemberResolution: Map<String, String> by lazy {
val res = mutableMapOf<String, String>()
// Traverse MRO in REVERSED order so that child classes override parent classes in the map.
for (cls in mro.reversed()) {
if (cls.className == "Obj") continue
for ((name, rec) in cls.members) {
if (rec.visibility == Visibility.Public) {
val key = if (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Delegated) cls.mangledName(name) else name
res[name] = key
}
}
cls.classScope?.objects?.forEach { (name, rec) ->
if (rec.visibility == Visibility.Public && (rec.value is Statement || rec.type == ObjRecord.Type.Delegated)) {
val key = if (rec.type == ObjRecord.Type.Delegated) cls.mangledName(name) else name
res[name] = key
}
}
}
res
}
val classNameObj by lazy { ObjString(className) } val classNameObj by lazy { ObjString(className) }
var constructorMeta: ArgsDeclaration? = null var constructorMeta: ArgsDeclaration? = null
@ -242,6 +270,43 @@ open class ObjClass(
return instance return instance
} }
/** Pre-calculated template for instanceScope.objects. */
private val instanceObjectsTemplate: Map<String, ObjRecord> by lazy {
val res = mutableMapOf<String, ObjRecord>()
for (cls in mro) {
// 1) members-defined methods and fields
for ((k, v) in cls.members) {
if (!v.isAbstract && (v.value is Statement || v.type == ObjRecord.Type.Delegated || v.type == ObjRecord.Type.Field)) {
val key = if (v.visibility == Visibility.Private || v.type == ObjRecord.Type.Field || v.type == ObjRecord.Type.Delegated) cls.mangledName(k) else k
if (!res.containsKey(key)) {
res[key] = v
}
}
}
// 2) class-scope members registered during class-body execution
cls.classScope?.objects?.forEach { (k, rec) ->
// ONLY copy methods and delegated members from class scope to instance scope.
// Fields in class scope are static fields and must NOT be per-instance.
if (!rec.isAbstract && (rec.value is Statement || rec.type == ObjRecord.Type.Delegated)) {
val key = if (rec.visibility == Visibility.Private || rec.type == ObjRecord.Type.Delegated) cls.mangledName(k) else k
// if not already present, copy reference for dispatch
if (!res.containsKey(key)) {
res[key] = rec
}
}
}
}
res
}
private val templateMethods: Map<String, ObjRecord> by lazy {
instanceObjectsTemplate.filter { it.value.type == ObjRecord.Type.Fun }
}
private val templateOthers: List<Pair<String, ObjRecord>> by lazy {
instanceObjectsTemplate.filter { it.value.type != ObjRecord.Type.Fun }.toList()
}
/** /**
* Create an instance of this class and initialize its [ObjInstance.instanceScope] with * Create an instance of this class and initialize its [ObjInstance.instanceScope] with
* methods. Does NOT run initializers or constructors. * methods. Does NOT run initializers or constructors.
@ -257,28 +322,9 @@ open class ObjClass(
// Expose instance methods (and other callable members) directly in the instance scope for fast lookup // Expose instance methods (and other callable members) directly in the instance scope for fast lookup
// This mirrors Obj.autoInstanceScope behavior for ad-hoc scopes and makes fb.method() resolution robust // This mirrors Obj.autoInstanceScope behavior for ad-hoc scopes and makes fb.method() resolution robust
for (cls in mro) { instance.instanceScope.objects.putAll(templateMethods)
// 1) members-defined methods and fields for (p in templateOthers) {
for ((k, v) in cls.members) { instance.instanceScope.objects[p.first] = p.second.copy()
if (!v.isAbstract && (v.value is Statement || v.type == ObjRecord.Type.Delegated || v.type == ObjRecord.Type.Field)) {
val key = if (v.visibility == Visibility.Private || v.type == ObjRecord.Type.Field || v.type == ObjRecord.Type.Delegated) "${cls.className}::$k" else k
if (!instance.instanceScope.objects.containsKey(key)) {
instance.instanceScope.objects[key] = if (v.type == ObjRecord.Type.Fun) v else v.copy()
}
}
}
// 2) class-scope members registered during class-body execution
cls.classScope?.objects?.forEach { (k, rec) ->
// ONLY copy methods and delegated members from class scope to instance scope.
// Fields in class scope are static fields and must NOT be per-instance.
if (!rec.isAbstract && (rec.value is Statement || rec.type == ObjRecord.Type.Delegated)) {
val key = if (rec.visibility == Visibility.Private || rec.type == ObjRecord.Type.Delegated) "${cls.className}::$k" else k
// if not already present, copy reference for dispatch
if (!instance.instanceScope.objects.containsKey(key)) {
instance.instanceScope.objects[key] = if (rec.type == ObjRecord.Type.Fun) rec else rec.copy()
}
}
}
} }
return instance return instance
} }
@ -327,7 +373,7 @@ open class ObjClass(
for (p in meta.params) { for (p in meta.params) {
val rec = instance.instanceScope.objects[p.name] val rec = instance.instanceScope.objects[p.name]
if (rec != null) { if (rec != null) {
val mangled = "${c.className}::${p.name}" val mangled = c.mangledName(p.name)
// Always point the mangled name to the current record to keep writes consistent // Always point the mangled name to the current record to keep writes consistent
// across re-bindings // across re-bindings
instance.instanceScope.objects[mangled] = rec instance.instanceScope.objects[mangled] = rec
@ -361,7 +407,7 @@ open class ObjClass(
for (p in meta.params) { for (p in meta.params) {
val rec = instance.instanceScope.objects[p.name] val rec = instance.instanceScope.objects[p.name]
if (rec != null) { if (rec != null) {
val mangled = "${c.className}::${p.name}" val mangled = c.mangledName(p.name)
instance.instanceScope.objects[mangled] = rec instance.instanceScope.objects[mangled] = rec
} }
} }

View File

@ -34,6 +34,18 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
override suspend fun readField(scope: Scope, name: String): ObjRecord { override suspend fun readField(scope: Scope, name: String): ObjRecord {
val caller = scope.currentClassCtx val caller = scope.currentClassCtx
// Fast path for public members when outside any class context
if (caller == null) {
objClass.publicMemberResolution[name]?.let { key ->
instanceScope.objects[key]?.let { rec ->
// Directly return fields to bypass resolveRecord overhead
if ((rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.ConstructorField) && !rec.isAbstract)
return rec
return resolveRecord(scope, rec, name, rec.declaringClass)
}
}
}
// 0. Private mangled of current class context // 0. Private mangled of current class context
caller?.let { c -> caller?.let { c ->
// Check for private methods/properties // Check for private methods/properties
@ -43,7 +55,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
} }
} }
// Check for private fields (stored in instanceScope) // Check for private fields (stored in instanceScope)
val mangled = "${c.className}::$name" val mangled = c.mangledName(name)
instanceScope.objects[mangled]?.let { rec -> instanceScope.objects[mangled]?.let { rec ->
if (rec.visibility == Visibility.Private) { if (rec.visibility == Visibility.Private) {
return resolveRecord(scope, rec, name, c) return resolveRecord(scope, rec, name, c)
@ -54,7 +66,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
// 1. MRO mangled storage // 1. MRO mangled storage
for (cls in objClass.mro) { for (cls in objClass.mro) {
if (cls.className == "Obj") break if (cls.className == "Obj") break
val mangled = "${cls.className}::$name" val mangled = cls.mangledName(name)
instanceScope.objects[mangled]?.let { rec -> instanceScope.objects[mangled]?.let { rec ->
if ((scope.thisObj === this && caller != null) || canAccessMember(rec.visibility, cls, caller)) { if ((scope.thisObj === this && caller != null) || canAccessMember(rec.visibility, cls, caller)) {
return resolveRecord(scope, rec, name, cls) return resolveRecord(scope, rec, name, cls)
@ -79,11 +91,11 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
if (obj.type.isArgument) return super.resolveRecord(scope, obj, name, decl) if (obj.type.isArgument) return super.resolveRecord(scope, obj, name, decl)
if (obj.type == ObjRecord.Type.Delegated) { if (obj.type == ObjRecord.Type.Delegated) {
val d = decl ?: obj.declaringClass val d = decl ?: obj.declaringClass
val storageName = "${d?.className}::$name" val storageName = d?.mangledName(name) ?: name
var del = instanceScope[storageName]?.delegate ?: obj.delegate var del = instanceScope[storageName]?.delegate ?: obj.delegate
if (del == null) { if (del == null) {
for (c in objClass.mro) { for (c in objClass.mro) {
del = instanceScope["${c.className}::$name"]?.delegate del = instanceScope[c.mangledName(name)]?.delegate
if (del != null) break if (del != null) break
} }
} }
@ -97,7 +109,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
var targetRec = obj var targetRec = obj
val d = decl ?: obj.declaringClass val d = decl ?: obj.declaringClass
if (d != null) { if (d != null) {
val mangled = "${d.className}::$name" val mangled = d.mangledName(name)
instanceScope.objects[mangled]?.let { instanceScope.objects[mangled]?.let {
targetRec = it targetRec = it
} }
@ -120,6 +132,24 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
willMutate(scope) willMutate(scope)
val caller = scope.currentClassCtx val caller = scope.currentClassCtx
// Fast path for public members when outside any class context
if (caller == null) {
objClass.publicMemberResolution[name]?.let { key ->
instanceScope.objects[key]?.let { rec ->
if (rec.effectiveWriteVisibility == Visibility.Public) {
// Skip property/delegated overhead if it's a plain mutable field
if (rec.type == ObjRecord.Type.Field && rec.isMutable && !rec.isAbstract) {
if (rec.value.assign(scope, newValue) == null)
rec.value = newValue
return
}
updateRecord(scope, rec, name, newValue, rec.declaringClass)
return
}
}
}
}
// 0. Private mangled of current class context // 0. Private mangled of current class context
caller?.let { c -> caller?.let { c ->
// Check for private methods/properties // Check for private methods/properties
@ -130,7 +160,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
} }
} }
// Check for private fields (stored in instanceScope) // Check for private fields (stored in instanceScope)
val mangled = "${c.className}::$name" val mangled = c.mangledName(name)
instanceScope.objects[mangled]?.let { rec -> instanceScope.objects[mangled]?.let { rec ->
if (rec.visibility == Visibility.Private) { if (rec.visibility == Visibility.Private) {
updateRecord(scope, rec, name, newValue, c) updateRecord(scope, rec, name, newValue, c)
@ -142,7 +172,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
// 1. MRO mangled storage // 1. MRO mangled storage
for (cls in objClass.mro) { for (cls in objClass.mro) {
if (cls.className == "Obj") break if (cls.className == "Obj") break
val mangled = "${cls.className}::$name" val mangled = cls.mangledName(name)
instanceScope.objects[mangled]?.let { rec -> instanceScope.objects[mangled]?.let { rec ->
if ((scope.thisObj === this && caller != null) || canAccessMember(rec.effectiveWriteVisibility, cls, caller)) { if ((scope.thisObj === this && caller != null) || canAccessMember(rec.effectiveWriteVisibility, cls, caller)) {
updateRecord(scope, rec, name, newValue, cls) updateRecord(scope, rec, name, newValue, cls)
@ -191,24 +221,42 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
scope: Scope, name: String, args: Arguments, scope: Scope, name: String, args: Arguments,
onNotFoundResult: (suspend () -> Obj?)? onNotFoundResult: (suspend () -> Obj?)?
): Obj { ): Obj {
// 0. Prefer private member of current class context val caller = scope.currentClassCtx
scope.currentClassCtx?.let { caller ->
val mangled = "${caller.className}::$name" // Fast path for public members when outside any class context
instanceScope.objects[mangled]?.let { rec -> if (caller == null) {
if (rec.visibility == Visibility.Private && !rec.isAbstract) { objClass.publicMemberResolution[name]?.let { key ->
if (rec.type == ObjRecord.Type.Property) { instanceScope.objects[key]?.let { rec ->
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, caller) if (rec.visibility == Visibility.Public && !rec.isAbstract) {
} else if (rec.type == ObjRecord.Type.Fun) { val decl = rec.declaringClass
return rec.value.invoke(instanceScope, this, args, caller) if (rec.type == ObjRecord.Type.Property) {
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
} else if (rec.type == ObjRecord.Type.Fun) {
return rec.value.invoke(instanceScope, this, args, decl)
}
} }
} }
} }
caller.members[name]?.let { rec -> }
// 0. Prefer private member of current class context
caller?.let { c ->
val mangled = c.mangledName(name)
instanceScope.objects[mangled]?.let { rec ->
if (rec.visibility == Visibility.Private && !rec.isAbstract) { if (rec.visibility == Visibility.Private && !rec.isAbstract) {
if (rec.type == ObjRecord.Type.Property) { if (rec.type == ObjRecord.Type.Property) {
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, caller) if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, c)
} else if (rec.type == ObjRecord.Type.Fun) { } else if (rec.type == ObjRecord.Type.Fun) {
return rec.value.invoke(instanceScope, this, args, caller) return rec.value.invoke(instanceScope, this, args, c)
}
}
}
c.members[name]?.let { rec ->
if (rec.visibility == Visibility.Private && !rec.isAbstract) {
if (rec.type == ObjRecord.Type.Property) {
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, c)
} else if (rec.type == ObjRecord.Type.Fun) {
return rec.value.invoke(instanceScope, this, args, c)
} }
} }
} }
@ -220,7 +268,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
val rec = cls.members[name] ?: cls.classScope?.objects?.get(name) val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
if (rec != null && !rec.isAbstract) { if (rec != null && !rec.isAbstract) {
if (rec.type == ObjRecord.Type.Delegated) { if (rec.type == ObjRecord.Type.Delegated) {
val storageName = "${cls.className}::$name" val storageName = cls.mangledName(name)
val del = instanceScope[storageName]?.delegate ?: rec.delegate val del = instanceScope[storageName]?.delegate ?: rec.delegate
?: scope.raiseError("Internal error: delegated member $name has no delegate (tried $storageName)") ?: scope.raiseError("Internal error: delegated member $name has no delegate (tried $storageName)")
@ -233,8 +281,8 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
}) })
} }
val decl = rec.declaringClass ?: cls val decl = rec.declaringClass ?: cls
val caller = scope.currentClassCtx ?: if (scope.thisObj === this) objClass else null val effectiveCaller = caller ?: if (scope.thisObj === this) objClass else null
if (!canAccessMember(rec.visibility, decl, caller)) if (!canAccessMember(rec.visibility, decl, effectiveCaller))
scope.raiseError( scope.raiseError(
ObjIllegalAccessException( ObjIllegalAccessException(
scope, scope,

File diff suppressed because it is too large Load Diff

View File

@ -419,6 +419,25 @@ class MiniAstTest {
assertEquals("Int", innerArg.segments.last().name) assertEquals("Int", innerArg.segments.last().name)
} }
@Test
fun verify_class_member_structure() = runTest {
val code = """
class A {
fun member() {}
val field = 1
}
""".trimIndent()
val (_, sink) = compileWithMini(code)
val mini = sink.build()!!
println("[DEBUG_LOG] Mini declarations: ${mini.declarations.map { it.name }}")
assertEquals(1, mini.declarations.size, "Should only have one top-level declaration (class A)")
val cls = mini.declarations[0] as MiniClassDecl
assertEquals("A", cls.name)
assertEquals(2, cls.members.size, "Class A should have 2 members (member() and field)")
assertTrue(cls.members.any { it.name == "member" && it is MiniMemberFunDecl })
assertTrue(cls.members.any { it.name == "field" && it is MiniMemberValDecl })
}
@Test @Test
fun inferTypeForValWithInference() = runTest { fun inferTypeForValWithInference() = runTest {
val code = """ val code = """

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -18,25 +18,25 @@
package net.sergeych.lyng package net.sergeych.lyng
actual object PerfDefaults { actual object PerfDefaults {
actual val LOCAL_SLOT_PIC: Boolean = false actual val LOCAL_SLOT_PIC: Boolean = true
actual val EMIT_FAST_LOCAL_REFS: Boolean = false actual val EMIT_FAST_LOCAL_REFS: Boolean = true
actual val ARG_BUILDER: Boolean = false actual val ARG_BUILDER: Boolean = true
actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = false actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = true
actual val SCOPE_POOL: Boolean = false actual val SCOPE_POOL: Boolean = false
actual val FIELD_PIC: Boolean = false actual val FIELD_PIC: Boolean = true
actual val METHOD_PIC: Boolean = false actual val METHOD_PIC: Boolean = true
actual val FIELD_PIC_SIZE_4: Boolean = false actual val FIELD_PIC_SIZE_4: Boolean = true
actual val METHOD_PIC_SIZE_4: Boolean = false actual val METHOD_PIC_SIZE_4: Boolean = true
actual val PIC_ADAPTIVE_2_TO_4: Boolean = false actual val PIC_ADAPTIVE_2_TO_4: Boolean = true
actual val PIC_ADAPTIVE_METHODS_ONLY: Boolean = false actual val PIC_ADAPTIVE_METHODS_ONLY: Boolean = true
actual val PIC_ADAPTIVE_HEURISTIC: Boolean = false actual val PIC_ADAPTIVE_HEURISTIC: Boolean = true
actual val PIC_DEBUG_COUNTERS: Boolean = false actual val PIC_DEBUG_COUNTERS: Boolean = false
actual val PRIMITIVE_FASTOPS: Boolean = false actual val PRIMITIVE_FASTOPS: Boolean = true
actual val RVAL_FASTPATH: Boolean = false actual val RVAL_FASTPATH: Boolean = true
// Regex caching (JVM-first): enabled by default on JVM // Regex caching (JVM-first): enabled by default on JVM
actual val REGEX_CACHE: Boolean = true actual val REGEX_CACHE: Boolean = true

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -18,7 +18,6 @@
package net.sergeych.lyng package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjVoid
/** /**
* JVM actual: per-thread scope frame pool backed by ThreadLocal. * JVM actual: per-thread scope frame pool backed by ThreadLocal.
@ -32,26 +31,31 @@ actual object ScopePool {
actual fun borrow(parent: Scope, args: Arguments, pos: Pos, thisObj: Obj): Scope { actual fun borrow(parent: Scope, args: Arguments, pos: Pos, thisObj: Obj): Scope {
val pool = threadLocalPool.get() val pool = threadLocalPool.get()
val s = if (pool.isNotEmpty()) pool.removeLast() else Scope(parent, args, pos, thisObj) if (pool.isNotEmpty()) {
return try { val s = pool.removeLast()
// Always reset state on borrow to guarantee fresh-frame semantics try {
s.resetForReuse(parent, args, pos, thisObj) // Re-initialize pooled instance
s s.resetForReuse(parent, args, pos, thisObj)
} catch (e: IllegalStateException) { return s
// Defensive fallback: if a cycle in scope parent chain is detected during reuse, } catch (e: IllegalStateException) {
// discard pooled instance for this call frame and allocate a fresh scope instead. // Defensive fallback: if a cycle in scope parent chain is detected during reuse,
if (e.message?.contains("cycle") == true && e.message?.contains("scope parent chain") == true) { // discard pooled instance for this call frame and allocate a fresh scope instead.
Scope(parent, args, pos, thisObj) if (e.message?.contains("cycle") == true && e.message?.contains("scope parent chain") == true) {
} else { return Scope(parent, args, pos, thisObj)
throw e } else {
throw e
}
} }
} }
return Scope(parent, args, pos, thisObj)
} }
actual fun release(scope: Scope) { actual fun release(scope: Scope) {
val pool = threadLocalPool.get() val pool = threadLocalPool.get()
// Scrub sensitive references to avoid accidental retention if (pool.size < MAX_POOL_SIZE) {
scope.resetForReuse(parent = null, args = Arguments.EMPTY, pos = Pos.builtIn, thisObj = ObjVoid) // Scrub sensitive references to avoid accidental retention before returning to pool
if (pool.size < MAX_POOL_SIZE) pool.addLast(scope) scope.scrub()
pool.addLast(scope)
}
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -88,20 +88,57 @@ class PicBenchmarkTest {
// PIC OFF // PIC OFF
PerfFlags.METHOD_PIC = false PerfFlags.METHOD_PIC = false
PerfFlags.SCOPE_POOL = false
val scope1 = Scope() val scope1 = Scope()
val t0 = System.nanoTime() val t0 = System.nanoTime()
val r1 = (scope1.eval(script) as ObjInt).value val r1 = (scope1.eval(script) as ObjInt).value
val t1 = System.nanoTime() val t1 = System.nanoTime()
println("[DEBUG_LOG] [BENCH] Method PIC=OFF: ${(t1 - t0) / 1_000_000.0} ms") println("[DEBUG_LOG] [BENCH] Method PIC=OFF, POOL=OFF: ${(t1 - t0) / 1_000_000.0} ms")
assertEquals(iterations.toLong(), r1) assertEquals(iterations.toLong(), r1)
// PIC ON // PIC ON
PerfFlags.METHOD_PIC = true PerfFlags.METHOD_PIC = true
PerfFlags.SCOPE_POOL = true
val scope2 = Scope() val scope2 = Scope()
val t2 = System.nanoTime() val t2 = System.nanoTime()
val r2 = (scope2.eval(script) as ObjInt).value val r2 = (scope2.eval(script) as ObjInt).value
val t3 = System.nanoTime() val t3 = System.nanoTime()
println("[DEBUG_LOG] [BENCH] Method PIC=ON: ${(t3 - t2) / 1_000_000.0} ms") println("[DEBUG_LOG] [BENCH] Method PIC=ON, POOL=ON: ${(t3 - t2) / 1_000_000.0} ms")
assertEquals(iterations.toLong(), r2)
}
@Test
fun benchmarkLoopScopePooling() = runBlocking {
val iterations = 500_000
val script = """
var x = 0
var i = 0
while(i < $iterations) {
if(true) {
var y = 1
x = x + y
}
i = i + 1
}
x
""".trimIndent()
// POOL OFF
PerfFlags.SCOPE_POOL = false
val scope1 = Scope()
val t0 = System.nanoTime()
val r1 = (scope1.eval(script) as ObjInt).value
val t1 = System.nanoTime()
println("[DEBUG_LOG] [BENCH] Loop Pool=OFF: ${(t1 - t0) / 1_000_000.0} ms")
assertEquals(iterations.toLong(), r1)
// POOL ON
PerfFlags.SCOPE_POOL = true
val scope2 = Scope()
val t2 = System.nanoTime()
val r2 = (scope2.eval(script) as ObjInt).value
val t3 = System.nanoTime()
println("[DEBUG_LOG] [BENCH] Loop Pool=ON: ${(t3 - t2) / 1_000_000.0} ms")
assertEquals(iterations.toLong(), r2) assertEquals(iterations.toLong(), r2)
} }
} }

View File

@ -1,3 +1,20 @@
/*
* Copyright 2026 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 package net.sergeych.lyng.miniast
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -462,6 +479,34 @@ class CompletionEngineLightTest {
assertTrue(ns.contains("size"), "String member 'size' should be suggested for local x[0] inside set(), but got: $ns") assertTrue(ns.contains("size"), "String member 'size' should be suggested for local x[0] inside set(), but got: $ns")
} }
@Test
fun functionArgumentsInBody() = runBlocking {
val code = """
fun test(myArg1, myArg2) {
myA<caret>
}
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("myArg1"), "Function argument 'myArg1' should be proposed, but got: $ns")
assertTrue(ns.contains("myArg2"), "Function argument 'myArg2' should be proposed, but got: $ns")
}
@Test
fun methodArgumentsInBody() = runBlocking {
val code = """
class MyClass {
fun test(myArg1, myArg2) {
myA<caret>
}
}
""".trimIndent()
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
val ns = names(items)
assertTrue(ns.contains("myArg1"), "Method argument 'myArg1' should be proposed, but got: $ns")
assertTrue(ns.contains("myArg2"), "Method argument 'myArg2' should be proposed, but got: $ns")
}
@Test @Test
fun nestedShadowingCompletion() = runBlocking { fun nestedShadowingCompletion() = runBlocking {
val code = """ val code = """

View File

@ -98,6 +98,7 @@ fun applyEnter(text: String, selStart: Int, selEnd: Int, tabSize: Int): EditResu
val lineStart = lineStartAt(text, start) val lineStart = lineStartAt(text, start)
val lineEnd = lineEndAt(text, start) val lineEnd = lineEndAt(text, start)
val indent = countIndentSpaces(text, lineStart, lineEnd) val indent = countIndentSpaces(text, lineStart, lineEnd)
val lineTrimmed = text.substring(lineStart, lineEnd).trim()
// Compute neighborhood characters early so rule precedence can use them // Compute neighborhood characters early so rule precedence can use them
val prevIdx = prevNonWs(text, start) val prevIdx = prevNonWs(text, start)
@ -107,7 +108,7 @@ fun applyEnter(text: String, selStart: Int, selEnd: Int, tabSize: Int): EditResu
val before = text.substring(0, start) val before = text.substring(0, start)
val after = text.substring(start) val after = text.substring(start)
// 1) Between braces { | } -> two lines, inner indented // Rule 4: Between braces on the same line {|}
if (prevCh == '{' && nextCh == '}') { if (prevCh == '{' && nextCh == '}') {
val innerIndent = indent + tabSize val innerIndent = indent + tabSize
val insertion = "\n" + " ".repeat(innerIndent) + "\n" + " ".repeat(indent) val insertion = "\n" + " ".repeat(innerIndent) + "\n" + " ".repeat(indent)
@ -115,22 +116,51 @@ fun applyEnter(text: String, selStart: Int, selEnd: Int, tabSize: Int): EditResu
val caret = start + 1 + innerIndent val caret = start + 1 + innerIndent
return EditResult(out, caret, caret) return EditResult(out, caret, caret)
} }
// 2) After '{'
// Rule 2: On a brace-only line '}'
if (lineTrimmed == "}") {
val newIndent = (indent - tabSize).coerceAtLeast(0)
val newCurrentLine = " ".repeat(newIndent) + "}"
val insertion = "\n" + " ".repeat(newIndent)
val out = safeSubstring(text, 0, lineStart) + newCurrentLine + insertion + safeSubstring(text, lineEnd, text.length)
val caret = lineStart + newCurrentLine.length + insertion.length
return EditResult(out, caret, caret)
}
// Rule 1: After '{'
if (prevCh == '{') { if (prevCh == '{') {
val insertion = "\n" + " ".repeat(indent + tabSize) val insertion = "\n" + " ".repeat(indent + tabSize)
val out = before + insertion + after val out = before + insertion + after
val caret = start + insertion.length val caret = start + insertion.length
return EditResult(out, caret, caret) return EditResult(out, caret, caret)
} }
// 3) Before '}'
if (nextCh == '}') { // Rule 3: End of a line before a brace-only next line
val insertion = "\n" + " ".repeat(indent) if (start == lineEnd && lineEnd < text.length) {
val nextLineStart = lineEnd + 1
val nextLineEnd = lineEndAt(text, nextLineStart)
val nextLineTrimmed = text.substring(nextLineStart, nextLineEnd).trim()
if (nextLineTrimmed == "}") {
val nextLineIndent = countIndentSpaces(text, nextLineStart, nextLineEnd)
val newNextLineIndent = (nextLineIndent - tabSize).coerceAtLeast(0)
val newNextLine = " ".repeat(newNextLineIndent) + "}"
val out = text.substring(0, nextLineStart) + newNextLine + text.substring(nextLineEnd)
val caret = nextLineStart + newNextLineIndent
return EditResult(out, caret, caret)
}
}
// Rule 5: After '}' with only spaces until end-of-line
val afterCaretOnLine = text.substring(start, lineEnd)
if (prevCh == '}' && afterCaretOnLine.trim().isEmpty()) {
val newIndent = (indent - tabSize).coerceAtLeast(0)
val insertion = "\n" + " ".repeat(newIndent)
val out = before + insertion + after val out = before + insertion + after
val caret = start + insertion.length val caret = start + insertion.length
return EditResult(out, caret, caret) return EditResult(out, caret, caret)
} }
// default keep same indent // Rule 6: Default smart indent
val insertion = "\n" + " ".repeat(indent) val insertion = "\n" + " ".repeat(indent)
val out = before + insertion + after val out = before + insertion + after
val caret = start + insertion.length val caret = start + insertion.length

View File

@ -329,7 +329,7 @@
<!-- Top-left version ribbon --> <!-- Top-left version ribbon -->
<div class="corner-ribbon bg-danger text-white"> <div class="corner-ribbon bg-danger text-white">
<span style="margin-left: -5em"> <span style="margin-left: -5em">
v1.1.1-SNAPSHOT v1.2.0-SNAPSHOT
</span> </span>
</div> </div>
<!-- Fixed top navbar for the whole site --> <!-- Fixed top navbar for the whole site -->

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -41,17 +41,23 @@ class EditorE2ETest {
// Programmatically type text into the textarea at current selection and dispatch an input event // Programmatically type text into the textarea at current selection and dispatch an input event
private fun typeText(ta: HTMLTextAreaElement, s: String) { private fun typeText(ta: HTMLTextAreaElement, s: String) {
for (ch in s) { for (ch in s) {
val start = ta.selectionStart ?: 0 val key = ch.toString()
val end = ta.selectionEnd ?: start val ev = js("new KeyboardEvent('keydown', {key: key, bubbles: true, cancelable: true})")
val before = ta.value.substring(0, start) val wasPrevented = !ta.dispatchEvent(ev.unsafeCast<org.w3c.dom.events.Event>())
val after = ta.value.substring(end)
ta.value = before + ch + after if (!wasPrevented) {
val newPos = start + 1 val start = ta.selectionStart ?: 0
ta.selectionStart = newPos val end = ta.selectionEnd ?: start
ta.selectionEnd = newPos val before = ta.value.substring(0, start)
// Fire input so EditorWithOverlay updates its state val after = ta.value.substring(end)
val ev = js("new Event('input', {bubbles:true})") ta.value = before + ch + after
ta.dispatchEvent(ev.unsafeCast<org.w3c.dom.events.Event>()) val newPos = start + 1
ta.selectionStart = newPos
ta.selectionEnd = newPos
// Fire input so EditorWithOverlay updates its state
val inputEv = js("new Event('input', {bubbles:true})")
ta.dispatchEvent(inputEv.unsafeCast<org.w3c.dom.events.Event>())
}
} }
} }
@ -243,10 +249,10 @@ class EditorE2ETest {
typeText(ta, "4"); nextFrame() typeText(ta, "4"); nextFrame()
dispatchKey(ta, key = "Enter"); nextFrame(); nextFrame() dispatchKey(ta, key = "Enter"); nextFrame(); nextFrame()
typeText(ta, "}"); nextFrame() typeText(ta, "}"); nextFrame(); nextFrame(); nextFrame()
dispatchKey(ta, key = "Enter"); nextFrame(); nextFrame() dispatchKey(ta, key = "Enter"); nextFrame(); nextFrame()
typeText(ta, "5"); nextFrame(); nextFrame() typeText(ta, "5"); nextFrame(); nextFrame(); nextFrame()
val expected = ( val expected = (
""" """