Compare commits

...
78 changed files with 5418 additions and 3063 deletions

No files matched your search

+6 -1
View File
@@ -2,6 +2,11 @@
### Unreleased
- Language: Refined `protected` visibility rules
- Ancestor classes can now access `protected` members of their descendants, provided the ancestor also defines or inherits a member with the same name (indicating an override of a member known to the ancestor).
- This allows patterns where a base class calls a `protected` method that is implemented in a subclass.
- Fixed a regression where self-calls to unmangled members sometimes bypassed visibility checks incorrectly; these are now handled by the refined logic.
- Language: Added `return` statement
- `return [expression]` exits the innermost enclosing callable (function or lambda).
- Supports non-local returns using `@label` syntax (e.g., `return@outer 42`).
@@ -96,7 +101,7 @@
- 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 any of its transitive subclasses; unrelated contexts cannot access it (qualification/casts do not bypass).
- `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.
+3 -2
View File
@@ -14,6 +14,7 @@ High-density specification for LLMs. Reference this for all Lyng code generation
4. `void` (if loop body never executed and no `else`).
- **Implicit Coroutines**: All functions are coroutines. No `async/await`. Use `launch { ... }` (returns `Deferred`) or `flow { ... }`.
- **Variables**: `val` (read-only), `var` (mutable). Supports late-init `val` in classes (must be assigned in `init` or body).
- **Serialization**: Use `@Transient` attribute before `val`/`var` or constructor parameters to exclude them from Lynon/JSON serialization. Transient fields are also ignored during `==` structural equality checks.
- **Null Safety**: `?` (nullable type), `?.` (safe access), `?( )` (safe invoke), `?{ }` (safe block invoke), `?[ ]` (safe index), `?:` or `??` (elvis), `?=` (assign-if-null).
- **Equality**: `==` (equals), `!=` (not equals), `===` (ref identity), `!==` (ref not identity).
- **Comparison**: `<`, `>`, `<=`, `>=`, `<=>` (shuttle/spaceship, returns -1, 0, 1).
@@ -32,7 +33,7 @@ High-density specification for LLMs. Reference this for all Lyng code generation
val area get = π * r * r
```
- **Mandatory `override`**: Required for all members existing in the ancestor chain.
- **Visibility**: `public` (default), `protected` (subclasses), `private` (this class instance only). `private set` / `protected set` allowed on properties.
- **Visibility**: `public` (default), `protected` (subclasses and ancestors for overrides), `private` (this class instance only). `private set` / `protected set` allowed on properties.
- **Disambiguation**: `this@Base.member()` or `(obj as Base).member()`. `as` returns a qualified view.
- **Abstract/Interface**: `interface` is a synonym for `abstract class`. Both support state and constructors.
- **Extensions**: `fun Class.ext()` or `val Class.ext get = ...`. Scope-isolated.
@@ -93,7 +94,7 @@ m += "c" => 3
val [first, middle..., last] = [1, 2, 3, 4, 5]
// Safe Navigation and Elvis
val companyName = person?.job?.company?.name ?? "Freelancer"
val companyName = person?.job?.company?.name ?: "Freelancer"
```
## 8. Standard Library Discovery
+32 -11
View File
@@ -425,7 +425,7 @@ Key rules and features:
- Visibility
- `private`: accessible only inside the declaring class body; not visible in subclasses and cannot be accessed via `this@Type` or casts.
- `protected`: accessible in the declaring class and in any of its transitive subclasses (including MI), but not from unrelated contexts; qualification/casts do not bypass it.
- `protected`: accessible in the declaring class and in any of its transitive subclasses (including MI). Additionally, ancestor classes can access protected members of their descendants if it's an override of a member known to the ancestor. Protected members are not visible from unrelated contexts; qualification/casts do not bypass it.
## Abstract Classes and Members
@@ -857,23 +857,44 @@ Private fields are visible only _inside the class instance_:
void
>>> void
### Transient fields
You can mark a field or a constructor parameter as transient using the `@Transient` attribute. Transient members are ignored during serialization (Lynon and JSON) and are also excluded from structural equality (`==`) checks.
```lyng
class Session(@Transient val token, val userId) {
@Transient var lastAccess = time.now()
var data = Map()
}
```
For more details on how transient fields behave during restoration, see the [Serialization Guide](serialization.md).
### Protected members
Protected members are available to the declaring class and all of its transitive subclasses (including via MI), but not from unrelated contexts:
Protected members are available to the declaring class and all of its transitive subclasses (including via MI). Additionally, an ancestor class can access a `protected` member of its descendant if the ancestor also defines or inherits a member with the same name (i.e., it is an override of something the ancestor knows about).
```
class A() {
protected fun ping() { "pong" }
}
class B() : A() {
fun call() { this@A.ping() }
Protected members are not available from unrelated contexts:
```lyng
class Base {
abstract protected fun foo()
fun bar() {
// Ancestor can see foo() because it's an override
// of a member it defines (even as abstract):
foo()
}
}
val b = B()
assertEquals("pong", b.call())
class Derived : Base {
override protected fun foo() { "ok" }
}
assertEquals("ok", Derived().bar())
// Unrelated access is forbidden, even via cast
assertThrows { (b as A).ping() }
assertThrows { (Derived() as Base).foo() }
```
It is possible to provide private constructor parameters so they can be
+4 -3
View File
@@ -98,10 +98,11 @@ Exclusive end char ranges are supported too:
| isEndInclusive | true for '..' | Bool |
| isOpen | at any end | Bool |
| isIntRange | both start and end are Int | Bool |
| start | | Bool |
| end | | Bool |
| start | | Any? |
| end | | Any? |
| size | for finite ranges, see above | Long |
| [] | see above | |
| | | |
Ranges are also used with the `clamp(value, range)` function and the `value.clamp(range)` extension method to limit values within boundaries.
[Iterable]: Iterable.md
+1
View File
@@ -19,6 +19,7 @@ you can use it's class to ensure type:
|-----------------|-------------------------------------------------------------|------|
| `.roundToInt()` | round to nearest int like round(x) | Int |
| `.toInt()` | convert integer part of real to `Int` dropping decimal part | Int |
| `.clamp(range)` | clamp value within range boundaries | Real |
| | | |
| | | |
| | | |
+12 -1
View File
@@ -20,7 +20,18 @@ Simple classes serialization is supported:
assertEquals( "{\"foo\":1,\"bar\":2}", Point(1,2).toJsonString() )
>>> void
Note that mutable members are serialized:
Note that mutable members are serialized by default. You can exclude any member (including constructor parameters) from JSON serialization using the `@Transient` attribute:
import lyng.serialization
class Point2(@Transient val foo, val bar) {
@Transient var reason = 42
var visible = 100
}
assertEquals( "{\"bar\":2,\"visible\":100}", Point2(1,2).toJsonString() )
>>> void
Note that if you override json serialization:
import lyng.serialization
+6
View File
@@ -92,6 +92,7 @@ or transformed `Real` otherwise.
| pow(x, y) | ${x^y}$ |
| sqrt(x) | $ \sqrt {x}$ |
| abs(x) | absolute value of x. Int if x is Int, Real otherwise |
| clamp(x, range) | limit x to be inside range boundaries |
For example:
@@ -102,6 +103,11 @@ For example:
// abs() keeps the argument type:
assert( abs(-1) is Int)
assert( abs(-2.21) == 2.21 )
// clamp() limits value to the range:
assert( clamp(15, 0..10) == 10 )
assert( clamp(-5, 0..10) == 0 )
assert( 5.clamp(0..10) == 5 )
>>> void
## Scientific constant
-1
View File
@@ -1,7 +1,6 @@
#!/bin/env lyng
import lyng.io.fs
import lyng.stdlib
val files = Path("../..").list().toList()
// most long is longest?
+25 -8
View File
@@ -20,20 +20,37 @@ It is as simple as:
assert( text.length > encodedBits.toBuffer().size )
>>> void
Any class you create is serializable by default; lynon serializes first constructor fields, then any `var` member fields:
Any class you create is serializable by default; lynon serializes first constructor fields, then any `var` member fields.
import lyng.serialization
## Transient Fields
class Point(x,y)
Sometimes you have fields that should not be serialized, for example, temporary caches, secret data, or derived values that are recomputed in `init` blocks. You can mark such fields with the `@Transient` attribute:
val p = Lynon.decode( Lynon.encode( Point(5,6) ) )
```lyng
class MyData(@Transient val tempSecret, val publicData) {
@Transient var cachedValue = 0
var persistentValue = 42
assertEquals( 5, p.x )
assertEquals( 6, p.y )
>>> void
init {
// cachedValue can be recomputed here upon deserialization
cachedValue = computeCache(publicData)
}
}
```
Transient fields:
- Are **omitted** from Lynon binary streams.
- Are **omitted** from JSON output (via `toJson`).
- Are **ignored** during structural equality checks (`==`).
- If a transient constructor parameter has a **default value**, it will be restored to that default value during deserialization. Otherwise, it will be `null`.
- Class body fields marked as `@Transient` will keep their initial values (or values assigned in `init`) after deserialization.
just as expected.
## Serialization of Objects and Classes
- **Singleton Objects**: `object` declarations are serializable by name. Their state (mutable fields) is also serialized and restored, respecting `@Transient`.
- **Classes**: Class objects themselves can be serialized. They are serialized by their full qualified name. When converted to JSON, a class object includes its public static fields (excluding those marked `@Transient`).
## Custom Serialization
Important is to understand that normally `Lynon.decode` wants [BitBuffer], as `Lynon.encode` produces. If you have the regular [Buffer], be sure to convert it:
+75 -136
View File
@@ -2,166 +2,105 @@
Lyng date and time support requires importing `lyng.time` packages. Lyng uses simple yet modern time object models:
- `Instant` class for time stamps with platform-dependent resolution
- `Duration` to represent amount of time not depending on the calendar, e.g. in absolute units (milliseconds, seconds,
hours, days)
- `Instant` class for absolute time stamps with platform-dependent resolution.
- `DateTime` class for calendar-aware points in time within a specific time zone.
- `Duration` to represent amount of time not depending on the calendar (e.g., milliseconds, seconds).
## Time instant: `Instant`
Represent some moment of time not depending on the calendar (calendar for example may b e changed, daylight saving time
can be for example introduced or dropped). It is similar to `TIMESTAMP` in SQL or `Instant` in Kotlin. Some moment of
time; not the calendar date.
Represent some moment of time not depending on the calendar. It is similar to `TIMESTAMP` in SQL or `Instant` in Kotlin.
Instant is comparable to other Instant. Subtracting instants produce `Duration`, period in time that is not dependent on
the calendar, e.g. absolute time period.
It is possible to add or subtract `Duration` to and from `Instant`, that gives another `Instant`.
Instants are converted to and from `Real` number of seconds before or after Unix Epoch, 01.01.1970. Constructor with
single number parameter constructs from such number of seconds,
and any instance provide `.epochSeconds` member:
### Constructing and converting
import lyng.time
// default constructor returns time now:
val t1 = Instant()
val t2 = Instant()
assert( t2 - t1 < 1.millisecond )
assert( t2.epochSeconds - t1.epochSeconds < 0.001 )
>>> void
## Constructing
import lyng.time
// empty constructor gives current time instant using system clock:
val now = Instant()
// constructor with Instant instance makes a copy:
assertEquals( now, Instant(now) )
// constructing from a number is trated as seconds since unix epoch:
val copyOfNow = Instant( now.epochSeconds )
// note that instant resolution is higher that Real can hold
// so reconstructed from real slightly differs:
assert( abs( (copyOfNow - now).milliseconds ) < 0.01 )
>>> void
The resolution of system clock could be more precise and double precision real number of `Real`, keep it in mind.
## Comparing and calculating periods
import lyng.time
val now = Instant()
// constructing from a number is treated as seconds since unix epoch:
val t2 = Instant(1704110400) // 2024-01-01T12:00:00Z
// you cam add or subtract periods, and compare
assert( now - 5.minutes < now )
val oneHourAgo = now - 1.hour
assertEquals( now, oneHourAgo + 1.hour)
// from RFC3339 string:
val t3 = Instant("2024-01-01T12:00:00.123456Z")
// truncation:
val t4 = t3.truncateToMinute
assertEquals(t4.toRFC3339(), "2024-01-01T12:00:00Z")
// to localized DateTime (uses system default TZ if not specified):
val dt = t3.toDateTime("+02:00")
assertEquals(dt.hour, 14)
>>> void
## Getting the max precision
Normally, subtracting instants gives precision to microseconds, which is well inside the jitter
the language VM adds. Still `Instant()` or `Instant.now()` capture most precise system timer at hand and provide inner
value of 12 bytes, up to nanoseconds (hopefully). To access it use:
import lyng.time
// capture time
val now = Instant.now()
// this is Int value, number of whole epoch
// milliseconds to the moment, it fits 8 bytes Int well
val seconds = now.epochWholeSeconds
assert(seconds is Int)
// and this is Int value of nanoseconds _since_ the epochMillis,
// it effectively add 4 more mytes int:
val nanos = now.nanosecondsOfSecond
assert(nanos is Int)
assert( nanos in 0..999_999_999 )
// we can construct epochSeconds from these parts:
assertEquals( now.epochSeconds, nanos * 1e-9 + seconds )
>>> void
## Truncating to more realistic precision
Full precision Instant is way too long and impractical to store, especially when serializing,
so it is possible to truncate it to milliseconds, microseconds or seconds:
import lyng.time
import lyng.serialization
// max supported size (now microseconds for serialized value):
// note that encoding return _bit array_ and this is a _bit size_:
val s0 = Lynon.encode(Instant.now()).size
// shorter: milliseconds only
val s1 = Lynon.encode(Instant.now().truncateToMillisecond()).size
// truncated to seconds, good for file mtime, etc:
val s2 = Lynon.encode(Instant.now().truncateToSecond()).size
assert( s1 < s0 )
assert( s2 < s1 )
>>> void
## Formatting instants
You can freely use `Instant` in string formatting. It supports usual sprintf-style formats:
import lyng.time
val now = Instant()
// will be something like "now: 12:10:05"
val currentTimeOnly24 = "now: %tT"(now)
// we can extract epoch second with formatting too,
// this was since early C time
// get epoch while seconds from formatting
val unixEpoch = "Now is %ts since unix epoch"(now)
// and it is the same as now.epochSeconds, int part:
assertEquals( unixEpoch, "Now is %d since unix epoch"(now.epochSeconds.toInt()) )
>>> void
See
the [complete list of available formats](https://github.com/sergeych/mp_stools?tab=readme-ov-file#datetime-formatting)
and the [formatting reference](https://github.com/sergeych/mp_stools?tab=readme-ov-file#printf--sprintf): it all works
in Lyng as `"format"(args...)`!
## Instant members
### Instant members
| member | description |
|--------------------------------|---------------------------------------------------------|
| epochSeconds: Real | positive or negative offset in seconds since Unix epoch |
| epochWholeSeconds: Int | same, but in _whole seconds_. Slightly faster |
| nanosecondsOfSecond: Int | offset from epochWholeSeconds in nanos (1) |
| nanosecondsOfSecond: Int | offset from epochWholeSeconds in nanos |
| isDistantFuture: Bool | true if it `Instant.distantFuture` |
| isDistantPast: Bool | true if it `Instant.distantPast` |
| truncateToSecond: Intant | create new instnce truncated to second |
| truncateToMillisecond: Instant | truncate new instance with to millisecond |
| truncateToMinute: Instant | create new instance truncated to minute |
| truncateToSecond: Instant | create new instance truncated to second |
| truncateToMillisecond: Instant | truncate new instance to millisecond |
| truncateToMicrosecond: Instant | truncate new instance to microsecond |
| toRFC3339(): String | format as RFC3339 string (UTC) |
| toDateTime(tz?): DateTime | localize to a TimeZone (ID string or offset seconds) |
(1)
: The value of nanoseconds is to be added to `epochWholeSeconds` to get exact time point. It is in 0..999_999_999 range.
The precise time instant value therefore needs as for now 12 bytes integer; we might use bigint later (it is planned to
be added)
## Calendar time: `DateTime`
## Class members
`DateTime` represents a point in time in a specific timezone. It provides access to calendar components like year,
month, and day.
| member | description |
|--------------------------------|----------------------------------------------|
| Instant.now() | create new instance with current system time |
| Instant.distantPast: Instant | most distant instant in past |
| Instant.distantFuture: Instant | most distant instant in future |
### Constructing
# `Duraion` class
import lyng.time
// Current time in system default timezone
val now = DateTime.now()
// Specific timezone
val offsetTime = DateTime.now("+02:00")
// From Instant
val dt = Instant().toDateTime("Z")
// By components (year, month, day, hour=0, minute=0, second=0, timeZone="UTC")
val dt2 = DateTime(2024, 1, 1, 12, 0, 0, "Z")
// From RFC3339 string
val dt3 = DateTime.parseRFC3339("2024-01-01T12:00:00+02:00")
### DateTime members
| member | description |
|----------------------------------|-----------------------------------------------|
| year: Int | year component |
| month: Int | month component (1..12) |
| day: Int | day of month (alias `dayOfMonth`) |
| hour: Int | hour component (0..23) |
| minute: Int | minute component (0..59) |
| second: Int | second component (0..59) |
| dayOfWeek: Int | day of week (1=Monday, 7=Sunday) |
| timeZone: String | timezone ID string |
| toInstant(): Instant | convert back to absolute Instant |
| toUTC(): DateTime | shortcut to convert to UTC |
| toTimeZone(tz): DateTime | convert to another timezone |
| addMonths(n): DateTime | add/subtract months (normalizes end of month) |
| addYears(n): DateTime | add/subtract years |
| toRFC3339(): String | format with timezone offset |
| static now(tz?): DateTime | create DateTime with current time |
| static parseRFC3339(s): DateTime | parse RFC3339 string |
### Arithmetic and normalization
`DateTime` handles calendar arithmetic correctly:
val leapDay = Instant("2024-02-29T12:00:00Z").toDateTime("Z")
val nextYear = leapDay.addYears(1)
assertEquals(nextYear.day, 28) // Feb 29, 2024 -> Feb 28, 2025
# `Duration` class
Represent absolute time distance between two `Instant`.
+4 -1
View File
@@ -1484,6 +1484,9 @@ A typical set of String functions includes:
| lower(), lowercase() | change case to unicode upper |
| upper(), uppercase() | change case to unicode lower |
| trim() | trim space chars from both ends |
| isEmpty() | true if string is empty |
| isNotEmpty() | true if string is not empty |
| isBlank() | true if empty or contains only whitespace |
| startsWith(prefix) | true if starts with a prefix |
| endsWith(prefix) | true if ends with a prefix |
| last() | get last character of a string or throw |
@@ -1701,7 +1704,7 @@ assertEquals(null, (buzz as? Foo)?.runA())
Notes:
- Resolution order uses C3 MRO (active): deterministic, monotonic order suitable for diamonds and complex hierarchies. Example: for `class D() : B(), C()` where both `B()` and `C()` derive from `A()`, the C3 order is `D → B → C → A`. The first visible match wins.
- `private` is visible only inside the declaring class; `protected` is visible from the declaring class and any of its transitive subclasses. Qualialsofication (`this@Type`) or casts do not bypass visibility.
- `private` is visible only inside the declaring class; `protected` is visible from the declaring class and its subclasses. Additionally, ancestors can access protected members of descendants if they override a member known to the ancestor. Qualification (`this@Type`) or casts do not bypass visibility.
- Safe‑call `?.` works with `as?` for optional dispatch.
## Extension members
+38
View File
@@ -24,6 +24,9 @@ class Person(private var _age: Int) {
### Private and Protected Setters
You can now restrict the visibility of a property's or field's setter using `private set` or `protected set`. This allows members to be publicly readable but only writable from within the declaring class or its subclasses.
### Refined Protected Visibility
Ancestor classes can now access `protected` members of their descendants if it is an override of a member known to the ancestor. This enables base classes to call protected methods that are implemented or overridden in subclasses.
```lyng
class Counter {
var count = 0
@@ -168,6 +171,41 @@ settings["theme"] ?= "dark"
The operator returns the final value of the receiver (the original value if it was not `null`, or the new value if the assignment occurred).
### Transient Attribute (`@Transient`)
The `@Transient` attribute can now be applied to class fields, constructor parameters, and static fields to exclude them from serialization.
```lyng
class MyData(@Transient val tempSecret, val publicData) {
@Transient var cachedValue = 0
var persistentValue = 42
}
```
Key features:
- **Serialization**: Transient members are omitted from both Lynon binary streams and JSON output.
- **Structural Equality**: Transient fields are automatically ignored during `==` equality checks.
- **Deserialization**: Transient constructor parameters with default values are correctly restored to those defaults upon restoration.
### Value Clamping (`clamp`)
A new `clamp()` function has been added to the standard library to limit a value within a specified range. It is available as both a global function and an extension method on all objects.
```lyng
// Global function
clamp(15, 0..10) // returns 10
clamp(-5, 0..10) // returns 0
// Extension method
val x = 15
x.clamp(0..10) // returns 10
// Exclusive and open-ended ranges
15.clamp(0..<10) // returns 9
15.clamp(..10) // returns 10
-5.clamp(0..) // returns 0
```
`clamp()` correctly handles inclusive (`..`) and exclusive (`..<`) ranges. For discrete types like `Int` and `Char`, clamping to an exclusive upper bound returns the previous value.
## Tooling and Infrastructure
### CLI: Formatting Command
+2 -2
View File
@@ -1,5 +1,5 @@
#
# Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
# 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.
@@ -16,7 +16,7 @@
#
#Gradle
org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M"
org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M"
org.gradle.caching=true
org.gradle.configuration-cache=true
#Kotlin
+2
View File
@@ -5,6 +5,7 @@ kotlin = "2.3.0"
android-minSdk = "24"
android-compileSdk = "34"
kotlinx-coroutines = "1.10.2"
kotlinx-datetime = "0.6.1"
mp_bintools = "0.3.2"
firebaseCrashlyticsBuildtools = "3.0.3"
okioVersion = "3.10.2"
@@ -16,6 +17,7 @@ clikt-markdown = { module = "com.github.ajalt.clikt:clikt-markdown", version.ref
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", 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" }
mp_bintools = { module = "net.sergeych:mp_bintools", version.ref = "mp_bintools" }
firebase-crashlytics-buildtools = { group = "com.google.firebase", name = "firebase-crashlytics-buildtools", version.ref = "firebaseCrashlyticsBuildtools" }
okio = { module = "com.squareup.okio:okio", version.ref = "okioVersion" }
@@ -139,7 +139,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
val e: Int = miniSource.offsetOf(d.range.end)
if (offset >= s && offset <= e) {
// For enum constant, we don't have detailed docs in MiniAst yet, but we can render a title
return "<div class='doc-title'>enum constant ${d.name}.${name}</div>"
return renderTitle("enum constant ${d.name}.${name}")
}
}
}
@@ -361,7 +361,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
"Print values to the standard output and append a newline. Accepts any number of arguments." else
"Print values to the standard output without a trailing newline. Accepts any number of arguments."
val title = "function $ident(values)"
return "<div class='doc-title'>${htmlEscape(title)}</div>" + styledMarkdown(htmlEscape(fallback))
return renderTitle(title) + styledMarkdown(htmlEscape(fallback))
}
// 4b) try class members like ClassName.member with inheritance fallback
val lhs = previousWordBefore(text, idRange.startOffset)
@@ -519,7 +519,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
}
}
val sb = StringBuilder()
sb.append("<div class='doc-title'>").append(htmlEscape(title)).append("</div>")
sb.append(renderTitle(title))
sb.append(renderDocBody(d.doc))
return sb.toString()
}
@@ -527,7 +527,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
private fun renderParamDoc(fn: MiniFunDecl, p: MiniParam): String {
val title = "parameter ${p.name}${typeOf(p.type)} in ${fn.name}${signatureOf(fn)}"
val sb = StringBuilder()
sb.append("<div class='doc-title'>").append(htmlEscape(title)).append("</div>")
sb.append(renderTitle(title))
// Find matching @param tag
fn.doc?.tags?.get("param")?.forEach { tag ->
@@ -549,7 +549,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
val staticStr = if (m.isStatic) "static " else ""
val title = "${staticStr}method $className.${m.name}(${params})${ret}"
val sb = StringBuilder()
sb.append("<div class='doc-title'>").append(htmlEscape(title)).append("</div>")
sb.append(renderTitle(title))
sb.append(renderDocBody(m.doc))
return sb.toString()
}
@@ -560,7 +560,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
val staticStr = if (m.isStatic) "static " else ""
val title = "${staticStr}${kind} $className.${m.name}${ts}"
val sb = StringBuilder()
sb.append("<div class='doc-title'>").append(htmlEscape(title)).append("</div>")
sb.append(renderTitle(title))
sb.append(renderDocBody(m.doc))
return sb.toString()
}
@@ -579,6 +579,10 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
return "(${params})${ret}"
}
private fun renderTitle(title: String): String {
return "<div class='doc-title' style='margin-bottom: 0.8em;'>${htmlEscape(title)}</div>"
}
private fun htmlEscape(s: String): String = buildString(s.length) {
for (ch in s) append(
when (ch) {
@@ -645,7 +649,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
private fun renderOverloads(name: String, overloads: List<MiniFunDecl>): String {
val sb = StringBuilder()
sb.append("<div class='doc-title'>Overloads for ").append(htmlEscape(name)).append("</div>")
sb.append(renderTitle("Overloads for $name"))
sb.append("<ul>")
overloads.forEach { fn ->
sb.append("<li><code>")
@@ -121,12 +121,39 @@ class LyngLexer : LexerBase() {
// Number
if (ch.isDigit()) {
// Check for hex: 0x...
if (ch == '0' && i + 1 < endOffset && buffer[i + 1] == 'x') {
i += 2
while (i < endOffset && (buffer[i].isDigit() || buffer[i] in 'a'..'f' || buffer[i] in 'A'..'F')) i++
myTokenEnd = i
myTokenType = LyngTokenTypes.NUMBER
return
}
// Decimal or integer
i++
var hasDot = false
var hasE = false
while (i < endOffset) {
val c = buffer[i]
if (c.isDigit()) { i++; continue }
if (c == '.' && !hasDot) { hasDot = true; i++; continue }
if (c.isDigit() || c == '_') {
i++
continue
}
if (c == '.' && !hasDot && !hasE) {
// Check if it's a fractional part (must be followed by a digit)
if (i + 1 < endOffset && buffer[i + 1].isDigit()) {
hasDot = true
i++
continue
}
}
if ((c == 'e' || c == 'E') && !hasE) {
hasE = true
i++
if (i < endOffset && (buffer[i] == '+' || buffer[i] == '-')) i++
continue
}
break
}
myTokenEnd = i
@@ -161,6 +188,35 @@ class LyngLexer : LexerBase() {
// Punctuation
if (isPunct(ch)) {
i++
// Handle common multi-char operators for better highlighting
when (ch) {
'.' -> {
if (i < endOffset && buffer[i] == '.') {
i++
if (i < endOffset && (buffer[i] == '.' || buffer[i] == '<')) i++
}
}
'=' -> {
if (i < endOffset && (buffer[i] == '=' || buffer[i] == '>' || buffer[i] == '~')) {
i++
if (buffer[i - 1] == '=' && i < endOffset && buffer[i] == '=') i++
}
}
'+', '-', '*', '/', '%', '!', '<', '>', '&', '|', '?', ':', '^' -> {
if (i < endOffset) {
val next = buffer[i]
if (next == '=' || next == ch) {
i++
if (ch == '<' && next == '=' && i < endOffset && buffer[i] == '>') i++
if (ch == '!' && next == '=' && i < endOffset && buffer[i] == '=') i++
} else if (ch == '?' && (next == '.' || next == '[' || next == '(' || next == '{' || next == ':' || next == '?')) {
i++
} else if (ch == '-' && next == '>') {
i++
}
}
}
}
myTokenEnd = i
myTokenType = LyngTokenTypes.PUNCT
return
@@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* 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.
@@ -23,9 +23,11 @@ package net.sergeych.lyng.io.fs
import net.sergeych.lyng.ModuleScope
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeCallable
import net.sergeych.lyng.miniast.*
import net.sergeych.lyng.obj.*
import net.sergeych.lyng.pacman.ImportManager
import net.sergeych.lyng.pacman.ModuleBuilder
import net.sergeych.lyngio.fs.LyngFS
import net.sergeych.lyngio.fs.LyngFs
import net.sergeych.lyngio.fs.LyngPath
@@ -50,9 +52,11 @@ fun createFsModule(policy: FsAccessPolicy, manager: ImportManager): Boolean {
// Avoid re-registering in this ImportManager
if (manager.packageNames.contains(name)) return false
manager.addPackage(name) { module ->
buildFsModule(module, policy)
}
manager.addPackage(name, object : ModuleBuilder {
override suspend fun build(module: ModuleScope) {
buildFsModule(module, policy)
}
})
return true
}
@@ -78,322 +82,394 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) {
name = "name",
doc = "Base name of the path (last segment).",
returns = type("lyng.String"),
moduleName = module.packageName
) {
val self = thisAs<ObjPath>()
self.path.name.toObj()
}
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val self = scp.thisAs<ObjPath>()
return self.path.name.toObj()
}
}
)
addFnDoc(
name = "parent",
doc = "Parent directory as a Path or null if none.",
returns = type("Path", nullable = true),
moduleName = module.packageName
) {
val self = thisAs<ObjPath>()
self.path.parent?.let {
ObjPath( this@apply, self.secured, it)
} ?: ObjNull
}
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val self = scp.thisAs<ObjPath>()
return self.path.parent?.let {
ObjPath(this@apply, self.secured, it)
} ?: ObjNull
}
}
)
addFnDoc(
name = "segments",
doc = "List of path segments.",
// returns: List<String>
returns = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.String"))),
moduleName = module.packageName
) {
val self = thisAs<ObjPath>()
ObjList(self.path.segments.map { ObjString(it) }.toMutableList())
}
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val self = scp.thisAs<ObjPath>()
return ObjList(self.path.segments.map { ObjString(it) }.toMutableList())
}
}
)
// exists(): Bool
addFnDoc(
name = "exists",
doc = "Check whether this path exists.",
returns = type("lyng.Bool"),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
(self.secured.exists(self.path)).toObj()
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return scp.fsGuard {
val self = scp.thisObj as ObjPath
(self.secured.exists(self.path)).toObj()
}
}
}
}
)
// isFile(): Bool — cached metadata
addFnDoc(
name = "isFile",
doc = "True if this path is a regular file (based on cached metadata).",
returns = type("lyng.Bool"),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
self.ensureMetadata().let { ObjBool(it.isRegularFile) }
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return scp.fsGuard {
val self = scp.thisObj as ObjPath
self.ensureMetadata().let { ObjBool(it.isRegularFile) }
}
}
}
}
)
// isDirectory(): Bool — cached metadata
addFnDoc(
name = "isDirectory",
doc = "True if this path is a directory (based on cached metadata).",
returns = type("lyng.Bool"),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
self.ensureMetadata().let { ObjBool(it.isDirectory) }
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return scp.fsGuard {
val self = scp.thisObj as ObjPath
self.ensureMetadata().let { ObjBool(it.isDirectory) }
}
}
}
}
)
// size(): Int? — null when unavailable
addFnDoc(
name = "size",
doc = "File size in bytes, or null when unavailable.",
returns = type("lyng.Int", nullable = true),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val m = self.ensureMetadata()
m.size?.let { ObjInt(it) } ?: ObjNull
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return scp.fsGuard {
val self = scp.thisObj as ObjPath
val m = self.ensureMetadata()
m.size?.let { ObjInt(it) } ?: ObjNull
}
}
}
}
)
// createdAt(): Instant? — Lyng Instant, null when unavailable
addFnDoc(
name = "createdAt",
doc = "Creation time as `Instant`, or null when unavailable.",
returns = type("lyng.Instant", nullable = true),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val m = self.ensureMetadata()
m.createdAtMillis?.let { ObjInstant(kotlin.time.Instant.fromEpochMilliseconds(it)) } ?: ObjNull
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return scp.fsGuard {
val self = scp.thisObj as ObjPath
val m = self.ensureMetadata()
m.createdAtMillis?.let { ObjInstant(kotlin.time.Instant.fromEpochMilliseconds(it)) } ?: ObjNull
}
}
}
}
)
// createdAtMillis(): Int? — milliseconds since epoch or null
addFnDoc(
name = "createdAtMillis",
doc = "Creation time in milliseconds since epoch, or null when unavailable.",
returns = type("lyng.Int", nullable = true),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val m = self.ensureMetadata()
m.createdAtMillis?.let { ObjInt(it) } ?: ObjNull
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return scp.fsGuard {
val self = scp.thisObj as ObjPath
val m = self.ensureMetadata()
m.createdAtMillis?.let { ObjInt(it) } ?: ObjNull
}
}
}
}
)
// modifiedAt(): Instant? — Lyng Instant, null when unavailable
addFnDoc(
name = "modifiedAt",
doc = "Last modification time as `Instant`, or null when unavailable.",
returns = type("lyng.Instant", nullable = true),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val m = self.ensureMetadata()
m.modifiedAtMillis?.let { ObjInstant(kotlinx.datetime.Instant.fromEpochMilliseconds(it)) } ?: ObjNull
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return scp.fsGuard {
val self = scp.thisObj as ObjPath
val m = self.ensureMetadata()
m.modifiedAtMillis?.let { ObjInstant(kotlinx.datetime.Instant.fromEpochMilliseconds(it)) } ?: ObjNull
}
}
}
}
)
// modifiedAtMillis(): Int? — milliseconds since epoch or null
addFnDoc(
name = "modifiedAtMillis",
doc = "Last modification time in milliseconds since epoch, or null when unavailable.",
returns = type("lyng.Int", nullable = true),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val m = self.ensureMetadata()
m.modifiedAtMillis?.let { ObjInt(it) } ?: ObjNull
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return scp.fsGuard {
val self = scp.thisObj as ObjPath
val m = self.ensureMetadata()
m.modifiedAtMillis?.let { ObjInt(it) } ?: ObjNull
}
}
}
}
)
// list(): List<Path>
addFnDoc(
name = "list",
doc = "List directory entries as `Path` objects.",
returns = TypeGenericDoc(type("lyng.List"), listOf(type("Path"))),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val items = self.secured.list(self.path).map { ObjPath(self.objClass, self.secured, it) }
ObjList(items.toMutableList())
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return scp.fsGuard {
val self = scp.thisObj as ObjPath
val items = self.secured.list(self.path).map { ObjPath(self.objClass, self.secured, it) }
ObjList(items.toMutableList())
}
}
}
}
)
// readBytes(): Buffer
addFnDoc(
name = "readBytes",
doc = "Read the file into a binary buffer.",
returns = type("lyng.Buffer"),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val bytes = self.secured.readBytes(self.path)
ObjBuffer(bytes.asUByteArray())
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return scp.fsGuard {
val self = scp.thisObj as ObjPath
val bytes = self.secured.readBytes(self.path)
ObjBuffer(bytes.asUByteArray())
}
}
}
}
)
// writeBytes(bytes: Buffer)
addFnDoc(
name = "writeBytes",
doc = "Write a binary buffer to the file, replacing content.",
params = listOf(ParamDoc("bytes", type("lyng.Buffer"))),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val buf = requiredArg<ObjBuffer>(0)
self.secured.writeBytes(self.path, buf.byteArray.asByteArray(), append = false)
ObjVoid
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return scp.fsGuard {
val self = scp.thisObj as ObjPath
val buf = scp.requiredArg<ObjBuffer>(0)
self.secured.writeBytes(self.path, buf.byteArray.asByteArray(), append = false)
ObjVoid
}
}
}
}
)
// appendBytes(bytes: Buffer)
addFnDoc(
name = "appendBytes",
doc = "Append a binary buffer to the end of the file.",
params = listOf(ParamDoc("bytes", type("lyng.Buffer"))),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val buf = requiredArg<ObjBuffer>(0)
self.secured.writeBytes(self.path, buf.byteArray.asByteArray(), append = true)
ObjVoid
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return scp.fsGuard {
val self = scp.thisObj as ObjPath
val buf = scp.requiredArg<ObjBuffer>(0)
self.secured.writeBytes(self.path, buf.byteArray.asByteArray(), append = true)
ObjVoid
}
}
}
}
)
// readUtf8(): String
addFnDoc(
name = "readUtf8",
doc = "Read the file as a UTF-8 string.",
returns = type("lyng.String"),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
self.secured.readUtf8(self.path).toObj()
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return scp.fsGuard {
val self = scp.thisObj as ObjPath
self.secured.readUtf8(self.path).toObj()
}
}
}
}
)
// writeUtf8(text: String)
addFnDoc(
name = "writeUtf8",
doc = "Write a UTF-8 string to the file, replacing content.",
params = listOf(ParamDoc("text", type("lyng.String"))),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val text = requireOnlyArg<ObjString>().value
self.secured.writeUtf8(self.path, text, append = false)
ObjVoid
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return scp.fsGuard {
val self = scp.thisObj as ObjPath
val text = scp.requireOnlyArg<ObjString>().value
self.secured.writeUtf8(self.path, text, append = false)
ObjVoid
}
}
}
}
)
// appendUtf8(text: String)
addFnDoc(
name = "appendUtf8",
doc = "Append UTF-8 text to the end of the file.",
params = listOf(ParamDoc("text", type("lyng.String"))),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val text = requireOnlyArg<ObjString>().value
self.secured.writeUtf8(self.path, text, append = true)
ObjVoid
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return scp.fsGuard {
val self = scp.thisObj as ObjPath
val text = scp.requireOnlyArg<ObjString>().value
self.secured.writeUtf8(self.path, text, append = true)
ObjVoid
}
}
}
}
)
// metadata(): Map
addFnDoc(
name = "metadata",
doc = "Fetch cached metadata as a map of fields: `isFile`, `isDirectory`, `size`, `createdAtMillis`, `modifiedAtMillis`, `isSymlink`.",
returns = TypeGenericDoc(type("lyng.Map"), listOf(type("lyng.String"), type("lyng.Any"))),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val m = self.secured.metadata(self.path)
ObjMap(mutableMapOf(
ObjString("isFile") to ObjBool(m.isRegularFile),
ObjString("isDirectory") to ObjBool(m.isDirectory),
ObjString("size") to (m.size?.toLong() ?: 0L).toObj(),
ObjString("createdAtMillis") to ((m.createdAtMillis ?: 0L)).toObj(),
ObjString("modifiedAtMillis") to ((m.modifiedAtMillis ?: 0L)).toObj(),
ObjString("isSymlink") to ObjBool(m.isSymlink),
))
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return scp.fsGuard {
val self = scp.thisObj as ObjPath
val m = self.secured.metadata(self.path)
ObjMap(mutableMapOf(
ObjString("isFile") to ObjBool(m.isRegularFile),
ObjString("isDirectory") to ObjBool(m.isDirectory),
ObjString("size") to (m.size?.toLong() ?: 0L).toObj(),
ObjString("createdAtMillis") to ((m.createdAtMillis ?: 0L)).toObj(),
ObjString("modifiedAtMillis") to ((m.modifiedAtMillis ?: 0L)).toObj(),
ObjString("isSymlink") to ObjBool(m.isSymlink),
))
}
}
}
}
)
// mkdirs(mustCreate: Bool=false)
addFnDoc(
name = "mkdirs",
doc = "Create directories (like `mkdir -p`). If `mustCreate` is true and the path already exists, the call fails. Otherwise it is a no‑op when the directory exists.",
params = listOf(ParamDoc("mustCreate", type("lyng.Bool"))),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val mustCreate = args.list.getOrNull(0)?.toBool() ?: false
self.secured.createDirectories(self.path, mustCreate)
ObjVoid
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return scp.fsGuard {
val self = scp.thisObj as ObjPath
val mustCreate = scp.args.list.getOrNull(0)?.toBool() ?: false
self.secured.createDirectories(self.path, mustCreate)
ObjVoid
}
}
}
}
)
// move(to: Path|String, overwrite: Bool=false)
addFnDoc(
name = "move",
doc = "Move this path to a new location. `to` may be a `Path` or `String`. When `overwrite` is false and the target exists, the operation fails (provider may throw `AccessDeniedException`).",
// types vary; keep generic description in doc
params = listOf(ParamDoc("to"), ParamDoc("overwrite", type("lyng.Bool"))),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val toPath = parsePathArg(this, self, requiredArg<Obj>(0))
val overwrite = args.list.getOrNull(1)?.toBool() ?: false
self.secured.move(self.path, toPath, overwrite)
ObjVoid
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return scp.fsGuard {
val self = scp.thisObj as ObjPath
val toPath = parsePathArg(scp, self, scp.requiredArg<Obj>(0))
val overwrite = scp.args.list.getOrNull(1)?.toBool() ?: false
self.secured.move(self.path, toPath, overwrite)
ObjVoid
}
}
}
}
)
// delete(mustExist: Bool=false, recursively: Bool=false)
addFnDoc(
name = "delete",
doc = "Delete this path. `mustExist=true` causes failure if the path does not exist. `recursively=true` removes directories with their contents. Providers can throw `AccessDeniedException` on policy violations.",
params = listOf(ParamDoc("mustExist", type("lyng.Bool")), ParamDoc("recursively", type("lyng.Bool"))),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val mustExist = args.list.getOrNull(0)?.toBool() ?: false
val recursively = args.list.getOrNull(1)?.toBool() ?: false
self.secured.delete(self.path, mustExist, recursively)
ObjVoid
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return scp.fsGuard {
val self = scp.thisObj as ObjPath
val mustExist = scp.args.list.getOrNull(0)?.toBool() ?: false
val recursively = scp.args.list.getOrNull(1)?.toBool() ?: false
self.secured.delete(self.path, mustExist, recursively)
ObjVoid
}
}
}
}
)
// copy(to: Path|String, overwrite: Bool=false)
addFnDoc(
name = "copy",
doc = "Copy this path to a new location. `to` may be a `Path` or `String`. When `overwrite` is false and the target exists, the operation fails (provider may throw `AccessDeniedException`).",
params = listOf(ParamDoc("to"), ParamDoc("overwrite", type("lyng.Bool"))),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val toPath = parsePathArg(this, self, requiredArg<Obj>(0))
val overwrite = args.list.getOrNull(1)?.toBool() ?: false
self.secured.copy(self.path, toPath, overwrite)
ObjVoid
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return scp.fsGuard {
val self = scp.thisObj as ObjPath
val toPath = parsePathArg(scp, self, scp.requiredArg<Obj>(0))
val overwrite = scp.args.list.getOrNull(1)?.toBool() ?: false
self.secured.copy(self.path, toPath, overwrite)
ObjVoid
}
}
}
}
)
// glob(pattern: String): List<Path>
addFnDoc(
name = "glob",
doc = "List entries matching a glob pattern (no recursion).",
params = listOf(ParamDoc("pattern", type("lyng.String"))),
returns = TypeGenericDoc(type("lyng.List"), listOf(type("Path"))),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val pattern = requireOnlyArg<ObjString>().value
val matches = self.secured.glob(self.path, pattern)
ObjList(matches.map { ObjPath(self.objClass, self.secured, it) }.toMutableList())
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return scp.fsGuard {
val self = scp.thisObj as ObjPath
val pattern = scp.requireOnlyArg<ObjString>().value
val matches = self.secured.glob(self.path, pattern)
ObjList(matches.map { ObjPath(self.objClass, self.secured, it) }.toMutableList())
}
}
}
}
)
// --- streaming readers (initial version: chunk from whole content, API stable) ---
@@ -403,15 +479,18 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) {
doc = "Read file in fixed-size chunks as an iterator of `Buffer`.",
params = listOf(ParamDoc("size", type("lyng.Int"))),
returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.Buffer"))),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val size = args.list.getOrNull(0)?.toInt() ?: 65536
val bytes = self.secured.readBytes(self.path)
ObjFsBytesIterator(bytes, size)
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return scp.fsGuard {
val self = scp.thisObj as ObjPath
val size = scp.args.list.getOrNull(0)?.toInt() ?: 65536
val bytes = self.secured.readBytes(self.path)
ObjFsBytesIterator(bytes, size)
}
}
}
}
)
// readUtf8Chunks(size: Int = 65536) -> Iterator<String>
addFnDoc(
@@ -419,28 +498,34 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) {
doc = "Read UTF-8 text in fixed-size chunks as an iterator of `String`.",
params = listOf(ParamDoc("size", type("lyng.Int"))),
returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.String"))),
moduleName = module.packageName
) {
fsGuard {
val self = this.thisObj as ObjPath
val size = args.list.getOrNull(0)?.toInt() ?: 65536
val text = self.secured.readUtf8(self.path)
ObjFsStringChunksIterator(text, size)
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return scp.fsGuard {
val self = scp.thisObj as ObjPath
val size = scp.args.list.getOrNull(0)?.toInt() ?: 65536
val text = self.secured.readUtf8(self.path)
ObjFsStringChunksIterator(text, size)
}
}
}
}
)
// lines() -> Iterator<String>, implemented via readUtf8Chunks
addFnDoc(
name = "lines",
doc = "Iterate lines of the file as `String` values.",
returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.String"))),
moduleName = module.packageName
) {
fsGuard {
val chunkIt = thisObj.invokeInstanceMethod(this, "readUtf8Chunks")
ObjFsLinesIterator(chunkIt)
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return scp.fsGuard {
val chunkIt = scp.thisObj.invokeInstanceMethod(scp, "readUtf8Chunks")
ObjFsLinesIterator(chunkIt)
}
}
}
}
)
}
// Export into the module scope with docs
@@ -518,39 +603,51 @@ class ObjFsBytesIterator(
name = "iterator",
doc = "Return this iterator instance (enables `for` loops).",
returns = type("BytesIterator"),
moduleName = "lyng.io.fs"
) { thisObj }
moduleName = "lyng.io.fs",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisObj
}
)
addFnDoc(
name = "hasNext",
doc = "Whether there is another chunk available.",
returns = type("lyng.Bool"),
moduleName = "lyng.io.fs"
) {
val self = thisAs<ObjFsBytesIterator>()
(self.pos < self.data.size).toObj()
}
moduleName = "lyng.io.fs",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val self = scp.thisAs<ObjFsBytesIterator>()
return (self.pos < self.data.size).toObj()
}
}
)
addFnDoc(
name = "next",
doc = "Return the next chunk as a `Buffer`.",
returns = type("lyng.Buffer"),
moduleName = "lyng.io.fs"
) {
val self = thisAs<ObjFsBytesIterator>()
if (self.pos >= self.data.size) raiseIllegalState("iterator exhausted")
val end = minOf(self.pos + self.chunkSize, self.data.size)
val chunk = self.data.copyOfRange(self.pos, end)
self.pos = end
ObjBuffer(chunk.asUByteArray())
}
moduleName = "lyng.io.fs",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val self = scp.thisAs<ObjFsBytesIterator>()
if (self.pos >= self.data.size) scp.raiseIllegalState("iterator exhausted")
val end = minOf(self.pos + self.chunkSize, self.data.size)
val chunk = self.data.copyOfRange(self.pos, end)
self.pos = end
return ObjBuffer(chunk.asUByteArray())
}
}
)
addFnDoc(
name = "cancelIteration",
doc = "Stop the iteration early; subsequent `hasNext` returns false.",
moduleName = "lyng.io.fs"
) {
val self = thisAs<ObjFsBytesIterator>()
self.pos = self.data.size
ObjVoid
}
moduleName = "lyng.io.fs",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val self = scp.thisAs<ObjFsBytesIterator>()
self.pos = self.data.size
return ObjVoid
}
}
)
}
}
}
@@ -573,35 +670,47 @@ class ObjFsStringChunksIterator(
name = "iterator",
doc = "Return this iterator instance (enables `for` loops).",
returns = type("StringChunksIterator"),
moduleName = "lyng.io.fs"
) { thisObj }
moduleName = "lyng.io.fs",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisObj
}
)
addFnDoc(
name = "hasNext",
doc = "Whether there is another chunk available.",
returns = type("lyng.Bool"),
moduleName = "lyng.io.fs"
) {
val self = thisAs<ObjFsStringChunksIterator>()
(self.pos < self.text.length).toObj()
}
moduleName = "lyng.io.fs",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val self = scp.thisAs<ObjFsStringChunksIterator>()
return (self.pos < self.text.length).toObj()
}
}
)
addFnDoc(
name = "next",
doc = "Return the next UTF-8 chunk as a `String`.",
returns = type("lyng.String"),
moduleName = "lyng.io.fs"
) {
val self = thisAs<ObjFsStringChunksIterator>()
if (self.pos >= self.text.length) raiseIllegalState("iterator exhausted")
val end = minOf(self.pos + self.chunkChars, self.text.length)
val chunk = self.text.substring(self.pos, end)
self.pos = end
ObjString(chunk)
}
moduleName = "lyng.io.fs",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val self = scp.thisAs<ObjFsStringChunksIterator>()
if (self.pos >= self.text.length) scp.raiseIllegalState("iterator exhausted")
val end = minOf(self.pos + self.chunkChars, self.text.length)
val chunk = self.text.substring(self.pos, end)
self.pos = end
return ObjString(chunk)
}
}
)
addFnDoc(
name = "cancelIteration",
doc = "Stop the iteration early; subsequent `hasNext` returns false.",
moduleName = "lyng.io.fs"
) { ObjVoid }
moduleName = "lyng.io.fs",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjVoid
}
)
}
}
}
@@ -624,46 +733,58 @@ class ObjFsLinesIterator(
name = "iterator",
doc = "Return this iterator instance (enables `for` loops).",
returns = type("LinesIterator"),
moduleName = "lyng.io.fs"
) { thisObj }
moduleName = "lyng.io.fs",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisObj
}
)
addFnDoc(
name = "hasNext",
doc = "Whether another line is available.",
returns = type("lyng.Bool"),
moduleName = "lyng.io.fs"
) {
val self = thisAs<ObjFsLinesIterator>()
self.ensureBufferFilled(this)
(self.buffer.isNotEmpty() || !self.exhausted).toObj()
}
moduleName = "lyng.io.fs",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val self = scp.thisAs<ObjFsLinesIterator>()
self.ensureBufferFilled(scp)
return (self.buffer.isNotEmpty() || !self.exhausted).toObj()
}
}
)
addFnDoc(
name = "next",
doc = "Return the next line as `String`.",
returns = type("lyng.String"),
moduleName = "lyng.io.fs"
) {
val self = thisAs<ObjFsLinesIterator>()
self.ensureBufferFilled(this)
if (self.buffer.isEmpty() && self.exhausted) raiseIllegalState("iterator exhausted")
val idx = self.buffer.indexOf('\n')
val line = if (idx >= 0) {
val l = self.buffer.substring(0, idx)
self.buffer = self.buffer.substring(idx + 1)
l
} else {
// last line without trailing newline
val l = self.buffer
self.buffer = ""
self.exhausted = true
l
moduleName = "lyng.io.fs",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val self = scp.thisAs<ObjFsLinesIterator>()
self.ensureBufferFilled(scp)
if (self.buffer.isEmpty() && self.exhausted) scp.raiseIllegalState("iterator exhausted")
val idx = self.buffer.indexOf('\n')
val line = if (idx >= 0) {
val l = self.buffer.substring(0, idx)
self.buffer = self.buffer.substring(idx + 1)
l
} else {
// last line without trailing newline
val l = self.buffer
self.buffer = ""
self.exhausted = true
l
}
return ObjString(line)
}
}
ObjString(line)
}
)
addFnDoc(
name = "cancelIteration",
doc = "Stop the iteration early; subsequent `hasNext` returns false.",
moduleName = "lyng.io.fs"
) { ObjVoid }
moduleName = "lyng.io.fs",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjVoid
}
)
}
}
}
@@ -20,9 +20,11 @@ package net.sergeych.lyng.io.process
import kotlinx.coroutines.flow.Flow
import net.sergeych.lyng.ModuleScope
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeCallable
import net.sergeych.lyng.miniast.*
import net.sergeych.lyng.obj.*
import net.sergeych.lyng.pacman.ImportManager
import net.sergeych.lyng.pacman.ModuleBuilder
import net.sergeych.lyng.statement
import net.sergeych.lyngio.process.*
import net.sergeych.lyngio.process.security.ProcessAccessDeniedException
@@ -39,9 +41,11 @@ fun createProcessModule(policy: ProcessAccessPolicy, manager: ImportManager): Bo
val name = "lyng.io.process"
if (manager.packageNames.contains(name)) return false
manager.addPackage(name) { module ->
buildProcessModule(module, policy)
}
manager.addPackage(name, object : ModuleBuilder {
override suspend fun build(module: ModuleScope) {
buildProcessModule(module, policy)
}
})
return true
}
@@ -59,59 +63,74 @@ private suspend fun buildProcessModule(module: ModuleScope, policy: ProcessAcces
name = "stdout",
doc = "Get standard output stream as a Flow of lines.",
returns = type("lyng.Flow"),
moduleName = module.packageName
) {
val self = thisAs<ObjRunningProcess>()
self.process.stdout.toLyngFlow(this)
}
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val self = scp.thisAs<ObjRunningProcess>()
return self.process.stdout.toLyngFlow(scp)
}
}
)
addFnDoc(
name = "stderr",
doc = "Get standard error stream as a Flow of lines.",
returns = type("lyng.Flow"),
moduleName = module.packageName
) {
val self = thisAs<ObjRunningProcess>()
self.process.stderr.toLyngFlow(this)
}
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val self = scp.thisAs<ObjRunningProcess>()
return self.process.stderr.toLyngFlow(scp)
}
}
)
addFnDoc(
name = "signal",
doc = "Send a signal to the process (e.g. 'SIGINT', 'SIGTERM', 'SIGKILL').",
params = listOf(ParamDoc("signal", type("lyng.String"))),
moduleName = module.packageName
) {
processGuard {
val sigStr = requireOnlyArg<ObjString>().value.uppercase()
val sig = try {
ProcessSignal.valueOf(sigStr)
} catch (e: Exception) {
try {
ProcessSignal.valueOf("SIG$sigStr")
} catch (e2: Exception) {
raiseIllegalArgument("Unknown signal: $sigStr")
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return scp.processGuard {
val sigStr = scp.requireOnlyArg<ObjString>().value.uppercase()
val sig = try {
ProcessSignal.valueOf(sigStr)
} catch (e: Exception) {
try {
ProcessSignal.valueOf("SIG$sigStr")
} catch (e2: Exception) {
scp.raiseIllegalArgument("Unknown signal: $sigStr")
}
}
scp.thisAs<ObjRunningProcess>().process.sendSignal(sig)
ObjVoid
}
}
thisAs<ObjRunningProcess>().process.sendSignal(sig)
ObjVoid
}
}
)
addFnDoc(
name = "waitFor",
doc = "Wait for the process to exit and return its exit code.",
returns = type("lyng.Int"),
moduleName = module.packageName
) {
processGuard {
thisAs<ObjRunningProcess>().process.waitFor().toObj()
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return scp.processGuard {
scp.thisAs<ObjRunningProcess>().process.waitFor().toObj()
}
}
}
}
)
addFnDoc(
name = "destroy",
doc = "Forcefully terminate the process.",
moduleName = module.packageName
) {
thisAs<ObjRunningProcess>().process.destroy()
ObjVoid
}
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
scp.thisAs<ObjRunningProcess>().process.destroy()
return ObjVoid
}
}
)
}
val processType = object : ObjClass("Process") {}
@@ -122,30 +141,36 @@ private suspend fun buildProcessModule(module: ModuleScope, policy: ProcessAcces
doc = "Execute a process with arguments.",
params = listOf(ParamDoc("executable", type("lyng.String")), ParamDoc("args", type("lyng.List"))),
returns = type("RunningProcess"),
moduleName = module.packageName
) {
if (runner == null) raiseError("Processes are not supported on this platform")
processGuard {
val executable = requiredArg<ObjString>(0).value
val args = requiredArg<ObjList>(1).list.map { it.toString() }
val lp = runner.execute(executable, args)
ObjRunningProcess(runningProcessType, lp)
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
if (runner == null) scp.raiseError("Processes are not supported on this platform")
return scp.processGuard {
val executable = scp.requiredArg<ObjString>(0).value
val args = scp.requiredArg<ObjList>(1).list.map { it.toString() }
val lp = runner.execute(executable, args)
ObjRunningProcess(runningProcessType, lp)
}
}
}
}
)
addClassFnDoc(
name = "shell",
doc = "Execute a command via system shell.",
params = listOf(ParamDoc("command", type("lyng.String"))),
returns = type("RunningProcess"),
moduleName = module.packageName
) {
if (runner == null) raiseError("Processes are not supported on this platform")
processGuard {
val command = requireOnlyArg<ObjString>().value
val lp = runner.shell(command)
ObjRunningProcess(runningProcessType, lp)
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
if (runner == null) scp.raiseError("Processes are not supported on this platform")
return scp.processGuard {
val command = scp.requireOnlyArg<ObjString>().value
val lp = runner.shell(command)
ObjRunningProcess(runningProcessType, lp)
}
}
}
}
)
}
val platformType = object : ObjClass("Platform") {}
@@ -155,24 +180,28 @@ private suspend fun buildProcessModule(module: ModuleScope, policy: ProcessAcces
name = "details",
doc = "Get platform core details.",
returns = type("lyng.Map"),
moduleName = module.packageName
) {
val d = getPlatformDetails()
ObjMap(mutableMapOf(
ObjString("name") to ObjString(d.name),
ObjString("version") to ObjString(d.version),
ObjString("arch") to ObjString(d.arch),
ObjString("kernelVersion") to (d.kernelVersion?.toObj() ?: ObjNull)
))
}
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val d = getPlatformDetails()
return ObjMap(mutableMapOf(
ObjString("name") to ObjString(d.name),
ObjString("version") to ObjString(d.version),
ObjString("arch") to ObjString(d.arch),
ObjString("kernelVersion") to (d.kernelVersion?.toObj() ?: ObjNull)
))
}
}
)
addClassFnDoc(
name = "isSupported",
doc = "Check if processes are supported on this platform.",
returns = type("lyng.Bool"),
moduleName = module.packageName
) {
isProcessSupported().toObj()
}
moduleName = module.packageName,
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = isProcessSupported().toObj()
}
)
}
module.addConstDoc(
@@ -216,19 +245,21 @@ private suspend inline fun Scope.processGuard(crossinline block: suspend () -> O
}
private fun Flow<String>.toLyngFlow(flowScope: Scope): ObjFlow {
val producer = statement {
val builder = (this as? net.sergeych.lyng.ClosureScope)?.callScope?.thisObj as? ObjFlowBuilder
?: this.thisObj as? ObjFlowBuilder
val producer = statement(f = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val builder = (scp as? net.sergeych.lyng.ClosureScope)?.callScope?.thisObj as? ObjFlowBuilder
?: scp.thisObj as? ObjFlowBuilder
this@toLyngFlow.collect {
try {
builder?.output?.send(ObjString(it))
} catch (e: Exception) {
// Channel closed or other error, stop collecting
return@collect
this@toLyngFlow.collect {
try {
builder?.output?.send(ObjString(it))
} catch (e: Exception) {
// Channel closed or other error, stop collecting
return@collect
}
}
return ObjVoid
}
ObjVoid
}
})
return ObjFlow(producer, flowScope)
}
+2 -1
View File
@@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych"
version = "1.2.0"
version = "1.2.1-SNAPSHOT"
// Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below
@@ -97,6 +97,7 @@ kotlin {
kotlin.srcDir("$buildDir/generated/buildConfig/commonMain/kotlin")
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
//put your multiplatform dependencies here
api(libs.kotlinx.coroutines.core)
api(libs.mp.bintools)
@@ -71,7 +71,8 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
value.byValueCopy(),
a.visibility ?: defaultVisibility,
recordType = ObjRecord.Type.Argument,
declaringClass = declaringClass)
declaringClass = declaringClass,
isTransient = a.isTransient)
}
return
}
@@ -82,7 +83,8 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
value.byValueCopy(),
a.visibility ?: defaultVisibility,
recordType = ObjRecord.Type.Argument,
declaringClass = declaringClass)
declaringClass = declaringClass,
isTransient = a.isTransient)
}
// Prepare positional args and parameter count, handle tail-block binding
@@ -152,69 +154,73 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
}
}
// Helper: assign head part, consuming from headPos; stop at ellipsis
suspend fun processHead(index: Int, headPos: Int): Pair<Int, Int> {
var i = index
var hp = headPos
// Locate ellipsis index within considered parameters
val ellipsisIndex = params.subList(0, paramsSize).indexOfFirst { it.isEllipsis }
var headPos = 0
var tailPos = callArgs.size - 1
if (ellipsisIndex >= 0) {
// Assign head first to know how many positionals are consumed from the start
var i = 0
while (i < paramsSize) {
val a = params[i]
if (a.isEllipsis) break
if (assignedByName[i]) {
assign(a, namedValues[i]!!)
} else {
val value = if (hp < callArgs.size) callArgs[hp++]
val value = if (headPos < callArgs.size) callArgs[headPos++]
else a.defaultValue?.execute(scope)
?: scope.raiseIllegalArgument("too few arguments for the call (missing ${a.name})")
assign(a, value)
}
i++
}
return i to hp
}
val afterHead = i
val headConsumedTo = headPos
// Helper: assign tail part from the end, consuming from tailPos; stop before ellipsis index
// Do not consume elements below headPosBound to avoid overlap with head consumption
suspend fun processTail(startExclusive: Int, tailStart: Int, headPosBound: Int): Int {
var i = paramsSize - 1
var tp = tailStart
while (i > startExclusive) {
// Then assign tail consuming from the end down to headConsumedTo boundary
i = paramsSize - 1
var tp = tailPos
while (i > ellipsisIndex) {
val a = params[i]
if (a.isEllipsis) break
if (i < assignedByName.size && assignedByName[i]) {
assign(a, namedValues[i]!!)
} else {
val value = if (tp >= headPosBound) callArgs[tp--]
val value = if (tp >= headConsumedTo) callArgs[tp--]
else a.defaultValue?.execute(scope)
?: scope.raiseIllegalArgument("too few arguments for the call")
assign(a, value)
}
i--
}
return tp
}
val tailConsumedFrom = tp
fun processEllipsis(index: Int, headPos: Int, tailPos: Int) {
val a = params[index]
val from = headPos
val to = tailPos
// Assign ellipsis list from remaining positionals between headConsumedTo..tailConsumedFrom
val a = params[ellipsisIndex]
val from = headConsumedTo
val to = tailConsumedFrom
val l = if (from > to) ObjList()
else ObjList(callArgs.subList(from, to + 1).toMutableList())
assign(a, l)
}
// Locate ellipsis index within considered parameters
val ellipsisIndex = params.subList(0, paramsSize).indexOfFirst { it.isEllipsis }
if (ellipsisIndex >= 0) {
// Assign head first to know how many positionals are consumed from the start
val (afterHead, headConsumedTo) = processHead(0, 0)
// Then assign tail consuming from the end down to headConsumedTo boundary
val tailConsumedFrom = processTail(ellipsisIndex, callArgs.size - 1, headConsumedTo)
// Assign ellipsis list from remaining positionals between headConsumedTo..tailConsumedFrom
processEllipsis(ellipsisIndex, headConsumedTo, tailConsumedFrom)
} else {
// No ellipsis: assign head only; any leftover positionals → error
val (_, headConsumedTo) = processHead(0, 0)
var i = 0
while (i < paramsSize) {
val a = params[i]
if (a.isEllipsis) break
if (assignedByName[i]) {
assign(a, namedValues[i]!!)
} else {
val value = if (headPos < callArgs.size) callArgs[headPos++]
else a.defaultValue?.execute(scope)
?: scope.raiseIllegalArgument("too few arguments for the call (missing ${a.name})")
assign(a, value)
}
i++
}
val headConsumedTo = headPos
if (headConsumedTo != callArgs.size)
scope.raiseIllegalArgument("too many arguments for the call")
}
@@ -239,5 +245,6 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
val defaultValue: Statement? = null,
val accessType: AccessType? = null,
val visibility: Visibility? = null,
val isTransient: Boolean = false,
)
}
@@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* 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.
@@ -244,10 +244,16 @@ data class ParsedArgument(
* Convert to list of kotlin objects, see [Obj.toKotlin].
*/
suspend fun toKotlinList(scope: Scope): List<Any?> {
return list.map { it.toKotlin(scope) }
val res = ArrayList<Any?>(list.size)
for (i in list) res.add(i.toKotlin(scope))
return res
}
suspend fun inspect(scope: Scope): String {
val res = ArrayList<String>(list.size)
for (i in list) res.add(i.inspect(scope))
return res.joinToString(",")
}
suspend fun inspect(scope: Scope): String = list.map{ it.inspect(scope)}.joinToString(",")
companion object {
val EMPTY = Arguments(emptyList())
@@ -54,14 +54,14 @@ class ClosureScope(val callScope: Scope, val closureScope: Scope) :
for (cls in effectiveClass.mro) {
val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
if (rec != null && !rec.isAbstract) {
if (canAccessMember(rec.visibility, rec.declaringClass ?: cls, currentClassCtx)) {
if (canAccessMember(rec.visibility, rec.declaringClass ?: cls, currentClassCtx, name)) {
return rec.copy(receiver = receiver)
}
}
}
// Finally, root object fallback
Obj.rootObjectType.members[name]?.let { rec ->
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) {
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx, name)) {
return rec.copy(receiver = receiver)
}
}
File diff suppressed because it is too large. Load diff
@@ -21,6 +21,23 @@ import net.sergeych.lyng.obj.*
import net.sergeych.lyng.pacman.ImportManager
import net.sergeych.lyng.pacman.ImportProvider
interface ScopeCallable {
suspend fun call(scope: Scope): Obj
}
interface VoidScopeCallable {
suspend fun call(scope: Scope)
}
interface ScopeBlock<R> {
suspend fun call(scope: Scope): R
}
private class FnStatement(val fn: ScopeCallable) : Statement() {
override val pos: Pos = Pos.builtIn
override suspend fun execute(scope: Scope): Obj = fn.call(scope)
}
// Simple per-frame id generator for perf caches (not thread-safe, fine for scripts)
object FrameIdGen { var c: Long = 1L; fun nextId(): Long = c++ }
fun nextFrameId(): Long = FrameIdGen.nextId()
@@ -112,7 +129,7 @@ open class Scope(
}
}
s.objects[name]?.let { rec ->
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller, name)) return rec
}
caller?.let { ctx ->
s.localBindings[ctx.mangledName(name)]?.let { rec ->
@@ -120,11 +137,11 @@ open class Scope(
}
}
s.localBindings[name]?.let { rec ->
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller, name)) return rec
}
s.getSlotIndexOf(name)?.let { idx ->
val rec = s.getSlotRecord(idx)
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller, name)) return rec
}
return null
}
@@ -163,7 +180,7 @@ open class Scope(
this.extensions[cls]?.get(name)?.let { return it }
}
return thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec ->
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) {
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx, name)) {
if (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property || rec.isAbstract) null
else rec
} else null
@@ -186,7 +203,7 @@ open class Scope(
s.extensions[cls]?.get(name)?.let { return it }
}
s.thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec ->
if (canAccessMember(rec.visibility, rec.declaringClass, caller)) {
if (canAccessMember(rec.visibility, rec.declaringClass, caller, name)) {
if (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property || rec.isAbstract) {
// ignore fields, properties and abstracts here, they will be handled by the caller via readField
} else return rec
@@ -358,14 +375,14 @@ open class Scope(
for (cls in effectiveClass.mro) {
val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
if (rec != null && !rec.isAbstract) {
if (canAccessMember(rec.visibility, rec.declaringClass ?: cls, currentClassCtx)) {
if (canAccessMember(rec.visibility, rec.declaringClass ?: cls, currentClassCtx, name)) {
return rec.copy(receiver = receiver)
}
}
}
// Finally, root object fallback
Obj.rootObjectType.members[name]?.let { rec ->
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) {
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx, name)) {
return rec.copy(receiver = receiver)
}
}
@@ -447,17 +464,17 @@ open class Scope(
* Execute a block inside a child frame. Guarded for future pooling via [PerfFlags.SCOPE_POOL].
* Currently always creates a fresh child scope to preserve unique frameId semantics.
*/
inline suspend fun <R> withChildFrame(args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null, crossinline block: suspend (Scope) -> R): R {
suspend fun <R> withChildFrame(args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null, block: ScopeBlock<R>): R {
if (PerfFlags.SCOPE_POOL) {
val child = ScopePool.borrow(this, args, pos, newThisObj ?: thisObj)
try {
return block(child)
return block.call(child)
} finally {
ScopePool.release(child)
}
} else {
val child = createChildScope(args, newThisObj)
return block(child)
return block.call(child)
}
}
@@ -516,7 +533,8 @@ open class Scope(
declaringClass: net.sergeych.lyng.obj.ObjClass? = currentClassCtx,
isAbstract: Boolean = false,
isClosed: Boolean = false,
isOverride: Boolean = false
isOverride: Boolean = false,
isTransient: Boolean = false
): ObjRecord {
val rec = ObjRecord(
value, isMutable, visibility, writeVisibility,
@@ -524,7 +542,8 @@ open class Scope(
type = recordType,
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride
isOverride = isOverride,
isTransient = isTransient
)
objects[name] = rec
// Index this binding within the current frame to help resolve locals across suspension
@@ -561,20 +580,17 @@ open class Scope(
return ns.objClass
}
inline fun addVoidFn(vararg names: String, crossinline fn: suspend Scope.() -> Unit) {
addFn(*names) {
fn(this)
ObjVoid
}
fun addVoidFn(vararg names: String, fn: VoidScopeCallable) {
addFn(*names, fn = object : ScopeCallable {
override suspend fun call(scope: Scope): Obj {
fn.call(scope)
return ObjVoid
}
})
}
fun addFn(vararg names: String, fn: suspend Scope.() -> Obj) {
val newFn = object : Statement() {
override val pos: Pos = Pos.builtIn
override suspend fun execute(scope: Scope): Obj = scope.fn()
}
fun addFn(vararg names: String, fn: ScopeCallable) {
val newFn = FnStatement(fn)
for (name in names) {
addItem(
name,
@@ -18,17 +18,13 @@
package net.sergeych.lyng
import kotlinx.coroutines.delay
import kotlinx.coroutines.yield
import net.sergeych.lyng.Script.Companion.defaultImportManager
import net.sergeych.lyng.miniast.addConstDoc
import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.addVoidFnDoc
import net.sergeych.lyng.miniast.type
import net.sergeych.lyng.miniast.*
import net.sergeych.lyng.obj.*
import net.sergeych.lyng.pacman.ImportManager
import net.sergeych.lyng.pacman.ModuleBuilder
import net.sergeych.lyng.stdlib_included.rootLyng
import net.sergeych.lynon.ObjLynonClass
import net.sergeych.mp_tools.globalDefer
import kotlin.math.*
@Suppress("TYPE_INTERSECTION_AS_REIFIED_WARNING")
@@ -62,150 +58,173 @@ class Script(
internal val rootScope: Scope = Scope(null).apply {
ObjException.addExceptionsToContext(this)
addConst("Unset", ObjUnset)
addFn("print") {
for ((i, a) in args.withIndex()) {
if (i > 0) print(' ' + a.toString(this).value)
else print(a.toString(this).value)
addFn("print", fn = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
for ((i, a) in scp.args.withIndex()) {
if (i > 0) print(' ' + a.toString(scp).value)
else print(a.toString(scp).value)
}
return ObjVoid
}
ObjVoid
}
addFn("println") {
for ((i, a) in args.withIndex()) {
if (i > 0) print(' ' + a.toString(this).value)
else print(a.toString(this).value)
})
addFn("println", fn = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
for ((i, a) in scp.args.withIndex()) {
if (i > 0) print(' ' + a.toString(scp).value)
else print(a.toString(scp).value)
}
println()
return ObjVoid
}
println()
ObjVoid
}
addFn("floor") {
val x = args.firstAndOnly()
(if (x is ObjInt) x
else ObjReal(floor(x.toDouble())))
}
addFn("ceil") {
val x = args.firstAndOnly()
(if (x is ObjInt) x
else ObjReal(ceil(x.toDouble())))
}
addFn("round") {
val x = args.firstAndOnly()
(if (x is ObjInt) x
else ObjReal(round(x.toDouble())))
}
})
addFn("floor", fn = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val x = scp.args.firstAndOnly()
return (if (x is ObjInt) x
else ObjReal(floor(x.toDouble())))
}
})
addFn("ceil", fn = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val x = scp.args.firstAndOnly()
return (if (x is ObjInt) x
else ObjReal(ceil(x.toDouble())))
}
})
addFn("round", fn = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val x = scp.args.firstAndOnly()
return (if (x is ObjInt) x
else ObjReal(round(x.toDouble())))
}
})
addFn("sin") {
ObjReal(sin(args.firstAndOnly().toDouble()))
}
addFn("cos") {
ObjReal(cos(args.firstAndOnly().toDouble()))
}
addFn("tan") {
ObjReal(tan(args.firstAndOnly().toDouble()))
}
addFn("asin") {
ObjReal(asin(args.firstAndOnly().toDouble()))
}
addFn("acos") {
ObjReal(acos(args.firstAndOnly().toDouble()))
}
addFn("atan") {
ObjReal(atan(args.firstAndOnly().toDouble()))
}
addFn("sin", fn = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = ObjReal(sin(scp.args.firstAndOnly().toDouble())) })
addFn("cos", fn = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = ObjReal(cos(scp.args.firstAndOnly().toDouble())) })
addFn("tan", fn = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = ObjReal(tan(scp.args.firstAndOnly().toDouble())) })
addFn("asin", fn = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = ObjReal(asin(scp.args.firstAndOnly().toDouble())) })
addFn("acos", fn = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = ObjReal(acos(scp.args.firstAndOnly().toDouble())) })
addFn("atan", fn = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = ObjReal(atan(scp.args.firstAndOnly().toDouble())) })
addFn("sinh") {
ObjReal(sinh(args.firstAndOnly().toDouble()))
}
addFn("cosh") {
ObjReal(cosh(args.firstAndOnly().toDouble()))
}
addFn("tanh") {
ObjReal(tanh(args.firstAndOnly().toDouble()))
}
addFn("asinh") {
ObjReal(asinh(args.firstAndOnly().toDouble()))
}
addFn("acosh") {
ObjReal(acosh(args.firstAndOnly().toDouble()))
}
addFn("atanh") {
ObjReal(atanh(args.firstAndOnly().toDouble()))
}
addFn("sinh", fn = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = ObjReal(sinh(scp.args.firstAndOnly().toDouble())) })
addFn("cosh", fn = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = ObjReal(cosh(scp.args.firstAndOnly().toDouble())) })
addFn("tanh", fn = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = ObjReal(tanh(scp.args.firstAndOnly().toDouble())) })
addFn("asinh", fn = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = ObjReal(asinh(scp.args.firstAndOnly().toDouble())) })
addFn("acosh", fn = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = ObjReal(acosh(scp.args.firstAndOnly().toDouble())) })
addFn("atanh", fn = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = ObjReal(atanh(scp.args.firstAndOnly().toDouble())) })
addFn("exp") {
ObjReal(exp(args.firstAndOnly().toDouble()))
}
addFn("ln") {
ObjReal(ln(args.firstAndOnly().toDouble()))
}
addFn("exp", fn = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = ObjReal(exp(scp.args.firstAndOnly().toDouble())) })
addFn("ln", fn = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = ObjReal(ln(scp.args.firstAndOnly().toDouble())) })
addFn("log10") {
ObjReal(log10(args.firstAndOnly().toDouble()))
}
addFn("log10", fn = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = ObjReal(log10(scp.args.firstAndOnly().toDouble())) })
addFn("log2") {
ObjReal(log2(args.firstAndOnly().toDouble()))
}
addFn("log2", fn = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = ObjReal(log2(scp.args.firstAndOnly().toDouble())) })
addFn("pow") {
requireExactCount(2)
ObjReal(
(args[0].toDouble()).pow(args[1].toDouble())
)
}
addFn("sqrt") {
ObjReal(
sqrt(args.firstAndOnly().toDouble())
)
}
addFn("abs") {
val x = args.firstAndOnly()
if (x is ObjInt) ObjInt(x.value.absoluteValue) else ObjReal(x.toDouble().absoluteValue)
}
addFn("pow", fn = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
scp.requireExactCount(2)
return ObjReal((scp.args[0].toDouble()).pow(scp.args[1].toDouble()))
}
})
addFn("sqrt", fn = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = ObjReal(sqrt(scp.args.firstAndOnly().toDouble())) })
addFn("abs", fn = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val x = scp.args.firstAndOnly()
return if (x is ObjInt) ObjInt(x.value.absoluteValue) else ObjReal(x.toDouble().absoluteValue)
}
})
addVoidFn("assert") {
val cond = requiredArg<ObjBool>(0)
val message = if (args.size > 1)
": " + (args[1] as Statement).execute(this).toString(this).value
else ""
if (!cond.value == true)
raiseError(ObjAssertionFailedException(this, "Assertion failed$message"))
}
addFnDoc<Obj>(
"clamp",
doc = "Clamps the value within the specified range. If the value is outside the range, it is set to the nearest boundary. Respects inclusive/exclusive range ends.",
params = listOf(ParamDoc("value"), ParamDoc("range")),
moduleName = "lyng.stdlib",
fn = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val value = scp.requiredArg<Obj>(0)
val range = scp.requiredArg<ObjRange>(1)
addVoidFn("assertEquals") {
val a = requiredArg<Obj>(0)
val b = requiredArg<Obj>(1)
if (a.compareTo(this, b) != 0)
raiseError(
ObjAssertionFailedException(
this,
"Assertion failed: ${a.inspect(this)} == ${b.inspect(this)}"
var result = value
if (range.start != null && !range.start.isNull) {
if (result.compareTo(scp, range.start) < 0) {
result = range.start
}
}
if (range.end != null && !range.end.isNull) {
val cmp = range.end.compareTo(scp, result)
if (range.isEndInclusive) {
if (cmp < 0) result = range.end
} else {
if (cmp <= 0) {
if (range.end is ObjInt) {
result = ObjInt.of(range.end.value - 1)
} else if (range.end is ObjChar) {
result = ObjChar((range.end.value.code - 1).toChar())
} else {
// For types where we can't easily find "previous" value (like Real),
// we just return the exclusive boundary as a fallback.
result = range.end
}
}
}
}
return result
}
}
)
addVoidFn("assert", fn = object : VoidScopeCallable {
override suspend fun call(scp: Scope) {
val cond = scp.requiredArg<ObjBool>(0)
val message = if (scp.args.size > 1)
": " + (scp.args[1] as Statement).execute(scp).toString(scp).value
else ""
if (!cond.value == true)
scp.raiseError(ObjAssertionFailedException(scp, "Assertion failed$message"))
}
})
addVoidFn("assertEquals", fn = object : VoidScopeCallable {
override suspend fun call(scp: Scope) {
val a = scp.requiredArg<Obj>(0)
val b = scp.requiredArg<Obj>(1)
if (a.compareTo(scp, b) != 0)
scp.raiseError(
ObjAssertionFailedException(
scp,
"Assertion failed: ${a.inspect(scp)} == ${b.inspect(scp)}"
)
)
)
}
}
})
// alias used in tests
addVoidFn("assertEqual") {
val a = requiredArg<Obj>(0)
val b = requiredArg<Obj>(1)
if (a.compareTo(this, b) != 0)
raiseError(
ObjAssertionFailedException(
this,
"Assertion failed: ${a.inspect(this)} == ${b.inspect(this)}"
addVoidFn("assertEqual", fn = object : VoidScopeCallable {
override suspend fun call(scp: Scope) {
val a = scp.requiredArg<Obj>(0)
val b = scp.requiredArg<Obj>(1)
if (a.compareTo(scp, b) != 0)
scp.raiseError(
ObjAssertionFailedException(
scp,
"Assertion failed: ${a.inspect(scp)} == ${b.inspect(scp)}"
)
)
)
}
addVoidFn("assertNotEquals") {
val a = requiredArg<Obj>(0)
val b = requiredArg<Obj>(1)
if (a.compareTo(this, b) == 0)
raiseError(
ObjAssertionFailedException(
this,
"Assertion failed: ${a.inspect(this)} != ${b.inspect(this)}"
}
})
addVoidFn("assertNotEquals", fn = object : VoidScopeCallable {
override suspend fun call(scp: Scope) {
val a = scp.requiredArg<Obj>(0)
val b = scp.requiredArg<Obj>(1)
if (a.compareTo(scp, b) == 0)
scp.raiseError(
ObjAssertionFailedException(
scp,
"Assertion failed: ${a.inspect(scp)} != ${b.inspect(scp)}"
)
)
)
}
addFnDoc(
}
})
addFnDoc<Obj>(
"assertThrows",
doc = """
Asserts that the provided code block throws an exception, with or without exception:
@@ -216,83 +235,97 @@ class Script(
If an expected exception class is provided,
it checks that the thrown exception is of that class. If no expected class is provided, any exception
will be accepted.
""".trimIndent()
) {
val code: Statement
val expectedClass: ObjClass?
when (args.size) {
1 -> {
code = requiredArg<Statement>(0)
expectedClass = null
}
""".trimIndent(),
fn = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val code: Statement
val expectedClass: ObjClass?
when (scp.args.size) {
1 -> {
code = scp.requiredArg<Statement>(0)
expectedClass = null
}
2 -> {
code = requiredArg<Statement>(1)
expectedClass = requiredArg<ObjClass>(0)
}
2 -> {
code = scp.requiredArg<Statement>(1)
expectedClass = scp.requiredArg<ObjClass>(0)
}
else -> raiseIllegalArgument("Expected 1 or 2 arguments, got ${args.size}")
}
val result = try {
code.execute(this)
null
} catch (e: ExecutionError) {
e.errorObject
} catch (_: ScriptError) {
ObjNull
}
if (result == null) raiseError(
ObjAssertionFailedException(
this,
"Expected exception but nothing was thrown"
)
)
expectedClass?.let {
if (!result.isInstanceOf(it)) {
val actual = if (result is ObjException) result.exceptionClass else result.objClass
raiseError("Expected $it, got $actual")
else -> scp.raiseIllegalArgument("Expected 1 or 2 arguments, got ${scp.args.size}")
}
val result = try {
code.execute(scp)
null
} catch (e: ExecutionError) {
e.errorObject
} catch (_: ScriptError) {
ObjNull
}
if (result == null) scp.raiseError(
ObjAssertionFailedException(
scp,
"Expected exception but nothing was thrown"
)
)
expectedClass?.let {
if (!result.isInstanceOf(it)) {
val actual = if (result is ObjException) result.exceptionClass else result.objClass
scp.raiseError("Expected $it, got $actual")
}
}
return result ?: ObjNull
}
}
result
}
)
addFn("dynamic") {
ObjDynamic.create(this, requireOnlyArg())
}
addFn("dynamic", fn = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return ObjDynamic.create(scp, scp.requireOnlyArg())
}
})
val root = this
val mathClass = ObjClass("Math").apply {
addFn("sqrt") {
ObjReal(sqrt(args.firstAndOnly().toDouble()))
}
addFn("sqrt", fn = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return ObjReal(sqrt(scp.args.firstAndOnly().toDouble()))
}
})
}
addItem("Math", false, ObjInstance(mathClass).apply {
instanceScope = Scope(root, thisObj = this)
})
addFn("require") {
val condition = requiredArg<ObjBool>(0)
if (!condition.value) {
var message = args.list.getOrNull(1)
if (message is Statement) message = message.execute(this)
raiseIllegalArgument(message?.toString() ?: "requirement not met")
addFn("require", fn = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val condition = scp.requiredArg<ObjBool>(0)
if (!condition.value) {
var message = scp.args.list.getOrNull(1)
if (message is Statement) message = message.execute(scp)
scp.raiseIllegalArgument(message?.toString() ?: "requirement not met")
}
return ObjVoid
}
ObjVoid
}
addFn("check") {
val condition = requiredArg<ObjBool>(0)
if (!condition.value) {
var message = args.list.getOrNull(1)
if (message is Statement) message = message.execute(this)
raiseIllegalState(message?.toString() ?: "check failed")
})
addFn("check", fn = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val condition = scp.requiredArg<ObjBool>(0)
if (!condition.value) {
var message = scp.args.list.getOrNull(1)
if (message is Statement) message = message.execute(scp)
scp.raiseIllegalState(message?.toString() ?: "check failed")
}
return ObjVoid
}
ObjVoid
}
addFn("traceScope") {
this.trace(args.getOrNull(0)?.toString() ?: "")
ObjVoid
}
})
addFn("traceScope", fn = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
scp.trace(scp.args.getOrNull(0)?.toString() ?: "")
return ObjVoid
}
})
/*
addVoidFn("delay") {
val a = args.firstAndOnly()
when (a) {
@@ -302,7 +335,7 @@ class Script(
else -> raiseIllegalArgument("Expected Int, Real or Duration, got ${a.inspect(this)}")
}
}
*/
addConst("Object", rootObjectType)
addConst("Real", ObjReal.type)
addConst("String", ObjString.type)
@@ -329,7 +362,7 @@ class Script(
addConst("Flow", ObjFlow.type)
addConst("Regex", ObjRegex.type)
/*
addFn("launch") {
val callable = requireOnlyArg<Statement>()
ObjDeferred(globalDefer {
@@ -347,7 +380,7 @@ class Script(
// we'll need it for the producer
ObjFlow(requireOnlyArg<Statement>(), this)
}
*/
val pi = ObjReal(PI)
addConstDoc(
name = "π",
@@ -370,54 +403,69 @@ class Script(
addTextPackages(
rootLyng
)
addPackage("lyng.buffer") {
it.addConstDoc(
name = "Buffer",
value = ObjBuffer.type,
doc = "Immutable sequence of bytes. Use for binary data and IO.",
type = type("lyng.Class")
)
it.addConstDoc(
name = "MutableBuffer",
value = ObjMutableBuffer.type,
doc = "Mutable byte buffer. Supports in-place modifications.",
type = type("lyng.Class")
)
}
addPackage("lyng.serialization") {
it.addConstDoc(
name = "Lynon",
value = ObjLynonClass,
doc = "Lynon serialization utilities: encode/decode data structures to a portable binary/text form.",
type = type("lyng.Class")
)
}
addPackage("lyng.time") {
it.addConstDoc(
name = "Instant",
value = ObjInstant.type,
doc = "Point in time (epoch-based).",
type = type("lyng.Class")
)
it.addConstDoc(
name = "Duration",
value = ObjDuration.type,
doc = "Time duration with millisecond precision.",
type = type("lyng.Class")
)
it.addVoidFnDoc(
"delay",
doc = "Suspend for the given time. Accepts Duration, Int seconds, or Real seconds."
) {
val a = args.firstAndOnly()
when (a) {
is ObjInt -> delay(a.value * 1000)
is ObjReal -> delay((a.value * 1000).roundToLong())
is ObjDuration -> delay(a.duration)
else -> raiseIllegalArgument("Expected Duration, Int or Real, got ${a.inspect(this)}")
}
addPackage("lyng.buffer", object : ModuleBuilder {
override suspend fun build(ms: ModuleScope) {
ms.addConstDoc(
name = "Buffer",
value = ObjBuffer.type,
doc = "Immutable sequence of bytes. Use for binary data and IO.",
type = type("lyng.Class")
)
ms.addConstDoc(
name = "MutableBuffer",
value = ObjMutableBuffer.type,
doc = "Mutable byte buffer. Supports in-place modifications.",
type = type("lyng.Class")
)
}
}
})
addPackage("lyng.serialization", object : ModuleBuilder {
override suspend fun build(ms: ModuleScope) {
ms.addConstDoc(
name = "Lynon",
value = ObjLynonClass,
doc = "Lynon serialization utilities: encode/decode data structures to a portable binary/text form.",
type = type("lyng.Class")
)
}
})
addPackage("lyng.time", object : ModuleBuilder {
override suspend fun build(ms: ModuleScope) {
ms.addConstDoc(
name = "Instant",
value = ObjInstant.type,
doc = "Point in time (epoch-based).",
type = type("lyng.Class")
)
ms.addConstDoc(
name = "DateTime",
value = ObjDateTime.type,
doc = "Point in time in a specific time zone.",
type = type("lyng.Class")
)
ms.addConstDoc(
name = "Duration",
value = ObjDuration.type,
doc = "Time duration with millisecond precision.",
type = type("lyng.Class")
)
ms.addVoidFnDoc(
"delay",
doc = "Suspend for the given time. Accepts Duration, Int seconds, or Real seconds.",
fn = object : VoidScopeCallable {
override suspend fun call(scp: Scope) {
val a = scp.args.firstAndOnly()
when (a) {
is ObjInt -> delay(a.value * 1000)
is ObjReal -> delay((a.value * 1000).roundToLong())
is ObjDuration -> delay(a.duration)
else -> scp.raiseIllegalArgument("Expected Duration, Int or Real, got ${a.inspect(scp)}")
}
}
}
)
}
})
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* 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.
@@ -25,7 +25,12 @@ enum class Visibility {
}
/** MI-aware visibility check: whether [caller] can access a member declared in [decl] with [visibility]. */
fun canAccessMember(visibility: Visibility, decl: net.sergeych.lyng.obj.ObjClass?, caller: net.sergeych.lyng.obj.ObjClass?): Boolean {
fun canAccessMember(
visibility: Visibility,
decl: net.sergeych.lyng.obj.ObjClass?,
caller: net.sergeych.lyng.obj.ObjClass?,
name: String? = null
): Boolean {
val res = when (visibility) {
Visibility.Public -> true
Visibility.Private -> (decl != null && caller === decl)
@@ -33,7 +38,14 @@ fun canAccessMember(visibility: Visibility, decl: net.sergeych.lyng.obj.ObjClass
decl == null -> false
caller == null -> false
caller === decl -> true
else -> (caller.allParentsSet.contains(decl))
caller.allParentsSet.contains(decl) -> true
name != null && decl.allParentsSet.contains(caller) -> {
// Ancestor can access protected member of its descendant if it also has this member
// (i.e. it's an override of something the ancestor knows about)
val existing = caller.getInstanceMemberOrNull(name, includeAbstract = true)
existing != null && (existing.visibility == Visibility.Protected || existing.visibility == Visibility.Public)
}
else -> false
}
}
return res
@@ -108,8 +108,9 @@ object LyngFormatter {
if (t == "finally") return true
// Short definition form: fun x() = or val x =
if (Regex("^(override\\s+)?(fun|fn)\\b.*=\\s*$").matches(t)) return true
if (Regex("^(private\\s+|protected\\s+|public\\s+|override\\s+)?(val|var)\\b.*=\\s*$").matches(t)) return true
// We allow optional 'static' as well
if (Regex("^(static\\s+)?(override\\s+)?(fun|fn)\\b.*=\\s*$").matches(t)) return true
if (Regex("^(static\\s+)?(private\\s+|protected\\s+|public\\s+|override\\s+)?(val|var)\\b.*=\\s*$").matches(t)) return true
// property accessors ending with ) or =
if (isAccessorRelated(t)) {
@@ -122,7 +123,8 @@ object LyngFormatter {
val t = s.trim()
if (!t.endsWith("=")) return false
// Is it a function or property definition?
if (Regex("\\b(fun|fn|val|var)\\b").containsMatchIn(t)) return true
// (Note: we exclude 'static' here to avoid double indent if it's already handled)
if (Regex("^(override\\s+)?(fun|fn|val|var)\\b").containsMatchIn(t)) return true
// Is it an accessor?
if (isPropertyAccessor(t)) return true
return false
@@ -17,9 +17,7 @@
package net.sergeych.lyng.miniast
import net.sergeych.lyng.ModuleScope
import net.sergeych.lyng.Scope
import net.sergeych.lyng.Visibility
import net.sergeych.lyng.*
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.ObjVoid
@@ -39,10 +37,10 @@ inline fun <reified T : Obj> Scope.addFnDoc(
returns: TypeDoc? = null,
tags: Map<String, List<String>> = emptyMap(),
moduleName: String? = null,
crossinline fn: suspend Scope.() -> T
fn: ScopeCallable
) {
// Register runtime function(s)
addFn(*names) { fn() }
addFn(*names, fn = fn)
// Determine module
val mod = moduleName ?: findModuleNameOrUnknown()
// Register docs once per name
@@ -56,7 +54,7 @@ inline fun Scope.addVoidFnDoc(
doc: String,
tags: Map<String, List<String>> = emptyMap(),
moduleName: String? = null,
crossinline fn: suspend Scope.() -> Unit
fn: VoidScopeCallable
) {
addFnDoc<ObjVoid>(
*names,
@@ -64,11 +62,14 @@ inline fun Scope.addVoidFnDoc(
params = emptyList(),
returns = null,
tags = tags,
moduleName = moduleName
) {
fn(this)
ObjVoid
}
moduleName = moduleName,
fn = object : ScopeCallable {
override suspend fun call(sc: Scope): Obj {
fn.call(sc)
return ObjVoid
}
}
)
}
fun Scope.addConstDoc(
@@ -97,7 +98,7 @@ fun ObjClass.addFnDoc(
visibility: Visibility = Visibility.Public,
tags: Map<String, List<String>> = emptyMap(),
moduleName: String? = null,
code: suspend Scope.() -> Obj
code: ScopeCallable
) {
// Register runtime method
addFn(name, isOpen, visibility, code = code)
@@ -135,7 +136,7 @@ fun ObjClass.addClassFnDoc(
isOpen: Boolean = false,
tags: Map<String, List<String>> = emptyMap(),
moduleName: String? = null,
code: suspend Scope.() -> Obj
code: ScopeCallable
) {
addClassFn(name, isOpen, code)
BuiltinDocRegistry.module(moduleName ?: ownerModuleNameFromClassOrUnknown()) {
@@ -151,8 +152,8 @@ fun ObjClass.addPropertyDoc(
type: TypeDoc? = null,
visibility: Visibility = Visibility.Public,
moduleName: String? = null,
getter: (suspend Scope.() -> Obj)? = null,
setter: (suspend Scope.(Obj) -> Unit)? = null
getter: ScopeCallable? = null,
setter: ScopeCallable? = null
) {
addProperty(name, getter, setter, visibility)
BuiltinDocRegistry.module(moduleName ?: ownerModuleNameFromClassOrUnknown()) {
@@ -44,13 +44,22 @@ data class MiniDoc(
fun parse(range: MiniRange, lines: Iterable<String>, extraTags: Map<String, List<String>> = emptyMap()): MiniDoc {
val parsedTags = mutableMapOf<String, MutableList<String>>()
var currentTag: String? = null
val currentContent = StringBuilder()
val currentContent = mutableListOf<String>()
fun flush() {
currentTag?.let { tag ->
parsedTags.getOrPut(tag) { mutableListOf() }.add(currentContent.toString().trim())
val trimmedContent = if (currentContent.size > 1) {
// First line is common content, we want to trim indent from subsequent lines
// based on THEIR common indent.
val firstLine = currentContent[0]
val remaining = currentContent.drop(1).joinToString("\n").trimIndent()
if (remaining.isEmpty()) firstLine else firstLine + "\n" + remaining
} else {
currentContent.joinToString("\n")
}
parsedTags.getOrPut(tag) { mutableListOf() }.add(trimmedContent.trim())
}
currentContent.setLength(0)
currentContent.clear()
}
val descriptionLines = mutableListOf<String>()
@@ -58,33 +67,45 @@ data class MiniDoc(
for (rawLine in lines) {
for (line in rawLine.lines()) {
val trimmed = line.trim()
// Strip leading '*' and space if present (standard doc comment style)
val cleanedLine = line.trim().let {
if (it.startsWith("*")) it.substring(1).removePrefix(" ") else line
}
val trimmed = cleanedLine.trim()
if (trimmed.startsWith("@")) {
inTags = true
flush()
val parts = trimmed.substring(1).split(Regex("\\s+"), 2)
currentTag = parts[0]
currentContent.append(parts.getOrNull(1) ?: "")
val tagFirstLine = parts.getOrNull(1) ?: ""
if (tagFirstLine.isNotEmpty()) {
// Find where the content starts in the cleaned line to preserve relative indentation
val contentIndex = cleanedLine.indexOf(tagFirstLine)
if (contentIndex >= 0) {
currentContent.add(cleanedLine.substring(contentIndex))
} else {
currentContent.add(tagFirstLine)
}
}
} else {
if (inTags) {
if (currentContent.isNotEmpty()) currentContent.append("\n")
currentContent.append(line)
currentContent.add(cleanedLine)
} else {
descriptionLines.add(line)
descriptionLines.add(cleanedLine)
}
}
}
}
flush()
val raw = descriptionLines.joinToString("\n").trimEnd()
val raw = descriptionLines.joinToString("\n").trimIndent().trim()
val summary = raw.lines().firstOrNull { it.isNotBlank() }?.trim()
val finalTags = parsedTags.toMutableMap()
extraTags.forEach { (k, v) ->
finalTags.getOrPut(k) { mutableListOf() }.addAll(v)
}
return MiniDoc(range, raw, summary, finalTags)
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* 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.
@@ -32,29 +32,41 @@ typealias DocCompiler = Compiler
*/
typealias Accessor = ObjRef
interface AccessorGetter {
suspend fun call(scope: Scope): ObjRecord
}
interface AccessorSetter {
suspend fun call(scope: Scope, value: Obj)
}
/** Lambda-based reference for edge cases that still construct access via lambdas. */
private class LambdaRef(
private val getterFn: suspend (Scope) -> ObjRecord,
private val setterFn: (suspend (Pos, Scope, Obj) -> Unit)? = null
private val getterFn: AccessorGetter,
private val setterFn: AccessorSetter? = null
) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord = getterFn(scope)
override suspend fun get(scope: Scope): ObjRecord = getterFn.call(scope)
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
val s = setterFn ?: throw ScriptError(pos, "can't assign value")
s(pos, scope, newValue)
s.call(scope, newValue)
}
}
// Factory functions to preserve current call sites like `Accessor { ... }`
fun Accessor(getter: suspend (Scope) -> ObjRecord): Accessor = LambdaRef(getter)
fun Accessor(getter: AccessorGetter): Accessor = LambdaRef(getter)
fun Accessor(
getter: suspend (Scope) -> ObjRecord,
setter: suspend (Scope, Obj) -> Unit
): Accessor = LambdaRef(getter) { _, scope, value -> setter(scope, value) }
getter: AccessorGetter,
setter: AccessorSetter
): Accessor = LambdaRef(getter, setter)
// Compatibility shims used throughout Compiler: `.getter(...)` and `.setter(pos)`
val Accessor.getter: suspend (Scope) -> ObjRecord
get() = { scope -> this.get(scope) }
val Accessor.getter: AccessorGetter
get() = object : AccessorGetter {
override suspend fun call(scope: Scope): ObjRecord = this@getter.get(scope)
}
fun Accessor.setter(pos: Pos): suspend (Scope, Obj) -> Unit = { scope, newValue ->
this.setAt(pos, scope, newValue)
fun Accessor.setter(pos: Pos): AccessorSetter = object : AccessorSetter {
override suspend fun call(scope: Scope, value: Obj) {
this@setter.setAt(pos, scope, value)
}
}
@@ -32,6 +32,14 @@ import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType
fun interface OnNotFound {
suspend fun call(): Obj?
}
fun interface EnumerateCallback {
suspend fun call(element: Obj): Boolean
}
open class Obj {
open val isConst: Boolean = false
@@ -91,7 +99,7 @@ open class Obj {
scope: Scope,
name: String,
args: Arguments = Arguments.EMPTY,
onNotFoundResult: (suspend () -> Obj?)? = null
onNotFoundResult: OnNotFound? = null
): Obj {
// 0. Prefer private member of current class context
scope.currentClassCtx?.let { caller ->
@@ -100,7 +108,7 @@ open class Obj {
if (rec.type == ObjRecord.Type.Property) {
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, caller)
} else if (rec.type != ObjRecord.Type.Delegated) {
return rec.value.invoke(scope, this, args, caller)
return rec.value.invokeCallable(scope, this, args, caller)
}
}
}
@@ -113,13 +121,13 @@ open class Obj {
if (rec != null && !rec.isAbstract) {
val decl = rec.declaringClass ?: cls
val caller = scope.currentClassCtx
if (!canAccessMember(rec.visibility, decl, caller))
scope.raiseError(ObjIllegalAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})"))
if (!canAccessMember(rec.visibility, decl, caller, name))
scope.raiseError(ObjIllegalAccessException(scope, "can't invokeCallable ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})"))
if (rec.type == ObjRecord.Type.Property) {
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
} else if (rec.type != ObjRecord.Type.Delegated) {
return rec.value.invoke(scope, this, args, decl)
return rec.value.invokeCallable(scope, this, args, decl)
}
}
}
@@ -130,7 +138,7 @@ open class Obj {
if (extension.type == ObjRecord.Type.Property) {
if (args.isEmpty()) return (extension.value as ObjProperty).callGetter(scope, this, extension.declaringClass)
} else if (extension.type != ObjRecord.Type.Delegated) {
return extension.value.invoke(scope, this, args)
return extension.value.invokeCallable(scope, this, args)
}
}
@@ -140,19 +148,19 @@ open class Obj {
cls.members[name]?.let { rec ->
val decl = rec.declaringClass ?: cls
val caller = scope.currentClassCtx
if (!canAccessMember(rec.visibility, decl, caller))
scope.raiseError(ObjIllegalAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})"))
if (!canAccessMember(rec.visibility, decl, caller, name))
scope.raiseError(ObjIllegalAccessException(scope, "can't invokeCallable ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})"))
if (rec.type == ObjRecord.Type.Property) {
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
} else if (rec.type != ObjRecord.Type.Delegated) {
return rec.value.invoke(scope, this, args, decl)
return rec.value.invokeCallable(scope, this, args, decl)
}
}
}
}
return onNotFoundResult?.invoke()
return onNotFoundResult?.call()
?: scope.raiseError(
"no such member: $name on ${objClass.className}. Considered order: ${objClass.renderLinearization(true)}. " +
"Tip: try this@Base.$name(...) or (obj as Base).$name(...) if ambiguous"
@@ -174,9 +182,11 @@ open class Obj {
open suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other === this) return 0
if (other === ObjNull || other === ObjUnset || other === ObjVoid) return 2
return invokeInstanceMethod(scope, "compareTo", Arguments(other)) {
scope.raiseNotImplemented("compareTo for ${objClass.className}")
}.cast<ObjInt>(scope).toInt()
return invokeInstanceMethod(scope, "compareTo", Arguments(other), onNotFoundResult = object : OnNotFound {
override suspend fun call(): Obj? {
scope.raiseNotImplemented("compareTo for ${objClass.className}")
}
}).toInt()
}
open suspend fun equals(scope: Scope, other: Obj): Boolean {
@@ -202,16 +212,16 @@ open class Obj {
*
* IF callback returns false, iteration is stopped.
*/
open suspend fun enumerate(scope: Scope, callback: suspend (Obj) -> Boolean) {
open suspend fun enumerate(scope: Scope, callback: EnumerateCallback) {
val iterator = invokeInstanceMethod(scope, "iterator")
val hasNext = iterator.getInstanceMethod(scope, "hasNext")
val next = iterator.getInstanceMethod(scope, "next")
var closeIt = false
try {
while (hasNext.invoke(scope, iterator).toBool()) {
val nextValue = next.invoke(scope, iterator)
while (hasNext.invokeCallable(scope, iterator).toBool()) {
val nextValue = next.invokeCallable(scope, iterator)
val shouldContinue = try {
callback(nextValue)
callback.call(nextValue)
} catch (e: Exception) {
// iteration aborted due to exception in callback
closeIt = true
@@ -429,10 +439,10 @@ open class Obj {
}
/**
* Convert Lyng object to its Kotlin counterpart
* Convert a Lyng object to its Kotlin counterpart
*/
open suspend fun toKotlin(scope: Scope): Any? {
return toString()
return toString(scope).value
}
fun willMutate(scope: Scope) {
@@ -448,7 +458,7 @@ open class Obj {
if (rec.visibility == Visibility.Private && !rec.isAbstract) {
val resolved = resolveRecord(scope, rec, name, caller)
if (resolved.type == ObjRecord.Type.Fun && resolved.value is Statement)
return resolved.copy(value = resolved.value.invoke(scope, this, Arguments.EMPTY, caller))
return resolved.copy(value = resolved.value.invokeCallable(scope, this, Arguments.EMPTY, caller))
return resolved
}
}
@@ -462,7 +472,7 @@ open class Obj {
val decl = rec.declaringClass ?: cls
val resolved = resolveRecord(scope, rec, name, decl)
if (resolved.type == ObjRecord.Type.Fun && resolved.value is Statement)
return resolved.copy(value = resolved.value.invoke(scope, this, Arguments.EMPTY, decl))
return resolved.copy(value = resolved.value.invokeCallable(scope, this, Arguments.EMPTY, decl))
return resolved
}
}
@@ -472,7 +482,7 @@ open class Obj {
if (extension != null) {
val resolved = resolveRecord(scope, extension, name, extension.declaringClass)
if (resolved.type == ObjRecord.Type.Fun && resolved.value is Statement)
return resolved.copy(value = resolved.value.invoke(scope, this, Arguments.EMPTY, extension.declaringClass))
return resolved.copy(value = resolved.value.invokeCallable(scope, this, Arguments.EMPTY, extension.declaringClass))
return resolved
}
@@ -482,11 +492,11 @@ open class Obj {
cls.members[name]?.let { rec ->
val decl = rec.declaringClass ?: cls
val caller = scope.currentClassCtx
if (!canAccessMember(rec.visibility, decl, caller))
if (!canAccessMember(rec.visibility, decl, caller, name))
scope.raiseError(ObjIllegalAccessException(scope, "can't access field ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})"))
val resolved = resolveRecord(scope, rec, name, decl)
if (resolved.type == ObjRecord.Type.Fun && resolved.value is Statement)
return resolved.copy(value = resolved.value.invoke(scope, this, Arguments.EMPTY, decl))
return resolved.copy(value = resolved.value.invokeCallable(scope, this, Arguments.EMPTY, decl))
return resolved
}
}
@@ -502,13 +512,13 @@ open class Obj {
val del = obj.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate")
val th = if (this === ObjVoid) ObjNull else this
val res = del.invokeInstanceMethod(scope, "getValue", Arguments(th, ObjString(name)), onNotFoundResult = {
// If getValue not found, return a wrapper that calls invoke
// If getValue not found, return a wrapper that calls invokeCallable
object : Statement() {
override val pos: Pos = Pos.builtIn
override suspend fun execute(s: Scope): Obj {
val th2 = if (s.thisObj === ObjVoid) ObjNull else s.thisObj
val allArgs = (listOf(th2, ObjString(name)) + s.args.list).toTypedArray()
return del.invokeInstanceMethod(s, "invoke", Arguments(*allArgs))
return del.invokeInstanceMethod(s, "invokeCallable", Arguments(*allArgs))
}
}
})
@@ -526,7 +536,7 @@ open class Obj {
}
val caller = scope.currentClassCtx
// Check visibility for non-property members here if they weren't checked before
if (!canAccessMember(obj.visibility, decl, caller))
if (!canAccessMember(obj.visibility, decl, caller, name))
scope.raiseError(ObjIllegalAccessException(scope, "can't access field ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})"))
return obj
}
@@ -574,7 +584,7 @@ open class Obj {
val decl = field.declaringClass
val caller = scope.currentClassCtx
if (!canAccessMember(field.effectiveWriteVisibility, decl, caller))
if (!canAccessMember(field.effectiveWriteVisibility, decl, caller, name))
scope.raiseError(ObjIllegalAccessException(scope, "can't assign field ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})"))
if (field.type == ObjRecord.Type.Delegated) {
val del = field.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate")
@@ -605,18 +615,20 @@ open class Obj {
scope.raiseNotImplemented()
}
suspend fun invoke(scope: Scope, thisObj: Obj, args: Arguments, declaringClass: ObjClass? = null): Obj =
suspend fun invokeCallable(scope: Scope, thisObj: Obj, args: Arguments, declaringClass: ObjClass? = null): Obj =
if (PerfFlags.SCOPE_POOL)
scope.withChildFrame(args, newThisObj = thisObj) { child ->
if (declaringClass != null) child.currentClassCtx = declaringClass
callOn(child)
}
scope.withChildFrame(args, newThisObj = thisObj, block = object : ScopeBlock<Obj> {
override suspend fun call(child: Scope): Obj {
if (declaringClass != null) child.currentClassCtx = declaringClass
return callOn(child)
}
})
else
callOn(scope.createChildScope(scope.pos, args = args, newThisObj = thisObj).also {
if (declaringClass != null) it.currentClassCtx = declaringClass
})
suspend fun invoke(scope: Scope, thisObj: Obj, vararg args: Obj): Obj =
suspend fun invokeCallable(scope: Scope, thisObj: Obj, vararg args: Obj): Obj =
callOn(
scope.createChildScope(
scope.pos,
@@ -625,7 +637,7 @@ open class Obj {
)
)
suspend fun invoke(scope: Scope, thisObj: Obj): Obj =
suspend fun invokeCallable(scope: Scope, thisObj: Obj): Obj =
callOn(
scope.createChildScope(
scope.pos,
@@ -634,7 +646,7 @@ open class Obj {
)
)
suspend fun invoke(scope: Scope, atPos: Pos, thisObj: Obj, args: Arguments): Obj =
suspend fun invokeCallable(scope: Scope, atPos: Pos, thisObj: Obj, args: Arguments): Obj =
callOn(scope.createChildScope(atPos, args = args, newThisObj = thisObj))
@@ -681,85 +693,138 @@ open class Obj {
name = "toString",
doc = "Returns a string representation of the object.",
returns = type("lyng.String"),
moduleName = "lyng.stdlib"
) {
thisObj.toString(this, true)
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisObj.toString(scp, true)
}
)
addFnDoc(
name = "inspect",
doc = "Returns a detailed string representation for debugging.",
returns = type("lyng.String"),
moduleName = "lyng.stdlib"
) {
thisObj.inspect(this).toObj()
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisObj.inspect(scp).toObj()
}
)
addFnDoc(
name = "contains",
doc = "Returns true if the object contains the given element.",
params = listOf(ParamDoc("element")),
returns = type("lyng.Bool"),
moduleName = "lyng.stdlib"
) {
ObjBool(thisObj.contains(this, args.firstAndOnly()))
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjBool(scp.thisObj.contains(scp, scp.args.firstAndOnly()))
}
)
// utilities
addFnDoc(
name = "let",
doc = "Calls the specified function block with `this` value as its argument and returns its result.",
params = listOf(ParamDoc("block")),
moduleName = "lyng.stdlib"
) {
args.firstAndOnly().callOn(createChildScope(Arguments(thisObj)))
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj =
scp.args.firstAndOnly().callOn(scp.createChildScope(Arguments(scp.thisObj)))
}
)
addFnDoc(
name = "apply",
doc = "Calls the specified function block with `this` value as its receiver and returns `this` value.",
params = listOf(ParamDoc("block")),
moduleName = "lyng.stdlib"
) {
val body = args.firstAndOnly()
(thisObj as? ObjInstance)?.let {
body.callOn(ApplyScope(this, it.instanceScope))
} ?: run {
body.callOn(this)
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val body = scp.args.firstAndOnly()
(scp.thisObj as? ObjInstance)?.let {
body.callOn(ApplyScope(scp, it.instanceScope))
} ?: run {
body.callOn(scp)
}
return scp.thisObj
}
}
thisObj
}
)
addFnDoc(
name = "also",
doc = "Calls the specified function block with `this` value as its argument and returns `this` value.",
params = listOf(ParamDoc("block")),
moduleName = "lyng.stdlib"
) {
args.firstAndOnly().callOn(createChildScope(Arguments(thisObj)))
thisObj
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
scp.args.firstAndOnly().callOn(scp.createChildScope(Arguments(scp.thisObj)))
return scp.thisObj
}
}
)
addFnDoc(
name = "run",
doc = "Calls the specified function block with `this` value as its receiver and returns its result.",
params = listOf(ParamDoc("block")),
moduleName = "lyng.stdlib"
) {
args.firstAndOnly().callOn(this)
}
addFn("getAt") {
requireExactCount(1)
thisObj.getAt(this, requiredArg<Obj>(0))
}
addFn("putAt") {
requireExactCount(2)
val newValue = args[1]
thisObj.putAt(this, requiredArg<Obj>(0), newValue)
newValue
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.args.firstAndOnly().callOn(scp)
}
)
addFn("getAt", code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
scp.requireExactCount(1)
return scp.thisObj.getAt(scp, scp.requiredArg<Obj>(0))
}
})
addFn("putAt", code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
scp.requireExactCount(2)
val newValue = scp.args[1]
scp.thisObj.putAt(scp, scp.requiredArg<Obj>(0), newValue)
return newValue
}
})
addFnDoc(
name = "toJsonString",
doc = "Encodes this object to a JSON string.",
returns = type("lyng.String"),
moduleName = "lyng.stdlib"
) {
thisObj.toJson(this).toString().toObj()
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj =
scp.thisObj.toJson(scp).toString().toObj()
}
)
addFnDoc(
name = "clamp",
doc = "Clamps this value within the specified range. If the value is outside the range, it is set to the nearest boundary. Respects inclusive/exclusive range ends.",
params = listOf(ParamDoc("range")),
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val range = scp.requiredArg<ObjRange>(0)
var result = scp.thisObj
if (range.start != null && !range.start.isNull) {
if (result.compareTo(scp, range.start) < 0) {
result = range.start
}
}
if (range.end != null && !range.end.isNull) {
val cmp = range.end.compareTo(scp, result)
if (range.isEndInclusive) {
if (cmp < 0) result = range.end
} else {
if (cmp <= 0) {
if (range.end is ObjInt) {
result = ObjInt.of(range.end.value - 1)
} else if (range.end is ObjChar) {
result = ObjChar((range.end.value.code - 1).toChar())
} else {
result = range.end
}
}
}
}
return result
}
}
)
}
@@ -887,7 +952,7 @@ object ObjUnset : Obj() {
scope: Scope,
name: String,
args: Arguments,
onNotFoundResult: (suspend () -> Obj?)?
onNotFoundResult: OnNotFound?
): Obj = scope.raiseUnset()
override suspend fun getAt(scope: Scope, index: Obj): Obj = scope.raiseUnset()
@@ -17,6 +17,8 @@
package net.sergeych.lyng.obj
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeCallable
import net.sergeych.lyng.miniast.*
val ObjArray by lazy {
@@ -31,8 +33,11 @@ val ObjArray by lazy {
name = "iterator",
doc = "Iterator over elements of this array using its indexer.",
returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.Any"))),
moduleName = "lyng.stdlib"
) { ObjArrayIterator(thisObj).also { it.init(this) } }
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjArrayIterator(scp.thisObj).also { it.init(scp) }
}
)
addFnDoc(
name = "contains",
@@ -40,26 +45,31 @@ val ObjArray by lazy {
params = listOf(ParamDoc("element")),
returns = type("lyng.Bool"),
isOpen = true,
moduleName = "lyng.stdlib"
) {
val obj = args.firstAndOnly()
for (i in 0..<thisObj.invokeInstanceMethod(this, "size").toInt()) {
if (thisObj.getAt(this, ObjInt(i.toLong())).compareTo(this, obj) == 0) return@addFnDoc ObjTrue
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val obj = scp.args.firstAndOnly()
for (i in 0..<scp.thisObj.invokeInstanceMethod(scp, "size").toInt()) {
if (scp.thisObj.getAt(scp, ObjInt(i.toLong())).compareTo(scp, obj) == 0) return ObjTrue
}
return ObjFalse
}
}
ObjFalse
}
)
addPropertyDoc(
name = "last",
doc = "The last element of this array.",
type = type("lyng.Any"),
moduleName = "lyng.stdlib",
getter = {
this.thisObj.invokeInstanceMethod(
this,
"getAt",
(this.thisObj.invokeInstanceMethod(this, "size").toInt() - 1).toObj()
)
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return scp.thisObj.invokeInstanceMethod(
scp,
"getAt",
(scp.thisObj.invokeInstanceMethod(scp, "size").toInt() - 1).toObj()
)
}
}
)
@@ -68,7 +78,9 @@ val ObjArray by lazy {
doc = "Index of the last element (size - 1).",
type = type("lyng.Int"),
moduleName = "lyng.stdlib",
getter = { (this.thisObj.invokeInstanceMethod(this, "size").toInt() - 1).toObj() }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = (scp.thisObj.invokeInstanceMethod(scp, "size").toInt() - 1).toObj()
}
)
addPropertyDoc(
@@ -76,7 +88,9 @@ val ObjArray by lazy {
doc = "Range of valid indices for this array.",
type = type("lyng.Range"),
moduleName = "lyng.stdlib",
getter = { ObjRange(0.toObj(), this.thisObj.invokeInstanceMethod(this, "size"), false) }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjRange(0.toObj(), scp.thisObj.invokeInstanceMethod(scp, "size"), false)
}
)
addFnDoc(
@@ -84,25 +98,28 @@ val ObjArray by lazy {
doc = "Binary search for a target in a sorted array. Returns index or negative insertion point - 1.",
params = listOf(ParamDoc("target")),
returns = type("lyng.Int"),
moduleName = "lyng.stdlib"
) {
val target = args.firstAndOnly()
var low = 0
var high = thisObj.invokeInstanceMethod(this, "size").toInt() - 1
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val target = scp.args.firstAndOnly()
var low = 0
var high = scp.thisObj.invokeInstanceMethod(scp, "size").toInt() - 1
while (low <= high) {
val mid = (low + high) / 2
val midVal = thisObj.getAt(this, ObjInt(mid.toLong()))
while (low <= high) {
val mid = (low + high) / 2
val midVal = scp.thisObj.getAt(scp, ObjInt(mid.toLong()))
val cmp = midVal.compareTo(this, target)
when {
cmp == 0 -> return@addFnDoc (mid).toObj()
cmp > 0 -> high = mid - 1
else -> low = mid + 1
val cmp = midVal.compareTo(scp, target)
when {
cmp == 0 -> return (mid).toObj()
cmp > 0 -> high = mid - 1
else -> low = mid + 1
}
}
return (-low - 1).toObj()
}
}
(-low - 1).toObj()
}
)
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* 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.
@@ -18,6 +18,7 @@
package net.sergeych.lyng.obj
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeCallable
class ObjArrayIterator(val array: Obj) : Obj() {
@@ -35,16 +36,20 @@ class ObjArrayIterator(val array: Obj) : Obj() {
companion object {
val type by lazy {
ObjClass("ArrayIterator", ObjIterator).apply {
addFn("next") {
val self = thisAs<ObjArrayIterator>()
if (self.nextIndex < self.lastIndex) {
self.array.invokeInstanceMethod(this, "getAt", (self.nextIndex++).toObj())
} else raiseError(ObjIterationFinishedException(this))
}
addFn("hasNext") {
val self = thisAs<ObjArrayIterator>()
if (self.nextIndex < self.lastIndex) ObjTrue else ObjFalse
}
addFn("next", code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val self = scp.thisAs<ObjArrayIterator>()
return if (self.nextIndex < self.lastIndex) {
self.array.invokeInstanceMethod(scp, "getAt", (self.nextIndex++).toObj())
} else scp.raiseError(ObjIterationFinishedException(scp))
}
})
addFn("hasNext", code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val self = scp.thisAs<ObjArrayIterator>()
return if (self.nextIndex < self.lastIndex) ObjTrue else ObjFalse
}
})
}
}
}
@@ -19,6 +19,7 @@ package net.sergeych.lyng.obj
import net.sergeych.bintools.toDump
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeCallable
import net.sergeych.lyng.miniast.addPropertyDoc
import net.sergeych.lyng.miniast.type
import net.sergeych.lynon.BitArray
@@ -35,29 +36,37 @@ class ObjBitBuffer(val bitArray: BitArray) : Obj() {
val type = object: ObjClass("BitBuffer", ObjArray) {
}.apply {
addFn("toBuffer") {
requireNoArgs()
ObjBuffer(thisAs<ObjBitBuffer>().bitArray.asUByteArray())
}
addFn("toDump") {
requireNoArgs()
ObjString(
thisAs<ObjBitBuffer>().bitArray.asUByteArray().toDump()
)
}
addFn("toBuffer", code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
scp.requireNoArgs()
return ObjBuffer(scp.thisAs<ObjBitBuffer>().bitArray.asUByteArray())
}
})
addFn("toDump", code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
scp.requireNoArgs()
return ObjString(
scp.thisAs<ObjBitBuffer>().bitArray.asUByteArray().toByteArray().toDump()
)
}
})
addPropertyDoc(
name = "size",
doc = "Size of the bit buffer in bits.",
type = type("lyng.Int"),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjBitBuffer>().bitArray.size.toObj() }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjBitBuffer>().bitArray.size.toObj()
}
)
addPropertyDoc(
name = "sizeInBytes",
doc = "Size of the bit buffer in full bytes (rounded up).",
type = type("lyng.Int"),
moduleName = "lyng.stdlib",
getter = { ObjInt((thisAs<ObjBitBuffer>().bitArray.size + 7) shr 3) }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjInt(((scp.thisAs<ObjBitBuffer>().bitArray.size + 7) shr 3).toLong())
}
)
}
}
@@ -23,6 +23,8 @@ import net.sergeych.bintools.decodeHex
import net.sergeych.bintools.encodeToHex
import net.sergeych.bintools.toDump
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeCallable
import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.addPropertyDoc
import net.sergeych.lyng.miniast.type
import net.sergeych.lynon.BitArray
@@ -169,51 +171,85 @@ open class ObjBuffer(val byteArray: UByteArray) : Obj() {
})
}.apply {
addClassFn("decodeBase64") {
ObjBuffer(requireOnlyArg<Obj>().toString().decodeBase64Url().asUByteArray())
}
addClassFn("decodeHex") {
ObjBuffer(requireOnlyArg<Obj>().toString().decodeHex().asUByteArray())
}
addClassFn("decodeBase64", code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj =
ObjBuffer(scp.requireOnlyArg<Obj>().toString().decodeBase64Url().asUByteArray())
})
addClassFn("decodeHex", code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj =
ObjBuffer(scp.requireOnlyArg<Obj>().toString().decodeHex().asUByteArray())
})
addPropertyDoc(
name = "size",
doc = "Number of bytes in this buffer.",
type = type("lyng.Int"),
moduleName = "lyng.stdlib",
getter = { (this.thisObj as ObjBuffer).byteArray.size.toObj() }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = (scp.thisObj as ObjBuffer).byteArray.size.toObj()
}
)
addPropertyDoc(
name = "hex",
doc = "Hexadecimal string representation of the buffer.",
type = type("lyng.String"),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjBuffer>().hex.toObj() }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjBuffer>().hex.toObj()
}
)
addPropertyDoc(
name = "base64",
doc = "Base64 (URL-safe) string representation of the buffer.",
type = type("lyng.String"),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjBuffer>().base64.toObj() }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjBuffer>().base64.toObj()
}
)
addFnDoc(
name = "decodeUtf8",
doc = "Decode the buffer content as a UTF-8 string.",
returns = type("lyng.String"),
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj =
ObjString(scp.thisAs<ObjBuffer>().byteArray.toByteArray().decodeToString())
}
)
addFnDoc(
name = "toMutable",
doc = "Return a mutable copy of this buffer.",
returns = type("lyng.MutableBuffer"),
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
scp.requireNoArgs()
return ObjMutableBuffer(scp.thisAs<ObjBuffer>().byteArray.copyOf())
}
}
)
addFnDoc(
name = "toDump",
doc = "Return a hexadecimal dump string of the buffer.",
returns = type("lyng.String"),
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
scp.requireNoArgs()
return ObjString(scp.thisAs<ObjBuffer>().byteArray.toByteArray().toDump())
}
}
)
addFnDoc(
name = "toBitInput",
doc = "Return a bit buffer for reading bits from this buffer.",
returns = type("lyng.BitBuffer"),
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj =
ObjBitBuffer(BitArray(scp.thisAs<ObjBuffer>().byteArray, 8))
}
)
addFn("decodeUtf8") {
ObjString(
thisAs<ObjBuffer>().byteArray.toByteArray().decodeToString()
)
}
addFn("toMutable") {
requireNoArgs()
ObjMutableBuffer(thisAs<ObjBuffer>().byteArray.copyOf())
}
addFn("toDump") {
requireNoArgs()
ObjString(
thisAs<ObjBuffer>().byteArray.toByteArray().toDump()
)
}
addFn("toBitInput") {
ObjBitBuffer(BitArray(thisAs<ObjBuffer>().byteArray, 8))
}
}
}
}
@@ -18,6 +18,7 @@
package net.sergeych.lyng.obj
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeCallable
import net.sergeych.lyng.miniast.addPropertyDoc
import net.sergeych.lyng.miniast.type
@@ -52,14 +53,16 @@ class ObjChar(val value: Char): Obj() {
doc = "Unicode code point (UTF-16 code unit) of this character.",
type = type("lyng.Int"),
moduleName = "lyng.stdlib",
getter = { ObjInt((this.thisObj as ObjChar).value.code.toLong()) }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjInt((scp.thisObj as ObjChar).value.code.toLong())
}
)
addFn("isDigit") {
thisAs<ObjChar>().value.isDigit().toObj()
}
addFn("isSpace") {
thisAs<ObjChar>().value.isWhitespace().toObj()
}
addFn("isDigit", code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjChar>().value.isDigit().toObj()
})
addFn("isSpace", code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjChar>().value.isWhitespace().toObj()
})
}
}
}
@@ -17,29 +17,41 @@
package net.sergeych.lyng.obj
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import net.sergeych.lyng.*
import net.sergeych.lyng.miniast.*
import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType
// Simple id generator for class identities (not thread-safe; fine for scripts)
private object ClassIdGen { var c: Long = 1L; fun nextId(): Long = c++ }
val ObjClassType by lazy {
ObjClass("Class").apply {
object : ObjClass("Class") {
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
val name = decoder.decodeObject(scope, ObjString.type, null) as ObjString
return scope.resolveQualifiedIdentifier(name.value)
}
}.apply {
addPropertyDoc(
name = "className",
doc = "Full name of this class including package if available.",
type = type("lyng.String"),
moduleName = "lyng.stdlib",
getter = { (this.thisObj as ObjClass).classNameObj }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = (scp.thisObj as ObjClass).classNameObj
}
)
addPropertyDoc(
name = "name",
doc = "Simple name of this class (without package).",
type = type("lyng.String"),
moduleName = "lyng.stdlib",
getter = { (this.thisObj as ObjClass).classNameObj }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = (scp.thisObj as ObjClass).classNameObj
}
)
addPropertyDoc(
@@ -47,16 +59,18 @@ val ObjClassType by lazy {
doc = "Declared instance fields of this class and its ancestors (C3 order), without duplicates.",
type = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.String"))),
moduleName = "lyng.stdlib",
getter = {
val cls = this.thisObj as ObjClass
val seen = hashSetOf<String>()
val names = mutableListOf<Obj>()
for (c in cls.mro) {
for ((n, rec) in c.members) {
if (rec.value !is Statement && seen.add(n)) names += ObjString(n)
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val cls = scp.thisObj as ObjClass
val seen = hashSetOf<String>()
val names = mutableListOf<Obj>()
for (c in cls.mro) {
for ((n, rec) in c.members) {
if (rec.value !is Statement && seen.add(n)) names += ObjString(n)
}
}
return ObjList(names.toMutableList())
}
ObjList(names.toMutableList())
}
)
@@ -65,16 +79,18 @@ val ObjClassType by lazy {
doc = "Declared instance methods of this class and its ancestors (C3 order), without duplicates.",
type = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.String"))),
moduleName = "lyng.stdlib",
getter = {
val cls = this.thisObj as ObjClass
val seen = hashSetOf<String>()
val names = mutableListOf<Obj>()
for (c in cls.mro) {
for ((n, rec) in c.members) {
if (rec.value is Statement && seen.add(n)) names += ObjString(n)
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val cls = scp.thisObj as ObjClass
val seen = hashSetOf<String>()
val names = mutableListOf<Obj>()
for (c in cls.mro) {
for ((n, rec) in c.members) {
if (rec.value is Statement && seen.add(n)) names += ObjString(n)
}
}
return ObjList(names.toMutableList())
}
ObjList(names.toMutableList())
}
)
@@ -83,13 +99,16 @@ val ObjClassType by lazy {
doc = "Lookup a member by name in this class (including ancestors) and return it, or null if absent.",
params = listOf(ParamDoc("name", type("lyng.String"))),
returns = type("lyng.Any", nullable = true),
moduleName = "lyng.stdlib"
) {
val cls = thisAs<ObjClass>()
val name = requiredArg<ObjString>(0).value
val rec = cls.getInstanceMemberOrNull(name)
rec?.value ?: ObjNull
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val cls = scp.thisAs<ObjClass>()
val name = scp.requiredArg<ObjString>(0).value
val rec = cls.getInstanceMemberOrNull(name)
return rec?.value ?: ObjNull
}
}
)
}
}
@@ -451,6 +470,7 @@ open class ObjClass(
isAbstract: Boolean = false,
isClosed: Boolean = false,
isOverride: Boolean = false,
isTransient: Boolean = false,
type: ObjRecord.Type = ObjRecord.Type.Field,
): ObjRecord {
// Validation of override rules: only for non-system declarations
@@ -461,7 +481,7 @@ open class ObjClass(
if (existing != null && existing.declaringClass != this) {
// If the existing member is private in the ancestor, it's not visible for overriding.
// It should be treated as a new member in this class.
if (!existing.visibility.isPublic && !canAccessMember(existing.visibility, existing.declaringClass, this)) {
if (!existing.visibility.isPublic && !canAccessMember(existing.visibility, existing.declaringClass, this, name)) {
// It's effectively not there for us, so actualOverride remains false
} else {
actualOverride = true
@@ -494,6 +514,7 @@ open class ObjClass(
isAbstract = isAbstract,
isClosed = isClosed,
isOverride = isOverride,
isTransient = isTransient,
type = type
)
members[name] = rec
@@ -514,13 +535,14 @@ open class ObjClass(
visibility: Visibility = Visibility.Public,
writeVisibility: Visibility? = null,
pos: Pos = Pos.builtIn,
isTransient: Boolean = false,
type: ObjRecord.Type = ObjRecord.Type.Field
): ObjRecord {
initClassScope()
val existing = classScope!!.objects[name]
if (existing != null)
throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
val rec = classScope!!.addItem(name, isMutable, initialValue, visibility, writeVisibility, recordType = type)
val rec = classScope!!.addItem(name, isMutable, initialValue, visibility, writeVisibility, recordType = type, isTransient = isTransient)
// Structural change: bump layout version for PIC invalidation
layoutVersion += 1
return rec
@@ -536,9 +558,9 @@ open class ObjClass(
isClosed: Boolean = false,
isOverride: Boolean = false,
pos: Pos = Pos.builtIn,
code: (suspend Scope.() -> Obj)? = null
code: ScopeCallable? = null
) {
val stmt = code?.let { statement { it() } } ?: ObjNull
val stmt = code?.let { statement(pos, f = it) } ?: ObjNull
createField(
name, stmt, isMutable, visibility, writeVisibility, pos, declaringClass,
isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride,
@@ -550,8 +572,8 @@ open class ObjClass(
fun addProperty(
name: String,
getter: (suspend Scope.() -> Obj)? = null,
setter: (suspend Scope.(Obj) -> Unit)? = null,
getter: ScopeCallable? = null,
setter: ScopeCallable? = null,
visibility: Visibility = Visibility.Public,
writeVisibility: Visibility? = null,
declaringClass: ObjClass? = this,
@@ -561,8 +583,8 @@ open class ObjClass(
pos: Pos = Pos.builtIn,
prop: ObjProperty? = null
) {
val g = getter?.let { statement { it() } }
val s = setter?.let { statement { it(requiredArg(0)); ObjVoid } }
val g = getter?.let { statement(pos, f = it) }
val s = setter?.let { statement(pos, f = it) }
val finalProp = prop ?: if (isAbstract) ObjNull else ObjProperty(name, g, s)
createField(
name, finalProp, false, visibility, writeVisibility, pos, declaringClass,
@@ -572,8 +594,8 @@ open class ObjClass(
}
fun addClassConst(name: String, value: Obj) = createClassField(name, value)
fun addClassFn(name: String, isOpen: Boolean = false, code: suspend Scope.() -> Obj) {
createClassField(name, statement { code() }, isOpen, type = ObjRecord.Type.Fun)
fun addClassFn(name: String, isOpen: Boolean = false, code: ScopeCallable) {
createClassField(name, statement(f = code), isOpen, type = ObjRecord.Type.Fun)
}
@@ -683,30 +705,46 @@ open class ObjClass(
override suspend fun invokeInstanceMethod(
scope: Scope, name: String, args: Arguments,
onNotFoundResult: (suspend () -> Obj?)?
onNotFoundResult: OnNotFound?
): Obj {
getInstanceMemberOrNull(name)?.let { rec ->
val decl = rec.declaringClass ?: findDeclaringClassOf(name) ?: this
if (rec.type == ObjRecord.Type.Delegated) {
val del = rec.delegate ?: scope.raiseError("Internal error: delegated member $name has no delegate")
val allArgs = (listOf(this, ObjString(name)) + args.list).toTypedArray()
return del.invokeInstanceMethod(scope, "invoke", Arguments(*allArgs), onNotFoundResult = {
return del.invokeInstanceMethod(scope, "invokeCallable", Arguments(*allArgs), onNotFoundResult = {
// Fallback: property delegation
val propVal = del.invokeInstanceMethod(scope, "getValue", Arguments(this, ObjString(name)))
propVal.invoke(scope, this, args, decl)
propVal.invokeCallable(scope, this, args, decl)
})
}
if (rec.type == ObjRecord.Type.Fun) {
return rec.value.invoke(scope, this, args, decl)
return rec.value.invokeCallable(scope, this, args, decl)
} else {
// Resolved field or property value
val resolved = readField(scope, name)
return resolved.value.invoke(scope, this, args, decl)
return resolved.value.invokeCallable(scope, this, args, decl)
}
}
return super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
}
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
if (isAnonymous) scope.raiseError("Cannot serialize anonymous class")
encoder.encodeObject(scope, classNameObj, ObjString.type.lynonType())
}
override suspend fun toJson(scope: Scope): JsonElement {
val result = mutableMapOf<String, JsonElement>()
result["__class_name"] = classNameObj.toJson(scope)
classScope?.objects?.forEach { (name, rec) ->
if (rec.type.serializable && rec.visibility.isPublic && !rec.isTransient) {
result[name] = rec.value.toJson(scope)
}
}
return JsonObject(result)
}
open suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
scope.raiseNotImplemented()
@@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* 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.
@@ -19,6 +19,7 @@ package net.sergeych.lyng.obj
import kotlinx.coroutines.CompletableDeferred
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeCallable
import net.sergeych.lyng.miniast.ParamDoc
import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.type
@@ -38,11 +39,14 @@ class ObjCompletableDeferred(val completableDeferred: CompletableDeferred<Obj>):
doc = "Complete this deferred with the given value. Subsequent calls have no effect.",
params = listOf(ParamDoc("value")),
returns = type("lyng.Void"),
moduleName = "lyng.stdlib"
) {
thisAs<ObjCompletableDeferred>().completableDeferred.complete(args.firstAndOnly())
ObjVoid
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
scp.thisAs<ObjCompletableDeferred>().completableDeferred.complete(scp.args.firstAndOnly())
return ObjVoid
}
}
)
}
}
}
@@ -0,0 +1,303 @@
/*
* 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 kotlinx.datetime.*
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeCallable
import net.sergeych.lyng.Statement
import net.sergeych.lyng.miniast.addClassFnDoc
import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.addPropertyDoc
import net.sergeych.lyng.miniast.type
import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType
class ObjDateTime(val instant: Instant, val timeZone: TimeZone) : Obj() {
override val objClass: ObjClass get() = type
val localDateTime: LocalDateTime by lazy {
instant.toLocalDateTime(timeZone)
}
override fun toString(): String {
return localDateTime.toString() + timeZone.toString()
}
override suspend fun readField(scope: Scope, name: String): ObjRecord {
for (cls in objClass.mro) {
val rec = cls.members[name]
if (rec != null) {
if (rec.type == ObjRecord.Type.Property) {
val prop = rec.value as? ObjProperty
?: (rec.value as? Statement)?.execute(scope) as? ObjProperty
if (prop != null) {
return ObjRecord(prop.callGetter(scope, this, rec.declaringClass ?: cls), rec.isMutable)
}
}
if (rec.type == ObjRecord.Type.Fun || rec.value is Statement) {
val s = rec.value as Statement
return ObjRecord(net.sergeych.lyng.statement(f = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = s.execute(scp.createChildScope(newThisObj = this@ObjDateTime))
}), rec.isMutable)
}
return resolveRecord(scope, rec, name, rec.declaringClass ?: cls)
}
}
return super.readField(scope, name)
}
override suspend fun plus(scope: Scope, other: Obj): Obj {
return when (other) {
is ObjDuration -> ObjDateTime(instant + other.duration, timeZone)
else -> super.plus(scope, other)
}
}
override suspend fun minus(scope: Scope, other: Obj): Obj {
return when (other) {
is ObjDuration -> ObjDateTime(instant - other.duration, timeZone)
is ObjDateTime -> ObjDuration(instant - other.instant)
is ObjInstant -> ObjDuration(instant - other.instant)
else -> super.minus(scope, other)
}
}
override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other is ObjDateTime) {
return instant.compareTo(other.instant)
}
if (other is ObjInstant) {
return instant.compareTo(other.instant)
}
return super.compareTo(scope, other)
}
override suspend fun toKotlin(scope: Scope): Any {
return localDateTime
}
override fun hashCode(): Int {
return instant.hashCode() xor timeZone.hashCode()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ObjDateTime) return false
return instant == other.instant && timeZone == other.timeZone
}
override suspend fun lynonType(): LynonType = LynonType.Other
override suspend fun serialize(scope: Scope, encoder: LynonEncoder, lynonType: LynonType?) {
encoder.encodeCached(this) {
encodeAny(scope, ObjInstant(instant))
encodeCached(timeZone.id) {
encodeBinaryData(timeZone.id.encodeToByteArray())
}
}
}
override suspend fun toJson(scope: Scope): JsonElement = JsonPrimitive(toRFC3339())
fun toRFC3339(): String {
val s = localDateTime.toString()
val tz = if (timeZone == TimeZone.UTC) "Z" else timeZone.id
return if (tz.startsWith("+") || tz.startsWith("-") || tz == "Z") s + tz else s + "[" + tz + "]"
}
companion object {
val type = object : ObjClass("DateTime") {
override suspend fun callOn(scope: Scope): Obj {
val args = scope.args
return when (val a0 = args.list.getOrNull(0)) {
is ObjInstant -> {
val tz = when (val a1 = args.list.getOrNull(1)) {
null -> TimeZone.currentSystemDefault()
is ObjString -> TimeZone.of(a1.value)
is ObjInt -> UtcOffset(seconds = a1.value.toInt()).asTimeZone()
else -> scope.raiseIllegalArgument("invalid timezone: $a1")
}
ObjDateTime(a0.instant, tz)
}
is ObjInt -> {
// DateTime(year, month, day, hour=0, minute=0, second=0, timeZone="UTC")
val year = a0.value.toInt()
val month = args.list.getOrNull(1)?.toInt() ?: scope.raiseIllegalArgument("month is required")
val day = args.list.getOrNull(2)?.toInt() ?: scope.raiseIllegalArgument("day is required")
val hour = args.list.getOrNull(3)?.toInt() ?: 0
val minute = args.list.getOrNull(4)?.toInt() ?: 0
val second = args.list.getOrNull(5)?.toInt() ?: 0
val tz = when (val a6 = args.list.getOrNull(6)) {
null -> TimeZone.UTC
is ObjString -> TimeZone.of(a6.value)
is ObjInt -> UtcOffset(seconds = a6.value.toInt()).asTimeZone()
else -> scope.raiseIllegalArgument("invalid timezone: $a6")
}
val ldt = LocalDateTime(year, month, day, hour, minute, second)
ObjDateTime(ldt.toInstant(tz), tz)
}
is ObjString -> {
val instant = Instant.parse(a0.value)
ObjDateTime(instant, TimeZone.UTC)
}
else -> scope.raiseIllegalArgument("can't construct DateTime from ${args.inspect(scope)}")
}
}
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
return decoder.decodeCached {
val instant = (decoder.decodeAny(scope) as ObjInstant).instant
val tzId = decoder.decodeCached { decoder.unpackBinaryData().decodeToString() }
ObjDateTime(instant, TimeZone.of(tzId))
}
}
}.apply {
addPropertyDoc("year", "The year component.", type("lyng.Int"), moduleName = "lyng.time",
getter = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjDateTime>().localDateTime.year.toObj() })
addPropertyDoc("month", "The month component (1..12).", type("lyng.Int"), moduleName = "lyng.time",
getter = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjDateTime>().localDateTime.monthNumber.toObj() })
addPropertyDoc("dayOfMonth", "The day of month component.", type("lyng.Int"), moduleName = "lyng.time",
getter = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjDateTime>().localDateTime.dayOfMonth.toObj() })
addPropertyDoc("day", "Alias to dayOfMonth.", type("lyng.Int"), moduleName = "lyng.time",
getter = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjDateTime>().localDateTime.dayOfMonth.toObj() })
addPropertyDoc("hour", "The hour component (0..23).", type("lyng.Int"), moduleName = "lyng.time",
getter = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjDateTime>().localDateTime.hour.toObj() })
addPropertyDoc("minute", "The minute component (0..59).", type("lyng.Int"), moduleName = "lyng.time",
getter = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjDateTime>().localDateTime.minute.toObj() })
addPropertyDoc("second", "The second component (0..59).", type("lyng.Int"), moduleName = "lyng.time",
getter = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjDateTime>().localDateTime.second.toObj() })
addPropertyDoc("dayOfWeek", "The day of week (1=Monday, 7=Sunday).", type("lyng.Int"), moduleName = "lyng.time",
getter = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjDateTime>().localDateTime.dayOfWeek.isoDayNumber.toObj() })
addPropertyDoc("timeZone", "The time zone ID (e.g. 'Z', '+02:00', 'Europe/Prague').", type("lyng.String"), moduleName = "lyng.time",
getter = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjDateTime>().timeZone.id.toObj() })
addFnDoc("toInstant", "Convert this localized date time back to an absolute Instant.", returns = type("lyng.Instant"), moduleName = "lyng.time",
code = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = ObjInstant(scp.thisAs<ObjDateTime>().instant) })
addFnDoc("toEpochSeconds", "Return the number of full seconds since the Unix epoch (UTC).", returns = type("lyng.Int"), moduleName = "lyng.time",
code = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjDateTime>().instant.epochSeconds.toObj() })
addFnDoc("toRFC3339", "Return the RFC3339 string representation of this date time, including its timezone offset.", returns = type("lyng.String"), moduleName = "lyng.time",
code = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjDateTime>().toRFC3339().toObj() })
addFnDoc("toSortableString", "Alias to toRFC3339.", returns = type("lyng.String"), moduleName = "lyng.time",
code = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjDateTime>().toRFC3339().toObj() })
addFnDoc("toEpochMilliseconds", "Return the number of milliseconds since the Unix epoch (UTC).", returns = type("lyng.Int"), moduleName = "lyng.time",
code = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjDateTime>().instant.toEpochMilliseconds().toObj() })
addFnDoc("toTimeZone", "Return a new DateTime representing the same instant but in a different time zone. " +
"Accepts a timezone ID string (e.g., 'UTC', '+02:00') or an integer offset in seconds.",
params = listOf(net.sergeych.lyng.miniast.ParamDoc("tz", type = type("lyng.Any"))),
returns = type("lyng.DateTime"), moduleName = "lyng.time",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val tz = when (val a = scp.args.list.getOrNull(0)) {
is ObjString -> TimeZone.of(a.value)
is ObjInt -> UtcOffset(seconds = a.value.toInt()).asTimeZone()
else -> scp.raiseIllegalArgument("invalid timezone: $a")
}
return ObjDateTime(scp.thisAs<ObjDateTime>().instant, tz)
}
})
addFnDoc("toUTC", "Shortcut to convert this date time to the UTC time zone.", returns = type("lyng.DateTime"), moduleName = "lyng.time",
code = object : ScopeCallable { override suspend fun call(scp: Scope): Obj = ObjDateTime(scp.thisAs<ObjDateTime>().instant, TimeZone.UTC) })
addFnDoc("addMonths", "Return a new DateTime with the specified number of months added (or subtracted if negative). " +
"Normalizes the day of month if necessary (e.g., Jan 31 + 1 month = Feb 28/29).",
params = listOf(net.sergeych.lyng.miniast.ParamDoc("months", type = type("lyng.Int"))),
returns = type("lyng.DateTime"), moduleName = "lyng.time",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val n = scp.args.list.getOrNull(0)?.toInt() ?: 0
val res = scp.thisAs<ObjDateTime>().instant.plus(n, DateTimeUnit.MONTH, scp.thisAs<ObjDateTime>().timeZone)
return ObjDateTime(res, scp.thisAs<ObjDateTime>().timeZone)
}
})
addFnDoc("addYears", "Return a new DateTime with the specified number of years added (or subtracted if negative).",
params = listOf(net.sergeych.lyng.miniast.ParamDoc("years", type = type("lyng.Int"))),
returns = type("lyng.DateTime"), moduleName = "lyng.time",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val n = scp.args.list.getOrNull(0)?.toInt() ?: 0
val res = scp.thisAs<ObjDateTime>().instant.plus(n, DateTimeUnit.YEAR, scp.thisAs<ObjDateTime>().timeZone)
return ObjDateTime(res, scp.thisAs<ObjDateTime>().timeZone)
}
})
addClassFn("now", code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val tz = when (val a = scp.args.list.getOrNull(0)) {
null -> TimeZone.currentSystemDefault()
is ObjString -> TimeZone.of(a.value)
is ObjInt -> UtcOffset(seconds = a.value.toInt()).asTimeZone()
else -> scp.raiseIllegalArgument("invalid timezone: $a")
}
return ObjDateTime(kotlin.time.Clock.System.now(), tz)
}
})
addClassFnDoc("parseRFC3339",
"Parse an RFC3339 string into a DateTime object. " +
"Note: if the string does not specify a timezone, UTC is assumed.",
params = listOf(net.sergeych.lyng.miniast.ParamDoc("string", type = type("lyng.String"))),
returns = type("lyng.DateTime"),
moduleName = "lyng.time",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val s = (scp.args.firstAndOnly() as ObjString).value
// kotlinx-datetime's Instant.parse handles RFC3339
// But we want to preserve the offset if present for DateTime.
// However, Instant.parse("...") always gives an Instant.
// If we want the specific offset from the string, we might need a more complex parse.
// For now, let's stick to parsing it as Instant and converting to UTC or specified TZ.
// Actually, if the string has an offset, Instant.parse handles it but returns UTC instant.
// Let's try to detect if there is an offset in the string.
// If not, use UTC.
val instant = Instant.parse(s)
// RFC3339 can have Z or +/-HH:mm or +/-HHmm or +/-HH
val tz = try {
if (s.endsWith("Z", ignoreCase = true)) {
TimeZone.of("Z")
} else {
// Look for the last + or - which is likely the start of the offset
val lastPlus = s.lastIndexOf('+')
val lastMinus = s.lastIndexOf('-')
val offsetStart = if (lastPlus > lastMinus) lastPlus else lastMinus
if (offsetStart > s.lastIndexOf('T')) {
// Likely an offset
val offsetStr = s.substring(offsetStart)
TimeZone.of(offsetStr)
} else {
TimeZone.UTC
}
}
} catch (e: Exception) {
TimeZone.UTC
}
return ObjDateTime(instant, tz)
}
})
}
}
}
@@ -19,6 +19,7 @@ package net.sergeych.lyng.obj
import kotlinx.coroutines.Deferred
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeCallable
import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.addPropertyDoc
import net.sergeych.lyng.miniast.type
@@ -37,23 +38,30 @@ open class ObjDeferred(val deferred: Deferred<Obj>): Obj() {
name = "await",
doc = "Suspend until completion and return the result value (or throw if failed).",
returns = type("lyng.Any"),
moduleName = "lyng.stdlib"
) { thisAs<ObjDeferred>().deferred.await() }
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjDeferred>().deferred.await()
}
)
addPropertyDoc(
name = "isCompleted",
doc = "Whether this deferred has completed (successfully or with an error).",
type = type("lyng.Bool"),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjDeferred>().deferred.isCompleted.toObj() }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjDeferred>().deferred.isCompleted.toObj()
}
)
addPropertyDoc(
name = "isActive",
doc = "Whether this deferred is currently active (not completed and not cancelled).",
type = type("lyng.Bool"),
moduleName = "lyng.stdlib",
getter = {
val d = thisAs<ObjDeferred>().deferred
(d.isActive || (!d.isCompleted && !d.isCancelled)).toObj()
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val d = scp.thisAs<ObjDeferred>().deferred
return (d.isActive || (!d.isCompleted && !d.isCancelled)).toObj()
}
}
)
addPropertyDoc(
@@ -61,7 +69,9 @@ open class ObjDeferred(val deferred: Deferred<Obj>): Obj() {
doc = "Whether this deferred was cancelled.",
type = type("lyng.Bool"),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjDeferred>().deferred.isCancelled.toObj() }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjDeferred>().deferred.isCancelled.toObj()
}
)
}
}
@@ -18,6 +18,7 @@
package net.sergeych.lyng.obj
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeCallable
import net.sergeych.lyng.miniast.addPropertyDoc
import net.sergeych.lyng.miniast.type
import kotlin.time.Duration
@@ -79,42 +80,54 @@ class ObjDuration(val duration: Duration) : Obj() {
doc = "Return this duration as a real number of days.",
type = type("lyng.Real"),
moduleName = "lyng.time",
getter = { thisAs<ObjDuration>().duration.toDouble(DurationUnit.DAYS).toObj() }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjDuration>().duration.toDouble(DurationUnit.DAYS).toObj()
}
)
addPropertyDoc(
name = "hours",
doc = "Return this duration as a real number of hours.",
type = type("lyng.Real"),
moduleName = "lyng.time",
getter = { thisAs<ObjDuration>().duration.toDouble(DurationUnit.HOURS).toObj() }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjDuration>().duration.toDouble(DurationUnit.HOURS).toObj()
}
)
addPropertyDoc(
name = "minutes",
doc = "Return this duration as a real number of minutes.",
type = type("lyng.Real"),
moduleName = "lyng.time",
getter = { thisAs<ObjDuration>().duration.toDouble(DurationUnit.MINUTES).toObj() }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjDuration>().duration.toDouble(DurationUnit.MINUTES).toObj()
}
)
addPropertyDoc(
name = "seconds",
doc = "Return this duration as a real number of seconds.",
type = type("lyng.Real"),
moduleName = "lyng.time",
getter = { thisAs<ObjDuration>().duration.toDouble(DurationUnit.SECONDS).toObj() }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjDuration>().duration.toDouble(DurationUnit.SECONDS).toObj()
}
)
addPropertyDoc(
name = "milliseconds",
doc = "Return this duration as a real number of milliseconds.",
type = type("lyng.Real"),
moduleName = "lyng.time",
getter = { thisAs<ObjDuration>().duration.toDouble(DurationUnit.MILLISECONDS).toObj() }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjDuration>().duration.toDouble(DurationUnit.MILLISECONDS).toObj()
}
)
addPropertyDoc(
name = "microseconds",
doc = "Return this duration as a real number of microseconds.",
type = type("lyng.Real"),
moduleName = "lyng.time",
getter = { thisAs<ObjDuration>().duration.toDouble(DurationUnit.MICROSECONDS).toObj() }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjDuration>().duration.toDouble(DurationUnit.MICROSECONDS).toObj()
}
)
// extensions
@@ -123,7 +136,9 @@ class ObjDuration(val duration: Duration) : Obj() {
doc = "Construct a `Duration` equal to this integer number of seconds.",
type = type("lyng.Duration"),
moduleName = "lyng.time",
getter = { ObjDuration(thisAs<ObjInt>().value.seconds) }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjDuration(scp.thisAs<ObjInt>().value.seconds)
}
)
ObjInt.type.addPropertyDoc(
@@ -131,14 +146,18 @@ class ObjDuration(val duration: Duration) : Obj() {
doc = "Construct a `Duration` equal to this integer number of seconds.",
type = type("lyng.Duration"),
moduleName = "lyng.time",
getter = { ObjDuration(thisAs<ObjInt>().value.seconds) }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjDuration(scp.thisAs<ObjInt>().value.seconds)
}
)
ObjInt.type.addPropertyDoc(
name = "milliseconds",
doc = "Construct a `Duration` equal to this integer number of milliseconds.",
type = type("lyng.Duration"),
moduleName = "lyng.time",
getter = { ObjDuration(thisAs<ObjInt>().value.milliseconds) }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjDuration(scp.thisAs<ObjInt>().value.milliseconds)
}
)
ObjInt.type.addPropertyDoc(
@@ -146,14 +165,18 @@ class ObjDuration(val duration: Duration) : Obj() {
doc = "Construct a `Duration` equal to this integer number of milliseconds.",
type = type("lyng.Duration"),
moduleName = "lyng.time",
getter = { ObjDuration(thisAs<ObjInt>().value.milliseconds) }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjDuration(scp.thisAs<ObjInt>().value.milliseconds)
}
)
ObjReal.type.addPropertyDoc(
name = "seconds",
doc = "Construct a `Duration` equal to this real number of seconds.",
type = type("lyng.Duration"),
moduleName = "lyng.time",
getter = { ObjDuration(thisAs<ObjReal>().value.seconds) }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjDuration(scp.thisAs<ObjReal>().value.seconds)
}
)
ObjReal.type.addPropertyDoc(
@@ -161,7 +184,9 @@ class ObjDuration(val duration: Duration) : Obj() {
doc = "Construct a `Duration` equal to this real number of seconds.",
type = type("lyng.Duration"),
moduleName = "lyng.time",
getter = { ObjDuration(thisAs<ObjReal>().value.seconds) }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjDuration(scp.thisAs<ObjReal>().value.seconds)
}
)
ObjReal.type.addPropertyDoc(
@@ -169,14 +194,18 @@ class ObjDuration(val duration: Duration) : Obj() {
doc = "Construct a `Duration` equal to this real number of milliseconds.",
type = type("lyng.Duration"),
moduleName = "lyng.time",
getter = { ObjDuration(thisAs<ObjReal>().value.milliseconds) }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjDuration(scp.thisAs<ObjReal>().value.milliseconds)
}
)
ObjReal.type.addPropertyDoc(
name = "millisecond",
doc = "Construct a `Duration` equal to this real number of milliseconds.",
type = type("lyng.Duration"),
moduleName = "lyng.time",
getter = { ObjDuration(thisAs<ObjReal>().value.milliseconds) }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjDuration(scp.thisAs<ObjReal>().value.milliseconds)
}
)
ObjInt.type.addPropertyDoc(
@@ -184,84 +213,108 @@ class ObjDuration(val duration: Duration) : Obj() {
doc = "Construct a `Duration` equal to this integer number of minutes.",
type = type("lyng.Duration"),
moduleName = "lyng.time",
getter = { ObjDuration(thisAs<ObjInt>().value.minutes) }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjDuration(scp.thisAs<ObjInt>().value.minutes)
}
)
ObjReal.type.addPropertyDoc(
name = "minutes",
doc = "Construct a `Duration` equal to this real number of minutes.",
type = type("lyng.Duration"),
moduleName = "lyng.time",
getter = { ObjDuration(thisAs<ObjReal>().value.minutes) }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjDuration(scp.thisAs<ObjReal>().value.minutes)
}
)
ObjInt.type.addPropertyDoc(
name = "minute",
doc = "Construct a `Duration` equal to this integer number of minutes.",
type = type("lyng.Duration"),
moduleName = "lyng.time",
getter = { ObjDuration(thisAs<ObjInt>().value.minutes) }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjDuration(scp.thisAs<ObjInt>().value.minutes)
}
)
ObjReal.type.addPropertyDoc(
name = "minute",
doc = "Construct a `Duration` equal to this real number of minutes.",
type = type("lyng.Duration"),
moduleName = "lyng.time",
getter = { ObjDuration(thisAs<ObjReal>().value.minutes) }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjDuration(scp.thisAs<ObjReal>().value.minutes)
}
)
ObjInt.type.addPropertyDoc(
name = "hours",
doc = "Construct a `Duration` equal to this integer number of hours.",
type = type("lyng.Duration"),
moduleName = "lyng.time",
getter = { ObjDuration(thisAs<ObjInt>().value.hours) }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjDuration(scp.thisAs<ObjInt>().value.hours)
}
)
ObjReal.type.addPropertyDoc(
name = "hours",
doc = "Construct a `Duration` equal to this real number of hours.",
type = type("lyng.Duration"),
moduleName = "lyng.time",
getter = { ObjDuration(thisAs<ObjReal>().value.hours) }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjDuration(scp.thisAs<ObjReal>().value.hours)
}
)
ObjInt.type.addPropertyDoc(
name = "hour",
doc = "Construct a `Duration` equal to this integer number of hours.",
type = type("lyng.Duration"),
moduleName = "lyng.time",
getter = { ObjDuration(thisAs<ObjInt>().value.hours) }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjDuration(scp.thisAs<ObjInt>().value.hours)
}
)
ObjReal.type.addPropertyDoc(
name = "hour",
doc = "Construct a `Duration` equal to this real number of hours.",
type = type("lyng.Duration"),
moduleName = "lyng.time",
getter = { ObjDuration(thisAs<ObjReal>().value.hours) }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjDuration(scp.thisAs<ObjReal>().value.hours)
}
)
ObjInt.type.addPropertyDoc(
name = "days",
doc = "Construct a `Duration` equal to this integer number of days.",
type = type("lyng.Duration"),
moduleName = "lyng.time",
getter = { ObjDuration(thisAs<ObjInt>().value.days) }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjDuration(scp.thisAs<ObjInt>().value.days)
}
)
ObjReal.type.addPropertyDoc(
name = "days",
doc = "Construct a `Duration` equal to this real number of days.",
type = type("lyng.Duration"),
moduleName = "lyng.time",
getter = { ObjDuration(thisAs<ObjReal>().value.days) }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjDuration(scp.thisAs<ObjReal>().value.days)
}
)
ObjInt.type.addPropertyDoc(
name = "day",
doc = "Construct a `Duration` equal to this integer number of days.",
type = type("lyng.Duration"),
moduleName = "lyng.time",
getter = { ObjDuration(thisAs<ObjInt>().value.days) }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjDuration(scp.thisAs<ObjInt>().value.days)
}
)
ObjReal.type.addPropertyDoc(
name = "day",
doc = "Construct a `Duration` equal to this real number of days.",
type = type("lyng.Duration"),
moduleName = "lyng.time",
getter = { ObjDuration(thisAs<ObjReal>().value.days) }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjDuration(scp.thisAs<ObjReal>().value.days)
}
)
@@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* 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.
@@ -17,31 +17,32 @@
package net.sergeych.lyng.obj
import net.sergeych.lyng.Arguments
import net.sergeych.lyng.ClosureScope
import net.sergeych.lyng.Scope
import net.sergeych.lyng.Statement
import net.sergeych.lyng.*
class ObjDynamicContext(val delegate: ObjDynamic) : Obj() {
override val objClass: ObjClass get() = type
companion object {
val type = ObjClass("DelegateContext").apply {
addFn("get") {
val d = thisAs<ObjDynamicContext>().delegate
if (d.readCallback != null)
raiseIllegalState("get already defined")
d.readCallback = requireOnlyArg()
ObjVoid
}
addFn("get", code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val d = scp.thisAs<ObjDynamicContext>().delegate
if (d.readCallback != null)
scp.raiseIllegalState("get already defined")
d.readCallback = scp.requireOnlyArg()
return ObjVoid
}
})
addFn("set") {
val d = thisAs<ObjDynamicContext>().delegate
if (d.writeCallback != null)
raiseIllegalState("set already defined")
d.writeCallback = requireOnlyArg()
ObjVoid
}
addFn("set", code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val d = scp.thisAs<ObjDynamicContext>().delegate
if (d.writeCallback != null)
scp.raiseIllegalState("set already defined")
d.writeCallback = scp.requireOnlyArg()
return ObjVoid
}
})
}
@@ -81,11 +82,11 @@ open class ObjDynamic(var readCallback: Statement? = null, var writeCallback: St
scope: Scope,
name: String,
args: Arguments,
onNotFoundResult: (suspend () -> Obj?)?
onNotFoundResult: OnNotFound?
): Obj {
val execBase = builderScope?.let { ClosureScope(scope, it) } ?: scope
val over = readCallback?.execute(execBase.createChildScope(Arguments(ObjString(name))))
return over?.invoke(scope, scope.thisObj, args)
return over?.invokeCallable(scope, scope.thisObj, args)
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
}
@@ -35,6 +35,7 @@ package net.sergeych.lyng.obj/*
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeCallable
import net.sergeych.lyng.miniast.addPropertyDoc
import net.sergeych.lyng.miniast.type
import net.sergeych.lynon.LynonDecoder
@@ -74,12 +75,18 @@ class ObjEnumClass(val name: String) : ObjClass(name, EnumBase) {
init {
addClassConst("entries", objEntries )
addClassFn("valueOf") {
val name = requireOnlyArg<ObjString>()
byName[name] ?: raiseSymbolNotFound("does not exists: enum ${className}.$name")
}
addPropertyDoc("name", doc = "Entry name as string", type = type("lyng.String"), getter = { thisAs<ObjEnumEntry>().name })
addPropertyDoc("ordinal", doc = "Entry ordinal position", type = type("lyng.Int"), getter = { thisAs<ObjEnumEntry>().ordinal })
addClassFn("valueOf", code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val name = scp.requireOnlyArg<ObjString>()
return byName[name] ?: scp.raiseSymbolNotFound("does not exists: enum ${className}.$name")
}
})
addPropertyDoc("name", doc = "Entry name as string", type = type("lyng.String"), getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjEnumEntry>().name
})
addPropertyDoc("ordinal", doc = "Entry ordinal position", type = type("lyng.Int"), getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjEnumEntry>().ordinal
})
}
@@ -125,7 +125,9 @@ open class ObjException(
class ExceptionClass(val name: String, vararg parents: ObjClass) : ObjClass(name, *parents) {
init {
constructorMeta = ArgsDeclaration(
listOf(ArgsDeclaration.Item("message", defaultValue = statement { ObjString(name) })),
listOf(ArgsDeclaration.Item("message", defaultValue = statement(f = object : ScopeCallable {
override suspend fun call(scope: Scope): Obj = ObjString(name)
}))),
Token.Type.RPAREN
)
}
@@ -163,27 +165,33 @@ open class ObjException(
}
val Root = ExceptionClass("Exception").apply {
instanceInitializers.add(statement {
if (thisObj is ObjInstance) {
val msg = get("message")?.value ?: ObjString("Exception")
(thisObj as ObjInstance).instanceScope.addItem("Exception::message", false, msg)
instanceInitializers.add(statement(f = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
if (scp.thisObj is ObjInstance) {
val msg = scp.get("message")?.value ?: ObjString("Exception")
(scp.thisObj as ObjInstance).instanceScope.addItem("Exception::message", false, msg)
val stack = captureStackTrace(this)
(thisObj as ObjInstance).instanceScope.addItem("Exception::stackTrace", false, stack)
val stack = captureStackTrace(scp)
(scp.thisObj as ObjInstance).instanceScope.addItem("Exception::stackTrace", false, stack)
}
return ObjVoid
}
ObjVoid
}))
instanceConstructor = statement(f = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjVoid
})
instanceConstructor = statement { ObjVoid }
addPropertyDoc(
name = "message",
doc = "Human‑readable error message.",
type = type("lyng.String"),
moduleName = "lyng.stdlib",
getter = {
when (val t = this.thisObj) {
is ObjException -> t.message
is ObjInstance -> t.instanceScope.get("Exception::message")?.value ?: ObjNull
else -> ObjNull
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return when (val t = scp.thisObj) {
is ObjException -> t.message
is ObjInstance -> t.instanceScope.get("Exception::message")?.value ?: ObjNull
else -> ObjNull
}
}
}
)
@@ -192,10 +200,12 @@ open class ObjException(
doc = "Extra data associated with the exception.",
type = type("lyng.Any", nullable = true),
moduleName = "lyng.stdlib",
getter = {
when (val t = this.thisObj) {
is ObjException -> t.extraData
else -> ObjNull
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return when (val t = scp.thisObj) {
is ObjException -> t.extraData
else -> ObjNull
}
}
}
)
@@ -204,11 +214,13 @@ open class ObjException(
doc = "Stack trace captured at throw site as a list of `StackTraceEntry`.",
type = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.StackTraceEntry"))),
moduleName = "lyng.stdlib",
getter = {
when (val t = this.thisObj) {
is ObjException -> t.getStackTrace()
is ObjInstance -> t.instanceScope.get("Exception::stackTrace")?.value as? ObjList ?: ObjList()
else -> ObjList()
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return when (val t = scp.thisObj) {
is ObjException -> t.getStackTrace()
is ObjInstance -> t.instanceScope.get("Exception::stackTrace")?.value as? ObjList ?: ObjList()
else -> ObjList()
}
}
}
)
@@ -216,23 +228,26 @@ open class ObjException(
name = "toString",
doc = "Human‑readable string representation of the error.",
returns = type("lyng.String"),
moduleName = "lyng.stdlib"
) {
val msg = when (val t = thisObj) {
is ObjException -> t.message.value
is ObjInstance -> (t.instanceScope.get("Exception::message")?.value as? ObjString)?.value
?: t.objClass.className
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val msg = when (val t = scp.thisObj) {
is ObjException -> t.message.value
is ObjInstance -> (t.instanceScope.get("Exception::message")?.value as? ObjString)?.value
?: t.objClass.className
else -> t.objClass.className
else -> t.objClass.className
}
val stack = when (val t = scp.thisObj) {
is ObjException -> t.getStackTrace()
is ObjInstance -> t.instanceScope.get("Exception::stackTrace")?.value as? ObjList ?: ObjList()
else -> ObjList()
}
val at = stack.list.firstOrNull()?.toString(scp) ?: ObjString("(unknown)")
return ObjString("${scp.thisObj.objClass.className}: $msg at $at")
}
}
val stack = when (val t = thisObj) {
is ObjException -> t.getStackTrace()
is ObjInstance -> t.instanceScope.get("Exception::stackTrace")?.value as? ObjList ?: ObjList()
else -> ObjList()
}
val at = stack.list.firstOrNull()?.toString(this) ?: ObjString("(unknown)")
ObjString("${thisObj.objClass.className}: $msg at $at")
}
)
}
private val op = ProtectedOp()
@@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* 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.
@@ -45,28 +45,31 @@ class ObjFlowBuilder(val output: SendChannel<Obj>) : Obj() {
doc = "Send a value to the flow consumer. Suspends if back‑pressured; no‑ops after consumer stops.",
params = listOf(ParamDoc("value", type("lyng.Any"))),
returns = type("lyng.Void"),
moduleName = "lyng.stdlib"
) {
val data = requireOnlyArg<Obj>()
try {
val channel = thisAs<ObjFlowBuilder>().output
if (!channel.isClosedForSend)
channel.send(data)
else
// Flow consumer is no longer collecting; signal producer to stop
throw ScriptFlowIsNoMoreCollected()
} catch (x: Exception) {
// Any failure to send (including closed channel) should gracefully stop the producer.
if (x is CancellationException) {
// Cancellation is a normal control-flow event
throw ScriptFlowIsNoMoreCollected()
} else {
// Treat other send failures as normal flow termination as well
throw ScriptFlowIsNoMoreCollected()
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val data = scp.requireOnlyArg<Obj>()
try {
val channel = scp.thisAs<ObjFlowBuilder>().output
if (!channel.isClosedForSend)
channel.send(data)
else
// Flow consumer is no longer collecting; signal producer to stop
throw ScriptFlowIsNoMoreCollected()
} catch (x: Exception) {
// Any failure to send (including closed channel) should gracefully stop the producer.
if (x is CancellationException) {
// Cancellation is a normal control-flow event
throw ScriptFlowIsNoMoreCollected()
} else {
// Treat other send failures as normal flow termination as well
throw ScriptFlowIsNoMoreCollected()
}
}
return ObjVoid
}
}
ObjVoid
}
)
}
}
}
@@ -103,15 +106,21 @@ class ObjFlow(val producer: Statement, val scope: Scope) : Obj() {
name = "iterator",
doc = "Create a pull‑based iterator over this flow. Each step resumes the producer as needed.",
returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.Any"))),
moduleName = "lyng.stdlib"
) {
val objFlow = thisAs<ObjFlow>()
ObjFlowIterator(statement {
objFlow.producer.execute(
ClosureScope(this, objFlow.scope)
)
})
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val objFlow = scp.thisAs<ObjFlow>()
return ObjFlowIterator(statement(f = object : ScopeCallable {
override suspend fun call(scp2: Scope): Obj {
objFlow.producer.execute(
ClosureScope(scp2, objFlow.scope)
)
return ObjVoid
}
}))
}
}
)
}
}
}
@@ -164,27 +173,36 @@ class ObjFlowIterator(val producer: Statement) : Obj() {
name = "hasNext",
doc = "Whether another element is available from the flow.",
returns = type("lyng.Bool"),
moduleName = "lyng.stdlib"
) { thisAs<ObjFlowIterator>().hasNext(this).toObj() }
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjFlowIterator>().hasNext(scp).toObj()
}
)
addFnDoc(
name = "next",
doc = "Receive the next element from the flow or throw if completed.",
returns = type("lyng.Any"),
moduleName = "lyng.stdlib"
) {
val x = thisAs<ObjFlowIterator>()
x.next(this)
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val x = scp.thisAs<ObjFlowIterator>()
return x.next(scp)
}
}
)
addFnDoc(
name = "cancelIteration",
doc = "Stop iteration and cancel the underlying flow producer.",
returns = type("lyng.Void"),
moduleName = "lyng.stdlib"
) {
val x = thisAs<ObjFlowIterator>()
x.cancel()
ObjVoid
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val x = scp.thisAs<ObjFlowIterator>()
x.cancel()
return ObjVoid
}
}
)
}
}
}
@@ -68,7 +68,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
if (cls.className == "Obj") break
val mangled = cls.mangledName(name)
instanceScope.objects[mangled]?.let { rec ->
if ((scope.thisObj === this && caller != null) || canAccessMember(rec.visibility, cls, caller)) {
if (canAccessMember(rec.visibility, cls, caller, name)) {
return resolveRecord(scope, rec, name, cls)
}
}
@@ -77,8 +77,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
// 2. Unmangled storage
instanceScope.objects[name]?.let { rec ->
val decl = rec.declaringClass
// Unmangled access is only allowed if it's public OR we are inside the same instance's method
if ((scope.thisObj === this && caller != null) || canAccessMember(rec.visibility, decl, caller)) {
if (canAccessMember(rec.visibility, decl, caller, name)) {
return resolveRecord(scope, rec, name, decl)
}
}
@@ -174,7 +173,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
if (cls.className == "Obj") break
val mangled = cls.mangledName(name)
instanceScope.objects[mangled]?.let { rec ->
if ((scope.thisObj === this && caller != null) || canAccessMember(rec.effectiveWriteVisibility, cls, caller)) {
if (canAccessMember(rec.effectiveWriteVisibility, cls, caller, name)) {
updateRecord(scope, rec, name, newValue, cls)
return
}
@@ -184,7 +183,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
// 2. Unmangled storage
instanceScope.objects[name]?.let { rec ->
val decl = rec.declaringClass
if ((scope.thisObj === this && caller != null) || canAccessMember(rec.effectiveWriteVisibility, decl, caller)) {
if (canAccessMember(rec.effectiveWriteVisibility, decl, caller, name)) {
updateRecord(scope, rec, name, newValue, decl)
return
}
@@ -219,7 +218,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
override suspend fun invokeInstanceMethod(
scope: Scope, name: String, args: Arguments,
onNotFoundResult: (suspend () -> Obj?)?
onNotFoundResult: OnNotFound?
): Obj {
val caller = scope.currentClassCtx
@@ -232,7 +231,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
if (rec.type == ObjRecord.Type.Property) {
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
} else if (rec.type == ObjRecord.Type.Fun) {
return rec.value.invoke(instanceScope, this, args, decl)
return rec.value.invokeCallable(instanceScope, this, args, decl)
}
}
}
@@ -247,7 +246,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
if (rec.type == ObjRecord.Type.Property) {
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, c)
} else if (rec.type == ObjRecord.Type.Fun) {
return rec.value.invoke(instanceScope, this, args, c)
return rec.value.invokeCallable(instanceScope, this, args, c)
}
}
}
@@ -256,7 +255,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
if (rec.type == ObjRecord.Type.Property) {
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, c)
} else if (rec.type == ObjRecord.Type.Fun) {
return rec.value.invoke(instanceScope, this, args, c)
return rec.value.invokeCallable(instanceScope, this, args, c)
}
}
}
@@ -272,28 +271,28 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
val del = instanceScope[storageName]?.delegate ?: rec.delegate
?: scope.raiseError("Internal error: delegated member $name has no delegate (tried $storageName)")
// For delegated member, try 'invoke' first if it's a function-like call
// For delegated member, try 'invokeCallable' first if it's a function-like call
val allArgs = (listOf(this, ObjString(name)) + args.list).toTypedArray()
return del.invokeInstanceMethod(scope, "invoke", Arguments(*allArgs), onNotFoundResult = {
return del.invokeInstanceMethod(scope, "invokeCallable", Arguments(*allArgs), onNotFoundResult = {
// Fallback: property delegation (getValue then call result)
val propVal = del.invokeInstanceMethod(scope, "getValue", Arguments(this, ObjString(name)))
propVal.invoke(scope, this, args, rec.declaringClass ?: cls)
propVal.invokeCallable(scope, this, args, rec.declaringClass ?: cls)
})
}
val decl = rec.declaringClass ?: cls
val effectiveCaller = caller ?: if (scope.thisObj === this) objClass else null
if (!canAccessMember(rec.visibility, decl, effectiveCaller))
if (!canAccessMember(rec.visibility, decl, effectiveCaller, name))
scope.raiseError(
ObjIllegalAccessException(
scope,
"can't invoke method $name (declared in ${decl.className})"
"can't invokeCallable method $name (declared in ${decl.className})"
)
)
if (rec.type == ObjRecord.Type.Property) {
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
} else if (rec.type == ObjRecord.Type.Fun) {
return rec.value.invoke(
return rec.value.invokeCallable(
instanceScope,
this,
args,
@@ -301,7 +300,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
)
} else if (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.ConstructorField || rec.type == ObjRecord.Type.Argument) {
val resolved = readField(scope, name)
return resolved.value.invoke(scope, this, args, resolved.declaringClass)
return resolved.value.invokeCallable(scope, this, args, resolved.declaringClass)
}
}
}
@@ -339,7 +338,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
// values, so we save size of the construction:
// using objlist allow for some optimizations:
val params = meta.params.map { readField(scope, it.name).value }
val params = meta.params.filter { !it.isTransient }.map { readField(scope, it.name).value }
encoder.encodeAnyList(scope, params)
val vars = serializingVars.values.map { it.value }
if (vars.isNotEmpty()) {
@@ -358,8 +357,10 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
val result = mutableMapOf<String, JsonElement>()
val meta = objClass.constructorMeta
?: scope.raiseError("can't serialize non-serializable object (no constructor meta)")
for (entry in meta.params)
result[entry.name] = readField(scope, entry.name).value.toJson(scope)
for (entry in meta.params) {
if (!entry.isTransient)
result[entry.name] = readField(scope, entry.name).value.toJson(scope)
}
for (i in serializingVars) {
// remove T:: prefix from the field name for JSON
val parts = i.key.split("::")
@@ -378,7 +379,8 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
it.value.type.serializable &&
it.value.type == ObjRecord.Type.Field &&
it.value.isMutable &&
!metaParams.contains(it.key)
!metaParams.contains(it.key) &&
!it.value.isTransient
}
}
@@ -399,7 +401,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
protected val comparableVars: Map<String, ObjRecord> by lazy {
instanceScope.objects.filter {
it.value.type.comparable && (it.value.type != ObjRecord.Type.Field || it.value.isMutable)
it.value.type.comparable && (it.value.type != ObjRecord.Type.Field || it.value.isMutable) && !it.value.isTransient
}
}
@@ -433,7 +435,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
// Visibility: declaring class is the qualified ancestor for mangled storage
val decl = rec.declaringClass ?: startClass
val caller = scope.currentClassCtx
if (!canAccessMember(rec.visibility, decl, caller))
if (!canAccessMember(rec.visibility, decl, caller, name))
scope.raiseError(ObjIllegalAccessException(scope, "can't access field $name (declared in ${decl.className})"))
return instance.resolveRecord(scope, rec, name, decl)
}
@@ -442,7 +444,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
instance.instanceScope[name]?.let { rec ->
val decl = rec.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
val caller = scope.currentClassCtx
if (!canAccessMember(rec.visibility, decl, caller))
if (!canAccessMember(rec.visibility, decl, caller, name))
scope.raiseError(
ObjIllegalAccessException(
scope,
@@ -456,7 +458,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
val r = memberFromAncestor(name) ?: scope.raiseError("no such field: $name")
val decl = r.declaringClass ?: startClass
val caller = scope.currentClassCtx
if (!canAccessMember(r.visibility, decl, caller))
if (!canAccessMember(r.visibility, decl, caller, name))
scope.raiseError(ObjIllegalAccessException(scope, "can't access field $name (declared in ${decl.className})"))
return instance.resolveRecord(scope, r, name, decl)
@@ -468,7 +470,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
instance.instanceScope.objects[mangled]?.let { f ->
val decl = f.declaringClass ?: startClass
val caller = scope.currentClassCtx
if (!canAccessMember(f.effectiveWriteVisibility, decl, caller))
if (!canAccessMember(f.effectiveWriteVisibility, decl, caller, name))
ObjIllegalAccessException(
scope,
"can't assign to field $name (declared in ${decl.className})"
@@ -482,7 +484,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
instance.instanceScope[name]?.let { f ->
val decl = f.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
val caller = scope.currentClassCtx
if (!canAccessMember(f.effectiveWriteVisibility, decl, caller))
if (!canAccessMember(f.effectiveWriteVisibility, decl, caller, name))
ObjIllegalAccessException(
scope,
"can't assign to field $name (declared in ${decl?.className ?: "?"})"
@@ -495,7 +497,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
val r = memberFromAncestor(name) ?: scope.raiseError("no such field: $name")
val decl = r.declaringClass ?: startClass
val caller = scope.currentClassCtx
if (!canAccessMember(r.effectiveWriteVisibility, decl, caller))
if (!canAccessMember(r.effectiveWriteVisibility, decl, caller, name))
ObjIllegalAccessException(scope, "can't assign to field $name (declared in ${decl.className})").raise()
if (!r.isMutable) scope.raiseError("can't assign to read-only field: $name")
if (r.value.assign(scope, newValue) == null) r.value = newValue
@@ -505,18 +507,18 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
scope: Scope,
name: String,
args: Arguments,
onNotFoundResult: (suspend () -> Obj?)?
onNotFoundResult: OnNotFound?
): Obj {
// Qualified method dispatch must start from the specified ancestor, not from the instance scope.
memberFromAncestor(name)?.let { rec ->
val decl = rec.declaringClass ?: startClass
val caller = scope.currentClassCtx
if (!canAccessMember(rec.visibility, decl, caller))
scope.raiseError(ObjIllegalAccessException(scope, "can't invoke method $name (declared in ${decl.className})"))
if (!canAccessMember(rec.visibility, decl, caller, name))
scope.raiseError(ObjIllegalAccessException(scope, "can't invokeCallable method $name (declared in ${decl.className})"))
val saved = instance.instanceScope.currentClassCtx
instance.instanceScope.currentClassCtx = decl
try {
return rec.value.invoke(instance.instanceScope, instance, args)
return rec.value.invokeCallable(instance.instanceScope, instance, args)
} finally {
instance.instanceScope.currentClassCtx = saved
}
@@ -526,23 +528,23 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
instance.instanceScope[name]?.let { rec ->
val decl = rec.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
val caller = scope.currentClassCtx
if (!canAccessMember(rec.visibility, decl, caller))
if (!canAccessMember(rec.visibility, decl, caller, name))
scope.raiseError(
ObjIllegalAccessException(
scope,
"can't invoke method $name (declared in ${decl?.className ?: "?"})"
"can't invokeCallable method $name (declared in ${decl?.className ?: "?"})"
)
)
val saved = instance.instanceScope.currentClassCtx
instance.instanceScope.currentClassCtx = decl
try {
return rec.value.invoke(instance.instanceScope, instance, args)
return rec.value.invokeCallable(instance.instanceScope, instance, args)
} finally {
instance.instanceScope.currentClassCtx = saved
}
}
}
return onNotFoundResult?.invoke() ?: scope.raiseSymbolNotFound(name)
return onNotFoundResult?.call() ?: scope.raiseSymbolNotFound(name)
}
override fun toString(): String = instance.toString()
@@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* 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.
@@ -28,13 +28,26 @@ import net.sergeych.lynon.LynonType
class ObjInstanceClass(val name: String, vararg parents: ObjClass) : ObjClass(name, *parents) {
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
val args = decoder.decodeAnyList(scope)
val actualSize = constructorMeta?.params?.size ?: 0
if (args.size > actualSize)
scope.raiseIllegalArgument("constructor $name has only $actualSize but serialized version has ${args.size}")
val newScope = scope.createChildScope(args = Arguments(args))
val instance = createInstance(newScope)
initializeInstance(instance, newScope.args, runConstructors = false)
val serializedArgs = decoder.decodeAnyList(scope)
val meta = constructorMeta ?: scope.raiseError("no constructor meta for $name")
val nonTransientCount = meta.params.count { !it.isTransient }
if (serializedArgs.size != nonTransientCount)
scope.raiseIllegalArgument("constructor $name expects $nonTransientCount non-transient arguments, but serialized version has ${serializedArgs.size}")
var sIdx = 0
val namedArgs = mutableMapOf<String, Obj>()
for (p in meta.params) {
if (!p.isTransient) {
namedArgs[p.name] = serializedArgs[sIdx++]
} else if (p.defaultValue == null) {
// If transient parameter has no default value, we use ObjNull to avoid "too few arguments" error
namedArgs[p.name] = ObjNull
}
}
// Using named arguments allows the constructor to apply default values for transient parameters
val newArgs = Arguments(list = emptyList<Obj>(), named = namedArgs)
val instance = createInstance(scope.createChildScope(args = newArgs))
initializeInstance(instance, newArgs, runConstructors = false)
return instance.apply {
deserializeStateVars(scope, decoder)
invokeInstanceMethod(scope, "onDeserialized") { ObjVoid }
@@ -17,18 +17,17 @@
package net.sergeych.lyng.obj
import kotlinx.datetime.*
import kotlinx.datetime.Instant
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import net.sergeych.lyng.Scope
import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.addPropertyDoc
import net.sergeych.lyng.miniast.type
import net.sergeych.lyng.ScopeCallable
import net.sergeych.lyng.miniast.*
import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonSettings
import net.sergeych.lynon.LynonType
import kotlin.time.Clock
import kotlin.time.Instant
import kotlin.time.isDistantFuture
import kotlin.time.isDistantPast
@@ -113,7 +112,7 @@ class ObjInstant(val instant: Instant,val truncateMode: LynonSettings.InstantTru
return ObjInstant(
when (a0) {
null -> {
val t = Clock.System.now()
val t = kotlin.time.Clock.System.now()
Instant.fromEpochSeconds(t.epochSeconds, t.nanosecondsOfSecond)
}
is ObjInt -> Instant.fromEpochSeconds(a0.value)
@@ -122,6 +121,7 @@ class ObjInstant(val instant: Instant,val truncateMode: LynonSettings.InstantTru
val nanos = (a0.value - seconds) * 1e9
Instant.fromEpochSeconds(seconds, nanos.toLong())
}
is ObjString -> Instant.parse(a0.value)
is ObjInstant -> a0.instant
else -> {
@@ -156,9 +156,11 @@ class ObjInstant(val instant: Instant,val truncateMode: LynonSettings.InstantTru
doc = "Return the number of seconds since the Unix epoch as a real number (including fractions).",
type = type("lyng.Real"),
moduleName = "lyng.time",
getter = {
val instant = thisAs<ObjInstant>().instant
ObjReal(instant.epochSeconds + instant.nanosecondsOfSecond * 1e-9)
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val instant = scp.thisAs<ObjInstant>().instant
return ObjReal(instant.epochSeconds + instant.nanosecondsOfSecond * 1e-9)
}
}
)
addPropertyDoc(
@@ -166,69 +168,143 @@ class ObjInstant(val instant: Instant,val truncateMode: LynonSettings.InstantTru
doc = "Whether this instant represents the distant future.",
type = type("lyng.Bool"),
moduleName = "lyng.time",
getter = { thisAs<ObjInstant>().instant.isDistantFuture.toObj() }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjInstant>().instant.isDistantFuture.toObj()
}
)
addPropertyDoc(
name = "isDistantPast",
doc = "Whether this instant represents the distant past.",
type = type("lyng.Bool"),
moduleName = "lyng.time",
getter = { thisAs<ObjInstant>().instant.isDistantPast.toObj() }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjInstant>().instant.isDistantPast.toObj()
}
)
addPropertyDoc(
name = "epochWholeSeconds",
doc = "Return the number of full seconds since the Unix epoch.",
type = type("lyng.Int"),
moduleName = "lyng.time",
getter = { ObjInt(thisAs<ObjInstant>().instant.epochSeconds) }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjInt(scp.thisAs<ObjInstant>().instant.epochSeconds)
}
)
addPropertyDoc(
name = "nanosecondsOfSecond",
doc = "The number of nanoseconds within the current second.",
type = type("lyng.Int"),
moduleName = "lyng.time",
getter = { ObjInt(thisAs<ObjInstant>().instant.nanosecondsOfSecond.toLong()) }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjInt(scp.thisAs<ObjInstant>().instant.nanosecondsOfSecond.toLong())
}
)
addFnDoc(
name = "truncateToMinute",
doc = "Truncate this instant to the nearest minute.",
returns = type("lyng.Instant"),
moduleName = "lyng.time",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val t = scp.thisAs<ObjInstant>().instant
val tz = TimeZone.UTC
val dt = t.toLocalDateTime(tz)
val truncated = LocalDateTime(dt.year, dt.month, dt.dayOfMonth, dt.hour, dt.minute, 0, 0)
return ObjInstant(truncated.toInstant(tz), LynonSettings.InstantTruncateMode.Second)
}
}
)
addFnDoc(
name = "truncateToSecond",
doc = "Truncate this instant to the nearest second.",
returns = type("lyng.Instant"),
moduleName = "lyng.time"
) {
val t = thisAs<ObjInstant>().instant
ObjInstant(Instant.fromEpochSeconds(t.epochSeconds), LynonSettings.InstantTruncateMode.Second)
}
moduleName = "lyng.time",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val t = scp.thisAs<ObjInstant>().instant
return ObjInstant(Instant.fromEpochSeconds(t.epochSeconds), LynonSettings.InstantTruncateMode.Second)
}
}
)
addFnDoc(
name = "truncateToMillisecond",
doc = "Truncate this instant to the nearest millisecond.",
returns = type("lyng.Instant"),
moduleName = "lyng.time"
) {
val t = thisAs<ObjInstant>().instant
ObjInstant(
Instant.fromEpochSeconds(t.epochSeconds, t.nanosecondsOfSecond / 1_000_000 * 1_000_000),
LynonSettings.InstantTruncateMode.Millisecond
)
}
moduleName = "lyng.time",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val t = scp.thisAs<ObjInstant>().instant
return ObjInstant(
Instant.fromEpochSeconds(t.epochSeconds, t.nanosecondsOfSecond / 1_000_000 * 1_000_000),
LynonSettings.InstantTruncateMode.Millisecond
)
}
}
)
addFnDoc(
name = "truncateToMicrosecond",
doc = "Truncate this instant to the nearest microsecond.",
returns = type("lyng.Instant"),
moduleName = "lyng.time"
) {
val t = thisAs<ObjInstant>().instant
ObjInstant(
Instant.fromEpochSeconds(t.epochSeconds, t.nanosecondsOfSecond / 1_000 * 1_000),
LynonSettings.InstantTruncateMode.Microsecond
)
}
moduleName = "lyng.time",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val t = scp.thisAs<ObjInstant>().instant
return ObjInstant(
Instant.fromEpochSeconds(t.epochSeconds, t.nanosecondsOfSecond / 1_000 * 1_000),
LynonSettings.InstantTruncateMode.Microsecond
)
}
}
)
addFnDoc(
name = "toRFC3339",
doc = "Return the RFC3339 string representation of this instant in UTC (e.g., '1970-01-01T00:00:00Z').",
returns = type("lyng.String"),
moduleName = "lyng.time",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjInstant>().instant.toString().toObj()
}
)
addFnDoc(
name = "toSortableString",
doc = "Alias to toRFC3339.",
returns = type("lyng.String"),
moduleName = "lyng.time",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjInstant>().instant.toString().toObj()
}
)
addFnDoc(
name = "toDateTime",
doc = "Convert this absolute instant to a localized DateTime object in the specified time zone. " +
"Accepts a timezone ID string (e.g., 'UTC', '+02:00') or an integer offset in seconds. " +
"If no argument is provided, the system's current default time zone is used.",
params = listOf(net.sergeych.lyng.miniast.ParamDoc("tz", type = type("lyng.Any", true))),
returns = type("lyng.DateTime"),
moduleName = "lyng.time",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val tz = when (val a = scp.args.list.getOrNull(0)) {
null -> TimeZone.currentSystemDefault()
is ObjString -> TimeZone.of(a.value)
is ObjInt -> UtcOffset(seconds = a.value.toInt()).asTimeZone()
else -> scp.raiseIllegalArgument("invalid timezone: $a")
}
return ObjDateTime(scp.thisAs<ObjInstant>().instant, tz)
}
}
)
// class members
addClassConst("distantFuture", distantFuture)
addClassConst("distantPast", distantPast)
addClassFn("now") {
ObjInstant(Clock.System.now())
}
addClassFn("now", code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjInstant(kotlin.time.Clock.System.now())
})
// addFn("epochMilliseconds") {
// ObjInt(instant.toEpochMilliseconds())
// }
@@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* 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.
@@ -20,6 +20,7 @@ package net.sergeych.lyng.obj
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeCallable
import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder
@@ -183,10 +184,11 @@ class ObjInt(val value: Long, override val isConst: Boolean = false) : Obj(), Nu
name = "toInt",
doc = "Returns this integer (identity operation).",
returns = net.sergeych.lyng.miniast.type("lyng.Int"),
moduleName = "lyng.stdlib"
) {
thisObj
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisObj
}
)
}
}
}
@@ -18,6 +18,8 @@
package net.sergeych.lyng.obj
import net.sergeych.lyng.Arguments
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeCallable
import net.sergeych.lyng.Statement
import net.sergeych.lyng.miniast.ParamDoc
import net.sergeych.lyng.miniast.addFnDoc
@@ -35,13 +37,15 @@ val ObjIterable by lazy {
doc = "Collect elements of this iterable into a new list.",
type = type("lyng.List"),
moduleName = "lyng.stdlib",
getter = {
val result = mutableListOf<Obj>()
val it = this.thisObj.invokeInstanceMethod(this, "iterator")
while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
result.add(it.invokeInstanceMethod(this, "next"))
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val result = mutableListOf<Obj>()
val it = scp.thisObj.invokeInstanceMethod(scp, "iterator")
while (it.invokeInstanceMethod(scp, "hasNext").toBool()) {
result.add(it.invokeInstanceMethod(scp, "next"))
}
return ObjList(result)
}
ObjList(result)
}
)
@@ -52,16 +56,19 @@ val ObjIterable by lazy {
params = listOf(ParamDoc("element")),
returns = type("lyng.Bool"),
isOpen = true,
moduleName = "lyng.stdlib"
) {
val obj = args.firstAndOnly()
val it = thisObj.invokeInstanceMethod(this, "iterator")
while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
if (obj.equals(this, it.invokeInstanceMethod(this, "next")))
return@addFnDoc ObjTrue
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val obj = scp.args.firstAndOnly()
val it = scp.thisObj.invokeInstanceMethod(scp, "iterator")
while (it.invokeInstanceMethod(scp, "hasNext").toBool()) {
if (obj.equals(scp, it.invokeInstanceMethod(scp, "next")))
return ObjTrue
}
return ObjFalse
}
}
ObjFalse
}
)
addFnDoc(
name = "indexOf",
@@ -69,34 +76,39 @@ val ObjIterable by lazy {
params = listOf(ParamDoc("element")),
returns = type("lyng.Int"),
isOpen = true,
moduleName = "lyng.stdlib"
) {
val obj = args.firstAndOnly()
var index = 0
val it = thisObj.invokeInstanceMethod(this, "iterator")
while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
if (obj.equals(this, it.invokeInstanceMethod(this, "next")))
return@addFnDoc ObjInt(index.toLong())
index++
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val obj = scp.args.firstAndOnly()
var index = 0
val it = scp.thisObj.invokeInstanceMethod(scp, "iterator")
while (it.invokeInstanceMethod(scp, "hasNext").toBool()) {
if (obj.equals(scp, it.invokeInstanceMethod(scp, "next")))
return ObjInt(index.toLong())
index++
}
return ObjInt(-1L)
}
}
ObjInt(-1L)
}
)
addPropertyDoc(
name = "toSet",
doc = "Collect elements of this iterable into a new set.",
type = type("lyng.Set"),
moduleName = "lyng.stdlib",
getter = {
if( this.thisObj.isInstanceOf(ObjSet.type) )
this.thisObj
else {
val result = mutableSetOf<Obj>()
val it = this.thisObj.invokeInstanceMethod(this, "iterator")
while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
result.add(it.invokeInstanceMethod(this, "next"))
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return if( scp.thisObj.isInstanceOf(ObjSet.type) )
scp.thisObj
else {
val result = mutableSetOf<Obj>()
val it = scp.thisObj.invokeInstanceMethod(scp, "iterator")
while (it.invokeInstanceMethod(scp, "hasNext").toBool()) {
result.add(it.invokeInstanceMethod(scp, "next"))
}
ObjSet(result)
}
ObjSet(result)
}
}
)
@@ -106,16 +118,20 @@ val ObjIterable by lazy {
doc = "Collect pairs into a map using [0] as key and [1] as value for each element.",
type = type("lyng.Map"),
moduleName = "lyng.stdlib",
getter = {
val result = mutableMapOf<Obj, Obj>()
this.thisObj.enumerate(this) { pair ->
when (pair) {
is ObjMapEntry -> result[pair.key] = pair.value
else -> result[pair.getAt(this, 0)] = pair.getAt(this, 1)
}
true
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val result = mutableMapOf<Obj, Obj>()
scp.thisObj.enumerate(scp, object : EnumerateCallback {
override suspend fun call(pair: Obj): Boolean {
when (pair) {
is ObjMapEntry -> result[pair.key] = pair.value
else -> result[pair.getAt(scp, ObjInt(0))] = pair.getAt(scp, ObjInt(1))
}
return true
}
})
return ObjMap(result)
}
ObjMap(result)
}
)
@@ -124,31 +140,37 @@ val ObjIterable by lazy {
doc = "Build a map from elements using the lambda result as key.",
params = listOf(ParamDoc("keySelector")),
returns = type("lyng.Map"),
moduleName = "lyng.stdlib"
) {
val association = requireOnlyArg<Statement>()
val result = ObjMap()
thisObj.toFlow(this).collect {
result.map[association.call(this, it)] = it
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val association = scp.requireOnlyArg<Statement>()
val result = ObjMap()
scp.thisObj.toFlow(scp).collect {
result.map[association.invokeCallable(scp, scp.thisObj, it)] = it
}
return result
}
}
result
}
)
addFnDoc(
name = "forEach",
doc = "Apply the lambda to each element in iteration order.",
params = listOf(ParamDoc("action")),
isOpen = true,
moduleName = "lyng.stdlib"
) {
val it = thisObj.invokeInstanceMethod(this, "iterator")
val fn = requiredArg<Statement>(0)
while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
val x = it.invokeInstanceMethod(this, "next")
fn.execute(this.createChildScope(Arguments(listOf(x))))
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val it = scp.thisObj.invokeInstanceMethod(scp, "iterator")
val fn = scp.requiredArg<Statement>(0)
while (it.invokeInstanceMethod(scp, "hasNext").toBool()) {
val x = it.invokeInstanceMethod(scp, "next")
fn.execute(scp.createChildScope(Arguments(listOf(x))))
}
return ObjVoid
}
}
ObjVoid
}
)
addFnDoc(
name = "map",
@@ -156,15 +178,18 @@ val ObjIterable by lazy {
params = listOf(ParamDoc("transform")),
returns = type("lyng.List"),
isOpen = true,
moduleName = "lyng.stdlib"
) {
val fn = requiredArg<Statement>(0)
val result = mutableListOf<Obj>()
thisObj.toFlow(this).collect {
result.add(fn.call(this, it))
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val fn = scp.requiredArg<Statement>(0)
val result = mutableListOf<Obj>()
scp.thisObj.toFlow(scp).collect {
result.add(fn.invokeCallable(scp, scp.thisObj, it))
}
return ObjList(result)
}
}
ObjList(result)
}
)
addFnDoc(
name = "mapNotNull",
@@ -172,46 +197,56 @@ val ObjIterable by lazy {
params = listOf(ParamDoc("transform")),
returns = type("lyng.List"),
isOpen = true,
moduleName = "lyng.stdlib"
) {
val fn = requiredArg<Statement>(0)
val result = mutableListOf<Obj>()
thisObj.toFlow(this).collect {
val transformed = fn.call(this, it)
if( transformed != ObjNull) result.add(transformed)
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val fn = scp.requiredArg<Statement>(0)
val result = mutableListOf<Obj>()
scp.thisObj.toFlow(scp).collect {
val transformed = fn.invokeCallable(scp, scp.thisObj, it)
if( transformed != ObjNull) result.add(transformed)
}
return ObjList(result)
}
}
ObjList(result)
}
)
addFnDoc(
name = "take",
doc = "Take the first N elements and return them as a list.",
params = listOf(ParamDoc("n", type("lyng.Int"))),
returns = type("lyng.List"),
moduleName = "lyng.stdlib"
) {
var n = requireOnlyArg<ObjInt>().value.toInt()
val result = mutableListOf<Obj>()
if (n > 0) {
thisObj.enumerate(this) {
result.add(it)
--n > 0
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
var n = scp.requireOnlyArg<ObjInt>().value.toInt()
val result = mutableListOf<Obj>()
if (n > 0) {
scp.thisObj.enumerate(scp, object : EnumerateCallback {
override suspend fun call(element: Obj): Boolean {
result.add(element)
return --n > 0
}
})
}
return ObjList(result)
}
}
ObjList(result)
}
)
addPropertyDoc(
name = "isEmpty",
doc = "Whether the iterable has no elements.",
type = type("lyng.Bool"),
moduleName = "lyng.stdlib",
getter = {
ObjBool(
this.thisObj.invokeInstanceMethod(this, "iterator")
.invokeInstanceMethod(this, "hasNext").toBool()
.not()
)
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return ObjBool(
scp.thisObj.invokeInstanceMethod(scp, "iterator")
.invokeInstanceMethod(scp, "hasNext").toBool()
.not()
)
}
}
)
@@ -220,25 +255,31 @@ val ObjIterable by lazy {
doc = "Return a new list sorted using the provided comparator `(a, b) -> Int`.",
params = listOf(ParamDoc("comparator")),
returns = type("lyng.List"),
moduleName = "lyng.stdlib"
) {
val list = thisObj.callMethod<ObjList>(this, "toList")
val comparator = requireOnlyArg<Statement>()
list.quicksort { a, b ->
comparator.call(this, a, b).toInt()
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val list = scp.thisObj.callMethod<ObjList>(scp, "toList")
val comparator = scp.requireOnlyArg<Statement>()
list.quicksort { a, b ->
comparator.invokeCallable(scp, scp.thisObj, a, b).toInt()
}
return list
}
}
list
}
)
addFnDoc(
name = "reversed",
doc = "Return a new list with elements in reverse order.",
returns = type("lyng.List"),
moduleName = "lyng.stdlib"
) {
val list = thisObj.callMethod<ObjList>(this, "toList")
list.list.reverse()
list
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val list = scp.thisObj.callMethod<ObjList>(scp, "toList")
list.list.reverse()
return list
}
}
)
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* 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.
@@ -17,6 +17,8 @@
package net.sergeych.lyng.obj
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeCallable
import net.sergeych.lyng.miniast.TypeGenericDoc
import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.type
@@ -38,44 +40,50 @@ val ObjIterator by lazy {
doc = "Optional hint to stop iteration early and free resources.",
returns = type("lyng.Void"),
isOpen = true,
moduleName = "lyng.stdlib"
) {
ObjVoid
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjVoid
}
)
addFnDoc(
name = "hasNext",
doc = "Whether another element is available.",
returns = type("lyng.Bool"),
isOpen = true,
moduleName = "lyng.stdlib"
) {
raiseNotImplemented("hasNext() is not implemented")
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.raiseNotImplemented("hasNext() is not implemented")
}
)
addFnDoc(
name = "next",
doc = "Return the next element.",
returns = type("lyng.Any"),
isOpen = true,
moduleName = "lyng.stdlib"
) {
raiseNotImplemented("next() is not implemented")
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.raiseNotImplemented("next() is not implemented")
}
)
// Helper to consume iterator into a list
addFnDoc(
name = "toList",
doc = "Consume this iterator and collect elements into a list.",
returns = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.Any"))),
moduleName = "lyng.stdlib"
) {
val out = mutableListOf<Obj>()
while (true) {
val has = thisObj.invokeInstanceMethod(this, "hasNext").toBool()
if (!has) break
val v = thisObj.invokeInstanceMethod(this, "next")
out += v
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val out = mutableListOf<Obj>()
while (true) {
val has = scp.thisObj.invokeInstanceMethod(scp, "hasNext").toBool()
if (!has) break
val v = scp.thisObj.invokeInstanceMethod(scp, "next")
out += v
}
return ObjList(out.toMutableList())
}
}
ObjList(out.toMutableList())
}
)
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* 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.
@@ -22,6 +22,7 @@ package net.sergeych.lyng.obj
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeCallable
/**
* Iterator wrapper to allow Kotlin collections to be returned from Lyng objects;
@@ -33,8 +34,12 @@ class ObjKotlinIterator(val iterator: Iterator<Any?>) : Obj() {
companion object {
val type = ObjClass("KotlinIterator", ObjIterator).apply {
addFn("next") { thisAs<ObjKotlinIterator>().iterator.next().toObj() }
addFn("hasNext") { thisAs<ObjKotlinIterator>().iterator.hasNext().toObj() }
addFn("next", code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjKotlinIterator>().iterator.next().toObj()
})
addFn("hasNext", code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjKotlinIterator>().iterator.hasNext().toObj()
})
}
}
@@ -50,12 +55,14 @@ class ObjKotlinObjIterator(val iterator: Iterator<Obj>) : Obj() {
companion object {
val type = ObjClass("KotlinIterator", ObjIterator).apply {
addFn("next") {
thisAs<ObjKotlinObjIterator>().iterator.next()
}
addFn("hasNext") {
thisAs<ObjKotlinObjIterator>().iterator.hasNext().toObj()
}
addFn("next", code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj =
scp.thisAs<ObjKotlinObjIterator>().iterator.next()
})
addFn("hasNext", code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj =
scp.thisAs<ObjKotlinObjIterator>().iterator.hasNext().toObj()
})
}
}
@@ -71,8 +78,8 @@ fun Obj.toFlow(scope: Scope): Flow<Obj> = flow {
val iterator = invokeInstanceMethod(scope, "iterator")
val hasNext = iterator.getInstanceMethod(scope, "hasNext")
val next = iterator.getInstanceMethod(scope, "next")
while (hasNext.invoke(scope, iterator).toBool()) {
emit(next.invoke(scope, iterator))
while (hasNext.invokeCallable(scope, iterator).toBool()) {
emit(next.invokeCallable(scope, iterator))
}
}
@@ -20,6 +20,7 @@ package net.sergeych.lyng.obj
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeCallable
import net.sergeych.lyng.Statement
import net.sergeych.lyng.miniast.ParamDoc
import net.sergeych.lyng.miniast.addFnDoc
@@ -111,13 +112,13 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
val next2 = it2.getInstanceMethod(scope, "next")
while (it1.hasNext()) {
if (!hasNext2.invoke(scope, it2).toBool()) return 1 // I'm longer
if (!hasNext2.invokeCallable(scope, it2).toBool()) return 1 // I'm longer
val v1 = it1.next()
val v2 = next2.invoke(scope, it2)
val v2 = next2.invokeCallable(scope, it2)
val d = v1.compareTo(scope, v2)
if (d != 0) return d
}
return if (hasNext2.invoke(scope, it2).toBool()) -1 else 0
return if (hasNext2.invokeCallable(scope, it2).toBool()) -1 else 0
}
return -2
}
@@ -169,9 +170,9 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
return list.contains(other)
}
override suspend fun enumerate(scope: Scope, callback: suspend (Obj) -> Boolean) {
override suspend fun enumerate(scope: Scope, callback: EnumerateCallback) {
for (item in list) {
if (!callback(item)) break
if (!callback.call(item)) break
}
}
@@ -179,7 +180,9 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
get() = type
override suspend fun toKotlin(scope: Scope): Any {
return list.map { it.toKotlin(scope) }
val res = ArrayList<Any?>(list.size)
for (i in list) res.add(i.toKotlin(scope))
return res
}
suspend fun quicksort(compare: suspend (Obj, Obj) -> Int) = quicksort(compare, 0, list.size - 1)
@@ -230,7 +233,9 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
override suspend fun lynonType(): LynonType = LynonType.List
override suspend fun toJson(scope: Scope): JsonElement {
return JsonArray(list.map { it.toJson(scope) })
val res = ArrayList<JsonElement>(list.size)
for (i in list) res.add(i.toJson(scope))
return JsonArray(res)
}
override suspend fun defaultToString(scope: Scope): ObjString {
@@ -256,256 +261,290 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
doc = "Number of elements in this list.",
type = type("lyng.Int"),
moduleName = "lyng.stdlib",
getter = {
val s = (this.thisObj as ObjList).list.size
s.toObj()
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return (scp.thisObj as ObjList).list.size.toObj()
}
}
)
addFnDoc(
name = "add",
doc = "Append one or more elements to the end of this list.",
moduleName = "lyng.stdlib"
) {
val l = thisAs<ObjList>().list
for (a in args) l.add(a)
ObjVoid
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val l = scp.thisAs<ObjList>().list
for (a in scp.args) l.add(a)
return ObjVoid
}
}
)
addFnDoc(
name = "insertAt",
doc = "Insert elements starting at the given index.",
params = listOf(ParamDoc("index", type("lyng.Int"))),
moduleName = "lyng.stdlib"
) {
if (args.size < 2) raiseError("addAt takes 2+ arguments")
val l = thisAs<ObjList>()
var index = requiredArg<ObjInt>(0).value.toInt()
for (i in 1..<args.size) l.list.add(index++, args[i])
ObjVoid
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
if (scp.args.size < 2) scp.raiseError("addAt takes 2+ arguments")
val l = scp.thisAs<ObjList>()
var index = scp.requiredArg<ObjInt>(0).value.toInt()
for (i in 1..<scp.args.size) l.list.add(index++, scp.args[i])
return ObjVoid
}
}
)
addFnDoc(
name = "removeAt",
doc = "Remove element at index, or a range [start,end) if two indices are provided. Returns the list.",
params = listOf(ParamDoc("start", type("lyng.Int")), ParamDoc("end", type("lyng.Int"))),
moduleName = "lyng.stdlib"
) {
val self = thisAs<ObjList>()
val start = requiredArg<ObjInt>(0).value.toInt()
if (args.size == 2) {
val end = requireOnlyArg<ObjInt>().value.toInt()
self.list.subList(start, end).clear()
} else
self.list.removeAt(start)
self
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val self = scp.thisAs<ObjList>()
val start = scp.requiredArg<ObjInt>(0).value.toInt()
if (scp.args.size == 2) {
val end = scp.requireOnlyArg<ObjInt>().value.toInt()
self.list.subList(start, end).clear()
} else
self.list.removeAt(start)
return self
}
}
)
addFnDoc(
name = "removeLast",
doc = "Remove the last element or the last N elements if a count is provided. Returns the list.",
params = listOf(ParamDoc("count", type("lyng.Int"))),
moduleName = "lyng.stdlib"
) {
val self = thisAs<ObjList>()
if (args.isNotEmpty()) {
val count = requireOnlyArg<ObjInt>().value.toInt()
val size = self.list.size
if (count >= size) self.list.clear()
else self.list.subList(size - count, size).clear()
} else self.list.removeLast()
self
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val self = scp.thisAs<ObjList>()
if (scp.args.isNotEmpty()) {
val count = scp.requireOnlyArg<ObjInt>().value.toInt()
val size = self.list.size
if (count >= size) self.list.clear()
else self.list.subList(size - count, size).clear()
} else self.list.removeLast()
return self
}
}
)
addFnDoc(
name = "removeRange",
doc = "Remove a range of elements. Accepts a Range or (start, endInclusive). Returns the list.",
params = listOf(ParamDoc("range")),
moduleName = "lyng.stdlib"
) {
val self = thisAs<ObjList>()
val list = self.list
val range = requiredArg<Obj>(0)
if (range is ObjRange) {
val index = range
when {
index.start is ObjInt && index.end is ObjInt -> {
if (index.isEndInclusive)
list.subList(index.start.toInt(), index.end.toInt() + 1)
else
list.subList(index.start.toInt(), index.end.toInt())
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val self = scp.thisAs<ObjList>()
val list = self.list
val range = scp.requiredArg<Obj>(0)
if (range is ObjRange) {
val index = range
when {
index.start is ObjInt && index.end is ObjInt -> {
if (index.isEndInclusive)
list.subList(index.start.toInt(), index.end.toInt() + 1)
else
list.subList(index.start.toInt(), index.end.toInt())
}
index.isOpenStart && !index.isOpenEnd -> {
if (index.isEndInclusive)
list.subList(0, index.end!!.toInt() + 1)
else
list.subList(0, index.end!!.toInt())
}
index.isOpenStart && !index.isOpenEnd -> {
if (index.isEndInclusive)
list.subList(0, index.end!!.toInt() + 1)
else
list.subList(0, index.end!!.toInt())
}
index.isOpenEnd && !index.isOpenStart -> {
list.subList(index.start!!.toInt(), list.size)
}
index.isOpenEnd && !index.isOpenStart -> {
list.subList(index.start!!.toInt(), list.size)
}
index.isOpenStart && index.isOpenEnd -> {
list
}
index.isOpenStart && index.isOpenEnd -> {
list
}
else -> {
throw RuntimeException("Can't apply range for index: $index")
else -> {
throw RuntimeException("Can't apply range for index: $index")
}
}.clear()
} else {
val start = range.toInt()
val end = scp.requiredArg<ObjInt>(1).value.toInt() + 1
self.list.subList(start, end).clear()
}
}.clear()
} else {
val start = range.toInt()
val end = requiredArg<ObjInt>(1).value.toInt() + 1
self.list.subList(start, end).clear()
return self
}
}
self
}
)
addFnDoc(
name = "sortWith",
doc = "Sort this list in-place using a comparator function (a, b) -> Int.",
params = listOf(ParamDoc("comparator")),
moduleName = "lyng.stdlib"
) {
val comparator = requireOnlyArg<Statement>()
thisAs<ObjList>().quicksort { a, b -> comparator.call(this, a, b).toInt() }
ObjVoid
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val comparator = scp.requireOnlyArg<Statement>()
scp.thisAs<ObjList>().quicksort { a, b -> comparator.invokeCallable(scp, scp.thisObj, a, b).toInt() }
return ObjVoid
}
}
)
addFnDoc(
name = "shuffle",
doc = "Shuffle elements of this list in-place.",
moduleName = "lyng.stdlib"
) {
thisAs<ObjList>().list.shuffle()
ObjVoid
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
scp.thisAs<ObjList>().list.shuffle()
return ObjVoid
}
}
)
addFnDoc(
name = "sum",
doc = "Sum elements using dynamic '+' or optimized integer path. Returns null for empty lists.",
moduleName = "lyng.stdlib"
) {
val self = thisAs<ObjList>()
val l = self.list
if (l.isEmpty()) return@addFnDoc ObjNull
if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) {
// Fast path: all ints → accumulate as long
var i = 0
var acc: Long = 0
while (i < l.size) {
val v = l[i]
if (v is ObjInt) {
acc += v.value
i++
} else {
// Fallback to generic dynamic '+' accumulation starting from current acc
var res: Obj = ObjInt(acc)
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val self = scp.thisAs<ObjList>()
val l = self.list
if (l.isEmpty()) return ObjNull
if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) {
// Fast path: all ints → accumulate as long
var i = 0
var acc: Long = 0
while (i < l.size) {
res = res.plus(this, l[i])
i++
val v = l[i]
if (v is ObjInt) {
acc += v.value
i++
} else {
// Fallback to generic dynamic '+' accumulation starting from current acc
var res: Obj = ObjInt(acc)
while (i < l.size) {
res = res.plus(scp, l[i])
i++
}
return res
}
}
return@addFnDoc res
return ObjInt(acc)
}
// Generic path: dynamic '+' starting from first element
var res: Obj = l[0]
var k = 1
while (k < l.size) {
res = res.plus(scp, l[k])
k++
}
return res
}
return@addFnDoc ObjInt(acc)
}
// Generic path: dynamic '+' starting from first element
var res: Obj = l[0]
var k = 1
while (k < l.size) {
res = res.plus(this, l[k])
k++
}
res
}
)
addFnDoc(
name = "min",
doc = "Minimum element by natural order. Returns null for empty lists.",
moduleName = "lyng.stdlib"
) {
val l = thisAs<ObjList>().list
if (l.isEmpty()) return@addFnDoc ObjNull
if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) {
var i = 0
var hasOnlyInts = true
var minVal: Long = Long.MAX_VALUE
while (i < l.size) {
val v = l[i]
if (v is ObjInt) {
if (v.value < minVal) minVal = v.value
} else {
hasOnlyInts = false
break
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val l = scp.thisAs<ObjList>().list
if (l.isEmpty()) return ObjNull
if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) {
var i = 0
var hasOnlyInts = true
var minVal: Long = Long.MAX_VALUE
while (i < l.size) {
val v = l[i]
if (v is ObjInt) {
if (v.value < minVal) minVal = v.value
} else {
hasOnlyInts = false
break
}
i++
}
if (hasOnlyInts) return ObjInt(minVal)
}
i++
var res: Obj = l[0]
var i = 1
while (i < l.size) {
val v = l[i]
if (v.compareTo(scp, res) < 0) res = v
i++
}
return res
}
if (hasOnlyInts) return@addFnDoc ObjInt(minVal)
}
var res: Obj = l[0]
var i = 1
while (i < l.size) {
val v = l[i]
if (v.compareTo(this, res) < 0) res = v
i++
}
res
}
)
addFnDoc(
name = "max",
doc = "Maximum element by natural order. Returns null for empty lists.",
moduleName = "lyng.stdlib"
) {
val l = thisAs<ObjList>().list
if (l.isEmpty()) return@addFnDoc ObjNull
if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) {
var i = 0
var hasOnlyInts = true
var maxVal: Long = Long.MIN_VALUE
while (i < l.size) {
val v = l[i]
if (v is ObjInt) {
if (v.value > maxVal) maxVal = v.value
} else {
hasOnlyInts = false
break
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val l = scp.thisAs<ObjList>().list
if (l.isEmpty()) return ObjNull
if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS) {
var i = 0
var hasOnlyInts = true
var maxVal: Long = Long.MIN_VALUE
while (i < l.size) {
val v = l[i]
if (v is ObjInt) {
if (v.value > maxVal) maxVal = v.value
} else {
hasOnlyInts = false
break
}
i++
}
if (hasOnlyInts) return ObjInt(maxVal)
}
i++
var res: Obj = l[0]
var i = 1
while (i < l.size) {
val v = l[i]
if (v.compareTo(scp, res) > 0) res = v
i++
}
return res
}
if (hasOnlyInts) return@addFnDoc ObjInt(maxVal)
}
var res: Obj = l[0]
var i = 1
while (i < l.size) {
val v = l[i]
if (v.compareTo(this, res) > 0) res = v
i++
}
res
}
)
addFnDoc(
name = "indexOf",
doc = "Index of the first occurrence of the given element, or -1 if not found.",
params = listOf(ParamDoc("element")),
returns = type("lyng.Int"),
moduleName = "lyng.stdlib"
) {
val l = thisAs<ObjList>().list
val needle = args.firstAndOnly()
if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS && needle is ObjInt) {
var i = 0
while (i < l.size) {
val v = l[i]
if (v is ObjInt && v.value == needle.value) return@addFnDoc ObjInt(i.toLong())
i++
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val l = scp.thisAs<ObjList>().list
val needle = scp.args.firstAndOnly()
if (net.sergeych.lyng.PerfFlags.PRIMITIVE_FASTOPS && needle is ObjInt) {
var i = 0
while (i < l.size) {
val v = l[i]
if (v is ObjInt && v.value == needle.value) return ObjInt(i.toLong())
i++
}
return ObjInt((-1).toLong())
}
var i = 0
while (i < l.size) {
if (l[i].compareTo(scp, needle) == 0) return ObjInt(i.toLong())
i++
}
return ObjInt((-1).toLong())
}
return@addFnDoc ObjInt((-1).toLong())
}
var i = 0
while (i < l.size) {
if (l[i].compareTo(this, needle) == 0) return@addFnDoc ObjInt(i.toLong())
i++
}
ObjInt((-1).toLong())
}
)
}
}
}
@@ -20,6 +20,7 @@ package net.sergeych.lyng.obj
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeCallable
import net.sergeych.lyng.Statement
import net.sergeych.lyng.miniast.*
import net.sergeych.lynon.LynonDecoder
@@ -78,21 +79,27 @@ class ObjMapEntry(val key: Obj, val value: Obj) : Obj() {
doc = "Key component of this map entry.",
type = type("lyng.Any"),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjMapEntry>().key }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjMapEntry>().key
}
)
addPropertyDoc(
name = "value",
doc = "Value component of this map entry.",
type = type("lyng.Any"),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjMapEntry>().value }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjMapEntry>().value
}
)
addPropertyDoc(
name = "size",
doc = "Number of components in this entry (always 2).",
type = type("lyng.Int"),
moduleName = "lyng.stdlib",
getter = { 2.toObj() }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = 2.toObj()
}
)
}
}
@@ -181,9 +188,11 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
}
override suspend fun toJson(scope: Scope): JsonElement {
return JsonObject(
map.map { it.key.toString(scope).value to it.value.toJson(scope) }.toMap()
)
val res = mutableMapOf<String, JsonElement>()
for ((k, v) in map) {
res[k.toString(scope).value] = v.toJson(scope)
}
return JsonObject(res)
}
companion object {
@@ -224,94 +233,119 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
}
}.apply {
implementingNames.add("Delegate")
addFn("getValue") {
val self = thisAs<ObjMap>()
val key = requiredArg<Obj>(1)
self.map[key] ?: ObjNull
}
addFn("setValue") {
val self = thisAs<ObjMap>()
val key = requiredArg<Obj>(1)
val value = requiredArg<Obj>(2)
self.map[key] = value
self
}
addFn("bind") {
val mode = requiredArg<ObjEnumEntry>(1)
if( mode.ordinal.value > 1)
raiseIllegalArgument("Map can be delegated only to val or var, got ${mode.name.value}")
thisObj
}
addFn("getValue", code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val self = scp.thisAs<ObjMap>()
val key = scp.requiredArg<Obj>(1)
return self.map[key] ?: ObjNull
}
})
addFn("setValue", code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val self = scp.thisAs<ObjMap>()
val key = scp.requiredArg<Obj>(1)
val value = scp.requiredArg<Obj>(2)
self.map[key] = value
return self
}
})
addFn("bind", code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val mode = scp.requiredArg<ObjEnumEntry>(1)
if (mode.ordinal.value > 1)
scp.raiseIllegalArgument("Map can be delegated only to val or var, got ${mode.name.value}")
return scp.thisObj
}
})
addFnDoc(
name = "getOrNull",
doc = "Get value by key or return null if the key is absent.",
params = listOf(ParamDoc("key")),
returns = type("lyng.Any", nullable = true),
moduleName = "lyng.stdlib"
) {
val key = args.firstAndOnly(pos)
thisAs<ObjMap>().map.getOrElse(key) { ObjNull }
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val key = scp.args.firstAndOnly(scp.pos)
return scp.thisAs<ObjMap>().map.getOrElse(key) { ObjNull }
}
}
)
addFnDoc(
name = "getOrPut",
doc = "Get value by key or compute, store, and return the default from a lambda.",
params = listOf(ParamDoc("key"), ParamDoc("default")),
returns = type("lyng.Any"),
moduleName = "lyng.stdlib"
) {
val key = requiredArg<Obj>(0)
thisAs<ObjMap>().map.getOrPut(key) {
val lambda = requiredArg<Statement>(1)
lambda.execute(this)
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val key = scp.requiredArg<Obj>(0)
return scp.thisAs<ObjMap>().map.getOrPut(key) {
val lambda = scp.requiredArg<Statement>(1)
lambda.execute(scp)
}
}
}
}
)
addPropertyDoc(
name = "size",
doc = "Number of entries in the map.",
type = type("lyng.Int"),
moduleName = "lyng.stdlib",
getter = { (this.thisObj as ObjMap).map.size.toObj() }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = (scp.thisObj as ObjMap).map.size.toObj()
}
)
addFnDoc(
name = "remove",
doc = "Remove the entry by key and return the previous value or null if absent.",
params = listOf(ParamDoc("key")),
returns = type("lyng.Any", nullable = true),
moduleName = "lyng.stdlib"
) {
thisAs<ObjMap>().map.remove(requiredArg<Obj>(0))?.toObj() ?: ObjNull
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return scp.thisAs<ObjMap>().map.remove(scp.requiredArg<Obj>(0))?.toObj() ?: ObjNull
}
}
)
addFnDoc(
name = "clear",
doc = "Remove all entries from this map. Returns the map.",
returns = type("lyng.Map"),
moduleName = "lyng.stdlib"
) {
thisAs<ObjMap>().map.clear()
thisObj
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
scp.thisAs<ObjMap>().map.clear()
return scp.thisObj
}
}
)
addPropertyDoc(
name = "keys",
doc = "List of keys in this map.",
type = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.Any"))),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjMap>().map.keys.toObj() }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjMap>().map.keys.toObj()
}
)
addPropertyDoc(
name = "values",
doc = "List of values in this map.",
type = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.Any"))),
moduleName = "lyng.stdlib",
getter = { ObjList(thisAs<ObjMap>().map.values.toMutableList()) }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjList(scp.thisAs<ObjMap>().map.values.toMutableList())
}
)
addFnDoc(
name = "iterator",
doc = "Iterator over map entries as MapEntry objects.",
returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.MapEntry"))),
moduleName = "lyng.stdlib"
) {
ObjKotlinIterator(thisAs<ObjMap>().map.entries.iterator())
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjKotlinIterator(scp.thisAs<ObjMap>().map.entries.iterator())
}
)
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* 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.
@@ -40,12 +40,12 @@ class ObjMutex(val mutex: Mutex): Obj() {
params = listOf(ParamDoc("action")),
returns = type("lyng.Any"),
moduleName = "lyng.stdlib"
) {
val f = requiredArg<Statement>(0)
) { scp ->
val f = scp.requiredArg<Statement>(0)
// Execute user lambda directly in the current scope to preserve the active scope
// ancestry across suspension points. The lambda still constructs a ClosureScope
// on top of this frame, and parseLambdaExpression sets skipScopeCreation for its body.
thisAs<ObjMutex>().mutex.withLock { f.execute(this) }
scp.thisAs<ObjMutex>().mutex.withLock { f.execute(scp) }
}
}
}
@@ -18,6 +18,7 @@
package net.sergeych.lyng.obj
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeCallable
import net.sergeych.lyng.miniast.TypeGenericDoc
import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.addPropertyDoc
@@ -115,17 +116,17 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
start is ObjChar && end is ObjChar
}
override suspend fun enumerate(scope: Scope, callback: suspend (Obj) -> Boolean) {
override suspend fun enumerate(scope: Scope, callback: EnumerateCallback) {
if (start is ObjInt && end is ObjInt) {
val s = start.value
val e = end.value
if (isEndInclusive) {
for (i in s..e) {
if (!callback(ObjInt.of(i))) break
if (!callback.call(ObjInt.of(i))) break
}
} else {
for (i in s..<e) {
if (!callback(ObjInt.of(i))) break
if (!callback.call(ObjInt.of(i))) break
}
}
} else if (start is ObjChar && end is ObjChar) {
@@ -133,11 +134,11 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
val e = end.value
if (isEndInclusive) {
for (c in s..e) {
if (!callback(ObjChar(c))) break
if (!callback.call(ObjChar(c))) break
}
} else {
for (c in s..<e) {
if (!callback(ObjChar(c))) break
if (!callback.call(ObjChar(c))) break
}
}
} else {
@@ -180,64 +181,79 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
doc = "Start bound of the range or null if open.",
type = type("lyng.Any", nullable = true),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjRange>().start ?: ObjNull }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjRange>().start ?: ObjNull
}
)
addPropertyDoc(
name = "end",
doc = "End bound of the range or null if open.",
type = type("lyng.Any", nullable = true),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjRange>().end ?: ObjNull }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjRange>().end ?: ObjNull
}
)
addPropertyDoc(
name = "isOpen",
doc = "Whether the range is open on either side (no start or no end).",
type = type("lyng.Bool"),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjRange>().let { it.start == null || it.end == null }.toObj() }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjRange>().let { it.start == null || it.end == null }.toObj()
}
)
addPropertyDoc(
name = "isIntRange",
doc = "True if both bounds are Int values.",
type = type("lyng.Bool"),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjRange>().isIntRange.toObj() }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjRange>().isIntRange.toObj()
}
)
addPropertyDoc(
name = "isCharRange",
doc = "True if both bounds are Char values.",
type = type("lyng.Bool"),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjRange>().isCharRange.toObj() }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjRange>().isCharRange.toObj()
}
)
addPropertyDoc(
name = "isEndInclusive",
doc = "Whether the end bound is inclusive.",
type = type("lyng.Bool"),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjRange>().isEndInclusive.toObj() }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjRange>().isEndInclusive.toObj()
}
)
addFnDoc(
name = "iterator",
doc = "Iterator over elements in this range (optimized for Int ranges).",
returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.Any"))),
moduleName = "lyng.stdlib"
) {
val self = thisAs<ObjRange>()
if (net.sergeych.lyng.PerfFlags.RANGE_FAST_ITER) {
val s = self.start
val e = self.end
if (s is ObjInt && e is ObjInt) {
val start = s.value.toInt()
val endExclusive = (if (self.isEndInclusive) e.value.toInt() + 1 else e.value.toInt())
// Only for ascending simple ranges; fall back otherwise
if (start <= endExclusive) {
return@addFnDoc ObjFastIntRangeIterator(start, endExclusive)
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val self = scp.thisAs<ObjRange>()
if (net.sergeych.lyng.PerfFlags.RANGE_FAST_ITER) {
val s = self.start
val e = self.end
if (s is ObjInt && e is ObjInt) {
val start = s.value.toInt()
val endExclusive = (if (self.isEndInclusive) e.value.toInt() + 1 else e.value.toInt())
// Only for ascending simple ranges; fall back otherwise
if (start <= endExclusive) {
return ObjFastIntRangeIterator(start, endExclusive)
}
}
}
return ObjRangeIterator(self).apply { init(scp) }
}
}
ObjRangeIterator(self).apply { init() }
}
)
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* 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.
@@ -19,6 +19,7 @@ package net.sergeych.lyng.obj
import net.sergeych.lyng.PerfFlags
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeCallable
class ObjRangeIterator(val self: ObjRange) : Obj() {
@@ -28,7 +29,7 @@ class ObjRangeIterator(val self: ObjRange) : Obj() {
override val objClass: ObjClass get() = type
fun Scope.init() {
fun init(scope: Scope) {
val s = self.start
val e = self.end
if (s is ObjInt && e is ObjInt) {
@@ -43,7 +44,7 @@ class ObjRangeIterator(val self: ObjRange) : Obj() {
else
(e.value.code - s.value.code)
} else {
raiseError("not implemented iterator for range of $this")
scope.raiseError("not implemented iterator for range of $this")
}
}
@@ -66,12 +67,14 @@ class ObjRangeIterator(val self: ObjRange) : Obj() {
companion object {
val type = ObjClass("RangeIterator", ObjIterator).apply {
addFn("hasNext") {
thisAs<ObjRangeIterator>().hasNext().toObj()
}
addFn("next") {
thisAs<ObjRangeIterator>().next(this)
}
addFn("hasNext", code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj =
scp.thisAs<ObjRangeIterator>().hasNext().toObj()
})
addFn("next", code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj =
scp.thisAs<ObjRangeIterator>().next(scp)
})
}
}
}
@@ -94,8 +97,12 @@ class ObjFastIntRangeIterator(private val start: Int, private val endExclusive:
companion object {
val type = ObjClass("FastIntRangeIterator", ObjIterator).apply {
addFn("hasNext") { thisAs<ObjFastIntRangeIterator>().hasNext().toObj() }
addFn("next") { thisAs<ObjFastIntRangeIterator>().next(this) }
addFn("hasNext", code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjFastIntRangeIterator>().hasNext().toObj()
})
addFn("next", code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjFastIntRangeIterator>().next(scp)
})
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* 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.
@@ -21,6 +21,7 @@ import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import net.sergeych.lyng.Pos
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeCallable
import net.sergeych.lyng.miniast.addConstDoc
import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.type
@@ -126,9 +127,9 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
// roundToInt: number rounded to the nearest integer
addConstDoc(
name = "roundToInt",
value = statement(Pos.builtIn) {
(it.thisObj as ObjReal).value.roundToLong().toObj()
},
value = statement(Pos.builtIn, f = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = (scp.thisObj as ObjReal).value.roundToLong().toObj()
}),
doc = "This real number rounded to the nearest integer.",
type = type("lyng.Int"),
moduleName = "lyng.stdlib"
@@ -137,10 +138,11 @@ data class ObjReal(val value: Double) : Obj(), Numeric {
name = "toInt",
doc = "Truncate this real number toward zero to an integer.",
returns = type("lyng.Int"),
moduleName = "lyng.stdlib"
) {
ObjInt.of(thisAs<ObjReal>().value.toLong())
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjInt.of(scp.thisAs<ObjReal>().value.toLong())
}
)
}
}
}
@@ -64,9 +64,40 @@ sealed interface ObjRef {
}
}
interface RecordProvider {
suspend fun getRecord(scope: Scope): ObjRecord
}
fun interface FieldGetter {
suspend fun call(obj: Obj, scope: Scope): ObjRecord
}
fun interface FieldSetter {
suspend fun call(obj: Obj, scope: Scope, value: Obj)
}
fun interface IndexGetter {
suspend fun call(obj: Obj, scope: Scope, index: Obj): Obj
}
fun interface IndexSetter {
suspend fun call(obj: Obj, scope: Scope, index: Obj, value: Obj)
}
fun interface MethodInvoker {
suspend fun call(obj: Obj, scope: Scope, args: Arguments): Obj
}
/** Runtime-computed read-only reference backed by a lambda. */
class ValueFnRef(private val fn: suspend (Scope) -> ObjRecord) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord = fn(scope)
class ValueFnRef(private val provider: RecordProvider) : ObjRef {
constructor(fn: suspend (Scope) -> ObjRecord) : this(object : RecordProvider {
override suspend fun getRecord(scope: Scope): ObjRecord = fn(scope)
})
override suspend fun get(scope: Scope): ObjRecord {
return provider.getRecord(scope)
}
}
/** Unary operations supported by ObjRef. */
@@ -523,16 +554,16 @@ class FieldRef(
) : ObjRef {
// 4-entry PIC for reads/writes (guarded by PerfFlags.FIELD_PIC)
// Reads
private var rKey1: Long = 0L; private var rVer1: Int = -1; private var rGetter1: (suspend (Obj, Scope) -> ObjRecord)? = null
private var rKey2: Long = 0L; private var rVer2: Int = -1; private var rGetter2: (suspend (Obj, Scope) -> ObjRecord)? = null
private var rKey3: Long = 0L; private var rVer3: Int = -1; private var rGetter3: (suspend (Obj, Scope) -> ObjRecord)? = null
private var rKey4: Long = 0L; private var rVer4: Int = -1; private var rGetter4: (suspend (Obj, Scope) -> ObjRecord)? = null
private var rKey1: Long = 0L; private var rVer1: Int = -1; private var rGetter1: FieldGetter? = null
private var rKey2: Long = 0L; private var rVer2: Int = -1; private var rGetter2: FieldGetter? = null
private var rKey3: Long = 0L; private var rVer3: Int = -1; private var rGetter3: FieldGetter? = null
private var rKey4: Long = 0L; private var rVer4: Int = -1; private var rGetter4: FieldGetter? = null
// Writes
private var wKey1: Long = 0L; private var wVer1: Int = -1; private var wSetter1: (suspend (Obj, Scope, Obj) -> Unit)? = null
private var wKey2: Long = 0L; private var wVer2: Int = -1; private var wSetter2: (suspend (Obj, Scope, Obj) -> Unit)? = null
private var wKey3: Long = 0L; private var wVer3: Int = -1; private var wSetter3: (suspend (Obj, Scope, Obj) -> Unit)? = null
private var wKey4: Long = 0L; private var wVer4: Int = -1; private var wSetter4: (suspend (Obj, Scope, Obj) -> Unit)? = null
private var wKey1: Long = 0L; private var wVer1: Int = -1; private var wSetter1: FieldSetter? = null
private var wKey2: Long = 0L; private var wVer2: Int = -1; private var wSetter2: FieldSetter? = null
private var wKey3: Long = 0L; private var wVer3: Int = -1; private var wSetter3: FieldSetter? = null
private var wKey4: Long = 0L; private var wVer4: Int = -1; private var wSetter4: FieldSetter? = null
// Transient per-step cache to optimize read-then-write sequences within the same frame
private var tKey: Long = 0L; private var tVer: Int = -1; private var tFrameId: Long = -1L; private var tRecord: ObjRecord? = null
@@ -598,7 +629,7 @@ class FieldRef(
rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) {
if (picCounters) PerfStats.fieldPicHit++
noteReadHit()
val rec0 = g(base, scope)
val rec0 = g.call(base, scope)
if (base is ObjClass) {
val idx0 = base.classScope?.getSlotIndexOf(name)
if (idx0 != null) { tKey = key; tVer = ver; tFrameId = scope.frameId; tRecord = rec0 } else { tRecord = null }
@@ -612,7 +643,7 @@ class FieldRef(
val tK = rKey2; val tV = rVer2; val tG = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tK; rVer1 = tV; rGetter1 = tG
val rec0 = g(base, scope)
val rec0 = g.call(base, scope)
if (base is ObjClass) {
val idx0 = base.classScope?.getSlotIndexOf(name)
if (idx0 != null) { tKey = key; tVer = ver; tFrameId = scope.frameId; tRecord = rec0 } else { tRecord = null }
@@ -627,7 +658,7 @@ class FieldRef(
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tK; rVer1 = tV; rGetter1 = tG
val rec0 = g(base, scope)
val rec0 = g.call(base, scope)
if (base is ObjClass) {
val idx0 = base.classScope?.getSlotIndexOf(name)
if (idx0 != null) { tKey = key; tVer = ver; tFrameId = scope.frameId; tRecord = rec0 } else { tRecord = null }
@@ -643,7 +674,7 @@ class FieldRef(
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tK; rVer1 = tV; rGetter1 = tG
val rec0 = g(base, scope)
val rec0 = g.call(base, scope)
if (base is ObjClass) {
val idx0 = base.classScope?.getSlotIndexOf(name)
if (idx0 != null) { tKey = key; tVer = ver; tFrameId = scope.frameId; tRecord = rec0 } else { tRecord = null }
@@ -660,7 +691,7 @@ class FieldRef(
rKey4 = rKey3; rVer4 = rVer3; rGetter4 = rGetter3
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = key; rVer1 = ver; rGetter1 = { _, sc -> sc.raiseError(e.message ?: "no such field: $name") }
rKey1 = key; rVer1 = ver; rGetter1 = FieldGetter { _, sc -> sc.raiseError(e.message ?: "no such field: $name") }
throw e
}
// Install move-to-front with a handle-aware getter; honor PIC size flag
@@ -674,7 +705,7 @@ class FieldRef(
val clsScope = base.classScope
val capturedIdx = clsScope?.getSlotIndexOf(name)
if (clsScope != null && capturedIdx != null) {
rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc ->
rKey1 = key; rVer1 = ver; rGetter1 = FieldGetter { obj, sc ->
val scope0 = (obj as ObjClass).classScope!!
val r0 = scope0.getSlotRecord(capturedIdx)
if (!r0.visibility.isPublic)
@@ -682,14 +713,14 @@ class FieldRef(
r0
}
} else {
rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc -> obj.readField(sc, name) }
rKey1 = key; rVer1 = ver; rGetter1 = FieldGetter { obj, sc -> obj.readField(sc, name) }
}
}
is ObjInstance -> {
val cls = base.objClass
val effectiveKey = cls.publicMemberResolution[name]
if (effectiveKey != null) {
rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc ->
rKey1 = key; rVer1 = ver; rGetter1 = FieldGetter { obj, sc ->
if (obj is ObjInstance && obj.objClass === cls) {
val rec = obj.instanceScope.objects[effectiveKey]
if (rec != null && rec.type != ObjRecord.Type.Delegated) rec
@@ -697,12 +728,12 @@ class FieldRef(
} else obj.readField(sc, name)
}
} else {
rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc -> obj.readField(sc, name) }
rKey1 = key; rVer1 = ver; rGetter1 = FieldGetter { obj, sc -> obj.readField(sc, name) }
}
}
else -> {
// For instances and other types, fall back to name-based lookup per access (slot index may differ per instance)
rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc -> obj.readField(sc, name) }
rKey1 = key; rVer1 = ver; rGetter1 = FieldGetter { obj, sc -> obj.readField(sc, name) }
}
}
return rec
@@ -745,7 +776,7 @@ class FieldRef(
wSetter1?.let { s -> if (key == wKey1 && ver == wVer1) {
if (picCounters) PerfStats.fieldPicSetHit++
noteWriteHit()
return s(base, scope, newValue)
return s.call(base, scope, newValue)
} }
wSetter2?.let { s -> if (key == wKey2 && ver == wVer2) {
if (picCounters) PerfStats.fieldPicSetHit++
@@ -754,7 +785,7 @@ class FieldRef(
val tK = wKey2; val tV = wVer2; val tS = wSetter2
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
wKey1 = tK; wVer1 = tV; wSetter1 = tS
return s(base, scope, newValue)
return s.call(base, scope, newValue)
} }
if (size4WritesEnabled()) wSetter3?.let { s -> if (key == wKey3 && ver == wVer3) {
if (picCounters) PerfStats.fieldPicSetHit++
@@ -764,7 +795,7 @@ class FieldRef(
wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
wKey1 = tK; wVer1 = tV; wSetter1 = tS
return s(base, scope, newValue)
return s.call(base, scope, newValue)
} }
if (size4WritesEnabled()) wSetter4?.let { s -> if (key == wKey4 && ver == wVer4) {
if (picCounters) PerfStats.fieldPicSetHit++
@@ -775,7 +806,7 @@ class FieldRef(
wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
wKey1 = tK; wVer1 = tV; wSetter1 = tS
return s(base, scope, newValue)
return s.call(base, scope, newValue)
} }
// Slow path
if (picCounters) PerfStats.fieldPicSetMiss++
@@ -792,7 +823,7 @@ class FieldRef(
val clsScope = base.classScope
val capturedIdx = clsScope?.getSlotIndexOf(name)
if (clsScope != null && capturedIdx != null) {
wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, v ->
wKey1 = key; wVer1 = ver; wSetter1 = FieldSetter { obj, sc, v ->
val scope0 = (obj as ObjClass).classScope!!
val r0 = scope0.getSlotRecord(capturedIdx)
if (!r0.isMutable)
@@ -800,14 +831,14 @@ class FieldRef(
if (r0.value.assign(sc, v) == null) r0.value = v
}
} else {
wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, v -> obj.writeField(sc, name, v) }
wKey1 = key; wVer1 = ver; wSetter1 = FieldSetter { obj, sc, v -> obj.writeField(sc, name, v) }
}
}
is ObjInstance -> {
val cls = base.objClass
val effectiveKey = cls.publicMemberResolution[name]
if (effectiveKey != null) {
wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, nv ->
wKey1 = key; wVer1 = ver; wSetter1 = FieldSetter { obj, sc, nv ->
if (obj is ObjInstance && obj.objClass === cls) {
val rec = obj.instanceScope.objects[effectiveKey]
if (rec != null && rec.effectiveWriteVisibility == Visibility.Public && rec.isMutable && rec.type == ObjRecord.Type.Field) {
@@ -816,12 +847,12 @@ class FieldRef(
} else obj.writeField(sc, name, nv)
}
} else {
wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, v -> obj.writeField(sc, name, v) }
wKey1 = key; wVer1 = ver; wSetter1 = FieldSetter { obj, sc, v -> obj.writeField(sc, name, v) }
}
}
else -> {
// For instances and other types, fall back to generic write (instance slot indices may differ per instance)
wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, v -> obj.writeField(sc, name, v) }
wKey1 = key; wVer1 = ver; wSetter1 = FieldSetter { obj, sc, v -> obj.writeField(sc, name, v) }
}
}
return
@@ -853,14 +884,14 @@ class FieldRef(
if (key != 0L) {
rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) {
if (picCounters) PerfStats.fieldPicHit++
return g(base, scope).value
return g.call(base, scope).value
} }
rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) {
if (picCounters) PerfStats.fieldPicHit++
val tK = rKey2; val tV = rVer2; val tG = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tK; rVer1 = tV; rGetter1 = tG
return g(base, scope).value
return g.call(base, scope).value
} }
if (size4ReadsEnabled()) rGetter3?.let { g -> if (key == rKey3 && ver == rVer3) {
if (picCounters) PerfStats.fieldPicHit++
@@ -868,7 +899,7 @@ class FieldRef(
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tK; rVer1 = tV; rGetter1 = tG
return g(base, scope).value
return g.call(base, scope).value
} }
if (size4ReadsEnabled()) rGetter4?.let { g -> if (key == rKey4 && ver == rVer4) {
if (picCounters) PerfStats.fieldPicHit++
@@ -877,12 +908,12 @@ class FieldRef(
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tK; rVer1 = tV; rGetter1 = tG
return g(base, scope).value
return g.call(base, scope).value
} }
if (picCounters) PerfStats.fieldPicMiss++
val rec = base.readField(scope, name)
// install primary generic getter for this shape
rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc -> obj.readField(sc, name) }
rKey1 = key; rVer1 = ver; rGetter1 = FieldGetter { obj, sc -> obj.readField(sc, name) }
return rec.value
}
}
@@ -899,16 +930,16 @@ class IndexRef(
private val isOptional: Boolean,
) : ObjRef {
// Tiny 4-entry PIC for index reads (guarded implicitly by RVAL_FASTPATH); move-to-front on hits
private var rKey1: Long = 0L; private var rVer1: Int = -1; private var rGetter1: (suspend (Obj, Scope, Obj) -> Obj)? = null
private var rKey2: Long = 0L; private var rVer2: Int = -1; private var rGetter2: (suspend (Obj, Scope, Obj) -> Obj)? = null
private var rKey3: Long = 0L; private var rVer3: Int = -1; private var rGetter3: (suspend (Obj, Scope, Obj) -> Obj)? = null
private var rKey4: Long = 0L; private var rVer4: Int = -1; private var rGetter4: (suspend (Obj, Scope, Obj) -> Obj)? = null
private var rKey1: Long = 0L; private var rVer1: Int = -1; private var rGetter1: IndexGetter? = null
private var rKey2: Long = 0L; private var rVer2: Int = -1; private var rGetter2: IndexGetter? = null
private var rKey3: Long = 0L; private var rVer3: Int = -1; private var rGetter3: IndexGetter? = null
private var rKey4: Long = 0L; private var rVer4: Int = -1; private var rGetter4: IndexGetter? = null
// Tiny 4-entry PIC for index writes
private var wKey1: Long = 0L; private var wVer1: Int = -1; private var wSetter1: (suspend (Obj, Scope, Obj, Obj) -> Unit)? = null
private var wKey2: Long = 0L; private var wVer2: Int = -1; private var wSetter2: (suspend (Obj, Scope, Obj, Obj) -> Unit)? = null
private var wKey3: Long = 0L; private var wVer3: Int = -1; private var wSetter3: (suspend (Obj, Scope, Obj, Obj) -> Unit)? = null
private var wKey4: Long = 0L; private var wVer4: Int = -1; private var wSetter4: (suspend (Obj, Scope, Obj, Obj) -> Unit)? = null
private var wKey1: Long = 0L; private var wVer1: Int = -1; private var wSetter1: IndexSetter? = null
private var wKey2: Long = 0L; private var wVer2: Int = -1; private var wSetter2: IndexSetter? = null
private var wKey3: Long = 0L; private var wVer3: Int = -1; private var wSetter3: IndexSetter? = null
private var wKey4: Long = 0L; private var wVer4: Int = -1; private var wSetter4: IndexSetter? = null
private fun receiverKeyAndVersion(obj: Obj): Pair<Long, Int> = when (obj) {
is ObjInstance -> obj.objClass.classId to obj.objClass.layoutVersion
@@ -949,14 +980,14 @@ class IndexRef(
if (key != 0L) {
rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) {
if (picCounters) PerfStats.indexPicHit++
return g(base, scope, idx).asMutable
return g.call(base, scope, idx).asMutable
} }
rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) {
if (picCounters) PerfStats.indexPicHit++
val tk = rKey2; val tv = rVer2; val tg = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tk; rVer1 = tv; rGetter1 = tg
return g(base, scope, idx).asMutable
return g.call(base, scope, idx).asMutable
} }
if (PerfFlags.INDEX_PIC_SIZE_4) rGetter3?.let { g -> if (key == rKey3 && ver == rVer3) {
if (picCounters) PerfStats.indexPicHit++
@@ -964,7 +995,7 @@ class IndexRef(
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tk; rVer1 = tv; rGetter1 = tg
return g(base, scope, idx).asMutable
return g.call(base, scope, idx).asMutable
} }
if (PerfFlags.INDEX_PIC_SIZE_4) rGetter4?.let { g -> if (key == rKey4 && ver == rVer4) {
if (picCounters) PerfStats.indexPicHit++
@@ -973,7 +1004,7 @@ class IndexRef(
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tk; rVer1 = tv; rGetter1 = tg
return g(base, scope, idx).asMutable
return g.call(base, scope, idx).asMutable
} }
// Miss: resolve and install generic handler
if (picCounters) PerfStats.indexPicMiss++
@@ -983,7 +1014,7 @@ class IndexRef(
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
}
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc, ix -> obj.getAt(sc, ix) }
rKey1 = key; rVer1 = ver; rGetter1 = IndexGetter { obj, sc, ix -> obj.getAt(sc, ix) }
return v.asMutable
}
}
@@ -1023,14 +1054,14 @@ class IndexRef(
if (key != 0L) {
rGetter1?.let { g -> if (key == rKey1 && ver == rVer1) {
if (picCounters) PerfStats.indexPicHit++
return g(base, scope, idx)
return g.call(base, scope, idx)
} }
rGetter2?.let { g -> if (key == rKey2 && ver == rVer2) {
if (picCounters) PerfStats.indexPicHit++
val tk = rKey2; val tv = rVer2; val tg = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tk; rVer1 = tv; rGetter1 = tg
return g(base, scope, idx)
return g.call(base, scope, idx)
} }
if (PerfFlags.INDEX_PIC_SIZE_4) rGetter3?.let { g -> if (key == rKey3 && ver == rVer3) {
if (picCounters) PerfStats.indexPicHit++
@@ -1038,7 +1069,7 @@ class IndexRef(
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tk; rVer1 = tv; rGetter1 = tg
return g(base, scope, idx)
return g.call(base, scope, idx)
} }
if (PerfFlags.INDEX_PIC_SIZE_4) rGetter4?.let { g -> if (key == rKey4 && ver == rVer4) {
if (picCounters) PerfStats.indexPicHit++
@@ -1047,7 +1078,7 @@ class IndexRef(
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = tk; rVer1 = tv; rGetter1 = tg
return g(base, scope, idx)
return g.call(base, scope, idx)
} }
if (picCounters) PerfStats.indexPicMiss++
val v = base.getAt(scope, idx)
@@ -1056,7 +1087,7 @@ class IndexRef(
rKey3 = rKey2; rVer3 = rVer2; rGetter3 = rGetter2
}
rKey2 = rKey1; rVer2 = rVer1; rGetter2 = rGetter1
rKey1 = key; rVer1 = ver; rGetter1 = { obj, sc, ix -> obj.getAt(sc, ix) }
rKey1 = key; rVer1 = ver; rGetter1 = IndexGetter { obj, sc, ix -> obj.getAt(sc, ix) }
return v
}
}
@@ -1093,19 +1124,19 @@ class IndexRef(
else -> { key = 0L; ver = -1 }
}
if (key != 0L) {
wSetter1?.let { s -> if (key == wKey1 && ver == wVer1) { s(base, scope, idx, newValue); return } }
wSetter1?.let { s -> if (key == wKey1 && ver == wVer1) { s.call(base, scope, idx, newValue); return } }
wSetter2?.let { s -> if (key == wKey2 && ver == wVer2) {
val tk = wKey2; val tv = wVer2; val ts = wSetter2
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
wKey1 = tk; wVer1 = tv; wSetter1 = ts
s(base, scope, idx, newValue); return
s.call(base, scope, idx, newValue); return
} }
if (PerfFlags.INDEX_PIC_SIZE_4) wSetter3?.let { s -> if (key == wKey3 && ver == wVer3) {
val tk = wKey3; val tv = wVer3; val ts = wSetter3
wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
wKey1 = tk; wVer1 = tv; wSetter1 = ts
s(base, scope, idx, newValue); return
s.call(base, scope, idx, newValue); return
} }
if (PerfFlags.INDEX_PIC_SIZE_4) wSetter4?.let { s -> if (key == wKey4 && ver == wVer4) {
val tk = wKey4; val tv = wVer4; val ts = wSetter4
@@ -1113,7 +1144,7 @@ class IndexRef(
wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
wKey1 = tk; wVer1 = tv; wSetter1 = ts
s(base, scope, idx, newValue); return
s.call(base, scope, idx, newValue); return
} }
// Miss: perform write and install generic handler
base.putAt(scope, idx, newValue)
@@ -1122,7 +1153,7 @@ class IndexRef(
wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2
}
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, ix, v -> obj.putAt(sc, ix, v) }
wKey1 = key; wVer1 = ver; wSetter1 = IndexSetter { obj, sc, ix, v -> obj.putAt(sc, ix, v) }
return
}
}
@@ -1173,10 +1204,10 @@ class MethodCallRef(
private val isOptional: Boolean,
) : ObjRef {
// 4-entry PIC for method invocations (guarded by PerfFlags.METHOD_PIC)
private var mKey1: Long = 0L; private var mVer1: Int = -1; private var mInvoker1: (suspend (Obj, Scope, Arguments) -> Obj)? = null
private var mKey2: Long = 0L; private var mVer2: Int = -1; private var mInvoker2: (suspend (Obj, Scope, Arguments) -> Obj)? = null
private var mKey3: Long = 0L; private var mVer3: Int = -1; private var mInvoker3: (suspend (Obj, Scope, Arguments) -> Obj)? = null
private var mKey4: Long = 0L; private var mVer4: Int = -1; private var mInvoker4: (suspend (Obj, Scope, Arguments) -> Obj)? = null
private var mKey1: Long = 0L; private var mVer1: Int = -1; private var mInvoker1: MethodInvoker? = null
private var mKey2: Long = 0L; private var mVer2: Int = -1; private var mInvoker2: MethodInvoker? = null
private var mKey3: Long = 0L; private var mVer3: Int = -1; private var mInvoker3: MethodInvoker? = null
private var mKey4: Long = 0L; private var mVer4: Int = -1; private var mInvoker4: MethodInvoker? = null
// Adaptive PIC (2→4) for methods
private var mAccesses: Int = 0; private var mMisses: Int = 0; private var mPromotedTo4: Boolean = false
@@ -1274,7 +1305,7 @@ class MethodCallRef(
if (key == mKey1 && ver == mVer1) {
if (picCounters) PerfStats.methodPicHit++
noteMethodHit()
return inv(base, scope, callArgs)
return inv.call(base, scope, callArgs)
}
}
mInvoker2?.let { inv ->
@@ -1285,7 +1316,7 @@ class MethodCallRef(
val tK = mKey2; val tV = mVer2; val tI = mInvoker2
mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1
mKey1 = tK; mVer1 = tV; mInvoker1 = tI
return inv(base, scope, callArgs)
return inv.call(base, scope, callArgs)
}
}
if (size4MethodsEnabled()) mInvoker3?.let { inv ->
@@ -1297,7 +1328,7 @@ class MethodCallRef(
mKey3 = mKey2; mVer3 = mVer2; mInvoker3 = mInvoker2
mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1
mKey1 = tK; mVer1 = tV; mInvoker1 = tI
return inv(base, scope, callArgs)
return inv.call(base, scope, callArgs)
}
}
if (size4MethodsEnabled()) mInvoker4?.let { inv ->
@@ -1310,7 +1341,7 @@ class MethodCallRef(
mKey3 = mKey2; mVer3 = mVer2; mInvoker3 = mInvoker2
mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1
mKey1 = tK; mVer1 = tV; mInvoker1 = tI
return inv(base, scope, callArgs)
return inv.call(base, scope, callArgs)
}
}
// Slow path
@@ -1323,7 +1354,7 @@ class MethodCallRef(
mKey4 = mKey3; mVer4 = mVer3; mInvoker4 = mInvoker3
mKey3 = mKey2; mVer3 = mVer2; mInvoker3 = mInvoker2
mKey2 = mKey1; mVer2 = mVer1; mInvoker2 = mInvoker1
mKey1 = key; mVer1 = ver; mInvoker1 = { _, sc, _ -> sc.raiseError(e.message ?: "method not found: $name") }
mKey1 = key; mVer1 = ver; mInvoker1 = MethodInvoker { _, sc, _ -> sc.raiseError(e.message ?: "method not found: $name") }
throw e
}
// Install move-to-front with a handle-aware invoker; honor PIC size flag
@@ -1361,15 +1392,15 @@ class MethodCallRef(
val visibility = hierarchyMember.visibility
val callable = hierarchyMember.value
val decl = hierarchyMember.declaringClass ?: base.objClass
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a ->
mKey1 = key; mVer1 = ver; mInvoker1 = MethodInvoker { obj, sc, a ->
val inst = obj as ObjInstance
if (!visibility.isPublic && !canAccessMember(visibility, decl, sc.currentClassCtx))
sc.raiseError(ObjIllegalAccessException(sc, "can't invoke non-public method $name"))
callable.invoke(inst.instanceScope, inst, a)
if (!visibility.isPublic && !canAccessMember(visibility, decl, sc.currentClassCtx, name))
sc.raiseError(ObjIllegalAccessException(sc, "can't invokeCallable non-public method $name"))
callable.invokeCallable(inst.instanceScope, inst, a)
}
} else {
// Fallback to name-based lookup per call (handles extensions and root members)
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> obj.invokeInstanceMethod(sc, name, a) }
mKey1 = key; mVer1 = ver; mInvoker1 = MethodInvoker { obj, sc, a -> obj.invokeInstanceMethod(sc, name, a) }
}
}
is ObjClass -> {
@@ -1377,13 +1408,13 @@ class MethodCallRef(
val rec = clsScope?.get(name)
if (rec != null) {
val callable = rec.value
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> callable.invoke(sc, obj, a) }
mKey1 = key; mVer1 = ver; mInvoker1 = MethodInvoker { obj, sc, a -> callable.invokeCallable(sc, obj, a) }
} else {
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> obj.invokeInstanceMethod(sc, name, a) }
mKey1 = key; mVer1 = ver; mInvoker1 = MethodInvoker { obj, sc, a -> obj.invokeInstanceMethod(sc, name, a) }
}
}
else -> {
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> obj.invokeInstanceMethod(sc, name, a) }
mKey1 = key; mVer1 = ver; mInvoker1 = MethodInvoker { obj, sc, a -> obj.invokeInstanceMethod(sc, name, a) }
}
}
return result
@@ -1447,7 +1478,7 @@ class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef {
val slot = if (hit) cachedSlot else resolveSlot(scope)
if (slot >= 0) {
val rec = scope.getSlotRecord(slot)
if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) {
if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name)) {
// Not visible via slot, fallback to other lookups
} else {
if (PerfFlags.PIC_DEBUG_COUNTERS) {
@@ -1499,7 +1530,7 @@ class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef {
val slot = if (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount()) cachedSlot else resolveSlot(scope)
if (slot >= 0) {
val rec = scope.getSlotRecord(slot)
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) {
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name)) {
scope.assign(rec, name, newValue)
return
}
@@ -1608,7 +1639,7 @@ class FastLocalVarRef(
val actualOwner = cachedOwnerScope
if (slot >= 0 && actualOwner != null) {
val rec = actualOwner.getSlotRecord(slot)
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) {
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name)) {
if (PerfFlags.PIC_DEBUG_COUNTERS) {
if (ownerValid) PerfStats.fastLocalHit++ else PerfStats.fastLocalMiss++
}
@@ -1652,7 +1683,7 @@ class FastLocalVarRef(
val actualOwner = cachedOwnerScope
if (slot >= 0 && actualOwner != null) {
val rec = actualOwner.getSlotRecord(slot)
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) {
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name)) {
return scope.resolve(rec, name)
}
}
@@ -1698,7 +1729,7 @@ class FastLocalVarRef(
val actualOwner = cachedOwnerScope
if (slot >= 0 && actualOwner != null) {
val rec = actualOwner.getSlotRecord(slot)
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) {
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx, name)) {
scope.assign(rec, name, newValue)
return
}
@@ -20,6 +20,7 @@ package net.sergeych.lyng.obj
import net.sergeych.lyng.PerfFlags
import net.sergeych.lyng.RegexCache
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeCallable
import net.sergeych.lyng.miniast.*
class ObjRegex(val regex: Regex) : Obj() {
@@ -49,29 +50,36 @@ class ObjRegex(val regex: Regex) : Obj() {
doc = "Whether the entire string matches this regular expression.",
params = listOf(ParamDoc("text", type("lyng.String"))),
returns = type("lyng.Bool"),
moduleName = "lyng.stdlib"
) {
ObjBool(args.firstAndOnly().toString().matches(thisAs<ObjRegex>().regex))
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj =
ObjBool(scp.args.firstAndOnly().toString().matches(scp.thisAs<ObjRegex>().regex))
}
)
addFnDoc(
name = "find",
doc = "Find the first match in the given string.",
params = listOf(ParamDoc("text", type("lyng.String"))),
returns = type("lyng.RegexMatch", nullable = true),
moduleName = "lyng.stdlib"
) {
thisAs<ObjRegex>().find(requireOnlyArg<ObjString>())
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj =
scp.thisAs<ObjRegex>().find(scp.requireOnlyArg<ObjString>())
}
)
addFnDoc(
name = "findAll",
doc = "Find all matches in the given string.",
params = listOf(ParamDoc("text", type("lyng.String"))),
returns = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.RegexMatch"))),
moduleName = "lyng.stdlib"
) {
val s = requireOnlyArg<ObjString>().value
ObjList(thisAs<ObjRegex>().regex.findAll(s).map { ObjRegexMatch(it) }.toMutableList())
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val s = scp.requireOnlyArg<ObjString>().value
return ObjList(scp.thisAs<ObjRegex>().regex.findAll(s).map { ObjRegexMatch(it) }.toMutableList())
}
}
)
}
}
}
@@ -125,21 +133,27 @@ class ObjRegexMatch(val match: MatchResult) : Obj() {
doc = "List of captured groups with index 0 as the whole match.",
type = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.String"))),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjRegexMatch>().objGroups }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjRegexMatch>().objGroups
}
)
addPropertyDoc(
name = "value",
doc = "The matched substring.",
type = type("lyng.String"),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjRegexMatch>().objValue }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjRegexMatch>().objValue
}
)
addPropertyDoc(
name = "range",
doc = "Range of the match in the input (end-exclusive).",
type = type("lyng.Range"),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjRegexMatch>().objRange }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjRegexMatch>().objRange
}
)
}
}
@@ -18,6 +18,7 @@
package net.sergeych.lyng.obj
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeCallable
import net.sergeych.lyng.miniast.*
class RingBuffer<T>(val maxSize: Int) : Iterable<T> {
@@ -96,39 +97,55 @@ class ObjRingBuffer(val capacity: Int) : Obj() {
doc = "Maximum number of elements the buffer can hold.",
type = type("lyng.Int"),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjRingBuffer>().capacity.toObj() }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjRingBuffer>().capacity.toObj()
}
)
addPropertyDoc(
name = "size",
doc = "Current number of elements in the buffer.",
type = type("lyng.Int"),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjRingBuffer>().buffer.size.toObj() }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjRingBuffer>().buffer.size.toObj()
}
)
addFnDoc(
name = "iterator",
doc = "Iterator over elements in insertion order (oldest to newest).",
returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.Any"))),
moduleName = "lyng.stdlib"
) {
val buffer = thisAs<ObjRingBuffer>().buffer
ObjKotlinObjIterator(buffer.iterator())
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val buffer = scp.thisAs<ObjRingBuffer>().buffer
return ObjKotlinObjIterator(buffer.iterator())
}
}
)
addFnDoc(
name = "add",
doc = "Append an element; if full, the oldest element is dropped.",
params = listOf(ParamDoc("value", type("lyng.Any"))),
returns = type("lyng.Void"),
moduleName = "lyng.stdlib"
) { thisAs<ObjRingBuffer>().apply { buffer.add(requireOnlyArg<Obj>()) } }
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val self = scp.thisAs<ObjRingBuffer>()
self.buffer.add(scp.requireOnlyArg<Obj>())
return ObjVoid
}
}
)
addPropertyDoc(
name = "first",
doc = "Return the oldest element in the buffer.",
type = type("lyng.Any"),
moduleName = "lyng.stdlib",
getter = {
val buffer = (this.thisObj as ObjRingBuffer).buffer
if (buffer.size == 0) ObjNull else buffer.first()
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val buffer = (scp.thisObj as ObjRingBuffer).buffer
return if (buffer.size == 0) ObjNull else buffer.iterator().next()
}
}
)
}
@@ -18,6 +18,7 @@
package net.sergeych.lyng.obj
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeCallable
import net.sergeych.lyng.miniast.ParamDoc
import net.sergeych.lyng.miniast.TypeGenericDoc
import net.sergeych.lyng.miniast.addFnDoc
@@ -46,9 +47,9 @@ class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
return set.contains(other)
}
override suspend fun enumerate(scope: Scope, callback: suspend (Obj) -> Boolean) {
override suspend fun enumerate(scope: Scope, callback: EnumerateCallback) {
for (item in set) {
if (!callback(item)) break
if (!callback.call(item)) break
}
}
@@ -164,56 +165,64 @@ class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
name = "size",
doc = "Number of elements in this set.",
returns = type("lyng.Int"),
moduleName = "lyng.stdlib"
) {
thisAs<ObjSet>().set.size.toObj()
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjSet>().set.size.toObj()
}
)
addFnDoc(
name = "intersect",
doc = "Intersection with another set. Returns a new set.",
params = listOf(ParamDoc("other", type("lyng.Set"))),
returns = type("lyng.Set"),
moduleName = "lyng.stdlib"
) {
thisAs<ObjSet>().mul(this, args.firstAndOnly())
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjSet>().mul(scp, scp.args.firstAndOnly())
}
)
addFnDoc(
name = "iterator",
doc = "Iterator over elements of this set.",
returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.Any"))),
moduleName = "lyng.stdlib"
) {
thisAs<ObjSet>().set.iterator().toObj()
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjKotlinIterator(scp.thisAs<ObjSet>().set.iterator())
}
)
addFnDoc(
name = "union",
doc = "Union with another set or iterable. Returns a new set.",
params = listOf(ParamDoc("other")),
returns = type("lyng.Set"),
moduleName = "lyng.stdlib"
) {
thisAs<ObjSet>().plus(this, args.firstAndOnly())
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjSet>().plus(scp, scp.args.firstAndOnly())
}
)
addFnDoc(
name = "subtract",
doc = "Subtract another set or iterable from this set. Returns a new set.",
params = listOf(ParamDoc("other")),
returns = type("lyng.Set"),
moduleName = "lyng.stdlib"
) {
thisAs<ObjSet>().minus(this, args.firstAndOnly())
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = scp.thisAs<ObjSet>().minus(scp, scp.args.firstAndOnly())
}
)
addFnDoc(
name = "remove",
doc = "Remove one or more elements. Returns true if the set changed.",
returns = type("lyng.Bool"),
moduleName = "lyng.stdlib"
) {
val set = thisAs<ObjSet>().set
val n = set.size
for( x in args.list ) set -= x
if( n == set.size ) ObjFalse else ObjTrue
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val set = scp.thisAs<ObjSet>().set
val n = set.size
for( x in scp.args.list ) set -= x
return if( n == set.size ) ObjFalse else ObjTrue
}
}
)
}
}
}
@@ -24,6 +24,7 @@ import kotlinx.serialization.json.JsonPrimitive
import net.sergeych.lyng.PerfFlags
import net.sergeych.lyng.RegexCache
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeCallable
import net.sergeych.lyng.miniast.*
import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder
@@ -139,193 +140,274 @@ data class ObjString(val value: String) : Obj() {
name = "iterator",
doc = "Iterator over characters of this string.",
returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.Char"))),
moduleName = "lyng.stdlib"
) { ObjKotlinIterator(thisAs<ObjString>().value.iterator()) }
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjKotlinIterator(scp.thisAs<ObjString>().value.iterator())
}
)
addFnDoc(
name = "toInt",
doc = "Parse this string as an integer or throw if it is not a valid integer.",
returns = type("lyng.Int"),
moduleName = "lyng.stdlib"
) {
ObjInt.of(
thisAs<ObjString>().value.toLongOrNull()
?: raiseIllegalArgument("can't convert to int: $thisObj")
)
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return ObjInt.of(
scp.thisAs<ObjString>().value.toLongOrNull()
?: scp.raiseIllegalArgument("can't convert to int: ${scp.thisObj}")
)
}
}
)
addFnDoc(
name = "startsWith",
doc = "Whether this string starts with the given prefix.",
params = listOf(ParamDoc("prefix", type("lyng.String"))),
returns = type("lyng.Bool"),
moduleName = "lyng.stdlib"
) {
ObjBool(thisAs<ObjString>().value.startsWith(requiredArg<ObjString>(0).value))
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return ObjBool(scp.thisAs<ObjString>().value.startsWith(scp.requiredArg<ObjString>(0).value))
}
}
)
addFnDoc(
name = "endsWith",
doc = "Whether this string ends with the given suffix.",
params = listOf(ParamDoc("suffix", type("lyng.String"))),
returns = type("lyng.Bool"),
moduleName = "lyng.stdlib"
) {
ObjBool(thisAs<ObjString>().value.endsWith(requiredArg<ObjString>(0).value))
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return ObjBool(scp.thisAs<ObjString>().value.endsWith(scp.requiredArg<ObjString>(0).value))
}
}
)
addPropertyDoc(
name = "length",
doc = "Number of UTF-16 code units in this string.",
type = type("lyng.Int"),
moduleName = "lyng.stdlib",
getter = { ObjInt.of((this.thisObj as ObjString).value.length.toLong()) }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjInt.of((scp.thisObj as ObjString).value.length.toLong())
}
)
addFnDoc(
name = "takeLast",
doc = "Return a string with the last N characters.",
params = listOf(ParamDoc("n", type("lyng.Int"))),
returns = type("lyng.String"),
moduleName = "lyng.stdlib"
) {
thisAs<ObjString>().value.takeLast(
requiredArg<ObjInt>(0).toInt()
).let(::ObjString)
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return ObjString(
scp.thisAs<ObjString>().value.takeLast(
scp.requiredArg<ObjInt>(0).toInt()
)
)
}
}
)
addFnDoc(
name = "take",
doc = "Return a string with the first N characters.",
params = listOf(ParamDoc("n", type("lyng.Int"))),
returns = type("lyng.String"),
moduleName = "lyng.stdlib"
) {
thisAs<ObjString>().value.take(
requiredArg<ObjInt>(0).toInt()
).let(::ObjString)
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return ObjString(
scp.thisAs<ObjString>().value.take(
scp.requiredArg<ObjInt>(0).toInt()
)
)
}
}
)
addFnDoc(
name = "drop",
doc = "Drop the first N characters and return the remainder.",
params = listOf(ParamDoc("n", type("lyng.Int"))),
returns = type("lyng.String"),
moduleName = "lyng.stdlib"
) {
thisAs<ObjString>().value.drop(
requiredArg<ObjInt>(0).toInt()
).let(::ObjString)
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return ObjString(
scp.thisAs<ObjString>().value.drop(
scp.requiredArg<ObjInt>(0).toInt()
)
)
}
}
)
addFnDoc(
name = "dropLast",
doc = "Drop the last N characters and return the remainder.",
params = listOf(ParamDoc("n", type("lyng.Int"))),
returns = type("lyng.String"),
moduleName = "lyng.stdlib"
) {
thisAs<ObjString>().value.dropLast(
requiredArg<ObjInt>(0).toInt()
).let(::ObjString)
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return ObjString(
scp.thisAs<ObjString>().value.dropLast(
scp.requiredArg<ObjInt>(0).toInt()
)
)
}
}
)
addFnDoc(
name = "lower",
doc = "Lowercase version of this string (default locale).",
returns = type("lyng.String"),
moduleName = "lyng.stdlib"
) {
thisAs<ObjString>().value.lowercase().let(::ObjString)
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjString(scp.thisAs<ObjString>().value.lowercase())
}
)
addFnDoc(
name = "lowercase",
doc = "Lowercase version of this string (default locale).",
returns = type("lyng.String"),
moduleName = "lyng.stdlib"
) {
thisAs<ObjString>().value.lowercase().let(::ObjString)
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjString(scp.thisAs<ObjString>().value.lowercase())
}
)
addFnDoc(
name = "upper",
doc = "Uppercase version of this string (default locale).",
returns = type("lyng.String"),
moduleName = "lyng.stdlib"
) {
thisAs<ObjString>().value.uppercase().let(::ObjString)
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjString(scp.thisAs<ObjString>().value.uppercase())
}
)
addFnDoc(
name = "uppercase",
doc = "Uppercase version of this string (default locale).",
returns = type("lyng.String"),
moduleName = "lyng.stdlib"
) {
thisAs<ObjString>().value.uppercase().let(::ObjString)
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjString(scp.thisAs<ObjString>().value.uppercase())
}
)
addPropertyDoc(
name = "characters",
doc = "List of characters of this string.",
type = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.Char"))),
moduleName = "lyng.stdlib",
getter = {
ObjList(
(this.thisObj as ObjString).value.map { ObjChar(it) }.toMutableList()
)
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return ObjList(
(scp.thisObj as ObjString).value.map { ObjChar(it) }.toMutableList()
)
}
}
)
addFnDoc(
name = "last",
doc = "The last character of this string or throw if the string is empty.",
returns = type("lyng.Char"),
moduleName = "lyng.stdlib"
) {
ObjChar(thisAs<ObjString>().value.lastOrNull() ?: raiseNoSuchElement("empty string"))
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
return ObjChar(scp.thisAs<ObjString>().value.lastOrNull() ?: scp.raiseNoSuchElement("empty string"))
}
}
)
addFnDoc(
name = "encodeUtf8",
doc = "Encode this string as UTF-8 bytes.",
returns = type("lyng.Buffer"),
moduleName = "lyng.stdlib"
) { ObjBuffer(thisAs<ObjString>().value.encodeToByteArray().asUByteArray()) }
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj =
ObjBuffer(scp.thisAs<ObjString>().value.encodeToByteArray().asUByteArray())
}
)
addPropertyDoc(
name = "size",
doc = "Alias for length: the number of characters (code units) in this string.",
type = type("lyng.Int"),
moduleName = "lyng.stdlib",
getter = { ObjInt.of((this.thisObj as ObjString).value.length.toLong()) }
getter = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjInt.of((scp.thisObj as ObjString).value.length.toLong())
}
)
addFnDoc(
name = "toReal",
doc = "Parse this string as a real number (floating point).",
returns = type("lyng.Real"),
moduleName = "lyng.stdlib"
) {
ObjReal.of(thisAs<ObjString>().value.toDouble())
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjReal.of(scp.thisAs<ObjString>().value.toDouble())
}
)
addFnDoc(
name = "trim",
doc = "Return a copy of this string with leading and trailing whitespace removed.",
returns = type("lyng.String"),
moduleName = "lyng.stdlib"
) {
thisAs<ObjString>().value.trim().let(::ObjString)
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjString(scp.thisAs<ObjString>().value.trim())
}
)
addFnDoc(
name = "isBlank",
doc = "Whether this string is empty or contains only whitespace characters.",
returns = type("lyng.Bool"),
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjBool(scp.thisAs<ObjString>().value.isBlank())
}
)
addFnDoc(
name = "isEmpty",
doc = "Whether this string is empty.",
returns = type("lyng.Bool"),
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjBool(scp.thisAs<ObjString>().value.isEmpty())
}
)
addFnDoc(
name = "isNotEmpty",
doc = "Whether this string is not empty.",
returns = type("lyng.Bool"),
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj = ObjBool(scp.thisAs<ObjString>().value.isNotEmpty())
}
)
addFnDoc(
name = "matches",
doc = "Whether this string matches the given regular expression or pattern string.",
params = listOf(ParamDoc("pattern")),
returns = type("lyng.Bool"),
moduleName = "lyng.stdlib"
) {
val s = requireOnlyArg<Obj>()
val self = thisAs<ObjString>().value
ObjBool(
when (s) {
is ObjRegex -> self.matches(s.regex)
is ObjString -> {
if (s.value == ".*") true
else {
val re = if (PerfFlags.REGEX_CACHE) RegexCache.get(s.value) else s.value.toRegex()
self.matches(re)
}
}
moduleName = "lyng.stdlib",
code = object : ScopeCallable {
override suspend fun call(scp: Scope): Obj {
val s = scp.requireOnlyArg<Obj>()
val self = scp.thisAs<ObjString>().value
return ObjBool(
when (s) {
is ObjRegex -> self.matches(s.regex)
is ObjString -> {
if (s.value == ".*") true
else {
val re = if (PerfFlags.REGEX_CACHE) RegexCache.get(s.value) else s.value.toRegex()
self.matches(re)
}
}
else ->
raiseIllegalArgument("can't match ${s.objClass.className}: required Regex or String")
else ->
scp.raiseIllegalArgument("can't match ${s.objClass.className}: required Regex or String")
}
)
}
)
}
}
)
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* 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.
@@ -21,6 +21,10 @@ import net.sergeych.lyng.*
import net.sergeych.synctools.ProtectedOp
import net.sergeych.synctools.withLock
interface ModuleBuilder {
suspend fun build(scope: ModuleScope)
}
/**
* Import manager allow to register packages with builder lambdas and act as an
* [ImportProvider]. Note that packages _must be registered_ first with [addPackage],
@@ -40,16 +44,16 @@ class ImportManager(
private inner class Entry(
val packageName: String,
val builder: suspend (ModuleScope) -> Unit,
val builder: ModuleBuilder,
var cachedScope: ModuleScope? = null
) {
suspend fun getScope(pos: Pos): ModuleScope {
cachedScope?.let { return it }
return ModuleScope(inner, pos, packageName).apply {
cachedScope = this
builder(this)
}
val ms = ModuleScope(inner, pos, packageName)
cachedScope = ms
builder.build(ms)
return ms
}
}
@@ -90,7 +94,7 @@ class ImportManager(
* @param name package name
* @param builder lambda to create actual package using the given [ModuleScope]
*/
fun addPackage(name: String, builder: suspend (ModuleScope) -> Unit) {
fun addPackage(name: String, builder: ModuleBuilder) {
op.withLock {
if (name in imports)
throw IllegalArgumentException("Package $name already exists")
@@ -102,7 +106,7 @@ class ImportManager(
* Bulk [addPackage] with slightly better performance
*/
@Suppress("unused")
fun addPackages(registrationData: List<Pair<String, suspend (ModuleScope) -> Unit>>) {
fun addPackages(registrationData: List<Pair<String, ModuleBuilder>>) {
op.withLock {
for (pp in registrationData) {
if (pp.first in imports)
@@ -129,9 +133,11 @@ class ImportManager(
*/
fun addSourcePackages(vararg sources: Source) {
for (s in sources) {
addPackage(s.extractPackageName()) {
it.eval(s)
}
addPackage(s.extractPackageName(), object : ModuleBuilder {
override suspend fun build(scope: ModuleScope) {
scope.eval(s)
}
})
}
}
@@ -144,7 +150,11 @@ class ImportManager(
var source = Source("tmp", s)
val packageName = source.extractPackageName()
source = Source(packageName, s)
addPackage(packageName) { it.eval(source) }
addPackage(packageName, object : ModuleBuilder {
override suspend fun build(scope: ModuleScope) {
scope.eval(source)
}
})
}
}
@@ -72,16 +72,16 @@ fun Statement.require(cond: Boolean, message: () -> String) {
if (!cond) raise(message())
}
fun statement(pos: Pos, isStaticConst: Boolean = false, isConst: Boolean = false, f: suspend (Scope) -> Obj): Statement =
fun statement(pos: Pos, isStaticConst: Boolean = false, isConst: Boolean = false, f: ScopeCallable): Statement =
object : Statement(isStaticConst, isConst) {
override val pos: Pos = pos
override suspend fun execute(scope: Scope): Obj = f(scope)
override suspend fun execute(scope: Scope): Obj = f.call(scope)
}
fun statement(isStaticConst: Boolean = false, isConst: Boolean = false, f: suspend Scope.() -> Obj): Statement =
fun statement(isStaticConst: Boolean = false, isConst: Boolean = false, f: ScopeCallable): Statement =
object : Statement(isStaticConst, isConst) {
override val pos: Pos = Pos.builtIn
override suspend fun execute(scope: Scope): Obj = f(scope)
override suspend fun execute(scope: Scope): Obj = f.call(scope)
}
object NopStatement: Statement(true, true, ObjType.Void) {
@@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* 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.
@@ -20,6 +20,7 @@ package net.sergeych.lynon
import net.sergeych.lyng.Scope
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.ObjInstance
import net.sergeych.lyng.obj.ObjString
open class LynonDecoder(val bin: BitInput, val settings: LynonSettings = LynonSettings.default) {
@@ -79,15 +80,16 @@ open class LynonDecoder(val bin: BitInput, val settings: LynonSettings = LynonSe
private suspend fun decodeClassObj(scope: Scope): ObjClass {
val className = decodeObject(scope, ObjString.type, null) as ObjString
return scope.get(className.value)?.value?.let {
if (it !is ObjClass)
scope.raiseClassCastError("Expected obj class but got ${it::class.simpleName}")
it
if (it is ObjClass) return it
if (it is ObjInstance && it.objClass.className == className.value) return it.objClass
scope.raiseClassCastError("Expected obj class but got ${it::class.simpleName}")
} ?: run {
// Use Scope API that mirrors compiler-emitted ObjRef chain for qualified identifiers
val evaluated = scope.resolveQualifiedIdentifier(className.value)
if (evaluated !is ObjClass)
scope.raiseClassCastError("Expected obj class but got ${evaluated::class.simpleName}")
evaluated
if (evaluated is ObjClass) return evaluated
if (evaluated is ObjInstance && evaluated.objClass.className == className.value) return evaluated.objClass
scope.raiseClassCastError("Expected obj class but got ${evaluated::class.simpleName}")
evaluated as ObjClass // unreachable but for compiler
}
}
@@ -41,11 +41,11 @@ object ObjLynonClass : ObjClass("Lynon") {
init {
addClassConst("test", ObjString("test_const"))
addClassFn("encode") {
encodeAny(this, requireOnlyArg<Obj>())
addClassFn("encode") { scp ->
encodeAny(scp, scp.requireOnlyArg<Obj>())
}
addClassFn("decode") {
decodeAny(this, requireOnlyArg<Obj>())
addClassFn("decode") { scp ->
decodeAny(scp, scp.requireOnlyArg<Obj>())
}
}
}
@@ -171,7 +171,7 @@ class BindingHighlightTest {
// Find the specific usage inside string-literal invocation: "%s is directory"(name)
val pattern = "\"%s is directory\"(name)"
val lineIdx = text.indexOf(pattern)
assertTrue(lineIdx >= 0, "Pattern with string invoke should be present in the snippet")
assertTrue(lineIdx >= 0, "Pattern with string invokeCallable should be present in the snippet")
val nameStart = lineIdx + pattern.indexOf("name")
val nameEnd = nameStart + "name".length
@@ -528,4 +528,52 @@ class MiniAstTest {
assertTrue(xParam.contains("first line of x"), "should contain first line")
assertTrue(xParam.contains("second line of x"), "should contain second line")
}
@Test
fun enum_minidocs_and_semicolon_robustness() = runTest {
val code = """
/** Enum doc */
enum E { A };
/** Next doc */
class C {}
""".trimIndent()
val (_, sink) = compileWithMini(code)
val mini = sink.build()
assertNotNull(mini)
val en = mini.declarations.filterIsInstance<MiniEnumDecl>().firstOrNull { it.name == "E" }
assertNotNull(en)
assertNotNull(en.doc)
assertEquals("Enum doc", en.doc.summary)
val cl = mini.declarations.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == "C" }
assertNotNull(cl)
assertNotNull(cl.doc)
assertEquals("Next doc", cl.doc.summary)
}
@Test
fun empty_enum_support() = runTest {
val code = "enum E {}"
val (_, sink) = compileWithMini(code)
val mini = sink.build()
assertNotNull(mini)
assertTrue(mini.declarations.any { it.name == "E" && it is MiniEnumDecl })
}
@Test
fun modifiers_with_comment_robustness() = runTest {
val code = """
class X {
static /** doc */ fun f() {}
}
""".trimIndent()
val (_, sink) = compileWithMini(code)
val mini = sink.build()
assertNotNull(mini)
val cls = mini.declarations.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == "X" }
assertNotNull(cls)
assertTrue(cls.members.any { it.name == "f" })
}
}
+135
View File
@@ -17,6 +17,7 @@
import kotlinx.coroutines.test.runTest
import net.sergeych.lyng.Script
import net.sergeych.lyng.Statement
import net.sergeych.lyng.eval
import net.sergeych.lyng.obj.ObjInstance
import net.sergeych.lyng.obj.ObjList
@@ -776,4 +777,138 @@ class OOTest {
assertEquals("success", result.toString(), "Parameter 'id' should shadow method 'id' in block")
}
@Test
fun testOverrideVisibilityRules1() = runTest {
val scope = Script.newScope()
scope.eval("""
interface Base {
abstract protected fun foo()
fun bar() {
// it must see foo() as it is protected and
// is declared here (even as abstract):
foo()
}
}
class Derived : Base {
protected val suffix = "!"
private fun fooPrivateImpl() = "bar"
override protected fun foo() {
// it should access own private and all protected memberes here:
fooPrivateImpl() + suffix
}
}
class Derived2: Base {
private var value = 42
private fun fooPrivateImpl() = value
override protected fun foo() {
fooPrivateImpl()
value++
}
}
assertEquals("bar!", Derived().bar())
val d = Derived2()
assertEquals(42, d.bar())
assertEquals(43, d.bar())
""".trimIndent())
scope.createChildScope().eval("""
assertEquals("bar!", Derived().bar())
assertEquals(42, Derived2().bar())
""".trimIndent())
}
@Test
fun testOverrideVisibilityRules2() = runTest {
val scope = Script.newScope()
val fn = scope.eval("""
interface Base {
abstract fun foo()
fun bar() {
// it must see foo() as it is protected and
// is declared here (even as abstract):
foo()
}
}
class Derived : Base {
protected val suffix = "!"
private fun fooPrivateImpl() = "bar"
override fun foo() {
// it should access own private and all protected memberes here:
fooPrivateImpl() + suffix
}
}
class Derived2: Base {
private var value = 42
private fun fooPrivateImpl() = value
override fun foo() {
fooPrivateImpl()
value++
}
}
assertEquals("bar!", Derived().bar())
val d = Derived2()
fun callBar() = d.bar()
assertEquals(42, callBar())
assertEquals(43, callBar())
callBar
""".trimIndent()) as Statement
val s2 = Script.newScope()
assertEquals(44L, fn.invokeCallable(scope, fn).toKotlin(s2))
assertEquals(45L, fn.invokeCallable(s2, fn).toKotlin(s2))
}
@Test
fun testToStringWithTransients() = runTest {
eval("""
class C(amount,@Transient transient=0) {
val l by lazy { transient + amount }
fun lock() {
if( transient < 10 )
C(amount).also { it.transient = transient + 10 }
else
this
}
}
println(C(1))
println(C(1).lock().amount)
println(C(1).lock().lock().amount)
""".trimIndent())
}
@Test
fun testToStringWithTransient() = runTest {
eval("""
class C(amount,@Transient transient=0) {
val l by lazy { transient + amount }
fun lock() {
if( transient < 10 )
C(amount).also { it.transient = transient + 10 }
else
this
}
}
println(C(1))
println(C(1).lock().amount)
println(C(1).lock().lock().amount)
""".trimIndent())
}
@Test
fun testToJsonString() = runTest {
eval("""
class T(a,b,@Transient c=0)
assertEquals("{\"a\":\"foo\",\"b\":\"bar\"}",T("foo", "bar").toJsonString())
""".trimIndent())
}
}
+192 -12
View File
@@ -138,10 +138,10 @@ class ScriptTest {
companion object {
val type = ObjClass("TestIterable", ObjIterable).apply {
addFn("iterator") {
ObjTestIterator(thisAs<ObjTestIterable>())
addFn("iterator") { scp ->
ObjTestIterator(scp.thisAs<ObjTestIterable>())
}
addFn("cancelCount") { thisAs<ObjTestIterable>().cancelCount.toObj() }
addFn("cancelCount") { scp -> scp.thisAs<ObjTestIterable>().cancelCount.toObj() }
}
}
}
@@ -158,10 +158,10 @@ class ScriptTest {
companion object {
val type = ObjClass("TestIterator", ObjIterator).apply {
addFn("hasNext") { thisAs<ObjTestIterator>().hasNext().toObj() }
addFn("next") { thisAs<ObjTestIterator>().next() }
addFn("cancelIteration") {
thisAs<ObjTestIterator>().cancelIteration()
addFn("hasNext") { scp -> scp.thisAs<ObjTestIterator>().hasNext().toObj() }
addFn("next") { scp -> scp.thisAs<ObjTestIterator>().next() }
addFn("cancelIteration") { scp ->
scp.thisAs<ObjTestIterator>().cancelIteration()
ObjVoid
}
}
@@ -2229,8 +2229,8 @@ class ScriptTest {
@Test
fun testThrowFromKotlin() = runTest {
val c = Script.newScope()
c.addFn("callThrow") {
raiseIllegalArgument("fromKotlin")
c.addFn("callThrow") { scp ->
scp.raiseIllegalArgument("fromKotlin")
}
c.eval(
"""
@@ -2738,8 +2738,8 @@ class ScriptTest {
companion object {
val klass = ObjClass("TestFoo").apply {
addFn("test") {
thisAs<ObjTestFoo>().value
addFn("test") { scp ->
scp.thisAs<ObjTestFoo>().value
}
}
}
@@ -3169,6 +3169,19 @@ class ScriptTest {
assertEquals( n1 - now, 7.seconds )
assertEquals( now - n1, -7.seconds )
val t3 = Instant("2024-01-01T12:00:00.123456Z")
val t4 = t3.truncateToMinute
assertEquals(t4.toRFC3339(), "2024-01-01T12:00:00Z")
assertEquals(
"2024-01-01T12:00:00Z",
Instant("2024-01-01T12:00:59.999Z").truncateToMinute().toRFC3339()
)
val t5 = t3.truncateToSecond
assertEquals(t5.toRFC3339(), "2024-01-01T12:00:00Z")
val t6 = t3.truncateToMillisecond
assertEquals(t6.toRFC3339(), "2024-01-01T12:00:00.123Z")
""".trimIndent()
)
delay(1000)
@@ -3209,6 +3222,137 @@ class ScriptTest {
)
}
@Test
fun testDateTimeComprehensive() = runTest {
eval("""
import lyng.time
import lyng.serialization
// 1. Timezone variations
val t1 = Instant("2024-01-01T12:00:00Z")
val dtZ = t1.toDateTime("Z")
assertEquals(dtZ.timeZone, "Z")
assertEquals(dtZ.hour, 12)
val dtP2 = t1.toDateTime("+02:00")
assertEquals(dtP2.timeZone, "+02:00")
assertEquals(dtP2.hour, 14)
val dtM330 = t1.toDateTime("-03:30")
assertEquals(dtM330.timeZone, "-03:30")
assertEquals(dtM330.hour, 8)
assertEquals(dtM330.minute, 30)
// 2. RFC3339 representations
// Note: ObjDateTime.toString() currently uses localDateTime.toString() + timeZone.toString()
// We should verify what it actually produces.
val s1 = dtP2.toRFC3339()
// kotlinx-datetime LocalDateTime.toString() is ISO8601
// TimeZone.toString() for offsets is usually the offset string itself
println("dtP2 RFC3339: " + s1)
// 3. Parsing
val t2 = Instant("2024-02-29T10:00:00+01:00")
assertEquals(t2.toDateTime("Z").hour, 9)
// val dt3 = DateTime(t1, "Europe/Prague")
// assertEquals(dt3.timeZone, "Europe/Prague")
// 4. Serialization (Lynon)
val bin = Lynon.encode(dtP2)
val dtP2_dec = Lynon.decode(bin)
assertEquals(dtP2, dtP2_dec)
assertEquals(dtP2_dec.hour, 14)
assertEquals(dtP2_dec.timeZone, "+02:00")
// 5. Serialization (JSON)
// val json = Lynon.toJson(dtM330) // toJson is not on Lynon yet
// println("JSON: " + json)
// 6. Arithmetic edge cases
val leapDay = Instant("2024-02-29T12:00:00Z").toDateTime("Z")
val nextYear = leapDay.addYears(1)
assertEquals(nextYear.year, 2025)
assertEquals(nextYear.month, 2)
assertEquals(nextYear.day, 28) // Normalized
val monthEnd = Instant("2024-01-31T12:00:00Z").toDateTime("Z")
val nextMonth = monthEnd.addMonths(1)
assertEquals(nextMonth.month, 2)
assertEquals(nextMonth.day, 29) // 2024 is leap year
// 7. Day of week
assertEquals(Instant("2024-01-01T12:00:00Z").toDateTime("Z").dayOfWeek, 1) // Monday
assertEquals(Instant("2024-01-07T12:00:00Z").toDateTime("Z").dayOfWeek, 7) // Sunday
// 8. DateTime to/from Instant
val inst = dtP2.toInstant()
assertEquals(inst, t1)
assertEquals(dtP2.toEpochSeconds(), t1.epochWholeSeconds)
// 9. toUTC and toTimeZone
val dtUTC = dtP2.toUTC()
assertEquals(dtUTC.timeZone, "UTC")
assertEquals(dtUTC.hour, 12)
val dtPrague = dtUTC.toTimeZone("+01:00")
// Equivalent to Prague winter
assertEquals(dtPrague.hour, 13)
// 10. Component-based constructor
val dtComp = DateTime(2024, 5, 20, 15, 30, 45, "+02:00")
assertEquals(dtComp.year, 2024)
assertEquals(dtComp.month, 5)
assertEquals(dtComp.day, 20)
assertEquals(dtComp.hour, 15)
assertEquals(dtComp.minute, 30)
assertEquals(dtComp.second, 45)
assertEquals(dtComp.timeZone, "+02:00")
// 11. parseRFC3339
val dtParsed = DateTime.parseRFC3339("2024-05-20T15:30:45+02:00")
assertEquals(dtParsed.year, 2024)
assertEquals(dtParsed.hour, 15)
assertEquals(dtParsed.timeZone, "+02:00")
val dtParsedZ = DateTime.parseRFC3339("2024-05-20T15:30:45Z")
assertEquals(dtParsedZ.timeZone, "Z")
assertEquals(dtParsedZ.hour, 15)
""".trimIndent())
}
@Test
fun testInstantComponents() = runTest {
eval("""
import lyng.time
val t1 = Instant("1970-05-06T07:11:56Z")
val dt = t1.toDateTime("Z")
assertEquals(dt.year, 1970)
assertEquals(dt.month, 5)
assertEquals(dt.day, 6)
assertEquals(dt.hour, 7)
assertEquals(dt.minute, 11)
assertEquals(dt.second, 56)
assertEquals(dt.dayOfWeek, 3) // 1970-05-06 was Wednesday
assertEquals("1970-05-06T07:11:56Z", t1.toRFC3339())
assertEquals("1970-05-06T07:11:56Z", t1.toSortableString())
val dt2 = dt.toTimeZone("+02:00")
assertEquals(dt2.hour, 9)
assertEquals(dt2.timeZone, "+02:00")
val dt3 = dt.addMonths(1)
assertEquals(dt3.month, 6)
assertEquals(dt3.day, 6)
val dt4 = dt.addYears(1)
assertEquals(dt4.year, 1971)
assertEquals(dt.toInstant(), t1)
""".trimIndent())
}
@Test
fun testDoubleImports() = runTest {
val s = Scope.new()
@@ -4336,7 +4480,7 @@ class ScriptTest {
val dummyThis = Obj()
// but we should be able to call it directly
val otherScope = baseScope.createChildScope()
val r = (exports["exportedFunction".toObj()] as Statement).invoke(otherScope, dummyThis, ObjInt(50))
val r = (exports["exportedFunction".toObj()] as Statement).invokeCallable(otherScope, dummyThis, ObjInt(50))
println(r)
assertEquals(51, r.toInt())
}
@@ -4855,5 +4999,41 @@ class ScriptTest {
assertEquals(["bar"], A().f2("bar"))
""")
}
@Test
fun testClamp() = runTest {
eval("""
// Global clamp
assertEquals(5, clamp(5, 0..10))
assertEquals(0, clamp(-5, 0..10))
assertEquals(10, clamp(15, 0..10))
// Extension clamp
assertEquals(5, 5.clamp(0..10))
assertEquals(0, (-5).clamp(0..10))
assertEquals(10, 15.clamp(0..10))
// Exclusive range
assertEquals(9, 15.clamp(0..<10))
assertEquals(0, (-5).clamp(0..<10))
// Open-ended range
assertEquals(10, 15.clamp(..10))
assertEquals(-5, (-5).clamp(..10))
assertEquals(5, 5.clamp(0..))
assertEquals(0, (-5).clamp(0..))
// Character range
assertEquals('e', 'e'.clamp('a'..'z'))
assertEquals('a', ' '.clamp('a'..'z'))
assertEquals('z', '}'.clamp('a'..'z'))
assertEquals('y', '}'.clamp('a'..<'z'))
// Real numbers (boundaries are inclusive in current impl for Real)
assertEquals(5.5, 5.5.clamp(0.0..10.0))
assertEquals(0.0, (-1.5).clamp(0.0..10.0))
assertEquals(10.0, 15.5.clamp(0.0..10.0))
""".trimIndent())
}
}
@@ -90,7 +90,7 @@ class DelegationTest {
fun testFunDelegation() = runTest {
eval("""
class ActionDelegate() {
fun invoke(thisRef, name, args...) {
fun invokeCallable(thisRef, name, args...) {
"Called %s with %d args: %s"(name, args.size, args.joinToString(","))
}
}
@@ -0,0 +1,241 @@
/*
* 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.obj.ObjInstance
import net.sergeych.lyng.obj.ObjInt
import net.sergeych.lyng.obj.ObjNull
import net.sergeych.lyng.obj.toBool
import net.sergeych.lynon.lynonDecodeAny
import net.sergeych.lynon.lynonEncodeAny
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
class TransientTest {
@Test
fun testTransient() = runTest {
val script = """
class TestTransient(@Transient val a, val b) {
@Transient var c = 10
var d = 20
fun check() {
a == 1 && b == 2 && c == 10 && d == 20
}
}
val t = TestTransient(1, 2)
t.c = 30
t.d = 40
t
""".trimIndent()
val scope = Scope()
val t = scope.eval(script) as ObjInstance
// Check initial state
assertEquals(1, (t.readField(scope, "a").value as ObjInt).value)
assertEquals(2, (t.readField(scope, "b").value as ObjInt).value)
assertEquals(30, (t.readField(scope, "c").value as ObjInt).value)
assertEquals(40, (t.readField(scope, "d").value as ObjInt).value)
// Serialize
val serialized = lynonEncodeAny(scope, t)
println("[DEBUG_LOG] Serialized size: ${serialized.size}")
// Deserialized
val t2 = lynonDecodeAny(scope, serialized) as ObjInstance
// b and d should be preserved
assertEquals(2, (t2.readField(scope, "b").value as ObjInt).value)
assertEquals(40, (t2.readField(scope, "d").value as ObjInt).value)
// a and c should be transient (lost or default/null)
// For constructor args, we currently set ObjNull if transient
assertEquals(ObjNull, t2.readField(scope, "a").value)
// For class fields, if it's transient it's not serialized, so it gets its initial value during construction
assertEquals(10, (t2.readField(scope, "c").value as ObjInt).value)
// Check JSON
val json = t.toJson(scope).toString()
println("[DEBUG_LOG] JSON: $json")
assertFalse(json.contains("\"a\":"))
assertFalse(json.contains("\"c\":"))
assertNotNull(json.contains("\"b\":2"))
assertNotNull(json.contains("\"d\":40"))
}
@Test
fun testTransientDefaultAndEquality() = runTest {
val script = """
class TestExt(@Transient val a = 100, val b) {
@Transient var c = 200
var d = 300
}
val t1 = TestExt(b: 2)
t1.c = 300
t1.d = 400
val t2 = TestExt(a: 50, b: 2)
t2.c = 500
t2.d = 400
// Equality should ignore transient fields a and c
val equal = (t1 == t2)
[t1, t2, equal]
""".trimIndent()
val scope = Scope()
val result = (scope.eval(script) as net.sergeych.lyng.obj.ObjList).list
val t1 = result[0] as ObjInstance
val t2 = result[1] as ObjInstance
val equal = result[2].toBool()
assertEquals(true, equal, "Objects should be equal despite different transient fields")
// Serialize t1
val serialized = lynonEncodeAny(scope, t1)
val t1d = lynonDecodeAny(scope, serialized) as ObjInstance
// a should have its default value 100, not null or 10
assertEquals(100, (t1d.readField(scope, "a").value as ObjInt).value)
// c should have its initial value 200
assertEquals(200, (t1d.readField(scope, "c").value as ObjInt).value)
// b and d should be preserved
assertEquals(2, (t1d.readField(scope, "b").value as ObjInt).value)
assertEquals(400, (t1d.readField(scope, "d").value as ObjInt).value)
}
@Test
fun testStaticTransient() = runTest {
val script = """
class TestStatic {
@Transient static var x = 10
static var y = 20
}
TestStatic.x = 30
TestStatic.y = 40
TestStatic
""".trimIndent()
val scope = Scope()
scope.eval(script)
// Static fields aren't serialized yet, but we ensure the parser accepts it
}
@Test
fun testTransientSize() = runTest {
val script = """
class Data1(val a, val b) {
var c = 30
}
class Data2(val a, val b, @Transient val x) {
var c = 30
@Transient var y = 40
}
val d1 = Data1(10, 20)
val d2 = Data2(10, 20, 100)
d2.y = 200
[d1, d2]
""".trimIndent()
val scope = Scope()
val result = (scope.eval(script) as net.sergeych.lyng.obj.ObjList).list
val d1 = result[0] as ObjInstance
val d2 = result[1] as ObjInstance
val s1 = lynonEncodeAny(scope, d1)
val s2 = lynonEncodeAny(scope, d2)
println("[DEBUG_LOG] Data1 size: ${s1.size}")
println("[DEBUG_LOG] Data2 size: ${s2.size}")
assertEquals(s1.size, s2.size, "Serialized sizes should match because transient fields are not serialized")
val j1 = d1.toJson(scope).toString()
val j2 = d2.toJson(scope).toString()
println("[DEBUG_LOG] Data1 JSON: $j1")
println("[DEBUG_LOG] Data2 JSON: $j2")
assertEquals(j1.length, j2.length, "JSON lengths should match")
}
@Test
fun testObjectTransient() = runTest {
val script = """
object MyObject {
@Transient var temp = 10
var persistent = 20
}
MyObject.temp = 30
MyObject.persistent = 40
MyObject
""".trimIndent()
val scope = Scope()
val obj = scope.eval(script) as ObjInstance
val serialized = lynonEncodeAny(scope, obj)
val deserialized = lynonDecodeAny(scope, serialized) as ObjInstance
// persistent should be 40
assertEquals(40, (deserialized.readField(scope, "persistent").value as ObjInt).value)
// temp should be restored to 10
assertEquals(10, (deserialized.readField(scope, "temp").value as ObjInt).value)
}
@Test
fun testStaticTransientToJson() = runTest {
val script = """
class TestStatic {
@Transient static var s1 = 10
static var s2 = 20
private static var s3 = 30
}
TestStatic
""".trimIndent()
val scope = Scope()
val cls = scope.eval(script) as net.sergeych.lyng.obj.ObjClass
val json = cls.toJson(scope).toString()
println("[DEBUG_LOG] Class JSON: $json")
// s2 should be in JSON
assertNotNull(json.contains("\"s2\":20"))
// s1 should NOT be in JSON (transient)
assertFalse(json.contains("\"s1\":"))
// s3 should NOT be in JSON (private)
assertFalse(json.contains("\"s3\":"))
// __class_name should be there
assertNotNull(json.contains("\"__class_name\":\"TestStatic\""))
// Test serialization/deserialization of the class itself
val serialized = lynonEncodeAny(scope, cls)
val deserialized = lynonDecodeAny(scope, serialized) as net.sergeych.lyng.obj.ObjClass
assertEquals(cls, deserialized)
}
}
@@ -882,6 +882,22 @@ class LyngFormatterTest {
}
""".trimIndent()
val cfg = LyngFormatConfig(indentSize = 4, continuationIndentSize = 4)
val out = LyngFormatter.reindent(src, cfg)
assertEquals(expected, out)
}
@Test
fun multlinedShortFunIndentation() {
val src = """
static fun create(walletId, trId, amount) =
Cells.createNew(Commission(amount) => ["commission", "w:" + walletId, "tr:" + trId, createdTag(), "pending"])
""".trimIndent()
val expected = """
static fun create(walletId, trId, amount) =
Cells.createNew(Commission(amount) => ["commission", "w:" + walletId, "tr:" + trId, createdTag(), "pending"])
""".trimIndent()
val cfg = LyngFormatConfig(indentSize = 4, continuationIndentSize = 4)
val out = LyngFormatter.reindent(src, cfg)
assertEquals(expected, out)
@@ -18,15 +18,15 @@
package net.sergeych.lyng
actual object PerfDefaults {
actual val LOCAL_SLOT_PIC: Boolean = true
actual val EMIT_FAST_LOCAL_REFS: Boolean = true
actual val LOCAL_SLOT_PIC: Boolean = false
actual val EMIT_FAST_LOCAL_REFS: Boolean = false
actual val ARG_BUILDER: Boolean = true
actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = true
actual val SCOPE_POOL: Boolean = true
actual val ARG_BUILDER: Boolean = false
actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = false
actual val SCOPE_POOL: Boolean = false
actual val FIELD_PIC: Boolean = true
actual val METHOD_PIC: Boolean = true
actual val FIELD_PIC: Boolean = false
actual val METHOD_PIC: Boolean = false
actual val FIELD_PIC_SIZE_4: Boolean = false
actual val METHOD_PIC_SIZE_4: Boolean = false
actual val PIC_ADAPTIVE_2_TO_4: Boolean = false
+2 -2
View File
@@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* 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.
@@ -118,7 +118,7 @@ fun App() {
Div({ classes("col-12", if (isDocsRoute) "col-lg-9" else "col-lg-12") }) {
when {
route.isBlank() -> HomePage()
route == "tryling" -> TryLyngPage()
route.startsWith("tryling") -> TryLyngPage(route)
route.startsWith("search") -> SearchPage(route)
!isDocsRoute -> ReferencePage()
else -> DocsPage(
+191 -43
View File
@@ -15,7 +15,9 @@
*
*/
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import kotlinx.browser.window
import kotlinx.coroutines.delay
import net.sergeych.lyngweb.ensureBootstrapCodeBlocks
import net.sergeych.lyngweb.highlightLyngHtml
import net.sergeych.lyngweb.htmlEscape
@@ -23,6 +25,141 @@ import org.jetbrains.compose.web.dom.*
@Composable
fun HomePage() {
val samples = remember {
listOf(
"""
// Everything is an expression
val x = 10
val status = if (x > 0) "Positive" else "Zero or Negative"
// Even loops return values!
val result = for (i in 1..5) {
if (i == 3) break "Found 3!"
} else "Not found"
println("Result: " + result)
""".trimIndent(),
"""
// Functional power with ranges and collections
val squares = (1..10)
.filter { it % 2 == 0 }
.map { it * it }
println("Even squares: " + squares)
// Output: [4, 16, 36, 64, 100]
""".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.0", status: "active" }
println(full)
""".trimIndent(),
"""
// Modern null safety
var config = 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(),
"""
// Diamond-safe Multiple Inheritance (C3 MRO)
interface Logger {
fun log(m) = println("[LOG] " + m)
}
interface Auth {
fun login(u) = println("Login: " + u)
}
class App() : Logger, Auth {
fun run() {
log("Starting...")
login("admin")
}
}
App().run()
""".trimIndent(),
"""
// Extension functions and properties
fun String.shout() = this.toUpperCase() + "!!!"
println("hello".shout())
val List.second get = this[1]
println([10, 20, 30].second)
""".trimIndent(),
"""
// Non-local returns from closures
fun findFirst(list, predicate) {
list.forEach {
if (predicate(it)) return@findFirst it
}
null
}
val found = findFirst([1, 5, 8, 12]) { it > 10 }
println("Found: " + found)
""".trimIndent(),
"""
// Easy operator overloading
class Vector(val x, val y) {
override fun plus(other) = Vector(x + other.x, y + other.y)
override fun toString() = "Vector(%g, %g)"(x, y)
}
val v1 = Vector(1, 2)
val v2 = Vector(3, 4)
println(v1 + v2)
""".trimIndent(),
"""
// Property delegation to Map
class User() {
var name by Map()
}
val u = User()
u.name = "Sergey"
println("User name: " + u.name)
""".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()
)
}
var currentSlide by remember { mutableStateOf((samples.indices).random()) }
LaunchedEffect(Unit) {
ensureSlideshowStyles()
while (true) {
delay(7000)
currentSlide = (currentSlide + 1) % samples.size
}
}
// Hero section
Section({ classes("py-4", "py-lg-5") }) {
Div({ classes("text-center") }) {
@@ -44,7 +181,7 @@ fun HomePage() {
}
}
// CTA buttons
Div({ classes("d-flex", "justify-content-center", "gap-2", "mb-4") }) {
Div({ classes("d-flex", "justify-content-center", "gap-2", "mb-2") }) {
A(attrs = {
classes("btn", "btn-primary", "btn-lg")
attr("href", "#/docs/tutorial.md")
@@ -72,47 +209,25 @@ fun HomePage() {
}
}
// Code sample
val code = """
// Create, transform, and verify — the Lyng way
import lyng.stdlib
fun findFirstPositive(list) {
list.forEach {
if (it > 0) return@findFirstPositive it
}
null
}
assertEquals(42, findFirstPositive([-1, 42, -5]))
val data = 1..5 // or [1,2,3,4,5]
val evens2 = data.filter { it % 2 == 0 }.map { it * it }
assertEquals([4, 16], evens2)
// Map literal with identifier keys, shorthand, and spread
val base = { a: 1, b: 2 }
val patch = { b: 3, c: }
val m = { "a": 0, ...base, ...patch, d: 4 }
assertEquals(1, m["a"]) // base overwrites 0
// Object expressions: anonymous classes on the fly
val worker = object : Runnable {
override fun run() = println("Working...")
}
worker.run()
// User-defined exceptions: real classes with custom fields
class MyError(val code, m) : Exception(m)
try {
throw MyError(500, "Something failed")
} catch (e: MyError) {
println("Error " + e.code + ": " + e.message)
}
>>> void
""".trimIndent()
val mapHtml = "<pre><code>" + htmlEscape(code) + "</code></pre>"
Div({ classes("markdown-body", "mt-3") }) {
UnsafeRawHtml(highlightLyngHtml(ensureBootstrapCodeBlocks(mapHtml)))
// Code sample slideshow
Div({
classes("markdown-body", "mt-0", "slide-container", "position-relative")
onClick {
window.location.hash = "#tryling?code=" + encodeURIComponent(samples[currentSlide])
}
}) {
Small({
classes("position-absolute", "top-0", "end-0", "m-2", "text-muted", "fw-light", "try-hint")
}) {
Text("click to try")
}
key(currentSlide) {
val slideCode = samples[currentSlide]
val mapHtml = "<pre><code>" + htmlEscape(slideCode) + "</code></pre>"
Div({ classes("slide-animation") }) {
UnsafeRawHtml(highlightLyngHtml(ensureBootstrapCodeBlocks(mapHtml)))
}
}
}
// Short features list
@@ -147,3 +262,36 @@ try {
}
}
}
fun ensureSlideshowStyles() {
if (window.document.getElementById("slideshow-styles") != null) return
val style = window.document.createElement("style") as org.w3c.dom.HTMLStyleElement
style.id = "slideshow-styles"
style.textContent = """
@keyframes slideIn {
from { opacity: 0; transform: translateX(30px); }
to { opacity: 1; transform: translateX(0); }
}
.slide-animation {
animation: slideIn 0.4s ease-out;
}
.slide-container {
min-height: 320px;
cursor: pointer;
transition: transform 0.2s;
background-color: var(--bs-body-bg);
}
.slide-container:hover {
transform: scale(1.005);
}
.try-hint {
opacity: 0.5;
transition: opacity 0.2s;
pointer-events: none;
}
.slide-container:hover .try-hint {
opacity: 1;
}
""".trimIndent()
window.document.head!!.appendChild(style)
}
+20 -11
View File
@@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* 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.
@@ -18,20 +18,30 @@
import androidx.compose.runtime.*
import kotlinx.coroutines.launch
import net.sergeych.lyng.LyngVersion
import net.sergeych.lyng.Scope
import net.sergeych.lyng.Script
import net.sergeych.lyng.ScriptError
import net.sergeych.lyngweb.EditorWithOverlay
import org.jetbrains.compose.web.dom.*
@Composable
fun TryLyngPage() {
fun TryLyngPage(route: String) {
val scope = rememberCoroutineScope()
var code by remember {
val initialCode = remember(route) {
val params = route.substringAfter('?', "")
val codeParam = params.split('&').find { it.startsWith("code=") }?.substringAfter('=')
if (codeParam != null) {
try {
decodeURIComponent(codeParam)
} catch (_: Throwable) {
null
}
} else null
}
var code by remember(initialCode) {
mutableStateOf(
"""
initialCode ?: """
// Welcome to Lyng! Edit and run.
// Try changing the data and press Ctrl+Enter or click Run.
import lyng.stdlib
val data = 1..5 // or [1, 2, 3, 4, 5]
data.filter { it % 2 == 0 }.map { it * it }
@@ -54,16 +64,16 @@ fun TryLyngPage() {
val printed = StringBuilder()
try {
// Create a fresh module scope each run so imports and vars are clean
val s = Scope.new()
val s = Script.newScope()
// Capture printed output from Lyng `print`/`println` into the UI result window
s.addVoidFn("print") {
for ((i, a) in args.withIndex()) {
for ((i, a) in this.args.withIndex()) {
if (i > 0) printed.append(' ')
printed.append(a.toString(this).value)
}
}
s.addVoidFn("println") {
for ((i, a) in args.withIndex()) {
for ((i, a) in this.args.withIndex()) {
if (i > 0) printed.append(' ')
printed.append(a.toString(this).value)
}
@@ -118,9 +128,8 @@ fun TryLyngPage() {
}
fun resetCode() {
code = """
code = initialCode ?: """
// Welcome to Lyng! Edit and run.
import lyng.stdlib
[1,2,3].map { it * 10 }
""".trimIndent()
output = null