Compare commits

...
9 Commits
22 changed files with 1508 additions and 840 deletions

No files matched your search

+8
View File
@@ -5,6 +5,14 @@
- For generics-heavy code generation, follow `docs/ai_language_reference.md` section `7.1 Generics Runtime Model and Bounds` and `7.2 Differences vs Java / Kotlin / Scala`. - For generics-heavy code generation, follow `docs/ai_language_reference.md` section `7.1 Generics Runtime Model and Bounds` and `7.2 Differences vs Java / Kotlin / Scala`.
- Use `docs/ai_stdlib_reference.md` for default runtime/module APIs and stdlib surface. - Use `docs/ai_stdlib_reference.md` for default runtime/module APIs and stdlib surface.
- Treat `LYNG_AI_SPEC.md` and older docs as secondary if they conflict with the two files above. - Treat `LYNG_AI_SPEC.md` and older docs as secondary if they conflict with the two files above.
- Prefer the shortest clear loop: use `for` for straightforward iteration/ranges; use `while` only when loop state/condition is irregular or changes in ways `for` cannot express cleanly.
- In Lyng code, slice strings with range indexing (`text[a..<b]`, `text[..<n]`, `text[n..]`) and avoid Java/Kotlin-style `substring(...)`.
## Lyng-First API Declarations
- Use `.lyng` declarations as the single source of truth for Lyng-facing API docs and types (especially module extern declarations).
- Prefer defining Lyng entities (enums/classes/type shapes) in `.lyng` files; only define them in Kotlin when there is Kotlin/platform-specific implementation detail that cannot be expressed in Lyng.
- Avoid hardcoding Lyng API documentation in Kotlin registrars when it can be declared in `.lyng`; Kotlin-side docs should be fallback/bridge only.
- For mixed pluggable modules (Lyng + Kotlin), embed module `.lyng` sources as generated Kotlin string literals, evaluate them into module scope during registration, then attach Kotlin implementations/bindings.
## Kotlin/Wasm generation guardrails ## Kotlin/Wasm generation guardrails
- Avoid creating suspend lambdas for compiler runtime statements. Prefer explicit `object : Statement()` with `override suspend fun execute(...)`. - Avoid creating suspend lambdas for compiler runtime statements. Prefer explicit `object : Statement()` with `override suspend fun execute(...)`.
+73 -141
View File
@@ -1,156 +1,88 @@
## 1.5.0-SNAPSHOT # Changelog
### Language Features This file tracks user-visible Lyng language/runtime/tooling changes.
- 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.
### Standard Library History note:
- Added `with(self, block)` for scoped execution. - The project had periods where changelog maintenance lagged behind commits.
- Added `clamp()` function and extension. - Entries below are synchronized and curated for `1.5.x`.
- Improved `Exception` and `StackTraceEntry` reporting. - Earlier history may be incomplete and should be cross-checked with git tags/commits when needed.
### Tooling and IDE ## 1.5.1 (2026-03-25)
- **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.
### 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 ### Docs and AI references
- 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). - Updated compiler-accurate AI language docs:
- This allows patterns where a base class calls a `protected` method that is implemented in a subclass. - interpolation syntax and escaping
- Fixed a regression where self-calls to unmangled members sometimes bypassed visibility checks incorrectly; these are now handled by the refined logic. - 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 ### Compatibility notes
- `return [expression]` exits the innermost enclosing callable (function or lambda). - Interpolation is enabled by default for normal string literals.
- Supports non-local returns using `@label` syntax (e.g., `return@outer 42`). - Existing code that intentionally used `$name` as literal text should use `\$name`, `$$name`, or the file directive `// feature: interpolation: off`.
- 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`.
- Language: stdlib improvements ## 1.5.0 (2026-03-22)
- 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.
- Language: Class properties with accessors ### Major runtime/compiler direction
- Support for `val` (read-only) and `var` (read-write) properties in classes. - Completed migration to bytecode-first/bytecode-only execution paths.
- Syntax: `val name [ : Type ] get() { body }` or `var name [ : Type ] get() { body } set(value) { body }`. - Removed interpreter fallback behavior in core execution hot paths.
- Laconic Expression Shorthand: `val prop get() = expression` and `var prop get() = read set(v) = write`. - Continued frame-slot-first local/capture model improvements and related diagnostics.
- 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`.
- Language: Restricted Setter Visibility ### Language features and semantics
- Support for `private set` and `protected set` modifiers on `var` fields and properties. - Added/finished `return` semantics including labeled non-local forms (`return@label`).
- Allows members to be publicly readable but only writable from within the declaring class or its subclasses. - Added abstract classes/members and `interface` support (as abstract-class-style construct).
- Enforcement at runtime: throws `AccessException` on unauthorized writes. - Completed and enabled multiple inheritance with C3 MRO by default.
- Supported only for declarations in class bodies (fields and properties). - Added class properties with accessors (`get`/`set`) and restricted setter visibility (`private set`, `protected set`).
- Documentation: New "Restricted Setter Visibility" section in `docs/OOP.md`. - 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 ### Type system and collections
- Support for declaring `val` without an immediate initializer in class bodies. - Added immutable collections hierarchy (`ImmutableList`, `ImmutableSet`, `ImmutableMap`).
- Compulsory initialization: every late-init `val` must be assigned at least once within the class body or an `init` block. - Improved generic runtime binding/checking for explicit type arguments and bounds.
- Write-once enforcement: assigning to a `val` is allowed only if its current value is `Unset`. - Added smarter type-aware collection ops (`+=`, `-=`) and stronger declared-member type checks.
- 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`.
- Docs: OOP improvements ### Extern/Kotlin bridge
- 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`). - Tightened extern declaration rules:
- 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`). - explicit extern members are required for extern class/object declarations.
- Tutorial: added quick link to Scopes and Closures. - 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) ### Standard library and modules
- Global completion: local declarations, in‑scope parameters, imported modules, and stdlib symbols. - Added `lyng.observable` improvements (`ObservableList` hooks/events).
- 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. - Added `Random` stdlib API used by updated samples.
- Inheritance-aware: direct class members first, then inherited (e.g., `List` includes `Collection`/`Iterable` methods). - Added/extended `lyngio.console` support and CLI integration for console interaction.
- Heuristics: handles literals (`"…"` → `String`, numbers → `Int/Real`, `[...]` → `List`, `{...}` → `Dict`) and static `Namespace.` members. - Migrated time APIs to `kotlin.time` (`Instant` migration and related docs/tests).
- 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.
- Language: Named arguments and named splats ### CLI, IDE, and docs/tooling
- New call-site syntax for named arguments using colon: `name: value`. - CLI:
- Positional arguments must come before named; positionals after a named argument inside parentheses are rejected. - added first-class `fmt` command
- Trailing-lambda interaction: if the last parameter is already assigned by name (or via a named splat), a trailing `{ ... }` block is illegal. - preserved direct script fast-path invocation
- Named splats: `...` can now expand a Map into named arguments. - improved command help/dispatch behavior
- Only string keys are allowed; non-string keys raise a clear error. - IntelliJ plugin:
- Duplicate assignment across named args and named splats is an error. - improved lightweight completion and documentation/inspection behavior
- Ellipsis (variadic) parameters remain positional-only and cannot be named. - continued highlighter and Grazie/spellchecking integration work
- 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?`. - Docs:
- Documentation updated: proposals and declaring-arguments sections now cover named args/splats and error cases. - substantial updates across tutorial/OOP/type/runtime references
- Tests added covering success cases and errors for named args/splats and trailing-lambda interactions. - expanded bytecode and advanced topics coverage
- Tooling: Highlighters and TextMate bundle updated for named args ## Migration checklist for 1.5.x
- 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.
- Multiple Inheritance (MI) completed and enabled by default: - If you rely on literal `$...` strings:
- Active C3 Method Resolution Order (MRO) for deterministic, monotonic lookup across complex hierarchies and diamonds. - replace with `\$...` or `$$...`, or
- Qualified dispatch: - add `// feature: interpolation: off` at file top.
- `this@Type.member(...)` inside class bodies starts lookup at the specified ancestor. - Review any code relying on interpreter-era fallback behavior; 1.5.x assumes bytecode-first execution.
- Cast-based disambiguation: `(expr as Type).member(...)`, `(expr as? Type)?.member(...)` (works with existing safe-call `?.`). - For extern declarations, ensure members are explicitly declared where required.
- Field inheritance (`val`/`var`) under MI: - For named arguments/splats, verify call sites follow stricter ordering/duplication rules.
- 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.
+8
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). - Supported escapes: `\n`, `\r`, `\t`, `\"`, `\\`, `\uXXXX` (4 hex digits).
- Unicode escapes use exactly 4 hex digits (for example: `"\u0416"` -> `Ж`). - Unicode escapes use exactly 4 hex digits (for example: `"\u0416"` -> `Ж`).
- Unknown `\x` escapes in strings are preserved literally as two characters (`\` and `x`). - 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`). - Numbers: `Int` (`123`, `1_000`), `Real` (`1.2`, `1e3`), hex (`0xFF`).
- Char: `'a'`, escaped chars supported. - Char: `'a'`, escaped chars supported.
- Supported escapes: `\n`, `\r`, `\t`, `\'`, `\\`, `\uXXXX` (4 hex digits). - Supported escapes: `\n`, `\r`, `\t`, `\'`, `\\`, `\uXXXX` (4 hex digits).
+15 -4
View File
@@ -40,7 +40,18 @@ Console.setCursorVisible(true)
Console.flush() Console.flush()
``` ```
Interactive sample script in this repo: #### Tetris sample
The repository includes a full interactive Tetris sample that demonstrates:
- alternate screen rendering
- raw keyboard input
- resize handling
- typed console events
![Lyng Tetris sample](/tetris.png)
Run it from the project root in a real TTY:
```bash ```bash
lyng examples/tetris_console.lyng lyng examples/tetris_console.lyng
@@ -50,7 +61,7 @@ lyng examples/tetris_console.lyng
- `Console.isSupported(): Bool` — whether console control is available on this platform/runtime. - `Console.isSupported(): Bool` — whether console control is available on this platform/runtime.
- `Console.isTty(): Bool` — whether output is attached to a TTY. - `Console.isTty(): Bool` — whether output is attached to a TTY.
- `Console.ansiLevel(): String` — `NONE`, `BASIC16`, `ANSI256`, `TRUECOLOR`. - `Console.ansiLevel(): ConsoleAnsiLevel` — `NONE`, `BASIC16`, `ANSI256`, `TRUECOLOR`.
- `Console.geometry(): ConsoleGeometry?` — `{columns, rows}` as typed object or `null`. - `Console.geometry(): ConsoleGeometry?` — `{columns, rows}` as typed object or `null`.
- `Console.details(): ConsoleDetails` — consolidated capability object. - `Console.details(): ConsoleDetails` — consolidated capability object.
- `Console.write(text: String)` — writes to console output. - `Console.write(text: String)` — writes to console output.
@@ -81,9 +92,9 @@ launch {
#### Event format #### Event format
`Console.events()` emits `ConsoleEvent` with at least: `Console.events()` emits `ConsoleEvent` with:
- `type: String` — `resize`, `keydown`, `keyup` - `type: ConsoleEventType` — `UNKNOWN`, `RESIZE`, `KEY_DOWN`, `KEY_UP`
Additional fields: Additional fields:
+46
View File
@@ -1573,6 +1573,52 @@ same as:
assert(s[1] == 'a') assert(s[1] == 'a')
>>> void >>> 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 ### Char literal escapes
Are the same as in string literals with little difference: Are the same as in string literals with little difference:
Regular → Executable
+257 -102
View File
@@ -8,15 +8,25 @@
* - Up arrow or W: rotate * - Up arrow or W: rotate
* - Down arrow or S: soft drop * - Down arrow or S: soft drop
* - Space: hard drop * - Space: hard drop
* - Q or Escape: quit * - P or Escape: pause
* - Q: quit
Tsted to score:
sergeych@sergeych-XPS-17-9720:~$ ~/dev/lyng/examples/tetris_console.lyng
Bye.
Score: 435480
Lines: 271
Level: 28
Ssergeych@sergeych-XPS-17-9720:~$
*/ */
import lyng.io.console import lyng.io.console
import lyng.io.fs
val MIN_COLS = 56 val MIN_COLS = 56
val MIN_ROWS = 24 val MIN_ROWS = 24
val PANEL_WIDTH = 24 val PANEL_WIDTH = 24
val BOARD_MARGIN_ROWS = 8 val BOARD_MARGIN_ROWS = 5
val BOARD_MIN_W = 10 val BOARD_MIN_W = 10
val BOARD_MAX_W = 16 val BOARD_MAX_W = 16
val BOARD_MIN_H = 16 val BOARD_MIN_H = 16
@@ -26,9 +36,11 @@ val DROP_FRAMES_BASE = 15
val DROP_FRAMES_MIN = 3 val DROP_FRAMES_MIN = 3
val FRAME_DELAY_MS = 35 val FRAME_DELAY_MS = 35
val RESIZE_WAIT_MS = 250 val RESIZE_WAIT_MS = 250
val MAX_PENDING_INPUTS = 64
val ROTATION_KICKS = [0, -1, 1, -2, 2] val ROTATION_KICKS = [0, -1, 1, -2, 2]
val ANSI_ESC = "\u001b[" val ANSI_ESC = "\u001b["
val ANSI_RESET = ANSI_ESC + "0m" val ANSI_RESET = ANSI_ESC + "0m"
val ERROR_LOG_PATH = "/tmp/lyng_tetris_errors.log"
val UNICODE_BLOCK = "██" val UNICODE_BLOCK = "██"
val UNICODE_TOP_LEFT = "┌" val UNICODE_TOP_LEFT = "┌"
val UNICODE_TOP_RIGHT = "┐" val UNICODE_TOP_RIGHT = "┐"
@@ -64,6 +76,7 @@ class GameState(
var level = 1 var level = 1
var running = true var running = true
var gameOver = false var gameOver = false
var paused = false
} }
class LoopFrame(val resized: Bool, val originRow: Int, val originCol: Int) {} class LoopFrame(val resized: Bool, val originRow: Int, val originCol: Int) {}
@@ -72,44 +85,44 @@ fun clearAndHome() {
Console.home() Console.home()
} }
fun logError(message: String, err: Object?): Void {
try {
var details = ""
if (err != null) {
try {
details = ": " + err
} catch (_: Object) {
details = ": <error-format-failed>"
}
}
Path(ERROR_LOG_PATH).appendUtf8(message + details + "\n")
} catch (_: Object) {
// Never let logging errors affect gameplay.
}
}
fun repeatText(s: String, n: Int): String { fun repeatText(s: String, n: Int): String {
var out: String = "" var out: String = ""
var i = 0 for (i in 0..<n) {
while (i < n) {
out += s out += s
i = i + 1
} }
out out
} }
fun maxInt(a: Int, b: Int): Int { fun max<T>(a: T, b: T): T = if (a > b) a else b
if (a > b) a else b
}
fun minInt(a: Int, b: Int): Int {
if (a < b) a else b
}
fun clampInt(v: Int, lo: Int, hi: Int): Int {
minInt(hi, maxInt(lo, v))
}
fun emptyRow(width: Int): Row { fun emptyRow(width: Int): Row {
val r: Row = [] val r: Row = []
var x = 0 for (x in 0..<width) {
while (x < width) {
r.add(0) r.add(0)
x = x + 1
} }
r r
} }
fun createBoard(width: Int, height: Int): Board { fun createBoard(width: Int, height: Int): Board {
val b: Board = [] val b: Board = []
var y = 0 for (y in 0..<height) {
while (y < height) {
b.add(emptyRow(width)) b.add(emptyRow(width))
y = y + 1
} }
b b
} }
@@ -139,7 +152,10 @@ fun emptyCellText(useColor: Bool): String {
} }
fun canPlace(board: Board, boardW: Int, boardH: Int, pieceId: Int, rot: Int, px: Int, py: Int): Bool { fun canPlace(board: Board, boardW: Int, boardH: Int, pieceId: Int, rot: Int, px: Int, py: Int): Bool {
try {
if (pieceId < 1 || pieceId > 7) return false
val piece: Piece = PIECES[pieceId - 1] val piece: Piece = PIECES[pieceId - 1]
if (rot < 0 || rot >= piece.rotations.size) return false
val cells = piece.rotations[rot] val cells = piece.rotations[rot]
for (cell in cells) { for (cell in cells) {
@@ -150,11 +166,16 @@ fun canPlace(board: Board, boardW: Int, boardH: Int, pieceId: Int, rot: Int, px:
if (y >= boardH) return false if (y >= boardH) return false
if (y >= 0) { if (y >= 0) {
if (y >= board.size) return false
val row = board[y] val row = board[y]
if (row == null) return false
if (row[x] != 0) return false if (row[x] != 0) return false
} }
} }
true true
} catch (_: Object) {
false
}
} }
fun lockPiece(board: Board, pieceId: Int, rot: Int, px: Int, py: Int): Void { fun lockPiece(board: Board, pieceId: Int, rot: Int, px: Int, py: Int): Void {
@@ -165,11 +186,13 @@ fun lockPiece(board: Board, pieceId: Int, rot: Int, px: Int, py: Int): Void {
val x = px + cell[0] val x = px + cell[0]
val y = py + cell[1] val y = py + cell[1]
if (y >= 0) { if (y >= 0 && y < board.size) {
val row = board[y] val row = board[y]
if (row != null) {
row[x] = pieceId row[x] = pieceId
} }
} }
}
} }
fun clearCompletedLines(board: Board, boardW: Int, boardH: Int): Int { fun clearCompletedLines(board: Board, boardW: Int, boardH: Int): Int {
@@ -178,23 +201,31 @@ fun clearCompletedLines(board: Board, boardW: Int, boardH: Int): Int {
var cleared = 0 var cleared = 0
while (y >= 0) { while (y >= 0) {
if (y >= b.size) {
y--
continue
}
val row = b[y] val row = b[y]
if (row == null) {
b.removeAt(y)
b.insertAt(0, emptyRow(boardW))
cleared++
continue
}
var full = true var full = true
var x = 0 for (x in 0..<boardW) {
while (x < boardW) {
if (row[x] == 0) { if (row[x] == 0) {
full = false full = false
break break
} }
x = x + 1
} }
if (full) { if (full) {
b.removeAt(y) b.removeAt(y)
b.insertAt(0, emptyRow(boardW)) b.insertAt(0, emptyRow(boardW))
cleared = cleared + 1 cleared++
} else { } else {
y = y - 1 y--
} }
} }
@@ -220,7 +251,7 @@ fun tryRotateCw(board: Board, boardW: Int, boardH: Int, pieceId: Int, rot: Int,
val nr = (rot + 1) % rotations val nr = (rot + 1) % rotations
for (kx in ROTATION_KICKS) { for (kx in ROTATION_KICKS) {
val nx = px + kx val nx = px + kx
if (canPlace(board, boardW, boardH, pieceId, nr, nx, py)) { if (canPlace(board, boardW, boardH, pieceId, nr, nx, py) == true) {
return RotateResult(true, nr, nx) return RotateResult(true, nr, nx)
} }
} }
@@ -230,21 +261,18 @@ fun tryRotateCw(board: Board, boardW: Int, boardH: Int, pieceId: Int, rot: Int,
fun nextPreviewLines(pieceId: Int, useColor: Bool): List<String> { fun nextPreviewLines(pieceId: Int, useColor: Bool): List<String> {
val out: List<String> = [] val out: List<String> = []
if (pieceId <= 0) { if (pieceId <= 0) {
for (i in 0..<4) {
out.add(" ") out.add(" ")
out.add(" ") }
out.add(" ")
out.add(" ")
return out return out
} }
val piece: Piece = PIECES[pieceId - 1] val piece: Piece = PIECES[pieceId - 1]
val cells = piece.rotations[0] val cells = piece.rotations[0]
var y = 0 for (y in 0..<4) {
while (y < 4) {
var line = "" var line = ""
var x = 0 for (x in 0..<4) {
while (x < 4) {
var filled = false var filled = false
for (cell in cells) { for (cell in cells) {
if (cell[0] == x && cell[1] == y) { if (cell[0] == x && cell[1] == y) {
@@ -253,10 +281,8 @@ fun nextPreviewLines(pieceId: Int, useColor: Bool): List<String> {
} }
} }
line += if (filled) blockText(pieceId, useColor) else " " line += if (filled) blockText(pieceId, useColor) else " "
x = x + 1
} }
out.add(line) out.add(line)
y = y + 1
} }
out out
} }
@@ -296,52 +322,101 @@ fun render(
panel.add("W/Up: rotate") panel.add("W/Up: rotate")
panel.add("S/Down: drop") panel.add("S/Down: drop")
panel.add("Space: hard drop") panel.add("Space: hard drop")
panel.add("Q/Esc: quit") panel.add("P/Esc: pause")
panel.add("Q: quit")
val frameLines: List<String> = [] val frameLines: List<String> = []
var y = 0 for (y in 0..<boardH) {
while (y < boardH) {
var line = UNICODE_VERTICAL var line = UNICODE_VERTICAL
var x = 0 for (x in 0..<boardW) {
while (x < boardW) {
val a = activeCellId(state.pieceId, state.rot, state.px, state.py, x, y) val a = activeCellId(state.pieceId, state.rot, state.px, state.py, x, y)
val row = board[y] val row = if (y < board.size) board[y] else null
val b = row[x] val b = if (row == null) 0 else row[x]
val id = if (a > 0) a else b val id = if (a > 0) a else b
line += if (id > 0) blockText(id, useColor) else emptyCellText(useColor) line += if (id > 0) blockText(id, useColor) else emptyCellText(useColor)
x = x + 1
} }
line += UNICODE_VERTICAL line += UNICODE_VERTICAL
val p = y if (y < panel.size) {
if (p < panel.size) { line += " " + panel[y]
line += " " + panel[p]
} }
frameLines.add(line) frameLines.add(line)
y = y + 1
} }
frameLines.add(bottomBorder) frameLines.add(bottomBorder)
val prev = prevFrameLines for (i in 0..<frameLines.size) {
var i = 0
while (i < frameLines.size) {
val line = frameLines[i] val line = frameLines[i]
val old = if (i < prev.size) prev[i] else null val old = if (i < prevFrameLines.size) prevFrameLines[i] else null
if (old != line) { if (old != line) {
Console.moveTo(originRow + i, originCol) Console.moveTo(originRow + i, originCol)
Console.clearLine() Console.clearLine()
Console.write(line) Console.write(line)
} }
i = i + 1
} }
frameLines frameLines
} }
fun fitLine(line: String, width: Int): String {
val maxLen = if (width > 0) width else 0
if (maxLen <= 0) return ""
if (line.size >= maxLen) return line[..<maxLen]
line + repeatText(" ", maxLen - line.size)
}
fun renderPauseOverlay(
originRow: Int,
originCol: Int,
boardW: Int,
boardH: Int,
): Void {
val contentWidth = boardW * 2 + 2 + 3 + PANEL_WIDTH
val contentHeight = boardH + 1
val lines: List<String> = []
lines.add("PAUSED")
lines.add("")
lines.add("Any key: continue game")
lines.add("Esc: exit game")
lines.add("")
lines.add("Move: A/D or arrows")
lines.add("Rotate: W or Up")
lines.add("Drop: S/Down, Space hard drop")
var innerWidth = 0
for (line in lines) {
if (line.size > innerWidth) innerWidth = line.size
}
innerWidth += 4
val maxInner = max(12, contentWidth - 2)
if (innerWidth > maxInner) innerWidth = maxInner
if (innerWidth % 2 != 0) innerWidth--
val boxWidth = innerWidth + 2
val boxHeight = lines.size + 2
val left = originCol + max(0, (contentWidth - boxWidth) / 2)
val top = originRow + max(0, (contentHeight - boxHeight) / 2)
val topBorder = UNICODE_TOP_LEFT + repeatText(UNICODE_HORIZONTAL, innerWidth / 2) + UNICODE_TOP_RIGHT
val bottomBorder = UNICODE_BOTTOM_LEFT + repeatText(UNICODE_HORIZONTAL, innerWidth / 2) + UNICODE_BOTTOM_RIGHT
Console.moveTo(top, left)
Console.write(topBorder)
for (i in 0..<lines.size) {
Console.moveTo(top + 1 + i, left)
Console.write(UNICODE_VERTICAL + fitLine(" " + lines[i], innerWidth) + UNICODE_VERTICAL)
}
Console.moveTo(top + boxHeight - 1, left)
Console.write(bottomBorder)
}
fun waitForMinimumSize(minCols: Int, minRows: Int): Object { fun waitForMinimumSize(minCols: Int, minRows: Int): Object {
while (true) { while (true) {
val g = Console.geometry() val g = Console.geometry()
@@ -351,9 +426,23 @@ fun waitForMinimumSize(minCols: Int, minRows: Int): Object {
if (cols >= minCols && rows >= minRows) return g if (cols >= minCols && rows >= minRows) return g
clearAndHome() clearAndHome()
println("Lyng Tetris needs at least %sx%s terminal size."(minCols, minRows)) val lines: List<String> = []
println("Current: %sx%s"(cols, rows)) lines.add("Lyng Tetris needs at least %sx%s terminal size."(minCols, minRows))
println("Resize the console window to continue...") lines.add("Current: %sx%s"(cols, rows))
lines.add("Resize the console window to continue.")
val visibleLines = if (rows < lines.size) rows else lines.size
if (visibleLines > 0) {
val startRow = max(1, ((rows - visibleLines) / 2) + 1)
for (i in 0..<visibleLines) {
val line = lines[i]
val startCol = max(1, ((cols - line.size) / 2) + 1)
Console.moveTo(startRow + i, startCol)
Console.clearLine()
Console.write(line)
}
Console.flush()
}
delay(RESIZE_WAIT_MS) delay(RESIZE_WAIT_MS)
} }
} }
@@ -434,8 +523,8 @@ if (!Console.isSupported()) {
val cols = g0?.columns ?: MIN_COLS val cols = g0?.columns ?: MIN_COLS
val rows = g0?.rows ?: MIN_ROWS val rows = g0?.rows ?: MIN_ROWS
val boardW = clampInt((cols - PANEL_WIDTH) / 2, BOARD_MIN_W, BOARD_MAX_W) val boardW = clamp((cols - PANEL_WIDTH) / 2, BOARD_MIN_W..BOARD_MAX_W)
val boardH = clampInt(rows - BOARD_MARGIN_ROWS, BOARD_MIN_H, BOARD_MAX_H) val boardH = clamp(rows - BOARD_MARGIN_ROWS, BOARD_MIN_H..BOARD_MAX_H)
val board: Board = createBoard(boardW, boardH) val board: Board = createBoard(boardW, boardH)
@@ -452,8 +541,9 @@ if (!Console.isSupported()) {
) )
var prevFrameLines: List<String> = [] var prevFrameLines: List<String> = []
val gameMutex = Mutex() val gameMutex: Mutex = Mutex()
var hasResizeEvent = false var forceRedraw = false
val pendingInputs: List<String> = []
val rawModeEnabled = Console.setRawMode(true) val rawModeEnabled = Console.setRawMode(true)
if (!rawModeEnabled) { if (!rawModeEnabled) {
@@ -461,20 +551,43 @@ if (!Console.isSupported()) {
println("Use jlyng in an interactive terminal with raw input support.") println("Use jlyng in an interactive terminal with raw input support.")
void void
} else { } else {
val useColor = Console.ansiLevel() != "NONE" val useColor = Console.ansiLevel() != ConsoleAnsiLevel.NONE
Console.enterAltScreen() Console.enterAltScreen()
Console.setCursorVisible(false) Console.setCursorVisible(false)
clearAndHome() clearAndHome()
fun resetActivePiece(s: GameState): Void {
s.pieceId = nextPieceId()
s.rot = 0
s.px = (boardW / 2) - 2
s.py = -1
}
fun applyKeyInput(s: GameState, key: String): Void { fun applyKeyInput(s: GameState, key: String): Void {
if (key == "__CTRL_C__" || key == "q" || key == "Q" || key == "Escape") { try {
if (key == "__CTRL_C__") {
s.running = false
}
else if (s.paused) {
if (key == "Escape") {
s.running = false
} else {
s.paused = false
forceRedraw = true
}
}
else if (key == "p" || key == "P" || key == "Escape") {
s.paused = true
forceRedraw = true
}
else if (key == "q" || key == "Q") {
s.running = false s.running = false
} }
else if (key == "ArrowLeft" || key == "a" || key == "A") { else if (key == "ArrowLeft" || key == "a" || key == "A") {
if (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px - 1, s.py)) s.px = s.px - 1 if (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px - 1, s.py) == true) s.px--
} }
else if (key == "ArrowRight" || key == "d" || key == "D") { else if (key == "ArrowRight" || key == "d" || key == "D") {
if (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px + 1, s.py)) s.px = s.px + 1 if (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px + 1, s.py) == true) s.px++
} }
else if (key == "ArrowUp" || key == "w" || key == "W") { else if (key == "ArrowUp" || key == "w" || key == "W") {
val rr: RotateResult = tryRotateCw(board, boardW, boardH, s.pieceId, s.rot, s.px, s.py) val rr: RotateResult = tryRotateCw(board, boardW, boardH, s.pieceId, s.rot, s.px, s.py)
@@ -484,17 +597,21 @@ if (!Console.isSupported()) {
} }
} }
else if (key == "ArrowDown" || key == "s" || key == "S") { else if (key == "ArrowDown" || key == "s" || key == "S") {
if (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px, s.py + 1)) { if (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px, s.py + 1) == true) {
s.py = s.py + 1 s.py++
s.score = s.score + 1 s.score++
} }
} }
else if (key == " ") { else if (key == " ") {
while (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px, s.py + 1)) { while (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px, s.py + 1) == true) {
s.py = s.py + 1 s.py++
s.score = s.score + 2 s.score += 2
} }
} }
} catch (inputErr: Object) {
logError("applyKeyInput recovered after error", inputErr)
resetActivePiece(s)
}
} }
var inputRunning = true var inputRunning = true
@@ -503,26 +620,47 @@ if (!Console.isSupported()) {
try { try {
for (ev in Console.events()) { for (ev in Console.events()) {
if (!inputRunning) break if (!inputRunning) break
// Isolate per-event failures so one bad event does not unwind the stream.
try {
if (ev is ConsoleKeyEvent) { if (ev is ConsoleKeyEvent) {
val ke = ev as ConsoleKeyEvent val ke = ev as ConsoleKeyEvent
if (ke.type == "keydown") { if (ke.type == ConsoleEventType.KEY_DOWN) {
val key = ke.key var key = ""
val ctrl = ke.ctrl var ctrl = false
gameMutex.withLock { try {
key = ke.key
} catch (keyErr: Object) {
logError("Input key read error", keyErr)
}
try {
ctrl = ke.ctrl == true
} catch (_: Object) {
ctrl = false
}
if (key == "") {
logError("Dropped key event with empty/null key", null)
continue
}
val mapped = if (ctrl && (key == "c" || key == "C")) "__CTRL_C__" else key val mapped = if (ctrl && (key == "c" || key == "C")) "__CTRL_C__" else key
applyKeyInput(state, mapped) val mm: Mutex = gameMutex
mm.withLock {
if (pendingInputs.size >= MAX_PENDING_INPUTS) {
pendingInputs.removeAt(0)
} }
} pendingInputs.add(mapped)
} else if (ev is ConsoleResizeEvent) {
gameMutex.withLock {
hasResizeEvent = true
prevFrameLines = []
} }
} }
} }
} catch (err: Exception) { } catch (eventErr: Object) {
// Keep game alive: transient console-event failures should not force quit. // Keep the input stream alive; report for diagnostics.
logError("Input event error", eventErr)
}
}
} catch (err: Object) {
// Recover stream-level failures by recreating event stream in next loop turn.
if (!inputRunning) break if (!inputRunning) break
logError("Input stream recovered after error", err)
Console.setRawMode(true) Console.setRawMode(true)
delay(50) delay(50)
} }
@@ -540,16 +678,10 @@ if (!Console.isSupported()) {
return null return null
} }
var resized = false
gameMutex.withLock {
resized = hasResizeEvent
hasResizeEvent = false
}
val contentCols = boardW * 2 + 2 + 3 + PANEL_WIDTH val contentCols = boardW * 2 + 2 + 3 + PANEL_WIDTH
val contentRows = boardH + 1 val contentRows = boardH + 1
val requiredCols = maxInt(MIN_COLS, contentCols) val requiredCols = max(MIN_COLS, contentCols)
val requiredRows = maxInt(MIN_ROWS, contentRows) val requiredRows = max(MIN_ROWS, contentRows)
if (c < requiredCols || r < requiredRows) { if (c < requiredCols || r < requiredRows) {
waitForMinimumSize(requiredCols, requiredRows) waitForMinimumSize(requiredCols, requiredRows)
clearAndHome() clearAndHome()
@@ -557,21 +689,21 @@ if (!Console.isSupported()) {
return null return null
} }
val originCol = maxInt(1, ((c - contentCols) / 2) + 1) val originCol = max(1, ((c - contentCols) / 2) + 1)
val originRow = maxInt(1, ((r - contentRows) / 2) + 1) val originRow = max(1, ((r - contentRows) / 2) + 1)
LoopFrame(resized, originRow, originCol) LoopFrame(false, originRow, originCol)
} }
fun advanceGravity(s: GameState, frame: Int): Int { fun advanceGravity(s: GameState, frame: Int): Int {
s.level = 1 + (s.totalLines / LEVEL_LINES_STEP) s.level = 1 + (s.totalLines / LEVEL_LINES_STEP)
val dropEvery = maxInt(DROP_FRAMES_MIN, DROP_FRAMES_BASE - s.level) val dropEvery = max(DROP_FRAMES_MIN, DROP_FRAMES_BASE - s.level)
var nextFrame = frame + 1 var nextFrame = frame + 1
if (nextFrame < dropEvery) return nextFrame if (nextFrame < dropEvery) return nextFrame
nextFrame = 0 nextFrame = 0
if (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px, s.py + 1)) { if (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px, s.py + 1) == true) {
s.py = s.py + 1 s.py++
return nextFrame return nextFrame
} }
@@ -579,8 +711,8 @@ if (!Console.isSupported()) {
val cleared = clearCompletedLines(board, boardW, boardH) val cleared = clearCompletedLines(board, boardW, boardH)
if (cleared > 0) { if (cleared > 0) {
s.totalLines = s.totalLines + cleared s.totalLines += cleared
s.score = s.score + scoreForLines(cleared, s.level) s.score += scoreForLines(cleared, s.level)
} }
s.pieceId = s.nextId s.pieceId = s.nextId
@@ -605,25 +737,45 @@ if (!Console.isSupported()) {
var shouldStop = false var shouldStop = false
var prevOriginRow = -1 var prevOriginRow = -1
var prevOriginCol = -1 var prevOriginCol = -1
var prevPaused = false
while (!shouldStop) { while (!shouldStop) {
val frameData = pollLoopFrame() val frameData = pollLoopFrame()
if (frameData == null) { if (frameData == null) {
frame = 0 frame = 0
prevPaused = false
continue continue
} }
gameMutex.withLock { val mm: Mutex = gameMutex
mm.withLock {
if (pendingInputs.size > 0) {
val toApply: List<String> = []
while (pendingInputs.size > 0) {
val k = pendingInputs[0]
pendingInputs.removeAt(0)
toApply.add(k)
}
for (k in toApply) {
applyKeyInput(state, k)
if (!state.running || state.gameOver) break
}
}
}
if (!state.running || state.gameOver) { if (!state.running || state.gameOver) {
shouldStop = true shouldStop = true
} else { } else {
val localForceRedraw = forceRedraw
forceRedraw = false
val movedOrigin = frameData.originRow != prevOriginRow || frameData.originCol != prevOriginCol val movedOrigin = frameData.originRow != prevOriginRow || frameData.originCol != prevOriginCol
if (frameData.resized || movedOrigin) { if (frameData.resized || movedOrigin || localForceRedraw) {
clearAndHome() clearAndHome()
prevFrameLines = [] prevFrameLines = []
} }
prevOriginRow = frameData.originRow prevOriginRow = frameData.originRow
prevOriginCol = frameData.originCol prevOriginCol = frameData.originCol
if (!state.paused) {
frame = advanceGravity(state, frame) frame = advanceGravity(state, frame)
}
prevFrameLines = render( prevFrameLines = render(
state, state,
board, board,
@@ -634,7 +786,10 @@ if (!Console.isSupported()) {
frameData.originCol, frameData.originCol,
useColor useColor
) )
if (state.paused && (!prevPaused || frameData.resized || movedOrigin)) {
renderPauseOverlay(frameData.originRow, frameData.originCol, boardW, boardH)
} }
prevPaused = state.paused
} }
Console.flush() Console.flush()
delay(FRAME_DELAY_MS) delay(FRAME_DELAY_MS)
+58
View File
@@ -112,6 +112,64 @@ kotlin {
} }
} }
abstract class GenerateLyngioConsoleDecls : DefaultTask() {
@get:InputFile
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val sourceFile: RegularFileProperty
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun generate() {
val targetPkg = "net.sergeych.lyngio.stdlib_included"
val pkgPath = targetPkg.replace('.', '/')
val targetDir = outputDir.get().asFile.resolve(pkgPath)
targetDir.mkdirs()
val text = sourceFile.get().asFile.readText()
fun escapeForQuoted(s: String): String = buildString {
for (ch in s) when (ch) {
'\\' -> append("\\\\")
'"' -> append("\\\"")
'\n' -> append("\\n")
'\r' -> {}
'\t' -> append("\\t")
else -> append(ch)
}
}
val out = buildString {
append("package ").append(targetPkg).append("\n\n")
append("@Suppress(\"Unused\", \"MemberVisibilityCanBePrivate\")\n")
append("internal val consoleLyng = \"")
append(escapeForQuoted(text))
append("\"\n")
}
targetDir.resolve("console_types_lyng.generated.kt").writeText(out)
}
}
val lyngioConsoleDeclsFile = layout.projectDirectory.file("stdlib/lyng/io/console.lyng")
val generatedLyngioDeclsDir = layout.buildDirectory.dir("generated/source/lyngioDecls/commonMain/kotlin")
val generateLyngioConsoleDecls by tasks.registering(GenerateLyngioConsoleDecls::class) {
sourceFile.set(lyngioConsoleDeclsFile)
outputDir.set(generatedLyngioDeclsDir)
}
kotlin.sourceSets.named("commonMain") {
kotlin.srcDir(generateLyngioConsoleDecls)
}
kotlin.targets.configureEach {
compilations.configureEach {
compileTaskProvider.configure {
dependsOn(generateLyngioConsoleDecls)
}
}
}
android { android {
namespace = "net.sergeych.lyngio" namespace = "net.sergeych.lyngio"
compileSdk = libs.versions.android.compileSdk.get().toInt() compileSdk = libs.versions.android.compileSdk.get().toInt()
@@ -20,9 +20,11 @@ package net.sergeych.lyng.io.console
import net.sergeych.lyng.ModuleScope import net.sergeych.lyng.ModuleScope
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeFacade import net.sergeych.lyng.ScopeFacade
import net.sergeych.lyng.miniast.* import net.sergeych.lyng.Source
import net.sergeych.lyng.obj.Obj import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjBool import net.sergeych.lyng.obj.ObjBool
import net.sergeych.lyng.obj.ObjEnumClass
import net.sergeych.lyng.obj.ObjEnumEntry
import net.sergeych.lyng.obj.ObjIterable import net.sergeych.lyng.obj.ObjIterable
import net.sergeych.lyng.obj.ObjIterationFinishedException import net.sergeych.lyng.obj.ObjIterationFinishedException
import net.sergeych.lyng.obj.ObjIterator import net.sergeych.lyng.obj.ObjIterator
@@ -35,13 +37,13 @@ import net.sergeych.lyng.obj.toObj
import net.sergeych.lyng.pacman.ImportManager import net.sergeych.lyng.pacman.ImportManager
import net.sergeych.lyng.raiseIllegalOperation import net.sergeych.lyng.raiseIllegalOperation
import net.sergeych.lyng.requireScope import net.sergeych.lyng.requireScope
import net.sergeych.lyngio.console.ConsoleEvent import net.sergeych.lyngio.console.*
import net.sergeych.lyngio.console.ConsoleEventSource
import net.sergeych.lyngio.console.LyngConsole
import net.sergeych.lyngio.console.getSystemConsole
import net.sergeych.lyngio.console.security.ConsoleAccessDeniedException import net.sergeych.lyngio.console.security.ConsoleAccessDeniedException
import net.sergeych.lyngio.console.security.ConsoleAccessPolicy import net.sergeych.lyngio.console.security.ConsoleAccessPolicy
import net.sergeych.lyngio.console.security.LyngConsoleSecured import net.sergeych.lyngio.console.security.LyngConsoleSecured
import net.sergeych.lyngio.stdlib_included.consoleLyng
private const val CONSOLE_MODULE_NAME = "lyng.io.console"
/** /**
* Install Lyng module `lyng.io.console` into the given scope's ImportManager. * Install Lyng module `lyng.io.console` into the given scope's ImportManager.
@@ -53,10 +55,9 @@ fun createConsole(policy: ConsoleAccessPolicy, scope: Scope): Boolean = createCo
/** Same as [createConsoleModule] but with explicit [ImportManager]. */ /** Same as [createConsoleModule] but with explicit [ImportManager]. */
fun createConsoleModule(policy: ConsoleAccessPolicy, manager: ImportManager): Boolean { fun createConsoleModule(policy: ConsoleAccessPolicy, manager: ImportManager): Boolean {
val name = "lyng.io.console" if (manager.packageNames.contains(CONSOLE_MODULE_NAME)) return false
if (manager.packageNames.contains(name)) return false
manager.addPackage(name) { module -> manager.addPackage(CONSOLE_MODULE_NAME) { module ->
buildConsoleModule(module, policy) buildConsoleModule(module, policy)
} }
return true return true
@@ -65,59 +66,37 @@ fun createConsoleModule(policy: ConsoleAccessPolicy, manager: ImportManager): Bo
fun createConsole(policy: ConsoleAccessPolicy, manager: ImportManager): Boolean = createConsoleModule(policy, manager) fun createConsole(policy: ConsoleAccessPolicy, manager: ImportManager): Boolean = createConsoleModule(policy, manager)
private suspend fun buildConsoleModule(module: ModuleScope, policy: ConsoleAccessPolicy) { private suspend fun buildConsoleModule(module: ModuleScope, policy: ConsoleAccessPolicy) {
// Load Lyng declarations for console enums/types first (module-local source of truth).
module.eval(Source(CONSOLE_MODULE_NAME, consoleLyng))
ConsoleEnums.initialize(module)
val console: LyngConsole = LyngConsoleSecured(getSystemConsole(), policy) val console: LyngConsole = LyngConsoleSecured(getSystemConsole(), policy)
val consoleType = object : net.sergeych.lyng.obj.ObjClass("Console") {} val consoleType = object : net.sergeych.lyng.obj.ObjClass("Console") {}
consoleType.apply { consoleType.apply {
addClassFnDoc( addClassFn("isSupported") {
name = "isSupported",
doc = "Whether console control API is supported on this platform.",
returns = type("lyng.Bool"),
moduleName = module.packageName
) {
ObjBool(console.isSupported) ObjBool(console.isSupported)
} }
addClassFnDoc( addClassFn("isTty") {
name = "isTty",
doc = "Whether current stdout is attached to an interactive TTY.",
returns = type("lyng.Bool"),
moduleName = module.packageName
) {
consoleGuard { consoleGuard {
ObjBool(console.isTty()) ObjBool(console.isTty())
} }
} }
addClassFnDoc( addClassFn("ansiLevel") {
name = "ansiLevel",
doc = "Detected ANSI color capability: NONE, BASIC16, ANSI256, TRUECOLOR.",
returns = type("lyng.String"),
moduleName = module.packageName
) {
consoleGuard { consoleGuard {
ObjString(console.ansiLevel().name) ConsoleEnums.ansiLevel(console.ansiLevel().name)
} }
} }
addClassFnDoc( addClassFn("geometry") {
name = "geometry",
doc = "Current terminal geometry or null.",
returns = type("ConsoleGeometry", nullable = true),
moduleName = module.packageName
) {
consoleGuard { consoleGuard {
console.geometry()?.let { ObjConsoleGeometry(it.columns, it.rows) } ?: ObjNull console.geometry()?.let { ObjConsoleGeometry(it.columns, it.rows) } ?: ObjNull
} }
} }
addClassFnDoc( addClassFn("details") {
name = "details",
doc = "Get consolidated console details.",
returns = type("ConsoleDetails"),
moduleName = module.packageName
) {
consoleGuard { consoleGuard {
val tty = console.isTty() val tty = console.isTty()
val ansi = console.ansiLevel() val ansi = console.ansiLevel()
@@ -125,18 +104,13 @@ private suspend fun buildConsoleModule(module: ModuleScope, policy: ConsoleAcces
ObjConsoleDetails( ObjConsoleDetails(
supported = console.isSupported, supported = console.isSupported,
isTty = tty, isTty = tty,
ansiLevel = ansi.name, ansiLevel = ConsoleEnums.ansiLevel(ansi.name),
geometry = geometry?.let { ObjConsoleGeometry(it.columns, it.rows) }, geometry = geometry?.let { ObjConsoleGeometry(it.columns, it.rows) },
) )
} }
} }
addClassFnDoc( addClassFn("write") {
name = "write",
doc = "Write text directly to console output.",
params = listOf(ParamDoc("text", type("lyng.String"))),
moduleName = module.packageName
) {
consoleGuard { consoleGuard {
val text = requiredArg<ObjString>(0).value val text = requiredArg<ObjString>(0).value
console.write(text) console.write(text)
@@ -144,48 +118,28 @@ private suspend fun buildConsoleModule(module: ModuleScope, policy: ConsoleAcces
} }
} }
addClassFnDoc( addClassFn("flush") {
name = "flush",
doc = "Flush console output buffer.",
moduleName = module.packageName
) {
consoleGuard { consoleGuard {
console.flush() console.flush()
ObjVoid ObjVoid
} }
} }
addClassFnDoc( addClassFn("home") {
name = "home",
doc = "Move cursor to home position (1,1).",
moduleName = module.packageName
) {
consoleGuard { consoleGuard {
console.write("\u001B[H") console.write("\u001B[H")
ObjVoid ObjVoid
} }
} }
addClassFnDoc( addClassFn("clear") {
name = "clear",
doc = "Clear the visible screen buffer.",
moduleName = module.packageName
) {
consoleGuard { consoleGuard {
console.write("\u001B[2J") console.write("\u001B[2J")
ObjVoid ObjVoid
} }
} }
addClassFnDoc( addClassFn("moveTo") {
name = "moveTo",
doc = "Move cursor to 1-based row and column.",
params = listOf(
ParamDoc("row", type("lyng.Int")),
ParamDoc("column", type("lyng.Int")),
),
moduleName = module.packageName
) {
consoleGuard { consoleGuard {
val row = requiredArg<net.sergeych.lyng.obj.ObjInt>(0).value val row = requiredArg<net.sergeych.lyng.obj.ObjInt>(0).value
val col = requiredArg<net.sergeych.lyng.obj.ObjInt>(1).value val col = requiredArg<net.sergeych.lyng.obj.ObjInt>(1).value
@@ -194,45 +148,28 @@ private suspend fun buildConsoleModule(module: ModuleScope, policy: ConsoleAcces
} }
} }
addClassFnDoc( addClassFn("clearLine") {
name = "clearLine",
doc = "Clear the current line.",
moduleName = module.packageName
) {
consoleGuard { consoleGuard {
console.write("\u001B[2K") console.write("\u001B[2K")
ObjVoid ObjVoid
} }
} }
addClassFnDoc( addClassFn("enterAltScreen") {
name = "enterAltScreen",
doc = "Switch to terminal alternate screen buffer.",
moduleName = module.packageName
) {
consoleGuard { consoleGuard {
console.write("\u001B[?1049h") console.write("\u001B[?1049h")
ObjVoid ObjVoid
} }
} }
addClassFnDoc( addClassFn("leaveAltScreen") {
name = "leaveAltScreen",
doc = "Return from alternate screen buffer to normal screen.",
moduleName = module.packageName
) {
consoleGuard { consoleGuard {
console.write("\u001B[?1049l") console.write("\u001B[?1049l")
ObjVoid ObjVoid
} }
} }
addClassFnDoc( addClassFn("setCursorVisible") {
name = "setCursorVisible",
doc = "Show or hide the terminal cursor.",
params = listOf(ParamDoc("visible", type("lyng.Bool"))),
moduleName = module.packageName
) {
consoleGuard { consoleGuard {
val visible = requiredArg<ObjBool>(0).value val visible = requiredArg<ObjBool>(0).value
console.write(if (visible) "\u001B[?25h" else "\u001B[?25l") console.write(if (visible) "\u001B[?25h" else "\u001B[?25l")
@@ -240,24 +177,13 @@ private suspend fun buildConsoleModule(module: ModuleScope, policy: ConsoleAcces
} }
} }
addClassFnDoc( addClassFn("events") {
name = "events",
doc = "Endless iterable console event source (resize, keydown, keyup). Use in a loop, often inside launch.",
returns = type("ConsoleEventStream"),
moduleName = module.packageName
) {
consoleGuard { consoleGuard {
console.events().toConsoleEventStream() console.events().toConsoleEventStream()
} }
} }
addClassFnDoc( addClassFn("setRawMode") {
name = "setRawMode",
doc = "Enable or disable raw keyboard mode. Returns true if mode changed.",
params = listOf(ParamDoc("enabled", type("lyng.Bool"))),
returns = type("lyng.Bool"),
moduleName = module.packageName
) {
consoleGuard { consoleGuard {
val enabled = requiredArg<ObjBool>(0).value val enabled = requiredArg<ObjBool>(0).value
ObjBool(console.setRawMode(enabled)) ObjBool(console.setRawMode(enabled))
@@ -265,55 +191,13 @@ private suspend fun buildConsoleModule(module: ModuleScope, policy: ConsoleAcces
} }
} }
module.addConstDoc( module.addConst("Console", consoleType)
name = "Console", module.addConst("ConsoleGeometry", ObjConsoleGeometry.type)
value = consoleType, module.addConst("ConsoleDetails", ObjConsoleDetails.type)
doc = "Console runtime API.", module.addConst("ConsoleEvent", ObjConsoleEvent.type)
type = type("Console"), module.addConst("ConsoleResizeEvent", ObjConsoleResizeEvent.type)
moduleName = module.packageName module.addConst("ConsoleKeyEvent", ObjConsoleKeyEvent.typeObj)
) module.addConst("ConsoleEventStream", ObjConsoleEventStream.type)
module.addConstDoc(
name = "ConsoleGeometry",
value = ObjConsoleGeometry.type,
doc = "Terminal geometry.",
type = type("lyng.Class"),
moduleName = module.packageName
)
module.addConstDoc(
name = "ConsoleDetails",
value = ObjConsoleDetails.type,
doc = "Consolidated console capability details.",
type = type("lyng.Class"),
moduleName = module.packageName
)
module.addConstDoc(
name = "ConsoleEvent",
value = ObjConsoleEvent.type,
doc = "Base class for console events.",
type = type("lyng.Class"),
moduleName = module.packageName
)
module.addConstDoc(
name = "ConsoleResizeEvent",
value = ObjConsoleResizeEvent.type,
doc = "Terminal resize event.",
type = type("lyng.Class"),
moduleName = module.packageName
)
module.addConstDoc(
name = "ConsoleKeyEvent",
value = ObjConsoleKeyEvent.typeObj,
doc = "Keyboard event.",
type = type("lyng.Class"),
moduleName = module.packageName
)
module.addConstDoc(
name = "ConsoleEventStream",
value = ObjConsoleEventStream.type,
doc = "Endless iterable stream of console events.",
type = type("lyng.Class"),
moduleName = module.packageName
)
} }
private suspend inline fun ScopeFacade.consoleGuard(crossinline block: suspend () -> Obj): Obj { private suspend inline fun ScopeFacade.consoleGuard(crossinline block: suspend () -> Obj): Obj {
@@ -338,12 +222,7 @@ private class ObjConsoleEventStream(
companion object { companion object {
val type = net.sergeych.lyng.obj.ObjClass("ConsoleEventStream", ObjIterable).apply { val type = net.sergeych.lyng.obj.ObjClass("ConsoleEventStream", ObjIterable).apply {
addFnDoc( addFn("iterator") {
name = "iterator",
doc = "Create an iterator over incoming console events.",
returns = type("lyng.Iterator"),
moduleName = "lyng.io.console",
) {
val stream = thisAs<ObjConsoleEventStream>() val stream = thisAs<ObjConsoleEventStream>()
ObjConsoleEventIterator(stream.source) ObjConsoleEventIterator(stream.source)
} }
@@ -363,19 +242,36 @@ private class ObjConsoleEventIterator(
private suspend fun ensureCached(): Boolean { private suspend fun ensureCached(): Boolean {
if (closed) return false if (closed) return false
if (cached != null) return true if (cached != null) return true
val event = source.nextEvent() while (!closed && cached == null) {
val event = try {
source.nextEvent()
} catch (e: Throwable) {
// Consumer loops must survive source/read failures: report and keep polling.
consoleFlowDebug("console-bridge: nextEvent failed; dropping failure and continuing", e)
continue
}
if (event == null) { if (event == null) {
closeSource() closeSource()
return false return false
} }
cached = event.toObjEvent() cached = try {
return true event.toObjEvent()
} catch (e: Throwable) {
// Malformed/native event payload must not terminate consumer iteration.
consoleFlowDebug("console-bridge: malformed event dropped: $event", e)
null
}
}
return cached != null
} }
private suspend fun closeSource() { private suspend fun closeSource() {
if (closed) return if (closed) return
closed = true closed = true
source.close() // Do not close the underlying console source from VM iterator cancellation.
// CmdFrame.cancelIterators() may call cancelIteration() while user code is still
// expected to keep processing input (e.g. recover from app-level exceptions).
// The source lifecycle is managed by the console runtime.
} }
suspend fun hasNext(): Boolean = ensureCached() suspend fun hasNext(): Boolean = ensureCached()
@@ -391,28 +287,13 @@ private class ObjConsoleEventIterator(
companion object { companion object {
val type = net.sergeych.lyng.obj.ObjClass("ConsoleEventIterator", ObjIterator).apply { val type = net.sergeych.lyng.obj.ObjClass("ConsoleEventIterator", ObjIterator).apply {
addFnDoc( addFn("hasNext") {
name = "hasNext",
doc = "Whether another console event is available.",
returns = type("lyng.Bool"),
moduleName = "lyng.io.console",
) {
thisAs<ObjConsoleEventIterator>().hasNext().toObj() thisAs<ObjConsoleEventIterator>().hasNext().toObj()
} }
addFnDoc( addFn("next") {
name = "next",
doc = "Return the next console event.",
returns = type("ConsoleEvent"),
moduleName = "lyng.io.console",
) {
thisAs<ObjConsoleEventIterator>().next(requireScope()) thisAs<ObjConsoleEventIterator>().next(requireScope())
} }
addFnDoc( addFn("cancelIteration") {
name = "cancelIteration",
doc = "Stop reading console events and release resources.",
returns = type("lyng.Void"),
moduleName = "lyng.io.console",
) {
thisAs<ObjConsoleEventIterator>().closeSource() thisAs<ObjConsoleEventIterator>().closeSource()
ObjVoid ObjVoid
} }
@@ -422,27 +303,119 @@ private class ObjConsoleEventIterator(
private fun ConsoleEvent.toObjEvent(): Obj = when (this) { private fun ConsoleEvent.toObjEvent(): Obj = when (this) {
is ConsoleEvent.Resize -> ObjConsoleResizeEvent(columns, rows) is ConsoleEvent.Resize -> ObjConsoleResizeEvent(columns, rows)
is ConsoleEvent.KeyDown -> ObjConsoleKeyEvent(type = "keydown", key = key, code = code, ctrl = ctrl, alt = alt, shift = shift, meta = meta) is ConsoleEvent.KeyDown -> ObjConsoleKeyEvent(type = ConsoleEnums.KEY_DOWN, key = sanitizedKeyOrFallback(key), codeName = code, ctrl = ctrl, alt = alt, shift = shift, meta = meta)
is ConsoleEvent.KeyUp -> ObjConsoleKeyEvent(type = "keyup", key = key, code = code, ctrl = ctrl, alt = alt, shift = shift, meta = meta) is ConsoleEvent.KeyUp -> ObjConsoleKeyEvent(type = ConsoleEnums.KEY_UP, key = sanitizedKeyOrFallback(key), codeName = code, ctrl = ctrl, alt = alt, shift = shift, meta = meta)
}
private fun sanitizedKeyOrFallback(key: String): String {
if (key.isNotEmpty()) return key
consoleFlowDebug("console-bridge: empty key value received; using fallback key name")
return "Unknown"
}
private object ConsoleEnums {
lateinit var eventTypeClass: ObjEnumClass
private set
lateinit var keyCodeClass: ObjEnumClass
private set
lateinit var ansiLevelClass: ObjEnumClass
private set
private lateinit var eventEntries: Map<String, ObjEnumEntry>
private lateinit var keyCodeEntries: Map<String, ObjEnumEntry>
private lateinit var ansiLevelEntries: Map<String, ObjEnumEntry>
val UNKNOWN: ObjEnumEntry get() = event("UNKNOWN")
val RESIZE: ObjEnumEntry get() = event("RESIZE")
val KEY_DOWN: ObjEnumEntry get() = event("KEY_DOWN")
val KEY_UP: ObjEnumEntry get() = event("KEY_UP")
val CODE_UNKNOWN: ObjEnumEntry get() = code("UNKNOWN")
val CHARACTER: ObjEnumEntry get() = code("CHARACTER")
fun initialize(module: ModuleScope) {
eventTypeClass = resolveEnum(module, "ConsoleEventType")
keyCodeClass = resolveEnum(module, "ConsoleKeyCode")
ansiLevelClass = resolveEnum(module, "ConsoleAnsiLevel")
eventEntries = resolveEntries(
eventTypeClass,
listOf("UNKNOWN", "RESIZE", "KEY_DOWN", "KEY_UP")
)
keyCodeEntries = resolveEntries(
keyCodeClass,
listOf(
"UNKNOWN", "CHARACTER", "ARROW_UP", "ARROW_DOWN", "ARROW_LEFT", "ARROW_RIGHT",
"HOME", "END", "INSERT", "DELETE", "PAGE_UP", "PAGE_DOWN",
"ESCAPE", "ENTER", "TAB", "BACKSPACE", "SPACE"
)
)
ansiLevelEntries = resolveEntries(
ansiLevelClass,
listOf("NONE", "BASIC16", "ANSI256", "TRUECOLOR")
)
}
private fun resolveEnum(module: ModuleScope, enumName: String): ObjEnumClass {
val local = module.get(enumName)?.value as? ObjEnumClass
if (local != null) return local
val root = module.importProvider.rootScope.get(enumName)?.value as? ObjEnumClass
return root ?: error("lyng.io.console declaration enum is missing: $enumName")
}
private fun resolveEntries(enumClass: ObjEnumClass, names: List<String>): Map<String, ObjEnumEntry> {
return names.associateWith { name ->
(enumClass.byName[ObjString(name)] as? ObjEnumEntry)
?: error("lyng.io.console enum entry is missing: ${enumClass.className}.$name")
}
}
fun event(name: String): ObjEnumEntry = eventEntries[name]
?: error("lyng.io.console enum entry is missing: ${eventTypeClass.className}.$name")
fun code(name: String): ObjEnumEntry = keyCodeEntries[name]
?: error("lyng.io.console enum entry is missing: ${keyCodeClass.className}.$name")
fun ansiLevel(name: String): ObjEnumEntry = ansiLevelEntries[name]
?: error("lyng.io.console enum entry is missing: ${ansiLevelClass.className}.$name")
}
private val KEY_CODE_BY_KEY_NAME = mapOf(
"ArrowUp" to "ARROW_UP",
"ArrowDown" to "ARROW_DOWN",
"ArrowLeft" to "ARROW_LEFT",
"ArrowRight" to "ARROW_RIGHT",
"Home" to "HOME",
"End" to "END",
"Insert" to "INSERT",
"Delete" to "DELETE",
"PageUp" to "PAGE_UP",
"PageDown" to "PAGE_DOWN",
"Escape" to "ESCAPE",
"Enter" to "ENTER",
"Tab" to "TAB",
"Backspace" to "BACKSPACE",
" " to "SPACE",
)
private fun codeFrom(key: String, codeName: String?): ObjEnumEntry {
val resolved = KEY_CODE_BY_KEY_NAME[codeName ?: key]
return when {
resolved != null -> ConsoleEnums.code(resolved)
key.length == 1 -> ConsoleEnums.CHARACTER
else -> ConsoleEnums.CODE_UNKNOWN
}
} }
private abstract class ObjConsoleEventBase( private abstract class ObjConsoleEventBase(
private val eventType: String, private val type: ObjEnumEntry,
final override val objClass: net.sergeych.lyng.obj.ObjClass, final override val objClass: net.sergeych.lyng.obj.ObjClass,
) : Obj() { ) : Obj() {
fun eventTypeName(): String = eventType fun type(): ObjEnumEntry = type
} }
private class ObjConsoleEvent : ObjConsoleEventBase("event", type) { private class ObjConsoleEvent : ObjConsoleEventBase(ConsoleEnums.UNKNOWN, type) {
companion object { companion object {
val type = net.sergeych.lyng.obj.ObjClass("ConsoleEvent").apply { val type = net.sergeych.lyng.obj.ObjClass("ConsoleEvent").apply {
addPropertyDoc( addProperty(name = "type", getter = { (this.thisObj as ObjConsoleEventBase).type() })
name = "type",
doc = "Event type string: resize, keydown, keyup.",
type = type("lyng.String"),
moduleName = "lyng.io.console",
getter = { ObjString((this.thisObj as ObjConsoleEventBase).eventTypeName()) }
)
} }
} }
} }
@@ -450,83 +423,40 @@ private class ObjConsoleEvent : ObjConsoleEventBase("event", type) {
private class ObjConsoleResizeEvent( private class ObjConsoleResizeEvent(
val columns: Int, val columns: Int,
val rows: Int, val rows: Int,
) : ObjConsoleEventBase("resize", type) { ) : ObjConsoleEventBase(ConsoleEnums.RESIZE, type) {
companion object { companion object {
val type = net.sergeych.lyng.obj.ObjClass("ConsoleResizeEvent", ObjConsoleEvent.type).apply { val type = net.sergeych.lyng.obj.ObjClass("ConsoleResizeEvent", ObjConsoleEvent.type).apply {
addPropertyDoc( addProperty(name = "columns", getter = { (this.thisObj as ObjConsoleResizeEvent).columns.toObj() })
name = "columns", addProperty(name = "rows", getter = { (this.thisObj as ObjConsoleResizeEvent).rows.toObj() })
doc = "Terminal width in character cells.",
type = type("lyng.Int"),
moduleName = "lyng.io.console",
getter = { (this.thisObj as ObjConsoleResizeEvent).columns.toObj() }
)
addPropertyDoc(
name = "rows",
doc = "Terminal height in character cells.",
type = type("lyng.Int"),
moduleName = "lyng.io.console",
getter = { (this.thisObj as ObjConsoleResizeEvent).rows.toObj() }
)
} }
} }
} }
private class ObjConsoleKeyEvent( private class ObjConsoleKeyEvent(
type: String, type: ObjEnumEntry,
val key: String, val key: String,
val code: String?, val codeName: String?,
val ctrl: Boolean, val ctrl: Boolean,
val alt: Boolean, val alt: Boolean,
val shift: Boolean, val shift: Boolean,
val meta: Boolean, val meta: Boolean,
) : ObjConsoleEventBase(type, typeObj) { ) : ObjConsoleEventBase(type, typeObj) {
init {
require(key.isNotEmpty()) { "ConsoleKeyEvent.key must never be empty" }
}
companion object { companion object {
val typeObj = net.sergeych.lyng.obj.ObjClass("ConsoleKeyEvent", ObjConsoleEvent.type).apply { val typeObj = net.sergeych.lyng.obj.ObjClass("ConsoleKeyEvent", ObjConsoleEvent.type).apply {
addPropertyDoc( addProperty(name = "key", getter = { ObjString((this.thisObj as ObjConsoleKeyEvent).key) })
name = "key", addProperty(name = "code", getter = { codeFrom((this.thisObj as ObjConsoleKeyEvent).key, (this.thisObj as ObjConsoleKeyEvent).codeName) })
doc = "Logical key name (e.g. ArrowLeft, a, Escape).", addProperty(name = "codeName", getter = {
type = type("lyng.String"), val code = (this.thisObj as ObjConsoleKeyEvent).codeName
moduleName = "lyng.io.console",
getter = { ObjString((this.thisObj as ObjConsoleKeyEvent).key) }
)
addPropertyDoc(
name = "code",
doc = "Optional hardware/code identifier.",
type = type("lyng.String", nullable = true),
moduleName = "lyng.io.console",
getter = {
val code = (this.thisObj as ObjConsoleKeyEvent).code
code?.let(::ObjString) ?: ObjNull code?.let(::ObjString) ?: ObjNull
} })
) addProperty(name = "ctrl", getter = { (this.thisObj as ObjConsoleKeyEvent).ctrl.toObj() })
addPropertyDoc( addProperty(name = "alt", getter = { (this.thisObj as ObjConsoleKeyEvent).alt.toObj() })
name = "ctrl", addProperty(name = "shift", getter = { (this.thisObj as ObjConsoleKeyEvent).shift.toObj() })
doc = "Whether Ctrl modifier is pressed.", addProperty(name = "meta", getter = { (this.thisObj as ObjConsoleKeyEvent).meta.toObj() })
type = type("lyng.Bool"),
moduleName = "lyng.io.console",
getter = { (this.thisObj as ObjConsoleKeyEvent).ctrl.toObj() }
)
addPropertyDoc(
name = "alt",
doc = "Whether Alt modifier is pressed.",
type = type("lyng.Bool"),
moduleName = "lyng.io.console",
getter = { (this.thisObj as ObjConsoleKeyEvent).alt.toObj() }
)
addPropertyDoc(
name = "shift",
doc = "Whether Shift modifier is pressed.",
type = type("lyng.Bool"),
moduleName = "lyng.io.console",
getter = { (this.thisObj as ObjConsoleKeyEvent).shift.toObj() }
)
addPropertyDoc(
name = "meta",
doc = "Whether Meta/Super modifier is pressed.",
type = type("lyng.Bool"),
moduleName = "lyng.io.console",
getter = { (this.thisObj as ObjConsoleKeyEvent).meta.toObj() }
)
} }
} }
} }
@@ -539,20 +469,8 @@ private class ObjConsoleGeometry(
companion object { companion object {
val type = net.sergeych.lyng.obj.ObjClass("ConsoleGeometry").apply { val type = net.sergeych.lyng.obj.ObjClass("ConsoleGeometry").apply {
addPropertyDoc( addProperty(name = "columns", getter = { (this.thisObj as ObjConsoleGeometry).columns.toObj() })
name = "columns", addProperty(name = "rows", getter = { (this.thisObj as ObjConsoleGeometry).rows.toObj() })
doc = "Terminal width in character cells.",
type = type("lyng.Int"),
moduleName = "lyng.io.console",
getter = { (this.thisObj as ObjConsoleGeometry).columns.toObj() }
)
addPropertyDoc(
name = "rows",
doc = "Terminal height in character cells.",
type = type("lyng.Int"),
moduleName = "lyng.io.console",
getter = { (this.thisObj as ObjConsoleGeometry).rows.toObj() }
)
} }
} }
} }
@@ -560,41 +478,17 @@ private class ObjConsoleGeometry(
private class ObjConsoleDetails( private class ObjConsoleDetails(
val supported: Boolean, val supported: Boolean,
val isTty: Boolean, val isTty: Boolean,
val ansiLevel: String, val ansiLevel: ObjEnumEntry,
val geometry: ObjConsoleGeometry?, val geometry: ObjConsoleGeometry?,
) : Obj() { ) : Obj() {
override val objClass: net.sergeych.lyng.obj.ObjClass get() = type override val objClass: net.sergeych.lyng.obj.ObjClass get() = type
companion object { companion object {
val type = net.sergeych.lyng.obj.ObjClass("ConsoleDetails").apply { val type = net.sergeych.lyng.obj.ObjClass("ConsoleDetails").apply {
addPropertyDoc( addProperty(name = "supported", getter = { (this.thisObj as ObjConsoleDetails).supported.toObj() })
name = "supported", addProperty(name = "isTty", getter = { (this.thisObj as ObjConsoleDetails).isTty.toObj() })
doc = "Whether console API is supported.", addProperty(name = "ansiLevel", getter = { (this.thisObj as ObjConsoleDetails).ansiLevel })
type = type("lyng.Bool"), addProperty(name = "geometry", getter = { (this.thisObj as ObjConsoleDetails).geometry ?: ObjNull })
moduleName = "lyng.io.console",
getter = { (this.thisObj as ObjConsoleDetails).supported.toObj() }
)
addPropertyDoc(
name = "isTty",
doc = "Whether output is connected to a TTY.",
type = type("lyng.Bool"),
moduleName = "lyng.io.console",
getter = { (this.thisObj as ObjConsoleDetails).isTty.toObj() }
)
addPropertyDoc(
name = "ansiLevel",
doc = "Detected ANSI color capability level.",
type = type("lyng.String"),
moduleName = "lyng.io.console",
getter = { ObjString((this.thisObj as ObjConsoleDetails).ansiLevel) }
)
addPropertyDoc(
name = "geometry",
doc = "Current terminal geometry or null.",
type = type("ConsoleGeometry", nullable = true),
moduleName = "lyng.io.console",
getter = { (this.thisObj as ObjConsoleDetails).geometry ?: ObjNull }
)
} }
} }
} }
@@ -100,8 +100,9 @@ object MordantLyngConsole : LyngConsole {
var running = true var running = true
globalLaunch { globalLaunch {
var lastWidth = t.updateSize().width val initialSize = runCatching { t.updateSize() }.getOrNull()
var lastHeight = t.updateSize().height var lastWidth = initialSize?.width ?: 0
var lastHeight = initialSize?.height ?: 0
val startMark = TimeSource.Monotonic.markNow() val startMark = TimeSource.Monotonic.markNow()
var lastHeartbeatMark = startMark var lastHeartbeatMark = startMark
var loops = 0L var loops = 0L
@@ -113,6 +114,18 @@ object MordantLyngConsole : LyngConsole {
var lastKeyMark = startMark var lastKeyMark = startMark
var lastRawRecoveryMark = startMark var lastRawRecoveryMark = startMark
fun tryEmitResize(width: Int, height: Int) {
if (width < 1 || height < 1) {
consoleFlowDebug("events: ignored invalid resize width=$width height=$height")
return
}
if (width == lastWidth && height == lastHeight) return
out.trySend(ConsoleEvent.Resize(width, height))
lastWidth = width
lastHeight = height
resizeEvents += 1
}
consoleFlowDebug("events: collector started") consoleFlowDebug("events: collector started")
try { try {
while (currentCoroutineContext().isActive && sourceState.withLock { running }) { while (currentCoroutineContext().isActive && sourceState.withLock { running }) {
@@ -122,12 +135,7 @@ object MordantLyngConsole : LyngConsole {
delay(150) delay(150)
continue continue
} }
if (currentSize.width != lastWidth || currentSize.height != lastHeight) { tryEmitResize(currentSize.width, currentSize.height)
out.trySend(ConsoleEvent.Resize(currentSize.width, currentSize.height))
lastWidth = currentSize.width
lastHeight = currentSize.height
resizeEvents += 1
}
val raw = stateMutex.withLock { val raw = stateMutex.withLock {
if (!rawModeRequested) { if (!rawModeRequested) {
@@ -173,10 +181,8 @@ object MordantLyngConsole : LyngConsole {
val ev = readResult.getOrNull() val ev = readResult.getOrNull()
val resized = runCatching { t.updateSize() }.getOrNull() val resized = runCatching { t.updateSize() }.getOrNull()
if (resized != null && (resized.width != lastWidth || resized.height != lastHeight)) { if (resized != null) {
out.trySend(ConsoleEvent.Resize(resized.width, resized.height)) tryEmitResize(resized.width, resized.height)
lastWidth = resized.width
lastHeight = resized.height
} }
when (ev) { when (ev) {
@@ -17,248 +17,12 @@
package net.sergeych.lyngio.docs package net.sergeych.lyngio.docs
import net.sergeych.lyng.miniast.BuiltinDocRegistry /**
import net.sergeych.lyng.miniast.ParamDoc * Console docs are declared in `lyngio/stdlib/lyng/io/console.lyng`.
import net.sergeych.lyng.miniast.type * Keep this shim for compatibility with reflective loaders.
*/
object ConsoleBuiltinDocs { object ConsoleBuiltinDocs {
private var registered = false
fun ensure() { fun ensure() {
if (registered) return // No Kotlin-side doc registration: console.lyng is the source of truth.
BuiltinDocRegistry.module("lyng.io.console") {
classDoc(
name = "Console",
doc = "Console runtime API."
) {
method(
name = "isSupported",
doc = "Whether console control API is supported on this platform.",
returns = type("lyng.Bool"),
isStatic = true
)
method(
name = "isTty",
doc = "Whether stdout is attached to an interactive TTY.",
returns = type("lyng.Bool"),
isStatic = true
)
method(
name = "ansiLevel",
doc = "Detected ANSI color capability: NONE, BASIC16, ANSI256, TRUECOLOR.",
returns = type("lyng.String"),
isStatic = true
)
method(
name = "geometry",
doc = "Current terminal geometry or null.",
returns = type("ConsoleGeometry", nullable = true),
isStatic = true
)
method(
name = "details",
doc = "Get consolidated console details.",
returns = type("ConsoleDetails"),
isStatic = true
)
method(
name = "write",
doc = "Write text directly to console output.",
params = listOf(ParamDoc("text", type("lyng.String"))),
isStatic = true
)
method(
name = "flush",
doc = "Flush console output buffer.",
isStatic = true
)
method(
name = "home",
doc = "Move cursor to home position (1,1).",
isStatic = true
)
method(
name = "clear",
doc = "Clear the visible screen buffer.",
isStatic = true
)
method(
name = "moveTo",
doc = "Move cursor to 1-based row and column.",
params = listOf(
ParamDoc("row", type("lyng.Int")),
ParamDoc("column", type("lyng.Int")),
),
isStatic = true
)
method(
name = "clearLine",
doc = "Clear the current line.",
isStatic = true
)
method(
name = "enterAltScreen",
doc = "Switch to terminal alternate screen buffer.",
isStatic = true
)
method(
name = "leaveAltScreen",
doc = "Return from alternate screen buffer to normal screen.",
isStatic = true
)
method(
name = "setCursorVisible",
doc = "Show or hide the terminal cursor.",
params = listOf(ParamDoc("visible", type("lyng.Bool"))),
isStatic = true
)
method(
name = "events",
doc = "Endless iterable console event source (resize, keydown, keyup). Use in a loop, often inside launch.",
returns = type("ConsoleEventStream"),
isStatic = true
)
method(
name = "setRawMode",
doc = "Enable or disable raw keyboard mode. Returns true if mode changed.",
params = listOf(ParamDoc("enabled", type("lyng.Bool"))),
returns = type("lyng.Bool"),
isStatic = true
)
}
classDoc(
name = "ConsoleEventStream",
doc = "Endless iterable stream of console events."
) {
method(
name = "iterator",
doc = "Create an iterator over incoming console events.",
returns = type("lyng.Iterator")
)
}
classDoc(
name = "ConsoleGeometry",
doc = "Terminal geometry."
) {
field(
name = "columns",
doc = "Terminal width in character cells.",
type = type("lyng.Int")
)
field(
name = "rows",
doc = "Terminal height in character cells.",
type = type("lyng.Int")
)
}
classDoc(
name = "ConsoleDetails",
doc = "Consolidated console capability details."
) {
field(
name = "supported",
doc = "Whether console API is supported.",
type = type("lyng.Bool")
)
field(
name = "isTty",
doc = "Whether output is attached to a TTY.",
type = type("lyng.Bool")
)
field(
name = "ansiLevel",
doc = "Detected ANSI color capability.",
type = type("lyng.String")
)
field(
name = "geometry",
doc = "Current geometry or null.",
type = type("ConsoleGeometry", nullable = true)
)
}
classDoc(
name = "ConsoleEvent",
doc = "Base class for console events."
) {
field(
name = "type",
doc = "Event kind string.",
type = type("lyng.String")
)
}
classDoc(
name = "ConsoleResizeEvent",
doc = "Resize event."
) {
field(
name = "type",
doc = "Event kind string: resize.",
type = type("lyng.String")
)
field(
name = "columns",
doc = "Terminal width in character cells.",
type = type("lyng.Int")
)
field(
name = "rows",
doc = "Terminal height in character cells.",
type = type("lyng.Int")
)
}
classDoc(
name = "ConsoleKeyEvent",
doc = "Keyboard event."
) {
field(
name = "type",
doc = "Event kind string: keydown or keyup.",
type = type("lyng.String")
)
field(
name = "key",
doc = "Logical key name.",
type = type("lyng.String")
)
field(
name = "code",
doc = "Optional hardware code.",
type = type("lyng.String", nullable = true)
)
field(
name = "ctrl",
doc = "Ctrl modifier state.",
type = type("lyng.Bool")
)
field(
name = "alt",
doc = "Alt modifier state.",
type = type("lyng.Bool")
)
field(
name = "shift",
doc = "Shift modifier state.",
type = type("lyng.Bool")
)
field(
name = "meta",
doc = "Meta modifier state.",
type = type("lyng.Bool")
)
}
valDoc(
name = "Console",
doc = "Console runtime API.",
type = type("Console")
)
valDoc(name = "ConsoleGeometry", doc = "Terminal geometry class.", type = type("lyng.Class"))
valDoc(name = "ConsoleDetails", doc = "Console details class.", type = type("lyng.Class"))
valDoc(name = "ConsoleEvent", doc = "Base console event class.", type = type("lyng.Class"))
valDoc(name = "ConsoleResizeEvent", doc = "Resize event class.", type = type("lyng.Class"))
valDoc(name = "ConsoleKeyEvent", doc = "Keyboard event class.", type = type("lyng.Class"))
valDoc(name = "ConsoleEventStream", doc = "Iterable console event stream class.", type = type("lyng.Class"))
}
registered = true
} }
} }
@@ -26,6 +26,7 @@ import org.jline.terminal.Terminal
import org.jline.terminal.TerminalBuilder import org.jline.terminal.TerminalBuilder
import org.jline.utils.NonBlockingReader import org.jline.utils.NonBlockingReader
import java.io.EOFException import java.io.EOFException
import java.io.InterruptedIOException
import java.util.* import java.util.*
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicLong
@@ -41,7 +42,7 @@ import kotlin.time.TimeSource
* to avoid dual-terminal contention. * to avoid dual-terminal contention.
*/ */
object JvmLyngConsole : LyngConsole { object JvmLyngConsole : LyngConsole {
private const val DEBUG_REVISION = "jline-r26-force-rebuild-on-noop-recovery-2026-03-19" private const val DEBUG_REVISION = "jline-r27-no-close-on-vm-iterator-cancel-2026-03-19"
private val codeSourceLocation: String by lazy { private val codeSourceLocation: String by lazy {
runCatching { runCatching {
JvmLyngConsole::class.java.protectionDomain?.codeSource?.location?.toString() JvmLyngConsole::class.java.protectionDomain?.codeSource?.location?.toString()
@@ -50,6 +51,44 @@ object JvmLyngConsole : LyngConsole {
private val terminalRef = AtomicReference<Terminal?>(null) private val terminalRef = AtomicReference<Terminal?>(null)
private val terminalInitLock = Any() private val terminalInitLock = Any()
private val shutdownHook = Thread(
{
restoreTerminalStateOnShutdown()
},
"lyng-console-shutdown"
).apply { isDaemon = true }
init {
runCatching { Runtime.getRuntime().addShutdownHook(shutdownHook) }
.onFailure { consoleFlowDebug("jline-events: shutdown hook install failed", it) }
}
private fun restoreTerminalStateOnShutdown() {
val term = terminalRef.get() ?: return
runCatching {
term.writer().print("\u001B[?25h")
term.writer().print("\u001B[?1049l")
term.writer().flush()
}.onFailure {
consoleFlowDebug("jline-events: shutdown visual restore failed", it)
}
val saved = if (runCatching { stateMutex.tryLock() }.getOrNull() == true) {
try {
rawModeRequested = false
val s = rawSavedAttributes
rawSavedAttributes = null
s
} finally {
stateMutex.unlock()
}
} else {
null
}
if (saved != null) {
runCatching { term.setAttributes(saved) }
.onFailure { consoleFlowDebug("jline-events: shutdown raw attrs restore failed", it) }
}
}
private fun currentTerminal(): Terminal? { private fun currentTerminal(): Terminal? {
val existing = terminalRef.get() val existing = terminalRef.get()
@@ -176,10 +215,29 @@ object JvmLyngConsole : LyngConsole {
var reader = activeTerm.reader() var reader = activeTerm.reader()
var keyThread: Thread? = null var keyThread: Thread? = null
var heartbeatThread: Thread? = null var heartbeatThread: Thread? = null
val resizeEmitMutex = Any()
var lastResizeCols = Int.MIN_VALUE
var lastResizeRows = Int.MIN_VALUE
fun emitResize() { fun emitResize() {
val size = runCatching { activeTerm.size }.getOrNull() ?: return val size = runCatching { activeTerm.size }.getOrNull() ?: return
out.trySend(ConsoleEvent.Resize(size.columns, size.rows)) val cols = size.columns
val rows = size.rows
if (cols < 1 || rows < 1) {
consoleFlowDebug("jline-events: ignored invalid resize columns=$cols rows=$rows")
return
}
val shouldEmit = synchronized(resizeEmitMutex) {
if (cols == lastResizeCols && rows == lastResizeRows) {
false
} else {
lastResizeCols = cols
lastResizeRows = rows
true
}
}
if (!shouldEmit) return
out.trySend(ConsoleEvent.Resize(cols, rows))
} }
fun cleanup() { fun cleanup() {
@@ -270,13 +328,13 @@ object JvmLyngConsole : LyngConsole {
.onFailure { consoleFlowDebug("jline-events: reader shutdown failed during recovery", it) } .onFailure { consoleFlowDebug("jline-events: reader shutdown failed during recovery", it) }
reader = activeTerm.reader() reader = activeTerm.reader()
if (reader.hashCode() == prevReader.hashCode()) { if (reader === prevReader) {
consoleFlowDebug("jline-events: reader recovery no-op oldReader=${prevReader.hashCode()} newReader=${reader.hashCode()} -> forcing terminal rebuild") consoleFlowDebug("jline-events: reader recovery no-op oldReader=${System.identityHashCode(prevReader)} newReader=${System.identityHashCode(reader)} -> forcing terminal rebuild")
if (!tryRebuildTerminal()) { if (!tryRebuildTerminal()) {
consoleFlowDebug("jline-events: forced terminal rebuild did not produce a new reader") consoleFlowDebug("jline-events: forced terminal rebuild did not produce a new reader")
} }
} else { } else {
consoleFlowDebug("jline-events: reader recovered oldReader=${prevReader.hashCode()} newReader=${reader.hashCode()}") consoleFlowDebug("jline-events: reader recovered oldReader=${System.identityHashCode(prevReader)} newReader=${System.identityHashCode(reader)}")
} }
readerRecoveries.incrementAndGet() readerRecoveries.incrementAndGet()
@@ -305,18 +363,56 @@ object JvmLyngConsole : LyngConsole {
} else { } else {
keySendFailures.incrementAndGet() keySendFailures.incrementAndGet()
} }
} catch (_: InterruptedException) { } catch (e: InterruptedException) {
break // Keep input alive if this is a transient interrupt while still running.
if (!running.get() || !keyLoopRunning.get()) break
recoveryRequested.set(true)
consoleFlowDebug("jline-events: key-reader interrupted; scheduling reader recovery", e)
Thread.interrupted()
continue
} catch (e: InterruptedIOException) {
// Common during reader shutdown/rebind. Recover silently and keep input flowing.
if (!running.get() || !keyLoopRunning.get()) break
recoveryRequested.set(true)
consoleFlowDebug("jline-events: read interrupted; scheduling reader recovery", e)
try {
Thread.sleep(10)
} catch (ie: InterruptedException) {
if (!running.get() || !keyLoopRunning.get()) break
recoveryRequested.set(true)
consoleFlowDebug("jline-events: interrupted during recovery backoff; continuing", ie)
Thread.interrupted()
}
} catch (e: EOFException) {
// EOF from reader should trigger rebind/rebuild rather than ending input stream.
if (!running.get() || !keyLoopRunning.get()) break
recoveryRequested.set(true)
consoleFlowDebug("jline-events: reader EOF; scheduling reader recovery", e)
try {
Thread.sleep(20)
} catch (ie: InterruptedException) {
if (!running.get() || !keyLoopRunning.get()) break
recoveryRequested.set(true)
consoleFlowDebug("jline-events: interrupted during EOF backoff; continuing", ie)
Thread.interrupted()
}
} catch (e: Throwable) { } catch (e: Throwable) {
readFailures.incrementAndGet() readFailures.incrementAndGet()
recoveryRequested.set(true)
consoleFlowDebug("jline-events: blocking read failed", e) consoleFlowDebug("jline-events: blocking read failed", e)
try { try {
Thread.sleep(50) Thread.sleep(50)
} catch (_: InterruptedException) { } catch (ie: InterruptedException) {
break if (!running.get() || !keyLoopRunning.get()) break
recoveryRequested.set(true)
consoleFlowDebug("jline-events: interrupted during error backoff; continuing", ie)
Thread.interrupted()
} }
} }
} }
consoleFlowDebug(
"jline-events: key-reader thread stopped running=${running.get()} keyLoopRunning=${keyLoopRunning.get()} loops=${keyLoopCount.get()} keys=${keyEvents.get()} readFailures=${readFailures.get()}"
)
} }
heartbeatThread = thread(start = true, isDaemon = true, name = "lyng-jline-heartbeat") { heartbeatThread = thread(start = true, isDaemon = true, name = "lyng-jline-heartbeat") {
@@ -334,13 +430,19 @@ object JvmLyngConsole : LyngConsole {
val readBlockedMs = if (readStartNs > 0L && readEndNs < readStartNs) { val readBlockedMs = if (readStartNs > 0L && readEndNs < readStartNs) {
(System.nanoTime() - readStartNs) / 1_000_000L (System.nanoTime() - readStartNs) / 1_000_000L
} else 0L } else 0L
if (requested && keyCodesRead.get() > 0L && idleMs >= 1400L) { val streamIdle = requested && keyCodesRead.get() > 0L && idleMs >= 1400L
val readStalled = requested && readBlockedMs >= 1600L
if (streamIdle || readStalled) {
val sinceRecoveryMs = (System.nanoTime() - lastRecoveryNs.get()) / 1_000_000L val sinceRecoveryMs = (System.nanoTime() - lastRecoveryNs.get()) / 1_000_000L
if (sinceRecoveryMs >= 1200L) { if (sinceRecoveryMs >= 1200L) {
recoveryRequested.set(true) recoveryRequested.set(true)
if (readStalled) {
consoleFlowDebug("jline-events: key read blocked ${readBlockedMs}ms; scheduling reader recovery")
} else {
consoleFlowDebug("jline-events: key stream idle ${idleMs}ms; scheduling reader recovery") consoleFlowDebug("jline-events: key stream idle ${idleMs}ms; scheduling reader recovery")
} }
} }
}
consoleFlowDebug( consoleFlowDebug(
"jline-events: heartbeat keyCodes=${keyCodesRead.get()} keysSent=${keyEvents.get()} sendFailures=${keySendFailures.get()} readFailures=${readFailures.get()} recoveries=${readerRecoveries.get()} rawRequested=$requested keyLoop=${keyLoopCount.get()} readBlockedMs=$readBlockedMs keyIdleMs=$idleMs keyPath=reader" "jline-events: heartbeat keyCodes=${keyCodesRead.get()} keysSent=${keyEvents.get()} sendFailures=${keySendFailures.get()} readFailures=${readFailures.get()} recoveries=${readerRecoveries.get()} rawRequested=$requested keyLoop=${keyLoopCount.get()} readBlockedMs=$readBlockedMs keyIdleMs=$idleMs keyPath=reader"
) )
@@ -366,6 +468,7 @@ object JvmLyngConsole : LyngConsole {
} }
override suspend fun close() { override suspend fun close() {
consoleFlowDebug("jline-events: collector close requested", Throwable("collector close caller"))
cleanup() cleanup()
consoleFlowDebug( consoleFlowDebug(
"jline-events: collector ended keys=${keyEvents.get()} readFailures=${readFailures.get()}" "jline-events: collector ended keys=${keyEvents.get()} readFailures=${readFailures.get()}"
@@ -485,7 +588,9 @@ object JvmLyngConsole : LyngConsole {
ctrl: Boolean = false, ctrl: Boolean = false,
alt: Boolean = false, alt: Boolean = false,
shift: Boolean = false, shift: Boolean = false,
): ConsoleEvent.KeyDown = ConsoleEvent.KeyDown( ): ConsoleEvent.KeyDown {
require(value.isNotEmpty()) { "ConsoleEvent.KeyDown.key must never be empty" }
return ConsoleEvent.KeyDown(
key = value, key = value,
code = null, code = null,
ctrl = ctrl, ctrl = ctrl,
@@ -493,4 +598,5 @@ object JvmLyngConsole : LyngConsole {
shift = shift, shift = shift,
meta = false meta = false
) )
}
} }
@@ -56,7 +56,7 @@ class LyngConsoleModuleTest {
val d = Console.details() val d = Console.details()
assert(d.supported is Bool) assert(d.supported is Bool)
assert(d.isTty is Bool) assert(d.isTty is Bool)
assert(d.ansiLevel is String) assert(d.ansiLevel is ConsoleAnsiLevel)
val g = Console.geometry() val g = Console.geometry()
if (g != null) { if (g != null) {
+157
View File
@@ -0,0 +1,157 @@
package lyng.io.console
/* Console event kinds used by `ConsoleEvent.type`. */
enum ConsoleEventType {
UNKNOWN,
RESIZE,
KEY_DOWN,
KEY_UP
}
/* Normalized key codes used by `ConsoleKeyEvent.code`. */
enum ConsoleKeyCode {
UNKNOWN,
CHARACTER,
ARROW_UP,
ARROW_DOWN,
ARROW_LEFT,
ARROW_RIGHT,
HOME,
END,
INSERT,
DELETE,
PAGE_UP,
PAGE_DOWN,
ESCAPE,
ENTER,
TAB,
BACKSPACE,
SPACE
}
/* Detected ANSI terminal capability level. */
enum ConsoleAnsiLevel {
NONE,
BASIC16,
ANSI256,
TRUECOLOR
}
/* Base class for console events. */
extern class ConsoleEvent {
/* Event kind for stable matching/switching. */
val type: ConsoleEventType
}
/* Terminal resize event. */
extern class ConsoleResizeEvent : ConsoleEvent {
/* Current terminal width in character cells. */
val columns: Int
/* Current terminal height in character cells. */
val rows: Int
}
/* Keyboard event. */
extern class ConsoleKeyEvent : ConsoleEvent {
/*
Logical key name normalized for app-level handling, for example:
"a", "A", "ArrowLeft", "Escape", "Enter".
*/
val key: String
/* Normalized key code enum for robust matching independent of backend specifics. */
val code: ConsoleKeyCode
/*
Optional backend-specific raw identifier (if available).
Not guaranteed to be present or stable across platforms.
*/
val codeName: String?
/* True when Ctrl was pressed during the key event. */
val ctrl: Bool
/* True when Alt/Option was pressed during the key event. */
val alt: Bool
/* True when Shift was pressed during the key event. */
val shift: Bool
/* True when Meta/Super/Command was pressed during the key event. */
val meta: Bool
}
/* Pull iterator over console events. */
extern class ConsoleEventIterator : Iterator<ConsoleEvent> {
/* Whether another event is currently available from the stream. */
override fun hasNext(): Bool
/* Returns next event or throws iteration-finished when exhausted/cancelled. */
override fun next(): ConsoleEvent
/* Stops this iterator. The underlying console service remains managed by runtime. */
override fun cancelIteration(): void
}
/* Endless iterable console event stream. */
extern class ConsoleEventStream : Iterable<ConsoleEvent> {
/* Creates a fresh event iterator bound to the current console input stream. */
override fun iterator(): ConsoleEventIterator
}
/* Terminal geometry in character cells. */
extern class ConsoleGeometry {
val columns: Int
val rows: Int
}
/* Snapshot of console support/capabilities. */
extern class ConsoleDetails {
/* True when current runtime has console control implementation. */
val supported: Bool
/* True when output/input are attached to an interactive terminal. */
val isTty: Bool
/* Detected terminal color capability level. */
val ansiLevel: ConsoleAnsiLevel
/* Current terminal size if available, otherwise null. */
val geometry: ConsoleGeometry?
}
/* Console API singleton object. */
extern object Console {
/* Returns true when console control API is implemented in this runtime. */
fun isSupported(): Bool
/* Returns true when process is attached to interactive TTY. */
fun isTty(): Bool
/* Returns detected color capability level. */
fun ansiLevel(): ConsoleAnsiLevel
/* Returns current terminal geometry, or null when unavailable. */
fun geometry(): ConsoleGeometry?
/* Returns combined capability snapshot in one call. */
fun details(): ConsoleDetails
/* Writes raw text to console output buffer (no implicit newline). */
fun write(text: String): void
/* Flushes pending console output. Call after batched writes. */
fun flush(): void
/* Moves cursor to home position (row 1, column 1). */
fun home(): void
/* Clears visible screen buffer. Cursor position is backend-dependent after clear. */
fun clear(): void
/* Moves cursor to 1-based row/column. Values outside viewport are backend-defined. */
fun moveTo(row: Int, column: Int): void
/* Clears current line content. Cursor stays on the same line. */
fun clearLine(): void
/* Switches terminal into alternate screen buffer (useful for TUIs). */
fun enterAltScreen(): void
/* Returns from alternate screen buffer to the normal terminal screen. */
fun leaveAltScreen(): void
/* Shows or hides the cursor. Prefer restoring visibility in finally blocks. */
fun setCursorVisible(visible: Bool): void
/*
Returns endless event stream (resize + key events).
Typical usage is consuming in a launched loop.
*/
fun events(): ConsoleEventStream
/*
Enables/disables raw keyboard mode.
Returns true when state was actually changed.
*/
fun setRawMode(enabled: Bool): Bool
}
+1 -1
View File
@@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych" group = "net.sergeych"
version = "1.5.0-RC" version = "1.5.1"
// Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below // 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 == '$' } val idFirstChars = { d: Char -> d.isLetter() || d == '_' || d == '$' }
fun parseLyng(source: Source): List<Token> { 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>() val tokens = mutableListOf<Token>()
do { do {
val t = p.nextToken() val t = p.nextToken()
@@ -35,9 +38,70 @@ fun parseLyng(source: Source): List<Token> {
return tokens 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 pos = MutablePos(fromPos)
private val bufferedTokens = ArrayDeque<Token>()
/** /**
* Immutable copy of current position * Immutable copy of current position
@@ -47,6 +111,7 @@ private class Parser(fromPos: Pos) {
private fun raise(msg: String): Nothing = throw ScriptError(currentPos, msg) private fun raise(msg: String): Nothing = throw ScriptError(currentPos, msg)
fun nextToken(): Token { fun nextToken(): Token {
if (bufferedTokens.isNotEmpty()) return bufferedTokens.removeFirst()
skipws() skipws()
if (pos.end) return Token("", currentPos, Token.Type.EOF) if (pos.end) return Token("", currentPos, Token.Type.EOF)
val from = currentPos val from = currentPos
@@ -296,7 +361,7 @@ private class Parser(fromPos: Pos) {
Token(":", from, Token.Type.COLON) Token(":", from, Token.Type.COLON)
} }
'"' -> loadStringToken() '"' -> loadStringTokens(from)
in digitsSet -> { in digitsSet -> {
pos.back() pos.back()
@@ -485,11 +550,6 @@ private class Parser(fromPos: Pos) {
private fun loadStringToken(): Token { private fun loadStringToken(): Token {
val start = currentPos val start = currentPos
// if (currentChar == '"') pos.advance()
// else start = start.back()
// start = start.back()
val sb = StringBuilder() val sb = StringBuilder()
var newlineDetected = false var newlineDetected = false
while (currentChar != '"') { while (currentChar != '"') {
@@ -548,6 +608,262 @@ private class Parser(fromPos: Pos) {
return Token(result, start, Token.Type.STRING) 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 { private fun loadUnicodeEscape(start: Pos): Char {
// Called when currentChar points to 'u' right after a backslash. // Called when currentChar points to 'u' right after a backslash.
if (currentChar != 'u') throw ScriptError(currentPos, "expected unicode escape marker: u") if (currentChar != 'u') throw ScriptError(currentPos, "expected unicode escape marker: u")
@@ -114,7 +114,7 @@ class ObjectExpressionTest {
eval(""" eval("""
val x = object { } val x = object { }
val name = ((x::class as Class).className as String) val name = ((x::class as Class).className as String)
assert(name.startsWith("${'$'}Anon_")) assert(name.startsWith("\${'$'}Anon_"))
""".trimIndent()) """.trimIndent())
} }
} }
+37 -20
View File
@@ -4194,26 +4194,43 @@ class ScriptTest {
// } // }
// @Ignore @Test
// @Test fun interpolationTest() = runTest {
// fun interpolationTest() = runTest { val d = '$'
// eval($$$""" eval(
// """
// val foo = "bar" val foo = "bar"
// val buzz = ["foo", "bar"] val buzz = ["foo", "bar"]
//
// // 1. simple interpolation // 1. simple interpolation
// assertEquals( "bar", "$foo" ) assertEquals("bar", "${d}foo")
// assertEquals( "bar", "${foo}" ) assertEquals("bar", "${d}{foo}")
//
// // 2. escaping the dollar sign // 2. escaping / literal dollar forms
// assertEquals( "$", "\$foo"[0] ) assertEquals("${d}${d}foo", "\${d}foo")
// assertEquals( "foo, "\$foo"[1..] ) assertEquals("${d}${d}foo", "${d}${d}foo")
// assertEquals("\\bar", "\\${d}foo")
// // 3. interpolation with expression
// assertEquals( "foo!. bar?", "${buzz[0]+"!"}. ${buzz[1]+"?"}" ) // 3. interpolation with expression
// """.trimIndent()) 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 @Test
fun testInlineArrayLiteral() = runTest { fun testInlineArrayLiteral() = runTest {
+1
View File
@@ -81,6 +81,7 @@ extern fun abs(x: Object): Real
extern fun ln(x: Object): Real extern fun ln(x: Object): Real
extern fun pow(x: Object, y: Object): Real extern fun pow(x: Object, y: Object): Real
extern fun sqrt(x: Object): Real extern fun sqrt(x: Object): Real
extern fun clamp<T>(value: T, range: Range<T>): T
class SeededRandom { class SeededRandom {
extern fun nextInt(): Int extern fun nextInt(): Int
+1 -1
View File
@@ -339,7 +339,7 @@
<!-- Top-left version ribbon --> <!-- Top-left version ribbon -->
<div class="corner-ribbon bg-danger text-white"> <div class="corner-ribbon bg-danger text-white">
<span style="margin-left: -5em"> <span style="margin-left: -5em">
v1.5.0-RC v1.5.1
</span> </span>
</div> </div>
<!-- Fixed top navbar for the whole site --> <!-- Fixed top navbar for the whole site -->
Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

+189
View File
@@ -0,0 +1,189 @@
# Lyng as a Common Platform for Smart Contracts and Distributed Execution
## Executive Summary
This proposal describes Lyng as a shared execution layer for:
- smart contracts across different blockchain networks,
- distributed compute workflows,
- decentralized application logic.
The goal is simple: write business logic once in Lyng, then run it consistently across chains, trusted execution nodes, and off-chain workers.
## Why a Common Lyng Layer
A unified language and runtime reduces fragmentation between contract code, off-chain services, and automation scripts.
Key benefits:
- One language across environments: the same Lyng code can be reused in contracts, validators, and distributed tasks.
- Capability-safe by default: no filesystem, process, or host access unless explicitly exposed by the host platform.
- Async-first execution model: lightweight concurrent tasks are natural, and host runtimes can schedule them efficiently.
- Strong typing with inference: concise source code without sacrificing type safety.
- Deterministic execution options: instruction/gas counting can be enabled for metered environments.
- Broad platform support: JVM, JS/Wasm, and native targets enable consistent tooling and deployment.
## Where It Fits
Lyng is a strong fit for:
- contract logic that must also run in simulation or off-chain verification,
- distributed orchestration and rule-based automations,
- cross-network shared libraries (validation, accounting rules, policy checks),
- deterministic business logic that should be portable between operators.
## Proposed Architecture
1. **Primary runtime**: Lyng VM/JIT for high-throughput trusted execution.
2. **Metered runtime**: Lyng VM mode with instruction counting and gas limits for blockchain execution.
3. **Standard API surface**:
- core Lyng standard library,
- storage contract (`key/value/tags`) shared across network adapters.
4. **Developer sandbox**:
- local deterministic sandbox for test/debug,
- optional feature-rich sandbox profile for integration testing.
## Strong Parts of the Lyng Approach
- **Code portability**: contract logic and off-chain logic remain aligned.
- **Lower maintenance**: fewer language/runtime rewrites across components.
- **Faster onboarding**: modern, readable syntax and concise constructs.
- **Safer host integration**: explicit capability injection limits accidental exposure.
- **Operational flexibility**: same package can run in metered or non-metered mode.
## Example: Deterministic Ledger Transfer
```lyng
class Ledger(storage) {
fun transfer(from: Int, to: Int, amount: Int) {
require(amount > 0)
val fromKey = "bal:$from"
val toKey = "bal:$to"
val fromBal = storage[fromKey] ?: 0
require(fromBal >= amount)
storage[fromKey] = fromBal - amount
storage[toKey] = (storage[toKey] ?: 0) + amount
}
}
```
The same logic can run:
- in a gas-metered contract runtime,
- in off-chain simulation,
- in distributed batch reconciliation.
## Example: Parallel Verification Workflow
```lyng
fun verifyBatch(items, verifier) {
val jobs = []
for (item in items) {
jobs += launch { verifier(item) }
}
val results = []
for (job in jobs) {
results += await(job)
}
results
}
```
This model is useful for distributed validation, scoring, and pre-consensus checks.
## Feature Showcases
### 1. Runtime generics with bounds (non-erased behavior)
```lyng
fun square<T: Int | Real>(x: T) = x * x
fun describeValue<T: Int | Real>(x: T): String = when (T) {
Int -> "Int value=$x"
Real -> "Real value=$x"
else -> "Numeric value=$x"
}
fun sameType<T>(x: T, y: Object): Bool = y is T
```
This enables practical runtime type-aware logic in generic code without JVM-style erasure limitations.
### 2. Multiple inheritance with lifted enums
```lyng
class Payable(val accountId: Int) {
fun channel() = "onchain"
}
class Audited {
fun mark() = "audited"
}
class Payment(accountId: Int) : Payable(accountId), Audited() {
enum Status* { Pending, Settled }
}
val p = Payment(42)
assertEquals("onchain", p.channel())
assertEquals("audited", p.mark())
assertEquals(Payment.Pending, Payment.Status.Pending)
```
This combines reusable behavior composition with concise domain modeling.
### 3. Unified delegation model (`val`, `var`, `fun`)
```lyng
val state = { "owner": "alice", "limit": 10 }
val owner by state
var limit by state
object RpcDelegate {
fun invoke(thisRef, name, args...) = "$name:${args.size}"
}
fun remoteCall by RpcDelegate
limit = 25
assertEquals(25, state["limit"])
assertEquals("remoteCall:2", remoteCall("alice", 10))
```
Delegation helps build compact, extensible models with minimal boilerplate.
## Rollout Strategy
1. Define the minimal portable storage API (`get/put/find by tags`) as a stable contract.
2. Publish a compatibility profile for metered execution (determinism + gas accounting).
3. Provide chain adapters that map Lyng storage/API calls to each target network.
4. Ship a reference sandbox and conformance tests for runtime parity.
5. Build shared Lyng libraries for common contract and compute patterns.
6. Launch a Lyng package manager and registry (NPM/Maven-style) with first-class support for unified scripts:
- package metadata for runtime profile (metered/off-chain), storage API version, and capability requirements,
- deterministic dependency resolution and lockfiles,
- signed package publishing and verification for supply-chain integrity.
## Existing Resources
- Lyng project repository: https://github.com/sergeych/lyng
- Lyng docs home: https://lynglang.com
- Lyng tutorial: https://lynglang.com/#/docs/tutorial.md
- Embedding Lyng: https://lynglang.com/#/docs/embedding.md
## Conclusion
Lyng can serve as a practical common platform for blockchain contracts and distributed execution by combining:
- portable language semantics,
- safe-by-default host interaction,
- metered deterministic execution when required,
- and reusable code across on-chain and off-chain environments.
This reduces duplication, improves consistency, and shortens delivery cycles for decentralized systems.