Add Lyng string interpolation with docs/tests and refresh changelog
This commit is contained in:
parent
3b83439788
commit
3d8bfe8863
214
CHANGELOG.md
214
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 `--- <path> ---` 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.
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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<Token> {
|
||||
val p = Parser(fromPos = source.startPos)
|
||||
val p = Parser(
|
||||
fromPos = source.startPos,
|
||||
interpolationEnabled = detectInterpolationEnabled(source.text)
|
||||
)
|
||||
val tokens = mutableListOf<Token>()
|
||||
do {
|
||||
val t = p.nextToken()
|
||||
@ -35,9 +38,70 @@ fun parseLyng(source: Source): List<Token> {
|
||||
return tokens
|
||||
}
|
||||
|
||||
private class Parser(fromPos: Pos) {
|
||||
private fun parseLyng(source: Source, interpolationEnabled: Boolean): List<Token> {
|
||||
val p = Parser(
|
||||
fromPos = source.startPos,
|
||||
interpolationEnabled = interpolationEnabled
|
||||
)
|
||||
val tokens = mutableListOf<Token>()
|
||||
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<Token>()
|
||||
|
||||
/**
|
||||
* 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<Token>, val pos: Pos) : StringChunk
|
||||
}
|
||||
|
||||
private fun loadStringTokens(startQuotePos: Pos): Token {
|
||||
if (!interpolationEnabled) return loadStringToken()
|
||||
val tokenPos = currentPos
|
||||
|
||||
val chunks = mutableListOf<StringChunk>()
|
||||
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<Token>()
|
||||
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<Token> {
|
||||
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")
|
||||
|
||||
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -339,7 +339,7 @@
|
||||
<!-- Top-left version ribbon -->
|
||||
<div class="corner-ribbon bg-danger text-white">
|
||||
<span style="margin-left: -5em">
|
||||
v1.5.0
|
||||
v1.5.1
|
||||
</span>
|
||||
</div>
|
||||
<!-- Fixed top navbar for the whole site -->
|
||||
|
||||
@ -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<T: Int | Real>(x: T) = x * x
|
||||
|
||||
fun describeValue<T: Int | Real>(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<T>(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
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user