Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e196bc3012 | ||
|
|
7b1ba71ef0 | ||
|
|
8b94432f66 | ||
|
|
338dc00573 | ||
|
|
d0230c5b89 | ||
|
|
5dc2159024 | ||
|
|
0759346e4b | ||
|
|
f848d79d39 | ||
|
|
52a3a96e3f | ||
|
|
b7dfda2f5d | ||
|
|
05e15e8e42 | ||
|
|
d4bb186f85 | ||
|
|
7bc17037f9 |
No files matched your search
+6
-1
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
@@ -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 |
|
||||
| | | |
|
||||
| | | |
|
||||
| | | |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,7 +1,6 @@
|
||||
#!/bin/env lyng
|
||||
|
||||
import lyng.io.fs
|
||||
import lyng.stdlib
|
||||
|
||||
val files = Path("../..").list().toList()
|
||||
// most long is longest?
|
||||
|
||||
+25
-8
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
This is a branch with a kotlin compiler bug problem
|
||||
|
||||
The problem is: the code built for wasmJS target does not load (tests does not load),
|
||||
the runtime error shows incorrect wasm binary content; $80 AI attempt to investigate
|
||||
it results that most probable source of the bug is bad compilation of the _anonymous suspending lambda expressions_.
|
||||
|
||||
Sorry to say it is one of most used constructions in the Lyng compiler.
|
||||
|
||||
So we have to suspend wasmJS development.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -239,5 +241,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,
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +122,24 @@ class Compiler(
|
||||
return doc
|
||||
}
|
||||
|
||||
private fun nextNonWhitespace(): Token {
|
||||
while (true) {
|
||||
val t = cc.next()
|
||||
when (t.type) {
|
||||
Token.Type.SINGLE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> {
|
||||
pushPendingDocToken(t)
|
||||
}
|
||||
|
||||
Token.Type.NEWLINE -> {
|
||||
if (!prevWasComment) clearPendingDoc() else prevWasComment = false
|
||||
}
|
||||
|
||||
Token.Type.EOF -> return t
|
||||
else -> return t
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set just before entering a declaration parse, taken from keyword token position
|
||||
private var pendingDeclStart: Pos? = null
|
||||
private var pendingDeclDoc: MiniDoc? = null
|
||||
@@ -283,11 +301,13 @@ class Compiler(
|
||||
}
|
||||
|
||||
private var lastAnnotation: (suspend (Scope, ObjString, Statement) -> Statement)? = null
|
||||
private var isTransientFlag: Boolean = false
|
||||
private var lastLabel: String? = null
|
||||
|
||||
private suspend fun parseStatement(braceMeansLambda: Boolean = false): Statement? {
|
||||
lastAnnotation = null
|
||||
lastLabel = null
|
||||
isTransientFlag = false
|
||||
while (true) {
|
||||
val t = cc.next()
|
||||
return when (t.type) {
|
||||
@@ -306,6 +326,10 @@ class Compiler(
|
||||
|
||||
Token.Type.ATLABEL -> {
|
||||
val label = t.value
|
||||
if (label == "Transient") {
|
||||
isTransientFlag = true
|
||||
continue
|
||||
}
|
||||
if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
|
||||
lastLabel = label
|
||||
}
|
||||
@@ -314,9 +338,15 @@ class Compiler(
|
||||
}
|
||||
|
||||
Token.Type.LABEL -> continue
|
||||
Token.Type.SINGLE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> continue
|
||||
Token.Type.SINGLE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> {
|
||||
pushPendingDocToken(t)
|
||||
continue
|
||||
}
|
||||
|
||||
Token.Type.NEWLINE -> continue
|
||||
Token.Type.NEWLINE -> {
|
||||
if (!prevWasComment) clearPendingDoc() else prevWasComment = false
|
||||
continue
|
||||
}
|
||||
|
||||
Token.Type.SEMICOLON -> continue
|
||||
|
||||
@@ -887,7 +917,15 @@ class Compiler(
|
||||
Token.Type.NEWLINE -> {}
|
||||
Token.Type.MULTILINE_COMMENT, Token.Type.SINGLE_LINE_COMMENT -> {}
|
||||
|
||||
Token.Type.ID -> {
|
||||
Token.Type.ID, Token.Type.ATLABEL -> {
|
||||
var isTransient = false
|
||||
if (t.type == Token.Type.ATLABEL) {
|
||||
if (t.value == "Transient") {
|
||||
isTransient = true
|
||||
t = cc.next()
|
||||
} else throw ScriptError(t.pos, "Unexpected label in argument list")
|
||||
}
|
||||
|
||||
// visibility
|
||||
val visibility = if (isClassDeclaration && t.value == "private") {
|
||||
t = cc.next()
|
||||
@@ -931,7 +969,8 @@ class Compiler(
|
||||
isEllipsis,
|
||||
defaultValue,
|
||||
access,
|
||||
visibility
|
||||
visibility,
|
||||
isTransient
|
||||
)
|
||||
|
||||
// important: valid argument list continues with ',' and ends with '->' or ')'
|
||||
@@ -1330,7 +1369,7 @@ class Compiler(
|
||||
modifiers.add(currentToken.value)
|
||||
val next = cc.peekNextNonWhitespace()
|
||||
if (next.type == Token.Type.ID || next.type == Token.Type.OBJECT) {
|
||||
currentToken = cc.next()
|
||||
currentToken = nextNonWhitespace()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
@@ -1358,7 +1397,9 @@ class Compiler(
|
||||
throw ScriptError(currentToken.pos, "abstract members cannot be private")
|
||||
|
||||
pendingDeclStart = firstId.pos
|
||||
pendingDeclDoc = consumePendingDoc()
|
||||
// pendingDeclDoc might be already set by an annotation
|
||||
if (pendingDeclDoc == null)
|
||||
pendingDeclDoc = consumePendingDoc()
|
||||
|
||||
val isMember = (codeContexts.lastOrNull() is CodeContext.ClassBody)
|
||||
|
||||
@@ -1885,27 +1926,31 @@ class Compiler(
|
||||
// skip '{'
|
||||
cc.skipTokenOfType(Token.Type.LBRACE)
|
||||
|
||||
do {
|
||||
val t = cc.nextNonWhitespace()
|
||||
when (t.type) {
|
||||
Token.Type.ID -> {
|
||||
names += t.value
|
||||
positions += t.pos
|
||||
val t1 = cc.nextNonWhitespace()
|
||||
when (t1.type) {
|
||||
Token.Type.COMMA ->
|
||||
continue
|
||||
if (cc.peekNextNonWhitespace().type != Token.Type.RBRACE) {
|
||||
do {
|
||||
val t = cc.nextNonWhitespace()
|
||||
when (t.type) {
|
||||
Token.Type.ID -> {
|
||||
names += t.value
|
||||
positions += t.pos
|
||||
val t1 = cc.nextNonWhitespace()
|
||||
when (t1.type) {
|
||||
Token.Type.COMMA ->
|
||||
continue
|
||||
|
||||
Token.Type.RBRACE -> break
|
||||
else -> {
|
||||
t1.raiseSyntax("unexpected token")
|
||||
Token.Type.RBRACE -> break
|
||||
else -> {
|
||||
t1.raiseSyntax("unexpected token")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> t.raiseSyntax("expected enum entry name")
|
||||
}
|
||||
} while (true)
|
||||
else -> t.raiseSyntax("expected enum entry name")
|
||||
}
|
||||
} while (true)
|
||||
} else {
|
||||
cc.nextNonWhitespace()
|
||||
}
|
||||
|
||||
miniSink?.onEnumDecl(
|
||||
MiniEnumDecl(
|
||||
@@ -2015,6 +2060,7 @@ class Compiler(
|
||||
|
||||
val newClass = ObjInstanceClass(className, *parentClasses.toTypedArray())
|
||||
newClass.isAnonymous = nameToken == null
|
||||
newClass.constructorMeta = ArgsDeclaration(emptyList(), Token.Type.RPAREN)
|
||||
for (i in parentClasses.indices) {
|
||||
val argsList = baseSpecs[i].args
|
||||
// In object, we evaluate parent args once at creation time
|
||||
@@ -2194,6 +2240,7 @@ class Compiler(
|
||||
// but we should pass Pos.builtIn to skip validation for now if needed,
|
||||
// or p.pos to allow it.
|
||||
pos = Pos.builtIn,
|
||||
isTransient = p.isTransient,
|
||||
type = ObjRecord.Type.ConstructorField
|
||||
)
|
||||
}
|
||||
@@ -2663,7 +2710,9 @@ class Compiler(
|
||||
isOverride: Boolean = false,
|
||||
isExtern: Boolean = false,
|
||||
isStatic: Boolean = false,
|
||||
isTransient: Boolean = isTransientFlag
|
||||
): Statement {
|
||||
isTransientFlag = false
|
||||
val actualExtern = isExtern || (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.isExtern == true
|
||||
var t = cc.next()
|
||||
val start = t.pos
|
||||
@@ -2833,16 +2882,16 @@ class Compiler(
|
||||
|
||||
val th = context.thisObj
|
||||
if (isStatic) {
|
||||
(th as ObjClass).createClassField(name, ObjUnset, false, visibility, null, start, type = ObjRecord.Type.Delegated).apply {
|
||||
(th as ObjClass).createClassField(name, ObjUnset, false, visibility, null, start, isTransient = isTransient, type = ObjRecord.Type.Delegated).apply {
|
||||
delegate = finalDelegate
|
||||
}
|
||||
context.addItem(name, false, ObjUnset, visibility, recordType = ObjRecord.Type.Delegated).apply {
|
||||
context.addItem(name, false, ObjUnset, visibility, recordType = ObjRecord.Type.Delegated, isTransient = isTransient).apply {
|
||||
delegate = finalDelegate
|
||||
}
|
||||
} else if (th is ObjClass) {
|
||||
val cls: ObjClass = th
|
||||
val storageName = "${cls.className}::$name"
|
||||
cls.createField(name, ObjUnset, false, visibility, null, start, declaringClass = cls, isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride, type = ObjRecord.Type.Delegated)
|
||||
cls.createField(name, ObjUnset, false, visibility, null, start, declaringClass = cls, isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride, isTransient = isTransient, type = ObjRecord.Type.Delegated)
|
||||
cls.instanceInitializers += statement(start) { scp ->
|
||||
val accessType2 = scp.resolveQualifiedIdentifier("DelegateAccess.Callable")
|
||||
val initValue2 = delegateExpression.execute(scp)
|
||||
@@ -2851,13 +2900,13 @@ class Compiler(
|
||||
} catch (e: Exception) {
|
||||
initValue2
|
||||
}
|
||||
scp.addItem(storageName, false, ObjUnset, visibility, null, recordType = ObjRecord.Type.Delegated, isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride).apply {
|
||||
scp.addItem(storageName, false, ObjUnset, visibility, null, recordType = ObjRecord.Type.Delegated, isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride, isTransient = isTransient).apply {
|
||||
delegate = finalDelegate2
|
||||
}
|
||||
ObjVoid
|
||||
}
|
||||
} else {
|
||||
context.addItem(name, false, ObjUnset, visibility, recordType = ObjRecord.Type.Delegated).apply {
|
||||
context.addItem(name, false, ObjUnset, visibility, recordType = ObjRecord.Type.Delegated, isTransient = isTransient).apply {
|
||||
delegate = finalDelegate
|
||||
}
|
||||
}
|
||||
@@ -2986,8 +3035,10 @@ class Compiler(
|
||||
isClosed: Boolean = false,
|
||||
isOverride: Boolean = false,
|
||||
isStatic: Boolean = false,
|
||||
isExtern: Boolean = false
|
||||
isExtern: Boolean = false,
|
||||
isTransient: Boolean = isTransientFlag
|
||||
): Statement {
|
||||
isTransientFlag = false
|
||||
val actualExtern = isExtern || (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.isExtern == true
|
||||
val nextToken = cc.next()
|
||||
val start = nextToken.pos
|
||||
@@ -3031,7 +3082,7 @@ class Compiler(
|
||||
return statement(start) { context ->
|
||||
val value = initialExpression.execute(context)
|
||||
for (name in names) {
|
||||
context.addItem(name, true, ObjVoid, visibility)
|
||||
context.addItem(name, true, ObjVoid, visibility, isTransient = isTransient)
|
||||
}
|
||||
pattern.setAt(start, context, value)
|
||||
if (!isMutable) {
|
||||
@@ -3233,17 +3284,18 @@ class Compiler(
|
||||
visibility,
|
||||
null,
|
||||
start,
|
||||
isTransient = isTransient,
|
||||
type = ObjRecord.Type.Delegated
|
||||
).apply {
|
||||
delegate = finalDelegate
|
||||
}
|
||||
// Also expose in current init scope
|
||||
addItem(name, isMutable, ObjUnset, visibility, null, ObjRecord.Type.Delegated).apply {
|
||||
addItem(name, isMutable, ObjUnset, visibility, null, ObjRecord.Type.Delegated, isTransient = isTransient).apply {
|
||||
delegate = finalDelegate
|
||||
}
|
||||
} else {
|
||||
(thisObj as ObjClass).createClassField(name, initValue, isMutable, visibility, null, start)
|
||||
addItem(name, isMutable, initValue, visibility, null, ObjRecord.Type.Field)
|
||||
(thisObj as ObjClass).createClassField(name, initValue, isMutable, visibility, null, start, isTransient = isTransient)
|
||||
addItem(name, isMutable, initValue, visibility, null, ObjRecord.Type.Field, isTransient = isTransient)
|
||||
}
|
||||
ObjVoid
|
||||
}
|
||||
@@ -3429,6 +3481,7 @@ class Compiler(
|
||||
visibility,
|
||||
setterVisibility,
|
||||
start,
|
||||
isTransient = isTransient,
|
||||
type = ObjRecord.Type.Delegated,
|
||||
isAbstract = isAbstract,
|
||||
isClosed = isClosed,
|
||||
@@ -3448,7 +3501,8 @@ class Compiler(
|
||||
recordType = ObjRecord.Type.Delegated,
|
||||
isAbstract = isAbstract,
|
||||
isClosed = isClosed,
|
||||
isOverride = isOverride
|
||||
isOverride = isOverride,
|
||||
isTransient = isTransient
|
||||
).apply {
|
||||
delegate = finalDelegate
|
||||
}
|
||||
@@ -3469,7 +3523,8 @@ class Compiler(
|
||||
recordType = ObjRecord.Type.Delegated,
|
||||
isAbstract = isAbstract,
|
||||
isClosed = isClosed,
|
||||
isOverride = isOverride
|
||||
isOverride = isOverride,
|
||||
isTransient = isTransient
|
||||
)
|
||||
rec.delegate = finalDelegate
|
||||
return@statement finalDelegate
|
||||
@@ -3488,7 +3543,8 @@ class Compiler(
|
||||
recordType = ObjRecord.Type.Delegated,
|
||||
isAbstract = isAbstract,
|
||||
isClosed = isClosed,
|
||||
isOverride = isOverride
|
||||
isOverride = isOverride,
|
||||
isTransient = isTransient
|
||||
)
|
||||
rec.delegate = finalDelegate
|
||||
return@statement finalDelegate
|
||||
@@ -3524,6 +3580,7 @@ class Compiler(
|
||||
isAbstract = isAbstract,
|
||||
isClosed = isClosed,
|
||||
isOverride = isOverride,
|
||||
isTransient = isTransient,
|
||||
type = ObjRecord.Type.Field
|
||||
)
|
||||
}
|
||||
@@ -3553,7 +3610,8 @@ class Compiler(
|
||||
recordType = ObjRecord.Type.Property,
|
||||
isAbstract = isAbstract,
|
||||
isClosed = isClosed,
|
||||
isOverride = isOverride
|
||||
isOverride = isOverride,
|
||||
isTransient = isTransient
|
||||
)
|
||||
prop
|
||||
}
|
||||
@@ -3576,6 +3634,7 @@ class Compiler(
|
||||
isClosed = isClosed,
|
||||
isOverride = isOverride,
|
||||
pos = start,
|
||||
isTransient = isTransient,
|
||||
type = ObjRecord.Type.Field
|
||||
)
|
||||
|
||||
@@ -3591,7 +3650,8 @@ class Compiler(
|
||||
recordType = ObjRecord.Type.Field,
|
||||
isAbstract = isAbstract,
|
||||
isClosed = isClosed,
|
||||
isOverride = isOverride
|
||||
isOverride = isOverride,
|
||||
isTransient = isTransient
|
||||
)
|
||||
ObjVoid
|
||||
}
|
||||
@@ -3608,14 +3668,15 @@ class Compiler(
|
||||
recordType = ObjRecord.Type.Field,
|
||||
isAbstract = isAbstract,
|
||||
isClosed = isClosed,
|
||||
isOverride = isOverride
|
||||
isOverride = isOverride,
|
||||
isTransient = isTransient
|
||||
)
|
||||
initValue
|
||||
}
|
||||
} else {
|
||||
// Not in class body: regular local/var declaration
|
||||
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
|
||||
context.addItem(name, isMutable, initValue, visibility, recordType = ObjRecord.Type.Other)
|
||||
context.addItem(name, isMutable, initValue, visibility, recordType = ObjRecord.Type.Other, isTransient = isTransient)
|
||||
initValue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,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 +120,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 +163,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 +186,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 +358,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)
|
||||
}
|
||||
}
|
||||
@@ -516,7 +516,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 +525,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
|
||||
|
||||
@@ -20,10 +20,7 @@ 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.stdlib_included.rootLyng
|
||||
@@ -162,6 +159,42 @@ class Script(
|
||||
if (x is ObjInt) ObjInt(x.value.absoluteValue) else ObjReal(x.toDouble().absoluteValue)
|
||||
}
|
||||
|
||||
addFnDoc(
|
||||
"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"
|
||||
) {
|
||||
val value = requiredArg<Obj>(0)
|
||||
val range = requiredArg<ObjRange>(1)
|
||||
|
||||
var result = value
|
||||
if (range.start != null && !range.start.isNull) {
|
||||
if (result.compareTo(this, range.start) < 0) {
|
||||
result = range.start
|
||||
}
|
||||
}
|
||||
if (range.end != null && !range.end.isNull) {
|
||||
val cmp = range.end.compareTo(this, 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
addVoidFn("assert") {
|
||||
val cond = requiredArg<ObjBool>(0)
|
||||
val message = if (args.size > 1)
|
||||
@@ -399,6 +432,12 @@ class Script(
|
||||
doc = "Point in time (epoch-based).",
|
||||
type = type("lyng.Class")
|
||||
)
|
||||
it.addConstDoc(
|
||||
name = "DateTime",
|
||||
value = ObjDateTime.type,
|
||||
doc = "Point in time in a specific time zone.",
|
||||
type = type("lyng.Class")
|
||||
)
|
||||
it.addConstDoc(
|
||||
name = "Duration",
|
||||
value = ObjDuration.type,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ open class Obj {
|
||||
if (rec != null && !rec.isAbstract) {
|
||||
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 invoke ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})"))
|
||||
|
||||
if (rec.type == ObjRecord.Type.Property) {
|
||||
@@ -140,7 +140,7 @@ 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 invoke ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})"))
|
||||
|
||||
if (rec.type == ObjRecord.Type.Property) {
|
||||
@@ -429,10 +429,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) {
|
||||
@@ -482,7 +482,7 @@ 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)
|
||||
@@ -526,7 +526,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 +574,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")
|
||||
@@ -760,6 +760,38 @@ open class Obj {
|
||||
) {
|
||||
thisObj.toJson(this).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"
|
||||
) {
|
||||
val range = requiredArg<ObjRange>(0)
|
||||
|
||||
var result = thisObj
|
||||
if (range.start != null && !range.start.isNull) {
|
||||
if (result.compareTo(this, range.start) < 0) {
|
||||
result = range.start
|
||||
}
|
||||
}
|
||||
if (range.end != null && !range.end.isNull) {
|
||||
val cmp = range.end.compareTo(this, 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -17,16 +17,24 @@
|
||||
|
||||
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.",
|
||||
@@ -451,6 +459,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 +470,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 +503,7 @@ open class ObjClass(
|
||||
isAbstract = isAbstract,
|
||||
isClosed = isClosed,
|
||||
isOverride = isOverride,
|
||||
isTransient = isTransient,
|
||||
type = type
|
||||
)
|
||||
members[name] = rec
|
||||
@@ -514,13 +524,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
|
||||
@@ -707,6 +718,22 @@ open class ObjClass(
|
||||
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()
|
||||
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
/*
|
||||
* 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.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 { s.execute(this.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 = { thisAs<ObjDateTime>().localDateTime.year.toObj() })
|
||||
addPropertyDoc("month", "The month component (1..12).", type("lyng.Int"), moduleName = "lyng.time",
|
||||
getter = { thisAs<ObjDateTime>().localDateTime.monthNumber.toObj() })
|
||||
addPropertyDoc("dayOfMonth", "The day of month component.", type("lyng.Int"), moduleName = "lyng.time",
|
||||
getter = { thisAs<ObjDateTime>().localDateTime.dayOfMonth.toObj() })
|
||||
addPropertyDoc("day", "Alias to dayOfMonth.", type("lyng.Int"), moduleName = "lyng.time",
|
||||
getter = { thisAs<ObjDateTime>().localDateTime.dayOfMonth.toObj() })
|
||||
addPropertyDoc("hour", "The hour component (0..23).", type("lyng.Int"), moduleName = "lyng.time",
|
||||
getter = { thisAs<ObjDateTime>().localDateTime.hour.toObj() })
|
||||
addPropertyDoc("minute", "The minute component (0..59).", type("lyng.Int"), moduleName = "lyng.time",
|
||||
getter = { thisAs<ObjDateTime>().localDateTime.minute.toObj() })
|
||||
addPropertyDoc("second", "The second component (0..59).", type("lyng.Int"), moduleName = "lyng.time",
|
||||
getter = { thisAs<ObjDateTime>().localDateTime.second.toObj() })
|
||||
addPropertyDoc("dayOfWeek", "The day of week (1=Monday, 7=Sunday).", type("lyng.Int"), moduleName = "lyng.time",
|
||||
getter = { 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 = { thisAs<ObjDateTime>().timeZone.id.toObj() })
|
||||
|
||||
addFnDoc("toInstant", "Convert this localized date time back to an absolute Instant.", returns = type("lyng.Instant"), moduleName = "lyng.time") {
|
||||
ObjInstant(thisAs<ObjDateTime>().instant)
|
||||
}
|
||||
addFnDoc("toEpochSeconds", "Return the number of full seconds since the Unix epoch (UTC).", returns = type("lyng.Int"), moduleName = "lyng.time") {
|
||||
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") {
|
||||
thisAs<ObjDateTime>().toRFC3339().toObj()
|
||||
}
|
||||
addFnDoc("toSortableString", "Alias to toRFC3339.", returns = type("lyng.String"), moduleName = "lyng.time") {
|
||||
thisAs<ObjDateTime>().toRFC3339().toObj()
|
||||
}
|
||||
|
||||
addFnDoc("toEpochMilliseconds", "Return the number of milliseconds since the Unix epoch (UTC).", returns = type("lyng.Int"), moduleName = "lyng.time") {
|
||||
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") {
|
||||
val tz = when (val a = args.list.getOrNull(0)) {
|
||||
is ObjString -> TimeZone.of(a.value)
|
||||
is ObjInt -> UtcOffset(seconds = a.value.toInt()).asTimeZone()
|
||||
else -> raiseIllegalArgument("invalid timezone: $a")
|
||||
}
|
||||
ObjDateTime(thisAs<ObjDateTime>().instant, tz)
|
||||
}
|
||||
addFnDoc("toUTC", "Shortcut to convert this date time to the UTC time zone.", returns = type("lyng.DateTime"), moduleName = "lyng.time") {
|
||||
ObjDateTime(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") {
|
||||
val n = args.list.getOrNull(0)?.toInt() ?: 0
|
||||
val res = thisAs<ObjDateTime>().instant.plus(n, DateTimeUnit.MONTH, thisAs<ObjDateTime>().timeZone)
|
||||
ObjDateTime(res, 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") {
|
||||
val n = args.list.getOrNull(0)?.toInt() ?: 0
|
||||
val res = thisAs<ObjDateTime>().instant.plus(n, DateTimeUnit.YEAR, thisAs<ObjDateTime>().timeZone)
|
||||
ObjDateTime(res, thisAs<ObjDateTime>().timeZone)
|
||||
}
|
||||
|
||||
addClassFn("now") {
|
||||
val tz = when (val a = args.list.getOrNull(0)) {
|
||||
null -> TimeZone.currentSystemDefault()
|
||||
is ObjString -> TimeZone.of(a.value)
|
||||
is ObjInt -> UtcOffset(seconds = a.value.toInt()).asTimeZone()
|
||||
else -> raiseIllegalArgument("invalid timezone: $a")
|
||||
}
|
||||
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") {
|
||||
val s = (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
|
||||
}
|
||||
|
||||
ObjDateTime(instant, tz)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -282,7 +281,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
}
|
||||
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,
|
||||
@@ -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
|
||||
@@ -511,7 +513,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
|
||||
memberFromAncestor(name)?.let { rec ->
|
||||
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 invoke method $name (declared in ${decl.className})"))
|
||||
val saved = instance.instanceScope.currentClassCtx
|
||||
instance.instanceScope.currentClassCtx = decl
|
||||
@@ -526,7 +528,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,
|
||||
|
||||
@@ -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,6 +17,7 @@
|
||||
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import kotlinx.datetime.*
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import net.sergeych.lyng.Scope
|
||||
@@ -28,7 +29,6 @@ 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
|
||||
|
||||
@@ -122,6 +122,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 -> {
|
||||
@@ -189,6 +190,18 @@ class ObjInstant(val instant: Instant,val truncateMode: LynonSettings.InstantTru
|
||||
moduleName = "lyng.time",
|
||||
getter = { ObjInt(thisAs<ObjInstant>().instant.nanosecondsOfSecond.toLong()) }
|
||||
)
|
||||
addFnDoc(
|
||||
name = "truncateToMinute",
|
||||
doc = "Truncate this instant to the nearest minute.",
|
||||
returns = type("lyng.Instant"),
|
||||
moduleName = "lyng.time"
|
||||
) {
|
||||
val t = 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)
|
||||
ObjInstant(truncated.toInstant(tz), LynonSettings.InstantTruncateMode.Second)
|
||||
}
|
||||
addFnDoc(
|
||||
name = "truncateToSecond",
|
||||
doc = "Truncate this instant to the nearest second.",
|
||||
@@ -222,6 +235,43 @@ class ObjInstant(val instant: Instant,val truncateMode: LynonSettings.InstantTru
|
||||
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"
|
||||
) {
|
||||
thisAs<ObjInstant>().instant.toString().toObj()
|
||||
}
|
||||
|
||||
addFnDoc(
|
||||
name = "toSortableString",
|
||||
doc = "Alias to toRFC3339.",
|
||||
returns = type("lyng.String"),
|
||||
moduleName = "lyng.time"
|
||||
) {
|
||||
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"
|
||||
) {
|
||||
val tz = when (val a = args.list.getOrNull(0)) {
|
||||
null -> TimeZone.currentSystemDefault()
|
||||
is ObjString -> TimeZone.of(a.value)
|
||||
is ObjInt -> UtcOffset(seconds = a.value.toInt()).asTimeZone()
|
||||
else -> raiseIllegalArgument("invalid timezone: $a")
|
||||
}
|
||||
ObjDateTime(thisAs<ObjInstant>().instant, tz)
|
||||
}
|
||||
|
||||
// class members
|
||||
|
||||
addClassConst("distantFuture", distantFuture)
|
||||
|
||||
@@ -1363,7 +1363,7 @@ class MethodCallRef(
|
||||
val decl = hierarchyMember.declaringClass ?: base.objClass
|
||||
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a ->
|
||||
val inst = obj as ObjInstance
|
||||
if (!visibility.isPublic && !canAccessMember(visibility, decl, sc.currentClassCtx))
|
||||
if (!visibility.isPublic && !canAccessMember(visibility, decl, sc.currentClassCtx, name))
|
||||
sc.raiseError(ObjIllegalAccessException(sc, "can't invoke non-public method $name"))
|
||||
callable.invoke(inst.instanceScope, inst, a)
|
||||
}
|
||||
@@ -1447,7 +1447,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 +1499,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 +1608,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 +1652,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 +1698,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
|
||||
}
|
||||
|
||||
@@ -301,6 +301,18 @@ data class ObjString(val value: String) : Obj() {
|
||||
) {
|
||||
thisAs<ObjString>().value.trim().let(::ObjString)
|
||||
}
|
||||
addFnDoc("isBlank", "Whether this string is empty or contains only whitespace characters.",
|
||||
returns = type("lyng.Bool"), moduleName = "lyng.stdlib") {
|
||||
ObjBool(thisAs<ObjString>().value.isBlank())
|
||||
}
|
||||
addFnDoc("isEmpty", "Whether this string is empty.",
|
||||
returns = type("lyng.Bool"), moduleName = "lyng.stdlib") {
|
||||
ObjBool(thisAs<ObjString>().value.isEmpty())
|
||||
}
|
||||
addFnDoc("isNotEmpty", "Whether this string is not empty.",
|
||||
returns = type("lyng.Bool"), moduleName = "lyng.stdlib") {
|
||||
ObjBool(thisAs<ObjString>().value.isNotEmpty())
|
||||
}
|
||||
addFnDoc(
|
||||
name = "matches",
|
||||
doc = "Whether this string matches the given regular expression or pattern 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.
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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" })
|
||||
}
|
||||
}
|
||||
@@ -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.invoke(scope, fn).toKotlin(s2))
|
||||
assertEquals(45L, fn.invoke(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())
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user