diff --git a/LYNG_AI_SPEC.md b/LYNG_AI_SPEC.md index 9fb5614..99954a8 100644 --- a/LYNG_AI_SPEC.md +++ b/LYNG_AI_SPEC.md @@ -1,9 +1,17 @@ -# Lyng Language AI Specification (V1.2) +# Lyng Language AI Specification (V1.3) High-density specification for LLMs. Reference this for all Lyng code generation. ## 1. Core Philosophy & Syntax -- **Everything is an Expression**: Blocks, `if`, `when`, `for`, `while` return their last expression (or `void`). +- **Everything is an Expression**: Blocks, `if`, `when`, `for`, `while`, `do-while` return their last expression (or `void`). +- **Loops with `else`**: `for`, `while`, and `do-while` support an optional `else` block. + - `else` executes **only if** the loop finishes normally (without a `break`). + - `break ` exits the loop and sets its return value. + - Loop Return Value: + 1. Value from `break `. + 2. Result of `else` block (if loop finished normally and `else` exists). + 3. Result of the last iteration (if loop finished normally and no `else`). + 4. `void` (if loop body never executed and no `else`). - **Implicit Coroutines**: All functions are coroutines. No `async/await`. Use `launch { ... }` (returns `Deferred`) or `flow { ... }`. - **Variables**: `val` (read-only), `var` (mutable). Supports late-init `val` in classes (must be assigned in `init` or body). - **Null Safety**: `?` (nullable type), `?.` (safe access), `?( )` (safe invoke), `?{ }` (safe block invoke), `?[ ]` (safe index), `?:` or `??` (elvis), `?=` (assign-if-null). @@ -87,3 +95,11 @@ val [first, middle..., last] = [1, 2, 3, 4, 5] // Safe Navigation and Elvis val companyName = person?.job?.company?.name ?? "Freelancer" ``` + +## 8. Standard Library Discovery +To collect data on the standard library and available APIs, AI should inspect: +- **Global Symbols**: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt` (root functions like `println`, `sqrt`, `assert`). +- **Core Type Members**: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/*.kt` (e.g., `ObjList.kt`, `ObjString.kt`, `ObjMap.kt`) for methods on built-in types. +- **Lyng-side Extensions**: `lynglib/stdlib/lyng/root.lyng` for high-level functional APIs (e.g., `map`, `filter`, `any`, `lazy`). +- **I/O & Processes**: `lyngio/src/commonMain/kotlin/net/sergeych/lyng/io/` for `fs` and `process` modules. +- **Documentation**: `docs/*.md` (e.g., `tutorial.md`, `lyngio.md`) for high-level usage and module overviews. diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/annotators/LyngExternalAnnotator.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/annotators/LyngExternalAnnotator.kt index 8eb98aa..9c5681c 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/annotators/LyngExternalAnnotator.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/annotators/LyngExternalAnnotator.kt @@ -62,6 +62,7 @@ class LyngExternalAnnotator : ExternalAnnotator role map for top-level vals/vars and parameters val nameRole = HashMap(8) mini.declarations.forEach { d -> @@ -280,7 +279,6 @@ class LyngExternalAnnotator : ExternalAnnotator if (s.kind == HighlightKind.Label) { val start = s.range.start @@ -322,7 +320,6 @@ class LyngExternalAnnotator : ExternalAnnotator if (s.kind == HighlightKind.EnumConstant) { val start = s.range.start @@ -334,24 +331,10 @@ class LyngExternalAnnotator : ExternalAnnotator() - try { - val binding = Binder.bind(text, mini) - binding.symbols.forEach { sym -> - val s = sym.declStart - val e = sym.declEnd - if (s in 0..e && e <= text.length && s < e) idRanges += (s until e) - } - binding.references.forEach { ref -> - val s = ref.start - val e = ref.end - if (s in 0..e && e <= text.length && s < e) idRanges += (s until e) - } - } catch (_: Throwable) { - // Best-effort; no identifiers if binder fails - } - val tokens = try { SimpleLyngHighlighter().highlight(text) } catch (_: Throwable) { emptyList() } + // Build spell index payload: identifiers + comments/strings from simple highlighter. + // We use the highlighter as the source of truth for all "words" to check, including + // identifiers that might not be bound by the Binder. + val idRanges = tokens.filter { it.kind == HighlightKind.Identifier }.map { it.range.start until it.range.endExclusive } val commentRanges = tokens.filter { it.kind == HighlightKind.Comment }.map { it.range.start until it.range.endExclusive } val stringRanges = tokens.filter { it.kind == HighlightKind.String }.map { it.range.start until it.range.endExclusive } diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/grazie/LyngGrazieAnnotator.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/grazie/LyngGrazieAnnotator.kt index 8bc8ab6..993a1cc 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/grazie/LyngGrazieAnnotator.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/grazie/LyngGrazieAnnotator.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com + * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -150,16 +150,18 @@ class LyngGrazieAnnotator : ExternalAnnotator"}, totalFindings=$totalReturned, painting=${findings.size}") - // IMPORTANT: Do NOT fallback to the tiny bundled vocabulary on modern IDEs. - // If Grazie/Natural Languages processing returned nothing, we simply exit here - // to avoid low‑quality results from the legacy dictionary. - if (findings.isEmpty()) return - for (f in findings) { val ab = holder.newAnnotation(HighlightSeverity.INFORMATION, f.message).range(f.range) applyTypoStyleIfRequested(file, ab) ab.create() } + + // SUPPLEMENT: Always run the fallback spellchecker to ensure spelling errors are not ignored. + // It will avoid duplicating findings already reported by Grazie. + val painted = fallbackWithLegacySpellcheckerIfAvailable(file, fragments, holder, findings) + if (painted > 0) { + log.info("LyngGrazieAnnotator.apply: supplemented with $painted typos from legacy engine") + } } private fun scheduleOneShotRestart(file: PsiFile, modStamp: Long) { @@ -381,7 +383,8 @@ class LyngGrazieAnnotator : ExternalAnnotator>, - holder: AnnotationHolder + holder: AnnotationHolder, + existingFindings: List ): Int { return try { val mgrCls = Class.forName("com.intellij.spellchecker.SpellCheckerManager") @@ -389,12 +392,12 @@ class LyngGrazieAnnotator : ExternalAnnotator>, - holder: AnnotationHolder + holder: AnnotationHolder, + existingFindings: List ): Int { var painted = 0 val docText = file.viewProvider.document?.text @@ -461,6 +470,14 @@ class LyngGrazieAnnotator : ExternalAnnotator { if (element is com.intellij.psi.PsiFile) return EMPTY_TOKENIZER - val hasGrazie = grazieInstalled() - val hasGrazieApi = grazieApiAvailable() val settings = LyngFormatterSettings.getInstance(element.project) - if (!loggedOnce) { - loggedOnce = true - log.info("LyngSpellcheckingStrategy activated: hasGrazie=$hasGrazie, grazieApi=$hasGrazieApi, preferGrazieForCommentsAndLiterals=${settings.preferGrazieForCommentsAndLiterals}, spellCheckStringLiterals=${settings.spellCheckStringLiterals}, grazieChecksIdentifiers=${settings.grazieChecksIdentifiers}") - } - - val file = element.containingFile ?: return EMPTY_TOKENIZER 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 + if (et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.IDENTIFIER || et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.LABEL) { + return IDENTIFIER_TOKENIZER + } + if (et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.LINE_COMMENT || et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.BLOCK_COMMENT) { + return COMMENT_TEXT_TOKENIZER + } + if (et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.STRING && settings.spellCheckStringLiterals) { + return STRING_WITH_PRINTF_EXCLUDES } - - val elRange = element.textRange ?: return EMPTY_TOKENIZER - fun overlaps(list: List) = list.any { it.intersects(elRange) } - - // Identifiers: only if range is within identifiers index and not delegated to Grazie - if (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 ((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 (et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.STRING && settings.spellCheckStringLiterals && overlaps(index.strings) && !preferGrazie) return STRING_WITH_PRINTF_EXCLUDES return EMPTY_TOKENIZER } diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index 81ff026..5728223 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -4796,5 +4796,20 @@ class ScriptTest { assertEquals("r:r", T().l("r")) """.trimIndent()) } + + @Test + fun testTypedArgsWithInitializers() = runTest { + eval(""" + fun f(a: String = "foo") = a + "!" + fun g(a: String? = null) = a ?: "!!" + assertEquals(f(), "foo!") + assertEquals(f(), "!!") + assertEquals(f("bar"), "bar!") + class T(b: Int=42,c: String?=null) + assertEquals(42, T().b) + assertEquals(null, T().c) + """.trimIndent()) + } + }