Refactored ObjRef evaluation logic for enhanced performance and consistency across unary, binary, and field operations. Added evalValue overrides for more streamlined value resolution.
This commit is contained in:
parent
827df9c8cd
commit
6b957ae6a3
@ -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 <idea-version> ourselves in plugin.xml to keep it open-ended (no upper cap)
|
||||
updateSinceUntilBuild.set(false)
|
||||
// Include only available bundled plugins for this IDE build
|
||||
|
||||
@ -167,8 +167,12 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
.withIcon(AllIcons.Nodes.Field)
|
||||
.let { b -> if (!ci.typeText.isNullOrBlank()) b.withTypeText(ci.typeText, true) else b }
|
||||
}
|
||||
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) {
|
||||
val existing = engineItems.map { it.name }.toMutableSet()
|
||||
@ -401,7 +405,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
}
|
||||
supplementPreferredBases(className)
|
||||
|
||||
fun emitGroup(map: LinkedHashMap<String, MutableList<MiniMemberDecl>>) {
|
||||
fun emitGroup(map: LinkedHashMap<String, MutableList<MiniMemberDecl>>, groupPriority: Double) {
|
||||
val keys = map.keys.sortedBy { it.lowercase() }
|
||||
for (name in keys) {
|
||||
val list = map[name] ?: continue
|
||||
@ -428,8 +432,12 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
.withTailText(tail, true)
|
||||
.withTypeText(ret, true)
|
||||
.withInsertHandler(ParenInsertHandler)
|
||||
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
|
||||
// Prefer a field variant with known type if available
|
||||
@ -439,16 +447,20 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
val builder = LookupElementBuilder.create(name)
|
||||
.withIcon(icon)
|
||||
.withTypeText(typeOf(chosen.type), true)
|
||||
if (groupPriority != 0.0) {
|
||||
emit(PrioritizedLookupElement.withPriority(builder, groupPriority))
|
||||
} else {
|
||||
emit(builder)
|
||||
}
|
||||
}
|
||||
is MiniInitDecl -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<TextRange>) = 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<TextRange>) = 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<PsiElement>() {
|
||||
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
|
||||
|
||||
@ -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 {
|
||||
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")
|
||||
true
|
||||
} catch (_: Throwable) {
|
||||
false
|
||||
anyLoaded = true
|
||||
} catch (_: Throwable) {}
|
||||
|
||||
try {
|
||||
val cls = Class.forName("net.sergeych.lyngio.docs.ProcessBuiltinDocs")
|
||||
val m = cls.getMethod("ensure")
|
||||
m.invoke(null)
|
||||
log.info("[LYNG_DEBUG] DocsBootstrap: external docs loaded: net.sergeych.lyngio.docs.ProcessBuiltinDocs.ensure() OK")
|
||||
anyLoaded = true
|
||||
} catch (_: Throwable) {}
|
||||
return anyLoaded
|
||||
}
|
||||
|
||||
private fun trySeedFallback(): Boolean = try {
|
||||
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")
|
||||
}
|
||||
|
||||
@ -16,8 +16,8 @@
|
||||
-->
|
||||
|
||||
<idea-plugin>
|
||||
<!-- Open-ended compatibility: 2024.3+ (build 243 and newer) -->
|
||||
<idea-version since-build="243"/>
|
||||
<!-- Open-ended compatibility: 2024.1+ (build 241 and newer) -->
|
||||
<idea-version since-build="241"/>
|
||||
<id>net.sergeych.lyng.idea</id>
|
||||
<name>Lyng</name>
|
||||
<vendor email="real.sergeych@gmail.com">Sergey Chernov</vendor>
|
||||
|
||||
@ -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.<caret>
|
||||
""".trimIndent()
|
||||
val imported = listOf("lyng.io.process")
|
||||
ensureDocs(imported)
|
||||
|
||||
val items = complete(code)
|
||||
assertTrue("Should contain 'execute'", items.contains("execute"))
|
||||
assertTrue("Should contain 'shell'", items.contains("shell"))
|
||||
}
|
||||
|
||||
fun test_RunningProcess_Completion() {
|
||||
val code = """
|
||||
import lyng.io.process
|
||||
val p = Process.shell("ls")
|
||||
p.<caret>
|
||||
""".trimIndent()
|
||||
val imported = listOf("lyng.io.process")
|
||||
ensureDocs(imported)
|
||||
|
||||
val items = complete(code)
|
||||
assertTrue("Should contain 'stdout'", items.contains("stdout"))
|
||||
assertTrue("Should contain 'waitFor'", items.contains("waitFor"))
|
||||
assertTrue("Should contain 'signal'", items.contains("signal"))
|
||||
}
|
||||
|
||||
fun test_RegistryDirect() {
|
||||
DocsBootstrap.ensure()
|
||||
val docs = BuiltinDocRegistry.docsForModule("lyng.io.process")
|
||||
assertTrue("Docs for lyng.io.process should not be empty", docs.isNotEmpty())
|
||||
val processClass = docs.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == "Process" }
|
||||
assertNotNull("Should contain Process class", processClass)
|
||||
assertTrue("Process should have members", processClass!!.members.isNotEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,6 +32,7 @@ group = "net.sergeych"
|
||||
version = "0.0.1-SNAPSHOT"
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
jvm()
|
||||
androidTarget {
|
||||
publishLibraryVariants("release")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -46,6 +46,7 @@ buildkonfig {
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
jvm()
|
||||
androidTarget {
|
||||
publishLibraryVariants("release")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>) {
|
||||
}
|
||||
}
|
||||
|
||||
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<Token>) {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
'*' -> {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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<String, MutableList<MiniMemberDecl>>) {
|
||||
fun emitGroup(map: LinkedHashMap<String, MutableList<MiniMemberDecl>>, 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<MiniMemberValDecl>()
|
||||
.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)
|
||||
|
||||
@ -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<MiniMemberDecl> = 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<String>,
|
||||
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
|
||||
|
||||
@ -106,6 +106,34 @@ open class ObjClass(
|
||||
val classId: Long = ClassIdGen.nextId()
|
||||
var layoutVersion: Int = 0
|
||||
|
||||
private val mangledNameCache = mutableMapOf<String, String>()
|
||||
fun mangledName(name: String): String = mangledNameCache.getOrPut(name) { "$className::$name" }
|
||||
|
||||
/**
|
||||
* Map of public member names to their effective storage keys in instanceScope.objects.
|
||||
* This is pre-calculated to avoid MRO traversal and string concatenation during common access.
|
||||
*/
|
||||
val publicMemberResolution: Map<String, String> by lazy {
|
||||
val res = mutableMapOf<String, String>()
|
||||
// Traverse MRO in REVERSED order so that child classes override parent classes in the map.
|
||||
for (cls in mro.reversed()) {
|
||||
if (cls.className == "Obj") continue
|
||||
for ((name, rec) in cls.members) {
|
||||
if (rec.visibility == Visibility.Public) {
|
||||
val key = if (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Delegated) cls.mangledName(name) else name
|
||||
res[name] = key
|
||||
}
|
||||
}
|
||||
cls.classScope?.objects?.forEach { (name, rec) ->
|
||||
if (rec.visibility == Visibility.Public && (rec.value is Statement || rec.type == ObjRecord.Type.Delegated)) {
|
||||
val key = if (rec.type == ObjRecord.Type.Delegated) cls.mangledName(name) else name
|
||||
res[name] = key
|
||||
}
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
val classNameObj by lazy { ObjString(className) }
|
||||
|
||||
var constructorMeta: ArgsDeclaration? = null
|
||||
@ -242,6 +270,43 @@ open class ObjClass(
|
||||
return instance
|
||||
}
|
||||
|
||||
/** Pre-calculated template for instanceScope.objects. */
|
||||
private val instanceObjectsTemplate: Map<String, ObjRecord> by lazy {
|
||||
val res = mutableMapOf<String, ObjRecord>()
|
||||
for (cls in mro) {
|
||||
// 1) members-defined methods and fields
|
||||
for ((k, v) in cls.members) {
|
||||
if (!v.isAbstract && (v.value is Statement || v.type == ObjRecord.Type.Delegated || v.type == ObjRecord.Type.Field)) {
|
||||
val key = if (v.visibility == Visibility.Private || v.type == ObjRecord.Type.Field || v.type == ObjRecord.Type.Delegated) cls.mangledName(k) else k
|
||||
if (!res.containsKey(key)) {
|
||||
res[key] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
// 2) class-scope members registered during class-body execution
|
||||
cls.classScope?.objects?.forEach { (k, rec) ->
|
||||
// ONLY copy methods and delegated members from class scope to instance scope.
|
||||
// Fields in class scope are static fields and must NOT be per-instance.
|
||||
if (!rec.isAbstract && (rec.value is Statement || rec.type == ObjRecord.Type.Delegated)) {
|
||||
val key = if (rec.visibility == Visibility.Private || rec.type == ObjRecord.Type.Delegated) cls.mangledName(k) else k
|
||||
// if not already present, copy reference for dispatch
|
||||
if (!res.containsKey(key)) {
|
||||
res[key] = rec
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
private val templateMethods: Map<String, ObjRecord> by lazy {
|
||||
instanceObjectsTemplate.filter { it.value.type == ObjRecord.Type.Fun }
|
||||
}
|
||||
|
||||
private val templateOthers: List<Pair<String, ObjRecord>> by lazy {
|
||||
instanceObjectsTemplate.filter { it.value.type != ObjRecord.Type.Fun }.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance of this class and initialize its [ObjInstance.instanceScope] with
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 0. Prefer private member of current class context
|
||||
scope.currentClassCtx?.let { caller ->
|
||||
val mangled = "${caller.className}::$name"
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
caller.members[name]?.let { rec ->
|
||||
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, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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,7 +587,14 @@ class FieldRef(
|
||||
val base = target.evalValue(scope)
|
||||
if (base == ObjNull && isOptional) return ObjNull.asMutable
|
||||
if (fieldPic) {
|
||||
val (key, ver) = 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 }
|
||||
}
|
||||
if (key != 0L) {
|
||||
rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) {
|
||||
if (picCounters) PerfStats.fieldPicHit++
|
||||
noteReadHit()
|
||||
@ -615,6 +685,21 @@ class FieldRef(
|
||||
rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc -> obj.readField(sc, name) }
|
||||
}
|
||||
}
|
||||
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) }
|
||||
@ -622,6 +707,7 @@ class FieldRef(
|
||||
}
|
||||
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,9 +741,7 @@ class FieldRef(
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fieldPic) {
|
||||
val (key, ver) = receiverKeyAndVersion(base)
|
||||
if (key != 0L) {
|
||||
wSetter1?.let { s -> if (key == wKey1 && ver == wVer1) {
|
||||
if (picCounters) PerfStats.fieldPicSetHit++
|
||||
noteWriteHit()
|
||||
@ -713,6 +803,22 @@ class FieldRef(
|
||||
wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, v -> obj.writeField(sc, name, v) }
|
||||
}
|
||||
}
|
||||
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) }
|
||||
@ -720,6 +826,7 @@ class FieldRef(
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
base.writeField(scope, name, newValue)
|
||||
}
|
||||
|
||||
@ -736,7 +843,14 @@ class FieldRef(
|
||||
val base = target.evalValue(scope)
|
||||
if (base == ObjNull && isOptional) return ObjNull
|
||||
if (fieldPic) {
|
||||
val (key, ver) = 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 }
|
||||
}
|
||||
if (key != 0L) {
|
||||
rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) {
|
||||
if (picCounters) PerfStats.fieldPicHit++
|
||||
return g(base, scope).value
|
||||
@ -768,20 +882,10 @@ class FieldRef(
|
||||
if (picCounters) PerfStats.fieldPicMiss++
|
||||
val rec = base.readField(scope, name)
|
||||
// install primary generic getter for this shape
|
||||
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
|
||||
}
|
||||
}
|
||||
rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc -> obj.readField(sc, name) }
|
||||
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,19 +1242,54 @@ class MethodCallRef(
|
||||
val base = receiver.evalValue(scope)
|
||||
if (base == ObjNull && isOptional) return ObjNull.asReadonly
|
||||
val callArgs = args.toArguments(scope, tailBlock)
|
||||
return performInvoke(scope, base, callArgs, methodPic, picCounters).asReadonly
|
||||
}
|
||||
|
||||
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, ver) = receiverKeyAndVersion(base)
|
||||
mInvoker1?.let { inv -> if (key == mKey1 && ver == mVer1) {
|
||||
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).asReadonly
|
||||
} }
|
||||
mInvoker2?.let { inv -> if (key == mKey2 && ver == mVer2) {
|
||||
return inv(base, scope, callArgs)
|
||||
}
|
||||
}
|
||||
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) {
|
||||
// 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
|
||||
@ -1150,9 +1297,11 @@ class MethodCallRef(
|
||||
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) {
|
||||
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
|
||||
@ -1161,8 +1310,9 @@ class MethodCallRef(
|
||||
mKey3 = mKey2; mVer3 = mVer2; mInvoker3 = mInvoker2
|
||||
mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1
|
||||
mKey1 = tK; mVer1 = tV; mInvoker1 = tI
|
||||
return inv(base, scope, callArgs).asReadonly
|
||||
} }
|
||||
return inv(base, scope, callArgs)
|
||||
}
|
||||
}
|
||||
// Slow path
|
||||
if (picCounters) PerfStats.methodPicMiss++
|
||||
noteMethodMiss()
|
||||
@ -1187,6 +1337,16 @@ class MethodCallRef(
|
||||
// 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)
|
||||
@ -1195,13 +1355,15 @@ class MethodCallRef(
|
||||
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, hierarchyMember.declaringClass ?: inst.objClass, sc.currentClassCtx))
|
||||
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)
|
||||
}
|
||||
@ -1224,10 +1386,10 @@ class MethodCallRef(
|
||||
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> obj.invokeInstanceMethod(sc, name, a) }
|
||||
}
|
||||
}
|
||||
return result.asReadonly
|
||||
return result
|
||||
}
|
||||
val result = base.invokeInstanceMethod(scope, name, callArgs)
|
||||
return result.asReadonly
|
||||
}
|
||||
return base.invokeInstanceMethod(scope, name, callArgs)
|
||||
}
|
||||
|
||||
private fun receiverKeyAndVersion(obj: Obj): Pair<Long, Int> = when (obj) {
|
||||
@ -1587,14 +1749,17 @@ class ListLiteralRef(private val entries: List<ListEntry>) : 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<Obj>(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<ListEntry>) : 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<MapLiteralEntry>) : 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<MapLiteralEntry>) : 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 = """
|
||||
|
||||
@ -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<caret>
|
||||
}
|
||||
""".trimIndent()
|
||||
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||
val ns = names(items)
|
||||
assertTrue(ns.contains("myArg1"), "Function argument 'myArg1' should be proposed, but got: $ns")
|
||||
assertTrue(ns.contains("myArg2"), "Function argument 'myArg2' should be proposed, but got: $ns")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun methodArgumentsInBody() = runBlocking {
|
||||
val code = """
|
||||
class MyClass {
|
||||
fun test(myArg1, myArg2) {
|
||||
myA<caret>
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val items = CompletionEngineLight.completeAtMarkerSuspend(code)
|
||||
val ns = names(items)
|
||||
assertTrue(ns.contains("myArg1"), "Method argument 'myArg1' should be proposed, but got: $ns")
|
||||
assertTrue(ns.contains("myArg2"), "Method argument 'myArg2' should be proposed, but got: $ns")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nestedShadowingCompletion() = runBlocking {
|
||||
val code = """
|
||||
|
||||
@ -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
|
||||
|
||||
@ -329,7 +329,7 @@
|
||||
<!-- Top-left version ribbon -->
|
||||
<div class="corner-ribbon bg-danger text-white">
|
||||
<span style="margin-left: -5em">
|
||||
v1.1.1-SNAPSHOT
|
||||
v1.2.0-SNAPSHOT
|
||||
</span>
|
||||
</div>
|
||||
<!-- Fixed top navbar for the whole site -->
|
||||
|
||||
@ -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,6 +41,11 @@ 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 key = ch.toString()
|
||||
val ev = js("new KeyboardEvent('keydown', {key: key, bubbles: true, cancelable: true})")
|
||||
val wasPrevented = !ta.dispatchEvent(ev.unsafeCast<org.w3c.dom.events.Event>())
|
||||
|
||||
if (!wasPrevented) {
|
||||
val start = ta.selectionStart ?: 0
|
||||
val end = ta.selectionEnd ?: start
|
||||
val before = ta.value.substring(0, start)
|
||||
@ -50,8 +55,9 @@ class EditorE2ETest {
|
||||
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<org.w3c.dom.events.Event>())
|
||||
val inputEv = js("new Event('input', {bubbles:true})")
|
||||
ta.dispatchEvent(inputEv.unsafeCast<org.w3c.dom.events.Event>())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -243,10 +249,10 @@ class EditorE2ETest {
|
||||
typeText(ta, "4"); nextFrame()
|
||||
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 = (
|
||||
"""
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user