From 6b957ae6a3fc5b435e0778da7301bb200e900184 Mon Sep 17 00:00:00 2001 From: sergeych Date: Sun, 11 Jan 2026 01:42:47 +0100 Subject: [PATCH] Refactored `ObjRef` evaluation logic for enhanced performance and consistency across unary, binary, and field operations. Added `evalValue` overrides for more streamlined value resolution. --- lyng-idea/build.gradle.kts | 4 +- .../completion/LyngCompletionContributor.kt | 32 +- .../idea/docs/LyngDocumentationProvider.kt | 24 +- .../lyng/idea/docs/ProcessDocsFallback.kt | 69 ++ .../idea/spell/LyngSpellcheckingStrategy.kt | 39 +- .../sergeych/lyng/idea/util/DocsBootstrap.kt | 50 +- .../src/main/resources/META-INF/plugin.xml | 4 +- .../completion/LyngCompletionMemberTest.kt | 54 ++ lyngio/build.gradle.kts | 1 + .../net/sergeych/lyngio/docs/FsBuiltinDocs.kt | 153 ++- .../lyngio/docs/ProcessBuiltinDocs.kt | 117 +++ lynglib/build.gradle.kts | 1 + .../kotlin/net/sergeych/lyng/Compiler.kt | 26 +- .../net/sergeych/lyng/CompilerContext.kt | 6 +- .../kotlin/net/sergeych/lyng/Parser.kt | 2 +- .../kotlin/net/sergeych/lyng/Token.kt | 4 +- .../lyng/highlight/SimpleLyngHighlighter.kt | 2 +- .../lyng/miniast/BuiltinDocRegistry.kt | 4 + .../lyng/miniast/CompletionEngineLight.kt | 37 +- .../net/sergeych/lyng/miniast/MiniAst.kt | 17 +- .../kotlin/net/sergeych/lyng/obj/ObjClass.kt | 94 +- .../net/sergeych/lyng/obj/ObjInstance.kt | 92 +- .../kotlin/net/sergeych/lyng/obj/ObjRef.kt | 871 +++++++++++------- lynglib/src/commonTest/kotlin/MiniAstTest.kt | 19 + .../lyng/miniast/CompletionEngineLightTest.kt | 45 + .../net/sergeych/lyngweb/EditorLogic.kt | 42 +- site/src/jsMain/resources/index.html | 2 +- site/src/jsTest/kotlin/EditorE2ETest.kt | 34 +- 28 files changed, 1339 insertions(+), 506 deletions(-) create mode 100644 lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/docs/ProcessDocsFallback.kt create mode 100644 lyngio/src/commonMain/kotlin/net/sergeych/lyngio/docs/ProcessBuiltinDocs.kt diff --git a/lyng-idea/build.gradle.kts b/lyng-idea/build.gradle.kts index 820396d..439efec 100644 --- a/lyng-idea/build.gradle.kts +++ b/lyng-idea/build.gradle.kts @@ -17,7 +17,7 @@ plugins { kotlin("jvm") - id("org.jetbrains.intellij") version "1.17.3" + id("org.jetbrains.intellij") version "1.17.4" } group = "net.sergeych.lyng" @@ -52,7 +52,7 @@ dependencies { intellij { type.set("IC") // 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 ourselves in plugin.xml to keep it open-ended (no upper cap) updateSinceUntilBuild.set(false) // Include only available bundled plugins for this IDE build diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/completion/LyngCompletionContributor.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/completion/LyngCompletionContributor.kt index 22acd83..e57d21a 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/completion/LyngCompletionContributor.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/completion/LyngCompletionContributor.kt @@ -167,7 +167,11 @@ class LyngCompletionContributor : CompletionContributor() { .withIcon(AllIcons.Nodes.Field) .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 if (memberDotPos != null) { @@ -401,7 +405,7 @@ class LyngCompletionContributor : CompletionContributor() { } supplementPreferredBases(className) - fun emitGroup(map: LinkedHashMap>) { + fun emitGroup(map: LinkedHashMap>, groupPriority: Double) { val keys = map.keys.sortedBy { it.lowercase() } for (name in keys) { val list = map[name] ?: continue @@ -428,7 +432,11 @@ class LyngCompletionContributor : CompletionContributor() { .withTailText(tail, true) .withTypeText(ret, true) .withInsertHandler(ParenInsertHandler) - emit(builder) + if (groupPriority != 0.0) { + emit(PrioritizedLookupElement.withPriority(builder, groupPriority)) + } else { + emit(builder) + } } is MiniMemberValDecl -> { val icon = if (rep.mutable) AllIcons.Nodes.Variable else AllIcons.Nodes.Field @@ -439,7 +447,11 @@ class LyngCompletionContributor : CompletionContributor() { val builder = LookupElementBuilder.create(name) .withIcon(icon) .withTypeText(typeOf(chosen.type), true) - emit(builder) + if (groupPriority != 0.0) { + emit(PrioritizedLookupElement.withPriority(builder, groupPriority)) + } else { + emit(builder) + } } is MiniInitDecl -> {} } @@ -447,8 +459,8 @@ class LyngCompletionContributor : CompletionContributor() { } // Emit what we have first - emitGroup(directMap) - emitGroup(inheritedMap) + emitGroup(directMap, 100.0) + emitGroup(inheritedMap, 0.0) // If suggestions are suspiciously sparse for known container classes, // try to conservatively supplement using a curated list resolved via docs registry. @@ -509,7 +521,7 @@ class LyngCompletionContributor : CompletionContributor() { .withInsertHandler(ParenInsertHandler) } } - emit(builder) + emit(PrioritizedLookupElement.withPriority(builder, 50.0)) already.add(name) } else { // Synthetic fallback: method without detailed params/types to improve UX in absence of docs @@ -523,7 +535,7 @@ class LyngCompletionContributor : CompletionContributor() { .withTailText("()", true) .withInsertHandler(ParenInsertHandler) } - emit(builder) + emit(PrioritizedLookupElement.withPriority(builder, 50.0)) already.add(name) } } @@ -576,7 +588,7 @@ class LyngCompletionContributor : CompletionContributor() { .withInsertHandler(ParenInsertHandler) } } - emit(builder) + emit(PrioritizedLookupElement.withPriority(builder, 50.0)) already.add(name) continue } @@ -585,7 +597,7 @@ class LyngCompletionContributor : CompletionContributor() { .withIcon(AllIcons.Nodes.Method) .withTailText("()", true) .withInsertHandler(ParenInsertHandler) - emit(builder) + emit(PrioritizedLookupElement.withPriority(builder, 50.0)) already.add(name) } } diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/docs/LyngDocumentationProvider.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/docs/LyngDocumentationProvider.kt index dc812c0..4954cbd 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/docs/LyngDocumentationProvider.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/docs/LyngDocumentationProvider.kt @@ -461,25 +461,39 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() { private fun ensureExternalDocsRegistered() { @Suppress("UNUSED_EXPRESSION") externalDocsLoaded } private fun tryLoadExternalDocs(): Boolean { - return try { + var anyLoaded = false + try { // Try known registrars; ignore failures if module is absent val cls = Class.forName("net.sergeych.lyngio.docs.FsBuiltinDocs") val m = cls.getMethod("ensure") m.invoke(null) log.info("[LYNG_DEBUG] QuickDoc: external docs loaded: net.sergeych.lyngio.docs.FsBuiltinDocs.ensure() OK") - true - } catch (_: Throwable) { + 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] 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 val seeded = try { FsDocsFallback.ensureOnce() + ProcessDocsFallback.ensureOnce() + true } catch (_: Throwable) { false } 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 { log.info("[LYNG_DEBUG] QuickDoc: external docs NOT found (lyngio absent on classpath)") } - seeded + return seeded } + return true } override fun getCustomDocumentationElement( diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/docs/ProcessDocsFallback.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/docs/ProcessDocsFallback.kt new file mode 100644 index 0000000..e3335eb --- /dev/null +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/docs/ProcessDocsFallback.kt @@ -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 + } + } +} diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/spell/LyngSpellcheckingStrategy.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/spell/LyngSpellcheckingStrategy.kt index 51cb183..9137418 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/spell/LyngSpellcheckingStrategy.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/spell/LyngSpellcheckingStrategy.kt @@ -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"); * you may not use this file except in compliance with the License. @@ -54,6 +54,8 @@ class LyngSpellcheckingStrategy : SpellcheckingStrategy() { } catch (_: Throwable) { false } override fun getTokenizer(element: PsiElement): Tokenizer<*> { + if (element is com.intellij.psi.PsiFile) return EMPTY_TOKENIZER + val hasGrazie = grazieInstalled() val hasGrazieApi = grazieApiAvailable() val settings = LyngFormatterSettings.getInstance(element.project) @@ -63,27 +65,40 @@ class LyngSpellcheckingStrategy : SpellcheckingStrategy() { } val file = element.containingFile ?: return EMPTY_TOKENIZER - val index = LyngSpellIndex.getUpToDate(file) ?: run { - // Suspend legacy spellcheck until MiniAst-based index is ready - return EMPTY_TOKENIZER - } - val elRange = element.textRange ?: return EMPTY_TOKENIZER - - fun overlaps(list: List) = list.any { it.intersects(elRange) } + val et = element.node?.elementType + val index = LyngSpellIndex.getUpToDate(file) // Decide responsibility per settings // 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 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) = list.any { it.intersects(elRange) } + // 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 - 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 - 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 } @@ -93,7 +108,7 @@ class LyngSpellcheckingStrategy : SpellcheckingStrategy() { } private object IDENTIFIER_TOKENIZER : Tokenizer() { - private val splitter = PlainTextSplitter.getInstance() + private val splitter = com.intellij.spellchecker.inspections.IdentifierSplitter.getInstance() override fun tokenize(element: PsiElement, consumer: TokenConsumer) { val text = element.text if (text.isNullOrEmpty()) return diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/util/DocsBootstrap.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/util/DocsBootstrap.kt index 852510c..83994dd 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/util/DocsBootstrap.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/util/DocsBootstrap.kt @@ -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 * 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 net.sergeych.lyng.idea.docs.FsDocsFallback +import net.sergeych.lyng.idea.docs.ProcessDocsFallback object DocsBootstrap { private val log = Logger.getInstance(DocsBootstrap::class.java) @@ -20,20 +38,32 @@ object DocsBootstrap { } } - private fun tryLoadExternal(): Boolean = try { - val cls = Class.forName("net.sergeych.lyngio.docs.FsBuiltinDocs") - val m = cls.getMethod("ensure") - m.invoke(null) - log.info("[LYNG_DEBUG] DocsBootstrap: external docs loaded: net.sergeych.lyngio.docs.FsBuiltinDocs.ensure() OK") - true - } catch (_: Throwable) { - false + private fun tryLoadExternal(): Boolean { + var anyLoaded = false + try { + val cls = Class.forName("net.sergeych.lyngio.docs.FsBuiltinDocs") + val m = cls.getMethod("ensure") + m.invoke(null) + log.info("[LYNG_DEBUG] DocsBootstrap: external docs loaded: net.sergeych.lyngio.docs.FsBuiltinDocs.ensure() OK") + 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 { - val seeded = FsDocsFallback.ensureOnce() + val seededFs = FsDocsFallback.ensureOnce() + val seededProcess = ProcessDocsFallback.ensureOnce() + val seeded = seededFs || seededProcess 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 { log.info("[LYNG_DEBUG] DocsBootstrap: external docs not found; no fallback seeded") } diff --git a/lyng-idea/src/main/resources/META-INF/plugin.xml b/lyng-idea/src/main/resources/META-INF/plugin.xml index f682710..bbe6af2 100644 --- a/lyng-idea/src/main/resources/META-INF/plugin.xml +++ b/lyng-idea/src/main/resources/META-INF/plugin.xml @@ -16,8 +16,8 @@ --> - - + + net.sergeych.lyng.idea Lyng Sergey Chernov diff --git a/lyng-idea/src/test/kotlin/net/sergeych/lyng/idea/completion/LyngCompletionMemberTest.kt b/lyng-idea/src/test/kotlin/net/sergeych/lyng/idea/completion/LyngCompletionMemberTest.kt index 698840b..e0d2422 100644 --- a/lyng-idea/src/test/kotlin/net/sergeych/lyng/idea/completion/LyngCompletionMemberTest.kt +++ b/lyng-idea/src/test/kotlin/net/sergeych/lyng/idea/completion/LyngCompletionMemberTest.kt @@ -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 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) assertTrue("Too few member suggestions after list literal: $items", items.size >= 3) } + + fun test_ProcessModule_Completion() { + val code = """ + import lyng.io.process + Process. + """.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. + """.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().firstOrNull { it.name == "Process" } + assertNotNull("Should contain Process class", processClass) + assertTrue("Process should have members", processClass!!.members.isNotEmpty()) + } } diff --git a/lyngio/build.gradle.kts b/lyngio/build.gradle.kts index 4b6890c..74e5c1e 100644 --- a/lyngio/build.gradle.kts +++ b/lyngio/build.gradle.kts @@ -32,6 +32,7 @@ group = "net.sergeych" version = "0.0.1-SNAPSHOT" kotlin { + jvmToolchain(17) jvm() androidTarget { publishLibraryVariants("release") diff --git a/lyngio/src/commonMain/kotlin/net/sergeych/lyngio/docs/FsBuiltinDocs.kt b/lyngio/src/commonMain/kotlin/net/sergeych/lyngio/docs/FsBuiltinDocs.kt index b6098eb..ccac69d 100644 --- a/lyngio/src/commonMain/kotlin/net/sergeych/lyngio/docs/FsBuiltinDocs.kt +++ b/lyngio/src/commonMain/kotlin/net/sergeych/lyngio/docs/FsBuiltinDocs.kt @@ -39,43 +39,174 @@ object FsBuiltinDocs { name = "Path", 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( name = "exists", - doc = "Whether the path exists on the filesystem.", + doc = "Check whether this path exists on the filesystem.", returns = type("lyng.Bool") ) method( 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") ) method( - name = "isDir", - doc = "Whether the path exists and is a directory.", + name = "isDirectory", + doc = "True if this path is a directory (based on cached metadata).", 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( 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") ) method( 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"))) ) method( - name = "bytes", - doc = "Iterate file content as `Buffer` chunks.", + name = "appendUtf8", + 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"))), returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.Buffer"))) ) method( - name = "lines", - doc = "Iterate file as lines of text.", + 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"))) ) + 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 diff --git a/lyngio/src/commonMain/kotlin/net/sergeych/lyngio/docs/ProcessBuiltinDocs.kt b/lyngio/src/commonMain/kotlin/net/sergeych/lyngio/docs/ProcessBuiltinDocs.kt new file mode 100644 index 0000000..8aeaeb0 --- /dev/null +++ b/lyngio/src/commonMain/kotlin/net/sergeych/lyngio/docs/ProcessBuiltinDocs.kt @@ -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 + } +} diff --git a/lynglib/build.gradle.kts b/lynglib/build.gradle.kts index 7393217..df0fdb0 100644 --- a/lynglib/build.gradle.kts +++ b/lynglib/build.gradle.kts @@ -46,6 +46,7 @@ buildkonfig { } kotlin { + jvmToolchain(17) jvm() androidTarget { publishLibraryVariants("release") diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index 8578bd2..f191e17 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -169,9 +169,9 @@ class Compiler( miniSink?.onScriptStart(start) do { 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) { - 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 -> { // A standalone newline not immediately following a comment resets doc buffer if (!prevWasComment) clearPendingDoc() else prevWasComment = false @@ -316,7 +316,7 @@ class Compiler( } 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 @@ -407,7 +407,7 @@ class Compiler( val t = cc.next() val startPos = t.pos 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 // } @@ -606,7 +606,7 @@ class Compiler( // to skip in parseExpression: val current = cc.current() 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 else parseExpression() @@ -887,7 +887,7 @@ class Compiler( } Token.Type.NEWLINE -> {} - Token.Type.MULTILINE_COMMENT, Token.Type.SINLGE_LINE_COMMENT -> {} + Token.Type.MULTILINE_COMMENT, Token.Type.SINGLE_LINE_COMMENT -> {} Token.Type.ID -> { // visibility @@ -2734,7 +2734,8 @@ class Compiler( doc = declDocLocal, nameStart = nameStartPos, receiver = receiverMini, - isExtern = actualExtern + isExtern = actualExtern, + isStatic = isStatic ) miniSink?.onFunDecl(node) pendingDeclDoc = null @@ -2941,10 +2942,11 @@ class Compiler( doc = declDocLocal, nameStart = nameStartPos, receiver = receiverMini, - isExtern = actualExtern + isExtern = actualExtern, + isStatic = isStatic ) - miniSink?.onFunDecl(node) miniSink?.onExitFunction(cc.currentPos()) + miniSink?.onFunDecl(node) } } @@ -3002,7 +3004,8 @@ class Compiler( initRange = null, doc = pendingDeclDoc, nameStart = namePos, - isExtern = actualExtern + isExtern = actualExtern, + isStatic = false ) miniSink?.onValDecl(node) } @@ -3194,7 +3197,8 @@ class Compiler( doc = pendingDeclDoc, nameStart = nameStartPos, receiver = receiverMini, - isExtern = actualExtern + isExtern = actualExtern, + isStatic = isStatic ) miniSink?.onValDecl(node) pendingDeclDoc = null diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CompilerContext.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CompilerContext.kt index 36d78df..4f4b350 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CompilerContext.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/CompilerContext.kt @@ -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"); * you may not use this file except in compliance with the License. @@ -238,7 +238,7 @@ class CompilerContext(val tokens: List) { } } - 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 -> { restorePos(pos); return false } @@ -265,6 +265,6 @@ class CompilerContext(val tokens: List) { } 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) } } \ No newline at end of file diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt index 84c9479..e52772e 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt @@ -131,7 +131,7 @@ private class Parser(fromPos: Pos) { pos.advance() val body = loadToEndOfLine() // 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) } '*' -> { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Token.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Token.kt index c85383a..19dcf32 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Token.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Token.kt @@ -22,7 +22,7 @@ data class Token(val value: String, val pos: Pos, val type: Type) { 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) = type == Type.ID && value == text @@ -41,7 +41,7 @@ data class Token(val value: String, val pos: Pos, val type: Type) { SHUTTLE, AND, BITAND, OR, BITOR, BITXOR, NOT, BITNOT, DOT, ARROW, EQARROW, QUESTION, COLONCOLON, SHL, SHR, - SINLGE_LINE_COMMENT, MULTILINE_COMMENT, + SINGLE_LINE_COMMENT, MULTILINE_COMMENT, LABEL, ATLABEL, // label@ at@label // type-checking/casting AS, ASNULL, OBJECT, diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/SimpleLyngHighlighter.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/SimpleLyngHighlighter.kt index ec745ed..1de8eb1 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/SimpleLyngHighlighter.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/highlight/SimpleLyngHighlighter.kt @@ -67,7 +67,7 @@ private fun kindOf(type: Type, value: String): HighlightKind? = when (type) { Type.REGEX -> HighlightKind.Regex // comments - Type.SINLGE_LINE_COMMENT, Type.MULTILINE_COMMENT -> HighlightKind.Comment + Type.SINGLE_LINE_COMMENT, Type.MULTILINE_COMMENT -> HighlightKind.Comment // punctuation Type.LPAREN, Type.RPAREN, Type.LBRACE, Type.RBRACE, Type.LBRACKET, Type.RBRACKET, diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt index dc5eab5..76b5165 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/BuiltinDocRegistry.kt @@ -148,6 +148,8 @@ class ModuleDocsBuilder internal constructor(private val moduleName: String) { body = null, doc = md, nameStart = Pos.builtIn, + isExtern = false, + isStatic = false ) } @@ -167,6 +169,8 @@ class ModuleDocsBuilder internal constructor(private val moduleName: String) { initRange = null, doc = md, nameStart = Pos.builtIn, + isExtern = false, + isStatic = false ) } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/CompletionEngineLight.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/CompletionEngineLight.kt index 39d99fd..a34a9e3 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/CompletionEngineLight.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/CompletionEngineLight.kt @@ -33,6 +33,7 @@ data class CompletionItem( val kind: Kind, val tailText: String? = null, val typeText: String? = null, + val priority: Double = 0.0, ) enum class Kind { Function, Class_, Enum, Value, Method, Field } @@ -107,7 +108,7 @@ object CompletionEngineLight { val locals = DocLookupUtils.extractLocalsAt(text, caret) for (name in locals) { 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) { is MiniFunDecl -> { 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 -> { // Propose constructor parameters (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 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) for (m in node.members) { @@ -189,10 +190,10 @@ object CompletionEngineLight { when (m) { is MiniMemberFunDecl -> { 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 -> { - 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 -> {} } @@ -200,7 +201,7 @@ object CompletionEngineLight { } is MiniMemberFunDecl -> { 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 -> {} @@ -250,7 +251,7 @@ object CompletionEngineLight { "Array" -> listOf("Collection", "Iterable").forEach { if (visited.add(it)) addMembersOf(it, false) } } - fun emitGroup(map: LinkedHashMap>) { + fun emitGroup(map: LinkedHashMap>, groupPriority: Double) { for (name in map.keys.sortedBy { it.lowercase() }) { val variants = map[name] ?: continue // 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 extra = variants.count { it is MiniMemberFunDecl } - 1 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 } is MiniMemberValDecl -> { @@ -273,7 +274,7 @@ object CompletionEngineLight { val chosen = variants.asSequence() .filterIsInstance() .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 } is MiniInitDecl -> {} @@ -281,8 +282,8 @@ object CompletionEngineLight { } } - emitGroup(directMap) - emitGroup(inheritedMap) + emitGroup(directMap, 100.0) + emitGroup(inheritedMap, 0.0) // Supplement with extension members (both stdlib and local) run { @@ -296,22 +297,22 @@ object CompletionEngineLight { val ci = when (m) { is MiniMemberFunDecl -> { 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 -> { 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 MiniValDecl -> CompletionItem(name, Kind.Field, typeText = typeOf(m.type)) - else -> CompletionItem(name, Kind.Method, tailText = "()", typeText = null) + is MiniMemberValDecl -> CompletionItem(name, Kind.Field, typeText = typeOf(m.type), priority = 50.0) + is MiniValDecl -> CompletionItem(name, Kind.Field, typeText = typeOf(m.type), priority = 50.0) + else -> CompletionItem(name, Kind.Method, tailText = "()", typeText = null, priority = 50.0) } if (ci.name.startsWith(prefix, true)) { out += ci already.add(name) } } 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)) { out += ci already.add(name) diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/MiniAst.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/MiniAst.kt index 53c65bf..9a144e4 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/MiniAst.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/miniast/MiniAst.kt @@ -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 val nameStart: Pos val isExtern: Boolean + val isStatic: Boolean } sealed interface MiniDecl : MiniNamedDecl @@ -114,7 +115,8 @@ data class MiniFunDecl( override val doc: MiniDoc?, override val nameStart: Pos, val receiver: MiniTypeRef? = null, - override val isExtern: Boolean = false + override val isExtern: Boolean = false, + override val isStatic: Boolean = false, ) : MiniDecl data class MiniValDecl( @@ -126,7 +128,8 @@ data class MiniValDecl( override val doc: MiniDoc?, override val nameStart: Pos, val receiver: MiniTypeRef? = null, - override val isExtern: Boolean = false + override val isExtern: Boolean = false, + override val isStatic: Boolean = false, ) : MiniDecl data class MiniClassDecl( @@ -141,6 +144,7 @@ data class MiniClassDecl( // Built-in extension: list of member declarations (functions and fields) val members: List = emptyList(), override val isExtern: Boolean = false, + override val isStatic: Boolean = false, val isObject: Boolean = false ) : MiniDecl @@ -150,7 +154,8 @@ data class MiniEnumDecl( val entries: List, override val doc: MiniDoc?, override val nameStart: Pos, - override val isExtern: Boolean = false + override val isExtern: Boolean = false, + override val isStatic: Boolean = false, ) : MiniDecl data class MiniCtorField( @@ -175,7 +180,7 @@ data class MiniIdentifier( // --- Class member declarations (for built-in/registry docs) --- sealed interface MiniMemberDecl : MiniNamedDecl { - val isStatic: Boolean + override val isStatic: Boolean } data class MiniMemberFunDecl( @@ -319,7 +324,7 @@ class MiniAstBuilder : MiniAstSink { returnType = attach.returnType, doc = attach.doc, nameStart = attach.nameStart, - isStatic = false, // TODO: track static if needed + isStatic = attach.isStatic, isExtern = attach.isExtern, body = attach.body ) @@ -359,7 +364,7 @@ class MiniAstBuilder : MiniAstSink { initRange = attach.initRange, doc = attach.doc, nameStart = attach.nameStart, - isStatic = false, // TODO: track static if needed + isStatic = attach.isStatic, isExtern = attach.isExtern ) // Duplicates for vals are rare but possible if Compiler calls it twice diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt index b789409..ed491c9 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjClass.kt @@ -106,6 +106,34 @@ open class ObjClass( val classId: Long = ClassIdGen.nextId() var layoutVersion: Int = 0 + private val mangledNameCache = mutableMapOf() + 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 by lazy { + val res = mutableMapOf() + // 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) } var constructorMeta: ArgsDeclaration? = null @@ -242,6 +270,43 @@ open class ObjClass( return instance } + /** Pre-calculated template for instanceScope.objects. */ + private val instanceObjectsTemplate: Map by lazy { + val res = mutableMapOf() + 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 by lazy { + instanceObjectsTemplate.filter { it.value.type == ObjRecord.Type.Fun } + } + + private val templateOthers: List> by lazy { + instanceObjectsTemplate.filter { it.value.type != ObjRecord.Type.Fun }.toList() + } + /** * Create an instance of this class and initialize its [ObjInstance.instanceScope] with * 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 // This mirrors Obj.autoInstanceScope behavior for ad-hoc scopes and makes fb.method() resolution robust - 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.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() - } - } - } + instance.instanceScope.objects.putAll(templateMethods) + for (p in templateOthers) { + instance.instanceScope.objects[p.first] = p.second.copy() } return instance } @@ -327,7 +373,7 @@ open class ObjClass( for (p in meta.params) { val rec = instance.instanceScope.objects[p.name] 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 // across re-bindings instance.instanceScope.objects[mangled] = rec @@ -361,7 +407,7 @@ open class ObjClass( for (p in meta.params) { val rec = instance.instanceScope.objects[p.name] if (rec != null) { - val mangled = "${c.className}::${p.name}" + val mangled = c.mangledName(p.name) instance.instanceScope.objects[mangled] = rec } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt index 401bc98..3bbc5da 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjInstance.kt @@ -34,6 +34,18 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { override suspend fun readField(scope: Scope, name: String): ObjRecord { 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 caller?.let { c -> // Check for private methods/properties @@ -43,7 +55,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { } } // Check for private fields (stored in instanceScope) - val mangled = "${c.className}::$name" + val mangled = c.mangledName(name) instanceScope.objects[mangled]?.let { rec -> if (rec.visibility == Visibility.Private) { return resolveRecord(scope, rec, name, c) @@ -54,7 +66,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { // 1. MRO mangled storage for (cls in objClass.mro) { if (cls.className == "Obj") break - val mangled = "${cls.className}::$name" + val mangled = cls.mangledName(name) instanceScope.objects[mangled]?.let { rec -> if ((scope.thisObj === this && caller != null) || canAccessMember(rec.visibility, cls, caller)) { 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 == ObjRecord.Type.Delegated) { val d = decl ?: obj.declaringClass - val storageName = "${d?.className}::$name" + val storageName = d?.mangledName(name) ?: name var del = instanceScope[storageName]?.delegate ?: obj.delegate if (del == null) { for (c in objClass.mro) { - del = instanceScope["${c.className}::$name"]?.delegate + del = instanceScope[c.mangledName(name)]?.delegate if (del != null) break } } @@ -97,7 +109,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { var targetRec = obj val d = decl ?: obj.declaringClass if (d != null) { - val mangled = "${d.className}::$name" + val mangled = d.mangledName(name) instanceScope.objects[mangled]?.let { targetRec = it } @@ -120,6 +132,24 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { willMutate(scope) 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 caller?.let { c -> // Check for private methods/properties @@ -130,7 +160,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { } } // Check for private fields (stored in instanceScope) - val mangled = "${c.className}::$name" + val mangled = c.mangledName(name) instanceScope.objects[mangled]?.let { rec -> if (rec.visibility == Visibility.Private) { updateRecord(scope, rec, name, newValue, c) @@ -142,7 +172,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { // 1. MRO mangled storage for (cls in objClass.mro) { if (cls.className == "Obj") break - val mangled = "${cls.className}::$name" + val mangled = cls.mangledName(name) instanceScope.objects[mangled]?.let { rec -> if ((scope.thisObj === this && caller != null) || canAccessMember(rec.effectiveWriteVisibility, cls, caller)) { updateRecord(scope, rec, name, newValue, cls) @@ -191,24 +221,42 @@ class ObjInstance(override val objClass: ObjClass) : Obj() { scope: Scope, name: String, args: Arguments, onNotFoundResult: (suspend () -> Obj?)? ): Obj { - // 0. Prefer private member of current class context - scope.currentClassCtx?.let { caller -> - val mangled = "${caller.className}::$name" - instanceScope.objects[mangled]?.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, caller) - } else if (rec.type == ObjRecord.Type.Fun) { - return rec.value.invoke(instanceScope, this, args, caller) + 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.visibility == Visibility.Public && !rec.isAbstract) { + val decl = rec.declaringClass + 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.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) { - 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) if (rec != null && !rec.isAbstract) { if (rec.type == ObjRecord.Type.Delegated) { - val storageName = "${cls.className}::$name" + val storageName = cls.mangledName(name) val del = instanceScope[storageName]?.delegate ?: rec.delegate ?: 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 caller = scope.currentClassCtx ?: if (scope.thisObj === this) objClass else null - if (!canAccessMember(rec.visibility, decl, caller)) + val effectiveCaller = caller ?: if (scope.thisObj === this) objClass else null + if (!canAccessMember(rec.visibility, decl, effectiveCaller)) scope.raiseError( ObjIllegalAccessException( scope, diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt index 54c5a25..5f0fd64 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjRef.kt @@ -114,11 +114,39 @@ class UnaryOpRef(private val op: UnaryOp, private val a: ObjRef) : ObjRef { } return r.asReadonly } + + override suspend fun evalValue(scope: Scope): Obj { + val v = a.evalValue(scope) + if (PerfFlags.PRIMITIVE_FASTOPS) { + val rFast: Obj? = when (op) { + UnaryOp.NOT -> if (v is ObjBool) if (!v.value) ObjTrue else ObjFalse else null + UnaryOp.NEGATE -> when (v) { + is ObjInt -> ObjInt(-v.value) + is ObjReal -> ObjReal(-v.value) + else -> null + } + UnaryOp.BITNOT -> if (v is ObjInt) ObjInt(v.value.inv()) else null + } + if (rFast != null) { + if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.primitiveFastOpsHit++ + return rFast + } + } + return when (op) { + UnaryOp.NOT -> v.logicalNot(scope) + UnaryOp.NEGATE -> v.negate(scope) + UnaryOp.BITNOT -> v.bitNot(scope) + } + } } /** R-value reference for binary operations. */ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val right: ObjRef) : ObjRef { override suspend fun get(scope: Scope): ObjRecord { + return evalValue(scope).asReadonly + } + + override suspend fun evalValue(scope: Scope): Obj { val a = left.evalValue(scope) val b = right.evalValue(scope) @@ -135,7 +163,7 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r } if (r != null) { if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.primitiveFastOpsHit++ - return r.asReadonly + return r } } // Fast integer ops when both operands are ObjInt @@ -163,7 +191,7 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r } if (r != null) { if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.primitiveFastOpsHit++ - return r.asReadonly + return r } } // Fast string operations when both are strings @@ -180,7 +208,7 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r } if (r != null) { if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.primitiveFastOpsHit++ - return r.asReadonly + return r } } // Fast char vs char comparisons @@ -198,7 +226,7 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r } if (r != null) { if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.primitiveFastOpsHit++ - return r.asReadonly + return r } } // Fast concatenation for String with Int/Char on either side @@ -206,19 +234,19 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r when { a is ObjString && b is ObjInt -> { if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.primitiveFastOpsHit++ - return ObjString(a.value + b.value.toString()).asReadonly + return ObjString(a.value + b.value.toString()) } a is ObjString && b is ObjChar -> { if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.primitiveFastOpsHit++ - return ObjString(a.value + b.value).asReadonly + return ObjString(a.value + b.value) } b is ObjString && a is ObjInt -> { if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.primitiveFastOpsHit++ - return ObjString(a.value.toString() + b.value).asReadonly + return ObjString(a.value.toString() + b.value) } b is ObjString && a is ObjChar -> { if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.primitiveFastOpsHit++ - return ObjString(a.value.toString() + b.value).asReadonly + return ObjString(a.value.toString() + b.value) } } } @@ -242,7 +270,7 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r } if (rNum != null) { if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.primitiveFastOpsHit++ - return rNum.asReadonly + return rNum } } } @@ -277,7 +305,7 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r BinOp.SLASH -> a.div(scope, b) BinOp.PERCENT -> a.mod(scope, b) } - return r.asReadonly + return r } } @@ -288,14 +316,21 @@ class ConditionalRef( private val ifFalse: ObjRef ) : ObjRef { override suspend fun get(scope: Scope): ObjRecord { + return evalCondition(scope).get(scope) + } + + override suspend fun evalValue(scope: Scope): Obj { + return evalCondition(scope).evalValue(scope) + } + + private suspend fun evalCondition(scope: Scope): ObjRef { val condVal = condition.evalValue(scope) val condTrue = when (condVal) { is ObjBool -> condVal.value is ObjInt -> condVal.value != 0L else -> condVal.toBool() } - val branch = if (condTrue) ifTrue else ifFalse - return branch.get(scope) + return if (condTrue) ifTrue else ifFalse } } @@ -309,7 +344,7 @@ class CastRef( override suspend fun get(scope: Scope): ObjRecord { val v0 = valueRef.evalValue(scope) val t = typeRef.evalValue(scope) - val target = (t as? ObjClass) ?: scope.raiseClassCastError("${'$'}t is not the class instance") + val target = (t as? ObjClass) ?: scope.raiseClassCastError("${t} is not the class instance") // unwrap qualified views val v = when (v0) { is ObjQualifiedView -> v0.instance @@ -320,7 +355,26 @@ class CastRef( if (v is ObjInstance) ObjQualifiedView(v, target).asReadonly else v.asReadonly } else { if (isNullable) ObjNull.asReadonly else scope.raiseClassCastError( - "Cannot cast ${'$'}{(v as? Obj)?.objClass?.className ?: v::class.simpleName} to ${'$'}{target.className}" + "Cannot cast ${(v as? Obj)?.objClass?.className ?: v::class.simpleName} to ${target.className}" + ) + } + } + + override suspend fun evalValue(scope: Scope): Obj { + val v0 = valueRef.evalValue(scope) + val t = typeRef.evalValue(scope) + val target = (t as? ObjClass) ?: scope.raiseClassCastError("${t} is not the class instance") + // unwrap qualified views + val v = when (v0) { + is ObjQualifiedView -> v0.instance + else -> v0 + } + return if (v.isInstanceOf(target)) { + // For instances, return a qualified view to enforce ancestor-start dispatch + if (v is ObjInstance) ObjQualifiedView(v, target) else v + } else { + if (isNullable) ObjNull else scope.raiseClassCastError( + "Cannot cast ${(v as? Obj)?.objClass?.className ?: v::class.simpleName} to ${target.className}" ) } } @@ -414,30 +468,38 @@ class ElvisRef(private val left: ObjRef, private val right: ObjRef) : ObjRef { /** Logical OR with short-circuit: a || b */ class LogicalOrRef(private val left: ObjRef, private val right: ObjRef) : ObjRef { override suspend fun get(scope: Scope): ObjRecord { + return evalValue(scope).asReadonly + } + + override suspend fun evalValue(scope: Scope): Obj { val a = left.evalValue(scope) - if ((a as? ObjBool)?.value == true) return ObjTrue.asReadonly + if ((a as? ObjBool)?.value == true) return ObjTrue val b = right.evalValue(scope) if (PerfFlags.PRIMITIVE_FASTOPS) { if (a is ObjBool && b is ObjBool) { - return if (a.value || b.value) ObjTrue.asReadonly else ObjFalse.asReadonly + return if (a.value || b.value) ObjTrue else ObjFalse } } - return a.logicalOr(scope, b).asReadonly + return a.logicalOr(scope, b) } } /** Logical AND with short-circuit: a && b */ class LogicalAndRef(private val left: ObjRef, private val right: ObjRef) : ObjRef { override suspend fun get(scope: Scope): ObjRecord { + return evalValue(scope).asReadonly + } + + override suspend fun evalValue(scope: Scope): Obj { val a = left.evalValue(scope) - if ((a as? ObjBool)?.value == false) return ObjFalse.asReadonly + if ((a as? ObjBool)?.value == false) return ObjFalse val b = right.evalValue(scope) if (PerfFlags.PRIMITIVE_FASTOPS) { if (a is ObjBool && b is ObjBool) { - return if (a.value && b.value) ObjTrue.asReadonly else ObjFalse.asReadonly + return if (a.value && b.value) ObjTrue else ObjFalse } } - return a.logicalAnd(scope, b).asReadonly + return a.logicalAnd(scope, b) } } @@ -446,6 +508,7 @@ class LogicalAndRef(private val left: ObjRef, private val right: ObjRef) : ObjRe */ class ConstRef(private val record: ObjRecord) : ObjRef { override suspend fun get(scope: Scope): ObjRecord = record + override suspend fun evalValue(scope: Scope): Obj = record.value // Expose constant value for compiler constant folding (pure, read-only) val constValue: Obj get() = record.value } @@ -524,103 +587,126 @@ class FieldRef( val base = target.evalValue(scope) if (base == ObjNull && isOptional) return ObjNull.asMutable if (fieldPic) { - val (key, ver) = receiverKeyAndVersion(base) - rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) { - if (picCounters) PerfStats.fieldPicHit++ - noteReadHit() - val rec0 = g(base, scope) - if (base is ObjClass) { - val idx0 = base.classScope?.getSlotIndexOf(name) - if (idx0 != null) { tKey = key; tVer = ver; tFrameId = scope.frameId; tRecord = rec0 } else { tRecord = null } - } else { tRecord = null } - return rec0 - } } - rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) { - if (picCounters) PerfStats.fieldPicHit++ - noteReadHit() - // move-to-front: promote 2→1 - val tK = rKey2; val tV = rVer2; val tG = rGetter2 - rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 - rKey1 = tK; rVer1 = tV; rGetter1 = tG - val rec0 = g(base, scope) - if (base is ObjClass) { - val idx0 = base.classScope?.getSlotIndexOf(name) - if (idx0 != null) { tKey = key; tVer = ver; tFrameId = scope.frameId; tRecord = rec0 } else { tRecord = null } - } else { tRecord = null } - return rec0 - } } - if (size4ReadsEnabled()) rGetter3?.let { g -> if (key == rKey3 && ver == rVer3) { - if (picCounters) PerfStats.fieldPicHit++ - noteReadHit() - // move-to-front: promote 3→1 - val tK = rKey3; val tV = rVer3; val tG = rGetter3 - rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 - rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 - rKey1 = tK; rVer1 = tV; rGetter1 = tG - val rec0 = g(base, scope) - if (base is ObjClass) { - val idx0 = base.classScope?.getSlotIndexOf(name) - if (idx0 != null) { tKey = key; tVer = ver; tFrameId = scope.frameId; tRecord = rec0 } else { tRecord = null } - } else { tRecord = null } - return rec0 - } } - if (size4ReadsEnabled()) rGetter4?.let { g -> if (key == rKey4 && ver == rVer4) { - if (picCounters) PerfStats.fieldPicHit++ - noteReadHit() - // move-to-front: promote 4→1 - val tK = rKey4; val tV = rVer4; val tG = rGetter4 - rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3 - rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 - rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 - rKey1 = tK; rVer1 = tV; rGetter1 = tG - val rec0 = g(base, scope) - if (base is ObjClass) { - val idx0 = base.classScope?.getSlotIndexOf(name) - if (idx0 != null) { tKey = key; tVer = ver; tFrameId = scope.frameId; tRecord = rec0 } else { tRecord = null } - } else { tRecord = null } - return rec0 - } } - // Slow path - if (picCounters) PerfStats.fieldPicMiss++ - noteReadMiss() - val rec = try { - base.readField(scope, name) - } catch (e: ExecutionError) { - // Cache-after-miss negative entry: rethrow the same error quickly for this shape - rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3 - rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 - rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 - rKey1 = key; rVer1 = ver; rGetter1 = { _, sc -> sc.raiseError(e.message ?: "no such field: $name") } - throw e - } - // Install move-to-front with a handle-aware getter; honor PIC size flag - if (size4ReadsEnabled()) { - rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3 - rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 - } - rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 + val key: Long + val ver: Int when (base) { - is ObjClass -> { - val clsScope = base.classScope - val capturedIdx = clsScope?.getSlotIndexOf(name) - if (clsScope != null && capturedIdx != null) { - rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc -> - val scope0 = (obj as ObjClass).classScope!! - val r0 = scope0.getSlotRecord(capturedIdx) - if (!r0.visibility.isPublic) - sc.raiseError(ObjIllegalAccessException(sc, "can't access non-public field $name")) - r0 + is ObjInstance -> { key = base.objClass.classId; ver = base.objClass.layoutVersion } + is ObjClass -> { key = base.classId; ver = base.layoutVersion } + else -> { key = 0L; ver = -1 } + } + if (key != 0L) { + rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) { + if (picCounters) PerfStats.fieldPicHit++ + noteReadHit() + val rec0 = g(base, scope) + if (base is ObjClass) { + val idx0 = base.classScope?.getSlotIndexOf(name) + if (idx0 != null) { tKey = key; tVer = ver; tFrameId = scope.frameId; tRecord = rec0 } else { tRecord = null } + } else { tRecord = null } + return rec0 + } } + rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) { + if (picCounters) PerfStats.fieldPicHit++ + noteReadHit() + // move-to-front: promote 2→1 + val tK = rKey2; val tV = rVer2; val tG = rGetter2 + rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 + rKey1 = tK; rVer1 = tV; rGetter1 = tG + val rec0 = g(base, scope) + if (base is ObjClass) { + val idx0 = base.classScope?.getSlotIndexOf(name) + if (idx0 != null) { tKey = key; tVer = ver; tFrameId = scope.frameId; tRecord = rec0 } else { tRecord = null } + } else { tRecord = null } + return rec0 + } } + if (size4ReadsEnabled()) rGetter3?.let { g -> if (key == rKey3 && ver == rVer3) { + if (picCounters) PerfStats.fieldPicHit++ + noteReadHit() + // move-to-front: promote 3→1 + val tK = rKey3; val tV = rVer3; val tG = rGetter3 + rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 + rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 + rKey1 = tK; rVer1 = tV; rGetter1 = tG + val rec0 = g(base, scope) + if (base is ObjClass) { + val idx0 = base.classScope?.getSlotIndexOf(name) + if (idx0 != null) { tKey = key; tVer = ver; tFrameId = scope.frameId; tRecord = rec0 } else { tRecord = null } + } else { tRecord = null } + return rec0 + } } + if (size4ReadsEnabled()) rGetter4?.let { g -> if (key == rKey4 && ver == rVer4) { + if (picCounters) PerfStats.fieldPicHit++ + noteReadHit() + // move-to-front: promote 4→1 + val tK = rKey4; val tV = rVer4; val tG = rGetter4 + rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3 + rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 + rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 + rKey1 = tK; rVer1 = tV; rGetter1 = tG + val rec0 = g(base, scope) + if (base is ObjClass) { + val idx0 = base.classScope?.getSlotIndexOf(name) + if (idx0 != null) { tKey = key; tVer = ver; tFrameId = scope.frameId; tRecord = rec0 } else { tRecord = null } + } else { tRecord = null } + return rec0 + } } + // Slow path + if (picCounters) PerfStats.fieldPicMiss++ + noteReadMiss() + val rec = try { + base.readField(scope, name) + } catch (e: ExecutionError) { + // Cache-after-miss negative entry: rethrow the same error quickly for this shape + rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3 + rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 + rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 + rKey1 = key; rVer1 = ver; rGetter1 = { _, sc -> sc.raiseError(e.message ?: "no such field: $name") } + throw e + } + // Install move-to-front with a handle-aware getter; honor PIC size flag + if (size4ReadsEnabled()) { + rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3 + rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 + } + rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 + when (base) { + is ObjClass -> { + val clsScope = base.classScope + val capturedIdx = clsScope?.getSlotIndexOf(name) + if (clsScope != null && capturedIdx != null) { + rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc -> + val scope0 = (obj as ObjClass).classScope!! + val r0 = scope0.getSlotRecord(capturedIdx) + if (!r0.visibility.isPublic) + sc.raiseError(ObjIllegalAccessException(sc, "can't access non-public field $name")) + r0 + } + } else { + rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc -> obj.readField(sc, name) } } - } else { + } + is ObjInstance -> { + val cls = base.objClass + val effectiveKey = cls.publicMemberResolution[name] + if (effectiveKey != null) { + rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc -> + if (obj is ObjInstance && obj.objClass === cls) { + val rec = obj.instanceScope.objects[effectiveKey] + if (rec != null && rec.type != ObjRecord.Type.Delegated) rec + else obj.readField(sc, name) + } else obj.readField(sc, name) + } + } else { + rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc -> obj.readField(sc, name) } + } + } + else -> { + // For instances and other types, fall back to name-based lookup per access (slot index may differ per instance) rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc -> obj.readField(sc, name) } } } - else -> { - // For instances and other types, fall back to name-based lookup per access (slot index may differ per instance) - rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc -> obj.readField(sc, name) } - } + return rec } - return rec } return base.readField(scope, name) } @@ -635,9 +721,15 @@ class FieldRef( } // Read→write micro fast-path: reuse transient record captured by get() if (fieldPic) { - val (k, v) = receiverKeyAndVersion(base) + val key: Long + val ver: Int + when (base) { + is ObjInstance -> { key = base.objClass.classId; ver = base.objClass.layoutVersion } + is ObjClass -> { key = base.classId; ver = base.layoutVersion } + else -> { key = 0L; ver = -1 } + } val rec = tRecord - if (rec != null && tKey == k && tVer == v && tFrameId == scope.frameId) { + if (rec != null && tKey == key && tVer == ver && tFrameId == scope.frameId) { // If it is a property, we must go through writeField (slow path for now) // or handle it here. if (rec.type != ObjRecord.Type.Property) { @@ -649,76 +741,91 @@ class FieldRef( return } } - } - if (fieldPic) { - val (key, ver) = receiverKeyAndVersion(base) - wSetter1?.let { s -> if (key == wKey1 && ver == wVer1) { - if (picCounters) PerfStats.fieldPicSetHit++ - noteWriteHit() - return s(base, scope, newValue) - } } - wSetter2?.let { s -> if (key == wKey2 && ver == wVer2) { - if (picCounters) PerfStats.fieldPicSetHit++ - noteWriteHit() - // move-to-front: promote 2→1 - val tK = wKey2; val tV = wVer2; val tS = wSetter2 + if (key != 0L) { + wSetter1?.let { s -> if (key == wKey1 && ver == wVer1) { + if (picCounters) PerfStats.fieldPicSetHit++ + noteWriteHit() + return s(base, scope, newValue) + } } + wSetter2?.let { s -> if (key == wKey2 && ver == wVer2) { + if (picCounters) PerfStats.fieldPicSetHit++ + noteWriteHit() + // move-to-front: promote 2→1 + val tK = wKey2; val tV = wVer2; val tS = wSetter2 + wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1 + wKey1 = tK; wVer1 = tV; wSetter1 = tS + return s(base, scope, newValue) + } } + if (size4WritesEnabled()) wSetter3?.let { s -> if (key == wKey3 && ver == wVer3) { + if (picCounters) PerfStats.fieldPicSetHit++ + noteWriteHit() + // move-to-front: promote 3→1 + val tK = wKey3; val tV = wVer3; val tS = wSetter3 + wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2 + wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1 + wKey1 = tK; wVer1 = tV; wSetter1 = tS + return s(base, scope, newValue) + } } + if (size4WritesEnabled()) wSetter4?.let { s -> if (key == wKey4 && ver == wVer4) { + if (picCounters) PerfStats.fieldPicSetHit++ + noteWriteHit() + // move-to-front: promote 4→1 + val tK = wKey4; val tV = wVer4; val tS = wSetter4 + wKey4 = wKey3; wVer4 = wVer3; wSetter4 = wSetter3 + wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2 + wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1 + wKey1 = tK; wVer1 = tV; wSetter1 = tS + return s(base, scope, newValue) + } } + // Slow path + if (picCounters) PerfStats.fieldPicSetMiss++ + noteWriteMiss() + base.writeField(scope, name, newValue) + // Install move-to-front with a handle-aware setter; honor PIC size flag + if (size4WritesEnabled()) { + wKey4 = wKey3; wVer4 = wVer3; wSetter4 = wSetter3 + wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2 + } wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1 - wKey1 = tK; wVer1 = tV; wSetter1 = tS - return s(base, scope, newValue) - } } - if (size4WritesEnabled()) wSetter3?.let { s -> if (key == wKey3 && ver == wVer3) { - if (picCounters) PerfStats.fieldPicSetHit++ - noteWriteHit() - // move-to-front: promote 3→1 - val tK = wKey3; val tV = wVer3; val tS = wSetter3 - wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2 - wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1 - wKey1 = tK; wVer1 = tV; wSetter1 = tS - return s(base, scope, newValue) - } } - if (size4WritesEnabled()) wSetter4?.let { s -> if (key == wKey4 && ver == wVer4) { - if (picCounters) PerfStats.fieldPicSetHit++ - noteWriteHit() - // move-to-front: promote 4→1 - val tK = wKey4; val tV = wVer4; val tS = wSetter4 - wKey4 = wKey3; wVer4 = wVer3; wSetter4 = wSetter3 - wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2 - wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1 - wKey1 = tK; wVer1 = tV; wSetter1 = tS - return s(base, scope, newValue) - } } - // Slow path - if (picCounters) PerfStats.fieldPicSetMiss++ - noteWriteMiss() - base.writeField(scope, name, newValue) - // Install move-to-front with a handle-aware setter; honor PIC size flag - if (size4WritesEnabled()) { - wKey4 = wKey3; wVer4 = wVer3; wSetter4 = wSetter3 - wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2 - } - wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1 - when (base) { - is ObjClass -> { - val clsScope = base.classScope - val capturedIdx = clsScope?.getSlotIndexOf(name) - if (clsScope != null && capturedIdx != null) { - wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, v -> - val scope0 = (obj as ObjClass).classScope!! - val r0 = scope0.getSlotRecord(capturedIdx) - if (!r0.isMutable) - sc.raiseError(ObjIllegalAssignmentException(sc, "can't reassign val $name")) - if (r0.value.assign(sc, v) == null) r0.value = v + when (base) { + is ObjClass -> { + val clsScope = base.classScope + val capturedIdx = clsScope?.getSlotIndexOf(name) + if (clsScope != null && capturedIdx != null) { + wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, v -> + val scope0 = (obj as ObjClass).classScope!! + val r0 = scope0.getSlotRecord(capturedIdx) + if (!r0.isMutable) + sc.raiseError(ObjIllegalAssignmentException(sc, "can't reassign val $name")) + if (r0.value.assign(sc, v) == null) r0.value = v + } + } else { + wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, v -> obj.writeField(sc, name, v) } } - } else { + } + is ObjInstance -> { + val cls = base.objClass + val effectiveKey = cls.publicMemberResolution[name] + if (effectiveKey != null) { + wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, nv -> + if (obj is ObjInstance && obj.objClass === cls) { + val rec = obj.instanceScope.objects[effectiveKey] + if (rec != null && rec.effectiveWriteVisibility == Visibility.Public && rec.isMutable && rec.type == ObjRecord.Type.Field) { + if (rec.value.assign(sc, nv) == null) rec.value = nv + } else obj.writeField(sc, name, nv) + } else obj.writeField(sc, name, nv) + } + } else { + wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, v -> obj.writeField(sc, name, v) } + } + } + else -> { + // For instances and other types, fall back to generic write (instance slot indices may differ per instance) wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, v -> obj.writeField(sc, name, v) } } } - else -> { - // For instances and other types, fall back to generic write (instance slot indices may differ per instance) - wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, v -> obj.writeField(sc, name, v) } - } + return } - return } base.writeField(scope, name, newValue) } @@ -736,51 +843,48 @@ class FieldRef( val base = target.evalValue(scope) if (base == ObjNull && isOptional) return ObjNull if (fieldPic) { - val (key, ver) = receiverKeyAndVersion(base) - rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) { - if (picCounters) PerfStats.fieldPicHit++ - return g(base, scope).value - } } - rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) { - if (picCounters) PerfStats.fieldPicHit++ - val tK = rKey2; val tV = rVer2; val tG = rGetter2 - rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 - rKey1 = tK; rVer1 = tV; rGetter1 = tG - return g(base, scope).value - } } - if (size4ReadsEnabled()) rGetter3?.let { g -> if (key == rKey3 && ver == rVer3) { - if (picCounters) PerfStats.fieldPicHit++ - val tK = rKey3; val tV = rVer3; val tG = rGetter3 - rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 - rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 - rKey1 = tK; rVer1 = tV; rGetter1 = tG - return g(base, scope).value - } } - if (size4ReadsEnabled()) rGetter4?.let { g -> if (key == rKey4 && ver == rVer4) { - if (picCounters) PerfStats.fieldPicHit++ - val tK = rKey4; val tV = rVer4; val tG = rGetter4 - rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3 - rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 - rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 - rKey1 = tK; rVer1 = tV; rGetter1 = tG - return g(base, scope).value - } } - if (picCounters) PerfStats.fieldPicMiss++ - val rec = base.readField(scope, name) - // install primary generic getter for this shape + val key: Long + val ver: Int when (base) { - is ObjClass -> { - rKey1 = base.classId; rVer1 = base.layoutVersion; rGetter1 = { obj, sc -> obj.readField(sc, name) } - } - is ObjInstance -> { - val cls = base.objClass - rKey1 = cls.classId; rVer1 = cls.layoutVersion; rGetter1 = { obj, sc -> obj.readField(sc, name) } - } - else -> { - rKey1 = 0L; rVer1 = -1; rGetter1 = null - } + is ObjInstance -> { key = base.objClass.classId; ver = base.objClass.layoutVersion } + is ObjClass -> { key = base.classId; ver = base.layoutVersion } + else -> { key = 0L; ver = -1 } + } + if (key != 0L) { + rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) { + if (picCounters) PerfStats.fieldPicHit++ + return g(base, scope).value + } } + rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) { + if (picCounters) PerfStats.fieldPicHit++ + val tK = rKey2; val tV = rVer2; val tG = rGetter2 + rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 + rKey1 = tK; rVer1 = tV; rGetter1 = tG + return g(base, scope).value + } } + if (size4ReadsEnabled()) rGetter3?.let { g -> if (key == rKey3 && ver == rVer3) { + if (picCounters) PerfStats.fieldPicHit++ + val tK = rKey3; val tV = rVer3; val tG = rGetter3 + rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 + rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 + rKey1 = tK; rVer1 = tV; rGetter1 = tG + return g(base, scope).value + } } + if (size4ReadsEnabled()) rGetter4?.let { g -> if (key == rKey4 && ver == rVer4) { + if (picCounters) PerfStats.fieldPicHit++ + val tK = rKey4; val tV = rVer4; val tG = rGetter4 + rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3 + rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 + rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 + rKey1 = tK; rVer1 = tV; rGetter1 = tG + return g(base, scope).value + } } + if (picCounters) PerfStats.fieldPicMiss++ + val rec = base.readField(scope, name) + // install primary generic getter for this shape + rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc -> obj.readField(sc, name) } + return rec.value } - return rec.value } return base.readField(scope, name).value } @@ -815,6 +919,7 @@ class IndexRef( val base = target.evalValue(scope) if (base == ObjNull && isOptional) return ObjNull.asMutable val idx = index.evalValue(scope) + val picCounters = PerfFlags.PIC_DEBUG_COUNTERS if (PerfFlags.RVAL_FASTPATH) { // Primitive list index fast path: avoid virtual dispatch to getAt when shapes match if (base is ObjList && idx is ObjInt) { @@ -834,25 +939,27 @@ class IndexRef( } if (PerfFlags.INDEX_PIC) { // Polymorphic inline cache for other common shapes - val (key, ver) = when (base) { - is ObjInstance -> base.objClass.classId to base.objClass.layoutVersion - is ObjClass -> base.classId to base.layoutVersion - else -> 0L to -1 + val key: Long + val ver: Int + when (base) { + is ObjInstance -> { key = base.objClass.classId; ver = base.objClass.layoutVersion } + is ObjClass -> { key = base.classId; ver = base.layoutVersion } + else -> { key = 0L; ver = -1 } } if (key != 0L) { rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) { - if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.indexPicHit++ + if (picCounters) PerfStats.indexPicHit++ return g(base, scope, idx).asMutable } } rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) { - if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.indexPicHit++ + if (picCounters) PerfStats.indexPicHit++ val tk = rKey2; val tv = rVer2; val tg = rGetter2 rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 rKey1 = tk; rVer1 = tv; rGetter1 = tg return g(base, scope, idx).asMutable } } if (PerfFlags.INDEX_PIC_SIZE_4) rGetter3?.let { g -> if (key == rKey3 && ver == rVer3) { - if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.indexPicHit++ + if (picCounters) PerfStats.indexPicHit++ val tk = rKey3; val tv = rVer3; val tg = rGetter3 rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 @@ -860,7 +967,7 @@ class IndexRef( return g(base, scope, idx).asMutable } } if (PerfFlags.INDEX_PIC_SIZE_4) rGetter4?.let { g -> if (key == rKey4 && ver == rVer4) { - if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.indexPicHit++ + if (picCounters) PerfStats.indexPicHit++ val tk = rKey4; val tv = rVer4; val tg = rGetter4 rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3 rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 @@ -869,7 +976,7 @@ class IndexRef( return g(base, scope, idx).asMutable } } // Miss: resolve and install generic handler - if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.indexPicMiss++ + if (picCounters) PerfStats.indexPicMiss++ val v = base.getAt(scope, idx) if (PerfFlags.INDEX_PIC_SIZE_4) { rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3 @@ -888,6 +995,7 @@ class IndexRef( val base = target.evalValue(scope) if (base == ObjNull && isOptional) return ObjNull val idx = index.evalValue(scope) + val picCounters = PerfFlags.PIC_DEBUG_COUNTERS if (PerfFlags.RVAL_FASTPATH) { // Fast list[int] path if (base is ObjList && idx is ObjInt) { @@ -905,25 +1013,27 @@ class IndexRef( } if (PerfFlags.INDEX_PIC) { // PIC path analogous to get(), but returning raw Obj - val (key, ver) = when (base) { - is ObjInstance -> base.objClass.classId to base.objClass.layoutVersion - is ObjClass -> base.classId to base.layoutVersion - else -> 0L to -1 + val key: Long + val ver: Int + when (base) { + is ObjInstance -> { key = base.objClass.classId; ver = base.objClass.layoutVersion } + is ObjClass -> { key = base.classId; ver = base.layoutVersion } + else -> { key = 0L; ver = -1 } } if (key != 0L) { rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) { - if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.indexPicHit++ + if (picCounters) PerfStats.indexPicHit++ return g(base, scope, idx) } } rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) { - if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.indexPicHit++ + if (picCounters) PerfStats.indexPicHit++ val tk = rKey2; val tv = rVer2; val tg = rGetter2 rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 rKey1 = tk; rVer1 = tv; rGetter1 = tg return g(base, scope, idx) } } if (PerfFlags.INDEX_PIC_SIZE_4) rGetter3?.let { g -> if (key == rKey3 && ver == rVer3) { - if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.indexPicHit++ + if (picCounters) PerfStats.indexPicHit++ val tk = rKey3; val tv = rVer3; val tg = rGetter3 rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1 @@ -931,7 +1041,7 @@ class IndexRef( return g(base, scope, idx) } } if (PerfFlags.INDEX_PIC_SIZE_4) rGetter4?.let { g -> if (key == rKey4 && ver == rVer4) { - if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.indexPicHit++ + if (picCounters) PerfStats.indexPicHit++ val tk = rKey4; val tv = rVer4; val tg = rGetter4 rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3 rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2 @@ -939,7 +1049,7 @@ class IndexRef( rKey1 = tk; rVer1 = tv; rGetter1 = tg return g(base, scope, idx) } } - if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.indexPicMiss++ + if (picCounters) PerfStats.indexPicMiss++ val v = base.getAt(scope, idx) if (PerfFlags.INDEX_PIC_SIZE_4) { rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3 @@ -975,10 +1085,12 @@ class IndexRef( } if (PerfFlags.RVAL_FASTPATH && PerfFlags.INDEX_PIC) { // Polymorphic inline cache for index write - val (key, ver) = when (base) { - is ObjInstance -> base.objClass.classId to base.objClass.layoutVersion - is ObjClass -> base.classId to base.layoutVersion - else -> 0L to -1 + val key: Long + val ver: Int + when (base) { + is ObjInstance -> { key = base.objClass.classId; ver = base.objClass.layoutVersion } + is ObjClass -> { key = base.classId; ver = base.layoutVersion } + else -> { key = 0L; ver = -1 } } if (key != 0L) { wSetter1?.let { s -> if (key == wKey1 && ver == wVer1) { s(base, scope, idx, newValue); return } } @@ -1130,104 +1242,154 @@ class MethodCallRef( val base = receiver.evalValue(scope) if (base == ObjNull && isOptional) return ObjNull.asReadonly val callArgs = args.toArguments(scope, tailBlock) - if (methodPic) { - val (key, ver) = receiverKeyAndVersion(base) - mInvoker1?.let { inv -> if (key == mKey1 && ver == mVer1) { - if (picCounters) PerfStats.methodPicHit++ - noteMethodHit() - return inv(base, scope, callArgs).asReadonly - } } - mInvoker2?.let { inv -> if (key == mKey2 && ver == mVer2) { - if (picCounters) PerfStats.methodPicHit++ - noteMethodHit() - return inv(base, scope, callArgs).asReadonly - } } - if (size4MethodsEnabled()) mInvoker3?.let { inv -> if (key == mKey3 && ver == mVer3) { - if (picCounters) PerfStats.methodPicHit++ - noteMethodHit() - // move-to-front: promote 3→1 - val tK = mKey3; val tV = mVer3; val tI = mInvoker3 - mKey3 = mKey2; mVer3 = mVer2; mInvoker3 = mInvoker2 - mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1 - mKey1 = tK; mVer1 = tV; mInvoker1 = tI - return inv(base, scope, callArgs).asReadonly - } } - if (size4MethodsEnabled()) mInvoker4?.let { inv -> if (key == mKey4 && ver == mVer4) { - if (picCounters) PerfStats.methodPicHit++ - noteMethodHit() - // move-to-front: promote 4→1 - val tK = mKey4; val tV = mVer4; val tI = mInvoker4 - mKey4 = mKey3; mVer4 = mVer3; mInvoker4 = mInvoker3 - mKey3 = mKey2; mVer3 = mVer2; mInvoker3 = mInvoker2 - mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1 - mKey1 = tK; mVer1 = tV; mInvoker1 = tI - return inv(base, scope, callArgs).asReadonly - } } - // Slow path - if (picCounters) PerfStats.methodPicMiss++ - noteMethodMiss() - val result = try { - base.invokeInstanceMethod(scope, name, callArgs) - } catch (e: ExecutionError) { - // Cache-after-miss negative entry for this shape - mKey4 = mKey3; mVer4 = mVer3; mInvoker4 = mInvoker3 - mKey3 = mKey2; mVer3 = mVer2; mInvoker3 = mInvoker2 - mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1 - mKey1 = key; mVer1 = ver; mInvoker1 = { _, sc, _ -> sc.raiseError(e.message ?: "method not found: $name") } - throw e - } - // Install move-to-front with a handle-aware invoker; honor PIC size flag - if (size4MethodsEnabled()) { - mKey4 = mKey3; mVer4 = mVer3; mInvoker4 = mInvoker3 - mKey3 = mKey2; mVer3 = mVer2; mInvoker3 = mInvoker2 - } - mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1 - when (base) { - is ObjInstance -> { - // Prefer resolved class member to avoid per-call lookup on hit - // BUT only if it's NOT a root object member (which can be shadowed by extensions) - var hierarchyMember: ObjRecord? = null - for (cls in base.objClass.mro) { - if (cls.className == "Obj") break - val rec = cls.members[name] ?: cls.classScope?.objects?.get(name) - if (rec != null && !rec.isAbstract && rec.type != ObjRecord.Type.Field) { - hierarchyMember = rec - break - } - } + return performInvoke(scope, base, callArgs, methodPic, picCounters).asReadonly + } - if (hierarchyMember != null) { - val visibility = hierarchyMember.visibility - val callable = hierarchyMember.value - mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> - val inst = obj as ObjInstance - if (!visibility.isPublic && !canAccessMember(visibility, hierarchyMember.declaringClass ?: inst.objClass, sc.currentClassCtx)) - sc.raiseError(ObjIllegalAccessException(sc, "can't invoke non-public method $name")) - callable.invoke(inst.instanceScope, inst, a) - } - } else { - // Fallback to name-based lookup per call (handles extensions and root members) - mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> obj.invokeInstanceMethod(sc, name, a) } - } - } - is ObjClass -> { - val clsScope = base.classScope - val rec = clsScope?.get(name) - if (rec != null) { - val callable = rec.value - mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> callable.invoke(sc, obj, a) } - } else { - mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> obj.invokeInstanceMethod(sc, name, a) } - } - } - else -> { - mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> obj.invokeInstanceMethod(sc, name, a) } - } + override suspend fun evalValue(scope: Scope): Obj { + val methodPic = PerfFlags.METHOD_PIC + val picCounters = PerfFlags.PIC_DEBUG_COUNTERS + val base = receiver.evalValue(scope) + if (base == ObjNull && isOptional) return ObjNull + val callArgs = args.toArguments(scope, tailBlock) + return performInvoke(scope, base, callArgs, methodPic, picCounters) + } + + private suspend fun performInvoke( + scope: Scope, + base: Obj, + callArgs: Arguments, + methodPic: Boolean, + picCounters: Boolean + ): Obj { + if (methodPic) { + val key: Long + val ver: Int + when (base) { + is ObjInstance -> { key = base.objClass.classId; ver = base.objClass.layoutVersion } + is ObjClass -> { key = base.classId; ver = base.layoutVersion } + else -> { key = 0L; ver = -1 } + } + if (key != 0L) { + mInvoker1?.let { inv -> + if (key == mKey1 && ver == mVer1) { + if (picCounters) PerfStats.methodPicHit++ + noteMethodHit() + return inv(base, scope, callArgs) + } + } + mInvoker2?.let { inv -> + if (key == mKey2 && ver == mVer2) { + if (picCounters) PerfStats.methodPicHit++ + noteMethodHit() + // move-to-front: promote 2→1 + val tK = mKey2; val tV = mVer2; val tI = mInvoker2 + mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1 + mKey1 = tK; mVer1 = tV; mInvoker1 = tI + return inv(base, scope, callArgs) + } + } + if (size4MethodsEnabled()) mInvoker3?.let { inv -> + if (key == mKey3 && ver == mVer3) { + if (picCounters) PerfStats.methodPicHit++ + noteMethodHit() + // move-to-front: promote 3→1 + val tK = mKey3; val tV = mVer3; val tI = mInvoker3 + mKey3 = mKey2; mVer3 = mVer2; mInvoker3 = mInvoker2 + mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1 + mKey1 = tK; mVer1 = tV; mInvoker1 = tI + return inv(base, scope, callArgs) + } + } + if (size4MethodsEnabled()) mInvoker4?.let { inv -> + if (key == mKey4 && ver == mVer4) { + if (picCounters) PerfStats.methodPicHit++ + noteMethodHit() + // move-to-front: promote 4→1 + val tK = mKey4; val tV = mVer4; val tI = mInvoker4 + mKey4 = mKey3; mVer4 = mVer3; mInvoker4 = mInvoker3 + mKey3 = mKey2; mVer3 = mVer2; mInvoker3 = mInvoker2 + mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1 + mKey1 = tK; mVer1 = tV; mInvoker1 = tI + return inv(base, scope, callArgs) + } + } + // Slow path + if (picCounters) PerfStats.methodPicMiss++ + noteMethodMiss() + val result = try { + base.invokeInstanceMethod(scope, name, callArgs) + } catch (e: ExecutionError) { + // Cache-after-miss negative entry for this shape + mKey4 = mKey3; mVer4 = mVer3; mInvoker4 = mInvoker3 + mKey3 = mKey2; mVer3 = mVer2; mInvoker3 = mInvoker2 + mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1 + mKey1 = key; mVer1 = ver; mInvoker1 = { _, sc, _ -> sc.raiseError(e.message ?: "method not found: $name") } + throw e + } + // Install move-to-front with a handle-aware invoker; honor PIC size flag + if (size4MethodsEnabled()) { + mKey4 = mKey3; mVer4 = mVer3; mInvoker4 = mInvoker3 + mKey3 = mKey2; mVer3 = mVer2; mInvoker3 = mInvoker2 + } + mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1 + when (base) { + is ObjInstance -> { + // Prefer resolved class member to avoid per-call lookup on hit + // BUT only if it's NOT a root object member (which can be shadowed by extensions) + var hierarchyMember: ObjRecord? = null + val cls0 = base.objClass + val keyInScope = cls0.publicMemberResolution[name] + if (keyInScope != null) { + val rec = base.instanceScope.objects[keyInScope] + if (rec != null && rec.type == ObjRecord.Type.Fun) { + hierarchyMember = rec + } + } + + if (hierarchyMember == null) { + for (cls in base.objClass.mro) { + if (cls.className == "Obj") break + val rec = cls.members[name] ?: cls.classScope?.objects?.get(name) + if (rec != null && !rec.isAbstract && rec.type != ObjRecord.Type.Field) { + hierarchyMember = rec + break + } + } + } + + if (hierarchyMember != null) { + val visibility = hierarchyMember.visibility + val callable = hierarchyMember.value + val decl = hierarchyMember.declaringClass ?: base.objClass + mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> + val inst = obj as ObjInstance + if (!visibility.isPublic && !canAccessMember(visibility, decl, sc.currentClassCtx)) + sc.raiseError(ObjIllegalAccessException(sc, "can't invoke non-public method $name")) + callable.invoke(inst.instanceScope, inst, a) + } + } else { + // Fallback to name-based lookup per call (handles extensions and root members) + mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> obj.invokeInstanceMethod(sc, name, a) } + } + } + is ObjClass -> { + val clsScope = base.classScope + val rec = clsScope?.get(name) + if (rec != null) { + val callable = rec.value + mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> callable.invoke(sc, obj, a) } + } else { + mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> obj.invokeInstanceMethod(sc, name, a) } + } + } + else -> { + mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> obj.invokeInstanceMethod(sc, name, a) } + } + } + return result } - return result.asReadonly } - val result = base.invokeInstanceMethod(scope, name, callArgs) - return result.asReadonly + return base.invokeInstanceMethod(scope, name, callArgs) } private fun receiverKeyAndVersion(obj: Obj): Pair = when (obj) { @@ -1587,14 +1749,17 @@ class ListLiteralRef(private val entries: List) : ObjRef { } override suspend fun get(scope: Scope): ObjRecord { + return evalValue(scope).asMutable + } + + override suspend fun evalValue(scope: Scope): Obj { // Heuristic capacity hint: count element entries; spreads handled opportunistically val elemCount = entries.count { it is ListEntry.Element } val list = ArrayList(elemCount) for (e in entries) { when (e) { is ListEntry.Element -> { - val v = if (PerfFlags.RVAL_FASTPATH) e.ref.evalValue(scope) else e.ref.evalValue(scope) - list += v + list += e.ref.evalValue(scope) } is ListEntry.Spread -> { val elements = e.ref.evalValue(scope) @@ -1609,7 +1774,7 @@ class ListLiteralRef(private val entries: List) : ObjRef { } } } - return ObjList(list).asMutable + return ObjList(list) } override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) { @@ -1669,6 +1834,10 @@ sealed class MapLiteralEntry { class MapLiteralRef(private val entries: List) : ObjRef { override suspend fun get(scope: Scope): ObjRecord { + return evalValue(scope).asReadonly + } + + override suspend fun evalValue(scope: Scope): Obj { val result = ObjMap(mutableMapOf()) for (e in entries) { when (e) { @@ -1685,7 +1854,7 @@ class MapLiteralRef(private val entries: List) : ObjRef { } } } - return result.asReadonly + return result } } @@ -1698,9 +1867,13 @@ class RangeRef( private val isEndInclusive: Boolean ) : ObjRef { override suspend fun get(scope: Scope): ObjRecord { + return evalValue(scope).asReadonly + } + + override suspend fun evalValue(scope: Scope): Obj { val l = left?.evalValue(scope) ?: ObjNull val r = right?.evalValue(scope) ?: ObjNull - return ObjRange(l, r, isEndInclusive = isEndInclusive).asReadonly + return ObjRange(l, r, isEndInclusive = isEndInclusive) } } @@ -1711,11 +1884,15 @@ class AssignIfNullRef( private val atPos: Pos, ) : ObjRef { override suspend fun get(scope: Scope): ObjRecord { + return evalValue(scope).asReadonly + } + + override suspend fun evalValue(scope: Scope): Obj { val current = target.evalValue(scope) - if (current != ObjNull) return current.asReadonly + if (current != ObjNull) return current val newValue = value.evalValue(scope) target.setAt(atPos, scope, newValue) - return newValue.asReadonly + return newValue } } @@ -1726,6 +1903,10 @@ class AssignRef( private val atPos: Pos, ) : ObjRef { override suspend fun get(scope: Scope): ObjRecord { + return evalValue(scope).asReadonly + } + + override suspend fun evalValue(scope: Scope): Obj { val v = value.evalValue(scope) // For properties, we should not call get() on target because it invokes the getter. // Instead, we call setAt directly. @@ -1738,7 +1919,7 @@ class AssignRef( target.setAt(atPos, scope, v) } } - return v.asReadonly + return v } } diff --git a/lynglib/src/commonTest/kotlin/MiniAstTest.kt b/lynglib/src/commonTest/kotlin/MiniAstTest.kt index 922b1d4..87fc92d 100644 --- a/lynglib/src/commonTest/kotlin/MiniAstTest.kt +++ b/lynglib/src/commonTest/kotlin/MiniAstTest.kt @@ -419,6 +419,25 @@ class MiniAstTest { 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 fun inferTypeForValWithInference() = runTest { val code = """ diff --git a/lynglib/src/jvmTest/kotlin/net/sergeych/lyng/miniast/CompletionEngineLightTest.kt b/lynglib/src/jvmTest/kotlin/net/sergeych/lyng/miniast/CompletionEngineLightTest.kt index ee83f27..23271fc 100644 --- a/lynglib/src/jvmTest/kotlin/net/sergeych/lyng/miniast/CompletionEngineLightTest.kt +++ b/lynglib/src/jvmTest/kotlin/net/sergeych/lyng/miniast/CompletionEngineLightTest.kt @@ -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 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") } + @Test + fun functionArgumentsInBody() = runBlocking { + val code = """ + fun test(myArg1, myArg2) { + myA + } + """.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 + } + } + """.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 fun nestedShadowingCompletion() = runBlocking { val code = """ diff --git a/lyngweb/src/jsMain/kotlin/net/sergeych/lyngweb/EditorLogic.kt b/lyngweb/src/jsMain/kotlin/net/sergeych/lyngweb/EditorLogic.kt index c83bea1..60ed886 100644 --- a/lyngweb/src/jsMain/kotlin/net/sergeych/lyngweb/EditorLogic.kt +++ b/lyngweb/src/jsMain/kotlin/net/sergeych/lyngweb/EditorLogic.kt @@ -98,6 +98,7 @@ fun applyEnter(text: String, selStart: Int, selEnd: Int, tabSize: Int): EditResu val lineStart = lineStartAt(text, start) val lineEnd = lineEndAt(text, start) val indent = countIndentSpaces(text, lineStart, lineEnd) + val lineTrimmed = text.substring(lineStart, lineEnd).trim() // Compute neighborhood characters early so rule precedence can use them 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 after = text.substring(start) - // 1) Between braces { | } -> two lines, inner indented + // Rule 4: Between braces on the same line {|} if (prevCh == '{' && nextCh == '}') { val innerIndent = indent + tabSize 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 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 == '{') { val insertion = "\n" + " ".repeat(indent + tabSize) val out = before + insertion + after val caret = start + insertion.length return EditResult(out, caret, caret) } - // 3) Before '}' - if (nextCh == '}') { - val insertion = "\n" + " ".repeat(indent) + + // Rule 3: End of a line before a brace-only next line + 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 caret = start + insertion.length return EditResult(out, caret, caret) } - // default keep same indent + // Rule 6: Default smart indent val insertion = "\n" + " ".repeat(indent) val out = before + insertion + after val caret = start + insertion.length diff --git a/site/src/jsMain/resources/index.html b/site/src/jsMain/resources/index.html index 601c077..3c3cb0b 100644 --- a/site/src/jsMain/resources/index.html +++ b/site/src/jsMain/resources/index.html @@ -329,7 +329,7 @@
- v1.1.1-SNAPSHOT + v1.2.0-SNAPSHOT
diff --git a/site/src/jsTest/kotlin/EditorE2ETest.kt b/site/src/jsTest/kotlin/EditorE2ETest.kt index fc6cc0c..bd729e7 100644 --- a/site/src/jsTest/kotlin/EditorE2ETest.kt +++ b/site/src/jsTest/kotlin/EditorE2ETest.kt @@ -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"); * 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 private fun typeText(ta: HTMLTextAreaElement, s: String) { for (ch in s) { - val start = ta.selectionStart ?: 0 - val end = ta.selectionEnd ?: start - val before = ta.value.substring(0, start) - val after = ta.value.substring(end) - ta.value = before + ch + after - val newPos = start + 1 - ta.selectionStart = newPos - ta.selectionEnd = newPos - // Fire input so EditorWithOverlay updates its state - val ev = js("new Event('input', {bubbles:true})") - ta.dispatchEvent(ev.unsafeCast()) + val key = ch.toString() + val ev = js("new KeyboardEvent('keydown', {key: key, bubbles: true, cancelable: true})") + val wasPrevented = !ta.dispatchEvent(ev.unsafeCast()) + + if (!wasPrevented) { + val start = ta.selectionStart ?: 0 + val end = ta.selectionEnd ?: start + val before = ta.value.substring(0, start) + val after = ta.value.substring(end) + ta.value = before + ch + after + 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()) + } } } @@ -243,10 +249,10 @@ class EditorE2ETest { typeText(ta, "4"); nextFrame() dispatchKey(ta, key = "Enter"); nextFrame(); nextFrame() - typeText(ta, "}"); nextFrame() + typeText(ta, "}"); nextFrame(); nextFrame(); nextFrame() dispatchKey(ta, key = "Enter"); nextFrame(); nextFrame() - typeText(ta, "5"); nextFrame(); nextFrame() + typeText(ta, "5"); nextFrame(); nextFrame(); nextFrame() val expected = ( """