diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b56322..b3835db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,156 +1,88 @@ -## 1.5.0-SNAPSHOT +# Changelog -### Language Features -- Added `return` statement with local and non-local exit support (`return@label`). -- Support for `abstract` classes, methods, and variables. -- Introduced `interface` as a synonym for `abstract class`. -- Multiple Inheritance (MI) completed and enabled by default (C3 MRO). -- Class properties with custom accessors (`get`, `set`). -- Restricted setter visibility (`private set`, `protected set`). -- Late-initialized `val` fields in classes with `Unset` protection. -- Named arguments (`name: value`) and named splats (`...Map`). -- Assign-if-null operator `?=`. -- Refined `protected` visibility rules and `closed` modifier. -- Transient attribute `@Transient` for serialization and equality. -- Unified Delegation model for `val`, `var`, and `fun`. -- Singleton objects (`object`) and object expressions. +This file tracks user-visible Lyng language/runtime/tooling changes. -### Standard Library -- Added `with(self, block)` for scoped execution. -- Added `clamp()` function and extension. -- Improved `Exception` and `StackTraceEntry` reporting. +History note: +- The project had periods where changelog maintenance lagged behind commits. +- Entries below are synchronized and curated for `1.5.x`. +- Earlier history may be incomplete and should be cross-checked with git tags/commits when needed. -### Tooling and IDE -- **CLI**: Added `fmt` as a first-class subcommand for code formatting. -- **IDEA Plugin**: Lightweight autocompletion (experimental), improved docs, and Grazie integration. -- **Highlighters**: Updated TextMate bundle and website highlighters for new syntax. +## 1.5.1 (2026-03-25) -### Detailed Changes: +### Language +- Added string interpolation: + - `"$name"` identifier interpolation. + - `"${expr}"` expression interpolation. +- Added literal-dollar forms in strings: + - `"\$"` -> `$` + - `"$$"` -> `$` + - `\\$x` is parsed as backslash + interpolation of `x`. +- Added per-file interpolation opt-out via leading directive comment: + - `// feature: interpolation: off` -- Language: Refined `protected` visibility rules - - Ancestor classes can now access `protected` members of their descendants, provided the ancestor also defines or inherits a member with the same name (indicating an override of a member known to the ancestor). - - This allows patterns where a base class calls a `protected` method that is implemented in a subclass. - - Fixed a regression where self-calls to unmangled members sometimes bypassed visibility checks incorrectly; these are now handled by the refined logic. +### Docs and AI references +- Updated compiler-accurate AI language docs: + - interpolation syntax and escaping + - per-file feature switch behavior +- Refreshed tutorial examples and doctests to reflect new interpolation semantics. +- Added/reworked current proposal/reference materials for Lyng common-platform guidance. -- Language: Added `return` statement - - `return [expression]` exits the innermost enclosing callable (function or lambda). - - Supports non-local returns using `@label` syntax (e.g., `return@outer 42`). - - Named functions automatically provide their name as a label for non-local returns. - - Labeled lambdas: lambdas can be explicitly labeled using `@label { ... }`. - - Restriction: `return` is forbidden in shorthand function definitions (e.g., `fun f(x) = return x` is a syntax error). - - Control Flow: `return` and `break` are now protected from being caught by user-defined `try-catch` blocks in Lyng. - - Documentation: New `docs/return_statement.md` and updated `tutorial.md`. +### Compatibility notes +- Interpolation is enabled by default for normal string literals. +- Existing code that intentionally used `$name` as literal text should use `\$name`, `$$name`, or the file directive `// feature: interpolation: off`. -- Language: stdlib improvements - - Added `with(self, block)` function to `root.lyng` which executes a block with `this` set to the provided object. -- Language: Abstract Classes and Interfaces - - Support for `abstract` modifier on classes, methods, and variables. - - Introduced `interface` as a synonym for `abstract class`, supporting full state (constructors, fields, `init` blocks) and implementation by parts via MI. - - New `closed` modifier (antonym to `open`) to prevent overriding class members. - - Refined `override` logic: mandatory keyword when re-declaring members that exist in the ancestor chain (MRO). - - MI Satisfaction: Abstract requirements are automatically satisfied by matching concrete members found later in the C3 MRO chain without requiring explicit proxy methods. - - Integration: Updated highlighters (lynglib, lyngweb, IDEA plugin), IDEA completion, and Grazie grammar checking. - - Documentation: Updated `docs/OOP.md` with sections on "Abstract Classes and Members", "Interfaces", and "Overriding and Virtual Dispatch". -- IDEA plugin: Improved natural language support and spellchecking - - Disabled the limited built-in English and Technical dictionaries. - - Enforced usage of the platform's standard Natural Languages (Grazie) and Spellchecker components. - - Integrated `SpellCheckerManager` for word suggestions and validation, respecting users' personal and project dictionaries. - - Added project-specific "learned words" support via `Lyng Formatter` settings and quick-fixes. - - Enhanced fallback spellchecker for technical terms and Lyng-specific vocabulary. +## 1.5.0 (2026-03-22) -- Language: Class properties with accessors - - Support for `val` (read-only) and `var` (read-write) properties in classes. - - Syntax: `val name [ : Type ] get() { body }` or `var name [ : Type ] get() { body } set(value) { body }`. - - Laconic Expression Shorthand: `val prop get() = expression` and `var prop get() = read set(v) = write`. - - Properties are pure accessors and do **not** have automatic backing fields. - - Validation: `var` properties must have both accessors; `val` must have only a getter. - - Integration: Updated TextMate grammar and IntelliJ plugin (highlighting + keywords). - - Documentation: New "Properties" section in `docs/OOP.md`. +### Major runtime/compiler direction +- Completed migration to bytecode-first/bytecode-only execution paths. +- Removed interpreter fallback behavior in core execution hot paths. +- Continued frame-slot-first local/capture model improvements and related diagnostics. -- Language: Restricted Setter Visibility - - Support for `private set` and `protected set` modifiers on `var` fields and properties. - - Allows members to be publicly readable but only writable from within the declaring class or its subclasses. - - Enforcement at runtime: throws `AccessException` on unauthorized writes. - - Supported only for declarations in class bodies (fields and properties). - - Documentation: New "Restricted Setter Visibility" section in `docs/OOP.md`. +### Language features and semantics +- Added/finished `return` semantics including labeled non-local forms (`return@label`). +- Added abstract classes/members and `interface` support (as abstract-class-style construct). +- Completed and enabled multiple inheritance with C3 MRO by default. +- Added class properties with accessors (`get`/`set`) and restricted setter visibility (`private set`, `protected set`). +- Added late-initialized class `val` support with `Unset` protection rules. +- Added named arguments (`name: value`) and named splats (`...map`) with stricter validation. +- Added assign-if-null operator `?=`. +- Improved nullable/type-checking behavior (including `T is nullable` and related type checks). +- Added variadic function types (`...` in function type declarations) and tighter lambda type checks. -- Language: Late-initialized `val` fields in classes - - Support for declaring `val` without an immediate initializer in class bodies. - - Compulsory initialization: every late-init `val` must be assigned at least once within the class body or an `init` block. - - Write-once enforcement: assigning to a `val` is allowed only if its current value is `Unset`. - - Access protection: reading a late-init `val` before it is assigned returns the `Unset` singleton; using `Unset` for most operations throws an `UnsetException`. - - Extension properties do not support late-init. - - Documentation: New "Late-initialized `val` fields" and "The `Unset` singleton" sections in `docs/OOP.md`. +### Type system and collections +- Added immutable collections hierarchy (`ImmutableList`, `ImmutableSet`, `ImmutableMap`). +- Improved generic runtime binding/checking for explicit type arguments and bounds. +- Added smarter type-aware collection ops (`+=`, `-=`) and stronger declared-member type checks. -- Docs: OOP improvements - - New page: `docs/scopes_and_closures.md` detailing `ClosureScope` resolution order, recursion‑safe helpers (`chainLookupIgnoreClosure`, `chainLookupWithMembers`, `baseGetIgnoreClosure`), cycle prevention, and capturing lexical environments for callbacks (`snapshotForClosure`). - - Updated: `docs/advanced_topics.md` (link to the new page), `docs/parallelism.md` (closures in `launch`/`flow`), `docs/OOP.md` (visibility from closures with preserved `currentClassCtx`), `docs/exceptions_handling.md` (compatibility alias `SymbolNotFound`). - - Tutorial: added quick link to Scopes and Closures. +### Extern/Kotlin bridge +- Tightened extern declaration rules: + - explicit extern members are required for extern class/object declarations. +- Improved extern generic class behavior and diagnostics. +- Extended bridge APIs for binding global functions/variables and object/member interop scenarios. -- IDEA plugin: Lightweight autocompletion (experimental) - - Global completion: local declarations, in‑scope parameters, imported modules, and stdlib symbols. - - Member completion: after a dot, suggests only members of the inferred receiver type (incl. chained calls like `Path(".." ).lines().` → `Iterator` methods). No global identifiers appear after a dot. - - Inheritance-aware: direct class members first, then inherited (e.g., `List` includes `Collection`/`Iterable` methods). - - Heuristics: handles literals (`"…"` → `String`, numbers → `Int/Real`, `[...]` → `List`, `{...}` → `Dict`) and static `Namespace.` members. - - Performance: capped results, early prefix filtering, per‑document MiniAst cache, cancellation checks. - - Toggle: Settings | Lyng Formatter → "Enable Lyng autocompletion (experimental)" (default ON). - - Stabilization: DEBUG completion/Quick Doc logs are OFF by default; behavior aligned between IDE and isolated engine tests. +### Standard library and modules +- Added `lyng.observable` improvements (`ObservableList` hooks/events). +- Added `Random` stdlib API used by updated samples. +- Added/extended `lyngio.console` support and CLI integration for console interaction. +- Migrated time APIs to `kotlin.time` (`Instant` migration and related docs/tests). -- Language: Named arguments and named splats - - New call-site syntax for named arguments using colon: `name: value`. - - Positional arguments must come before named; positionals after a named argument inside parentheses are rejected. - - Trailing-lambda interaction: if the last parameter is already assigned by name (or via a named splat), a trailing `{ ... }` block is illegal. - - Named splats: `...` can now expand a Map into named arguments. - - Only string keys are allowed; non-string keys raise a clear error. - - Duplicate assignment across named args and named splats is an error. - - Ellipsis (variadic) parameters remain positional-only and cannot be named. - - Rationale: `=` is assignment and an expression in Lyng; `:` at call sites avoids ambiguity. Declarations keep `name: Type`; call-site casts continue to use `as` / `as?`. - - Documentation updated: proposals and declaring-arguments sections now cover named args/splats and error cases. - - Tests added covering success cases and errors for named args/splats and trailing-lambda interactions. +### CLI, IDE, and docs/tooling +- CLI: + - added first-class `fmt` command + - preserved direct script fast-path invocation + - improved command help/dispatch behavior +- IntelliJ plugin: + - improved lightweight completion and documentation/inspection behavior + - continued highlighter and Grazie/spellchecking integration work +- Docs: + - substantial updates across tutorial/OOP/type/runtime references + - expanded bytecode and advanced topics coverage -- Tooling: Highlighters and TextMate bundle updated for named args - - Website/editor highlighter (lyngweb + site) works with `name: value` and `...Map("k" => v)`; added JS tests covering punctuation/operator spans for `:` and `...`. - - TextMate grammar updated to recognize named call arguments: `name: value` after `(` or `,` with `name` highlighted as `variable.parameter.named.lyng` and `:` as punctuation; excludes `::`. - - TextMate bundle version bumped to 0.0.3; README updated with details and guidance. +## Migration checklist for 1.5.x -- Multiple Inheritance (MI) completed and enabled by default: - - Active C3 Method Resolution Order (MRO) for deterministic, monotonic lookup across complex hierarchies and diamonds. - - Qualified dispatch: - - `this@Type.member(...)` inside class bodies starts lookup at the specified ancestor. - - Cast-based disambiguation: `(expr as Type).member(...)`, `(expr as? Type)?.member(...)` (works with existing safe-call `?.`). - - Field inheritance (`val`/`var`) under MI: - - Instance storage is disambiguated per declaring class; unqualified read/write resolves to the first match in MRO. - - Qualified read/write targets the chosen ancestor’s storage. - - Constructors and initialization: - - Direct bases are initialized left-to-right; each ancestor is initialized at most once (diamond-safe de-duplication). - - Header-specified constructor arguments are passed to direct bases. - - Visibility enforcement under MI: - - `private` visible only inside the declaring class body. - - `protected` visible inside the declaring class and its transitive subclasses. Additionally, ancestor classes can access protected members of their descendants if it's an override of a member known to the ancestor. Unrelated contexts cannot access it (qualification/casts do not bypass). - - Diagnostics improvements: - - Missing member/field messages include receiver class and linearization order; hints for `this@Type` or casts when helpful. - - Invalid `this@Type` reports that the qualifier is not an ancestor and shows the receiver lineage. - - `as`/`as?` cast errors include actual and target type names. - -- Documentation updated (docs/OOP.md and tutorial quick-start) to reflect MI with active C3 MRO. - -- CLI: Added `fmt` as a first-class Clikt subcommand. - - Default behavior: formats files to stdout (no in-place edits by default). - - Options: - - `--check`: check only; print files that would change; exit with code 2 if any changes are needed. - - `-i, --in-place`: write formatted result back to files. - - `--spacing`: apply spacing normalization. - - `--wrap`, `--wrapping`: enable line wrapping. - - Mutually exclusive: `--check` and `--in-place` together now produce an error and exit with code 1. - - Multi-file stdout prints headers `--- ---` per file. - - `lyng --help` shows `fmt`; `lyng fmt --help` displays dedicated help. - - Fix: Property accessors (`get`, `set`, `private set`, `protected set`) are now correctly indented relative to the property declaration. - - Fix: Indentation now correctly carries over into blocks that start on extra‑indented lines (e.g., nested `if` statements or property accessor bodies). - - Fix: Formatting Markdown files no longer deletes content in `.lyng` code fences and works correctly with injected files (resolves clobbering, `StringIndexOutOfBoundsException`, and `nonempty text is not covered by block` errors). - -- CLI: Preserved legacy script invocation fast-paths: - - `lyng script.lyng [args...]` executes the script directly. - - `lyng -- -file.lyng [args...]` executes a script whose name begins with `-`. - -- CLI: Fixed a regression where the root help banner could print before subcommands. - - Root command no longer prints help when a subcommand (e.g., `fmt`) is invoked. +- If you rely on literal `$...` strings: + - replace with `\$...` or `$$...`, or + - add `// feature: interpolation: off` at file top. +- Review any code relying on interpreter-era fallback behavior; 1.5.x assumes bytecode-first execution. +- For extern declarations, ensure members are explicitly declared where required. +- For named arguments/splats, verify call sites follow stricter ordering/duplication rules. diff --git a/docs/ai_language_reference.md b/docs/ai_language_reference.md index 389b2df..1fcff8f 100644 --- a/docs/ai_language_reference.md +++ b/docs/ai_language_reference.md @@ -17,6 +17,14 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T - Supported escapes: `\n`, `\r`, `\t`, `\"`, `\\`, `\uXXXX` (4 hex digits). - Unicode escapes use exactly 4 hex digits (for example: `"\u0416"` -> `Ж`). - Unknown `\x` escapes in strings are preserved literally as two characters (`\` and `x`). + - String interpolation is supported: + - identifier form: `"$name"` + - expression form: `"${expr}"` + - escaped dollar: `"\$"` and `"$$"` both produce literal `$`. + - `\\$x` means backslash + interpolated `x`. + - Per-file opt-out is supported via leading comment directive: + - `// feature: interpolation: off` + - with this directive, `$...` stays literal text. - Numbers: `Int` (`123`, `1_000`), `Real` (`1.2`, `1e3`), hex (`0xFF`). - Char: `'a'`, escaped chars supported. - Supported escapes: `\n`, `\r`, `\t`, `\'`, `\\`, `\uXXXX` (4 hex digits). diff --git a/docs/tutorial.md b/docs/tutorial.md index 91d6825..4fd6ba8 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -1573,6 +1573,52 @@ same as: assert(s[1] == 'a') >>> void +### String interpolation + +Supported forms: + +- `$name` +- `${expr}` + +Literal dollar forms: + +- `\$` -> `$` +- `$$` -> `$` + +Example: + + val name = "Lyng" + assertEquals("hello, Lyng!", "hello, $name!") + assertEquals("sum=3", "sum=${1+2}") + assertEquals("\$name", "\$name") + assertEquals("\$name", "$$name") + assertEquals("\\Lyng", "\\$name") + >>> void + +Interpolation and `printf`-style formatting can be combined when needed: + + val method = "transfer" + val argc = 2 + val compact = "%s:%d"(method, argc) + assertEquals("call=transfer:2", "call=$compact") + assertEquals("[transfer:2] ok", "[${"%s:%d"(method, argc)}] ok") + >>> void + +Interpolation also works well with regex patterns. To keep a literal `$` in the +regex, escape it in the resulting pattern: + + val currency = "USD" + val amount = 15 + val escapedDollar = "\\$" + val re = Regex("^${currency}${escapedDollar}${amount}$") + assert("USD$15" =~ re) + assert("USD15" !~ re) + >>> void + +If you need old literal behavior in a file, add a leading directive comment: + + // feature: interpolation: off + ### Char literal escapes Are the same as in string literals with little difference: diff --git a/lynglib/build.gradle.kts b/lynglib/build.gradle.kts index 11a311d..4927be8 100644 --- a/lynglib/build.gradle.kts +++ b/lynglib/build.gradle.kts @@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.dsl.JvmTarget group = "net.sergeych" -version = "1.5.0" +version = "1.5.1" // Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt index 76744f5..371c834 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Parser.kt @@ -26,7 +26,10 @@ val idNextChars = { d: Char -> d.isLetter() || d == '_' || d.isDigit() || d == ' val idFirstChars = { d: Char -> d.isLetter() || d == '_' || d == '$' } fun parseLyng(source: Source): List { - val p = Parser(fromPos = source.startPos) + val p = Parser( + fromPos = source.startPos, + interpolationEnabled = detectInterpolationEnabled(source.text) + ) val tokens = mutableListOf() do { val t = p.nextToken() @@ -35,9 +38,70 @@ fun parseLyng(source: Source): List { return tokens } -private class Parser(fromPos: Pos) { +private fun parseLyng(source: Source, interpolationEnabled: Boolean): List { + val p = Parser( + fromPos = source.startPos, + interpolationEnabled = interpolationEnabled + ) + val tokens = mutableListOf() + do { + val t = p.nextToken() + tokens += t + } while (t.type != Token.Type.EOF) + return tokens +} + +private fun detectInterpolationEnabled(text: String): Boolean { + // Per-file feature switch in leading comments: + // // feature: interpolation: off + // // feature: interpolation: on + var enabled = true + val lines = text.split('\n') + var i = 0 + while (i < lines.size) { + val line = lines[i] + val trimmed = line.trim() + if (trimmed.isBlank()) { + i++ + continue + } + if (i == 0 && trimmed.startsWith("#!")) { + i++ + continue + } + if (trimmed.startsWith("//")) { + val m = Regex("^//\\s*feature\\s*:\\s*interpolation\\s*:\\s*(on|off)\\s*$", RegexOption.IGNORE_CASE) + .matchEntire(trimmed) + if (m != null) { + enabled = m.groupValues[1].equals("on", ignoreCase = true) + } + i++ + continue + } + if (trimmed.startsWith("/*")) { + // Skip leading block comment(s); directives are intentionally line-comment based. + var j = i + var closed = false + while (j < lines.size) { + if (lines[j].contains("*/")) { + closed = true + break + } + j++ + } + if (!closed) break + i = j + 1 + continue + } + break + } + return enabled +} + +private class Parser(fromPos: Pos, private val interpolationEnabled: Boolean = true) { private val pos = MutablePos(fromPos) + private val bufferedTokens = ArrayDeque() /** * Immutable copy of current position @@ -47,6 +111,7 @@ private class Parser(fromPos: Pos) { private fun raise(msg: String): Nothing = throw ScriptError(currentPos, msg) fun nextToken(): Token { + if (bufferedTokens.isNotEmpty()) return bufferedTokens.removeFirst() skipws() if (pos.end) return Token("", currentPos, Token.Type.EOF) val from = currentPos @@ -296,7 +361,7 @@ private class Parser(fromPos: Pos) { Token(":", from, Token.Type.COLON) } - '"' -> loadStringToken() + '"' -> loadStringTokens(from) in digitsSet -> { pos.back() @@ -485,11 +550,6 @@ private class Parser(fromPos: Pos) { private fun loadStringToken(): Token { val start = currentPos - -// if (currentChar == '"') pos.advance() -// else start = start.back() -// start = start.back() - val sb = StringBuilder() var newlineDetected = false while (currentChar != '"') { @@ -548,6 +608,262 @@ private class Parser(fromPos: Pos) { return Token(result, start, Token.Type.STRING) } + private sealed interface StringChunk { + data class Literal(val text: String, val pos: Pos) : StringChunk + data class Expr(val tokens: List, val pos: Pos) : StringChunk + } + + private fun loadStringTokens(startQuotePos: Pos): Token { + if (!interpolationEnabled) return loadStringToken() + val tokenPos = currentPos + + val chunks = mutableListOf() + val literal = StringBuilder() + var newlineDetected = false + var hasInterpolation = false + + fun flushLiteralChunk() { + if (literal.isNotEmpty()) { + chunks += StringChunk.Literal(literal.toString(), tokenPos) + literal.clear() + } + } + + while (currentChar != '"') { + if (pos.end) throw ScriptError(startQuotePos, "unterminated string started there") + when (currentChar) { + '\\' -> { + pos.advance() ?: raise("unterminated string") + when (currentChar) { + 'n' -> { + literal.append('\n'); pos.advance() + } + + 'r' -> { + literal.append('\r'); pos.advance() + } + + 't' -> { + literal.append('\t'); pos.advance() + } + + '"' -> { + literal.append('"'); pos.advance() + } + + '\\' -> { + literal.append('\\'); pos.advance() + } + + 'u' -> { + literal.append(loadUnicodeEscape(tokenPos)) + } + + '$' -> { + // Backslash-escaped dollar is always literal. + literal.append('$') + pos.advance() + } + + else -> { + literal.append('\\').append(currentChar) + pos.advance() + } + } + } + + '$' -> { + pos.advance() + when { + currentChar == '$' -> { + // $$ -> literal '$' + literal.append('$') + pos.advance() + } + + currentChar == '{' -> { + hasInterpolation = true + flushLiteralChunk() + val exprStart = pos.toPos() + pos.advance() // consume '{' + val exprText = readInterpolationExprText(startQuotePos) + val exprTokens = parseEmbeddedExpressionTokens(exprText, exprStart) + chunks += StringChunk.Expr(exprTokens, exprStart) + } + + idFirstChars(currentChar) -> { + hasInterpolation = true + flushLiteralChunk() + val idPos = pos.toPos() + val id = loadChars(idNextChars) + val idToken = Token(id, idPos, Token.Type.ID) + chunks += StringChunk.Expr(listOf(idToken), idPos) + } + + else -> { + // Bare '$' before non-interpolation text stays literal. + literal.append('$') + } + } + } + + '\n', '\r' -> { + newlineDetected = true + literal.append(currentChar) + pos.advance() + } + + else -> { + literal.append(currentChar) + pos.advance() + } + } + } + pos.advance() // closing quote + + if (!hasInterpolation) { + val result = literal.toString().let { if (newlineDetected) fixMultilineStringLiteral(it) else it } + return Token(result, tokenPos, Token.Type.STRING) + } + + flushLiteralChunk() + if (chunks.isEmpty()) { + return Token("", tokenPos, Token.Type.STRING) + } + val expanded = mutableListOf() + expanded += Token("(", tokenPos, Token.Type.LPAREN) + var emittedPieces = 0 + for (chunk in chunks) { + val pieceTokens = when (chunk) { + is StringChunk.Literal -> { + if (chunk.text.isEmpty()) emptyList() + else listOf(Token(chunk.text, chunk.pos, Token.Type.STRING)) + } + is StringChunk.Expr -> { + if (chunk.tokens.isEmpty()) throw ScriptError(chunk.pos, "empty interpolation expression") + if (chunk.tokens.size == 1) { + chunk.tokens + } else { + listOf(Token("(", chunk.pos, Token.Type.LPAREN)) + + chunk.tokens + + listOf(Token(")", chunk.pos, Token.Type.RPAREN)) + } + } + } + if (pieceTokens.isEmpty()) continue + if (emittedPieces > 0) expanded += Token("+", tokenPos, Token.Type.PLUS) + expanded += pieceTokens + emittedPieces++ + } + if (emittedPieces == 0) { + expanded += Token("", tokenPos, Token.Type.STRING) + } + expanded += Token(")", tokenPos, Token.Type.RPAREN) + + val first = expanded.first() + for (i in 1 until expanded.size) bufferedTokens.addLast(expanded[i]) + return first + } + + private fun parseEmbeddedExpressionTokens(text: String, exprPos: Pos): List { + val tokens = parseLyng(Source(exprPos.source.fileName, text), interpolationEnabled) + if (tokens.isEmpty()) return emptyList() + val withoutEof = if (tokens.last().type == Token.Type.EOF) tokens.dropLast(1) else tokens + return withoutEof + } + + private fun readInterpolationExprText(start: Pos): String { + val out = StringBuilder() + var depth = 1 + while (!pos.end) { + val ch = currentChar + if (ch == '"') { + appendQuoted(out, '"') + continue + } + if (ch == '\'') { + appendQuoted(out, '\'') + continue + } + if (ch == '/' && peekChar() == '/') { + out.append('/').append('/') + pos.advance() + pos.advance() + while (!pos.end && currentChar != '\n') { + out.append(currentChar) + pos.advance() + } + continue + } + if (ch == '/' && peekChar() == '*') { + out.append('/').append('*') + pos.advance() + pos.advance() + var closed = false + while (!pos.end) { + val c = currentChar + if (c == '*' && peekChar() == '/') { + out.append('*').append('/') + pos.advance() + pos.advance() + closed = true + break + } + out.append(c) + pos.advance() + } + if (!closed) throw ScriptError(start, "unterminated block comment in interpolation") + continue + } + if (ch == '{') { + depth++ + out.append(ch) + pos.advance() + continue + } + if (ch == '}') { + depth-- + if (depth == 0) { + pos.advance() // consume closing '}' + return out.toString() + } + out.append(ch) + pos.advance() + continue + } + out.append(ch) + pos.advance() + } + throw ScriptError(start, "unterminated interpolation expression") + } + + private fun appendQuoted(out: StringBuilder, quote: Char) { + out.append(quote) + pos.advance() + while (!pos.end) { + val c = currentChar + out.append(c) + pos.advance() + if (c == '\\') { + if (!pos.end) { + out.append(currentChar) + pos.advance() + } + continue + } + if (c == quote) return + } + } + + private fun peekChar(): Char { + if (pos.end) return 0.toChar() + val mark = pos.toPos() + pos.advance() + val result = currentChar + pos.resetTo(mark) + return result + } + private fun loadUnicodeEscape(start: Pos): Char { // Called when currentChar points to 'u' right after a backslash. if (currentChar != 'u') throw ScriptError(currentPos, "expected unicode escape marker: u") diff --git a/lynglib/src/commonTest/kotlin/ObjectExpressionTest.kt b/lynglib/src/commonTest/kotlin/ObjectExpressionTest.kt index bad5b70..48df570 100644 --- a/lynglib/src/commonTest/kotlin/ObjectExpressionTest.kt +++ b/lynglib/src/commonTest/kotlin/ObjectExpressionTest.kt @@ -114,7 +114,7 @@ class ObjectExpressionTest { eval(""" val x = object { } val name = ((x::class as Class).className as String) - assert(name.startsWith("${'$'}Anon_")) + assert(name.startsWith("\${'$'}Anon_")) """.trimIndent()) } } diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index 11c7799..21cc08d 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -4194,26 +4194,43 @@ class ScriptTest { // } -// @Ignore -// @Test -// fun interpolationTest() = runTest { -// eval($$$""" -// -// val foo = "bar" -// val buzz = ["foo", "bar"] -// -// // 1. simple interpolation -// assertEquals( "bar", "$foo" ) -// assertEquals( "bar", "${foo}" ) -// -// // 2. escaping the dollar sign -// assertEquals( "$", "\$foo"[0] ) -// assertEquals( "foo, "\$foo"[1..] ) -// -// // 3. interpolation with expression -// assertEquals( "foo!. bar?", "${buzz[0]+"!"}. ${buzz[1]+"?"}" ) -// """.trimIndent()) -// } + @Test + fun interpolationTest() = runTest { + val d = '$' + eval( + """ + val foo = "bar" + val buzz = ["foo", "bar"] + + // 1. simple interpolation + assertEquals("bar", "${d}foo") + assertEquals("bar", "${d}{foo}") + + // 2. escaping / literal dollar forms + assertEquals("${d}${d}foo", "\${d}foo") + assertEquals("${d}${d}foo", "${d}${d}foo") + assertEquals("\\bar", "\\${d}foo") + + // 3. interpolation with expression + assertEquals("foo!. bar?", "${d}{buzz[0] + "!"}. ${d}{buzz[1] + "?"}") + """.trimIndent() + ) + } + + @Test + fun interpolationCanBeDisabledByFeatureComment() = runTest { + val d = '$' + eval( + """ + // feature: interpolation: off + val name = "lyng" + assertEquals("hello, ${d}name!", "hello, ${d}name!") + assertEquals("${d}{name}", "${d}{name}") + assertEquals("${d}${d}name", "${d}${d}name") + assertEquals("\\${d}name", "\\${d}name") + """.trimIndent() + ) + } @Test fun testInlineArrayLiteral() = runTest { diff --git a/site/src/jsMain/resources/index.html b/site/src/jsMain/resources/index.html index 2bdee5e..b8ccce0 100644 --- a/site/src/jsMain/resources/index.html +++ b/site/src/jsMain/resources/index.html @@ -339,7 +339,7 @@
- v1.5.0 + v1.5.1
diff --git a/work_documents/lyng_as_common_platform.md b/work_documents/lyng_as_common_platform.md index 627de80..42ecf6e 100644 --- a/work_documents/lyng_as_common_platform.md +++ b/work_documents/lyng_as_common_platform.md @@ -58,8 +58,8 @@ class Ledger(storage) { fun transfer(from: Int, to: Int, amount: Int) { require(amount > 0) - val fromKey = "bal:%05d"(from) - val toKey = "bal:%05d"(to) + val fromKey = "bal:$from" + val toKey = "bal:$to" val fromBal = storage[fromKey] ?: 0 require(fromBal >= amount) @@ -105,9 +105,9 @@ This model is useful for distributed validation, scoring, and pre-consensus chec fun square(x: T) = x * x fun describeValue(x: T): String = when (T) { - Int -> "Int value=%s"(x) - Real -> "Real value=%s"(x) - else -> "Numeric value=%s"(x) + Int -> "Int value=$x" + Real -> "Real value=$x" + else -> "Numeric value=$x" } fun sameType(x: T, y: Object): Bool = y is T @@ -147,7 +147,7 @@ val owner by state var limit by state object RpcDelegate { - fun invoke(thisRef, name, args...) = "%s:%d"(name, args.size) + fun invoke(thisRef, name, args...) = "$name:${args.size}" } fun remoteCall by RpcDelegate