Add Lyng string interpolation with docs/tests and refresh changelog

This commit is contained in:
Sergey Chernov 2026-03-25 18:42:36 +03:00
parent 3b83439788
commit 3d8bfe8863
9 changed files with 497 additions and 178 deletions

View File

@ -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.

View File

@ -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).

View File

@ -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:

View File

@ -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

View File

@ -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")

View File

@ -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())
}
}

View File

@ -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 {

View File

@ -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 -->

View File

@ -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