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
|
### 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
|
- Language: Added `return` statement
|
||||||
- `return [expression]` exits the innermost enclosing callable (function or lambda).
|
- `return [expression]` exits the innermost enclosing callable (function or lambda).
|
||||||
- Supports non-local returns using `@label` syntax (e.g., `return@outer 42`).
|
- 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.
|
- Header-specified constructor arguments are passed to direct bases.
|
||||||
- Visibility enforcement under MI:
|
- Visibility enforcement under MI:
|
||||||
- `private` visible only inside the declaring class body.
|
- `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:
|
- Diagnostics improvements:
|
||||||
- Missing member/field messages include receiver class and linearization order; hints for `this@Type` or casts when helpful.
|
- 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.
|
- 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`).
|
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 { ... }`.
|
- **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).
|
- **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).
|
- **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).
|
- **Equality**: `==` (equals), `!=` (not equals), `===` (ref identity), `!==` (ref not identity).
|
||||||
- **Comparison**: `<`, `>`, `<=`, `>=`, `<=>` (shuttle/spaceship, returns -1, 0, 1).
|
- **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
|
val area get = π * r * r
|
||||||
```
|
```
|
||||||
- **Mandatory `override`**: Required for all members existing in the ancestor chain.
|
- **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.
|
- **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.
|
- **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.
|
- **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]
|
val [first, middle..., last] = [1, 2, 3, 4, 5]
|
||||||
|
|
||||||
// Safe Navigation and Elvis
|
// Safe Navigation and Elvis
|
||||||
val companyName = person?.job?.company?.name ?? "Freelancer"
|
val companyName = person?.job?.company?.name ?: "Freelancer"
|
||||||
```
|
```
|
||||||
|
|
||||||
## 8. Standard Library Discovery
|
## 8. Standard Library Discovery
|
||||||
|
|||||||
+31
-10
@@ -425,7 +425,7 @@ Key rules and features:
|
|||||||
|
|
||||||
- Visibility
|
- Visibility
|
||||||
- `private`: accessible only inside the declaring class body; not visible in subclasses and cannot be accessed via `this@Type` or casts.
|
- `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
|
## Abstract Classes and Members
|
||||||
|
|
||||||
@@ -857,23 +857,44 @@ Private fields are visible only _inside the class instance_:
|
|||||||
void
|
void
|
||||||
>>> 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
|
||||||
|
|
||||||
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).
|
||||||
|
|
||||||
```
|
Protected members are not available from unrelated contexts:
|
||||||
class A() {
|
|
||||||
protected fun ping() { "pong" }
|
```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()
|
||||||
}
|
}
|
||||||
class B() : A() {
|
|
||||||
fun call() { this@A.ping() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val b = B()
|
class Derived : Base {
|
||||||
assertEquals("pong", b.call())
|
override protected fun foo() { "ok" }
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("ok", Derived().bar())
|
||||||
|
|
||||||
// Unrelated access is forbidden, even via cast
|
// 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
|
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 |
|
| isEndInclusive | true for '..' | Bool |
|
||||||
| isOpen | at any end | Bool |
|
| isOpen | at any end | Bool |
|
||||||
| isIntRange | both start and end are Int | Bool |
|
| isIntRange | both start and end are Int | Bool |
|
||||||
| start | | Bool |
|
| start | | Any? |
|
||||||
| end | | Bool |
|
| end | | Any? |
|
||||||
| size | for finite ranges, see above | Long |
|
| size | for finite ranges, see above | Long |
|
||||||
| [] | see above | |
|
| [] | 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
|
[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 |
|
| `.roundToInt()` | round to nearest int like round(x) | Int |
|
||||||
| `.toInt()` | convert integer part of real to `Int` dropping decimal part | 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() )
|
assertEquals( "{\"foo\":1,\"bar\":2}", Point(1,2).toJsonString() )
|
||||||
>>> void
|
>>> 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
|
import lyng.serialization
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ or transformed `Real` otherwise.
|
|||||||
| pow(x, y) | ${x^y}$ |
|
| pow(x, y) | ${x^y}$ |
|
||||||
| sqrt(x) | $ \sqrt {x}$ |
|
| sqrt(x) | $ \sqrt {x}$ |
|
||||||
| abs(x) | absolute value of x. Int if x is Int, Real otherwise |
|
| 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:
|
For example:
|
||||||
|
|
||||||
@@ -102,6 +103,11 @@ For example:
|
|||||||
// abs() keeps the argument type:
|
// abs() keeps the argument type:
|
||||||
assert( abs(-1) is Int)
|
assert( abs(-1) is Int)
|
||||||
assert( abs(-2.21) == 2.21 )
|
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
|
>>> void
|
||||||
|
|
||||||
## Scientific constant
|
## Scientific constant
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
#!/bin/env lyng
|
#!/bin/env lyng
|
||||||
|
|
||||||
import lyng.io.fs
|
import lyng.io.fs
|
||||||
import lyng.stdlib
|
|
||||||
|
|
||||||
val files = Path("../..").list().toList()
|
val files = Path("../..").list().toList()
|
||||||
// most long is longest?
|
// most long is longest?
|
||||||
|
|||||||
+25
-8
@@ -20,20 +20,37 @@ It is as simple as:
|
|||||||
assert( text.length > encodedBits.toBuffer().size )
|
assert( text.length > encodedBits.toBuffer().size )
|
||||||
>>> void
|
>>> 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 )
|
init {
|
||||||
assertEquals( 6, p.y )
|
// cachedValue can be recomputed here upon deserialization
|
||||||
>>> void
|
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:
|
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:
|
||||||
|
|
||||||
|
|||||||
+72
-133
@@ -2,166 +2,105 @@
|
|||||||
|
|
||||||
Lyng date and time support requires importing `lyng.time` packages. Lyng uses simple yet modern time object models:
|
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
|
- `Instant` class for absolute time stamps with platform-dependent resolution.
|
||||||
- `Duration` to represent amount of time not depending on the calendar, e.g. in absolute units (milliseconds, seconds,
|
- `DateTime` class for calendar-aware points in time within a specific time zone.
|
||||||
hours, days)
|
- `Duration` to represent amount of time not depending on the calendar (e.g., milliseconds, seconds).
|
||||||
|
|
||||||
## Time instant: `Instant`
|
## Time instant: `Instant`
|
||||||
|
|
||||||
Represent some moment of time not depending on the calendar (calendar for example may b e changed, daylight saving time
|
Represent some moment of time not depending on the calendar. It is similar to `TIMESTAMP` in SQL or `Instant` in Kotlin.
|
||||||
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.
|
|
||||||
|
|
||||||
Instant is comparable to other Instant. Subtracting instants produce `Duration`, period in time that is not dependent on
|
### Constructing and converting
|
||||||
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:
|
|
||||||
|
|
||||||
import lyng.time
|
import lyng.time
|
||||||
|
|
||||||
// default constructor returns time now:
|
// default constructor returns time now:
|
||||||
val t1 = Instant()
|
val t1 = Instant()
|
||||||
val t2 = Instant()
|
|
||||||
assert( t2 - t1 < 1.millisecond )
|
|
||||||
assert( t2.epochSeconds - t1.epochSeconds < 0.001 )
|
|
||||||
>>> void
|
|
||||||
|
|
||||||
## Constructing
|
// constructing from a number is treated as seconds since unix epoch:
|
||||||
|
val t2 = Instant(1704110400) // 2024-01-01T12:00:00Z
|
||||||
|
|
||||||
import lyng.time
|
// from RFC3339 string:
|
||||||
|
val t3 = Instant("2024-01-01T12:00:00.123456Z")
|
||||||
|
|
||||||
// empty constructor gives current time instant using system clock:
|
// truncation:
|
||||||
val now = Instant()
|
val t4 = t3.truncateToMinute
|
||||||
|
assertEquals(t4.toRFC3339(), "2024-01-01T12:00:00Z")
|
||||||
|
|
||||||
// constructor with Instant instance makes a copy:
|
// to localized DateTime (uses system default TZ if not specified):
|
||||||
assertEquals( now, Instant(now) )
|
val dt = t3.toDateTime("+02:00")
|
||||||
|
assertEquals(dt.hour, 14)
|
||||||
|
|
||||||
// constructing from a number is trated as seconds since unix epoch:
|
### Instant members
|
||||||
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()
|
|
||||||
|
|
||||||
// you cam add or subtract periods, and compare
|
|
||||||
assert( now - 5.minutes < now )
|
|
||||||
val oneHourAgo = now - 1.hour
|
|
||||||
assertEquals( now, oneHourAgo + 1.hour)
|
|
||||||
|
|
||||||
>>> 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
|
|
||||||
|
|
||||||
| member | description |
|
| member | description |
|
||||||
|--------------------------------|---------------------------------------------------------|
|
|--------------------------------|---------------------------------------------------------|
|
||||||
| epochSeconds: Real | positive or negative offset in seconds since Unix epoch |
|
| epochSeconds: Real | positive or negative offset in seconds since Unix epoch |
|
||||||
| epochWholeSeconds: Int | same, but in _whole seconds_. Slightly faster |
|
| 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` |
|
| isDistantFuture: Bool | true if it `Instant.distantFuture` |
|
||||||
| isDistantPast: Bool | true if it `Instant.distantPast` |
|
| isDistantPast: Bool | true if it `Instant.distantPast` |
|
||||||
| truncateToSecond: Intant | create new instnce truncated to second |
|
| truncateToMinute: Instant | create new instance truncated to minute |
|
||||||
| truncateToMillisecond: Instant | truncate new instance with to millisecond |
|
| truncateToSecond: Instant | create new instance truncated to second |
|
||||||
|
| truncateToMillisecond: Instant | truncate new instance to millisecond |
|
||||||
| truncateToMicrosecond: Instant | truncate new instance to microsecond |
|
| 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)
|
## Calendar time: `DateTime`
|
||||||
: 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)
|
|
||||||
|
|
||||||
## Class members
|
`DateTime` represents a point in time in a specific timezone. It provides access to calendar components like year,
|
||||||
|
month, and day.
|
||||||
|
|
||||||
|
### Constructing
|
||||||
|
|
||||||
|
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 |
|
| member | description |
|
||||||
|--------------------------------|----------------------------------------------|
|
|----------------------------------|-----------------------------------------------|
|
||||||
| Instant.now() | create new instance with current system time |
|
| year: Int | year component |
|
||||||
| Instant.distantPast: Instant | most distant instant in past |
|
| month: Int | month component (1..12) |
|
||||||
| Instant.distantFuture: Instant | most distant instant in future |
|
| 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 |
|
||||||
|
|
||||||
# `Duraion` class
|
### 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`.
|
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 |
|
| lower(), lowercase() | change case to unicode upper |
|
||||||
| upper(), uppercase() | change case to unicode lower |
|
| upper(), uppercase() | change case to unicode lower |
|
||||||
| trim() | trim space chars from both ends |
|
| 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 |
|
| startsWith(prefix) | true if starts with a prefix |
|
||||||
| endsWith(prefix) | true if ends with a prefix |
|
| endsWith(prefix) | true if ends with a prefix |
|
||||||
| last() | get last character of a string or throw |
|
| last() | get last character of a string or throw |
|
||||||
@@ -1701,7 +1704,7 @@ assertEquals(null, (buzz as? Foo)?.runA())
|
|||||||
|
|
||||||
Notes:
|
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.
|
- 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.
|
- Safe‑call `?.` works with `as?` for optional dispatch.
|
||||||
|
|
||||||
## Extension members
|
## Extension members
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ class Person(private var _age: Int) {
|
|||||||
### Private and Protected Setters
|
### 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.
|
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
|
```lyng
|
||||||
class Counter {
|
class Counter {
|
||||||
var count = 0
|
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).
|
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
|
## Tooling and Infrastructure
|
||||||
|
|
||||||
### CLI: Formatting Command
|
### CLI: Formatting Command
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ kotlin = "2.3.0"
|
|||||||
android-minSdk = "24"
|
android-minSdk = "24"
|
||||||
android-compileSdk = "34"
|
android-compileSdk = "34"
|
||||||
kotlinx-coroutines = "1.10.2"
|
kotlinx-coroutines = "1.10.2"
|
||||||
|
kotlinx-datetime = "0.6.1"
|
||||||
mp_bintools = "0.3.2"
|
mp_bintools = "0.3.2"
|
||||||
firebaseCrashlyticsBuildtools = "3.0.3"
|
firebaseCrashlyticsBuildtools = "3.0.3"
|
||||||
okioVersion = "3.10.2"
|
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" }
|
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-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-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" }
|
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" }
|
firebase-crashlytics-buildtools = { group = "com.google.firebase", name = "firebase-crashlytics-buildtools", version.ref = "firebaseCrashlyticsBuildtools" }
|
||||||
okio = { module = "com.squareup.okio:okio", version.ref = "okioVersion" }
|
okio = { module = "com.squareup.okio:okio", version.ref = "okioVersion" }
|
||||||
|
|||||||
@@ -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)
|
val e: Int = miniSource.offsetOf(d.range.end)
|
||||||
if (offset >= s && offset <= e) {
|
if (offset >= s && offset <= e) {
|
||||||
// For enum constant, we don't have detailed docs in MiniAst yet, but we can render a title
|
// 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 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."
|
"Print values to the standard output without a trailing newline. Accepts any number of arguments."
|
||||||
val title = "function $ident(values)"
|
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
|
// 4b) try class members like ClassName.member with inheritance fallback
|
||||||
val lhs = previousWordBefore(text, idRange.startOffset)
|
val lhs = previousWordBefore(text, idRange.startOffset)
|
||||||
@@ -519,7 +519,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
sb.append("<div class='doc-title'>").append(htmlEscape(title)).append("</div>")
|
sb.append(renderTitle(title))
|
||||||
sb.append(renderDocBody(d.doc))
|
sb.append(renderDocBody(d.doc))
|
||||||
return sb.toString()
|
return sb.toString()
|
||||||
}
|
}
|
||||||
@@ -527,7 +527,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
private fun renderParamDoc(fn: MiniFunDecl, p: MiniParam): String {
|
private fun renderParamDoc(fn: MiniFunDecl, p: MiniParam): String {
|
||||||
val title = "parameter ${p.name}${typeOf(p.type)} in ${fn.name}${signatureOf(fn)}"
|
val title = "parameter ${p.name}${typeOf(p.type)} in ${fn.name}${signatureOf(fn)}"
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
sb.append("<div class='doc-title'>").append(htmlEscape(title)).append("</div>")
|
sb.append(renderTitle(title))
|
||||||
|
|
||||||
// Find matching @param tag
|
// Find matching @param tag
|
||||||
fn.doc?.tags?.get("param")?.forEach { tag ->
|
fn.doc?.tags?.get("param")?.forEach { tag ->
|
||||||
@@ -549,7 +549,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
val staticStr = if (m.isStatic) "static " else ""
|
val staticStr = if (m.isStatic) "static " else ""
|
||||||
val title = "${staticStr}method $className.${m.name}(${params})${ret}"
|
val title = "${staticStr}method $className.${m.name}(${params})${ret}"
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
sb.append("<div class='doc-title'>").append(htmlEscape(title)).append("</div>")
|
sb.append(renderTitle(title))
|
||||||
sb.append(renderDocBody(m.doc))
|
sb.append(renderDocBody(m.doc))
|
||||||
return sb.toString()
|
return sb.toString()
|
||||||
}
|
}
|
||||||
@@ -560,7 +560,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
val staticStr = if (m.isStatic) "static " else ""
|
val staticStr = if (m.isStatic) "static " else ""
|
||||||
val title = "${staticStr}${kind} $className.${m.name}${ts}"
|
val title = "${staticStr}${kind} $className.${m.name}${ts}"
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
sb.append("<div class='doc-title'>").append(htmlEscape(title)).append("</div>")
|
sb.append(renderTitle(title))
|
||||||
sb.append(renderDocBody(m.doc))
|
sb.append(renderDocBody(m.doc))
|
||||||
return sb.toString()
|
return sb.toString()
|
||||||
}
|
}
|
||||||
@@ -579,6 +579,10 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
return "(${params})${ret}"
|
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) {
|
private fun htmlEscape(s: String): String = buildString(s.length) {
|
||||||
for (ch in s) append(
|
for (ch in s) append(
|
||||||
when (ch) {
|
when (ch) {
|
||||||
@@ -645,7 +649,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
|||||||
|
|
||||||
private fun renderOverloads(name: String, overloads: List<MiniFunDecl>): String {
|
private fun renderOverloads(name: String, overloads: List<MiniFunDecl>): String {
|
||||||
val sb = StringBuilder()
|
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>")
|
sb.append("<ul>")
|
||||||
overloads.forEach { fn ->
|
overloads.forEach { fn ->
|
||||||
sb.append("<li><code>")
|
sb.append("<li><code>")
|
||||||
|
|||||||
@@ -121,12 +121,39 @@ class LyngLexer : LexerBase() {
|
|||||||
|
|
||||||
// Number
|
// Number
|
||||||
if (ch.isDigit()) {
|
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++
|
i++
|
||||||
var hasDot = false
|
var hasDot = false
|
||||||
|
var hasE = false
|
||||||
while (i < endOffset) {
|
while (i < endOffset) {
|
||||||
val c = buffer[i]
|
val c = buffer[i]
|
||||||
if (c.isDigit()) { i++; continue }
|
if (c.isDigit() || c == '_') {
|
||||||
if (c == '.' && !hasDot) { hasDot = true; i++; continue }
|
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
|
break
|
||||||
}
|
}
|
||||||
myTokenEnd = i
|
myTokenEnd = i
|
||||||
@@ -161,6 +188,35 @@ class LyngLexer : LexerBase() {
|
|||||||
// Punctuation
|
// Punctuation
|
||||||
if (isPunct(ch)) {
|
if (isPunct(ch)) {
|
||||||
i++
|
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
|
myTokenEnd = i
|
||||||
myTokenType = LyngTokenTypes.PUNCT
|
myTokenType = LyngTokenTypes.PUNCT
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
|||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
group = "net.sergeych"
|
group = "net.sergeych"
|
||||||
version = "1.2.0"
|
version = "1.2.1-SNAPSHOT"
|
||||||
|
|
||||||
// Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below
|
// Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below
|
||||||
|
|
||||||
@@ -97,6 +97,7 @@ kotlin {
|
|||||||
kotlin.srcDir("$buildDir/generated/buildConfig/commonMain/kotlin")
|
kotlin.srcDir("$buildDir/generated/buildConfig/commonMain/kotlin")
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
|
||||||
//put your multiplatform dependencies here
|
//put your multiplatform dependencies here
|
||||||
api(libs.kotlinx.coroutines.core)
|
api(libs.kotlinx.coroutines.core)
|
||||||
api(libs.mp.bintools)
|
api(libs.mp.bintools)
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
|
|||||||
value.byValueCopy(),
|
value.byValueCopy(),
|
||||||
a.visibility ?: defaultVisibility,
|
a.visibility ?: defaultVisibility,
|
||||||
recordType = ObjRecord.Type.Argument,
|
recordType = ObjRecord.Type.Argument,
|
||||||
declaringClass = declaringClass)
|
declaringClass = declaringClass,
|
||||||
|
isTransient = a.isTransient)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -82,7 +83,8 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
|
|||||||
value.byValueCopy(),
|
value.byValueCopy(),
|
||||||
a.visibility ?: defaultVisibility,
|
a.visibility ?: defaultVisibility,
|
||||||
recordType = ObjRecord.Type.Argument,
|
recordType = ObjRecord.Type.Argument,
|
||||||
declaringClass = declaringClass)
|
declaringClass = declaringClass,
|
||||||
|
isTransient = a.isTransient)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare positional args and parameter count, handle tail-block binding
|
// 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 defaultValue: Statement? = null,
|
||||||
val accessType: AccessType? = null,
|
val accessType: AccessType? = null,
|
||||||
val visibility: Visibility? = 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) {
|
for (cls in effectiveClass.mro) {
|
||||||
val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
|
val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
|
||||||
if (rec != null && !rec.isAbstract) {
|
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)
|
return rec.copy(receiver = receiver)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Finally, root object fallback
|
// Finally, root object fallback
|
||||||
Obj.rootObjectType.members[name]?.let { rec ->
|
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)
|
return rec.copy(receiver = receiver)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,6 +122,24 @@ class Compiler(
|
|||||||
return doc
|
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
|
// Set just before entering a declaration parse, taken from keyword token position
|
||||||
private var pendingDeclStart: Pos? = null
|
private var pendingDeclStart: Pos? = null
|
||||||
private var pendingDeclDoc: MiniDoc? = null
|
private var pendingDeclDoc: MiniDoc? = null
|
||||||
@@ -283,11 +301,13 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var lastAnnotation: (suspend (Scope, ObjString, Statement) -> Statement)? = null
|
private var lastAnnotation: (suspend (Scope, ObjString, Statement) -> Statement)? = null
|
||||||
|
private var isTransientFlag: Boolean = false
|
||||||
private var lastLabel: String? = null
|
private var lastLabel: String? = null
|
||||||
|
|
||||||
private suspend fun parseStatement(braceMeansLambda: Boolean = false): Statement? {
|
private suspend fun parseStatement(braceMeansLambda: Boolean = false): Statement? {
|
||||||
lastAnnotation = null
|
lastAnnotation = null
|
||||||
lastLabel = null
|
lastLabel = null
|
||||||
|
isTransientFlag = false
|
||||||
while (true) {
|
while (true) {
|
||||||
val t = cc.next()
|
val t = cc.next()
|
||||||
return when (t.type) {
|
return when (t.type) {
|
||||||
@@ -306,6 +326,10 @@ class Compiler(
|
|||||||
|
|
||||||
Token.Type.ATLABEL -> {
|
Token.Type.ATLABEL -> {
|
||||||
val label = t.value
|
val label = t.value
|
||||||
|
if (label == "Transient") {
|
||||||
|
isTransientFlag = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
|
if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
|
||||||
lastLabel = label
|
lastLabel = label
|
||||||
}
|
}
|
||||||
@@ -314,9 +338,15 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Token.Type.LABEL -> continue
|
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
|
Token.Type.SEMICOLON -> continue
|
||||||
|
|
||||||
@@ -887,7 +917,15 @@ class Compiler(
|
|||||||
Token.Type.NEWLINE -> {}
|
Token.Type.NEWLINE -> {}
|
||||||
Token.Type.MULTILINE_COMMENT, Token.Type.SINGLE_LINE_COMMENT -> {}
|
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
|
// visibility
|
||||||
val visibility = if (isClassDeclaration && t.value == "private") {
|
val visibility = if (isClassDeclaration && t.value == "private") {
|
||||||
t = cc.next()
|
t = cc.next()
|
||||||
@@ -931,7 +969,8 @@ class Compiler(
|
|||||||
isEllipsis,
|
isEllipsis,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
access,
|
access,
|
||||||
visibility
|
visibility,
|
||||||
|
isTransient
|
||||||
)
|
)
|
||||||
|
|
||||||
// important: valid argument list continues with ',' and ends with '->' or ')'
|
// important: valid argument list continues with ',' and ends with '->' or ')'
|
||||||
@@ -1330,7 +1369,7 @@ class Compiler(
|
|||||||
modifiers.add(currentToken.value)
|
modifiers.add(currentToken.value)
|
||||||
val next = cc.peekNextNonWhitespace()
|
val next = cc.peekNextNonWhitespace()
|
||||||
if (next.type == Token.Type.ID || next.type == Token.Type.OBJECT) {
|
if (next.type == Token.Type.ID || next.type == Token.Type.OBJECT) {
|
||||||
currentToken = cc.next()
|
currentToken = nextNonWhitespace()
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -1358,6 +1397,8 @@ class Compiler(
|
|||||||
throw ScriptError(currentToken.pos, "abstract members cannot be private")
|
throw ScriptError(currentToken.pos, "abstract members cannot be private")
|
||||||
|
|
||||||
pendingDeclStart = firstId.pos
|
pendingDeclStart = firstId.pos
|
||||||
|
// pendingDeclDoc might be already set by an annotation
|
||||||
|
if (pendingDeclDoc == null)
|
||||||
pendingDeclDoc = consumePendingDoc()
|
pendingDeclDoc = consumePendingDoc()
|
||||||
|
|
||||||
val isMember = (codeContexts.lastOrNull() is CodeContext.ClassBody)
|
val isMember = (codeContexts.lastOrNull() is CodeContext.ClassBody)
|
||||||
@@ -1885,6 +1926,7 @@ class Compiler(
|
|||||||
// skip '{'
|
// skip '{'
|
||||||
cc.skipTokenOfType(Token.Type.LBRACE)
|
cc.skipTokenOfType(Token.Type.LBRACE)
|
||||||
|
|
||||||
|
if (cc.peekNextNonWhitespace().type != Token.Type.RBRACE) {
|
||||||
do {
|
do {
|
||||||
val t = cc.nextNonWhitespace()
|
val t = cc.nextNonWhitespace()
|
||||||
when (t.type) {
|
when (t.type) {
|
||||||
@@ -1906,6 +1948,9 @@ class Compiler(
|
|||||||
else -> t.raiseSyntax("expected enum entry name")
|
else -> t.raiseSyntax("expected enum entry name")
|
||||||
}
|
}
|
||||||
} while (true)
|
} while (true)
|
||||||
|
} else {
|
||||||
|
cc.nextNonWhitespace()
|
||||||
|
}
|
||||||
|
|
||||||
miniSink?.onEnumDecl(
|
miniSink?.onEnumDecl(
|
||||||
MiniEnumDecl(
|
MiniEnumDecl(
|
||||||
@@ -2015,6 +2060,7 @@ class Compiler(
|
|||||||
|
|
||||||
val newClass = ObjInstanceClass(className, *parentClasses.toTypedArray())
|
val newClass = ObjInstanceClass(className, *parentClasses.toTypedArray())
|
||||||
newClass.isAnonymous = nameToken == null
|
newClass.isAnonymous = nameToken == null
|
||||||
|
newClass.constructorMeta = ArgsDeclaration(emptyList(), Token.Type.RPAREN)
|
||||||
for (i in parentClasses.indices) {
|
for (i in parentClasses.indices) {
|
||||||
val argsList = baseSpecs[i].args
|
val argsList = baseSpecs[i].args
|
||||||
// In object, we evaluate parent args once at creation time
|
// 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,
|
// but we should pass Pos.builtIn to skip validation for now if needed,
|
||||||
// or p.pos to allow it.
|
// or p.pos to allow it.
|
||||||
pos = Pos.builtIn,
|
pos = Pos.builtIn,
|
||||||
|
isTransient = p.isTransient,
|
||||||
type = ObjRecord.Type.ConstructorField
|
type = ObjRecord.Type.ConstructorField
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -2663,7 +2710,9 @@ class Compiler(
|
|||||||
isOverride: Boolean = false,
|
isOverride: Boolean = false,
|
||||||
isExtern: Boolean = false,
|
isExtern: Boolean = false,
|
||||||
isStatic: Boolean = false,
|
isStatic: Boolean = false,
|
||||||
|
isTransient: Boolean = isTransientFlag
|
||||||
): Statement {
|
): Statement {
|
||||||
|
isTransientFlag = false
|
||||||
val actualExtern = isExtern || (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.isExtern == true
|
val actualExtern = isExtern || (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.isExtern == true
|
||||||
var t = cc.next()
|
var t = cc.next()
|
||||||
val start = t.pos
|
val start = t.pos
|
||||||
@@ -2833,16 +2882,16 @@ class Compiler(
|
|||||||
|
|
||||||
val th = context.thisObj
|
val th = context.thisObj
|
||||||
if (isStatic) {
|
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
|
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
|
delegate = finalDelegate
|
||||||
}
|
}
|
||||||
} else if (th is ObjClass) {
|
} else if (th is ObjClass) {
|
||||||
val cls: ObjClass = th
|
val cls: ObjClass = th
|
||||||
val storageName = "${cls.className}::$name"
|
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 ->
|
cls.instanceInitializers += statement(start) { scp ->
|
||||||
val accessType2 = scp.resolveQualifiedIdentifier("DelegateAccess.Callable")
|
val accessType2 = scp.resolveQualifiedIdentifier("DelegateAccess.Callable")
|
||||||
val initValue2 = delegateExpression.execute(scp)
|
val initValue2 = delegateExpression.execute(scp)
|
||||||
@@ -2851,13 +2900,13 @@ class Compiler(
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
initValue2
|
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
|
delegate = finalDelegate2
|
||||||
}
|
}
|
||||||
ObjVoid
|
ObjVoid
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
delegate = finalDelegate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2986,8 +3035,10 @@ class Compiler(
|
|||||||
isClosed: Boolean = false,
|
isClosed: Boolean = false,
|
||||||
isOverride: Boolean = false,
|
isOverride: Boolean = false,
|
||||||
isStatic: Boolean = false,
|
isStatic: Boolean = false,
|
||||||
isExtern: Boolean = false
|
isExtern: Boolean = false,
|
||||||
|
isTransient: Boolean = isTransientFlag
|
||||||
): Statement {
|
): Statement {
|
||||||
|
isTransientFlag = false
|
||||||
val actualExtern = isExtern || (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.isExtern == true
|
val actualExtern = isExtern || (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.isExtern == true
|
||||||
val nextToken = cc.next()
|
val nextToken = cc.next()
|
||||||
val start = nextToken.pos
|
val start = nextToken.pos
|
||||||
@@ -3031,7 +3082,7 @@ class Compiler(
|
|||||||
return statement(start) { context ->
|
return statement(start) { context ->
|
||||||
val value = initialExpression.execute(context)
|
val value = initialExpression.execute(context)
|
||||||
for (name in names) {
|
for (name in names) {
|
||||||
context.addItem(name, true, ObjVoid, visibility)
|
context.addItem(name, true, ObjVoid, visibility, isTransient = isTransient)
|
||||||
}
|
}
|
||||||
pattern.setAt(start, context, value)
|
pattern.setAt(start, context, value)
|
||||||
if (!isMutable) {
|
if (!isMutable) {
|
||||||
@@ -3233,17 +3284,18 @@ class Compiler(
|
|||||||
visibility,
|
visibility,
|
||||||
null,
|
null,
|
||||||
start,
|
start,
|
||||||
|
isTransient = isTransient,
|
||||||
type = ObjRecord.Type.Delegated
|
type = ObjRecord.Type.Delegated
|
||||||
).apply {
|
).apply {
|
||||||
delegate = finalDelegate
|
delegate = finalDelegate
|
||||||
}
|
}
|
||||||
// Also expose in current init scope
|
// 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
|
delegate = finalDelegate
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
(thisObj as ObjClass).createClassField(name, initValue, isMutable, visibility, null, start)
|
(thisObj as ObjClass).createClassField(name, initValue, isMutable, visibility, null, start, isTransient = isTransient)
|
||||||
addItem(name, isMutable, initValue, visibility, null, ObjRecord.Type.Field)
|
addItem(name, isMutable, initValue, visibility, null, ObjRecord.Type.Field, isTransient = isTransient)
|
||||||
}
|
}
|
||||||
ObjVoid
|
ObjVoid
|
||||||
}
|
}
|
||||||
@@ -3429,6 +3481,7 @@ class Compiler(
|
|||||||
visibility,
|
visibility,
|
||||||
setterVisibility,
|
setterVisibility,
|
||||||
start,
|
start,
|
||||||
|
isTransient = isTransient,
|
||||||
type = ObjRecord.Type.Delegated,
|
type = ObjRecord.Type.Delegated,
|
||||||
isAbstract = isAbstract,
|
isAbstract = isAbstract,
|
||||||
isClosed = isClosed,
|
isClosed = isClosed,
|
||||||
@@ -3448,7 +3501,8 @@ class Compiler(
|
|||||||
recordType = ObjRecord.Type.Delegated,
|
recordType = ObjRecord.Type.Delegated,
|
||||||
isAbstract = isAbstract,
|
isAbstract = isAbstract,
|
||||||
isClosed = isClosed,
|
isClosed = isClosed,
|
||||||
isOverride = isOverride
|
isOverride = isOverride,
|
||||||
|
isTransient = isTransient
|
||||||
).apply {
|
).apply {
|
||||||
delegate = finalDelegate
|
delegate = finalDelegate
|
||||||
}
|
}
|
||||||
@@ -3469,7 +3523,8 @@ class Compiler(
|
|||||||
recordType = ObjRecord.Type.Delegated,
|
recordType = ObjRecord.Type.Delegated,
|
||||||
isAbstract = isAbstract,
|
isAbstract = isAbstract,
|
||||||
isClosed = isClosed,
|
isClosed = isClosed,
|
||||||
isOverride = isOverride
|
isOverride = isOverride,
|
||||||
|
isTransient = isTransient
|
||||||
)
|
)
|
||||||
rec.delegate = finalDelegate
|
rec.delegate = finalDelegate
|
||||||
return@statement finalDelegate
|
return@statement finalDelegate
|
||||||
@@ -3488,7 +3543,8 @@ class Compiler(
|
|||||||
recordType = ObjRecord.Type.Delegated,
|
recordType = ObjRecord.Type.Delegated,
|
||||||
isAbstract = isAbstract,
|
isAbstract = isAbstract,
|
||||||
isClosed = isClosed,
|
isClosed = isClosed,
|
||||||
isOverride = isOverride
|
isOverride = isOverride,
|
||||||
|
isTransient = isTransient
|
||||||
)
|
)
|
||||||
rec.delegate = finalDelegate
|
rec.delegate = finalDelegate
|
||||||
return@statement finalDelegate
|
return@statement finalDelegate
|
||||||
@@ -3524,6 +3580,7 @@ class Compiler(
|
|||||||
isAbstract = isAbstract,
|
isAbstract = isAbstract,
|
||||||
isClosed = isClosed,
|
isClosed = isClosed,
|
||||||
isOverride = isOverride,
|
isOverride = isOverride,
|
||||||
|
isTransient = isTransient,
|
||||||
type = ObjRecord.Type.Field
|
type = ObjRecord.Type.Field
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -3553,7 +3610,8 @@ class Compiler(
|
|||||||
recordType = ObjRecord.Type.Property,
|
recordType = ObjRecord.Type.Property,
|
||||||
isAbstract = isAbstract,
|
isAbstract = isAbstract,
|
||||||
isClosed = isClosed,
|
isClosed = isClosed,
|
||||||
isOverride = isOverride
|
isOverride = isOverride,
|
||||||
|
isTransient = isTransient
|
||||||
)
|
)
|
||||||
prop
|
prop
|
||||||
}
|
}
|
||||||
@@ -3576,6 +3634,7 @@ class Compiler(
|
|||||||
isClosed = isClosed,
|
isClosed = isClosed,
|
||||||
isOverride = isOverride,
|
isOverride = isOverride,
|
||||||
pos = start,
|
pos = start,
|
||||||
|
isTransient = isTransient,
|
||||||
type = ObjRecord.Type.Field
|
type = ObjRecord.Type.Field
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3591,7 +3650,8 @@ class Compiler(
|
|||||||
recordType = ObjRecord.Type.Field,
|
recordType = ObjRecord.Type.Field,
|
||||||
isAbstract = isAbstract,
|
isAbstract = isAbstract,
|
||||||
isClosed = isClosed,
|
isClosed = isClosed,
|
||||||
isOverride = isOverride
|
isOverride = isOverride,
|
||||||
|
isTransient = isTransient
|
||||||
)
|
)
|
||||||
ObjVoid
|
ObjVoid
|
||||||
}
|
}
|
||||||
@@ -3608,14 +3668,15 @@ class Compiler(
|
|||||||
recordType = ObjRecord.Type.Field,
|
recordType = ObjRecord.Type.Field,
|
||||||
isAbstract = isAbstract,
|
isAbstract = isAbstract,
|
||||||
isClosed = isClosed,
|
isClosed = isClosed,
|
||||||
isOverride = isOverride
|
isOverride = isOverride,
|
||||||
|
isTransient = isTransient
|
||||||
)
|
)
|
||||||
initValue
|
initValue
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Not in class body: regular local/var declaration
|
// Not in class body: regular local/var declaration
|
||||||
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
|
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
|
initValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ open class Scope(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.objects[name]?.let { rec ->
|
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 ->
|
caller?.let { ctx ->
|
||||||
s.localBindings[ctx.mangledName(name)]?.let { rec ->
|
s.localBindings[ctx.mangledName(name)]?.let { rec ->
|
||||||
@@ -120,11 +120,11 @@ open class Scope(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.localBindings[name]?.let { rec ->
|
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 ->
|
s.getSlotIndexOf(name)?.let { idx ->
|
||||||
val rec = s.getSlotRecord(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
|
return null
|
||||||
}
|
}
|
||||||
@@ -163,7 +163,7 @@ open class Scope(
|
|||||||
this.extensions[cls]?.get(name)?.let { return it }
|
this.extensions[cls]?.get(name)?.let { return it }
|
||||||
}
|
}
|
||||||
return thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec ->
|
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
|
if (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property || rec.isAbstract) null
|
||||||
else rec
|
else rec
|
||||||
} else null
|
} else null
|
||||||
@@ -186,7 +186,7 @@ open class Scope(
|
|||||||
s.extensions[cls]?.get(name)?.let { return it }
|
s.extensions[cls]?.get(name)?.let { return it }
|
||||||
}
|
}
|
||||||
s.thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec ->
|
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) {
|
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
|
// ignore fields, properties and abstracts here, they will be handled by the caller via readField
|
||||||
} else return rec
|
} else return rec
|
||||||
@@ -358,14 +358,14 @@ open class Scope(
|
|||||||
for (cls in effectiveClass.mro) {
|
for (cls in effectiveClass.mro) {
|
||||||
val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
|
val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
|
||||||
if (rec != null && !rec.isAbstract) {
|
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)
|
return rec.copy(receiver = receiver)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Finally, root object fallback
|
// Finally, root object fallback
|
||||||
Obj.rootObjectType.members[name]?.let { rec ->
|
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)
|
return rec.copy(receiver = receiver)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -516,7 +516,8 @@ open class Scope(
|
|||||||
declaringClass: net.sergeych.lyng.obj.ObjClass? = currentClassCtx,
|
declaringClass: net.sergeych.lyng.obj.ObjClass? = currentClassCtx,
|
||||||
isAbstract: Boolean = false,
|
isAbstract: Boolean = false,
|
||||||
isClosed: Boolean = false,
|
isClosed: Boolean = false,
|
||||||
isOverride: Boolean = false
|
isOverride: Boolean = false,
|
||||||
|
isTransient: Boolean = false
|
||||||
): ObjRecord {
|
): ObjRecord {
|
||||||
val rec = ObjRecord(
|
val rec = ObjRecord(
|
||||||
value, isMutable, visibility, writeVisibility,
|
value, isMutable, visibility, writeVisibility,
|
||||||
@@ -524,7 +525,8 @@ open class Scope(
|
|||||||
type = recordType,
|
type = recordType,
|
||||||
isAbstract = isAbstract,
|
isAbstract = isAbstract,
|
||||||
isClosed = isClosed,
|
isClosed = isClosed,
|
||||||
isOverride = isOverride
|
isOverride = isOverride,
|
||||||
|
isTransient = isTransient
|
||||||
)
|
)
|
||||||
objects[name] = rec
|
objects[name] = rec
|
||||||
// Index this binding within the current frame to help resolve locals across suspension
|
// 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.delay
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import net.sergeych.lyng.Script.Companion.defaultImportManager
|
import net.sergeych.lyng.Script.Companion.defaultImportManager
|
||||||
import net.sergeych.lyng.miniast.addConstDoc
|
import net.sergeych.lyng.miniast.*
|
||||||
import net.sergeych.lyng.miniast.addFnDoc
|
|
||||||
import net.sergeych.lyng.miniast.addVoidFnDoc
|
|
||||||
import net.sergeych.lyng.miniast.type
|
|
||||||
import net.sergeych.lyng.obj.*
|
import net.sergeych.lyng.obj.*
|
||||||
import net.sergeych.lyng.pacman.ImportManager
|
import net.sergeych.lyng.pacman.ImportManager
|
||||||
import net.sergeych.lyng.stdlib_included.rootLyng
|
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)
|
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") {
|
addVoidFn("assert") {
|
||||||
val cond = requiredArg<ObjBool>(0)
|
val cond = requiredArg<ObjBool>(0)
|
||||||
val message = if (args.size > 1)
|
val message = if (args.size > 1)
|
||||||
@@ -399,6 +432,12 @@ class Script(
|
|||||||
doc = "Point in time (epoch-based).",
|
doc = "Point in time (epoch-based).",
|
||||||
type = type("lyng.Class")
|
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(
|
it.addConstDoc(
|
||||||
name = "Duration",
|
name = "Duration",
|
||||||
value = ObjDuration.type,
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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]. */
|
/** 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) {
|
val res = when (visibility) {
|
||||||
Visibility.Public -> true
|
Visibility.Public -> true
|
||||||
Visibility.Private -> (decl != null && caller === decl)
|
Visibility.Private -> (decl != null && caller === decl)
|
||||||
@@ -33,7 +38,14 @@ fun canAccessMember(visibility: Visibility, decl: net.sergeych.lyng.obj.ObjClass
|
|||||||
decl == null -> false
|
decl == null -> false
|
||||||
caller == null -> false
|
caller == null -> false
|
||||||
caller === decl -> true
|
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
|
return res
|
||||||
|
|||||||
@@ -108,8 +108,9 @@ object LyngFormatter {
|
|||||||
if (t == "finally") return true
|
if (t == "finally") return true
|
||||||
|
|
||||||
// Short definition form: fun x() = or val x =
|
// Short definition form: fun x() = or val x =
|
||||||
if (Regex("^(override\\s+)?(fun|fn)\\b.*=\\s*$").matches(t)) return true
|
// We allow optional 'static' as well
|
||||||
if (Regex("^(private\\s+|protected\\s+|public\\s+|override\\s+)?(val|var)\\b.*=\\s*$").matches(t)) return true
|
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 =
|
// property accessors ending with ) or =
|
||||||
if (isAccessorRelated(t)) {
|
if (isAccessorRelated(t)) {
|
||||||
@@ -122,7 +123,8 @@ object LyngFormatter {
|
|||||||
val t = s.trim()
|
val t = s.trim()
|
||||||
if (!t.endsWith("=")) return false
|
if (!t.endsWith("=")) return false
|
||||||
// Is it a function or property definition?
|
// 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?
|
// Is it an accessor?
|
||||||
if (isPropertyAccessor(t)) return true
|
if (isPropertyAccessor(t)) return true
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -44,13 +44,22 @@ data class MiniDoc(
|
|||||||
fun parse(range: MiniRange, lines: Iterable<String>, extraTags: Map<String, List<String>> = emptyMap()): MiniDoc {
|
fun parse(range: MiniRange, lines: Iterable<String>, extraTags: Map<String, List<String>> = emptyMap()): MiniDoc {
|
||||||
val parsedTags = mutableMapOf<String, MutableList<String>>()
|
val parsedTags = mutableMapOf<String, MutableList<String>>()
|
||||||
var currentTag: String? = null
|
var currentTag: String? = null
|
||||||
val currentContent = StringBuilder()
|
val currentContent = mutableListOf<String>()
|
||||||
|
|
||||||
fun flush() {
|
fun flush() {
|
||||||
currentTag?.let { tag ->
|
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")
|
||||||
}
|
}
|
||||||
currentContent.setLength(0)
|
parsedTags.getOrPut(tag) { mutableListOf() }.add(trimmedContent.trim())
|
||||||
|
}
|
||||||
|
currentContent.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
val descriptionLines = mutableListOf<String>()
|
val descriptionLines = mutableListOf<String>()
|
||||||
@@ -58,26 +67,38 @@ data class MiniDoc(
|
|||||||
|
|
||||||
for (rawLine in lines) {
|
for (rawLine in lines) {
|
||||||
for (line in rawLine.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("@")) {
|
if (trimmed.startsWith("@")) {
|
||||||
inTags = true
|
inTags = true
|
||||||
flush()
|
flush()
|
||||||
val parts = trimmed.substring(1).split(Regex("\\s+"), 2)
|
val parts = trimmed.substring(1).split(Regex("\\s+"), 2)
|
||||||
currentTag = parts[0]
|
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 {
|
} else {
|
||||||
if (inTags) {
|
if (inTags) {
|
||||||
if (currentContent.isNotEmpty()) currentContent.append("\n")
|
currentContent.add(cleanedLine)
|
||||||
currentContent.append(line)
|
|
||||||
} else {
|
} else {
|
||||||
descriptionLines.add(line)
|
descriptionLines.add(cleanedLine)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
flush()
|
flush()
|
||||||
|
|
||||||
val raw = descriptionLines.joinToString("\n").trimEnd()
|
val raw = descriptionLines.joinToString("\n").trimIndent().trim()
|
||||||
val summary = raw.lines().firstOrNull { it.isNotBlank() }?.trim()
|
val summary = raw.lines().firstOrNull { it.isNotBlank() }?.trim()
|
||||||
|
|
||||||
val finalTags = parsedTags.toMutableMap()
|
val finalTags = parsedTags.toMutableMap()
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ open class Obj {
|
|||||||
if (rec != null && !rec.isAbstract) {
|
if (rec != null && !rec.isAbstract) {
|
||||||
val decl = rec.declaringClass ?: cls
|
val decl = rec.declaringClass ?: cls
|
||||||
val caller = scope.currentClassCtx
|
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 ?: "?"})"))
|
scope.raiseError(ObjIllegalAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})"))
|
||||||
|
|
||||||
if (rec.type == ObjRecord.Type.Property) {
|
if (rec.type == ObjRecord.Type.Property) {
|
||||||
@@ -140,7 +140,7 @@ open class Obj {
|
|||||||
cls.members[name]?.let { rec ->
|
cls.members[name]?.let { rec ->
|
||||||
val decl = rec.declaringClass ?: cls
|
val decl = rec.declaringClass ?: cls
|
||||||
val caller = scope.currentClassCtx
|
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 ?: "?"})"))
|
scope.raiseError(ObjIllegalAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})"))
|
||||||
|
|
||||||
if (rec.type == ObjRecord.Type.Property) {
|
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? {
|
open suspend fun toKotlin(scope: Scope): Any? {
|
||||||
return toString()
|
return toString(scope).value
|
||||||
}
|
}
|
||||||
|
|
||||||
fun willMutate(scope: Scope) {
|
fun willMutate(scope: Scope) {
|
||||||
@@ -482,7 +482,7 @@ open class Obj {
|
|||||||
cls.members[name]?.let { rec ->
|
cls.members[name]?.let { rec ->
|
||||||
val decl = rec.declaringClass ?: cls
|
val decl = rec.declaringClass ?: cls
|
||||||
val caller = scope.currentClassCtx
|
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 ?: "?"})"))
|
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)
|
val resolved = resolveRecord(scope, rec, name, decl)
|
||||||
if (resolved.type == ObjRecord.Type.Fun && resolved.value is Statement)
|
if (resolved.type == ObjRecord.Type.Fun && resolved.value is Statement)
|
||||||
@@ -526,7 +526,7 @@ open class Obj {
|
|||||||
}
|
}
|
||||||
val caller = scope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
// Check visibility for non-property members here if they weren't checked before
|
// 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 ?: "?"})"))
|
scope.raiseError(ObjIllegalAccessException(scope, "can't access field ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})"))
|
||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
@@ -574,7 +574,7 @@ open class Obj {
|
|||||||
|
|
||||||
val decl = field.declaringClass
|
val decl = field.declaringClass
|
||||||
val caller = scope.currentClassCtx
|
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 ?: "?"})"))
|
scope.raiseError(ObjIllegalAccessException(scope, "can't assign field ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})"))
|
||||||
if (field.type == ObjRecord.Type.Delegated) {
|
if (field.type == ObjRecord.Type.Delegated) {
|
||||||
val del = field.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate")
|
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()
|
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
|
package net.sergeych.lyng.obj
|
||||||
|
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
import net.sergeych.lyng.*
|
import net.sergeych.lyng.*
|
||||||
import net.sergeych.lyng.miniast.*
|
import net.sergeych.lyng.miniast.*
|
||||||
import net.sergeych.lynon.LynonDecoder
|
import net.sergeych.lynon.LynonDecoder
|
||||||
|
import net.sergeych.lynon.LynonEncoder
|
||||||
import net.sergeych.lynon.LynonType
|
import net.sergeych.lynon.LynonType
|
||||||
|
|
||||||
// Simple id generator for class identities (not thread-safe; fine for scripts)
|
// Simple id generator for class identities (not thread-safe; fine for scripts)
|
||||||
private object ClassIdGen { var c: Long = 1L; fun nextId(): Long = c++ }
|
private object ClassIdGen { var c: Long = 1L; fun nextId(): Long = c++ }
|
||||||
|
|
||||||
val ObjClassType by lazy {
|
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(
|
addPropertyDoc(
|
||||||
name = "className",
|
name = "className",
|
||||||
doc = "Full name of this class including package if available.",
|
doc = "Full name of this class including package if available.",
|
||||||
@@ -451,6 +459,7 @@ open class ObjClass(
|
|||||||
isAbstract: Boolean = false,
|
isAbstract: Boolean = false,
|
||||||
isClosed: Boolean = false,
|
isClosed: Boolean = false,
|
||||||
isOverride: Boolean = false,
|
isOverride: Boolean = false,
|
||||||
|
isTransient: Boolean = false,
|
||||||
type: ObjRecord.Type = ObjRecord.Type.Field,
|
type: ObjRecord.Type = ObjRecord.Type.Field,
|
||||||
): ObjRecord {
|
): ObjRecord {
|
||||||
// Validation of override rules: only for non-system declarations
|
// Validation of override rules: only for non-system declarations
|
||||||
@@ -461,7 +470,7 @@ open class ObjClass(
|
|||||||
if (existing != null && existing.declaringClass != this) {
|
if (existing != null && existing.declaringClass != this) {
|
||||||
// If the existing member is private in the ancestor, it's not visible for overriding.
|
// 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.
|
// 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
|
// It's effectively not there for us, so actualOverride remains false
|
||||||
} else {
|
} else {
|
||||||
actualOverride = true
|
actualOverride = true
|
||||||
@@ -494,6 +503,7 @@ open class ObjClass(
|
|||||||
isAbstract = isAbstract,
|
isAbstract = isAbstract,
|
||||||
isClosed = isClosed,
|
isClosed = isClosed,
|
||||||
isOverride = isOverride,
|
isOverride = isOverride,
|
||||||
|
isTransient = isTransient,
|
||||||
type = type
|
type = type
|
||||||
)
|
)
|
||||||
members[name] = rec
|
members[name] = rec
|
||||||
@@ -514,13 +524,14 @@ open class ObjClass(
|
|||||||
visibility: Visibility = Visibility.Public,
|
visibility: Visibility = Visibility.Public,
|
||||||
writeVisibility: Visibility? = null,
|
writeVisibility: Visibility? = null,
|
||||||
pos: Pos = Pos.builtIn,
|
pos: Pos = Pos.builtIn,
|
||||||
|
isTransient: Boolean = false,
|
||||||
type: ObjRecord.Type = ObjRecord.Type.Field
|
type: ObjRecord.Type = ObjRecord.Type.Field
|
||||||
): ObjRecord {
|
): ObjRecord {
|
||||||
initClassScope()
|
initClassScope()
|
||||||
val existing = classScope!!.objects[name]
|
val existing = classScope!!.objects[name]
|
||||||
if (existing != null)
|
if (existing != null)
|
||||||
throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
|
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
|
// Structural change: bump layout version for PIC invalidation
|
||||||
layoutVersion += 1
|
layoutVersion += 1
|
||||||
return rec
|
return rec
|
||||||
@@ -707,6 +718,22 @@ open class ObjClass(
|
|||||||
return super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
|
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 =
|
open suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
|
||||||
scope.raiseNotImplemented()
|
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
|
if (cls.className == "Obj") break
|
||||||
val mangled = cls.mangledName(name)
|
val mangled = cls.mangledName(name)
|
||||||
instanceScope.objects[mangled]?.let { rec ->
|
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)
|
return resolveRecord(scope, rec, name, cls)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,8 +77,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
// 2. Unmangled storage
|
// 2. Unmangled storage
|
||||||
instanceScope.objects[name]?.let { rec ->
|
instanceScope.objects[name]?.let { rec ->
|
||||||
val decl = rec.declaringClass
|
val decl = rec.declaringClass
|
||||||
// Unmangled access is only allowed if it's public OR we are inside the same instance's method
|
if (canAccessMember(rec.visibility, decl, caller, name)) {
|
||||||
if ((scope.thisObj === this && caller != null) || canAccessMember(rec.visibility, decl, caller)) {
|
|
||||||
return resolveRecord(scope, rec, name, decl)
|
return resolveRecord(scope, rec, name, decl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,7 +173,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
if (cls.className == "Obj") break
|
if (cls.className == "Obj") break
|
||||||
val mangled = cls.mangledName(name)
|
val mangled = cls.mangledName(name)
|
||||||
instanceScope.objects[mangled]?.let { rec ->
|
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)
|
updateRecord(scope, rec, name, newValue, cls)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -184,7 +183,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
// 2. Unmangled storage
|
// 2. Unmangled storage
|
||||||
instanceScope.objects[name]?.let { rec ->
|
instanceScope.objects[name]?.let { rec ->
|
||||||
val decl = rec.declaringClass
|
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)
|
updateRecord(scope, rec, name, newValue, decl)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -282,7 +281,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
}
|
}
|
||||||
val decl = rec.declaringClass ?: cls
|
val decl = rec.declaringClass ?: cls
|
||||||
val effectiveCaller = caller ?: if (scope.thisObj === this) objClass else null
|
val effectiveCaller = caller ?: if (scope.thisObj === this) objClass else null
|
||||||
if (!canAccessMember(rec.visibility, decl, effectiveCaller))
|
if (!canAccessMember(rec.visibility, decl, effectiveCaller, name))
|
||||||
scope.raiseError(
|
scope.raiseError(
|
||||||
ObjIllegalAccessException(
|
ObjIllegalAccessException(
|
||||||
scope,
|
scope,
|
||||||
@@ -339,7 +338,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
// values, so we save size of the construction:
|
// values, so we save size of the construction:
|
||||||
|
|
||||||
// using objlist allow for some optimizations:
|
// 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)
|
encoder.encodeAnyList(scope, params)
|
||||||
val vars = serializingVars.values.map { it.value }
|
val vars = serializingVars.values.map { it.value }
|
||||||
if (vars.isNotEmpty()) {
|
if (vars.isNotEmpty()) {
|
||||||
@@ -358,8 +357,10 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
val result = mutableMapOf<String, JsonElement>()
|
val result = mutableMapOf<String, JsonElement>()
|
||||||
val meta = objClass.constructorMeta
|
val meta = objClass.constructorMeta
|
||||||
?: scope.raiseError("can't serialize non-serializable object (no constructor meta)")
|
?: scope.raiseError("can't serialize non-serializable object (no constructor meta)")
|
||||||
for (entry in meta.params)
|
for (entry in meta.params) {
|
||||||
|
if (!entry.isTransient)
|
||||||
result[entry.name] = readField(scope, entry.name).value.toJson(scope)
|
result[entry.name] = readField(scope, entry.name).value.toJson(scope)
|
||||||
|
}
|
||||||
for (i in serializingVars) {
|
for (i in serializingVars) {
|
||||||
// remove T:: prefix from the field name for JSON
|
// remove T:: prefix from the field name for JSON
|
||||||
val parts = i.key.split("::")
|
val parts = i.key.split("::")
|
||||||
@@ -378,7 +379,8 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
it.value.type.serializable &&
|
it.value.type.serializable &&
|
||||||
it.value.type == ObjRecord.Type.Field &&
|
it.value.type == ObjRecord.Type.Field &&
|
||||||
it.value.isMutable &&
|
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 {
|
protected val comparableVars: Map<String, ObjRecord> by lazy {
|
||||||
instanceScope.objects.filter {
|
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
|
// Visibility: declaring class is the qualified ancestor for mangled storage
|
||||||
val decl = rec.declaringClass ?: startClass
|
val decl = rec.declaringClass ?: startClass
|
||||||
val caller = scope.currentClassCtx
|
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})"))
|
scope.raiseError(ObjIllegalAccessException(scope, "can't access field $name (declared in ${decl.className})"))
|
||||||
return instance.resolveRecord(scope, rec, name, decl)
|
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 ->
|
instance.instanceScope[name]?.let { rec ->
|
||||||
val decl = rec.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
|
val decl = rec.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
|
||||||
val caller = scope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
if (!canAccessMember(rec.visibility, decl, caller))
|
if (!canAccessMember(rec.visibility, decl, caller, name))
|
||||||
scope.raiseError(
|
scope.raiseError(
|
||||||
ObjIllegalAccessException(
|
ObjIllegalAccessException(
|
||||||
scope,
|
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 r = memberFromAncestor(name) ?: scope.raiseError("no such field: $name")
|
||||||
val decl = r.declaringClass ?: startClass
|
val decl = r.declaringClass ?: startClass
|
||||||
val caller = scope.currentClassCtx
|
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})"))
|
scope.raiseError(ObjIllegalAccessException(scope, "can't access field $name (declared in ${decl.className})"))
|
||||||
|
|
||||||
return instance.resolveRecord(scope, r, name, decl)
|
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 ->
|
instance.instanceScope.objects[mangled]?.let { f ->
|
||||||
val decl = f.declaringClass ?: startClass
|
val decl = f.declaringClass ?: startClass
|
||||||
val caller = scope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
if (!canAccessMember(f.effectiveWriteVisibility, decl, caller))
|
if (!canAccessMember(f.effectiveWriteVisibility, decl, caller, name))
|
||||||
ObjIllegalAccessException(
|
ObjIllegalAccessException(
|
||||||
scope,
|
scope,
|
||||||
"can't assign to field $name (declared in ${decl.className})"
|
"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 ->
|
instance.instanceScope[name]?.let { f ->
|
||||||
val decl = f.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
|
val decl = f.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
|
||||||
val caller = scope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
if (!canAccessMember(f.effectiveWriteVisibility, decl, caller))
|
if (!canAccessMember(f.effectiveWriteVisibility, decl, caller, name))
|
||||||
ObjIllegalAccessException(
|
ObjIllegalAccessException(
|
||||||
scope,
|
scope,
|
||||||
"can't assign to field $name (declared in ${decl?.className ?: "?"})"
|
"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 r = memberFromAncestor(name) ?: scope.raiseError("no such field: $name")
|
||||||
val decl = r.declaringClass ?: startClass
|
val decl = r.declaringClass ?: startClass
|
||||||
val caller = scope.currentClassCtx
|
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()
|
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.isMutable) scope.raiseError("can't assign to read-only field: $name")
|
||||||
if (r.value.assign(scope, newValue) == null) r.value = newValue
|
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 ->
|
memberFromAncestor(name)?.let { rec ->
|
||||||
val decl = rec.declaringClass ?: startClass
|
val decl = rec.declaringClass ?: startClass
|
||||||
val caller = scope.currentClassCtx
|
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})"))
|
scope.raiseError(ObjIllegalAccessException(scope, "can't invoke method $name (declared in ${decl.className})"))
|
||||||
val saved = instance.instanceScope.currentClassCtx
|
val saved = instance.instanceScope.currentClassCtx
|
||||||
instance.instanceScope.currentClassCtx = decl
|
instance.instanceScope.currentClassCtx = decl
|
||||||
@@ -526,7 +528,7 @@ class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjCla
|
|||||||
instance.instanceScope[name]?.let { rec ->
|
instance.instanceScope[name]?.let { rec ->
|
||||||
val decl = rec.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
|
val decl = rec.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
|
||||||
val caller = scope.currentClassCtx
|
val caller = scope.currentClassCtx
|
||||||
if (!canAccessMember(rec.visibility, decl, caller))
|
if (!canAccessMember(rec.visibility, decl, caller, name))
|
||||||
scope.raiseError(
|
scope.raiseError(
|
||||||
ObjIllegalAccessException(
|
ObjIllegalAccessException(
|
||||||
scope,
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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) {
|
class ObjInstanceClass(val name: String, vararg parents: ObjClass) : ObjClass(name, *parents) {
|
||||||
|
|
||||||
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
|
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
|
||||||
val args = decoder.decodeAnyList(scope)
|
val serializedArgs = decoder.decodeAnyList(scope)
|
||||||
val actualSize = constructorMeta?.params?.size ?: 0
|
val meta = constructorMeta ?: scope.raiseError("no constructor meta for $name")
|
||||||
if (args.size > actualSize)
|
val nonTransientCount = meta.params.count { !it.isTransient }
|
||||||
scope.raiseIllegalArgument("constructor $name has only $actualSize but serialized version has ${args.size}")
|
if (serializedArgs.size != nonTransientCount)
|
||||||
val newScope = scope.createChildScope(args = Arguments(args))
|
scope.raiseIllegalArgument("constructor $name expects $nonTransientCount non-transient arguments, but serialized version has ${serializedArgs.size}")
|
||||||
val instance = createInstance(newScope)
|
|
||||||
initializeInstance(instance, newScope.args, runConstructors = false)
|
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 {
|
return instance.apply {
|
||||||
deserializeStateVars(scope, decoder)
|
deserializeStateVars(scope, decoder)
|
||||||
invokeInstanceMethod(scope, "onDeserialized") { ObjVoid }
|
invokeInstanceMethod(scope, "onDeserialized") { ObjVoid }
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
package net.sergeych.lyng.obj
|
package net.sergeych.lyng.obj
|
||||||
|
|
||||||
|
import kotlinx.datetime.*
|
||||||
import kotlinx.serialization.json.JsonElement
|
import kotlinx.serialization.json.JsonElement
|
||||||
import kotlinx.serialization.json.JsonPrimitive
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
import net.sergeych.lyng.Scope
|
import net.sergeych.lyng.Scope
|
||||||
@@ -28,7 +29,6 @@ import net.sergeych.lynon.LynonEncoder
|
|||||||
import net.sergeych.lynon.LynonSettings
|
import net.sergeych.lynon.LynonSettings
|
||||||
import net.sergeych.lynon.LynonType
|
import net.sergeych.lynon.LynonType
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
import kotlin.time.Instant
|
|
||||||
import kotlin.time.isDistantFuture
|
import kotlin.time.isDistantFuture
|
||||||
import kotlin.time.isDistantPast
|
import kotlin.time.isDistantPast
|
||||||
|
|
||||||
@@ -122,6 +122,7 @@ class ObjInstant(val instant: Instant,val truncateMode: LynonSettings.InstantTru
|
|||||||
val nanos = (a0.value - seconds) * 1e9
|
val nanos = (a0.value - seconds) * 1e9
|
||||||
Instant.fromEpochSeconds(seconds, nanos.toLong())
|
Instant.fromEpochSeconds(seconds, nanos.toLong())
|
||||||
}
|
}
|
||||||
|
is ObjString -> Instant.parse(a0.value)
|
||||||
is ObjInstant -> a0.instant
|
is ObjInstant -> a0.instant
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
@@ -189,6 +190,18 @@ class ObjInstant(val instant: Instant,val truncateMode: LynonSettings.InstantTru
|
|||||||
moduleName = "lyng.time",
|
moduleName = "lyng.time",
|
||||||
getter = { ObjInt(thisAs<ObjInstant>().instant.nanosecondsOfSecond.toLong()) }
|
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(
|
addFnDoc(
|
||||||
name = "truncateToSecond",
|
name = "truncateToSecond",
|
||||||
doc = "Truncate this instant to the nearest second.",
|
doc = "Truncate this instant to the nearest second.",
|
||||||
@@ -222,6 +235,43 @@ class ObjInstant(val instant: Instant,val truncateMode: LynonSettings.InstantTru
|
|||||||
LynonSettings.InstantTruncateMode.Microsecond
|
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
|
// class members
|
||||||
|
|
||||||
addClassConst("distantFuture", distantFuture)
|
addClassConst("distantFuture", distantFuture)
|
||||||
|
|||||||
@@ -1363,7 +1363,7 @@ class MethodCallRef(
|
|||||||
val decl = hierarchyMember.declaringClass ?: base.objClass
|
val decl = hierarchyMember.declaringClass ?: base.objClass
|
||||||
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a ->
|
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a ->
|
||||||
val inst = obj as ObjInstance
|
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"))
|
sc.raiseError(ObjIllegalAccessException(sc, "can't invoke non-public method $name"))
|
||||||
callable.invoke(inst.instanceScope, inst, a)
|
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)
|
val slot = if (hit) cachedSlot else resolveSlot(scope)
|
||||||
if (slot >= 0) {
|
if (slot >= 0) {
|
||||||
val rec = scope.getSlotRecord(slot)
|
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
|
// Not visible via slot, fallback to other lookups
|
||||||
} else {
|
} else {
|
||||||
if (PerfFlags.PIC_DEBUG_COUNTERS) {
|
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)
|
val slot = if (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount()) cachedSlot else resolveSlot(scope)
|
||||||
if (slot >= 0) {
|
if (slot >= 0) {
|
||||||
val rec = scope.getSlotRecord(slot)
|
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)
|
scope.assign(rec, name, newValue)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1608,7 +1608,7 @@ class FastLocalVarRef(
|
|||||||
val actualOwner = cachedOwnerScope
|
val actualOwner = cachedOwnerScope
|
||||||
if (slot >= 0 && actualOwner != null) {
|
if (slot >= 0 && actualOwner != null) {
|
||||||
val rec = actualOwner.getSlotRecord(slot)
|
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 (PerfFlags.PIC_DEBUG_COUNTERS) {
|
||||||
if (ownerValid) PerfStats.fastLocalHit++ else PerfStats.fastLocalMiss++
|
if (ownerValid) PerfStats.fastLocalHit++ else PerfStats.fastLocalMiss++
|
||||||
}
|
}
|
||||||
@@ -1652,7 +1652,7 @@ class FastLocalVarRef(
|
|||||||
val actualOwner = cachedOwnerScope
|
val actualOwner = cachedOwnerScope
|
||||||
if (slot >= 0 && actualOwner != null) {
|
if (slot >= 0 && actualOwner != null) {
|
||||||
val rec = actualOwner.getSlotRecord(slot)
|
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)
|
return scope.resolve(rec, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1698,7 +1698,7 @@ class FastLocalVarRef(
|
|||||||
val actualOwner = cachedOwnerScope
|
val actualOwner = cachedOwnerScope
|
||||||
if (slot >= 0 && actualOwner != null) {
|
if (slot >= 0 && actualOwner != null) {
|
||||||
val rec = actualOwner.getSlotRecord(slot)
|
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)
|
scope.assign(rec, name, newValue)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -301,6 +301,18 @@ data class ObjString(val value: String) : Obj() {
|
|||||||
) {
|
) {
|
||||||
thisAs<ObjString>().value.trim().let(::ObjString)
|
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(
|
addFnDoc(
|
||||||
name = "matches",
|
name = "matches",
|
||||||
doc = "Whether this string matches the given regular expression or pattern string.",
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.Scope
|
||||||
import net.sergeych.lyng.obj.Obj
|
import net.sergeych.lyng.obj.Obj
|
||||||
import net.sergeych.lyng.obj.ObjClass
|
import net.sergeych.lyng.obj.ObjClass
|
||||||
|
import net.sergeych.lyng.obj.ObjInstance
|
||||||
import net.sergeych.lyng.obj.ObjString
|
import net.sergeych.lyng.obj.ObjString
|
||||||
|
|
||||||
open class LynonDecoder(val bin: BitInput, val settings: LynonSettings = LynonSettings.default) {
|
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 {
|
private suspend fun decodeClassObj(scope: Scope): ObjClass {
|
||||||
val className = decodeObject(scope, ObjString.type, null) as ObjString
|
val className = decodeObject(scope, ObjString.type, null) as ObjString
|
||||||
return scope.get(className.value)?.value?.let {
|
return scope.get(className.value)?.value?.let {
|
||||||
if (it !is ObjClass)
|
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}")
|
scope.raiseClassCastError("Expected obj class but got ${it::class.simpleName}")
|
||||||
it
|
|
||||||
} ?: run {
|
} ?: run {
|
||||||
// Use Scope API that mirrors compiler-emitted ObjRef chain for qualified identifiers
|
// Use Scope API that mirrors compiler-emitted ObjRef chain for qualified identifiers
|
||||||
val evaluated = scope.resolveQualifiedIdentifier(className.value)
|
val evaluated = scope.resolveQualifiedIdentifier(className.value)
|
||||||
if (evaluated !is ObjClass)
|
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}")
|
scope.raiseClassCastError("Expected obj class but got ${evaluated::class.simpleName}")
|
||||||
evaluated
|
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("first line of x"), "should contain first line")
|
||||||
assertTrue(xParam.contains("second line of x"), "should contain second 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 kotlinx.coroutines.test.runTest
|
||||||
import net.sergeych.lyng.Script
|
import net.sergeych.lyng.Script
|
||||||
|
import net.sergeych.lyng.Statement
|
||||||
import net.sergeych.lyng.eval
|
import net.sergeych.lyng.eval
|
||||||
import net.sergeych.lyng.obj.ObjInstance
|
import net.sergeych.lyng.obj.ObjInstance
|
||||||
import net.sergeych.lyng.obj.ObjList
|
import net.sergeych.lyng.obj.ObjList
|
||||||
@@ -776,4 +777,138 @@ class OOTest {
|
|||||||
|
|
||||||
assertEquals("success", result.toString(), "Parameter 'id' should shadow method 'id' in block")
|
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( n1 - now, 7.seconds )
|
||||||
assertEquals( now - n1, -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()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
delay(1000)
|
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
|
@Test
|
||||||
fun testDoubleImports() = runTest {
|
fun testDoubleImports() = runTest {
|
||||||
val s = Scope.new()
|
val s = Scope.new()
|
||||||
@@ -4855,5 +4999,41 @@ class ScriptTest {
|
|||||||
assertEquals(["bar"], A().f2("bar"))
|
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()
|
""".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 cfg = LyngFormatConfig(indentSize = 4, continuationIndentSize = 4)
|
||||||
val out = LyngFormatter.reindent(src, cfg)
|
val out = LyngFormatter.reindent(src, cfg)
|
||||||
assertEquals(expected, out)
|
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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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") }) {
|
Div({ classes("col-12", if (isDocsRoute) "col-lg-9" else "col-lg-12") }) {
|
||||||
when {
|
when {
|
||||||
route.isBlank() -> HomePage()
|
route.isBlank() -> HomePage()
|
||||||
route == "tryling" -> TryLyngPage()
|
route.startsWith("tryling") -> TryLyngPage(route)
|
||||||
route.startsWith("search") -> SearchPage(route)
|
route.startsWith("search") -> SearchPage(route)
|
||||||
!isDocsRoute -> ReferencePage()
|
!isDocsRoute -> ReferencePage()
|
||||||
else -> DocsPage(
|
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.ensureBootstrapCodeBlocks
|
||||||
import net.sergeych.lyngweb.highlightLyngHtml
|
import net.sergeych.lyngweb.highlightLyngHtml
|
||||||
import net.sergeych.lyngweb.htmlEscape
|
import net.sergeych.lyngweb.htmlEscape
|
||||||
@@ -23,6 +25,141 @@ import org.jetbrains.compose.web.dom.*
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomePage() {
|
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
|
// Hero section
|
||||||
Section({ classes("py-4", "py-lg-5") }) {
|
Section({ classes("py-4", "py-lg-5") }) {
|
||||||
Div({ classes("text-center") }) {
|
Div({ classes("text-center") }) {
|
||||||
@@ -44,7 +181,7 @@ fun HomePage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// CTA buttons
|
// 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 = {
|
A(attrs = {
|
||||||
classes("btn", "btn-primary", "btn-lg")
|
classes("btn", "btn-primary", "btn-lg")
|
||||||
attr("href", "#/docs/tutorial.md")
|
attr("href", "#/docs/tutorial.md")
|
||||||
@@ -72,48 +209,26 @@ fun HomePage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Code sample
|
// Code sample slideshow
|
||||||
val code = """
|
Div({
|
||||||
// Create, transform, and verify — the Lyng way
|
classes("markdown-body", "mt-0", "slide-container", "position-relative")
|
||||||
import lyng.stdlib
|
onClick {
|
||||||
|
window.location.hash = "#tryling?code=" + encodeURIComponent(samples[currentSlide])
|
||||||
fun findFirstPositive(list) {
|
|
||||||
list.forEach {
|
|
||||||
if (it > 0) return@findFirstPositive it
|
|
||||||
}
|
}
|
||||||
null
|
}) {
|
||||||
|
Small({
|
||||||
|
classes("position-absolute", "top-0", "end-0", "m-2", "text-muted", "fw-light", "try-hint")
|
||||||
|
}) {
|
||||||
|
Text("click to try")
|
||||||
}
|
}
|
||||||
assertEquals(42, findFirstPositive([-1, 42, -5]))
|
key(currentSlide) {
|
||||||
|
val slideCode = samples[currentSlide]
|
||||||
val data = 1..5 // or [1,2,3,4,5]
|
val mapHtml = "<pre><code>" + htmlEscape(slideCode) + "</code></pre>"
|
||||||
val evens2 = data.filter { it % 2 == 0 }.map { it * it }
|
Div({ classes("slide-animation") }) {
|
||||||
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)))
|
UnsafeRawHtml(highlightLyngHtml(ensureBootstrapCodeBlocks(mapHtml)))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Short features list
|
// Short features list
|
||||||
Div({ classes("row", "g-4", "mt-1") }) {
|
Div({ classes("row", "g-4", "mt-1") }) {
|
||||||
@@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -18,20 +18,30 @@
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import net.sergeych.lyng.LyngVersion
|
import net.sergeych.lyng.LyngVersion
|
||||||
import net.sergeych.lyng.Scope
|
import net.sergeych.lyng.Script
|
||||||
import net.sergeych.lyng.ScriptError
|
import net.sergeych.lyng.ScriptError
|
||||||
import net.sergeych.lyngweb.EditorWithOverlay
|
import net.sergeych.lyngweb.EditorWithOverlay
|
||||||
import org.jetbrains.compose.web.dom.*
|
import org.jetbrains.compose.web.dom.*
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TryLyngPage() {
|
fun TryLyngPage(route: String) {
|
||||||
val scope = rememberCoroutineScope()
|
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(
|
mutableStateOf(
|
||||||
"""
|
initialCode ?: """
|
||||||
// Welcome to Lyng! Edit and run.
|
// Welcome to Lyng! Edit and run.
|
||||||
// Try changing the data and press Ctrl+Enter or click 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]
|
val data = 1..5 // or [1, 2, 3, 4, 5]
|
||||||
data.filter { it % 2 == 0 }.map { it * it }
|
data.filter { it % 2 == 0 }.map { it * it }
|
||||||
@@ -54,16 +64,16 @@ fun TryLyngPage() {
|
|||||||
val printed = StringBuilder()
|
val printed = StringBuilder()
|
||||||
try {
|
try {
|
||||||
// Create a fresh module scope each run so imports and vars are clean
|
// 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
|
// Capture printed output from Lyng `print`/`println` into the UI result window
|
||||||
s.addVoidFn("print") {
|
s.addVoidFn("print") {
|
||||||
for ((i, a) in args.withIndex()) {
|
for ((i, a) in this.args.withIndex()) {
|
||||||
if (i > 0) printed.append(' ')
|
if (i > 0) printed.append(' ')
|
||||||
printed.append(a.toString(this).value)
|
printed.append(a.toString(this).value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.addVoidFn("println") {
|
s.addVoidFn("println") {
|
||||||
for ((i, a) in args.withIndex()) {
|
for ((i, a) in this.args.withIndex()) {
|
||||||
if (i > 0) printed.append(' ')
|
if (i > 0) printed.append(' ')
|
||||||
printed.append(a.toString(this).value)
|
printed.append(a.toString(this).value)
|
||||||
}
|
}
|
||||||
@@ -118,9 +128,8 @@ fun TryLyngPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun resetCode() {
|
fun resetCode() {
|
||||||
code = """
|
code = initialCode ?: """
|
||||||
// Welcome to Lyng! Edit and run.
|
// Welcome to Lyng! Edit and run.
|
||||||
import lyng.stdlib
|
|
||||||
[1,2,3].map { it * 10 }
|
[1,2,3].map { it * 10 }
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
output = null
|
output = null
|
||||||
|
|||||||
Reference in new issue
Block a user