Compare commits
16
Commits
1.5.0-RC
...
bigdecimals
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
580256d520 | ||
|
|
4269310beb | ||
|
|
7c059b4741 | ||
|
|
217787e17a | ||
|
|
9ddc7dbee6 | ||
|
|
24937c7cf5 | ||
|
|
ecc4d8b9ed | ||
|
|
3d8bfe8863 | ||
|
|
3b83439788 | ||
|
|
0af0f31e48 | ||
|
|
d17ad9ef0d | ||
|
|
222a653040 | ||
|
|
f24c7d2715 | ||
|
|
2546a5327f | ||
|
|
4df6612eca | ||
|
|
2a79c718ba |
No files matched your search
@@ -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
@@ -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.
|
|
||||||
+250
@@ -0,0 +1,250 @@
|
|||||||
|
# Decimal (`lyng.decimal`)
|
||||||
|
|
||||||
|
`lyng.decimal` adds an arbitrary-precision decimal type to Lyng as a normal library module.
|
||||||
|
|
||||||
|
Import it when you need decimal arithmetic that should not inherit `Real`'s binary floating-point behavior:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
```
|
||||||
|
|
||||||
|
## What `BigDecimal` Is For
|
||||||
|
|
||||||
|
Use `BigDecimal` when values are fundamentally decimal:
|
||||||
|
|
||||||
|
- money
|
||||||
|
- human-entered quantities
|
||||||
|
- exact decimal text
|
||||||
|
- predictable decimal rounding
|
||||||
|
- user-facing formatting and tests
|
||||||
|
|
||||||
|
Do not use it just because a number has a fractional part. `Real` is still the right type for ordinary double-precision numeric work.
|
||||||
|
|
||||||
|
## Creating Decimal Values
|
||||||
|
|
||||||
|
There are three supported conversions:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
val a = 1.d
|
||||||
|
val b = 2.2.d
|
||||||
|
val c = "2.2".d
|
||||||
|
|
||||||
|
assertEquals("1", a.toStringExpanded())
|
||||||
|
assertEquals("2.2", b.toStringExpanded())
|
||||||
|
assertEquals("2.2", c.toStringExpanded())
|
||||||
|
```
|
||||||
|
|
||||||
|
The three forms mean different things:
|
||||||
|
|
||||||
|
- `1.d`: convert `Int -> BigDecimal`
|
||||||
|
- `2.2.d`: convert `Real -> BigDecimal`
|
||||||
|
- `"2.2".d`: parse exact decimal text
|
||||||
|
|
||||||
|
That distinction is intentional.
|
||||||
|
|
||||||
|
### `Real.d` vs `"..." .d`
|
||||||
|
|
||||||
|
`Real.d` preserves the current `Real` value. It does not pretend the source code was exact decimal text.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
assertEquals("0.30000000000000004", (0.1 + 0.2).d.toStringExpanded())
|
||||||
|
assertEquals("0.3", "0.3".d.toStringExpanded())
|
||||||
|
```
|
||||||
|
|
||||||
|
This follows the "minimal confusion" rule:
|
||||||
|
|
||||||
|
- if you start from a `Real`, you get a decimal representation of that `Real`
|
||||||
|
- if you want exact decimal source text, use a `String`
|
||||||
|
|
||||||
|
## Factory Functions
|
||||||
|
|
||||||
|
The explicit factory methods are:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
BigDecimal.fromInt(10)
|
||||||
|
BigDecimal.fromReal(2.5)
|
||||||
|
BigDecimal.fromString("12.34")
|
||||||
|
```
|
||||||
|
|
||||||
|
These are equivalent to the conversion-property forms, but sometimes clearer in APIs or generated code.
|
||||||
|
|
||||||
|
## Core Operations
|
||||||
|
|
||||||
|
`BigDecimal` supports:
|
||||||
|
|
||||||
|
- `+`
|
||||||
|
- `-`
|
||||||
|
- `*`
|
||||||
|
- `/`
|
||||||
|
- `%`
|
||||||
|
- unary `-`
|
||||||
|
- comparison operators
|
||||||
|
- equality operators
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
assertEquals("3.75", ("1.5".d + "2.25".d).toStringExpanded())
|
||||||
|
assertEquals("1.25", ("2.5".d - "1.25".d).toStringExpanded())
|
||||||
|
assertEquals("3.0", ("1.5".d * 2.d).toStringExpanded())
|
||||||
|
assertEquals("0.5", (1.d / 2.d).toStringExpanded())
|
||||||
|
assert("2.0".d > "1.5".d)
|
||||||
|
assert("2.0".d == 2.d)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Interoperability With `Int` and `Real`
|
||||||
|
|
||||||
|
The decimal module registers mixed-operand operator bridges so both sides read naturally:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
assertEquals(3.d, 1 + 2.d)
|
||||||
|
assertEquals(3.d, 2.d + 1)
|
||||||
|
assertEquals(1.5.d, 1.d + 0.5)
|
||||||
|
assertEquals(1.5.d, 0.5 + 1.d)
|
||||||
|
assert(2 == 2.d)
|
||||||
|
assert(3 > 2.d)
|
||||||
|
```
|
||||||
|
|
||||||
|
Without this registration mechanism, only the cases directly implemented on the left-hand class would work. The bridge fills the gap for expressions such as `Int + BigDecimal` and `Real + BigDecimal`.
|
||||||
|
|
||||||
|
See [OperatorInterop.md](OperatorInterop.md) for the generic mechanism behind that.
|
||||||
|
|
||||||
|
## String Representation
|
||||||
|
|
||||||
|
Use `toStringExpanded()` when you want plain decimal output without scientific notation:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
assertEquals("12.34", "12.34".d.toStringExpanded())
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the recommended representation for:
|
||||||
|
|
||||||
|
- tests
|
||||||
|
- user-visible diagnostics
|
||||||
|
- decimal formatting checks
|
||||||
|
|
||||||
|
## Conversions Back To Built-ins
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
assertEquals(2, "2.9".d.toInt())
|
||||||
|
assertEquals(2.9, "2.9".d.toReal())
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `toReal()` only when you are willing to return to binary floating-point semantics.
|
||||||
|
|
||||||
|
## Division Context
|
||||||
|
|
||||||
|
Division is the operation where precision and rounding matter most.
|
||||||
|
|
||||||
|
By default, decimal division uses:
|
||||||
|
|
||||||
|
- precision: `34` significant digits
|
||||||
|
- rounding: `HalfEven`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
assertEquals("0.3333333333333333333333333333333333", (1.d / 3.d).toStringExpanded())
|
||||||
|
assertEquals("0.6666666666666666666666666666666667", ("2".d / 3.d).toStringExpanded())
|
||||||
|
```
|
||||||
|
|
||||||
|
## `withDecimalContext(...)`
|
||||||
|
|
||||||
|
Use `withDecimalContext(...)` to override decimal division rules inside a block:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"0.3333333333",
|
||||||
|
withDecimalContext(10) { (1.d / 3.d).toStringExpanded() }
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also pass an explicit context object:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
val ctx = DecimalContext(6, DecimalRounding.HalfAwayFromZero)
|
||||||
|
assertEquals("0.666667", withDecimalContext(ctx) { ("2".d / 3.d).toStringExpanded() })
|
||||||
|
```
|
||||||
|
|
||||||
|
The context is dynamic and local to the block. After the block exits, the previous context is restored.
|
||||||
|
|
||||||
|
## Rounding Modes
|
||||||
|
|
||||||
|
Available rounding modes:
|
||||||
|
|
||||||
|
- `HalfEven`
|
||||||
|
- `HalfAwayFromZero`
|
||||||
|
- `HalfTowardsZero`
|
||||||
|
- `Ceiling`
|
||||||
|
- `Floor`
|
||||||
|
- `AwayFromZero`
|
||||||
|
- `TowardsZero`
|
||||||
|
|
||||||
|
Tie example at precision `2`:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
assertEquals("0.12", withDecimalContext(2, DecimalRounding.HalfEven) { (1.d / 8.d).toStringExpanded() })
|
||||||
|
assertEquals("0.13", withDecimalContext(2, DecimalRounding.HalfAwayFromZero) { (1.d / 8.d).toStringExpanded() })
|
||||||
|
assertEquals("0.12", withDecimalContext(2, DecimalRounding.HalfTowardsZero) { (1.d / 8.d).toStringExpanded() })
|
||||||
|
assertEquals("0.13", withDecimalContext(2, DecimalRounding.Ceiling) { (1.d / 8.d).toStringExpanded() })
|
||||||
|
assertEquals("0.12", withDecimalContext(2, DecimalRounding.Floor) { (1.d / 8.d).toStringExpanded() })
|
||||||
|
```
|
||||||
|
|
||||||
|
Negative values follow the same named policy in the obvious direction:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
assertEquals("-0.13", withDecimalContext(2, DecimalRounding.HalfAwayFromZero) { (-1.d / 8.d).toStringExpanded() })
|
||||||
|
assertEquals("-0.12", withDecimalContext(2, DecimalRounding.HalfTowardsZero) { (-1.d / 8.d).toStringExpanded() })
|
||||||
|
```
|
||||||
|
|
||||||
|
## Recommended Usage Rules
|
||||||
|
|
||||||
|
If you care about exact decimal source text:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
"12.34".d
|
||||||
|
```
|
||||||
|
|
||||||
|
If you intentionally convert an existing binary floating-point value:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
someReal.d
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want local control over division:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
withDecimalContext(precision, rounding) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want custom mixed operators for your own type, follow the same pattern as Decimal and use the operator registry:
|
||||||
|
|
||||||
|
- define the operators on your own class
|
||||||
|
- choose a common class
|
||||||
|
- register mixed operand bridges
|
||||||
|
|
||||||
|
See [OperatorInterop.md](OperatorInterop.md).
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
# Operator Interop Registry
|
||||||
|
|
||||||
|
`lyng.operators` provides a runtime registry for mixed-class binary operators.
|
||||||
|
|
||||||
|
Import it when you want expressions such as:
|
||||||
|
|
||||||
|
- `1 + MyType(...)`
|
||||||
|
- `2 < MyType(...)`
|
||||||
|
- `3 == MyType(...)`
|
||||||
|
|
||||||
|
to work without modifying the built-in `Int` or `Real` classes.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.operators
|
||||||
|
```
|
||||||
|
|
||||||
|
## Why This Exists
|
||||||
|
|
||||||
|
If your class defines:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
class Amount(val value: Int) {
|
||||||
|
fun plus(other: Amount) = Amount(value + other.value)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
then:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
Amount(1) + Amount(2)
|
||||||
|
```
|
||||||
|
|
||||||
|
works, because the left operand already knows how to add another `Amount`.
|
||||||
|
|
||||||
|
But:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
1 + Amount(2)
|
||||||
|
```
|
||||||
|
|
||||||
|
does not naturally work, because `Int` has not been rewritten to know about `Amount`.
|
||||||
|
|
||||||
|
The operator interop registry solves exactly that problem.
|
||||||
|
|
||||||
|
## Mental Model
|
||||||
|
|
||||||
|
Registration describes a mixed pair:
|
||||||
|
|
||||||
|
- left class `L`
|
||||||
|
- right class `R`
|
||||||
|
- common class `C`
|
||||||
|
|
||||||
|
When Lyng sees `L op R`, it:
|
||||||
|
|
||||||
|
1. converts `L -> C`
|
||||||
|
2. converts `R -> C`
|
||||||
|
3. evaluates the operator as `C op C`
|
||||||
|
|
||||||
|
So the registry is a bridge, not a separate arithmetic engine.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
OperatorInterop.register(
|
||||||
|
leftClass,
|
||||||
|
rightClass,
|
||||||
|
commonClass,
|
||||||
|
operators,
|
||||||
|
leftToCommon,
|
||||||
|
rightToCommon
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
|
||||||
|
- `leftClass`: original left operand class
|
||||||
|
- `rightClass`: original right operand class
|
||||||
|
- `commonClass`: class that will actually execute the operator methods
|
||||||
|
- `operators`: list of operators enabled for this pair
|
||||||
|
- `leftToCommon`: conversion from left operand to common class
|
||||||
|
- `rightToCommon`: conversion from right operand to common class
|
||||||
|
|
||||||
|
## Supported Operators
|
||||||
|
|
||||||
|
`BinaryOperator` values:
|
||||||
|
|
||||||
|
- `Plus`
|
||||||
|
- `Minus`
|
||||||
|
- `Mul`
|
||||||
|
- `Div`
|
||||||
|
- `Mod`
|
||||||
|
- `Compare`
|
||||||
|
- `Equals`
|
||||||
|
|
||||||
|
Meaning:
|
||||||
|
|
||||||
|
- `Compare` enables `<`, `<=`, `>`, `>=`, and `<=>`
|
||||||
|
- `Equals` enables `==` and `!=`
|
||||||
|
|
||||||
|
## Minimal Working Example
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
package test.decimalbox
|
||||||
|
import lyng.operators
|
||||||
|
|
||||||
|
class DecimalBox(val value: Int) {
|
||||||
|
fun plus(other: DecimalBox) = DecimalBox(value + other.value)
|
||||||
|
fun minus(other: DecimalBox) = DecimalBox(value - other.value)
|
||||||
|
fun mul(other: DecimalBox) = DecimalBox(value * other.value)
|
||||||
|
fun div(other: DecimalBox) = DecimalBox(value / other.value)
|
||||||
|
fun mod(other: DecimalBox) = DecimalBox(value % other.value)
|
||||||
|
fun compareTo(other: DecimalBox) = value <=> other.value
|
||||||
|
}
|
||||||
|
|
||||||
|
OperatorInterop.register(
|
||||||
|
Int,
|
||||||
|
DecimalBox,
|
||||||
|
DecimalBox,
|
||||||
|
[
|
||||||
|
BinaryOperator.Plus,
|
||||||
|
BinaryOperator.Minus,
|
||||||
|
BinaryOperator.Mul,
|
||||||
|
BinaryOperator.Div,
|
||||||
|
BinaryOperator.Mod,
|
||||||
|
BinaryOperator.Compare,
|
||||||
|
BinaryOperator.Equals
|
||||||
|
],
|
||||||
|
{ x: Int -> DecimalBox(x) },
|
||||||
|
{ x: DecimalBox -> x }
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import test.decimalbox
|
||||||
|
|
||||||
|
assertEquals(DecimalBox(3), 1 + DecimalBox(2))
|
||||||
|
assertEquals(DecimalBox(1), 3 - DecimalBox(2))
|
||||||
|
assertEquals(DecimalBox(8), 4 * DecimalBox(2))
|
||||||
|
assertEquals(DecimalBox(4), 8 / DecimalBox(2))
|
||||||
|
assertEquals(DecimalBox(1), 7 % DecimalBox(2))
|
||||||
|
assert(1 < DecimalBox(2))
|
||||||
|
assert(2 <= DecimalBox(2))
|
||||||
|
assert(3 > DecimalBox(2))
|
||||||
|
assert(2 == DecimalBox(2))
|
||||||
|
assert(2 != DecimalBox(3))
|
||||||
|
```
|
||||||
|
|
||||||
|
## How Decimal Uses It
|
||||||
|
|
||||||
|
`lyng.decimal` uses this same mechanism so that:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
1 + 2.d
|
||||||
|
0.5 + 1.d
|
||||||
|
2 == 2.d
|
||||||
|
3 > 2.d
|
||||||
|
```
|
||||||
|
|
||||||
|
work naturally even though `Int` and `Real` themselves were not edited to know `BigDecimal`.
|
||||||
|
|
||||||
|
The shape is:
|
||||||
|
|
||||||
|
- `leftClass = Int` or `Real`
|
||||||
|
- `rightClass = BigDecimal`
|
||||||
|
- `commonClass = BigDecimal`
|
||||||
|
- convert built-ins into `BigDecimal`
|
||||||
|
- leave `BigDecimal` values unchanged
|
||||||
|
|
||||||
|
## Step-By-Step Pattern For Your Own Type
|
||||||
|
|
||||||
|
### 1. Pick the common class
|
||||||
|
|
||||||
|
Choose one class that will be the actual arithmetic domain.
|
||||||
|
|
||||||
|
For numeric-like types, that is usually your own class:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
class Rational(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Implement operators on that class
|
||||||
|
|
||||||
|
The common class should define the operations you plan to register.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
class Rational(val num: Int, val den: Int) {
|
||||||
|
fun plus(other: Rational) = Rational(num * other.den + other.num * den, den * other.den)
|
||||||
|
fun minus(other: Rational) = Rational(num * other.den - other.num * den, den * other.den)
|
||||||
|
fun mul(other: Rational) = Rational(num * other.num, den * other.den)
|
||||||
|
fun div(other: Rational) = Rational(num * other.den, den * other.num)
|
||||||
|
fun compareTo(other: Rational) = (num * other.den) <=> (other.num * den)
|
||||||
|
|
||||||
|
static fun fromInt(value: Int) = Rational(value, 1)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Register the mixed pair
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.operators
|
||||||
|
|
||||||
|
OperatorInterop.register(
|
||||||
|
Int,
|
||||||
|
Rational,
|
||||||
|
Rational,
|
||||||
|
[
|
||||||
|
BinaryOperator.Plus,
|
||||||
|
BinaryOperator.Minus,
|
||||||
|
BinaryOperator.Mul,
|
||||||
|
BinaryOperator.Div,
|
||||||
|
BinaryOperator.Compare,
|
||||||
|
BinaryOperator.Equals
|
||||||
|
],
|
||||||
|
{ x: Int -> Rational.fromInt(x) },
|
||||||
|
{ x: Rational -> x }
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Use it
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
assertEquals(Rational(3, 2), 1 + Rational(1, 2))
|
||||||
|
assert(Rational(3, 2) == Rational(3, 2))
|
||||||
|
assert(2 > Rational(3, 2))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Registering More Than One Built-in Type
|
||||||
|
|
||||||
|
If you want both `Int + MyType` and `Real + MyType`, register both pairs explicitly:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
OperatorInterop.register(
|
||||||
|
Int,
|
||||||
|
MyType,
|
||||||
|
MyType,
|
||||||
|
[BinaryOperator.Plus, BinaryOperator.Compare, BinaryOperator.Equals],
|
||||||
|
{ x: Int -> MyType.fromInt(x) },
|
||||||
|
{ x: MyType -> x }
|
||||||
|
)
|
||||||
|
|
||||||
|
OperatorInterop.register(
|
||||||
|
Real,
|
||||||
|
MyType,
|
||||||
|
MyType,
|
||||||
|
[BinaryOperator.Plus, BinaryOperator.Compare, BinaryOperator.Equals],
|
||||||
|
{ x: Real -> MyType.fromReal(x) },
|
||||||
|
{ x: MyType -> x }
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Each mixed pair is independent.
|
||||||
|
|
||||||
|
## Pure Lyng Registration
|
||||||
|
|
||||||
|
This mechanism is intentionally useful from pure Lyng code, not only from Kotlin-backed modules.
|
||||||
|
|
||||||
|
That means you can:
|
||||||
|
|
||||||
|
- declare a class in Lyng
|
||||||
|
- define its operators in Lyng
|
||||||
|
- register mixed operand bridges in Lyng
|
||||||
|
|
||||||
|
without touching compiler internals.
|
||||||
|
|
||||||
|
## Where To Register
|
||||||
|
|
||||||
|
Register once during module initialization.
|
||||||
|
|
||||||
|
Top-level module code is a good place:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
package my.rational
|
||||||
|
import lyng.operators
|
||||||
|
|
||||||
|
class Rational(...)
|
||||||
|
|
||||||
|
OperatorInterop.register(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
That keeps registration close to the type declaration and makes importing the module enough to activate the interop.
|
||||||
|
|
||||||
|
## What Registration Does Not Do
|
||||||
|
|
||||||
|
The registry does not:
|
||||||
|
|
||||||
|
- invent operators your common class does not implement
|
||||||
|
- change the original `Int`, `Real`, or other built-ins
|
||||||
|
- automatically cover every class pair
|
||||||
|
- replace normal method overload resolution when the left-hand class already knows what to do
|
||||||
|
|
||||||
|
It only teaches Lyng how to bridge a specific mixed pair into a common class for the listed operators.
|
||||||
|
|
||||||
|
## Recommended Design Rules
|
||||||
|
|
||||||
|
If you want interop to feel natural:
|
||||||
|
|
||||||
|
- choose one obvious common class
|
||||||
|
- make conversions explicit and unsurprising
|
||||||
|
- implement `compareTo` if you want ordering operators
|
||||||
|
- register `Equals` whenever mixed equality should work
|
||||||
|
- keep the registered operator list minimal and accurate
|
||||||
|
|
||||||
|
For decimal-like semantics, also read [Decimal.md](Decimal.md).
|
||||||
@@ -17,6 +17,14 @@ Primary sources used: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/{Parser,T
|
|||||||
- Supported escapes: `\n`, `\r`, `\t`, `\"`, `\\`, `\uXXXX` (4 hex digits).
|
- 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).
|
||||||
|
|||||||
@@ -72,6 +72,14 @@ For module-level APIs, the default workflow is:
|
|||||||
1. declare globals in Lyng using `extern fun` / `extern val` / `extern var`;
|
1. declare globals in Lyng using `extern fun` / `extern val` / `extern var`;
|
||||||
2. bind Kotlin implementation via `ModuleScope.globalBinder()`.
|
2. bind Kotlin implementation via `ModuleScope.globalBinder()`.
|
||||||
|
|
||||||
|
This is also the recommended way to expose a Kotlin-backed value that should behave like a true
|
||||||
|
Lyng global variable/property. If you need `x` to read/write through Kotlin on every access, use
|
||||||
|
`extern var` / `extern val` plus `bindGlobalVar(...)`.
|
||||||
|
|
||||||
|
Do not use `addConst(...)` for this case: `addConst(...)` installs a value, not a Kotlin-backed
|
||||||
|
property accessor. It is appropriate for fixed values and objects, but not for a global that should
|
||||||
|
delegate reads/writes back into Kotlin state.
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
import net.sergeych.lyng.bridge.*
|
import net.sergeych.lyng.bridge.*
|
||||||
import net.sergeych.lyng.obj.ObjInt
|
import net.sergeych.lyng.obj.ObjInt
|
||||||
@@ -117,6 +125,12 @@ assertEquals("changed", globalProp)
|
|||||||
assertEquals("1.0.0", globalVersion)
|
assertEquals("1.0.0", globalVersion)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Minimal rule of thumb:
|
||||||
|
|
||||||
|
- use `bindGlobalFun(...)` for global functions
|
||||||
|
- use `bindGlobalVar(...)` for Kotlin-backed global variables/properties
|
||||||
|
- use `addConst(...)` only for fixed values/objects that do not need getter/setter behavior
|
||||||
|
|
||||||
For custom argument handling and full runtime access:
|
For custom argument handling and full runtime access:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
@@ -462,14 +476,21 @@ im.addPackage("my.tools") { module: ModuleScope ->
|
|||||||
module.eval(
|
module.eval(
|
||||||
"""
|
"""
|
||||||
extern val version: String
|
extern val version: String
|
||||||
|
extern var status: String
|
||||||
extern fun triple(x: Int): Int
|
extern fun triple(x: Int): Int
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
val binder = module.globalBinder()
|
val binder = module.globalBinder()
|
||||||
|
var status = "ready"
|
||||||
binder.bindGlobalVar(
|
binder.bindGlobalVar(
|
||||||
name = "version",
|
name = "version",
|
||||||
get = { "1.0" }
|
get = { "1.0" }
|
||||||
)
|
)
|
||||||
|
binder.bindGlobalVar(
|
||||||
|
name = "status",
|
||||||
|
get = { status },
|
||||||
|
set = { status = it }
|
||||||
|
)
|
||||||
binder.bindGlobalFun1<Int>("triple") { x ->
|
binder.bindGlobalFun1<Int>("triple") { x ->
|
||||||
ObjInt.of((x * 3).toLong())
|
ObjInt.of((x * 3).toLong())
|
||||||
}
|
}
|
||||||
@@ -479,6 +500,7 @@ im.addPackage("my.tools") { module: ModuleScope ->
|
|||||||
scope.eval("""
|
scope.eval("""
|
||||||
import my.tools.*
|
import my.tools.*
|
||||||
val v = triple(14)
|
val v = triple(14)
|
||||||
|
status = "busy"
|
||||||
""")
|
""")
|
||||||
val v = scope.eval("v").toKotlin(scope) // -> 42
|
val v = scope.eval("v").toKotlin(scope) // -> 42
|
||||||
```
|
```
|
||||||
|
|||||||
+15
-4
@@ -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
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -5,6 +5,73 @@ For a programmer-focused migration summary, see `docs/whats_new_1_5.md`.
|
|||||||
|
|
||||||
## Language Features
|
## Language Features
|
||||||
|
|
||||||
|
### Decimal Arithmetic Module (`lyng.decimal`)
|
||||||
|
Lyng now ships a first-class decimal module built as a regular extension library rather than a deep core special case.
|
||||||
|
|
||||||
|
It provides:
|
||||||
|
|
||||||
|
- `BigDecimal`
|
||||||
|
- convenient `.d` conversions from `Int`, `Real`, and `String`
|
||||||
|
- mixed arithmetic with `Int` and `Real`
|
||||||
|
- local division precision and rounding control via `withDecimalContext(...)`
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
assertEquals("3", (1 + 2.d).toStringExpanded())
|
||||||
|
assertEquals("0.30000000000000004", (0.1 + 0.2).d.toStringExpanded())
|
||||||
|
assertEquals("0.3", "0.3".d.toStringExpanded())
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"0.3333333333",
|
||||||
|
withDecimalContext(10) { (1.d / 3.d).toStringExpanded() }
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
The distinction between `Real -> Decimal` and exact decimal parsing is explicit by design:
|
||||||
|
|
||||||
|
- `2.2.d` converts the current `Real` value
|
||||||
|
- `"2.2".d` parses exact decimal text
|
||||||
|
|
||||||
|
See [Decimal](Decimal.md).
|
||||||
|
|
||||||
|
### Binary Operator Interop Registry
|
||||||
|
Lyng now provides a general mechanism for mixed binary operators through `lyng.operators`.
|
||||||
|
|
||||||
|
This solves cases like:
|
||||||
|
|
||||||
|
- `Int + MyType`
|
||||||
|
- `Real < MyType`
|
||||||
|
- `Int == MyType`
|
||||||
|
|
||||||
|
without requiring changes to built-in classes.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
import lyng.operators
|
||||||
|
|
||||||
|
class DecimalBox(val value: Int) {
|
||||||
|
fun plus(other: DecimalBox) = DecimalBox(value + other.value)
|
||||||
|
fun compareTo(other: DecimalBox) = value <=> other.value
|
||||||
|
}
|
||||||
|
|
||||||
|
OperatorInterop.register(
|
||||||
|
Int,
|
||||||
|
DecimalBox,
|
||||||
|
DecimalBox,
|
||||||
|
[BinaryOperator.Plus, BinaryOperator.Compare, BinaryOperator.Equals],
|
||||||
|
{ x: Int -> DecimalBox(x) },
|
||||||
|
{ x: DecimalBox -> x }
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(DecimalBox(3), 1 + DecimalBox(2))
|
||||||
|
assert(1 < DecimalBox(2))
|
||||||
|
assert(2 == DecimalBox(2))
|
||||||
|
```
|
||||||
|
|
||||||
|
`lyng.decimal` uses this same mechanism internally to interoperate with `Int` and `Real`.
|
||||||
|
|
||||||
|
See [Operator Interop Registry](OperatorInterop.md).
|
||||||
|
|
||||||
### Class Properties with Accessors
|
### Class Properties with Accessors
|
||||||
Classes now support properties with custom `get()` and `set()` accessors. Properties in Lyng do **not** have automatic backing fields; they are pure accessors.
|
Classes now support properties with custom `get()` and `set()` accessors. Properties in Lyng do **not** have automatic backing fields; they are pure accessors.
|
||||||
|
|
||||||
|
|||||||
Regular → Executable
+306
-151
@@ -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,22 +152,30 @@ 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 {
|
||||||
val piece: Piece = PIECES[pieceId - 1]
|
try {
|
||||||
val cells = piece.rotations[rot]
|
if (pieceId < 1 || pieceId > 7) return false
|
||||||
|
val piece: Piece = PIECES[pieceId - 1]
|
||||||
|
if (rot < 0 || rot >= piece.rotations.size) return false
|
||||||
|
val cells = piece.rotations[rot]
|
||||||
|
|
||||||
for (cell in cells) {
|
for (cell in cells) {
|
||||||
val x = px + cell[0]
|
val x = px + cell[0]
|
||||||
val y = py + cell[1]
|
val y = py + cell[1]
|
||||||
|
|
||||||
if (x < 0 || x >= boardW) return false
|
if (x < 0 || x >= boardW) return false
|
||||||
if (y >= boardH) return false
|
if (y >= boardH) return false
|
||||||
|
|
||||||
if (y >= 0) {
|
if (y >= 0) {
|
||||||
val row = board[y]
|
if (y >= board.size) return false
|
||||||
if (row[x] != 0) return false
|
val row = board[y]
|
||||||
|
if (row == null) return false
|
||||||
|
if (row[x] != 0) return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
true
|
||||||
|
} catch (_: Object) {
|
||||||
|
false
|
||||||
}
|
}
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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,9 +186,11 @@ 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]
|
||||||
row[x] = pieceId
|
if (row != null) {
|
||||||
|
row[x] = pieceId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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) {
|
||||||
out.add(" ")
|
for (i in 0..<4) {
|
||||||
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,39 +551,66 @@ 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 {
|
||||||
s.running = false
|
if (key == "__CTRL_C__") {
|
||||||
}
|
s.running = false
|
||||||
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
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
else if (key == "ArrowUp" || key == "w" || key == "W") {
|
|
||||||
val rr: RotateResult = tryRotateCw(board, boardW, boardH, s.pieceId, s.rot, s.px, s.py)
|
|
||||||
if (rr.ok) {
|
|
||||||
s.rot = rr.rot
|
|
||||||
s.px = rr.px
|
|
||||||
}
|
}
|
||||||
}
|
else if (s.paused) {
|
||||||
else if (key == "ArrowDown" || key == "s" || key == "S") {
|
if (key == "Escape") {
|
||||||
if (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px, s.py + 1)) {
|
s.running = false
|
||||||
s.py = s.py + 1
|
} else {
|
||||||
s.score = s.score + 1
|
s.paused = false
|
||||||
|
forceRedraw = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
else if (key == "p" || key == "P" || key == "Escape") {
|
||||||
else if (key == " ") {
|
s.paused = true
|
||||||
while (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px, s.py + 1)) {
|
forceRedraw = true
|
||||||
s.py = s.py + 1
|
|
||||||
s.score = s.score + 2
|
|
||||||
}
|
}
|
||||||
|
else if (key == "q" || key == "Q") {
|
||||||
|
s.running = false
|
||||||
|
}
|
||||||
|
else if (key == "ArrowLeft" || key == "a" || key == "A") {
|
||||||
|
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") {
|
||||||
|
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") {
|
||||||
|
val rr: RotateResult = tryRotateCw(board, boardW, boardH, s.pieceId, s.rot, s.px, s.py)
|
||||||
|
if (rr.ok) {
|
||||||
|
s.rot = rr.rot
|
||||||
|
s.px = rr.px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (key == "ArrowDown" || key == "s" || key == "S") {
|
||||||
|
if (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px, s.py + 1) == true) {
|
||||||
|
s.py++
|
||||||
|
s.score++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (key == " ") {
|
||||||
|
while (canPlace(board, boardW, boardH, s.pieceId, s.rot, s.px, s.py + 1) == true) {
|
||||||
|
s.py++
|
||||||
|
s.score += 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (inputErr: Object) {
|
||||||
|
logError("applyKeyInput recovered after error", inputErr)
|
||||||
|
resetActivePiece(s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
if (ev is ConsoleKeyEvent) {
|
|
||||||
val ke = ev as ConsoleKeyEvent
|
// Isolate per-event failures so one bad event does not unwind the stream.
|
||||||
if (ke.type == "keydown") {
|
try {
|
||||||
val key = ke.key
|
if (ev is ConsoleKeyEvent) {
|
||||||
val ctrl = ke.ctrl
|
val ke = ev as ConsoleKeyEvent
|
||||||
gameMutex.withLock {
|
if (ke.type == ConsoleEventType.KEY_DOWN) {
|
||||||
|
var key = ""
|
||||||
|
var ctrl = false
|
||||||
|
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) {
|
} catch (eventErr: Object) {
|
||||||
gameMutex.withLock {
|
// Keep the input stream alive; report for diagnostics.
|
||||||
hasResizeEvent = true
|
logError("Input event error", eventErr)
|
||||||
prevFrameLines = []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err: Exception) {
|
} catch (err: Object) {
|
||||||
// Keep game alive: transient console-event failures should not force quit.
|
// 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,37 +737,60 @@ 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
|
||||||
if (!state.running || state.gameOver) {
|
mm.withLock {
|
||||||
shouldStop = true
|
if (pendingInputs.size > 0) {
|
||||||
} else {
|
val toApply: List<String> = []
|
||||||
val movedOrigin = frameData.originRow != prevOriginRow || frameData.originCol != prevOriginCol
|
while (pendingInputs.size > 0) {
|
||||||
if (frameData.resized || movedOrigin) {
|
val k = pendingInputs[0]
|
||||||
clearAndHome()
|
pendingInputs.removeAt(0)
|
||||||
prevFrameLines = []
|
toApply.add(k)
|
||||||
|
}
|
||||||
|
for (k in toApply) {
|
||||||
|
applyKeyInput(state, k)
|
||||||
|
if (!state.running || state.gameOver) break
|
||||||
}
|
}
|
||||||
prevOriginRow = frameData.originRow
|
|
||||||
prevOriginCol = frameData.originCol
|
|
||||||
frame = advanceGravity(state, frame)
|
|
||||||
prevFrameLines = render(
|
|
||||||
state,
|
|
||||||
board,
|
|
||||||
boardW,
|
|
||||||
boardH,
|
|
||||||
prevFrameLines,
|
|
||||||
frameData.originRow,
|
|
||||||
frameData.originCol,
|
|
||||||
useColor
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!state.running || state.gameOver) {
|
||||||
|
shouldStop = true
|
||||||
|
} else {
|
||||||
|
val localForceRedraw = forceRedraw
|
||||||
|
forceRedraw = false
|
||||||
|
val movedOrigin = frameData.originRow != prevOriginRow || frameData.originCol != prevOriginCol
|
||||||
|
if (frameData.resized || movedOrigin || localForceRedraw) {
|
||||||
|
clearAndHome()
|
||||||
|
prevFrameLines = []
|
||||||
|
}
|
||||||
|
prevOriginRow = frameData.originRow
|
||||||
|
prevOriginCol = frameData.originCol
|
||||||
|
if (!state.paused) {
|
||||||
|
frame = advanceGravity(state, frame)
|
||||||
|
}
|
||||||
|
prevFrameLines = render(
|
||||||
|
state,
|
||||||
|
board,
|
||||||
|
boardW,
|
||||||
|
boardH,
|
||||||
|
prevFrameLines,
|
||||||
|
frameData.originRow,
|
||||||
|
frameData.originCol,
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ android-compileSdk = "34"
|
|||||||
kotlinx-coroutines = "1.10.2"
|
kotlinx-coroutines = "1.10.2"
|
||||||
kotlinx-datetime = "0.6.1"
|
kotlinx-datetime = "0.6.1"
|
||||||
mp_bintools = "0.3.2"
|
mp_bintools = "0.3.2"
|
||||||
|
ionspin-bignum = "0.3.10"
|
||||||
firebaseCrashlyticsBuildtools = "3.0.3"
|
firebaseCrashlyticsBuildtools = "3.0.3"
|
||||||
okioVersion = "3.10.2"
|
okioVersion = "3.10.2"
|
||||||
compiler = "3.2.0-alpha11"
|
compiler = "3.2.0-alpha11"
|
||||||
@@ -22,6 +23,7 @@ kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-t
|
|||||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
|
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
|
||||||
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
|
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
|
||||||
mp_bintools = { module = "net.sergeych:mp_bintools", version.ref = "mp_bintools" }
|
mp_bintools = { module = "net.sergeych:mp_bintools", version.ref = "mp_bintools" }
|
||||||
|
ionspin-bignum = { module = "com.ionspin.kotlin:bignum", version.ref = "ionspin-bignum" }
|
||||||
firebase-crashlytics-buildtools = { group = "com.google.firebase", name = "firebase-crashlytics-buildtools", version.ref = "firebaseCrashlyticsBuildtools" }
|
firebase-crashlytics-buildtools = { group = "com.google.firebase", name = "firebase-crashlytics-buildtools", version.ref = "firebaseCrashlyticsBuildtools" }
|
||||||
okio = { module = "com.squareup.okio:okio", version.ref = "okioVersion" }
|
okio = { module = "com.squareup.okio:okio", version.ref = "okioVersion" }
|
||||||
okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "okioVersion" }
|
okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "okioVersion" }
|
||||||
|
|||||||
@@ -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) {
|
||||||
if (event == null) {
|
val event = try {
|
||||||
closeSource()
|
source.nextEvent()
|
||||||
return false
|
} 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) {
|
||||||
|
closeSource()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
cached = try {
|
||||||
|
event.toObjEvent()
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
// Malformed/native event payload must not terminate consumer iteration.
|
||||||
|
consoleFlowDebug("console-bridge: malformed event dropped: $event", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cached = event.toObjEvent()
|
return cached != null
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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",
|
code?.let(::ObjString) ?: ObjNull
|
||||||
getter = { ObjString((this.thisObj as ObjConsoleKeyEvent).key) }
|
})
|
||||||
)
|
addProperty(name = "ctrl", getter = { (this.thisObj as ObjConsoleKeyEvent).ctrl.toObj() })
|
||||||
addPropertyDoc(
|
addProperty(name = "alt", getter = { (this.thisObj as ObjConsoleKeyEvent).alt.toObj() })
|
||||||
name = "code",
|
addProperty(name = "shift", getter = { (this.thisObj as ObjConsoleKeyEvent).shift.toObj() })
|
||||||
doc = "Optional hardware/code identifier.",
|
addProperty(name = "meta", getter = { (this.thisObj as ObjConsoleKeyEvent).meta.toObj() })
|
||||||
type = type("lyng.String", nullable = true),
|
|
||||||
moduleName = "lyng.io.console",
|
|
||||||
getter = {
|
|
||||||
val code = (this.thisObj as ObjConsoleKeyEvent).code
|
|
||||||
code?.let(::ObjString) ?: ObjNull
|
|
||||||
}
|
|
||||||
)
|
|
||||||
addPropertyDoc(
|
|
||||||
name = "ctrl",
|
|
||||||
doc = "Whether Ctrl modifier is pressed.",
|
|
||||||
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,11 +430,17 @@ 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)
|
||||||
consoleFlowDebug("jline-events: key stream idle ${idleMs}ms; scheduling reader recovery")
|
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(
|
consoleFlowDebug(
|
||||||
@@ -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,12 +588,15 @@ 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 {
|
||||||
key = value,
|
require(value.isNotEmpty()) { "ConsoleEvent.KeyDown.key must never be empty" }
|
||||||
code = null,
|
return ConsoleEvent.KeyDown(
|
||||||
ctrl = ctrl,
|
key = value,
|
||||||
alt = alt,
|
code = null,
|
||||||
shift = shift,
|
ctrl = ctrl,
|
||||||
meta = false
|
alt = alt,
|
||||||
)
|
shift = shift,
|
||||||
|
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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -101,6 +101,7 @@ kotlin {
|
|||||||
//put your multiplatform dependencies here
|
//put your multiplatform dependencies here
|
||||||
api(libs.kotlinx.coroutines.core)
|
api(libs.kotlinx.coroutines.core)
|
||||||
api(libs.mp.bintools)
|
api(libs.mp.bintools)
|
||||||
|
implementation(libs.ionspin.bignum)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val commonTest by getting {
|
val commonTest by getting {
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ class Compiler(
|
|||||||
private val encodedPayloadTypeByName: MutableMap<String, ObjClass> = mutableMapOf()
|
private val encodedPayloadTypeByName: MutableMap<String, ObjClass> = mutableMapOf()
|
||||||
private val objectDeclNames: MutableSet<String> = mutableSetOf()
|
private val objectDeclNames: MutableSet<String> = mutableSetOf()
|
||||||
private val externCallableNames: MutableSet<String> = mutableSetOf()
|
private val externCallableNames: MutableSet<String> = mutableSetOf()
|
||||||
|
private val externBindingNames: MutableSet<String> = mutableSetOf()
|
||||||
private val moduleDeclaredNames: MutableSet<String> = mutableSetOf()
|
private val moduleDeclaredNames: MutableSet<String> = mutableSetOf()
|
||||||
private var seedingSlotPlan: Boolean = false
|
private var seedingSlotPlan: Boolean = false
|
||||||
|
|
||||||
@@ -192,6 +193,7 @@ class Compiler(
|
|||||||
if (plan.slots.isEmpty()) return emptyMap()
|
if (plan.slots.isEmpty()) return emptyMap()
|
||||||
val result = LinkedHashMap<String, ForcedLocalSlotInfo>(plan.slots.size)
|
val result = LinkedHashMap<String, ForcedLocalSlotInfo>(plan.slots.size)
|
||||||
for ((name, entry) in plan.slots) {
|
for ((name, entry) in plan.slots) {
|
||||||
|
if (externBindingNames.contains(name)) continue
|
||||||
result[name] = ForcedLocalSlotInfo(
|
result[name] = ForcedLocalSlotInfo(
|
||||||
index = entry.index,
|
index = entry.index,
|
||||||
isMutable = entry.isMutable,
|
isMutable = entry.isMutable,
|
||||||
@@ -581,7 +583,7 @@ class Compiler(
|
|||||||
val baseNames = cls.directParents.map { it.className }
|
val baseNames = cls.directParents.map { it.className }
|
||||||
val nextFieldId = (fieldIds.values.maxOrNull() ?: -1) + 1
|
val nextFieldId = (fieldIds.values.maxOrNull() ?: -1) + 1
|
||||||
val nextMethodId = (methodIds.values.maxOrNull() ?: -1) + 1
|
val nextMethodId = (methodIds.values.maxOrNull() ?: -1) + 1
|
||||||
return CompileClassInfo(name, fieldIds, methodIds, nextFieldId, nextMethodId, baseNames)
|
return CompileClassInfo(name, cls.logicalPackageName, fieldIds, methodIds, nextFieldId, nextMethodId, baseNames)
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class BaseMemberIds(
|
private data class BaseMemberIds(
|
||||||
@@ -885,6 +887,7 @@ class Compiler(
|
|||||||
name = declaredName,
|
name = declaredName,
|
||||||
isMutable = false,
|
isMutable = false,
|
||||||
visibility = Visibility.Public,
|
visibility = Visibility.Public,
|
||||||
|
actualExtern = false,
|
||||||
initializer = initStmt,
|
initializer = initStmt,
|
||||||
isTransient = false,
|
isTransient = false,
|
||||||
typeDecl = null,
|
typeDecl = null,
|
||||||
@@ -1550,6 +1553,7 @@ class Compiler(
|
|||||||
|
|
||||||
private data class CompileClassInfo(
|
private data class CompileClassInfo(
|
||||||
val name: String,
|
val name: String,
|
||||||
|
val packageName: String?,
|
||||||
val fieldIds: Map<String, Int>,
|
val fieldIds: Map<String, Int>,
|
||||||
val methodIds: Map<String, Int>,
|
val methodIds: Map<String, Int>,
|
||||||
val nextFieldId: Int,
|
val nextFieldId: Int,
|
||||||
@@ -1751,6 +1755,7 @@ class Compiler(
|
|||||||
enumEntriesByName = enumEntriesByName,
|
enumEntriesByName = enumEntriesByName,
|
||||||
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
|
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
|
||||||
callableReturnTypeByName = callableReturnTypeByName,
|
callableReturnTypeByName = callableReturnTypeByName,
|
||||||
|
externBindingNames = externBindingNames,
|
||||||
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
|
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
|
||||||
) as BytecodeStatement
|
) as BytecodeStatement
|
||||||
unwrapped to bytecodeStmt.bytecodeFunction()
|
unwrapped to bytecodeStmt.bytecodeFunction()
|
||||||
@@ -1957,6 +1962,9 @@ class Compiler(
|
|||||||
val scopeIndex = slotPlanStack.indexOfLast { it.id == slotLoc.scopeId }
|
val scopeIndex = slotPlanStack.indexOfLast { it.id == slotLoc.scopeId }
|
||||||
if (functionIndex >= 0 && scopeIndex >= functionIndex) return null
|
if (functionIndex >= 0 && scopeIndex >= functionIndex) return null
|
||||||
val modulePlan = moduleSlotPlan()
|
val modulePlan = moduleSlotPlan()
|
||||||
|
if (modulePlan != null && slotLoc.scopeId == modulePlan.id && externBindingNames.contains(name)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
if (useScopeSlots && modulePlan != null && slotLoc.scopeId == modulePlan.id) {
|
if (useScopeSlots && modulePlan != null && slotLoc.scopeId == modulePlan.id) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -2075,6 +2083,7 @@ class Compiler(
|
|||||||
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
|
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
|
||||||
callableReturnTypeByName = callableReturnTypeByName,
|
callableReturnTypeByName = callableReturnTypeByName,
|
||||||
externCallableNames = externCallableNames,
|
externCallableNames = externCallableNames,
|
||||||
|
externBindingNames = externBindingNames,
|
||||||
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
|
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -2105,6 +2114,7 @@ class Compiler(
|
|||||||
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
|
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
|
||||||
callableReturnTypeByName = callableReturnTypeByName,
|
callableReturnTypeByName = callableReturnTypeByName,
|
||||||
externCallableNames = externCallableNames,
|
externCallableNames = externCallableNames,
|
||||||
|
externBindingNames = externBindingNames,
|
||||||
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
|
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -2160,6 +2170,7 @@ class Compiler(
|
|||||||
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
|
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
|
||||||
callableReturnTypeByName = callableReturnTypeByName,
|
callableReturnTypeByName = callableReturnTypeByName,
|
||||||
externCallableNames = externCallableNames,
|
externCallableNames = externCallableNames,
|
||||||
|
externBindingNames = externBindingNames,
|
||||||
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
|
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -2343,6 +2354,7 @@ class Compiler(
|
|||||||
stmt.name,
|
stmt.name,
|
||||||
stmt.isMutable,
|
stmt.isMutable,
|
||||||
stmt.visibility,
|
stmt.visibility,
|
||||||
|
stmt.actualExtern,
|
||||||
init,
|
init,
|
||||||
stmt.isTransient,
|
stmt.isTransient,
|
||||||
stmt.typeDecl,
|
stmt.typeDecl,
|
||||||
@@ -4655,6 +4667,7 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
is MethodCallRef -> methodReturnTypeDeclByRef[ref]
|
is MethodCallRef -> methodReturnTypeDeclByRef[ref]
|
||||||
is CallRef -> callReturnTypeDeclByRef[ref]
|
is CallRef -> callReturnTypeDeclByRef[ref]
|
||||||
|
is BinaryOpRef -> inferBinaryOpReturnTypeDecl(ref)
|
||||||
is StatementRef -> (ref.statement as? ExpressionStatement)?.let { resolveReceiverTypeDecl(it.ref) }
|
is StatementRef -> (ref.statement as? ExpressionStatement)?.let { resolveReceiverTypeDecl(it.ref) }
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
@@ -4718,6 +4731,7 @@ class Compiler(
|
|||||||
is ThisMethodSlotCallRef -> inferMethodCallReturnClass(ref.methodName())
|
is ThisMethodSlotCallRef -> inferMethodCallReturnClass(ref.methodName())
|
||||||
is QualifiedThisMethodSlotCallRef -> inferMethodCallReturnClass(ref.methodName())
|
is QualifiedThisMethodSlotCallRef -> inferMethodCallReturnClass(ref.methodName())
|
||||||
is CallRef -> inferCallReturnClass(ref)
|
is CallRef -> inferCallReturnClass(ref)
|
||||||
|
is BinaryOpRef -> inferBinaryOpReturnClass(ref)
|
||||||
is FieldRef -> {
|
is FieldRef -> {
|
||||||
val targetClass = resolveReceiverClassForMember(ref.target)
|
val targetClass = resolveReceiverClassForMember(ref.target)
|
||||||
inferFieldReturnClass(targetClass, ref.name)
|
inferFieldReturnClass(targetClass, ref.name)
|
||||||
@@ -4732,10 +4746,105 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun typeDeclOfClass(objClass: ObjClass): TypeDecl = TypeDecl.Simple(objClass.className, false)
|
||||||
|
|
||||||
|
private fun binaryOpMethodName(op: BinOp): String? = when (op) {
|
||||||
|
BinOp.PLUS -> "plus"
|
||||||
|
BinOp.MINUS -> "minus"
|
||||||
|
BinOp.STAR -> "mul"
|
||||||
|
BinOp.SLASH -> "div"
|
||||||
|
BinOp.PERCENT -> "mod"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun interopOperatorFor(op: BinOp): InteropOperator? = when (op) {
|
||||||
|
BinOp.PLUS -> InteropOperator.Plus
|
||||||
|
BinOp.MINUS -> InteropOperator.Minus
|
||||||
|
BinOp.STAR -> InteropOperator.Mul
|
||||||
|
BinOp.SLASH -> InteropOperator.Div
|
||||||
|
BinOp.PERCENT -> InteropOperator.Mod
|
||||||
|
BinOp.LT, BinOp.LTE, BinOp.GT, BinOp.GTE -> InteropOperator.Compare
|
||||||
|
BinOp.EQ, BinOp.NEQ -> InteropOperator.Equals
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sameClassArithmeticFallback(leftClass: ObjClass, rightClass: ObjClass, op: BinOp): TypeDecl? {
|
||||||
|
if (leftClass !== rightClass) return null
|
||||||
|
val methodName = binaryOpMethodName(op) ?: return null
|
||||||
|
if (leftClass.getInstanceMemberOrNull(methodName, includeAbstract = true) == null) return null
|
||||||
|
return typeDeclOfClass(leftClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun inferBinaryOpReturnTypeDecl(ref: BinaryOpRef): TypeDecl? {
|
||||||
|
val leftClass = resolveReceiverClassForMember(ref.left) ?: inferObjClassFromRef(ref.left)
|
||||||
|
val rightClass = resolveReceiverClassForMember(ref.right) ?: inferObjClassFromRef(ref.right)
|
||||||
|
val boolType = typeDeclOfClass(ObjBool.type)
|
||||||
|
val intType = typeDeclOfClass(ObjInt.type)
|
||||||
|
val realType = typeDeclOfClass(ObjReal.type)
|
||||||
|
val stringType = typeDeclOfClass(ObjString.type)
|
||||||
|
|
||||||
|
when (ref.op) {
|
||||||
|
BinOp.OR, BinOp.AND,
|
||||||
|
BinOp.EQARROW, BinOp.EQ, BinOp.NEQ, BinOp.REF_EQ, BinOp.REF_NEQ, BinOp.MATCH, BinOp.NOTMATCH,
|
||||||
|
BinOp.LTE, BinOp.LT, BinOp.GTE, BinOp.GT,
|
||||||
|
BinOp.IN, BinOp.NOTIN,
|
||||||
|
BinOp.IS, BinOp.NOTIS -> return boolType
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (leftClass == null || rightClass == null) return null
|
||||||
|
|
||||||
|
val leftIsInt = leftClass == ObjInt.type
|
||||||
|
val rightIsInt = rightClass == ObjInt.type
|
||||||
|
val leftIsReal = leftClass == ObjReal.type
|
||||||
|
val rightIsReal = rightClass == ObjReal.type
|
||||||
|
val leftIsNumeric = leftIsInt || leftIsReal
|
||||||
|
val rightIsNumeric = rightIsInt || rightIsReal
|
||||||
|
|
||||||
|
return when (ref.op) {
|
||||||
|
BinOp.PLUS -> when {
|
||||||
|
leftIsInt && rightIsInt -> intType
|
||||||
|
leftIsNumeric && rightIsNumeric -> realType
|
||||||
|
leftClass == ObjString.type -> stringType
|
||||||
|
interopOperatorFor(ref.op)?.let {
|
||||||
|
OperatorInteropRegistry.commonClassFor(leftClass, rightClass, it)
|
||||||
|
} != null -> typeDeclOfClass(
|
||||||
|
OperatorInteropRegistry.commonClassFor(leftClass, rightClass, interopOperatorFor(ref.op)!!)!!
|
||||||
|
)
|
||||||
|
else -> binaryOpMethodName(ref.op)?.let { classMethodReturnTypeDecl(leftClass, it) }
|
||||||
|
?: sameClassArithmeticFallback(leftClass, rightClass, ref.op)
|
||||||
|
}
|
||||||
|
BinOp.MINUS, BinOp.STAR, BinOp.SLASH, BinOp.PERCENT -> when {
|
||||||
|
leftIsInt && rightIsInt -> intType
|
||||||
|
leftIsNumeric && rightIsNumeric -> realType
|
||||||
|
ref.op == BinOp.STAR && leftClass == ObjString.type && rightIsNumeric -> stringType
|
||||||
|
interopOperatorFor(ref.op)?.let {
|
||||||
|
OperatorInteropRegistry.commonClassFor(leftClass, rightClass, it)
|
||||||
|
} != null -> typeDeclOfClass(
|
||||||
|
OperatorInteropRegistry.commonClassFor(leftClass, rightClass, interopOperatorFor(ref.op)!!)!!
|
||||||
|
)
|
||||||
|
else -> binaryOpMethodName(ref.op)?.let { classMethodReturnTypeDecl(leftClass, it) }
|
||||||
|
?: sameClassArithmeticFallback(leftClass, rightClass, ref.op)
|
||||||
|
}
|
||||||
|
BinOp.BAND, BinOp.BOR, BinOp.BXOR, BinOp.SHL, BinOp.SHR ->
|
||||||
|
if (leftIsInt && rightIsInt) intType else null
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun inferBinaryOpReturnClass(ref: BinaryOpRef): ObjClass? {
|
private fun inferBinaryOpReturnClass(ref: BinaryOpRef): ObjClass? {
|
||||||
|
inferBinaryOpReturnTypeDecl(ref)?.let { declared ->
|
||||||
|
resolveTypeDeclObjClass(declared)?.let { return it }
|
||||||
|
}
|
||||||
val leftClass = resolveReceiverClassForMember(ref.left) ?: inferObjClassFromRef(ref.left)
|
val leftClass = resolveReceiverClassForMember(ref.left) ?: inferObjClassFromRef(ref.left)
|
||||||
val rightClass = resolveReceiverClassForMember(ref.right) ?: inferObjClassFromRef(ref.right)
|
val rightClass = resolveReceiverClassForMember(ref.right) ?: inferObjClassFromRef(ref.right)
|
||||||
if (leftClass == null || rightClass == null) return null
|
if (leftClass == null || rightClass == null) return null
|
||||||
|
interopOperatorFor(ref.op)?.let { op ->
|
||||||
|
OperatorInteropRegistry.commonClassFor(leftClass, rightClass, op)?.let { return it }
|
||||||
|
}
|
||||||
|
sameClassArithmeticFallback(leftClass, rightClass, ref.op)?.let { declared ->
|
||||||
|
resolveTypeDeclObjClass(declared)?.let { return it }
|
||||||
|
}
|
||||||
return when (ref.op) {
|
return when (ref.op) {
|
||||||
BinOp.PLUS, BinOp.MINUS -> when {
|
BinOp.PLUS, BinOp.MINUS -> when {
|
||||||
leftClass == ObjInstant.type && rightClass == ObjInstant.type && ref.op == BinOp.MINUS -> ObjDuration.type
|
leftClass == ObjInstant.type && rightClass == ObjInstant.type && ref.op == BinOp.MINUS -> ObjDuration.type
|
||||||
@@ -6446,7 +6555,7 @@ class Compiler(
|
|||||||
WhenStatement(value, cases, elseCase, whenPos)
|
WhenStatement(value, cases, elseCase, whenPos)
|
||||||
} else {
|
} else {
|
||||||
// when { cond -> ... }
|
// when { cond -> ... }
|
||||||
TODO("when without object is not yet implemented")
|
throw ScriptError(t.pos, "when without subject is not implemented")
|
||||||
}
|
}
|
||||||
return wrapBytecode(stmt)
|
return wrapBytecode(stmt)
|
||||||
}
|
}
|
||||||
@@ -6699,6 +6808,7 @@ class Compiler(
|
|||||||
)
|
)
|
||||||
compileClassInfos[qualifiedName] = CompileClassInfo(
|
compileClassInfos[qualifiedName] = CompileClassInfo(
|
||||||
name = qualifiedName,
|
name = qualifiedName,
|
||||||
|
packageName = packageName,
|
||||||
fieldIds = fieldIds,
|
fieldIds = fieldIds,
|
||||||
methodIds = methodIds,
|
methodIds = methodIds,
|
||||||
nextFieldId = fieldIds.size,
|
nextFieldId = fieldIds.size,
|
||||||
@@ -6821,6 +6931,7 @@ class Compiler(
|
|||||||
classCtx?.let { ctx ->
|
classCtx?.let { ctx ->
|
||||||
compileClassInfos[className] = CompileClassInfo(
|
compileClassInfos[className] = CompileClassInfo(
|
||||||
name = className,
|
name = className,
|
||||||
|
packageName = packageName,
|
||||||
fieldIds = ctx.memberFieldIds.toMap(),
|
fieldIds = ctx.memberFieldIds.toMap(),
|
||||||
methodIds = ctx.memberMethodIds.toMap(),
|
methodIds = ctx.memberMethodIds.toMap(),
|
||||||
nextFieldId = ctx.nextFieldId,
|
nextFieldId = ctx.nextFieldId,
|
||||||
@@ -6862,6 +6973,7 @@ class Compiler(
|
|||||||
val baseIds = collectBaseMemberIds(baseSpecs.map { it.name })
|
val baseIds = collectBaseMemberIds(baseSpecs.map { it.name })
|
||||||
compileClassInfos[className] = CompileClassInfo(
|
compileClassInfos[className] = CompileClassInfo(
|
||||||
name = className,
|
name = className,
|
||||||
|
packageName = packageName,
|
||||||
fieldIds = baseIds.fieldIds,
|
fieldIds = baseIds.fieldIds,
|
||||||
methodIds = baseIds.methodIds,
|
methodIds = baseIds.methodIds,
|
||||||
nextFieldId = baseIds.nextFieldId,
|
nextFieldId = baseIds.nextFieldId,
|
||||||
@@ -7110,6 +7222,7 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
compileClassInfos[qualifiedName] = CompileClassInfo(
|
compileClassInfos[qualifiedName] = CompileClassInfo(
|
||||||
name = qualifiedName,
|
name = qualifiedName,
|
||||||
|
packageName = packageName,
|
||||||
fieldIds = ctx.memberFieldIds.toMap(),
|
fieldIds = ctx.memberFieldIds.toMap(),
|
||||||
methodIds = ctx.memberMethodIds.toMap(),
|
methodIds = ctx.memberMethodIds.toMap(),
|
||||||
nextFieldId = ctx.nextFieldId,
|
nextFieldId = ctx.nextFieldId,
|
||||||
@@ -7125,6 +7238,7 @@ class Compiler(
|
|||||||
classCtx?.let { ctx ->
|
classCtx?.let { ctx ->
|
||||||
compileClassInfos[qualifiedName] = CompileClassInfo(
|
compileClassInfos[qualifiedName] = CompileClassInfo(
|
||||||
name = qualifiedName,
|
name = qualifiedName,
|
||||||
|
packageName = packageName,
|
||||||
fieldIds = ctx.memberFieldIds.toMap(),
|
fieldIds = ctx.memberFieldIds.toMap(),
|
||||||
methodIds = ctx.memberMethodIds.toMap(),
|
methodIds = ctx.memberMethodIds.toMap(),
|
||||||
nextFieldId = ctx.nextFieldId,
|
nextFieldId = ctx.nextFieldId,
|
||||||
@@ -7211,6 +7325,7 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
compileClassInfos[qualifiedName] = CompileClassInfo(
|
compileClassInfos[qualifiedName] = CompileClassInfo(
|
||||||
name = qualifiedName,
|
name = qualifiedName,
|
||||||
|
packageName = packageName,
|
||||||
fieldIds = ctx.memberFieldIds.toMap(),
|
fieldIds = ctx.memberFieldIds.toMap(),
|
||||||
methodIds = ctx.memberMethodIds.toMap(),
|
methodIds = ctx.memberMethodIds.toMap(),
|
||||||
nextFieldId = ctx.nextFieldId,
|
nextFieldId = ctx.nextFieldId,
|
||||||
@@ -8390,7 +8505,7 @@ class Compiler(
|
|||||||
is ImplicitThisMethodCallRef -> {
|
is ImplicitThisMethodCallRef -> {
|
||||||
when (directRef.methodName()) {
|
when (directRef.methodName()) {
|
||||||
"iterator" -> ObjIterator
|
"iterator" -> ObjIterator
|
||||||
"lazy" -> ObjLazyDelegate.type
|
"lazy" -> resolveClassByName("lazy") ?: inferMethodCallReturnClass(directRef.methodName())
|
||||||
else -> inferMethodCallReturnClass(directRef.methodName())
|
else -> inferMethodCallReturnClass(directRef.methodName())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8409,7 +8524,7 @@ class Compiler(
|
|||||||
when {
|
when {
|
||||||
target is LocalSlotRef -> {
|
target is LocalSlotRef -> {
|
||||||
when (target.name) {
|
when (target.name) {
|
||||||
"lazy" -> ObjLazyDelegate.type
|
"lazy" -> resolveClassByName("lazy")
|
||||||
"iterator" -> ObjIterator
|
"iterator" -> ObjIterator
|
||||||
"flow" -> ObjFlow.type
|
"flow" -> ObjFlow.type
|
||||||
"launch" -> ObjDeferred.type
|
"launch" -> ObjDeferred.type
|
||||||
@@ -8420,7 +8535,7 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
target is LocalVarRef -> {
|
target is LocalVarRef -> {
|
||||||
when (target.name) {
|
when (target.name) {
|
||||||
"lazy" -> ObjLazyDelegate.type
|
"lazy" -> resolveClassByName("lazy")
|
||||||
"iterator" -> ObjIterator
|
"iterator" -> ObjIterator
|
||||||
"flow" -> ObjFlow.type
|
"flow" -> ObjFlow.type
|
||||||
"launch" -> ObjDeferred.type
|
"launch" -> ObjDeferred.type
|
||||||
@@ -8459,7 +8574,9 @@ class Compiler(
|
|||||||
?: unwrapDirectRef(initializer)?.let { inferObjClassFromRef(it) }
|
?: unwrapDirectRef(initializer)?.let { inferObjClassFromRef(it) }
|
||||||
?: throw ScriptError(initializer.pos, "Delegate type must be known at compile time")
|
?: throw ScriptError(initializer.pos, "Delegate type must be known at compile time")
|
||||||
if (initClass !== delegateClass &&
|
if (initClass !== delegateClass &&
|
||||||
|
initClass.logicalName != delegateClass.logicalName &&
|
||||||
!initClass.allParentsSet.contains(delegateClass) &&
|
!initClass.allParentsSet.contains(delegateClass) &&
|
||||||
|
!initClass.allParentsSet.any { it.logicalName == delegateClass.logicalName } &&
|
||||||
!initClass.allImplementingNames.contains(delegateClass.className)
|
!initClass.allImplementingNames.contains(delegateClass.className)
|
||||||
) {
|
) {
|
||||||
throw ScriptError(
|
throw ScriptError(
|
||||||
@@ -8549,7 +8666,9 @@ class Compiler(
|
|||||||
if (closedParent != null) {
|
if (closedParent != null) {
|
||||||
throw ScriptError(Pos.builtIn, "can't inherit from closed class ${closedParent.className}")
|
throw ScriptError(Pos.builtIn, "can't inherit from closed class ${closedParent.className}")
|
||||||
}
|
}
|
||||||
ObjInstanceClass(info.name, *parents.toTypedArray())
|
ObjInstanceClass(info.name, *parents.toTypedArray()).apply {
|
||||||
|
logicalPackageNameOverride = info.packageName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (stub is ObjInstanceClass) {
|
if (stub is ObjInstanceClass) {
|
||||||
for ((fieldName, fieldId) in info.fieldIds) {
|
for ((fieldName, fieldId) in info.fieldIds) {
|
||||||
@@ -8893,7 +9012,7 @@ class Compiler(
|
|||||||
val effectiveEqToken = if (isProperty) null else eqToken
|
val effectiveEqToken = if (isProperty) null else eqToken
|
||||||
|
|
||||||
// Register the local name at compile time so that subsequent identifiers can be emitted as fast locals
|
// Register the local name at compile time so that subsequent identifiers can be emitted as fast locals
|
||||||
if (!isStatic && declaringClassNameCaptured == null) declareLocalName(name, isMutable)
|
if (!isStatic && declaringClassNameCaptured == null && !actualExtern) declareLocalName(name, isMutable)
|
||||||
val declKind = if (codeContexts.lastOrNull() is CodeContext.ClassBody) {
|
val declKind = if (codeContexts.lastOrNull() is CodeContext.ClassBody) {
|
||||||
SymbolKind.MEMBER
|
SymbolKind.MEMBER
|
||||||
} else {
|
} else {
|
||||||
@@ -8902,6 +9021,8 @@ class Compiler(
|
|||||||
resolutionSink?.declareSymbol(name, declKind, isMutable, nameStartPos, isOverride = isOverride)
|
resolutionSink?.declareSymbol(name, declKind, isMutable, nameStartPos, isOverride = isOverride)
|
||||||
if (declKind == SymbolKind.MEMBER && extTypeName == null) {
|
if (declKind == SymbolKind.MEMBER && extTypeName == null) {
|
||||||
(codeContexts.lastOrNull() as? CodeContext.ClassBody)?.declaredMembers?.add(name)
|
(codeContexts.lastOrNull() as? CodeContext.ClassBody)?.declaredMembers?.add(name)
|
||||||
|
} else if (actualExtern) {
|
||||||
|
externBindingNames.add(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
val isDelegate = if (isAbstract || actualExtern) {
|
val isDelegate = if (isAbstract || actualExtern) {
|
||||||
@@ -8967,7 +9088,11 @@ class Compiler(
|
|||||||
|
|
||||||
if (isDelegate && initialExpression != null) {
|
if (isDelegate && initialExpression != null) {
|
||||||
ensureDelegateType(initialExpression)
|
ensureDelegateType(initialExpression)
|
||||||
if (isMutable && resolveInitializerObjClass(initialExpression) == ObjLazyDelegate.type) {
|
val lazyClass = resolveClassByName("lazy")
|
||||||
|
if (isMutable &&
|
||||||
|
lazyClass != null &&
|
||||||
|
resolveInitializerObjClass(initialExpression)?.logicalName == lazyClass.logicalName
|
||||||
|
) {
|
||||||
throw ScriptError(initialExpression.pos, "lazy delegate is read-only")
|
throw ScriptError(initialExpression.pos, "lazy delegate is read-only")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9059,6 +9184,7 @@ class Compiler(
|
|||||||
name,
|
name,
|
||||||
isMutable,
|
isMutable,
|
||||||
visibility,
|
visibility,
|
||||||
|
actualExtern,
|
||||||
initialExpression,
|
initialExpression,
|
||||||
isTransient,
|
isTransient,
|
||||||
declaredType,
|
declaredType,
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.sergeych.lyng
|
||||||
|
|
||||||
|
import net.sergeych.lyng.obj.Obj
|
||||||
|
import net.sergeych.lyng.obj.ObjClass
|
||||||
|
import net.sergeych.lyng.obj.ObjInt
|
||||||
|
import net.sergeych.lyng.obj.ObjVoid
|
||||||
|
|
||||||
|
internal enum class InteropOperator(val memberName: String?) {
|
||||||
|
Plus("plus"),
|
||||||
|
Minus("minus"),
|
||||||
|
Mul("mul"),
|
||||||
|
Div("div"),
|
||||||
|
Mod("mod"),
|
||||||
|
Compare("compareTo"),
|
||||||
|
Equals("equals");
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromName(name: String): InteropOperator =
|
||||||
|
entries.firstOrNull { it.name == name }
|
||||||
|
?: throw IllegalArgumentException("unknown interop operator: $name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class InteropRule(
|
||||||
|
val commonClass: ObjClass,
|
||||||
|
val operators: Set<InteropOperator>,
|
||||||
|
val leftToCommon: Obj,
|
||||||
|
val rightToCommon: Obj
|
||||||
|
)
|
||||||
|
|
||||||
|
internal data class PromotedOperands(val left: Obj, val right: Obj)
|
||||||
|
|
||||||
|
internal object OperatorInteropRegistry {
|
||||||
|
private val rules = mutableMapOf<Pair<ObjClass, ObjClass>, InteropRule>()
|
||||||
|
|
||||||
|
fun register(
|
||||||
|
leftClass: ObjClass,
|
||||||
|
rightClass: ObjClass,
|
||||||
|
commonClass: ObjClass,
|
||||||
|
operatorNames: List<String>,
|
||||||
|
leftToCommon: Obj,
|
||||||
|
rightToCommon: Obj
|
||||||
|
) {
|
||||||
|
val operators = operatorNames.mapTo(linkedSetOf(), InteropOperator::fromName)
|
||||||
|
rules[leftClass to rightClass] = InteropRule(commonClass, operators, leftToCommon, rightToCommon)
|
||||||
|
if (leftClass !== rightClass) {
|
||||||
|
rules[rightClass to leftClass] = InteropRule(commonClass, operators, rightToCommon, leftToCommon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun promote(scope: Scope, left: Obj, right: Obj, operator: InteropOperator): PromotedOperands? {
|
||||||
|
val leftValue = unwrap(left)
|
||||||
|
val rightValue = unwrap(right)
|
||||||
|
val rule = rules[leftValue.objClass to rightValue.objClass] ?: return null
|
||||||
|
if (operator !in rule.operators) return null
|
||||||
|
val promotedLeft = rule.leftToCommon.invoke(scope, ObjVoid, Arguments(leftValue))
|
||||||
|
val promotedRight = rule.rightToCommon.invoke(scope, ObjVoid, Arguments(rightValue))
|
||||||
|
if (promotedLeft.objClass !== rule.commonClass || promotedRight.objClass !== rule.commonClass) {
|
||||||
|
scope.raiseIllegalState(
|
||||||
|
"Operator interop promotion must return ${rule.commonClass.className}, " +
|
||||||
|
"got ${promotedLeft.objClass.className} and ${promotedRight.objClass.className}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return PromotedOperands(promotedLeft, promotedRight)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun invokeBinary(scope: Scope, left: Obj, right: Obj, operator: InteropOperator): Obj? {
|
||||||
|
val promoted = promote(scope, left, right, operator) ?: return null
|
||||||
|
val memberName = operator.memberName ?: return null
|
||||||
|
return promoted.left.invokeInstanceMethod(scope, memberName, Arguments(promoted.right))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun invokeCompare(scope: Scope, left: Obj, right: Obj): Int? {
|
||||||
|
val promoted = promote(scope, left, right, InteropOperator.Compare) ?: return null
|
||||||
|
return promoted.left.invokeInstanceMethod(scope, "compareTo", Arguments(promoted.right))
|
||||||
|
.cast<ObjInt>(scope)
|
||||||
|
.value
|
||||||
|
.toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun commonClassFor(leftClass: ObjClass, rightClass: ObjClass, operator: InteropOperator): ObjClass? {
|
||||||
|
val rule = rules[leftClass to rightClass] ?: return null
|
||||||
|
if (operator !in rule.operators) return null
|
||||||
|
return rule.commonClass
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun unwrap(obj: Obj): Obj = when (obj) {
|
||||||
|
is FrameSlotRef -> obj.read()
|
||||||
|
is RecordSlotRef -> obj.read()
|
||||||
|
else -> obj
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ import net.sergeych.lyng.bytecode.CmdVm
|
|||||||
import net.sergeych.lyng.miniast.*
|
import net.sergeych.lyng.miniast.*
|
||||||
import net.sergeych.lyng.obj.*
|
import net.sergeych.lyng.obj.*
|
||||||
import net.sergeych.lyng.pacman.ImportManager
|
import net.sergeych.lyng.pacman.ImportManager
|
||||||
|
import net.sergeych.lyng.stdlib_included.decimalLyng
|
||||||
import net.sergeych.lyng.stdlib_included.observableLyng
|
import net.sergeych.lyng.stdlib_included.observableLyng
|
||||||
|
import net.sergeych.lyng.stdlib_included.operatorsLyng
|
||||||
import net.sergeych.lyng.stdlib_included.rootLyng
|
import net.sergeych.lyng.stdlib_included.rootLyng
|
||||||
import net.sergeych.lynon.ObjLynonClass
|
import net.sergeych.lynon.ObjLynonClass
|
||||||
import net.sergeych.mp_tools.globalDefer
|
import net.sergeych.mp_tools.globalDefer
|
||||||
@@ -46,6 +48,28 @@ class Script(
|
|||||||
// private val catchReturn: Boolean = false,
|
// private val catchReturn: Boolean = false,
|
||||||
) : Statement() {
|
) : Statement() {
|
||||||
fun statements(): List<Statement> = statements
|
fun statements(): List<Statement> = statements
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Explicitly apply this script's import/module bindings to [scope] without executing the script.
|
||||||
|
* This is intended for embedding scenarios where the host owns scope lifecycle and wants
|
||||||
|
* script-specific imports to be a separate, opt-in preparation step.
|
||||||
|
*/
|
||||||
|
suspend fun importInto(scope: Scope, seedScope: Scope = scope): Scope {
|
||||||
|
prepareScriptScope(scope, seedScope)
|
||||||
|
return scope
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a fresh raw module scope and apply this script's import/module bindings to it.
|
||||||
|
* If [seedScope] is provided, its import provider is reused and seed-bound imports resolve from it.
|
||||||
|
*/
|
||||||
|
suspend fun instantiateModule(seedScope: Scope? = null, pos: Pos = this.pos): ModuleScope {
|
||||||
|
val provider = seedScope?.currentImportProvider ?: defaultImportManager
|
||||||
|
val module = provider.newModuleAt(pos)
|
||||||
|
prepareScriptScope(module, seedScope ?: module)
|
||||||
|
return module
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun execute(scope: Scope): Obj {
|
override suspend fun execute(scope: Scope): Obj {
|
||||||
scope.pos = pos
|
scope.pos = pos
|
||||||
val execScope = resolveModuleScope(scope) ?: scope
|
val execScope = resolveModuleScope(scope) ?: scope
|
||||||
@@ -75,6 +99,19 @@ class Script(
|
|||||||
return ObjVoid
|
return ObjVoid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun prepareScriptScope(scope: Scope, seedScope: Scope) {
|
||||||
|
if (importBindings.isNotEmpty() || importedModules.isNotEmpty()) {
|
||||||
|
seedImportBindings(scope, seedScope)
|
||||||
|
}
|
||||||
|
if (moduleSlotPlan.isNotEmpty()) {
|
||||||
|
scope.applySlotPlan(moduleSlotPlan)
|
||||||
|
for (name in moduleSlotPlan.keys) {
|
||||||
|
val record = scope.objects[name] ?: scope.localBindings[name] ?: continue
|
||||||
|
scope.updateSlotFor(name, record)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun seedModuleSlots(scope: Scope, seedScope: Scope) {
|
private suspend fun seedModuleSlots(scope: Scope, seedScope: Scope) {
|
||||||
if (importBindings.isEmpty() && importedModules.isEmpty()) return
|
if (importBindings.isEmpty() && importedModules.isEmpty()) return
|
||||||
seedImportBindings(scope, seedScope)
|
seedImportBindings(scope, seedScope)
|
||||||
@@ -505,10 +542,6 @@ class Script(
|
|||||||
cachedValue
|
cachedValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
addFn("lazy") {
|
|
||||||
val builder = requireOnlyArg<Obj>()
|
|
||||||
ObjLazyDelegate(builder, requireScope())
|
|
||||||
}
|
|
||||||
addVoidFn("delay") {
|
addVoidFn("delay") {
|
||||||
val a = args.firstAndOnly()
|
val a = args.firstAndOnly()
|
||||||
when (a) {
|
when (a) {
|
||||||
@@ -696,6 +729,36 @@ class Script(
|
|||||||
module.addConst("ChangeRejectionException", ObjChangeRejectionExceptionClass)
|
module.addConst("ChangeRejectionException", ObjChangeRejectionExceptionClass)
|
||||||
module.eval(Source("lyng.observable", observableLyng))
|
module.eval(Source("lyng.observable", observableLyng))
|
||||||
}
|
}
|
||||||
|
addPackage("lyng.operators") { module ->
|
||||||
|
module.eval(Source("lyng.operators", operatorsLyng))
|
||||||
|
module.bindObject("OperatorInterop") {
|
||||||
|
addFun("register") {
|
||||||
|
val leftClass = requiredArg<ObjClass>(0)
|
||||||
|
val rightClass = requiredArg<ObjClass>(1)
|
||||||
|
val commonClass = requiredArg<ObjClass>(2)
|
||||||
|
val operators = requiredArg<ObjList>(3).list.map { value ->
|
||||||
|
val entry = value as? ObjEnumEntry
|
||||||
|
?: requireScope().raiseIllegalArgument(
|
||||||
|
"OperatorInterop.register expects BinaryOperator enum entries"
|
||||||
|
)
|
||||||
|
entry.name.value
|
||||||
|
}
|
||||||
|
OperatorInteropRegistry.register(
|
||||||
|
leftClass = leftClass,
|
||||||
|
rightClass = rightClass,
|
||||||
|
commonClass = commonClass,
|
||||||
|
operatorNames = operators,
|
||||||
|
leftToCommon = args[4],
|
||||||
|
rightToCommon = args[5]
|
||||||
|
)
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addPackage("lyng.decimal") { module ->
|
||||||
|
module.eval(Source("lyng.decimal", decimalLyng))
|
||||||
|
ObjBigDecimalSupport.bindTo(module)
|
||||||
|
}
|
||||||
addPackage("lyng.buffer") {
|
addPackage("lyng.buffer") {
|
||||||
it.addConstDoc(
|
it.addConstDoc(
|
||||||
name = "Buffer",
|
name = "Buffer",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class VarDeclStatement(
|
|||||||
val name: String,
|
val name: String,
|
||||||
val isMutable: Boolean,
|
val isMutable: Boolean,
|
||||||
val visibility: Visibility,
|
val visibility: Visibility,
|
||||||
|
val actualExtern: Boolean,
|
||||||
val initializer: Statement?,
|
val initializer: Statement?,
|
||||||
val isTransient: Boolean,
|
val isTransient: Boolean,
|
||||||
val typeDecl: TypeDecl?,
|
val typeDecl: TypeDecl?,
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class BytecodeCompiler(
|
|||||||
private val callableReturnTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(),
|
private val callableReturnTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(),
|
||||||
private val callableReturnTypeByName: Map<String, ObjClass> = emptyMap(),
|
private val callableReturnTypeByName: Map<String, ObjClass> = emptyMap(),
|
||||||
private val externCallableNames: Set<String> = emptySet(),
|
private val externCallableNames: Set<String> = emptySet(),
|
||||||
|
private val externBindingNames: Set<String> = emptySet(),
|
||||||
private val lambdaCaptureEntriesByRef: Map<ValueFnRef, List<LambdaCaptureEntry>> = emptyMap(),
|
private val lambdaCaptureEntriesByRef: Map<ValueFnRef, List<LambdaCaptureEntry>> = emptyMap(),
|
||||||
) {
|
) {
|
||||||
private val useScopeSlots: Boolean = allowedScopeNames != null || scopeSlotNameSet != null
|
private val useScopeSlots: Boolean = allowedScopeNames != null || scopeSlotNameSet != null
|
||||||
@@ -1088,7 +1089,6 @@ class BytecodeCompiler(
|
|||||||
|
|
||||||
private fun isDelegateClass(receiverClass: ObjClass): Boolean =
|
private fun isDelegateClass(receiverClass: ObjClass): Boolean =
|
||||||
receiverClass.className == "Delegate" ||
|
receiverClass.className == "Delegate" ||
|
||||||
receiverClass.className == "LazyDelegate" ||
|
|
||||||
receiverClass.implementingNames.contains("Delegate")
|
receiverClass.implementingNames.contains("Delegate")
|
||||||
|
|
||||||
private fun operatorMemberName(op: BinOp): String? = when (op) {
|
private fun operatorMemberName(op: BinOp): String? = when (op) {
|
||||||
@@ -4508,7 +4508,7 @@ class BytecodeCompiler(
|
|||||||
val resolved = slotTypes[slot] ?: SlotType.UNKNOWN
|
val resolved = slotTypes[slot] ?: SlotType.UNKNOWN
|
||||||
return CompiledValue(slot, resolved)
|
return CompiledValue(slot, resolved)
|
||||||
}
|
}
|
||||||
if (useScopeSlots && allowedScopeNames?.contains(name) == true) {
|
if (useScopeSlots && isPreparedScopeName(name)) {
|
||||||
scopeSlotIndexByName[name]?.let { slot ->
|
scopeSlotIndexByName[name]?.let { slot ->
|
||||||
val resolved = slotTypes[slot] ?: SlotType.UNKNOWN
|
val resolved = slotTypes[slot] ?: SlotType.UNKNOWN
|
||||||
return CompiledValue(slot, resolved)
|
return CompiledValue(slot, resolved)
|
||||||
@@ -8073,7 +8073,7 @@ class BytecodeCompiler(
|
|||||||
slotInitClassByKey[ScopeSlotKey(scopeId, slotIndex)] = cls
|
slotInitClassByKey[ScopeSlotKey(scopeId, slotIndex)] = cls
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (allowLocalSlots && slotIndex != null && !shouldUseScopeSlotFor(scopeId)) {
|
if (allowLocalSlots && slotIndex != null && (stmt.actualExtern || !shouldUseScopeSlotFor(scopeId, stmt.name, isDelegated = false))) {
|
||||||
val key = ScopeSlotKey(scopeId, slotIndex)
|
val key = ScopeSlotKey(scopeId, slotIndex)
|
||||||
declaredLocalKeys.add(key)
|
declaredLocalKeys.add(key)
|
||||||
if (!localSlotInfoMap.containsKey(key)) {
|
if (!localSlotInfoMap.containsKey(key)) {
|
||||||
@@ -8100,7 +8100,7 @@ class BytecodeCompiler(
|
|||||||
val scopeId = stmt.spec.scopeId ?: 0
|
val scopeId = stmt.spec.scopeId ?: 0
|
||||||
if (slotIndex != null) {
|
if (slotIndex != null) {
|
||||||
val key = ScopeSlotKey(scopeId, slotIndex)
|
val key = ScopeSlotKey(scopeId, slotIndex)
|
||||||
if (allowLocalSlots && !shouldUseScopeSlotFor(scopeId)) {
|
if (allowLocalSlots && !shouldUseScopeSlotFor(scopeId, stmt.spec.name, isDelegated = false)) {
|
||||||
if (!localSlotInfoMap.containsKey(key)) {
|
if (!localSlotInfoMap.containsKey(key)) {
|
||||||
localSlotInfoMap[key] = LocalSlotInfo(stmt.spec.name, isMutable = false, isDelegated = false)
|
localSlotInfoMap[key] = LocalSlotInfo(stmt.spec.name, isMutable = false, isDelegated = false)
|
||||||
}
|
}
|
||||||
@@ -8117,7 +8117,7 @@ class BytecodeCompiler(
|
|||||||
is DelegatedVarDeclStatement -> {
|
is DelegatedVarDeclStatement -> {
|
||||||
val slotIndex = stmt.slotIndex
|
val slotIndex = stmt.slotIndex
|
||||||
val scopeId = stmt.scopeId ?: 0
|
val scopeId = stmt.scopeId ?: 0
|
||||||
if (allowLocalSlots && slotIndex != null && !shouldUseScopeSlotFor(scopeId)) {
|
if (allowLocalSlots && slotIndex != null && !shouldUseScopeSlotFor(scopeId, stmt.name, isDelegated = true)) {
|
||||||
val key = ScopeSlotKey(scopeId, slotIndex)
|
val key = ScopeSlotKey(scopeId, slotIndex)
|
||||||
declaredLocalKeys.add(key)
|
declaredLocalKeys.add(key)
|
||||||
if (!localSlotInfoMap.containsKey(key)) {
|
if (!localSlotInfoMap.containsKey(key)) {
|
||||||
@@ -8283,15 +8283,26 @@ class BytecodeCompiler(
|
|||||||
|
|
||||||
private fun isModuleSlot(scopeId: Int, name: String?): Boolean {
|
private fun isModuleSlot(scopeId: Int, name: String?): Boolean {
|
||||||
if (moduleScopeId != null && scopeId != moduleScopeId) return false
|
if (moduleScopeId != null && scopeId != moduleScopeId) return false
|
||||||
val scopeNames = allowedScopeNames ?: scopeSlotNameSet
|
return isPreparedScopeName(name)
|
||||||
if (scopeNames == null || name == null) return false
|
|
||||||
return scopeNames.contains(name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shouldUseScopeSlotFor(scopeId: Int): Boolean {
|
private fun shouldUseScopeSlotFor(scopeId: Int): Boolean {
|
||||||
return useScopeSlots && moduleScopeId != null && scopeId == moduleScopeId
|
return useScopeSlots && moduleScopeId != null && scopeId == moduleScopeId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun shouldUseScopeSlotFor(scopeId: Int, name: String, isDelegated: Boolean): Boolean {
|
||||||
|
if (moduleScopeId == null || scopeId != moduleScopeId) return false
|
||||||
|
if (isDelegated) return false
|
||||||
|
if (externBindingNames.contains(name)) return true
|
||||||
|
return useScopeSlots && isPreparedScopeName(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isPreparedScopeName(name: String?): Boolean {
|
||||||
|
if (name == null) return false
|
||||||
|
if (scopeSlotNameSet?.contains(name) == true) return true
|
||||||
|
return allowedScopeNames?.contains(name) == true
|
||||||
|
}
|
||||||
|
|
||||||
private fun collectLoopVarNames(stmt: Statement) {
|
private fun collectLoopVarNames(stmt: Statement) {
|
||||||
if (stmt is BytecodeStatement) {
|
if (stmt is BytecodeStatement) {
|
||||||
collectLoopVarNames(stmt.original)
|
collectLoopVarNames(stmt.original)
|
||||||
@@ -8400,8 +8411,9 @@ class BytecodeCompiler(
|
|||||||
captureSlotKeys.add(key)
|
captureSlotKeys.add(key)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
val forceScopeSlot = shouldUseScopeSlotFor(scopeId, ref.name, ref.isDelegated)
|
||||||
val isModuleSlot = if (ref.isDelegated) false else isModuleSlot(scopeId, ref.name)
|
val isModuleSlot = if (ref.isDelegated) false else isModuleSlot(scopeId, ref.name)
|
||||||
if (allowLocalSlots && !isModuleSlot) {
|
if (allowLocalSlots && !isModuleSlot && !forceScopeSlot) {
|
||||||
if (!localSlotInfoMap.containsKey(key)) {
|
if (!localSlotInfoMap.containsKey(key)) {
|
||||||
localSlotInfoMap[key] = LocalSlotInfo(ref.name, ref.isMutable, ref.isDelegated)
|
localSlotInfoMap[key] = LocalSlotInfo(ref.name, ref.isMutable, ref.isDelegated)
|
||||||
}
|
}
|
||||||
@@ -8448,8 +8460,9 @@ class BytecodeCompiler(
|
|||||||
}
|
}
|
||||||
captureSlotKeys.add(key)
|
captureSlotKeys.add(key)
|
||||||
} else {
|
} else {
|
||||||
|
val forceScopeSlot = shouldUseScopeSlotFor(scopeId, target.name, target.isDelegated)
|
||||||
val isModuleSlot = if (target.isDelegated) false else isModuleSlot(scopeId, target.name)
|
val isModuleSlot = if (target.isDelegated) false else isModuleSlot(scopeId, target.name)
|
||||||
if (allowLocalSlots && !isModuleSlot) {
|
if (allowLocalSlots && !isModuleSlot && !forceScopeSlot) {
|
||||||
if (!localSlotInfoMap.containsKey(key)) {
|
if (!localSlotInfoMap.containsKey(key)) {
|
||||||
localSlotInfoMap[key] = LocalSlotInfo(target.name, target.isMutable, target.isDelegated)
|
localSlotInfoMap[key] = LocalSlotInfo(target.name, target.isMutable, target.isDelegated)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ class BytecodeStatement private constructor(
|
|||||||
callableReturnTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(),
|
callableReturnTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(),
|
||||||
callableReturnTypeByName: Map<String, ObjClass> = emptyMap(),
|
callableReturnTypeByName: Map<String, ObjClass> = emptyMap(),
|
||||||
externCallableNames: Set<String> = emptySet(),
|
externCallableNames: Set<String> = emptySet(),
|
||||||
|
externBindingNames: Set<String> = emptySet(),
|
||||||
lambdaCaptureEntriesByRef: Map<ValueFnRef, List<LambdaCaptureEntry>> = emptyMap(),
|
lambdaCaptureEntriesByRef: Map<ValueFnRef, List<LambdaCaptureEntry>> = emptyMap(),
|
||||||
slotTypeDeclByScopeId: Map<Int, Map<Int, TypeDecl>> = emptyMap(),
|
slotTypeDeclByScopeId: Map<Int, Map<Int, TypeDecl>> = emptyMap(),
|
||||||
): Statement {
|
): Statement {
|
||||||
@@ -122,6 +123,7 @@ class BytecodeStatement private constructor(
|
|||||||
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
|
callableReturnTypeByScopeId = callableReturnTypeByScopeId,
|
||||||
callableReturnTypeByName = callableReturnTypeByName,
|
callableReturnTypeByName = callableReturnTypeByName,
|
||||||
externCallableNames = externCallableNames,
|
externCallableNames = externCallableNames,
|
||||||
|
externBindingNames = externBindingNames,
|
||||||
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
|
lambdaCaptureEntriesByRef = lambdaCaptureEntriesByRef
|
||||||
)
|
)
|
||||||
val compiled = compiler.compileStatement(nameHint, statement)
|
val compiled = compiler.compileStatement(nameHint, statement)
|
||||||
@@ -236,6 +238,7 @@ class BytecodeStatement private constructor(
|
|||||||
stmt.name,
|
stmt.name,
|
||||||
stmt.isMutable,
|
stmt.isMutable,
|
||||||
stmt.visibility,
|
stmt.visibility,
|
||||||
|
stmt.actualExtern,
|
||||||
stmt.initializer?.let { unwrapDeep(it) },
|
stmt.initializer?.let { unwrapDeep(it) },
|
||||||
stmt.isTransient,
|
stmt.isTransient,
|
||||||
stmt.typeDecl,
|
stmt.typeDecl,
|
||||||
|
|||||||
@@ -2392,8 +2392,13 @@ class CmdDeclLocal(internal val constId: Int, internal val slot: Int) : Cmd() {
|
|||||||
?: error("DECL_LOCAL expects LocalDecl at $constId")
|
?: error("DECL_LOCAL expects LocalDecl at $constId")
|
||||||
if (slot < frame.fn.scopeSlotCount) {
|
if (slot < frame.fn.scopeSlotCount) {
|
||||||
val target = frame.scopeTarget(slot)
|
val target = frame.scopeTarget(slot)
|
||||||
frame.ensureScopeSlot(target, slot)
|
val index = frame.ensureScopeSlot(target, slot)
|
||||||
val value = frame.slotToObj(slot).byValueCopy()
|
val raw = target.getSlotRecord(index).value
|
||||||
|
val value = when (raw) {
|
||||||
|
is FrameSlotRef -> raw.read()
|
||||||
|
is RecordSlotRef -> raw.read()
|
||||||
|
else -> raw
|
||||||
|
}.byValueCopy()
|
||||||
target.updateSlotFor(
|
target.updateSlotFor(
|
||||||
decl.name,
|
decl.name,
|
||||||
ObjRecord(
|
ObjRecord(
|
||||||
@@ -3930,7 +3935,7 @@ class CmdFrame(
|
|||||||
} else {
|
} else {
|
||||||
val raw = frame.getRawObj(localIndex)
|
val raw = frame.getRawObj(localIndex)
|
||||||
if (raw == null && name != null) {
|
if (raw == null && name != null) {
|
||||||
val record = scope.get(name)
|
val record = findNamedExistingRecord(scope, name)
|
||||||
if (record != null) {
|
if (record != null) {
|
||||||
val value = record.value
|
val value = record.value
|
||||||
return@mapIndexed when (value) {
|
return@mapIndexed when (value) {
|
||||||
@@ -3939,6 +3944,12 @@ class CmdFrame(
|
|||||||
else -> ObjRecord(value, isMutable)
|
else -> ObjRecord(value, isMutable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (hasNamedScopeBinding(scope, name)) {
|
||||||
|
throw ScriptError(
|
||||||
|
ensureScope().pos,
|
||||||
|
"captured binding '$name' is not available in the execution scope; prepare the script imports/module bindings explicitly"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
when (raw) {
|
when (raw) {
|
||||||
is FrameSlotRef -> ObjRecord(raw, isMutable)
|
is FrameSlotRef -> ObjRecord(raw, isMutable)
|
||||||
@@ -3952,22 +3963,27 @@ class CmdFrame(
|
|||||||
val target = moduleScope
|
val target = moduleScope
|
||||||
val name = captureNames?.getOrNull(index)
|
val name = captureNames?.getOrNull(index)
|
||||||
if (name != null) {
|
if (name != null) {
|
||||||
target.tryGetLocalRecord(target, name, target.currentClassCtx)?.let { return@mapIndexed it }
|
findNamedExistingRecord(target, name)?.let { return@mapIndexed it }
|
||||||
target.getSlotIndexOf(name)?.let { return@mapIndexed target.getSlotRecord(it) }
|
|
||||||
target.get(name)?.let { return@mapIndexed it }
|
|
||||||
// Fallback to current scope in case the module scope isn't in the parent chain
|
// Fallback to current scope in case the module scope isn't in the parent chain
|
||||||
// or doesn't carry the imported symbol yet.
|
// or doesn't carry the imported symbol yet.
|
||||||
scope.tryGetLocalRecord(scope, name, scope.currentClassCtx)?.let { return@mapIndexed it }
|
findNamedExistingRecord(scope, name)?.let { return@mapIndexed it }
|
||||||
scope.get(name)?.let { return@mapIndexed it }
|
|
||||||
}
|
}
|
||||||
if (slotId < target.slotCount) {
|
if (slotId < target.slotCount) {
|
||||||
return@mapIndexed target.getSlotRecord(slotId)
|
val existing = target.getSlotRecord(slotId)
|
||||||
|
if (name == null || existing.value !== ObjUnset || hasResolvedNamedScopeBinding(target, name)) {
|
||||||
|
return@mapIndexed existing
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (name != null) {
|
if (name != null) {
|
||||||
target.applySlotPlan(mapOf(name to slotId))
|
throw ScriptError(
|
||||||
return@mapIndexed target.getSlotRecord(slotId)
|
ensureScope().pos,
|
||||||
|
"module capture '$name' is not available in the execution scope; prepare the script imports/module bindings explicitly"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
error("Missing module capture slot $slotId")
|
throw ScriptError(
|
||||||
|
ensureScope().pos,
|
||||||
|
"missing module capture slot $slotId"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4546,7 +4562,7 @@ class CmdFrame(
|
|||||||
return getScopeSlotValueAtAddr(addrSlot)
|
return getScopeSlotValueAtAddr(addrSlot)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAddrObj(addrSlot: Int, value: Obj) {
|
suspend fun setAddrObj(addrSlot: Int, value: Obj) {
|
||||||
setScopeSlotValueAtAddr(addrSlot, value)
|
setScopeSlotValueAtAddr(addrSlot, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4554,7 +4570,7 @@ class CmdFrame(
|
|||||||
return getScopeSlotValueAtAddr(addrSlot).toLong()
|
return getScopeSlotValueAtAddr(addrSlot).toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAddrInt(addrSlot: Int, value: Long) {
|
suspend fun setAddrInt(addrSlot: Int, value: Long) {
|
||||||
setScopeSlotValueAtAddr(addrSlot, ObjInt.of(value))
|
setScopeSlotValueAtAddr(addrSlot, ObjInt.of(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4562,7 +4578,7 @@ class CmdFrame(
|
|||||||
return getScopeSlotValueAtAddr(addrSlot).toDouble()
|
return getScopeSlotValueAtAddr(addrSlot).toDouble()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAddrReal(addrSlot: Int, value: Double) {
|
suspend fun setAddrReal(addrSlot: Int, value: Double) {
|
||||||
setScopeSlotValueAtAddr(addrSlot, ObjReal.of(value))
|
setScopeSlotValueAtAddr(addrSlot, ObjReal.of(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4570,7 +4586,7 @@ class CmdFrame(
|
|||||||
return getScopeSlotValueAtAddr(addrSlot).toBool()
|
return getScopeSlotValueAtAddr(addrSlot).toBool()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAddrBool(addrSlot: Int, value: Boolean) {
|
suspend fun setAddrBool(addrSlot: Int, value: Boolean) {
|
||||||
setScopeSlotValueAtAddr(addrSlot, if (value) ObjTrue else ObjFalse)
|
setScopeSlotValueAtAddr(addrSlot, if (value) ObjTrue else ObjFalse)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4776,12 +4792,13 @@ class CmdFrame(
|
|||||||
|
|
||||||
private suspend fun getScopeSlotValue(slot: Int): Obj {
|
private suspend fun getScopeSlotValue(slot: Int): Obj {
|
||||||
val target = scopeTarget(slot)
|
val target = scopeTarget(slot)
|
||||||
|
val name = fn.scopeSlotNames[slot]
|
||||||
|
val hadNamedBinding = name != null && hasResolvedNamedScopeBinding(target, name)
|
||||||
val index = ensureScopeSlot(target, slot)
|
val index = ensureScopeSlot(target, slot)
|
||||||
val record = target.getSlotRecord(index)
|
val record = target.getSlotRecord(index)
|
||||||
val direct = record.value
|
val direct = record.value
|
||||||
if (direct is FrameSlotRef) return direct.read()
|
if (direct is FrameSlotRef) return direct.read()
|
||||||
if (direct is RecordSlotRef) return direct.read()
|
if (direct is RecordSlotRef) return direct.read()
|
||||||
val name = fn.scopeSlotNames[slot]
|
|
||||||
if (name != null && record.memberName != null && record.memberName != name) {
|
if (name != null && record.memberName != null && record.memberName != name) {
|
||||||
val resolved = target.get(name)
|
val resolved = target.get(name)
|
||||||
if (resolved != null) {
|
if (resolved != null) {
|
||||||
@@ -4802,9 +4819,15 @@ class CmdFrame(
|
|||||||
return direct
|
return direct
|
||||||
}
|
}
|
||||||
if (name == null) return record.value
|
if (name == null) return record.value
|
||||||
val resolved = target.get(name) ?: return record.value
|
val resolved = target.get(name)
|
||||||
|
if (resolved == null) {
|
||||||
|
failMissingPreparedModuleBinding(slot, name, hadNamedBinding, record)
|
||||||
|
return record.value
|
||||||
|
}
|
||||||
if (resolved.value !== ObjUnset) {
|
if (resolved.value !== ObjUnset) {
|
||||||
target.updateSlotFor(name, resolved)
|
target.updateSlotFor(name, resolved)
|
||||||
|
} else {
|
||||||
|
failMissingPreparedModuleBinding(slot, name, hadNamedBinding, resolved)
|
||||||
}
|
}
|
||||||
return resolved.value
|
return resolved.value
|
||||||
}
|
}
|
||||||
@@ -4812,12 +4835,13 @@ class CmdFrame(
|
|||||||
private suspend fun getScopeSlotValueAtAddr(addrSlot: Int): Obj {
|
private suspend fun getScopeSlotValueAtAddr(addrSlot: Int): Obj {
|
||||||
val target = addrScopes[addrSlot] ?: error("Address slot $addrSlot is not resolved")
|
val target = addrScopes[addrSlot] ?: error("Address slot $addrSlot is not resolved")
|
||||||
val index = addrIndices[addrSlot]
|
val index = addrIndices[addrSlot]
|
||||||
|
val slotId = addrScopeSlots[addrSlot]
|
||||||
|
val name = fn.scopeSlotNames.getOrNull(slotId)
|
||||||
|
val hadNamedBinding = name != null && hasResolvedNamedScopeBinding(target, name)
|
||||||
val record = target.getSlotRecord(index)
|
val record = target.getSlotRecord(index)
|
||||||
val direct = record.value
|
val direct = record.value
|
||||||
if (direct is FrameSlotRef) return direct.read()
|
if (direct is FrameSlotRef) return direct.read()
|
||||||
if (direct is RecordSlotRef) return direct.read()
|
if (direct is RecordSlotRef) return direct.read()
|
||||||
val slotId = addrScopeSlots[addrSlot]
|
|
||||||
val name = fn.scopeSlotNames.getOrNull(slotId)
|
|
||||||
if (name != null && record.memberName != null && record.memberName != name) {
|
if (name != null && record.memberName != null && record.memberName != name) {
|
||||||
val resolved = target.get(name)
|
val resolved = target.get(name)
|
||||||
if (resolved != null) {
|
if (resolved != null) {
|
||||||
@@ -4838,16 +4862,29 @@ class CmdFrame(
|
|||||||
return direct
|
return direct
|
||||||
}
|
}
|
||||||
if (name == null) return record.value
|
if (name == null) return record.value
|
||||||
val resolved = target.get(name) ?: return record.value
|
val resolved = target.get(name)
|
||||||
|
if (resolved == null) {
|
||||||
|
failMissingPreparedModuleBinding(slotId, name, hadNamedBinding, record)
|
||||||
|
return record.value
|
||||||
|
}
|
||||||
if (resolved.value !== ObjUnset) {
|
if (resolved.value !== ObjUnset) {
|
||||||
target.updateSlotFor(name, resolved)
|
target.updateSlotFor(name, resolved)
|
||||||
|
} else {
|
||||||
|
failMissingPreparedModuleBinding(slotId, name, hadNamedBinding, resolved)
|
||||||
}
|
}
|
||||||
return resolved.value
|
return resolved.value
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setScopeSlotValueAtAddr(addrSlot: Int, value: Obj) {
|
private suspend fun setScopeSlotValueAtAddr(addrSlot: Int, value: Obj) {
|
||||||
val target = addrScopes[addrSlot] ?: error("Address slot $addrSlot is not resolved")
|
val target = addrScopes[addrSlot] ?: error("Address slot $addrSlot is not resolved")
|
||||||
val index = addrIndices[addrSlot]
|
val index = addrIndices[addrSlot]
|
||||||
|
val record = target.getSlotRecord(index)
|
||||||
|
val slotId = addrScopeSlots[addrSlot]
|
||||||
|
val name = fn.scopeSlotNames.getOrNull(slotId)
|
||||||
|
if (name != null && (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || record.value is ObjProperty)) {
|
||||||
|
target.assign(record, name, value)
|
||||||
|
return
|
||||||
|
}
|
||||||
target.setSlotValue(index, value)
|
target.setSlotValue(index, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4875,6 +4912,41 @@ class CmdFrame(
|
|||||||
return index
|
return index
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun hasNamedScopeBinding(target: Scope, name: String): Boolean {
|
||||||
|
if (target.tryGetLocalRecord(target, name, target.currentClassCtx) != null) return true
|
||||||
|
if (target.getSlotIndexOf(name) != null) return true
|
||||||
|
if (target.get(name) != null) return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasResolvedNamedScopeBinding(target: Scope, name: String): Boolean =
|
||||||
|
findNamedExistingRecord(target, name) != null
|
||||||
|
|
||||||
|
private fun findNamedExistingRecord(target: Scope, name: String): ObjRecord? {
|
||||||
|
target.tryGetLocalRecord(target, name, target.currentClassCtx)?.let { return it }
|
||||||
|
target.get(name)?.let { record ->
|
||||||
|
if (record.value !== ObjUnset || record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property) {
|
||||||
|
return record
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun failMissingPreparedModuleBinding(
|
||||||
|
slot: Int,
|
||||||
|
name: String,
|
||||||
|
hadNamedBinding: Boolean,
|
||||||
|
record: ObjRecord
|
||||||
|
) {
|
||||||
|
if (hadNamedBinding) return
|
||||||
|
if (record.value !== ObjUnset) return
|
||||||
|
if (fn.scopeSlotIsModule.getOrNull(slot) != true) return
|
||||||
|
throw ScriptError(
|
||||||
|
ensureScope().pos,
|
||||||
|
"module binding '$name' is not available in the execution scope; prepare the script imports/module bindings explicitly"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun ensureLocalMutable(localIndex: Int) {
|
private fun ensureLocalMutable(localIndex: Int) {
|
||||||
val name = fn.localSlotNames.getOrNull(localIndex) ?: return
|
val name = fn.localSlotNames.getOrNull(localIndex) ?: return
|
||||||
val isMutable = fn.localSlotMutables.getOrNull(localIndex) ?: true
|
val isMutable = fn.localSlotMutables.getOrNull(localIndex) ?: true
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ internal suspend fun seedFrameLocalsFromScope(frame: CmdFrame, scope: Scope) {
|
|||||||
} else {
|
} else {
|
||||||
record.value
|
record.value
|
||||||
}
|
}
|
||||||
if (value is net.sergeych.lyng.FrameSlotRef && value.refersTo(frame.frame, base + i)) {
|
if (value is net.sergeych.lyng.FrameSlotRef && value.refersTo(frame.frame, i)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
frame.setObjUnchecked(base + i, value)
|
frame.setObjUnchecked(base + i, value)
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import kotlinx.serialization.json.JsonElement
|
|||||||
import kotlinx.serialization.json.JsonNull
|
import kotlinx.serialization.json.JsonNull
|
||||||
import kotlinx.serialization.serializer
|
import kotlinx.serialization.serializer
|
||||||
import net.sergeych.lyng.*
|
import net.sergeych.lyng.*
|
||||||
|
import net.sergeych.lyng.InteropOperator
|
||||||
|
import net.sergeych.lyng.OperatorInteropRegistry
|
||||||
import net.sergeych.lyng.miniast.ParamDoc
|
import net.sergeych.lyng.miniast.ParamDoc
|
||||||
import net.sergeych.lyng.miniast.addFnDoc
|
import net.sergeych.lyng.miniast.addFnDoc
|
||||||
import net.sergeych.lyng.miniast.type
|
import net.sergeych.lyng.miniast.type
|
||||||
@@ -175,7 +177,8 @@ open class Obj {
|
|||||||
if (other === this) return 0
|
if (other === this) return 0
|
||||||
if (other === ObjNull || other === ObjUnset || other === ObjVoid) return 2
|
if (other === ObjNull || other === ObjUnset || other === ObjVoid) return 2
|
||||||
return invokeInstanceMethod(scope, "compareTo", Arguments(other)) {
|
return invokeInstanceMethod(scope, "compareTo", Arguments(other)) {
|
||||||
scope.raiseNotImplemented("compareTo for ${objClass.className}")
|
OperatorInteropRegistry.invokeCompare(scope, this, other)?.let { ObjInt.of(it.toLong()) }
|
||||||
|
?: scope.raiseNotImplemented("compareTo for ${objClass.className}")
|
||||||
}.cast<ObjInt>(scope).toInt()
|
}.cast<ObjInt>(scope).toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,7 +287,8 @@ open class Obj {
|
|||||||
}
|
}
|
||||||
if (self !== this) return self.plus(scope, otherValue)
|
if (self !== this) return self.plus(scope, otherValue)
|
||||||
return invokeInstanceMethod(scope, "plus", Arguments(otherValue)) {
|
return invokeInstanceMethod(scope, "plus", Arguments(otherValue)) {
|
||||||
scope.raiseNotImplemented("plus for ${objClass.className}")
|
OperatorInteropRegistry.invokeBinary(scope, self, otherValue, InteropOperator.Plus)
|
||||||
|
?: scope.raiseNotImplemented("plus for ${objClass.className}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +305,8 @@ open class Obj {
|
|||||||
}
|
}
|
||||||
if (self !== this) return self.minus(scope, otherValue)
|
if (self !== this) return self.minus(scope, otherValue)
|
||||||
return invokeInstanceMethod(scope, "minus", Arguments(otherValue)) {
|
return invokeInstanceMethod(scope, "minus", Arguments(otherValue)) {
|
||||||
scope.raiseNotImplemented("minus for ${objClass.className}")
|
OperatorInteropRegistry.invokeBinary(scope, self, otherValue, InteropOperator.Minus)
|
||||||
|
?: scope.raiseNotImplemented("minus for ${objClass.className}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,7 +329,8 @@ open class Obj {
|
|||||||
}
|
}
|
||||||
if (self !== this) return self.mul(scope, otherValue)
|
if (self !== this) return self.mul(scope, otherValue)
|
||||||
return invokeInstanceMethod(scope, "mul", Arguments(otherValue)) {
|
return invokeInstanceMethod(scope, "mul", Arguments(otherValue)) {
|
||||||
scope.raiseNotImplemented("mul for ${objClass.className}")
|
OperatorInteropRegistry.invokeBinary(scope, self, otherValue, InteropOperator.Mul)
|
||||||
|
?: scope.raiseNotImplemented("mul for ${objClass.className}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,7 +347,8 @@ open class Obj {
|
|||||||
}
|
}
|
||||||
if (self !== this) return self.div(scope, otherValue)
|
if (self !== this) return self.div(scope, otherValue)
|
||||||
return invokeInstanceMethod(scope, "div", Arguments(otherValue)) {
|
return invokeInstanceMethod(scope, "div", Arguments(otherValue)) {
|
||||||
scope.raiseNotImplemented("div for ${objClass.className}")
|
OperatorInteropRegistry.invokeBinary(scope, self, otherValue, InteropOperator.Div)
|
||||||
|
?: scope.raiseNotImplemented("div for ${objClass.className}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,7 +365,8 @@ open class Obj {
|
|||||||
}
|
}
|
||||||
if (self !== this) return self.mod(scope, otherValue)
|
if (self !== this) return self.mod(scope, otherValue)
|
||||||
return invokeInstanceMethod(scope, "mod", Arguments(otherValue)) {
|
return invokeInstanceMethod(scope, "mod", Arguments(otherValue)) {
|
||||||
scope.raiseNotImplemented("mod for ${objClass.className}")
|
OperatorInteropRegistry.invokeBinary(scope, self, otherValue, InteropOperator.Mod)
|
||||||
|
?: scope.raiseNotImplemented("mod for ${objClass.className}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,313 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.sergeych.lyng.obj
|
||||||
|
|
||||||
|
import com.ionspin.kotlin.bignum.decimal.BigDecimal as IonBigDecimal
|
||||||
|
import com.ionspin.kotlin.bignum.decimal.DecimalMode
|
||||||
|
import com.ionspin.kotlin.bignum.decimal.RoundingMode
|
||||||
|
import com.ionspin.kotlin.bignum.integer.BigInteger
|
||||||
|
import net.sergeych.lyng.Arguments
|
||||||
|
import net.sergeych.lyng.FrameSlotRef
|
||||||
|
import net.sergeych.lyng.InteropOperator
|
||||||
|
import net.sergeych.lyng.ModuleScope
|
||||||
|
import net.sergeych.lyng.OperatorInteropRegistry
|
||||||
|
import net.sergeych.lyng.RecordSlotRef
|
||||||
|
import net.sergeych.lyng.Scope
|
||||||
|
import net.sergeych.lyng.ScopeFacade
|
||||||
|
import net.sergeych.lyng.TypeDecl
|
||||||
|
import net.sergeych.lyng.miniast.addPropertyDoc
|
||||||
|
import net.sergeych.lyng.miniast.type
|
||||||
|
import net.sergeych.lyng.requiredArg
|
||||||
|
|
||||||
|
object ObjBigDecimalSupport {
|
||||||
|
private const val decimalContextVar = "__lyng_decimal_context__"
|
||||||
|
// For Real -> BigDecimal, preserve the actual IEEE-754 Double value using a
|
||||||
|
// round-trip-safe precision. This intentionally does not try to recover source text.
|
||||||
|
private val realConversionMode = DecimalMode(17L, RoundingMode.ROUND_HALF_TO_EVEN)
|
||||||
|
// Division needs an explicit stopping rule for non-terminating results. Use a
|
||||||
|
// decimal128-like default context until Lyng exposes per-operation contexts.
|
||||||
|
private val defaultDivisionMode = DecimalMode(34L, RoundingMode.ROUND_HALF_TO_EVEN)
|
||||||
|
private val zero: IonBigDecimal = IonBigDecimal.ZERO
|
||||||
|
private val decimalTypeDecl = TypeDecl.Simple("lyng.decimal.BigDecimal", false)
|
||||||
|
private object BoundMarker
|
||||||
|
private data class DecimalRuntimeContext(
|
||||||
|
val precision: Long,
|
||||||
|
val rounding: RoundingMode
|
||||||
|
) : Obj()
|
||||||
|
|
||||||
|
suspend fun bindTo(module: ModuleScope) {
|
||||||
|
val decimalClass = module.requireClass("BigDecimal")
|
||||||
|
if (decimalClass.kotlinClassData === BoundMarker) return
|
||||||
|
decimalClass.kotlinClassData = BoundMarker
|
||||||
|
decimalClass.isAbstract = false
|
||||||
|
val hooks = decimalClass.bridgeInitHooks ?: mutableListOf<suspend (ScopeFacade, ObjInstance) -> Unit>().also {
|
||||||
|
decimalClass.bridgeInitHooks = it
|
||||||
|
}
|
||||||
|
hooks += { _, instance ->
|
||||||
|
instance.kotlinInstanceData = zero
|
||||||
|
}
|
||||||
|
decimalClass.addFn("plus") {
|
||||||
|
newInstance(decimalClass, valueOf(thisObj).plus(coerceArg(requireScope(), args.firstAndOnly())))
|
||||||
|
}
|
||||||
|
decimalClass.addFn("minus") {
|
||||||
|
newInstance(decimalClass, valueOf(thisObj).minus(coerceArg(requireScope(), args.firstAndOnly())))
|
||||||
|
}
|
||||||
|
decimalClass.addFn("mul") {
|
||||||
|
newInstance(decimalClass, valueOf(thisObj).times(coerceArg(requireScope(), args.firstAndOnly())))
|
||||||
|
}
|
||||||
|
decimalClass.addFn("div") {
|
||||||
|
newInstance(decimalClass, divideWithContext(valueOf(thisObj), coerceArg(requireScope(), args.firstAndOnly()), currentDivisionMode(requireScope())))
|
||||||
|
}
|
||||||
|
decimalClass.addFn("mod") {
|
||||||
|
newInstance(decimalClass, valueOf(thisObj).rem(coerceArg(requireScope(), args.firstAndOnly())))
|
||||||
|
}
|
||||||
|
decimalClass.addFn("compareTo") {
|
||||||
|
ObjInt.of(valueOf(thisObj).compareTo(coerceArg(requireScope(), args.firstAndOnly())).toLong())
|
||||||
|
}
|
||||||
|
decimalClass.addFn("negate") {
|
||||||
|
newInstance(decimalClass, valueOf(thisObj).unaryMinus())
|
||||||
|
}
|
||||||
|
decimalClass.addFn("toInt") {
|
||||||
|
ObjInt.of(valueOf(thisObj).longValue(false))
|
||||||
|
}
|
||||||
|
decimalClass.addFn("toReal") {
|
||||||
|
ObjReal.of(valueOf(thisObj).doubleValue(false))
|
||||||
|
}
|
||||||
|
decimalClass.addFn("toStringExpanded") {
|
||||||
|
ObjString(valueOf(thisObj).toStringExpanded())
|
||||||
|
}
|
||||||
|
decimalClass.addClassFn("fromInt") {
|
||||||
|
val value = requiredArg<ObjInt>(0).value
|
||||||
|
newInstance(decimalClass, IonBigDecimal.fromLongAsSignificand(value))
|
||||||
|
}
|
||||||
|
decimalClass.addClassFn("fromReal") {
|
||||||
|
val value = requiredArg<ObjReal>(0).value
|
||||||
|
newInstance(decimalClass, IonBigDecimal.fromDouble(value, realConversionMode))
|
||||||
|
}
|
||||||
|
decimalClass.addClassFn("fromString") {
|
||||||
|
val value = requiredArg<ObjString>(0).value
|
||||||
|
try {
|
||||||
|
newInstance(decimalClass, IonBigDecimal.parseStringWithMode(value))
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
requireScope().raiseIllegalArgument("invalid BigDecimal string: $value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.addFn("withDecimalContext") {
|
||||||
|
val (context, block) = when (args.list.size) {
|
||||||
|
2 -> {
|
||||||
|
val first = args[0]
|
||||||
|
val block = args[1]
|
||||||
|
if (first is ObjInt) {
|
||||||
|
DecimalRuntimeContext(first.value, RoundingMode.ROUND_HALF_TO_EVEN) to block
|
||||||
|
} else {
|
||||||
|
normalizeContext(requireScope(), first) to block
|
||||||
|
}
|
||||||
|
}
|
||||||
|
3 -> {
|
||||||
|
val precision = requiredArg<ObjInt>(0).value
|
||||||
|
val rounding = roundingModeFromObj(requireScope(), args[1])
|
||||||
|
DecimalRuntimeContext(precision, rounding) to args[2]
|
||||||
|
}
|
||||||
|
else -> requireScope().raiseIllegalArgument("withDecimalContext expects (context, block), (precision, block), or (precision, rounding, block)")
|
||||||
|
}
|
||||||
|
val child = requireScope().createChildScope()
|
||||||
|
child.addConst(decimalContextVar, context)
|
||||||
|
block.callOn(child)
|
||||||
|
}
|
||||||
|
registerBuiltinConversions(decimalClass)
|
||||||
|
registerInterop(decimalClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun valueOf(obj: Obj): IonBigDecimal {
|
||||||
|
val instance = obj as? ObjInstance ?: error("BigDecimal receiver must be an object instance")
|
||||||
|
return instance.kotlinInstanceData as? IonBigDecimal ?: zero
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun currentDivisionMode(scope: Scope): DecimalMode {
|
||||||
|
val context = findContextObject(scope) ?: return defaultDivisionMode
|
||||||
|
return DecimalMode(context.precision, context.rounding)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun divideWithContext(left: IonBigDecimal, right: IonBigDecimal, mode: DecimalMode): IonBigDecimal {
|
||||||
|
if (mode.decimalPrecision <= 0L) {
|
||||||
|
return stripMode(left.divide(right, mode))
|
||||||
|
}
|
||||||
|
val exactLeft = stripMode(left)
|
||||||
|
val exactRight = stripMode(right)
|
||||||
|
val guardMode = DecimalMode(mode.decimalPrecision + 2, RoundingMode.TOWARDS_ZERO)
|
||||||
|
var guarded = stripMode(exactLeft.divide(exactRight, guardMode))
|
||||||
|
val hasMoreTail = !stripMode(exactLeft - stripMode(guarded * exactRight)).isZero()
|
||||||
|
if (hasMoreTail && isHalfRounding(mode.roundingMode) && looksLikeExactHalf(guarded, mode.decimalPrecision)) {
|
||||||
|
guarded = nudgeLastDigitAwayFromZero(guarded)
|
||||||
|
}
|
||||||
|
return stripMode(guarded.roundSignificand(mode))
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun ScopeFacade.newInstance(decimalClass: ObjClass, value: IonBigDecimal): ObjInstance {
|
||||||
|
val instance = call(decimalClass) as? ObjInstance
|
||||||
|
?: raiseIllegalState("BigDecimal() did not return an object instance")
|
||||||
|
instance.kotlinInstanceData = value
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun coerceArg(scope: Scope, value: Obj): IonBigDecimal = when (value) {
|
||||||
|
is ObjInt -> IonBigDecimal.fromLongAsSignificand(value.value)
|
||||||
|
is ObjReal -> IonBigDecimal.fromDouble(value.value, realConversionMode)
|
||||||
|
is ObjInstance -> {
|
||||||
|
if (value.objClass.className != "BigDecimal") {
|
||||||
|
scope.raiseIllegalArgument("expected BigDecimal-compatible value, got ${value.objClass.className}")
|
||||||
|
}
|
||||||
|
value.kotlinInstanceData as? IonBigDecimal ?: zero
|
||||||
|
}
|
||||||
|
else -> scope.raiseIllegalArgument("expected BigDecimal-compatible value, got ${value.objClass.className}")
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun normalizeContext(scope: Scope, value: Obj): DecimalRuntimeContext {
|
||||||
|
val instance = value as? ObjInstance
|
||||||
|
?: scope.raiseClassCastError("withDecimalContext expects DecimalContext as the first argument")
|
||||||
|
if (instance.objClass.className != "DecimalContext") {
|
||||||
|
scope.raiseClassCastError("withDecimalContext expects DecimalContext as the first argument")
|
||||||
|
}
|
||||||
|
return decimalRuntimeContextFromInstance(scope, instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findContextObject(scope: Scope): DecimalRuntimeContext? {
|
||||||
|
var current: Scope? = scope
|
||||||
|
while (current != null) {
|
||||||
|
val record = current.objects[decimalContextVar] ?: current.localBindings[decimalContextVar]
|
||||||
|
val value = when (val raw = record?.value) {
|
||||||
|
is FrameSlotRef -> raw.peekValue()
|
||||||
|
is RecordSlotRef -> raw.peekValue()
|
||||||
|
else -> raw
|
||||||
|
}
|
||||||
|
when (value) {
|
||||||
|
is DecimalRuntimeContext -> return value
|
||||||
|
}
|
||||||
|
current = current.parent
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun decimalRuntimeContextFromInstance(scope: Scope, context: ObjInstance): DecimalRuntimeContext {
|
||||||
|
val precision = context.readField(scope, "precision").value as? ObjInt
|
||||||
|
?: scope.raiseClassCastError("DecimalContext.precision must be Int")
|
||||||
|
if (precision.value <= 0L) {
|
||||||
|
scope.raiseIllegalArgument("DecimalContext precision must be positive")
|
||||||
|
}
|
||||||
|
val rounding = roundingModeFromObj(scope, context.readField(scope, "rounding").value)
|
||||||
|
return DecimalRuntimeContext(precision.value, rounding)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stripMode(value: IonBigDecimal): IonBigDecimal =
|
||||||
|
IonBigDecimal.fromBigIntegerWithExponent(value.significand, value.exponent)
|
||||||
|
|
||||||
|
private fun isHalfRounding(mode: RoundingMode): Boolean = when (mode) {
|
||||||
|
RoundingMode.ROUND_HALF_TO_EVEN,
|
||||||
|
RoundingMode.ROUND_HALF_AWAY_FROM_ZERO,
|
||||||
|
RoundingMode.ROUND_HALF_TOWARDS_ZERO,
|
||||||
|
RoundingMode.ROUND_HALF_CEILING,
|
||||||
|
RoundingMode.ROUND_HALF_FLOOR,
|
||||||
|
RoundingMode.ROUND_HALF_TO_ODD -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun looksLikeExactHalf(value: IonBigDecimal, targetPrecision: Long): Boolean {
|
||||||
|
val digits = value.significand.abs().toString(10)
|
||||||
|
if (digits.length <= targetPrecision) return false
|
||||||
|
val discarded = digits.substring(targetPrecision.toInt())
|
||||||
|
return discarded[0] == '5' && discarded.drop(1).all { it == '0' }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun nudgeLastDigitAwayFromZero(value: IonBigDecimal): IonBigDecimal {
|
||||||
|
val ulpExponent = value.exponent - value.precision + 1
|
||||||
|
val ulp = IonBigDecimal.fromBigIntegerWithExponent(BigInteger.ONE, ulpExponent)
|
||||||
|
return if (value.significand.signum() < 0) value - ulp else value + ulp
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun roundingModeFromObj(scope: Scope, value: Obj): RoundingMode {
|
||||||
|
val entry = value as? ObjEnumEntry ?: scope.raiseClassCastError("DecimalContext.rounding must be DecimalRounding")
|
||||||
|
return when (entry.name.value) {
|
||||||
|
"HalfEven" -> RoundingMode.ROUND_HALF_TO_EVEN
|
||||||
|
"HalfAwayFromZero" -> RoundingMode.ROUND_HALF_AWAY_FROM_ZERO
|
||||||
|
"HalfTowardsZero" -> RoundingMode.ROUND_HALF_TOWARDS_ZERO
|
||||||
|
"Ceiling" -> RoundingMode.CEILING
|
||||||
|
"Floor" -> RoundingMode.FLOOR
|
||||||
|
"AwayFromZero" -> RoundingMode.AWAY_FROM_ZERO
|
||||||
|
"TowardsZero" -> RoundingMode.TOWARDS_ZERO
|
||||||
|
else -> scope.raiseIllegalArgument("unsupported DecimalRounding: ${entry.name.value}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun registerBuiltinConversions(decimalClass: ObjClass) {
|
||||||
|
ObjInt.type.addPropertyDoc(
|
||||||
|
name = "d",
|
||||||
|
doc = "Convert this integer to a BigDecimal.",
|
||||||
|
type = type("lyng.decimal.BigDecimal"),
|
||||||
|
moduleName = "lyng.decimal",
|
||||||
|
getter = { newInstance(decimalClass, IonBigDecimal.fromLongAsSignificand(thisAs<ObjInt>().value)) }
|
||||||
|
)
|
||||||
|
ObjInt.type.members["d"] = ObjInt.type.members.getValue("d").copy(typeDecl = decimalTypeDecl)
|
||||||
|
ObjReal.type.addPropertyDoc(
|
||||||
|
name = "d",
|
||||||
|
doc = "Convert this real number to a BigDecimal by preserving the current IEEE-754 value with 17 significant digits and half-even rounding.",
|
||||||
|
type = type("lyng.decimal.BigDecimal"),
|
||||||
|
moduleName = "lyng.decimal",
|
||||||
|
getter = { newInstance(decimalClass, IonBigDecimal.fromDouble(thisAs<ObjReal>().value, realConversionMode)) }
|
||||||
|
)
|
||||||
|
ObjReal.type.members["d"] = ObjReal.type.members.getValue("d").copy(typeDecl = decimalTypeDecl)
|
||||||
|
ObjString.type.addPropertyDoc(
|
||||||
|
name = "d",
|
||||||
|
doc = "Parse this string as a BigDecimal.",
|
||||||
|
type = type("lyng.decimal.BigDecimal"),
|
||||||
|
moduleName = "lyng.decimal",
|
||||||
|
getter = {
|
||||||
|
val value = thisAs<ObjString>().value
|
||||||
|
try {
|
||||||
|
newInstance(decimalClass, IonBigDecimal.parseStringWithMode(value))
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
requireScope().raiseIllegalArgument("invalid BigDecimal string: $value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ObjString.type.members["d"] = ObjString.type.members.getValue("d").copy(typeDecl = decimalTypeDecl)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun registerInterop(decimalClass: ObjClass) {
|
||||||
|
OperatorInteropRegistry.register(
|
||||||
|
leftClass = ObjInt.type,
|
||||||
|
rightClass = decimalClass,
|
||||||
|
commonClass = decimalClass,
|
||||||
|
operatorNames = listOf(
|
||||||
|
InteropOperator.Plus.name,
|
||||||
|
InteropOperator.Minus.name,
|
||||||
|
InteropOperator.Mul.name,
|
||||||
|
InteropOperator.Div.name,
|
||||||
|
InteropOperator.Mod.name,
|
||||||
|
InteropOperator.Compare.name,
|
||||||
|
InteropOperator.Equals.name
|
||||||
|
),
|
||||||
|
leftToCommon = ObjExternCallable.fromBridge {
|
||||||
|
val value = requiredArg<ObjInt>(0).value
|
||||||
|
newInstance(decimalClass, IonBigDecimal.fromLongAsSignificand(value))
|
||||||
|
},
|
||||||
|
rightToCommon = ObjExternCallable.fromBridge {
|
||||||
|
requiredArg<Obj>(0)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -106,10 +106,20 @@ open class ObjClass(
|
|||||||
vararg parents: ObjClass,
|
vararg parents: ObjClass,
|
||||||
) : Obj() {
|
) : Obj() {
|
||||||
|
|
||||||
|
private fun declaringModulePackageName(scope: Scope?): String? {
|
||||||
|
var current = scope
|
||||||
|
while (current != null) {
|
||||||
|
if (current is ModuleScope) return current.packageName
|
||||||
|
current = current.parent
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
var isAnonymous: Boolean = false
|
var isAnonymous: Boolean = false
|
||||||
|
|
||||||
var isAbstract: Boolean = false
|
var isAbstract: Boolean = false
|
||||||
var isClosed: Boolean = false
|
var isClosed: Boolean = false
|
||||||
|
var logicalPackageNameOverride: String? = null
|
||||||
|
|
||||||
// Stable identity and simple structural version for PICs
|
// Stable identity and simple structural version for PICs
|
||||||
val classId: Long = ClassIdGen.nextId()
|
val classId: Long = ClassIdGen.nextId()
|
||||||
@@ -169,6 +179,16 @@ open class ObjClass(
|
|||||||
*/
|
*/
|
||||||
var classScope: Scope? = null
|
var classScope: Scope? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stable logical identity for class matching across separately instantiated modules.
|
||||||
|
* Uses declaring module package plus class name when available.
|
||||||
|
*/
|
||||||
|
val logicalPackageName: String?
|
||||||
|
get() = logicalPackageNameOverride ?: declaringModulePackageName(classScope)
|
||||||
|
|
||||||
|
val logicalName: String
|
||||||
|
get() = logicalPackageName?.let { "$it::$className" } ?: className
|
||||||
|
|
||||||
/** Direct parents in declaration order (kept deterministic). */
|
/** Direct parents in declaration order (kept deterministic). */
|
||||||
val directParents: List<ObjClass> = parents.toList()
|
val directParents: List<ObjClass> = parents.toList()
|
||||||
|
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ open class ObjDynamic(var readCallback: Obj? = null, var writeCallback: Obj? = n
|
|||||||
}
|
}
|
||||||
|
|
||||||
val type = object : ObjClass("Delegate") {}.apply {
|
val type = object : ObjClass("Delegate") {}.apply {
|
||||||
|
logicalPackageNameOverride = "lyng.stdlib"
|
||||||
addFn("getValue") { raiseError("Delegate.getValue is not implemented") }
|
addFn("getValue") { raiseError("Delegate.getValue is not implemented") }
|
||||||
addFn("setValue") { raiseError("Delegate.setValue is not implemented") }
|
addFn("setValue") { raiseError("Delegate.setValue is not implemented") }
|
||||||
addFn("invoke") { raiseError("Delegate.invoke is not implemented") }
|
addFn("invoke") { raiseError("Delegate.invoke is not implemented") }
|
||||||
|
|||||||
@@ -364,6 +364,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
onNotFoundResult: (suspend () -> Obj?)?
|
onNotFoundResult: (suspend () -> Obj?)?
|
||||||
): Obj {
|
): Obj {
|
||||||
val caller = scope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
|
val methodScope = scope.applyClosure(instanceScope)
|
||||||
|
|
||||||
// Fast path for public members when outside any class context
|
// Fast path for public members when outside any class context
|
||||||
if (caller == null) {
|
if (caller == null) {
|
||||||
@@ -373,7 +374,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
if (rec.type == ObjRecord.Type.Property) {
|
if (rec.type == ObjRecord.Type.Property) {
|
||||||
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
|
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
|
||||||
} else if (rec.type == ObjRecord.Type.Fun) {
|
} else if (rec.type == ObjRecord.Type.Fun) {
|
||||||
return rec.value.invoke(instanceScope, this, args, decl)
|
return rec.value.invoke(methodScope, this, args, decl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -384,7 +385,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
if (rec.type == ObjRecord.Type.Property) {
|
if (rec.type == ObjRecord.Type.Property) {
|
||||||
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
|
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
|
||||||
} else if (rec.type == ObjRecord.Type.Fun) {
|
} else if (rec.type == ObjRecord.Type.Fun) {
|
||||||
return rec.value.invoke(instanceScope, this, args, decl)
|
return rec.value.invoke(methodScope, this, args, decl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -394,7 +395,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
if (rec.type == ObjRecord.Type.Property) {
|
if (rec.type == ObjRecord.Type.Property) {
|
||||||
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
|
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
|
||||||
} else if (rec.type == ObjRecord.Type.Fun) {
|
} else if (rec.type == ObjRecord.Type.Fun) {
|
||||||
return rec.value.invoke(instanceScope, this, args, decl)
|
return rec.value.invoke(methodScope, this, args, decl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -409,7 +410,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
if (rec.type == ObjRecord.Type.Property) {
|
if (rec.type == ObjRecord.Type.Property) {
|
||||||
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, c)
|
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, c)
|
||||||
} else if (rec.type == ObjRecord.Type.Fun) {
|
} else if (rec.type == ObjRecord.Type.Fun) {
|
||||||
return rec.value.invoke(instanceScope, this, args, c)
|
return rec.value.invoke(methodScope, this, args, c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -418,7 +419,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
if (rec.type == ObjRecord.Type.Property) {
|
if (rec.type == ObjRecord.Type.Property) {
|
||||||
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, c)
|
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, c)
|
||||||
} else if (rec.type == ObjRecord.Type.Fun) {
|
} else if (rec.type == ObjRecord.Type.Fun) {
|
||||||
return rec.value.invoke(instanceScope, this, args, c)
|
return rec.value.invoke(methodScope, this, args, c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -427,7 +428,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
if (rec.type == ObjRecord.Type.Property) {
|
if (rec.type == ObjRecord.Type.Property) {
|
||||||
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, c)
|
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, c)
|
||||||
} else if (rec.type == ObjRecord.Type.Fun) {
|
} else if (rec.type == ObjRecord.Type.Fun) {
|
||||||
return rec.value.invoke(instanceScope, this, args, c)
|
return rec.value.invoke(methodScope, this, args, c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -439,7 +440,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
val decl = rec.declaringClass ?: objClass.findDeclaringClassOf(name) ?: objClass
|
val decl = rec.declaringClass ?: objClass.findDeclaringClassOf(name) ?: objClass
|
||||||
val effectiveCaller = caller ?: if (scope.thisObj === this) objClass else null
|
val effectiveCaller = caller ?: if (scope.thisObj === this) objClass else null
|
||||||
if (canAccessMember(rec.visibility, decl, effectiveCaller, name)) {
|
if (canAccessMember(rec.visibility, decl, effectiveCaller, name)) {
|
||||||
return rec.value.invoke(instanceScope, this, args, decl)
|
return rec.value.invoke(methodScope, this, args, decl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -474,7 +475,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
|
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
|
||||||
} else if (rec.type == ObjRecord.Type.Fun) {
|
} else if (rec.type == ObjRecord.Type.Fun) {
|
||||||
return rec.value.invoke(
|
return rec.value.invoke(
|
||||||
instanceScope,
|
methodScope,
|
||||||
this,
|
this,
|
||||||
args,
|
args,
|
||||||
decl
|
decl
|
||||||
@@ -586,8 +587,10 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||||
if (other !is ObjInstance) return -1
|
if (other !is ObjInstance || other.objClass != objClass) {
|
||||||
if (other.objClass != objClass) return -1
|
OperatorInteropRegistry.invokeCompare(scope, this, other)?.let { return it }
|
||||||
|
return -1
|
||||||
|
}
|
||||||
for (f in comparableVars) {
|
for (f in comparableVars) {
|
||||||
val a = f.value.value
|
val a = f.value.value
|
||||||
val b = other.instanceScope.objects[f.key]?.value ?: scope.raiseError("Internal error: field ${f.key} not found in other instance")
|
val b = other.instanceScope.objects[f.key]?.value ?: scope.raiseError("Internal error: field ${f.key} not found in other instance")
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ package net.sergeych.lyng.obj
|
|||||||
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
import kotlinx.serialization.json.JsonPrimitive
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import net.sergeych.lyng.InteropOperator
|
||||||
|
import net.sergeych.lyng.OperatorInteropRegistry
|
||||||
import net.sergeych.lyng.Scope
|
import net.sergeych.lyng.Scope
|
||||||
import net.sergeych.lyng.miniast.addFnDoc
|
import net.sergeych.lyng.miniast.addFnDoc
|
||||||
import net.sergeych.lynon.LynonDecoder
|
import net.sergeych.lynon.LynonDecoder
|
||||||
@@ -54,10 +56,11 @@ class ObjInt(val value: Long, override val isConst: Boolean = false) : Obj(), Nu
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||||
if (other !is Numeric) return -2
|
|
||||||
return if (other is ObjInt) {
|
return if (other is ObjInt) {
|
||||||
value.compareTo(other.value)
|
value.compareTo(other.value)
|
||||||
} else {
|
} else {
|
||||||
|
OperatorInteropRegistry.invokeCompare(scope, this, other)?.let { return it }
|
||||||
|
if (other !is Numeric) return -2
|
||||||
doubleValue.compareTo(other.doubleValue)
|
doubleValue.compareTo(other.doubleValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,28 +73,39 @@ class ObjInt(val value: Long, override val isConst: Boolean = false) : Obj(), Nu
|
|||||||
if (other is ObjInt)
|
if (other is ObjInt)
|
||||||
of(this.value + other.value)
|
of(this.value + other.value)
|
||||||
else
|
else
|
||||||
|
OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Plus)
|
||||||
|
?:
|
||||||
ObjReal.of(this.doubleValue + other.toDouble())
|
ObjReal.of(this.doubleValue + other.toDouble())
|
||||||
|
|
||||||
override suspend fun minus(scope: Scope, other: Obj): Obj =
|
override suspend fun minus(scope: Scope, other: Obj): Obj =
|
||||||
if (other is ObjInt)
|
if (other is ObjInt)
|
||||||
of(this.value - other.value)
|
of(this.value - other.value)
|
||||||
else
|
else
|
||||||
|
OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Minus)
|
||||||
|
?:
|
||||||
ObjReal.of(this.doubleValue - other.toDouble())
|
ObjReal.of(this.doubleValue - other.toDouble())
|
||||||
|
|
||||||
override suspend fun mul(scope: Scope, other: Obj): Obj =
|
override suspend fun mul(scope: Scope, other: Obj): Obj =
|
||||||
if (other is ObjInt) {
|
if (other is ObjInt) {
|
||||||
of(this.value * other.value)
|
of(this.value * other.value)
|
||||||
} else ObjReal.of(this.value * other.toDouble())
|
} else {
|
||||||
|
OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Mul)
|
||||||
|
?: ObjReal.of(this.value * other.toDouble())
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun div(scope: Scope, other: Obj): Obj =
|
override suspend fun div(scope: Scope, other: Obj): Obj =
|
||||||
if (other is ObjInt)
|
if (other is ObjInt)
|
||||||
of(this.value / other.value)
|
of(this.value / other.value)
|
||||||
else ObjReal.of(this.value / other.toDouble())
|
else
|
||||||
|
OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Div)
|
||||||
|
?: ObjReal.of(this.value / other.toDouble())
|
||||||
|
|
||||||
override suspend fun mod(scope: Scope, other: Obj): Obj =
|
override suspend fun mod(scope: Scope, other: Obj): Obj =
|
||||||
if (other is ObjInt)
|
if (other is ObjInt)
|
||||||
of(this.value % other.value)
|
of(this.value % other.value)
|
||||||
else ObjReal.of(this.value.toDouble() % other.toDouble())
|
else
|
||||||
|
OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Mod)
|
||||||
|
?: ObjReal.of(this.value.toDouble() % other.toDouble())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Numbers are now immutable, so we can't do in-place assignment.
|
* Numbers are now immutable, so we can't do in-place assignment.
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package net.sergeych.lyng.obj
|
|
||||||
|
|
||||||
import net.sergeych.lyng.Arguments
|
|
||||||
import net.sergeych.lyng.Pos
|
|
||||||
import net.sergeych.lyng.Scope
|
|
||||||
import net.sergeych.lyng.Statement
|
|
||||||
import net.sergeych.lyng.Visibility
|
|
||||||
import net.sergeych.lyng.executeBytecodeWithSeed
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lazy delegate used by `val x by lazy { ... }`.
|
|
||||||
*/
|
|
||||||
class ObjLazyDelegate(
|
|
||||||
private val builder: Obj,
|
|
||||||
private val capturedScope: Scope,
|
|
||||||
) : Obj() {
|
|
||||||
override val objClass: ObjClass = type
|
|
||||||
|
|
||||||
private var calculated = false
|
|
||||||
private var cachedValue: Obj = ObjVoid
|
|
||||||
|
|
||||||
override suspend fun invokeInstanceMethod(
|
|
||||||
scope: Scope,
|
|
||||||
name: String,
|
|
||||||
args: Arguments,
|
|
||||||
onNotFoundResult: (suspend () -> Obj?)?,
|
|
||||||
): Obj {
|
|
||||||
return when (name) {
|
|
||||||
"getValue" -> {
|
|
||||||
if (!calculated) {
|
|
||||||
val callScope = capturedScope.createChildScope(capturedScope.pos, args = Arguments.EMPTY)
|
|
||||||
cachedValue = if (builder is Statement) {
|
|
||||||
executeBytecodeWithSeed(callScope, builder, "lazy delegate")
|
|
||||||
} else {
|
|
||||||
builder.callOn(callScope)
|
|
||||||
}
|
|
||||||
calculated = true
|
|
||||||
}
|
|
||||||
cachedValue
|
|
||||||
}
|
|
||||||
"setValue" -> scope.raiseIllegalAssignment("lazy delegate is read-only")
|
|
||||||
else -> super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val type = ObjClass("LazyDelegate").apply {
|
|
||||||
implementingNames.add("Delegate")
|
|
||||||
createField(
|
|
||||||
"getValue",
|
|
||||||
ObjNull,
|
|
||||||
isMutable = false,
|
|
||||||
visibility = Visibility.Public,
|
|
||||||
pos = Pos.builtIn,
|
|
||||||
declaringClass = this,
|
|
||||||
type = ObjRecord.Type.Fun
|
|
||||||
)
|
|
||||||
createField(
|
|
||||||
"setValue",
|
|
||||||
ObjNull,
|
|
||||||
isMutable = false,
|
|
||||||
visibility = Visibility.Public,
|
|
||||||
pos = Pos.builtIn,
|
|
||||||
declaringClass = this,
|
|
||||||
type = ObjRecord.Type.Fun
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,6 +19,8 @@ package net.sergeych.lyng.obj
|
|||||||
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
import kotlinx.serialization.json.JsonPrimitive
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import net.sergeych.lyng.InteropOperator
|
||||||
|
import net.sergeych.lyng.OperatorInteropRegistry
|
||||||
import net.sergeych.lyng.Scope
|
import net.sergeych.lyng.Scope
|
||||||
import net.sergeych.lyng.miniast.addConstDoc
|
import net.sergeych.lyng.miniast.addConstDoc
|
||||||
import net.sergeych.lyng.miniast.addFnDoc
|
import net.sergeych.lyng.miniast.addFnDoc
|
||||||
@@ -40,6 +42,8 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
|
|||||||
override fun byValueCopy(): Obj = this
|
override fun byValueCopy(): Obj = this
|
||||||
|
|
||||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||||
|
if (other is ObjReal) return value.compareTo(other.value)
|
||||||
|
OperatorInteropRegistry.invokeCompare(scope, this, other)?.let { return it }
|
||||||
if (other !is Numeric) return -2
|
if (other !is Numeric) return -2
|
||||||
return value.compareTo(other.doubleValue)
|
return value.compareTo(other.doubleValue)
|
||||||
}
|
}
|
||||||
@@ -63,19 +67,24 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun plus(scope: Scope, other: Obj): Obj =
|
override suspend fun plus(scope: Scope, other: Obj): Obj =
|
||||||
of(this.value + other.toDouble())
|
OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Plus)
|
||||||
|
?: of(this.value + other.toDouble())
|
||||||
|
|
||||||
override suspend fun minus(scope: Scope, other: Obj): Obj =
|
override suspend fun minus(scope: Scope, other: Obj): Obj =
|
||||||
of(this.value - other.toDouble())
|
OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Minus)
|
||||||
|
?: of(this.value - other.toDouble())
|
||||||
|
|
||||||
override suspend fun mul(scope: Scope, other: Obj): Obj =
|
override suspend fun mul(scope: Scope, other: Obj): Obj =
|
||||||
of(this.value * other.toDouble())
|
OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Mul)
|
||||||
|
?: of(this.value * other.toDouble())
|
||||||
|
|
||||||
override suspend fun div(scope: Scope, other: Obj): Obj =
|
override suspend fun div(scope: Scope, other: Obj): Obj =
|
||||||
of(this.value / other.toDouble())
|
OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Div)
|
||||||
|
?: of(this.value / other.toDouble())
|
||||||
|
|
||||||
override suspend fun mod(scope: Scope, other: Obj): Obj =
|
override suspend fun mod(scope: Scope, other: Obj): Obj =
|
||||||
of(this.value % other.toDouble())
|
OperatorInteropRegistry.invokeBinary(scope, this, other, InteropOperator.Mod)
|
||||||
|
?: of(this.value % other.toDouble())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns unboxed Double value
|
* Returns unboxed Double value
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.sergeych.lyng.stdlib_included
|
||||||
|
|
||||||
|
@Suppress("Unused", "MemberVisibilityCanBePrivate")
|
||||||
|
internal val decimalLyng = """
|
||||||
|
package lyng.decimal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rounding policies used by [DecimalContext] and `withDecimalContext(...)`.
|
||||||
|
*
|
||||||
|
* These modes currently affect decimal division. They are designed to be explicit and readable in Lyng code.
|
||||||
|
*
|
||||||
|
* Common examples at precision `2`:
|
||||||
|
* - `HalfEven`: `1.d / 8.d -> 0.12`
|
||||||
|
* - `HalfAwayFromZero`: `1.d / 8.d -> 0.13`, `-1.d / 8.d -> -0.13`
|
||||||
|
* - `HalfTowardsZero`: `1.d / 8.d -> 0.12`, `-1.d / 8.d -> -0.12`
|
||||||
|
* - `Ceiling`: rounds toward positive infinity
|
||||||
|
* - `Floor`: rounds toward negative infinity
|
||||||
|
* - `AwayFromZero`: always increases magnitude when rounding is needed
|
||||||
|
* - `TowardsZero`: always truncates toward zero
|
||||||
|
*/
|
||||||
|
enum DecimalRounding {
|
||||||
|
HalfEven,
|
||||||
|
HalfAwayFromZero,
|
||||||
|
HalfTowardsZero,
|
||||||
|
Ceiling,
|
||||||
|
Floor,
|
||||||
|
AwayFromZero,
|
||||||
|
TowardsZero
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamic decimal arithmetic settings.
|
||||||
|
*
|
||||||
|
* A decimal context is not attached permanently to a `BigDecimal` value. Instead, it is applied dynamically
|
||||||
|
* inside `withDecimalContext(...)`, which makes the rule local to a block of code.
|
||||||
|
*
|
||||||
|
* Default context:
|
||||||
|
* - precision: `34` significant digits
|
||||||
|
* - rounding: `DecimalRounding.HalfEven`
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* import lyng.decimal
|
||||||
|
*
|
||||||
|
* (1.d / 3.d).toStringExpanded()
|
||||||
|
* >>> "0.3333333333333333333333333333333333"
|
||||||
|
*
|
||||||
|
* withDecimalContext(10) { (1.d / 3.d).toStringExpanded() }
|
||||||
|
* >>> "0.3333333333"
|
||||||
|
*
|
||||||
|
* withDecimalContext(2, DecimalRounding.HalfAwayFromZero) { (1.d / 8.d).toStringExpanded() }
|
||||||
|
* >>> "0.13"
|
||||||
|
*/
|
||||||
|
class DecimalContext(
|
||||||
|
val precision: Int = 34,
|
||||||
|
val rounding: DecimalRounding = DecimalRounding.HalfEven
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Arbitrary-precision decimal value.
|
||||||
|
*
|
||||||
|
* `BigDecimal` is intended for decimal arithmetic where binary floating-point (`Real`) is the wrong tool:
|
||||||
|
* - money
|
||||||
|
* - human-entered decimal values
|
||||||
|
* - ratios that should round in decimal, not in binary
|
||||||
|
* - reproducible decimal formatting
|
||||||
|
*
|
||||||
|
* Creating values:
|
||||||
|
*
|
||||||
|
* - `1.d` converts `Int -> BigDecimal`
|
||||||
|
* - `2.2.d` converts `Real -> BigDecimal` by preserving the current IEEE-754 value
|
||||||
|
* - `"2.2".d` parses exact decimal text
|
||||||
|
* - `BigDecimal.fromInt(...)`, `fromReal(...)`, `fromString(...)` are explicit factory forms
|
||||||
|
*
|
||||||
|
* Important distinction:
|
||||||
|
*
|
||||||
|
* - `2.2.d` means "take the current `Real` value and convert it"
|
||||||
|
* - `"2.2".d` means "parse this exact decimal literal text"
|
||||||
|
*
|
||||||
|
* Therefore:
|
||||||
|
*
|
||||||
|
* import lyng.decimal
|
||||||
|
*
|
||||||
|
* 2.2.d.toStringExpanded()
|
||||||
|
* >>> "2.2"
|
||||||
|
*
|
||||||
|
* (0.1 + 0.2).d.toStringExpanded()
|
||||||
|
* >>> "0.30000000000000004"
|
||||||
|
*
|
||||||
|
* "0.3".d.toStringExpanded()
|
||||||
|
* >>> "0.3"
|
||||||
|
*
|
||||||
|
* Mixed arithmetic:
|
||||||
|
*
|
||||||
|
* `BigDecimal` defines its own operators against decimal-compatible values, and the decimal module also registers
|
||||||
|
* interop bridges so built-in left-hand operands work naturally:
|
||||||
|
*
|
||||||
|
* import lyng.decimal
|
||||||
|
*
|
||||||
|
* 1.d + 2
|
||||||
|
* >>> 3.d
|
||||||
|
*
|
||||||
|
* 1 + 2.d
|
||||||
|
* >>> 3.d
|
||||||
|
*
|
||||||
|
* 0.5 + 1.d
|
||||||
|
* >>> 1.5.d
|
||||||
|
*
|
||||||
|
* Precision and rounding:
|
||||||
|
*
|
||||||
|
* - division uses the default decimal context unless overridden
|
||||||
|
* - use `withDecimalContext(...)` to apply a local precision/rounding policy
|
||||||
|
*
|
||||||
|
* Exact decimal literal style:
|
||||||
|
*
|
||||||
|
* If you want the source text itself to be the decimal value, use a string:
|
||||||
|
*
|
||||||
|
* "2.2".d
|
||||||
|
*
|
||||||
|
* That is the precise form. `2.2.d` remains a `Real -> BigDecimal` conversion by design.
|
||||||
|
*/
|
||||||
|
extern class BigDecimal() {
|
||||||
|
/** Add another decimal-compatible value. */
|
||||||
|
extern fun plus(other: Object): BigDecimal
|
||||||
|
/** Subtract another decimal-compatible value. */
|
||||||
|
extern fun minus(other: Object): BigDecimal
|
||||||
|
/** Multiply by another decimal-compatible value. */
|
||||||
|
extern fun mul(other: Object): BigDecimal
|
||||||
|
/**
|
||||||
|
* Divide by another decimal-compatible value.
|
||||||
|
*
|
||||||
|
* Division uses the current decimal context:
|
||||||
|
* - by default: `34` significant digits, `HalfEven`
|
||||||
|
* - inside `withDecimalContext(...)`: the context active for the current block
|
||||||
|
*/
|
||||||
|
extern fun div(other: Object): BigDecimal
|
||||||
|
/** Remainder with another decimal-compatible value. */
|
||||||
|
extern fun mod(other: Object): BigDecimal
|
||||||
|
/** Compare with another decimal-compatible value. */
|
||||||
|
extern fun compareTo(other: Object): Int
|
||||||
|
/** Unary minus. */
|
||||||
|
extern fun negate(): BigDecimal
|
||||||
|
/** Convert to `Int` by dropping the fractional part according to backend conversion rules. */
|
||||||
|
extern fun toInt(): Int
|
||||||
|
/** Convert to `Real`. */
|
||||||
|
extern fun toReal(): Real
|
||||||
|
/**
|
||||||
|
* Convert to a plain decimal string without scientific notation.
|
||||||
|
*
|
||||||
|
* This is the preferred representation for user-facing decimal tests and diagnostics.
|
||||||
|
*/
|
||||||
|
extern fun toStringExpanded(): String
|
||||||
|
|
||||||
|
/** Create a decimal from an `Int`. */
|
||||||
|
static extern fun fromInt(value: Int): BigDecimal
|
||||||
|
/**
|
||||||
|
* Create a decimal from a `Real`.
|
||||||
|
*
|
||||||
|
* This preserves the current IEEE-754 value using a round-trip-safe decimal conversion.
|
||||||
|
* It does not try to recover the original source text.
|
||||||
|
*/
|
||||||
|
static extern fun fromReal(value: Real): BigDecimal
|
||||||
|
/** Parse exact decimal text. */
|
||||||
|
static extern fun fromString(value: String): BigDecimal
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run [block] with the provided decimal context.
|
||||||
|
*
|
||||||
|
* This is the main way to control decimal division precision and rounding locally without changing global behavior.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* import lyng.decimal
|
||||||
|
*
|
||||||
|
* withDecimalContext(10) {
|
||||||
|
* (1.d / 3.d).toStringExpanded()
|
||||||
|
* }
|
||||||
|
* >>> "0.3333333333"
|
||||||
|
*
|
||||||
|
* Contexts are dynamic and block-local. After the block finishes, the previous context is restored.
|
||||||
|
*/
|
||||||
|
extern fun withDecimalContext<T>(context: DecimalContext, block: ()->T): T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience overload for changing only precision.
|
||||||
|
*
|
||||||
|
* Equivalent to `withDecimalContext(DecimalContext(precision, DecimalRounding.HalfEven), block)`.
|
||||||
|
*/
|
||||||
|
extern fun withDecimalContext<T>(precision: Int, block: ()->T): T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience overload for changing precision and rounding explicitly.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* import lyng.decimal
|
||||||
|
*
|
||||||
|
* withDecimalContext(2, DecimalRounding.HalfAwayFromZero) {
|
||||||
|
* (1.d / 8.d).toStringExpanded()
|
||||||
|
* }
|
||||||
|
* >>> "0.13"
|
||||||
|
*/
|
||||||
|
extern fun withDecimalContext<T>(precision: Int, rounding: DecimalRounding, block: ()->T): T
|
||||||
|
""".trimIndent()
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.sergeych.lyng.stdlib_included
|
||||||
|
|
||||||
|
@Suppress("Unused", "MemberVisibilityCanBePrivate")
|
||||||
|
internal val operatorsLyng = """
|
||||||
|
package lyng.operators
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binary operators that can be bridged between two different operand classes.
|
||||||
|
*
|
||||||
|
* Registering a pair means:
|
||||||
|
* - the runtime can evaluate `left op right` when `left` has class `L` and `right` has class `R`
|
||||||
|
* - both operands are first converted to a shared "common" class `C`
|
||||||
|
* - the actual operator implementation is then looked up on `C`
|
||||||
|
*
|
||||||
|
* This is primarily useful when:
|
||||||
|
* - you add a new numeric-like type in a library or in pure Lyng code
|
||||||
|
* - your type already implements operators against itself
|
||||||
|
* - you also want existing left-hand types such as `Int` or `Real` to work when your type is on the right
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* import lyng.operators
|
||||||
|
*
|
||||||
|
* class DecimalBox(val value: Int) {
|
||||||
|
* fun plus(other: DecimalBox) = DecimalBox(value + other.value)
|
||||||
|
* fun compareTo(other: DecimalBox) = value <=> other.value
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* OperatorInterop.register(
|
||||||
|
* Int,
|
||||||
|
* DecimalBox,
|
||||||
|
* DecimalBox,
|
||||||
|
* [BinaryOperator.Plus, BinaryOperator.Compare, BinaryOperator.Equals],
|
||||||
|
* { x: Int -> DecimalBox(x) },
|
||||||
|
* { x: DecimalBox -> x }
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* After registration:
|
||||||
|
* - `1 + DecimalBox(2)` works
|
||||||
|
* - `1 < DecimalBox(2)` works
|
||||||
|
* - `2 == DecimalBox(2)` works
|
||||||
|
*
|
||||||
|
* But this registration does not replace methods on the original classes. It only teaches
|
||||||
|
* Lyng how to bridge a mixed pair into a common class for the listed operators.
|
||||||
|
*/
|
||||||
|
enum BinaryOperator {
|
||||||
|
/** `a + b` */
|
||||||
|
Plus,
|
||||||
|
/** `a - b` */
|
||||||
|
Minus,
|
||||||
|
/** `a * b` */
|
||||||
|
Mul,
|
||||||
|
/** `a / b` */
|
||||||
|
Div,
|
||||||
|
/** `a % b` */
|
||||||
|
Mod,
|
||||||
|
/**
|
||||||
|
* Ordering comparisons.
|
||||||
|
*
|
||||||
|
* Registering `Compare` enables `<`, `<=`, `>`, `>=`, and the shuttle operator `<=>`
|
||||||
|
* for the mixed operand pair.
|
||||||
|
*/
|
||||||
|
Compare,
|
||||||
|
/**
|
||||||
|
* Equality comparisons.
|
||||||
|
*
|
||||||
|
* Registering `Equals` enables `==` and `!=` for the mixed operand pair.
|
||||||
|
*/
|
||||||
|
Equals
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime registry for mixed-class binary operators.
|
||||||
|
*
|
||||||
|
* `register(L, R, C, ...)` defines how Lyng should evaluate expressions where:
|
||||||
|
* - the left operand has class `L`
|
||||||
|
* - the right operand has class `R`
|
||||||
|
* - the actual operation should be executed as if both were values of class `C`
|
||||||
|
*
|
||||||
|
* The registry is symmetric for the converted values, but not for the original syntax.
|
||||||
|
* Its job is specifically to fill the gap where your custom type appears on the right:
|
||||||
|
*
|
||||||
|
* - `myDecimal + 1` usually already works if `BigDecimal.plus(Int)` exists
|
||||||
|
* - `1 + myDecimal` needs registration because `Int` itself is not rewritten
|
||||||
|
*
|
||||||
|
* Typical pattern for a custom type:
|
||||||
|
*
|
||||||
|
* import lyng.operators
|
||||||
|
*
|
||||||
|
* class Rational(val num: Int, val den: Int) {
|
||||||
|
* fun plus(other: Rational) = Rational(num * other.den + other.num * den, den * other.den)
|
||||||
|
* fun minus(other: Rational) = Rational(num * other.den - other.num * den, den * other.den)
|
||||||
|
* fun mul(other: Rational) = Rational(num * other.num, den * other.den)
|
||||||
|
* fun div(other: Rational) = Rational(num * other.den, den * other.num)
|
||||||
|
* fun compareTo(other: Rational) = (num * other.den) <=> (other.num * den)
|
||||||
|
*
|
||||||
|
* static fun fromInt(value: Int) = Rational(value, 1)
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* OperatorInterop.register(
|
||||||
|
* Int,
|
||||||
|
* Rational,
|
||||||
|
* Rational,
|
||||||
|
* [
|
||||||
|
* BinaryOperator.Plus,
|
||||||
|
* BinaryOperator.Minus,
|
||||||
|
* BinaryOperator.Mul,
|
||||||
|
* BinaryOperator.Div,
|
||||||
|
* BinaryOperator.Compare,
|
||||||
|
* BinaryOperator.Equals
|
||||||
|
* ],
|
||||||
|
* { x: Int -> Rational.fromInt(x) },
|
||||||
|
* { x: Rational -> x }
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* Then:
|
||||||
|
* - `1 + Rational(1, 2)` works
|
||||||
|
* - `3 > Rational(5, 2)` works
|
||||||
|
* - `2 == Rational(2, 1)` works
|
||||||
|
*
|
||||||
|
* Decimal uses the same mechanism internally to make `Int + BigDecimal` and `Real + BigDecimal`
|
||||||
|
* work without changing the built-in `Int` or `Real` classes.
|
||||||
|
*/
|
||||||
|
extern object OperatorInterop {
|
||||||
|
/**
|
||||||
|
* Register a mixed-operand operator bridge.
|
||||||
|
*
|
||||||
|
* @param leftClass class of the original left operand
|
||||||
|
* @param rightClass class of the original right operand
|
||||||
|
* @param commonClass class that will actually execute the operator methods
|
||||||
|
* @param operators operators supported for this mixed pair
|
||||||
|
* @param leftToCommon conversion from `L` to `C`
|
||||||
|
* @param rightToCommon conversion from `R` to `C`
|
||||||
|
*
|
||||||
|
* Requirements for `commonClass`:
|
||||||
|
* - if you register `Plus`, `C` should implement `fun plus(other: C): C` or equivalent accepted result type
|
||||||
|
* - if you register `Minus`, `C` should implement `fun minus(other: C): ...`
|
||||||
|
* - if you register `Mul`, `C` should implement `fun mul(other: C): ...`
|
||||||
|
* - if you register `Div`, `C` should implement `fun div(other: C): ...`
|
||||||
|
* - if you register `Mod`, `C` should implement `fun mod(other: C): ...`
|
||||||
|
* - if you register `Compare`, `C` should implement `fun compareTo(other: C): Int`
|
||||||
|
*
|
||||||
|
* `Equals` reuses comparison/equality semantics of the promoted values.
|
||||||
|
*
|
||||||
|
* Registration is usually done once at module initialization time:
|
||||||
|
*
|
||||||
|
* package my.rational
|
||||||
|
* import lyng.operators
|
||||||
|
*
|
||||||
|
* class Rational(val num: Int, val den: Int) {
|
||||||
|
* fun plus(other: Rational) = Rational(num * other.den + other.num * den, den * other.den)
|
||||||
|
* fun compareTo(other: Rational) = (num * other.den) <=> (other.num * den)
|
||||||
|
* static fun fromInt(value: Int) = Rational(value, 1)
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* OperatorInterop.register(
|
||||||
|
* Int,
|
||||||
|
* Rational,
|
||||||
|
* Rational,
|
||||||
|
* [BinaryOperator.Plus, BinaryOperator.Compare, BinaryOperator.Equals],
|
||||||
|
* { x: Int -> Rational.fromInt(x) },
|
||||||
|
* { x: Rational -> x }
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
extern fun register<L, R, C>(
|
||||||
|
leftClass: Class<L>,
|
||||||
|
rightClass: Class<R>,
|
||||||
|
commonClass: Class<C>,
|
||||||
|
operators: List<BinaryOperator>,
|
||||||
|
leftToCommon: (L)->C,
|
||||||
|
rightToCommon: (R)->C
|
||||||
|
): Void
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import net.sergeych.lyng.Compiler
|
||||||
|
import net.sergeych.lyng.Scope
|
||||||
|
import net.sergeych.lyng.Script
|
||||||
|
import net.sergeych.lyng.ScriptError
|
||||||
|
import net.sergeych.lyng.Source
|
||||||
|
import net.sergeych.lyng.asFacade
|
||||||
|
import net.sergeych.lyng.obj.ObjString
|
||||||
|
import net.sergeych.lyng.obj.toInt
|
||||||
|
import net.sergeych.lyng.pacman.ImportManager
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertContains
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
|
class CompilerVmReviewRegressionTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun missingModuleCaptureFailsFastInsteadOfBecomingUnset() = runTest {
|
||||||
|
val manager = ImportManager()
|
||||||
|
manager.addTextPackages(
|
||||||
|
"""
|
||||||
|
package foo
|
||||||
|
|
||||||
|
val answer = 42
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
val script = Compiler.compile(
|
||||||
|
Source(
|
||||||
|
"<missing-module-capture>",
|
||||||
|
"""
|
||||||
|
import foo
|
||||||
|
fun make() = { answer }
|
||||||
|
make()
|
||||||
|
""".trimIndent()
|
||||||
|
),
|
||||||
|
manager
|
||||||
|
)
|
||||||
|
|
||||||
|
val prepared = manager.newModule()
|
||||||
|
script.importInto(prepared)
|
||||||
|
val preparedLambda = script.execute(prepared)
|
||||||
|
assertEquals(42, prepared.asFacade().call(preparedLambda).toInt())
|
||||||
|
|
||||||
|
val rawModule = manager.newModule()
|
||||||
|
val hostScope = Scope(parent = rawModule, thisObj = ObjString("receiver"))
|
||||||
|
|
||||||
|
val lambda = script.execute(hostScope)
|
||||||
|
val ex = assertFailsWith<ScriptError> {
|
||||||
|
hostScope.asFacade().call(lambda)
|
||||||
|
}
|
||||||
|
assertContains(ex.errorMessage, "module binding 'answer'")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun subjectlessWhenReportsScriptError() = runTest {
|
||||||
|
val ex = assertFailsWith<ScriptError> {
|
||||||
|
Compiler.compile(
|
||||||
|
Source(
|
||||||
|
"<when-without-subject>",
|
||||||
|
"""
|
||||||
|
when {
|
||||||
|
true -> 1
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
),
|
||||||
|
Script.defaultImportManager
|
||||||
|
)
|
||||||
|
}
|
||||||
|
assertContains(ex.errorMessage, "when without subject")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.sergeych.lyng
|
||||||
|
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import net.sergeych.lyng.bridge.bindGlobalVar
|
||||||
|
import net.sergeych.lyng.bridge.globalBinder
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class GlobalPropertyCaptureRegressionTest {
|
||||||
|
@Test
|
||||||
|
fun externGlobalVarAssignmentInsideFunctionShouldCallBoundSetter() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
var x = 1.0
|
||||||
|
|
||||||
|
scope.eval(
|
||||||
|
"""
|
||||||
|
extern var X: Real
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
X = X + 1.0
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
scope.globalBinder().bindGlobalVar(
|
||||||
|
name = "X",
|
||||||
|
get = { x },
|
||||||
|
set = { x = it }
|
||||||
|
)
|
||||||
|
|
||||||
|
scope.eval("main()")
|
||||||
|
|
||||||
|
assertEquals(2.0, x, "bound extern var should stay live inside function bodies")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import net.sergeych.lyng.Compiler
|
||||||
|
import net.sergeych.lyng.Script
|
||||||
|
import net.sergeych.lyng.Source
|
||||||
|
import net.sergeych.lyng.obj.toInt
|
||||||
|
import net.sergeych.lyng.pacman.ImportManager
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNotNull
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
|
||||||
|
class ScriptImportPreparationTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun scriptImportIntoExplicitlyPreparesExistingScope() = runTest {
|
||||||
|
val manager = ImportManager()
|
||||||
|
manager.addTextPackages(
|
||||||
|
"""
|
||||||
|
package foo
|
||||||
|
|
||||||
|
val answer = 42
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
val script = Compiler.compile(
|
||||||
|
Source(
|
||||||
|
"<prepare-scope>",
|
||||||
|
"""
|
||||||
|
import foo
|
||||||
|
answer
|
||||||
|
""".trimIndent()
|
||||||
|
),
|
||||||
|
manager
|
||||||
|
)
|
||||||
|
val scope = manager.newModule()
|
||||||
|
|
||||||
|
assertNull(scope["answer"])
|
||||||
|
|
||||||
|
script.importInto(scope)
|
||||||
|
|
||||||
|
val record = assertNotNull(scope["answer"])
|
||||||
|
assertEquals(42, scope.resolve(record, "answer").toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun scriptInstantiateModuleUsesSeedScopeImportProvider() = runTest {
|
||||||
|
val manager = ImportManager()
|
||||||
|
manager.addTextPackages(
|
||||||
|
"""
|
||||||
|
package foo
|
||||||
|
|
||||||
|
val answer = 42
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
val script = Compiler.compile(
|
||||||
|
Source(
|
||||||
|
"<instantiate-module>",
|
||||||
|
"""
|
||||||
|
import foo
|
||||||
|
answer
|
||||||
|
""".trimIndent()
|
||||||
|
),
|
||||||
|
manager
|
||||||
|
)
|
||||||
|
val seedScope = manager.newModule()
|
||||||
|
|
||||||
|
val module = script.instantiateModule(seedScope)
|
||||||
|
|
||||||
|
val record = assertNotNull(module["answer"])
|
||||||
|
assertEquals(42, module.resolve(record, "answer").toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import net.sergeych.lyng.FrameSlotRef
|
||||||
|
import net.sergeych.lyng.Pos
|
||||||
|
import net.sergeych.lyng.Scope
|
||||||
|
import net.sergeych.lyng.bytecode.BytecodeConst
|
||||||
|
import net.sergeych.lyng.bytecode.CmdFrame
|
||||||
|
import net.sergeych.lyng.bytecode.CmdFunction
|
||||||
|
import net.sergeych.lyng.bytecode.CmdVm
|
||||||
|
import net.sergeych.lyng.bytecode.seedFrameLocalsFromScope
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
|
||||||
|
class SeedLocalsRegressionTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun seedFrameLocalsSkipsSelfReferentialFrameSlotRef() = runTest {
|
||||||
|
val fn = CmdFunction(
|
||||||
|
name = "seed-self-ref",
|
||||||
|
localCount = 1,
|
||||||
|
addrCount = 0,
|
||||||
|
returnLabels = emptySet(),
|
||||||
|
scopeSlotCount = 1,
|
||||||
|
scopeSlotIndices = intArrayOf(0),
|
||||||
|
scopeSlotNames = arrayOf(null),
|
||||||
|
scopeSlotIsModule = booleanArrayOf(false),
|
||||||
|
localSlotNames = arrayOf("x"),
|
||||||
|
localSlotMutables = booleanArrayOf(true),
|
||||||
|
localSlotDelegated = booleanArrayOf(false),
|
||||||
|
localSlotCaptures = booleanArrayOf(false),
|
||||||
|
constants = listOf(BytecodeConst.IntVal(0)),
|
||||||
|
cmds = emptyArray(),
|
||||||
|
posByIp = emptyArray()
|
||||||
|
)
|
||||||
|
val scope = Scope(null, pos = Pos.builtIn)
|
||||||
|
val record = scope.addConst("x", net.sergeych.lyng.obj.ObjInt.of(1))
|
||||||
|
val frame = CmdFrame(CmdVm(), fn, scope, emptyList())
|
||||||
|
record.value = FrameSlotRef(frame.frame, 0)
|
||||||
|
scope.updateSlotFor("x", record)
|
||||||
|
|
||||||
|
seedFrameLocalsFromScope(frame, scope)
|
||||||
|
|
||||||
|
assertNull(frame.frame.getRawObj(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.sergeych.lyng
|
||||||
|
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlin.test.Test
|
||||||
|
|
||||||
|
class BigDecimalModuleTest {
|
||||||
|
@Test
|
||||||
|
fun testDecimalModuleFactoriesAndConversions() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
scope.eval(
|
||||||
|
"""
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
assertEquals("12.34", BigDecimal.fromString("12.34").toStringExpanded())
|
||||||
|
assertEquals("1", BigDecimal.fromInt(1).toStringExpanded())
|
||||||
|
assertEquals("2.5", "2.5".d.toStringExpanded())
|
||||||
|
assertEquals("1", 1.d.toStringExpanded())
|
||||||
|
assertEquals("2.2", 2.2.d.toStringExpanded())
|
||||||
|
assertEquals("3", (1 + 2).d.toStringExpanded())
|
||||||
|
assertEquals("1.5", (1 + 0.5).d.toStringExpanded())
|
||||||
|
assertEquals("3", (1 + 2.d).toStringExpanded())
|
||||||
|
assertEquals("3", (2.d + 1).toStringExpanded())
|
||||||
|
assertEquals(2.5, "2.5".d.toReal())
|
||||||
|
assertEquals(2, "2.5".d.toInt())
|
||||||
|
assertEquals(2.2, 2.2.d.toReal())
|
||||||
|
assertEquals("0.30000000000000004", (0.1 + 0.2).d.toStringExpanded())
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDecimalModuleMixedIntOperators() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
scope.eval(
|
||||||
|
"""
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
assertEquals(3.d, 1 + 2.d)
|
||||||
|
assertEquals(3.d, 2.d + 1)
|
||||||
|
assertEquals(1.d, 3 - 2.d)
|
||||||
|
assertEquals(1.d, 3.d - 2)
|
||||||
|
assertEquals(8.d, 4 * 2.d)
|
||||||
|
assertEquals(8.d, 4.d * 2)
|
||||||
|
assertEquals(4.d, 8 / 2.d)
|
||||||
|
assertEquals(4.d, 8.d / 2)
|
||||||
|
assertEquals(1.d, 7 % 2.d)
|
||||||
|
assertEquals(1.d, 7.d % 2)
|
||||||
|
assert(1 < 2.d)
|
||||||
|
assert(2 <= 2.d)
|
||||||
|
assert(3 > 2.d)
|
||||||
|
assert(3.d > 2)
|
||||||
|
assert(2 == 2.d)
|
||||||
|
assert(2.d == 2)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDecimalDivisionUsesDefaultContext() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
scope.eval(
|
||||||
|
"""
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
assertEquals("0.125", (1.d / 8.d).toStringExpanded())
|
||||||
|
assertEquals("0.3333333333333333333333333333333333", (1.d / 3.d).toStringExpanded())
|
||||||
|
assertEquals("0.6666666666666666666666666666666667", ("2".d / 3.d).toStringExpanded())
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testWithDecimalContextOverridesDivisionContext() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
scope.eval(
|
||||||
|
"""
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
assertEquals("0.3333333333333333333333333333333333", (1.d / 3.d).toStringExpanded())
|
||||||
|
assertEquals("0.3333333333", withDecimalContext(10) { (1.d / 3.d).toStringExpanded() })
|
||||||
|
assertEquals("0.666667", withDecimalContext(6) { ("2".d / 3.d).toStringExpanded() })
|
||||||
|
assertEquals("0.666667", withDecimalContext(DecimalContext(6)) { ("2".d / 3.d).toStringExpanded() })
|
||||||
|
assertEquals("0.12", withDecimalContext(2) { (1.d / 8.d).toStringExpanded() })
|
||||||
|
assertEquals("0.13", withDecimalContext(2, DecimalRounding.HalfAwayFromZero) { (1.d / 8.d).toStringExpanded() })
|
||||||
|
assertEquals("0.13", withDecimalContext(DecimalContext(2, DecimalRounding.HalfAwayFromZero)) { (1.d / 8.d).toStringExpanded() })
|
||||||
|
assertEquals("0.3333333333333333333333333333333333", (1.d / 3.d).toStringExpanded())
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDecimalDivisionRoundingMatrix() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
scope.eval(
|
||||||
|
"""
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
assertEquals("0.12", withDecimalContext(2, DecimalRounding.HalfEven) { (1.d / 8.d).toStringExpanded() })
|
||||||
|
assertEquals("-0.12", withDecimalContext(2, DecimalRounding.HalfEven) { (-1.d / 8.d).toStringExpanded() })
|
||||||
|
|
||||||
|
assertEquals("0.13", withDecimalContext(2, DecimalRounding.HalfAwayFromZero) { (1.d / 8.d).toStringExpanded() })
|
||||||
|
assertEquals("-0.13", withDecimalContext(2, DecimalRounding.HalfAwayFromZero) { (-1.d / 8.d).toStringExpanded() })
|
||||||
|
|
||||||
|
assertEquals("0.12", withDecimalContext(2, DecimalRounding.HalfTowardsZero) { (1.d / 8.d).toStringExpanded() })
|
||||||
|
assertEquals("-0.12", withDecimalContext(2, DecimalRounding.HalfTowardsZero) { (-1.d / 8.d).toStringExpanded() })
|
||||||
|
|
||||||
|
assertEquals("0.13", withDecimalContext(2, DecimalRounding.Ceiling) { (1.d / 8.d).toStringExpanded() })
|
||||||
|
assertEquals("-0.12", withDecimalContext(2, DecimalRounding.Ceiling) { (-1.d / 8.d).toStringExpanded() })
|
||||||
|
|
||||||
|
assertEquals("0.12", withDecimalContext(2, DecimalRounding.Floor) { (1.d / 8.d).toStringExpanded() })
|
||||||
|
assertEquals("-0.13", withDecimalContext(2, DecimalRounding.Floor) { (-1.d / 8.d).toStringExpanded() })
|
||||||
|
|
||||||
|
assertEquals("0.13", withDecimalContext(2, DecimalRounding.AwayFromZero) { (1.d / 8.d).toStringExpanded() })
|
||||||
|
assertEquals("-0.13", withDecimalContext(2, DecimalRounding.AwayFromZero) { (-1.d / 8.d).toStringExpanded() })
|
||||||
|
|
||||||
|
assertEquals("0.12", withDecimalContext(2, DecimalRounding.TowardsZero) { (1.d / 8.d).toStringExpanded() })
|
||||||
|
assertEquals("-0.12", withDecimalContext(2, DecimalRounding.TowardsZero) { (-1.d / 8.d).toStringExpanded() })
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -212,6 +212,96 @@ class DelegationTest {
|
|||||||
assertTrue(badThrown)
|
assertTrue(badThrown)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testPureLyngLazyPreservesReceiverAndClosure() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
scope.eval(
|
||||||
|
"""
|
||||||
|
val GLOBAL_NUMBERS = [1,2,3]
|
||||||
|
|
||||||
|
class PureLazy<T,ThisRefType=Object>(creatorParam: ThisRefType.()->T) : Delegate<T,ThisRefType> {
|
||||||
|
private val creator: ThisRefType.()->T = creatorParam
|
||||||
|
private var value = Unset
|
||||||
|
|
||||||
|
override fun bind(name: String, access, thisRef: ThisRefType): Object = this
|
||||||
|
|
||||||
|
override fun getValue(thisRef: ThisRefType, name: String): T {
|
||||||
|
if (value == Unset)
|
||||||
|
value = creator(thisRef)
|
||||||
|
value as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pureLazy<T,ThisRefType=Object>(creator: ThisRefType.()->T): Delegate<T,ThisRefType> = PureLazy(creator)
|
||||||
|
|
||||||
|
class A {
|
||||||
|
val numbers = [1,2,3]
|
||||||
|
val fromThis: List by pureLazy { this.numbers }
|
||||||
|
val fromScope: List by pureLazy { GLOBAL_NUMBERS }
|
||||||
|
}
|
||||||
|
|
||||||
|
class B {
|
||||||
|
val a: A by pureLazy { A() }
|
||||||
|
val test: List by pureLazy { (a as A).fromThis + [4] }
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals([1,2,3], A().fromThis)
|
||||||
|
assertEquals([1,2,3], A().fromScope)
|
||||||
|
assertEquals([1,2,3,4], B().test)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testImportedPureLyngLazyPreservesReceiverAndClosure() = runTest {
|
||||||
|
val scope = Script.newScope()
|
||||||
|
scope.importManager.addTextPackages(
|
||||||
|
"""
|
||||||
|
package repro.lazy
|
||||||
|
|
||||||
|
import lyng.stdlib
|
||||||
|
|
||||||
|
class PureLazy<T,ThisRefType=Object>(creatorParam: ThisRefType.()->T) : Delegate<T,ThisRefType> {
|
||||||
|
private val creator: ThisRefType.()->T = creatorParam
|
||||||
|
private var value = Unset
|
||||||
|
|
||||||
|
override fun bind(name: String, access: DelegateAccess, thisRef: ThisRefType): Object {
|
||||||
|
if (access != DelegateAccess.Val) throw "lazy delegate can only be used with 'val'"
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getValue(thisRef: ThisRefType, name: String): T {
|
||||||
|
if (value == Unset)
|
||||||
|
value = with(thisRef, creator)
|
||||||
|
value as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
scope.eval(
|
||||||
|
"""
|
||||||
|
import repro.lazy
|
||||||
|
|
||||||
|
val GLOBAL_NUMBERS = [1,2,3]
|
||||||
|
|
||||||
|
class A {
|
||||||
|
val numbers = [1,2,3]
|
||||||
|
val fromThis: List by PureLazy { this.numbers }
|
||||||
|
val fromScope: List by PureLazy { GLOBAL_NUMBERS }
|
||||||
|
}
|
||||||
|
|
||||||
|
class B {
|
||||||
|
val a: A by PureLazy { A() }
|
||||||
|
val test: List by PureLazy { (a as A).fromThis + [4] }
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals([1,2,3], A().fromThis)
|
||||||
|
assertEquals([1,2,3], A().fromScope)
|
||||||
|
assertEquals([1,2,3,4], B().test)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testLazyIsDelegate() = runTest {
|
fun testLazyIsDelegate() = runTest {
|
||||||
eval("""
|
eval("""
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.sergeych.lyng
|
||||||
|
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlin.test.Test
|
||||||
|
|
||||||
|
class OperatorInteropTest {
|
||||||
|
@Test
|
||||||
|
fun testPureLyngOperatorInteropRegistration() = runTest {
|
||||||
|
val im = Script.defaultImportManager.copy()
|
||||||
|
im.addPackage("test.decimalbox") { scope ->
|
||||||
|
scope.eval(
|
||||||
|
"""
|
||||||
|
package test.decimalbox
|
||||||
|
import lyng.operators
|
||||||
|
|
||||||
|
class DecimalBox(val value: Int) {
|
||||||
|
fun plus(other: DecimalBox) = DecimalBox(value + other.value)
|
||||||
|
fun minus(other: DecimalBox) = DecimalBox(value - other.value)
|
||||||
|
fun mul(other: DecimalBox) = DecimalBox(value * other.value)
|
||||||
|
fun div(other: DecimalBox) = DecimalBox(value / other.value)
|
||||||
|
fun mod(other: DecimalBox) = DecimalBox(value % other.value)
|
||||||
|
fun compareTo(other: DecimalBox) = value <=> other.value
|
||||||
|
}
|
||||||
|
|
||||||
|
OperatorInterop.register(
|
||||||
|
Int,
|
||||||
|
DecimalBox,
|
||||||
|
DecimalBox,
|
||||||
|
[BinaryOperator.Plus, BinaryOperator.Minus, BinaryOperator.Mul, BinaryOperator.Div, BinaryOperator.Mod, BinaryOperator.Compare, BinaryOperator.Equals],
|
||||||
|
{ x: Int -> DecimalBox(x) },
|
||||||
|
{ x: DecimalBox -> x }
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val scope = im.newStdScope()
|
||||||
|
scope.eval(
|
||||||
|
"""
|
||||||
|
import test.decimalbox
|
||||||
|
|
||||||
|
assertEquals(DecimalBox(3), 1 + DecimalBox(2))
|
||||||
|
assertEquals(DecimalBox(1), 3 - DecimalBox(2))
|
||||||
|
assertEquals(DecimalBox(8), 4 * DecimalBox(2))
|
||||||
|
assertEquals(DecimalBox(4), 8 / DecimalBox(2))
|
||||||
|
assertEquals(DecimalBox(1), 7 % DecimalBox(2))
|
||||||
|
assert(1 < DecimalBox(2))
|
||||||
|
assert(2 <= DecimalBox(2))
|
||||||
|
assert(3 > DecimalBox(2))
|
||||||
|
assert(2 >= DecimalBox(2))
|
||||||
|
assert(2 == DecimalBox(2))
|
||||||
|
assert(2 != DecimalBox(3))
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testRealInteropRegistrationUsesTopLevelModuleCode() = runTest {
|
||||||
|
val im = Script.defaultImportManager.copy()
|
||||||
|
im.addPackage("test.realbox") { scope ->
|
||||||
|
scope.eval(
|
||||||
|
"""
|
||||||
|
package test.realbox
|
||||||
|
import lyng.operators
|
||||||
|
|
||||||
|
class RealBox(val value: Real) {
|
||||||
|
fun plus(other: RealBox) = RealBox(value + other.value)
|
||||||
|
fun compareTo(other: RealBox) = value <=> other.value
|
||||||
|
}
|
||||||
|
|
||||||
|
OperatorInterop.register(
|
||||||
|
Real,
|
||||||
|
RealBox,
|
||||||
|
RealBox,
|
||||||
|
[BinaryOperator.Plus, BinaryOperator.Compare, BinaryOperator.Equals],
|
||||||
|
{ x: Real -> RealBox(x) },
|
||||||
|
{ x: RealBox -> x }
|
||||||
|
)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val scope = im.newStdScope()
|
||||||
|
scope.eval(
|
||||||
|
"""
|
||||||
|
import test.realbox
|
||||||
|
|
||||||
|
assertEquals(RealBox(1.75), 0.5 + RealBox(1.25))
|
||||||
|
assert(1.5 < RealBox(2.0))
|
||||||
|
assert(2.0 == RealBox(2.0))
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -420,7 +421,6 @@ fun with<T,R>(self: T, block: T.()->R): R {
|
|||||||
Can only be used with 'val' properties.
|
Can only be used with 'val' properties.
|
||||||
*/
|
*/
|
||||||
class lazy<T,ThisRefType=Object>(creatorParam: ThisRefType.()->T) : Delegate<T,ThisRefType> {
|
class lazy<T,ThisRefType=Object>(creatorParam: ThisRefType.()->T) : Delegate<T,ThisRefType> {
|
||||||
private val creator: ThisRefType.()->T = creatorParam
|
|
||||||
private var value = Unset
|
private var value = Unset
|
||||||
|
|
||||||
override fun bind(name: String, access: DelegateAccess, thisRef: ThisRefType): Object {
|
override fun bind(name: String, access: DelegateAccess, thisRef: ThisRefType): Object {
|
||||||
@@ -430,7 +430,7 @@ class lazy<T,ThisRefType=Object>(creatorParam: ThisRefType.()->T) : Delegate<T,T
|
|||||||
|
|
||||||
override fun getValue(thisRef: ThisRefType, name: String): T {
|
override fun getValue(thisRef: ThisRefType, name: String): T {
|
||||||
if (value == Unset)
|
if (value == Unset)
|
||||||
value = with(thisRef,creator)
|
value = with(thisRef, creatorParam)
|
||||||
value as T
|
value as T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# :lynglib Compiler and VM Review (2026-03-26)
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
- Reviewed `:lynglib` compiler and bytecode VM paths, focusing on compile/execution correctness.
|
||||||
|
- Read core files around `Compiler`, `Script`, `BytecodeCompiler`, `CmdRuntime`, `Scope`, `ModuleScope`, and capture/slot plumbing.
|
||||||
|
- Ran `./gradlew :lynglib:jvmTest` on 2026-03-26: PASS.
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
### 1. High: local seeding uses the wrong slot index when checking for self-referential `FrameSlotRef`
|
||||||
|
- Status: fixed in worktree on 2026-03-26; covered by `SeedLocalsRegressionTest`.
|
||||||
|
- File: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/SeedLocals.kt:39`
|
||||||
|
- The self-reference guard compares `FrameSlotRef` against `base + i`, but `FrameSlotRef` stores the underlying `BytecodeFrame` local slot index, not the VM absolute slot id.
|
||||||
|
- The same slot is then written using `setObjUnchecked(base + i, value)` at `SeedLocals.kt:42`, which means a self-reference is not filtered out before being reinserted.
|
||||||
|
- Impact: this can seed a local with a reference to itself, creating recursive `FrameSlotRef` chains. Reads then recurse through `BytecodeFrame.getObj()` / `FrameSlotRef.read()` instead of resolving to a value, which is a realistic path to stack overflow or non-terminating execution in bytecode-only helper execution (`executeBytecodeWithSeed`).
|
||||||
|
- Why this looks real: the equivalent check in `CmdFrame.applyCaptureRecords()` uses the local index directly (`CmdRuntime.kt:3867`), and `Script.seedModuleLocals()` also compares with the local index, not `scopeSlotCount + i` (`Script.kt:109`).
|
||||||
|
- Suggested fix: compare with `i`, not `base + i`, and add a regression test around `executeBytecodeWithSeed` seeding a scope that already contains a `FrameSlotRef` to the same local.
|
||||||
|
|
||||||
|
### 2. Design note: script-specific import/module preparation should stay explicit, not hidden in `execute(scope)`
|
||||||
|
- Status: resolved by API addition, without changing `Script.execute(scope)` semantics.
|
||||||
|
- File: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt:51-57`
|
||||||
|
- `shouldSeedModule` is only true when `execScope` is a `ModuleScope` or `thisObj === ObjVoid`.
|
||||||
|
- If a compiled script is executed against a non-module scope with a real receiver object, `seedModuleSlots()` is skipped entirely, so script imports and explicit module bindings are never installed into `moduleTarget`.
|
||||||
|
- Original concern: top-level bytecode that expects imported names or module globals can see different behavior depending on whether the host prepared the target scope explicitly.
|
||||||
|
- Resolution taken: do **not** change `Script.execute(scope)`, because in this codebase it is expected to run on exactly the provided scope and many embedding flows already rely on explicit scope setup via `Script.newScope()`, `Scope.eval(...)`, `addFn(...)`, `addConst(...)`, and direct import-manager configuration.
|
||||||
|
- New explicit APIs added instead:
|
||||||
|
- `Script.importInto(scope, seedScope = scope)`
|
||||||
|
- `Script.instantiateModule(seedScope = null, pos = script.pos)`
|
||||||
|
- This keeps existing execution behavior stable while giving hosts an opt-in way to apply a script's import/module bindings when they actually want that preparation step.
|
||||||
|
- Coverage added: `ScriptImportPreparationTest`.
|
||||||
|
|
||||||
|
### 3. Medium: missing module captures are silently converted into fresh `Unset` slots
|
||||||
|
- Status: fixed in worktree on 2026-03-27; covered by `CompilerVmReviewRegressionTest`.
|
||||||
|
- File: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/bytecode/CmdRuntime.kt:3950-3970`
|
||||||
|
- In `CmdFrame.buildCaptureRecords()`, the module-capture path first tries several lookups. If the requested `slotId` is missing but the capture has a `name`, it calls `target.applySlotPlan(mapOf(name to slotId))` and immediately returns `target.getSlotRecord(slotId)`.
|
||||||
|
- That record is a newly created placeholder from `Scope.applySlotPlan()` (`Scope.kt:460-471`) and defaults to `ObjUnset`.
|
||||||
|
- Impact: a compiler/runtime disagreement in capture resolution is masked as a normal capture of `Unset`, so the failure moves far away from closure creation and becomes data corruption or an unrelated later exception. This will be difficult to debug when it happens.
|
||||||
|
- Resolution taken: the VM now refuses to treat synthetic placeholder slots as real module/captured bindings. Reads and capture construction fail with a source-positioned `ScriptError` when the execution scope was not prepared with the script's required module/import bindings.
|
||||||
|
|
||||||
|
### 4. Medium: subject-less `when { ... }` still crashes through a raw Kotlin `TODO`
|
||||||
|
- Status: fixed in worktree on 2026-03-27; covered by `CompilerVmReviewRegressionTest`.
|
||||||
|
- File: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt:6447-6449`
|
||||||
|
- The unsupported branch uses `TODO("when without object is not yet implemented")`.
|
||||||
|
- Current docs explicitly say subject-less `when` is not implemented, so the language limitation itself is documented. The problem is the failure mode: the compiler throws a raw Kotlin `NotImplementedError` instead of a normal `ScriptError` or a feature diagnostic.
|
||||||
|
- Impact: IDE/embedding callers get an implementation exception rather than a source-positioned language error, which is especially bad across non-JVM targets and for editor tooling.
|
||||||
|
- Resolution taken: the parser now throws a normal `ScriptError` at the `when` position with an explicit "when without subject is not implemented" message.
|
||||||
|
|
||||||
|
## Risks Worth Checking Next
|
||||||
|
|
||||||
|
### 5. Medium risk: module frame growth only retargets records stored in `objects`/`localBindings`
|
||||||
|
- File: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/ModuleScope.kt:39-67`
|
||||||
|
- When `ensureModuleFrame()` replaces the old `BytecodeFrame` with a larger one, it retargets `FrameSlotRef`s only in `objects` and `localBindings`.
|
||||||
|
- Existing closures/capture records that already hold `FrameSlotRef(oldFrame, slot)` are not retargeted here.
|
||||||
|
- Impact: if a module scope survives across recompilation/re-execution with a larger local frame, previously exported closures can keep reading stale values from the old frame instance.
|
||||||
|
- Confidence is lower because this depends on module reuse across differing compiled shapes, but the reference-retargeting logic is clearly incomplete for that scenario.
|
||||||
|
- Suggested check: add a regression covering module re-execution with increased local count and an exported closure captured before the resize.
|
||||||
|
|
||||||
|
## Test Status
|
||||||
|
- `./gradlew :lynglib:jvmTest` passed during this review.
|
||||||
|
- `./gradlew :lynglib:jvmTest --tests ScriptImportPreparationTest --tests SeedLocalsRegressionTest` passed after the fixes/API additions.
|
||||||
|
- `./gradlew :lynglib:jvmTest --tests CompilerVmReviewRegressionTest --tests ScriptImportPreparationTest --tests SeedLocalsRegressionTest` passed after fixing findings 3 and 4.
|
||||||
|
- Finding 1 is covered directly; finding 2 is covered by explicit preparation API tests.
|
||||||
|
|
||||||
|
## Suggested Fix Order
|
||||||
|
1. Fix finding 1 first: it is a concrete slot-index bug with likely recursive failure modes. Done.
|
||||||
|
2. Keep `Script.execute(scope)` semantics stable and use explicit preparation APIs where script-owned import/module setup is needed. Done.
|
||||||
|
3. Tighten finding 3 next: fail fast on capture mismatches. Done.
|
||||||
|
4. Replace the raw `TODO` in finding 4 so unsupported syntax produces normal diagnostics. Done.
|
||||||
|
5. Decide whether finding 5 matters for current module-reload workflows; add a regression before changing behavior.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
WASM использует стековую модель памяти. Lyng стеки не использует. В чем разница?
|
||||||
|
|
||||||
|
Стек это очень древняя структура, была придумана в 1955 (магазинная память Бауэра и Самельсона), и приняла современную форму в 1960, благодаря Барбаре Лисокв, вполседствии лауреата премии Тьюринга, которого, в свою очередь, за выдающийся вклад в развитие IT кастрировали, формально, за гомосексуализм, но в британии, где на трех джентльменов приходилось тогда, и приходится и теперь, четыре гомосексуала, все понимают, что это был просто предлог.
|
||||||
|
|
||||||
|
Стек был прекрасен в 60е годы, когда IBM планировала продавать по компьютеру в год в лучшем случае, а за идеи о микропроцессорах или ядрах можно было устроиться только пациентом психушки. Стек это заранее жестко выделенная область памяти, которая обычно вообще не используется, и по ней в одну сторону растет используемая область, которая хранит локальные переменные, адреса возврата и иногда состояние процессора.
|
||||||
|
|
||||||
|
Она крайне плохо подходит для многозадачности. Представь себе что у тебя есть wasm процесс, со своим стеком, и ему надо переключиться, остановив текущий поток исполнения (например, он ждет ответа от сети). Тогда тебе придется создать _новый стек_ — пока еще пустой, переключиться на него и исполнять там другую задачу. Сколько потоков - столько и стеков. И все они по большей части не используются — там просто запас памяти "на вырост", если исполнение потребует. А оценить заранее адекватно почти невозможно, дают с запасом.
|
||||||
|
|
||||||
|
Далее, васм использует фиксированную память выделенную на процесс. Ее тоже приходится брать "с запасом", так как если по ходу не хватит, процесс вылетит. И она тоже в изрядной части пустая. Ну и назасладочку, васм был задуман однозадачным, и все попытки туда хотя бы треды засунуть, получаются очень кривыми.
|
||||||
|
|
||||||
|
Линг использует фреймы и сопрограммы. Линг вообще треды не использует. Основная команда исполнения программы на линге - сопрограмма (coroutine), ей не нужен стейк, она им не пользуется. Вместо этого каждый вызов в линге, грубо говоря, это вызов сопрограммы. Для него формируется фрейм (не на стеке, а в динамической памяти!), которого гарантировано достаточно для исполнения собственно кода. Он заменяет собой "стековый фрейм", но его размер известен заранее, и его можно распределять и освобождать проще, как обычную динамическую память.
|
||||||
|
|
||||||
|
Далее, линг использует ту же динамическую память, что и его родительская платформа. Если он работает на Java, то использует память со сборкой мусора JVM, на Javascript - память машины JS, в нативных машинах - специальнй менеджер памяти со сборкой мусора, от Kotlin Native (довольно хороший и очень быстро развивающийся). В результате, запустить одновременно десять, или сто тысяч программ на лигне, которые будут исполняться конкурентно, вполне реально. Просто попытка запустить 10к васм-машин на v8 или другом движке скорее всего убьет систему, да и 10 тысяч тредов редко какой сервер приложению даст. Огромный перерасход ресурсов и тормоза.
|
||||||
|
|
||||||
|
Далее. Васм СТЕКОВАЯ машина со стековыми командами, он без стека вообще не может. Посмотрим как он считает примитивное выражение:
|
||||||
|
|
||||||
|
```wasm
|
||||||
|
(module
|
||||||
|
;; Экспортируем функцию, чтобы её можно было вызвать из JS
|
||||||
|
(func (export "calc") (param $a i32) (param $b i32) (param $c i32) (param $d i32) (result i32)
|
||||||
|
;; --- вычисление (a + b) / c * d ---
|
||||||
|
local.get $a ;; помещаем a на стек
|
||||||
|
local.get $b ;; помещаем b на стек
|
||||||
|
i32.add ;; складываем: стек содержит (a+b)
|
||||||
|
|
||||||
|
local.get $c ;; помещаем c
|
||||||
|
i32.div_s ;; знаковое деление: стек содержит ((a+b) / c)
|
||||||
|
|
||||||
|
local.get $d ;; помещаем d
|
||||||
|
i32.mul ;; умножение: стек содержит (((a+b)/c) * d)
|
||||||
|
|
||||||
|
;; результат остаётся на стеке — это возвращаемое значение функции
|
||||||
|
)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
На ВМ Линга нет стека, он трехадресный универсальный ассемблер, если можно так сказать:
|
||||||
|
|
||||||
|
```
|
||||||
|
add_int s1, s2 -> s5 // a + b -> s5
|
||||||
|
div_int s5, s3 -> s5 // s5 -> s5 / c
|
||||||
|
mul_int s5, s4 -> s5 // s5 -> s5 * d
|
||||||
|
return s5
|
||||||
|
```
|
||||||
|
|
||||||
|
Этот код крайне эффективно реализуется на любом реальном процессоре, и хорошо оптимизируется. Исполняется он без всякого стека.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
Почему не WASM?
|
||||||
|
|
||||||
|
- wasm очень громоздкий и неудобный для разработки, он проектировался очень давно и с другими целями, так что сейчас его безуспешно пытаются "перезаточить" в современные области, но он поддается с трудом. Компилировать в wasm была бы задача по сложности превосходящая весь проект, а результат был бы уныл: мы и так имеем все достоинсва wasm, полезные для Lyng, но не имеем многих его недостатков.
|
||||||
|
|
||||||
|
Виртуалка lyng легкая, и доступна на всех платформах уже сейчас, а вот прикрутить скажем wasm машину к котлину на сервере, чтобы она работала как сопрограммы первого класса вместе с котлинскими, задача сейчас неразрешимая.
|
||||||
|
|
||||||
|
То есть, преимущества wasm что он доступен на многих платформах, для нас неважно - мы и так на тех же платформах имеем полную поддержку.
|
||||||
|
|
||||||
|
- Lyng VM изначально заточена на исполнение именно сопрограмм со сборкой мусора. Эти два ключевых механизма линга практически отсутствуют в wasm, их мучительно прикручивают как расширения, и они довольно слабо поддерживаются.
|
||||||
|
|
||||||
|
- LyngVM заточен на исполнение программ с гибридной ООП-ФП моделью, она хорошо поддерживает множественное наследование, делегацию, работу со сложными списками аргументов (ФП фичи, подстановки, деструктурирование), на васме это очень непросто написать, будет медленно и громоздко.
|
||||||
|
|
||||||
|
- одна из целей Lyng была получить быстрое безопасное и очень мощное скриптовое решение для вклучения в проекты на Java/Kotlin multiplatform. На wasm это в принципе невозможно - его включение это кошмар, он громоздкий. К тому же требует компиляции, а Lyng работает с JIT (исполняет прямо исходник, компилируя его на лету, как JS), и может использоваться как скрипт (wasm не может)
|
||||||
|
|
||||||
|
- типы данных которые мы используем в Lyng в WASM отсутстсвтуют, их бы пришлось добавлять
|
||||||
|
|
||||||
|
В результате если бы мы компилировали Lyng->WASM мы бы получили огромный и медленный код. Для которого потребовалась бы огромная wasm VM. Практического смысла в этом нет.
|
||||||
|
|
||||||
|
Если же ты рассматриваешь идею в единой платформе использовать разные языки, а wasm как промежуточный язых совместимости, то это собственно противоречит идее использовать общую кодовую базу и интерфейсы. Разные языки имеют несовпадающие, несовместимые интерфейсы вызовов, так что приходится писать руками биндинги, которые не добавляют скорости и добавляют ошибки, к тому же, разные языки предоставляют часто вообще несовместимые модели программирования (например прототипы в JS, множественное наследование С++ или Линга и кошмар на улице вязов от руста, в владением ссылками).
|
||||||
|
|
||||||
|
Платформ на васме полно, и там и ловить нечего, и неинтересно. Я же предлагаю вылезти в другой класс. Навеяно реальным опоытом переносов контрактов из старого золота в мольтпей, которое получается на удивление хорошо
|
||||||
@@ -27,74 +27,85 @@ import org.jetbrains.compose.web.dom.*
|
|||||||
fun HomePage() {
|
fun HomePage() {
|
||||||
val samples = remember {
|
val samples = remember {
|
||||||
listOf(
|
listOf(
|
||||||
|
"""
|
||||||
|
// Decimal arithmetic with explicit local precision
|
||||||
|
import lyng.decimal
|
||||||
|
|
||||||
|
val exact = "0.3".d
|
||||||
|
val fromReal = (0.1 + 0.2).d
|
||||||
|
|
||||||
|
println(exact.toStringExpanded())
|
||||||
|
println(fromReal.toStringExpanded())
|
||||||
|
println(withDecimalContext(10) { (1.d / 3.d).toStringExpanded() })
|
||||||
|
""".trimIndent(),
|
||||||
|
"""
|
||||||
|
// Mixed operators for your own type
|
||||||
|
import lyng.operators
|
||||||
|
|
||||||
|
class Coins(val amount: Int) {
|
||||||
|
fun plus(other: Coins) = Coins(amount + other.amount)
|
||||||
|
fun compareTo(other: Coins) = amount <=> other.amount
|
||||||
|
}
|
||||||
|
|
||||||
|
OperatorInterop.register(
|
||||||
|
Int,
|
||||||
|
Coins,
|
||||||
|
Coins,
|
||||||
|
[BinaryOperator.Plus, BinaryOperator.Compare, BinaryOperator.Equals],
|
||||||
|
{ x: Int -> Coins(x) },
|
||||||
|
{ x: Coins -> x }
|
||||||
|
)
|
||||||
|
|
||||||
|
println(1 + Coins(2))
|
||||||
|
println(3 > Coins(2))
|
||||||
|
""".trimIndent(),
|
||||||
|
"""
|
||||||
|
// Non-local returns from closures
|
||||||
|
fun findFirst<T>(list: Iterable<T>, predicate: (T)->Bool): T? {
|
||||||
|
list.forEach {
|
||||||
|
if (predicate(it)) return@findFirst it
|
||||||
|
}
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val found: Int? = findFirst([1, 5, 8, 12]) { it > 10 }
|
||||||
|
println("Found: " + found)
|
||||||
|
""".trimIndent(),
|
||||||
|
"""
|
||||||
|
// Implicit coroutines: parallelism without ceremony
|
||||||
|
import lyng.time
|
||||||
|
|
||||||
|
val d1 = launch {
|
||||||
|
delay(100.milliseconds)
|
||||||
|
"Task A finished"
|
||||||
|
}
|
||||||
|
val d2 = launch {
|
||||||
|
delay(50.milliseconds)
|
||||||
|
"Task B finished"
|
||||||
|
}
|
||||||
|
|
||||||
|
println(d1.await())
|
||||||
|
println(d2.await())
|
||||||
|
""".trimIndent(),
|
||||||
"""
|
"""
|
||||||
// Everything is an expression
|
// Everything is an expression
|
||||||
val x: Int = 10
|
val x: Int = 10
|
||||||
val status: String = if (x > 0) "Positive" else "Zero or Negative"
|
val status: String = if (x > 0) "Positive" else "Zero or Negative"
|
||||||
|
|
||||||
// Even loops return values!
|
|
||||||
val result = for (i in 1..5) {
|
val result = for (i in 1..5) {
|
||||||
if (i == 3) break "Found 3!"
|
if (i == 3) break "Found 3!"
|
||||||
} else "Not found"
|
} else "Not found"
|
||||||
|
|
||||||
println("Result: " + result)
|
println(status)
|
||||||
|
println(result)
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
"""
|
"""
|
||||||
// Functional power with generics and collections
|
// Functional collections with strict static types
|
||||||
val squares: List<Int> = (1..10)
|
val squares: List<Int> = (1..10)
|
||||||
.filter { it % 2 == 0 }
|
.filter { it % 2 == 0 }
|
||||||
.map { it * it }
|
.map { it * it }
|
||||||
|
|
||||||
println("Even squares: " + squares)
|
println("Even squares: " + squares)
|
||||||
// Output: [4, 16, 36, 64, 100]
|
|
||||||
""".trimIndent(),
|
|
||||||
"""
|
|
||||||
// Generics and type aliases
|
|
||||||
type Num = Int | Real
|
|
||||||
|
|
||||||
class Box<out T: Num>(val value: T) {
|
|
||||||
fun get(): T = value
|
|
||||||
}
|
|
||||||
|
|
||||||
val intBox = Box(42)
|
|
||||||
val realBox = Box(3.14)
|
|
||||||
println("Boxes: " + intBox.get() + ", " + realBox.get())
|
|
||||||
""".trimIndent(),
|
|
||||||
"""
|
|
||||||
// Strict compile-time types and symbol resolution
|
|
||||||
fun greet(name: String, count: Int) {
|
|
||||||
for (i in 1..count) {
|
|
||||||
println("Hello, " + name + "!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
greet("Lyng", 3)
|
|
||||||
// greet(10, "error") // This would be a compile-time error!
|
|
||||||
""".trimIndent(),
|
|
||||||
"""
|
|
||||||
// Flexible map literals and shorthands
|
|
||||||
val id = 101
|
|
||||||
val name = "Lyng"
|
|
||||||
val base = { id:, name: } // Shorthand for id: id, name: name
|
|
||||||
|
|
||||||
val full = { ...base, version: "1.5.0-SNAPSHOT", status: "active" }
|
|
||||||
println(full)
|
|
||||||
""".trimIndent(),
|
|
||||||
"""
|
|
||||||
// Modern null safety
|
|
||||||
var config: Map<String, Int>? = null
|
|
||||||
config ?= { timeout: 30 } // Assign only if null
|
|
||||||
|
|
||||||
val timeout = config?["timeout"] ?: 60
|
|
||||||
println("Timeout is: " + timeout)
|
|
||||||
""".trimIndent(),
|
|
||||||
"""
|
|
||||||
// Destructuring with splat operator
|
|
||||||
val [first, middle..., last] = [1, 2, 3, 4, 5, 6]
|
|
||||||
|
|
||||||
println("First: " + first)
|
|
||||||
println("Middle: " + middle)
|
|
||||||
println("Last: " + last)
|
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
"""
|
"""
|
||||||
// Diamond-safe Multiple Inheritance (C3 MRO)
|
// Diamond-safe Multiple Inheritance (C3 MRO)
|
||||||
@@ -123,18 +134,6 @@ fun HomePage() {
|
|||||||
println([10, 20, 30].second)
|
println([10, 20, 30].second)
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
"""
|
"""
|
||||||
// Non-local returns from closures
|
|
||||||
fun findFirst<T>(list: Iterable<T>, predicate: (T)->Bool): T? {
|
|
||||||
list.forEach {
|
|
||||||
if (predicate(it)) return@findFirst it
|
|
||||||
}
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
val found: Int? = findFirst([1, 5, 8, 12]) { it > 10 }
|
|
||||||
println("Found: " + found)
|
|
||||||
""".trimIndent(),
|
|
||||||
"""
|
|
||||||
// Easy operator overloading
|
// Easy operator overloading
|
||||||
class Vector(val x: Real, val y: Real) {
|
class Vector(val x: Real, val y: Real) {
|
||||||
fun plus(other: Vector): Vector = Vector(x + other.x, y + other.y)
|
fun plus(other: Vector): Vector = Vector(x + other.x, y + other.y)
|
||||||
@@ -156,20 +155,13 @@ fun HomePage() {
|
|||||||
println("User name: " + u.name)
|
println("User name: " + u.name)
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
"""
|
"""
|
||||||
// Implicit coroutines: parallelism without ceremony
|
// Flexible map literals and shorthands
|
||||||
import lyng.time
|
val id = 101
|
||||||
|
val name = "Lyng"
|
||||||
|
val base = { id:, name: }
|
||||||
|
val full = { ...base, status: "active", tags: ["typed", "portable"] }
|
||||||
|
|
||||||
val d1 = launch {
|
println(full)
|
||||||
delay(100.milliseconds)
|
|
||||||
"Task A finished"
|
|
||||||
}
|
|
||||||
val d2 = launch {
|
|
||||||
delay(50.milliseconds)
|
|
||||||
"Task B finished"
|
|
||||||
}
|
|
||||||
|
|
||||||
println(d1.await())
|
|
||||||
println(d2.await())
|
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -195,6 +187,8 @@ fun HomePage() {
|
|||||||
Div({ classes("d-flex", "justify-content-center", "gap-2", "flex-wrap", "mb-4") }) {
|
Div({ classes("d-flex", "justify-content-center", "gap-2", "flex-wrap", "mb-4") }) {
|
||||||
// Benefits pills
|
// Benefits pills
|
||||||
listOf(
|
listOf(
|
||||||
|
"Decimal arithmetic",
|
||||||
|
"Operator interop",
|
||||||
"Strict static typing",
|
"Strict static typing",
|
||||||
"Generics & Type Aliases",
|
"Generics & Type Aliases",
|
||||||
"Implicit coroutines",
|
"Implicit coroutines",
|
||||||
@@ -233,6 +227,26 @@ fun HomePage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Div({ classes("row", "g-3", "mb-4") }) {
|
||||||
|
listOf(
|
||||||
|
Triple("New: Decimal", "Exact decimal values, local precision control, and mixed operators with Int and Real.", "#/docs/Decimal.md"),
|
||||||
|
Triple("New: Operator Interop", "Teach Lyng how your custom types interact with built-ins on the left-hand side.", "#/docs/OperatorInterop.md"),
|
||||||
|
Triple("What Changed", "Recent language, stdlib, and tooling improvements in one place.", "#/docs/whats_new.md")
|
||||||
|
).forEach { (title, text, href) ->
|
||||||
|
Div({ classes("col-12", "col-lg-4") }) {
|
||||||
|
A(attrs = {
|
||||||
|
classes("text-decoration-none")
|
||||||
|
attr("href", href)
|
||||||
|
}) {
|
||||||
|
Div({ classes("h-100", "p-3", "border", "rounded-3", "bg-body-tertiary") }) {
|
||||||
|
H3({ classes("h5", "mb-2", "text-body") }) { Text(title) }
|
||||||
|
P({ classes("mb-0", "text-muted") }) { Text(text) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Code sample slideshow
|
// Code sample slideshow
|
||||||
Div({
|
Div({
|
||||||
classes("markdown-body", "mt-0", "slide-container", "position-relative")
|
classes("markdown-body", "mt-0", "slide-container", "position-relative")
|
||||||
|
|||||||
@@ -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 |
@@ -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.
|
||||||
Reference in new issue
Block a user