Compare commits
No commits in common. "master" and "bytecode-spec" have entirely different histories.
master
...
bytecode-s
@ -13,7 +13,6 @@
|
||||
- Object members are always allowed even on unknown types; non-Object members require explicit casts. Remove `inspect` from Object and use `toInspectString()` instead.
|
||||
- Type expression checks: `x is T` is value instance check; `T1 is T2` is type-subset; `A in T` means `A` is subset of `T`; `==` is structural type equality.
|
||||
- Type aliases: `type Name = TypeExpr` (generic allowed) expand to their underlying type expressions; no nominal distinctness.
|
||||
- Bounds and variance: `T: A & B` / `T: A | B` for bounds; declaration-site variance with `out` / `in`.
|
||||
- Do not reintroduce bytecode fallback opcodes (e.g., `GET_NAME`, `EVAL_*`, `CALL_FALLBACK`) or runtime name-resolution fallbacks; all symbol resolution must stay compile-time only.
|
||||
|
||||
## Bytecode frame-first migration plan
|
||||
|
||||
@ -4,7 +4,6 @@ High-density specification for LLMs. Reference this for all Lyng code generation
|
||||
|
||||
## 1. Core Philosophy & Syntax
|
||||
- **Everything is an Expression**: Blocks, `if`, `when`, `for`, `while`, `do-while` return their last expression (or `void`).
|
||||
- **Static Types + Inference**: Every declaration has a compile-time type (explicit or inferred). Types are Kotlin‑style: non‑null by default, nullable with `?`.
|
||||
- **Loops with `else`**: `for`, `while`, and `do-while` support an optional `else` block.
|
||||
- `else` executes **only if** the loop finishes normally (without a `break`).
|
||||
- `break <value>` exits the loop and sets its return value.
|
||||
@ -14,7 +13,6 @@ High-density specification for LLMs. Reference this for all Lyng code generation
|
||||
3. Result of the last iteration (if loop finished normally 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 { ... }`.
|
||||
- **Functions**: Use `fun` or the short form `fn`. Function declarations are expressions returning a callable.
|
||||
- **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).
|
||||
@ -46,21 +44,9 @@ High-density specification for LLMs. Reference this for all Lyng code generation
|
||||
- **Root Type**: Everything is an `Object` (root of the hierarchy).
|
||||
- **Nullability**: Non-null by default (`T`), nullable with `T?`, `!!` asserts non-null.
|
||||
- **Untyped params**: `fun foo(x)` -> `x: Object`, `fun foo(x?)` -> `x: Object?`.
|
||||
- **Untyped vars**: `var x` is `Unset` until first assignment locks the type (including nullability).
|
||||
- `val x = null` -> type `Null`; `var x = null` -> type `Object?`.
|
||||
- **Inference**:
|
||||
- List literals infer union element types; empty list defaults to `List<Object>` unless constrained.
|
||||
- Map literals infer key/value types; empty map defaults to `Map<Object, Object>` unless constrained.
|
||||
- Mixed numeric ops promote `Int` + `Real` to `Real`.
|
||||
- **Type aliases**: `type Name = TypeExpr` (generic allowed). Aliases expand to their underlying type expressions (no nominal distinctness).
|
||||
- **Generics**: Bounds with `T: A & B` or `T: A | B`; variance uses `out`/`in` (declaration‑site only).
|
||||
- **Casts**: `as` is a runtime-checked cast; `as?` is safe-cast returning `null`. If the value is nullable, `as T` implies `!!`.
|
||||
|
||||
## 2.2 Type Expressions and Checks
|
||||
- **Value checks**: `x is T` (runtime instance check).
|
||||
- **Type checks**: `T1 is T2` and `A in T` are subset checks between type expressions (compile-time where possible).
|
||||
- **Type equality**: `T1 == T2` is structural (unions/intersections are order‑insensitive).
|
||||
- **Compile-time enforcement**: Bounds are checked at call sites; runtime checks only appear when the compile‑time type is too general.
|
||||
- **Untyped vars**: `var x` is `Unset` until first assignment locks the type.
|
||||
- **Inference**: List/map literals infer union element types; empty list is `List<Object>`, empty map is `{:}`.
|
||||
- **Generics**: Bounds with `T: A & B` or `T: A | B`; variance uses `out`/`in`.
|
||||
|
||||
## 3. Delegation (`by`)
|
||||
Unified model for `val`, `var`, and `fun`.
|
||||
|
||||
@ -32,9 +32,9 @@ class A {
|
||||
enum E* { One, Two }
|
||||
}
|
||||
val ab = A.B()
|
||||
assertEquals(null, ab.x)
|
||||
assertEquals("bar", A.Inner.foo)
|
||||
assertEquals(A.E.One, A.One)
|
||||
assertEquals(ab.x, null)
|
||||
assertEquals(A.Inner.foo, "bar")
|
||||
assertEquals(A.One, A.E.One)
|
||||
```
|
||||
|
||||
- extremely simple Kotlin integration on any platform (JVM, JS, WasmJS, Lunux, MacOS, iOS, Windows)
|
||||
|
||||
@ -35,27 +35,3 @@ tasks.register<Exec>("generateDocs") {
|
||||
description = "Generates a single-file documentation HTML using bin/generate_docs.sh"
|
||||
commandLine("./bin/generate_docs.sh")
|
||||
}
|
||||
|
||||
// Sample generator task for .lyng.d definition files (not wired into build).
|
||||
// Usage: ./gradlew generateLyngDefsSample
|
||||
tasks.register("generateLyngDefsSample") {
|
||||
group = "lyng"
|
||||
description = "Generate a sample .lyng.d file under build/generated/lyng/defs"
|
||||
outputs.dir(layout.buildDirectory.dir("generated/lyng/defs"))
|
||||
doLast {
|
||||
val outDir = layout.buildDirectory.dir("generated/lyng/defs").get().asFile
|
||||
outDir.mkdirs()
|
||||
val outFile = outDir.resolve("sample.lyng.d")
|
||||
outFile.writeText(
|
||||
"""
|
||||
/** Generated API */
|
||||
extern fun ping(): Int
|
||||
|
||||
/** Generated class */
|
||||
class Generated(val name: String) {
|
||||
fun greet(): String = "hi " + name
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,8 +108,8 @@ You can also use flow variations that return a cold `Flow` instead of a `List`,
|
||||
Find the minimum or maximum value of a function applied to each element:
|
||||
|
||||
val source = ["abc", "de", "fghi"]
|
||||
assertEquals(2, source.minOf { (it as String).length })
|
||||
assertEquals(4, source.maxOf { (it as String).length })
|
||||
assertEquals(2, source.minOf { it.length })
|
||||
assertEquals(4, source.maxOf { it.length })
|
||||
>>> void
|
||||
|
||||
## flatten and flatMap
|
||||
@ -218,4 +218,4 @@ For high-performance Kotlin-side interop and custom iterable implementation deta
|
||||
|
||||
[Set]: Set.md
|
||||
|
||||
[RingBuffer]: RingBuffer.md
|
||||
[RingBuffer]: RingBuffer.md
|
||||
@ -94,8 +94,7 @@ Or iterate its key-value pairs that are instances of [MapEntry] class:
|
||||
|
||||
val map = Map( ["foo", 1], ["bar", "buzz"], [42, "answer"] )
|
||||
for( entry in map ) {
|
||||
val e: MapEntry = entry as MapEntry
|
||||
println("map[%s] = %s"(e.key, e.value))
|
||||
println("map[%s] = %s"(entry.key, entry.value))
|
||||
}
|
||||
void
|
||||
>>> map[foo] = 1
|
||||
@ -176,4 +175,4 @@ Notes:
|
||||
- Spreads inside map literals and `+`/`+=` merges allow any objects as keys.
|
||||
- When you need computed or non-string keys, use the constructor form `Map(...)`, map literals with computed keys (if supported), or build entries with `=>` and then merge.
|
||||
|
||||
[Collection](Collection.md)
|
||||
[Collection](Collection.md)
|
||||
53
docs/OOP.md
53
docs/OOP.md
@ -9,7 +9,7 @@ Lyng supports first class OOP constructs, based on classes with multiple inherit
|
||||
The class clause looks like
|
||||
|
||||
class Point(x,y)
|
||||
assertEquals("Point", Point.className)
|
||||
assert( Point is Class )
|
||||
>>> void
|
||||
|
||||
It creates new `Class` with two fields. Here is the more practical sample:
|
||||
@ -376,10 +376,11 @@ Functions defined inside a class body are methods, and unless declared
|
||||
`private` are available to be called from outside the class:
|
||||
|
||||
class Point(x,y) {
|
||||
// private method:
|
||||
private fun d2() { x*x + y*y }
|
||||
// public method declaration:
|
||||
fun length() { sqrt(d2()) }
|
||||
|
||||
// private method:
|
||||
private fun d2() {x*x + y*y}
|
||||
}
|
||||
val p = Point(3,4)
|
||||
// private called from inside public: OK
|
||||
@ -978,7 +979,7 @@ You can mark a field or a method as static. This is borrowed from Java as more p
|
||||
|
||||
static fun exclamation() {
|
||||
// here foo is a regular var:
|
||||
Value.foo.x + "!"
|
||||
foo.x + "!"
|
||||
}
|
||||
}
|
||||
assertEquals( Value.foo.x, "foo" )
|
||||
@ -989,16 +990,24 @@ You can mark a field or a method as static. This is borrowed from Java as more p
|
||||
assertEquals( "bar!", Value.exclamation() )
|
||||
>>> void
|
||||
|
||||
Static fields can be accessed from static methods via the class qualifier:
|
||||
As usual, private statics are not accessible from the outside:
|
||||
|
||||
class Test {
|
||||
static var data = "foo"
|
||||
static fun getData() { Test.data }
|
||||
// private, inacessible from outside protected data:
|
||||
private static var data = null
|
||||
|
||||
// the interface to access and change it:
|
||||
static fun getData() { data }
|
||||
static fun setData(value) { data = value }
|
||||
}
|
||||
|
||||
assertEquals( "foo", Test.getData() )
|
||||
Test.data = "bar"
|
||||
assertEquals("bar", Test.getData() )
|
||||
// no direct access:
|
||||
assertThrows { Test.data }
|
||||
|
||||
// accessible with the interface:
|
||||
assertEquals( null, Test.getData() )
|
||||
Test.setData("fubar")
|
||||
assertEquals("fubar", Test.getData() )
|
||||
>>> void
|
||||
|
||||
# Extending classes
|
||||
@ -1007,13 +1016,25 @@ It sometimes happen that the class is missing some particular functionality that
|
||||
|
||||
## Extension methods
|
||||
|
||||
For example, we want to create an extension method that would test if a value can be interpreted as an integer:
|
||||
For example, we want to create an extension method that would test if some object of unknown type contains something that can be interpreted as an integer. In this case we _extend_ class `Object`, as it is the parent class for any instance of any type:
|
||||
|
||||
fun Int.isInteger() { true }
|
||||
fun Real.isInteger() { this.toInt() == this }
|
||||
fun String.isInteger() { (this.toReal() as Real).isInteger() }
|
||||
fun Object.isInteger() {
|
||||
when(this) {
|
||||
// already Int?
|
||||
is Int -> true
|
||||
|
||||
// Let's test:
|
||||
// real, but with no declimal part?
|
||||
is Real -> toInt() == this
|
||||
|
||||
// string with int or real reuusig code above
|
||||
is String -> toReal().isInteger()
|
||||
|
||||
// otherwise, no:
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
// Let's test:
|
||||
assert( 12.isInteger() == true )
|
||||
assert( 12.1.isInteger() == false )
|
||||
assert( "5".isInteger() )
|
||||
@ -1115,7 +1136,7 @@ The same we can provide writable dynamic fields (var-type), adding set method:
|
||||
// mutable field
|
||||
"bar" -> storedValueForBar
|
||||
|
||||
else -> throw SymbolNotFound()
|
||||
else -> throw SymbolNotFoundException()
|
||||
}
|
||||
}
|
||||
set { name, value ->
|
||||
|
||||
@ -24,14 +24,13 @@ counterpart, _not match_ operator `!~`:
|
||||
|
||||
When you need to find groups, and more detailed match information, use `Regex.find`:
|
||||
|
||||
val result: RegexMatch? = Regex("abc(\d)(\d)(\d)").find( "bad456 good abc123")
|
||||
val result = Regex("abc(\d)(\d)(\d)").find( "bad456 good abc123")
|
||||
assert( result != null )
|
||||
val match: RegexMatch = result as RegexMatch
|
||||
assertEquals( 12 ..< 17, match.range )
|
||||
assertEquals( "abc123", match[0] )
|
||||
assertEquals( "1", match[1] )
|
||||
assertEquals( "2", match[2] )
|
||||
assertEquals( "3", match[3] )
|
||||
assertEquals( 12 .. 17, result.range )
|
||||
assertEquals( "abc123", result[0] )
|
||||
assertEquals( "1", result[1] )
|
||||
assertEquals( "2", result[2] )
|
||||
assertEquals( "3", result[3] )
|
||||
>>> void
|
||||
|
||||
Note that the object `RegexMatch`, returned by [Regex.find], behaves much like in many other languages: it provides the
|
||||
@ -40,12 +39,11 @@ index range and groups matches as indexes.
|
||||
Match operator actually also provides `RegexMatch` in `$~` reserved variable (borrowed from Ruby too):
|
||||
|
||||
assert( "bad456 good abc123" =~ "abc(\d)(\d)(\d)".re )
|
||||
val match2: RegexMatch = $~ as RegexMatch
|
||||
assertEquals( 12 ..< 17, match2.range )
|
||||
assertEquals( "abc123", match2[0] )
|
||||
assertEquals( "1", match2[1] )
|
||||
assertEquals( "2", match2[2] )
|
||||
assertEquals( "3", match2[3] )
|
||||
assertEquals( 12 .. 17, $~.range )
|
||||
assertEquals( "abc123", $~[0] )
|
||||
assertEquals( "1", $~[1] )
|
||||
assertEquals( "2", $~[2] )
|
||||
assertEquals( "3", $~[3] )
|
||||
>>> void
|
||||
|
||||
This is often more readable than calling `find`.
|
||||
@ -61,7 +59,7 @@ string can be either left or right operator, but not both:
|
||||
|
||||
Also, string indexing is Regex-aware, and works like `Regex.find` (_not findall!_):
|
||||
|
||||
assert( "cd" == ("abcdef"[ "c.".re ] as RegexMatch).value )
|
||||
assert( "cd" == "abcdef"[ "c.".re ].value )
|
||||
>>> void
|
||||
|
||||
|
||||
@ -90,3 +88,4 @@ Also, string indexing is Regex-aware, and works like `Regex.find` (_not findall!
|
||||
[List]: List.md
|
||||
|
||||
[Range]: Range.md
|
||||
|
||||
|
||||
@ -26,8 +26,8 @@ no indexing. Use [set.toList] as needed.
|
||||
|
||||
// intersection
|
||||
assertEquals( Set(1,4), Set(3, 1, 4).intersect(Set(2, 4, 1)) )
|
||||
// or simple (intersection)
|
||||
assertEquals( Set(1,4), Set(3, 1, 4).intersect(Set(2, 4, 1)) )
|
||||
// or simple
|
||||
assertEquals( Set(1,4), Set(3, 1, 4) * Set(2, 4, 1) )
|
||||
|
||||
// To find collection elements not present in another collection, use the
|
||||
// subtract() or `-`:
|
||||
@ -91,4 +91,4 @@ Sets are only equal when contains exactly same elements, order, as was said, is
|
||||
Also, it inherits methods from [Iterable].
|
||||
|
||||
|
||||
[Range]: Range.md
|
||||
[Range]: Range.md
|
||||
@ -154,10 +154,9 @@ Function annotation can have more args specified at call time. There arguments m
|
||||
@Registered("bar")
|
||||
fun foo2() { "called foo2" }
|
||||
|
||||
val fooFn: Callable = registered["foo"] as Callable
|
||||
val barFn: Callable = registered["bar"] as Callable
|
||||
assertEquals(fooFn(), "called foo")
|
||||
assertEquals(barFn(), "called foo2")
|
||||
assertEquals(registered["foo"](), "called foo")
|
||||
assertEquals(registered["bar"](), "called foo2")
|
||||
>>> void
|
||||
|
||||
[parallelism]: parallelism.md
|
||||
|
||||
|
||||
@ -34,18 +34,13 @@ Valid examples:
|
||||
|
||||
Ellipsis are used to declare variadic arguments. It basically means "all the arguments available here". It means, ellipsis argument could be in any part of the list, being, end or middle, but there could be only one ellipsis argument and it must not have default value, its default value is always `[]`, en empty list.
|
||||
|
||||
Ellipsis can also appear in **function types** to denote a variadic position:
|
||||
|
||||
var f: (Int, Object..., String)->Real
|
||||
var anyArgs: (...)->Int // shorthand for (Object...)->Int
|
||||
|
||||
Ellipsis argument receives what is left from arguments after processing regular one that could be before or after.
|
||||
|
||||
Ellipsis could be a first argument:
|
||||
|
||||
fun testCountArgs(data...,size) {
|
||||
assert(size is Int)
|
||||
assertEquals(size, (data as List).size)
|
||||
assertEquals(size, data.size)
|
||||
}
|
||||
testCountArgs( 1, 2, "three", 3)
|
||||
>>> void
|
||||
@ -54,7 +49,7 @@ Ellipsis could also be a last one:
|
||||
|
||||
fun testCountArgs(size, data...) {
|
||||
assert(size is Int)
|
||||
assertEquals(size, (data as List).size)
|
||||
assertEquals(size, data.size)
|
||||
}
|
||||
testCountArgs( 3, 10, 2, "three")
|
||||
>>> void
|
||||
@ -63,7 +58,7 @@ Or in the middle:
|
||||
|
||||
fun testCountArgs(size, data..., textToReturn) {
|
||||
assert(size is Int)
|
||||
assertEquals(size, (data as List).size)
|
||||
assertEquals(size, data.size)
|
||||
textToReturn
|
||||
}
|
||||
testCountArgs( 3, 10, 2, "three", "All OK")
|
||||
|
||||
@ -114,17 +114,6 @@ scope.eval("val y = inc(41); log('Answer:', y)")
|
||||
|
||||
You can register multiple names (aliases) at once: `addFn<ObjInt>("inc", "increment") { ... }`.
|
||||
|
||||
Scope-backed Kotlin lambdas receive a `ScopeFacade` (not a full `Scope`). For migration and convenience, these utilities are available on the facade:
|
||||
|
||||
- Access: `args`, `pos`, `thisObj`, `get(name)`
|
||||
- Invocation: `call(...)`, `resolve(...)`, `assign(...)`, `toStringOf(...)`, `inspect(...)`, `trace(...)`
|
||||
- Args helpers: `requiredArg<T>()`, `requireOnlyArg<T>()`, `requireExactCount(...)`, `requireNoArgs()`, `thisAs<T>()`
|
||||
- Errors: `raiseError(...)`, `raiseClassCastError(...)`, `raiseIllegalArgument(...)`, `raiseIllegalState(...)`, `raiseNoSuchElement(...)`,
|
||||
`raiseSymbolNotFound(...)`, `raiseNotImplemented(...)`, `raiseNPE()`, `raiseIndexOutOfBounds(...)`, `raiseIllegalAssignment(...)`,
|
||||
`raiseUnset(...)`, `raiseNotFound(...)`, `raiseAssertionFailed(...)`, `raiseIllegalOperation(...)`, `raiseIterationFinished()`
|
||||
|
||||
If you truly need the full `Scope` (e.g., for low-level interop), use `requireScope()` explicitly.
|
||||
|
||||
### 5) Add Kotlin‑backed fields
|
||||
|
||||
If you need a simple field (with a value) instead of a computed property, use `createField`. This adds a field to the class that will be present in all its instances.
|
||||
@ -236,45 +225,6 @@ Notes:
|
||||
- Use [LyngClassBridge] to bind by name/module, or by an already resolved `ObjClass`.
|
||||
- Use `ObjInstance.data` / `ObjClass.classData` to attach Kotlin‑side state when needed.
|
||||
|
||||
### 6.5a) Bind Kotlin implementations to declared Lyng objects
|
||||
|
||||
For `extern object` declarations, bind implementations to the singleton instance using `ModuleScope.bindObject`.
|
||||
This mirrors class binding but targets an already created object instance.
|
||||
|
||||
```lyng
|
||||
// Lyng side (in a module)
|
||||
extern object HostObject {
|
||||
extern fun add(a: Int, b: Int): Int
|
||||
extern val status: String
|
||||
extern var count: Int
|
||||
}
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Kotlin side (binding)
|
||||
val moduleScope = importManager.createModuleScope(Pos.builtIn, "bridge.obj")
|
||||
moduleScope.bindObject("HostObject") {
|
||||
classData = "OK"
|
||||
init { _ -> data = 0L }
|
||||
addFun("add") { _, _, args ->
|
||||
val a = args.requiredArg<ObjInt>(0).value
|
||||
val b = args.requiredArg<ObjInt>(1).value
|
||||
ObjInt.of(a + b)
|
||||
}
|
||||
addVal("status") { _, _ -> ObjString(classData as String) }
|
||||
addVar(
|
||||
"count",
|
||||
get = { _, inst -> ObjInt.of((inst as ObjInstance).data as Long) },
|
||||
set = { _, inst, value -> (inst as ObjInstance).data = (value as ObjInt).value }
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Members must be marked `extern` so the compiler emits ABI slots for Kotlin bindings.
|
||||
- You can also bind by name/module via `LyngObjectBridge.bind(...)`.
|
||||
|
||||
### 6.6) Preferred: Kotlin reflection bridge for call‑by‑name
|
||||
|
||||
For Kotlin code that needs dynamic access to Lyng variables, functions, or members, use the bridge resolver.
|
||||
|
||||
@ -9,12 +9,9 @@ should be compatible with other IDEA flavors, notably [OpenIDE](https://openide.
|
||||
- reformat code (indents, spaces)
|
||||
- reformat on paste
|
||||
- smart enter key
|
||||
- `.lyng.d` definition files (merged into analysis for completion, navigation, Quick Docs, and error checking)
|
||||
|
||||
Features are configurable via the plugin settings page, in system settings.
|
||||
|
||||
See `docs/lyng_d_files.md` for `.lyng.d` syntax and examples.
|
||||
|
||||
> Recommended for IntelliJ-based IDEs: While IntelliJ can import TextMate bundles
|
||||
> (Settings/Preferences → Editor → TextMate Bundles), the native Lyng plugin provides
|
||||
> better support (formatting, smart enter, background analysis, etc.). Prefer installing
|
||||
@ -29,4 +26,4 @@ See `docs/lyng_d_files.md` for `.lyng.d` syntax and examples.
|
||||
|
||||
### [Download plugin v0.0.2-SNAPSHOT](https://lynglang.com/distributables/lyng-idea-0.0.2-SNAPSHOT.zip)
|
||||
|
||||
Your ideas and bugreports are welcome on the [project gitea page](https://gitea.sergeych.net/SergeychWorks/lyng/issues)
|
||||
Your ideas and bugreports are welcome on the [project gitea page](https://gitea.sergeych.net/SergeychWorks/lyng/issues)
|
||||
@ -1,116 +0,0 @@
|
||||
# `.lyng.d` Definition Files
|
||||
|
||||
`.lyng.d` files declare Lyng symbols for tooling without shipping runtime implementations. The IntelliJ IDEA plugin merges
|
||||
all `*.lyng.d` files from the current directory and its parent directories into the active file’s analysis, enabling:
|
||||
|
||||
- completion
|
||||
- navigation
|
||||
- error checking for declared symbols
|
||||
- Quick Docs for declarations defined in `.lyng.d`
|
||||
|
||||
Place `*.lyng.d` files next to the code they describe (or in a parent folder). The plugin will pick them up automatically.
|
||||
|
||||
## Writing `.lyng.d` Files
|
||||
|
||||
You can declare any language-level symbol in a `.lyng.d` file. Use doc comments before declarations to make Quick Docs
|
||||
work in the IDE. The doc parser accepts standard comments (`/** ... */` or `// ...`) and supports tags like `@param`.
|
||||
|
||||
### Full Example
|
||||
|
||||
```lyng
|
||||
/** Library entry point */
|
||||
extern fun connect(url: String, timeoutMs: Int = 5000): Client
|
||||
|
||||
/** Type alias with generics */
|
||||
type NameMap = Map<String, String>
|
||||
|
||||
/** Multiple inheritance via interfaces */
|
||||
interface A { abstract fun a(): Int }
|
||||
interface B { abstract fun b(): Int }
|
||||
|
||||
/** A concrete class implementing both */
|
||||
class Multi(name: String) : A, B {
|
||||
/** Public field */
|
||||
val id: Int = 0
|
||||
|
||||
/** Mutable property with accessors */
|
||||
var size: Int
|
||||
get() = 0
|
||||
set(v) { }
|
||||
|
||||
/** Instance method */
|
||||
fun a(): Int = 1
|
||||
fun b(): Int = 2
|
||||
}
|
||||
|
||||
/** Nullable and dynamic types */
|
||||
extern val dynValue: dynamic
|
||||
extern var dynVar: dynamic?
|
||||
|
||||
/** Delegated property */
|
||||
class LazyBox(val create) {
|
||||
fun getValue(thisRef, name) = create()
|
||||
}
|
||||
|
||||
val cached by LazyBox { 42 }
|
||||
|
||||
/** Delegated function */
|
||||
object RpcDelegate {
|
||||
fun invoke(thisRef, name, args...) = Unset
|
||||
}
|
||||
|
||||
fun remoteCall by RpcDelegate
|
||||
|
||||
/** Singleton object */
|
||||
object Settings {
|
||||
val version: String = "1.0"
|
||||
}
|
||||
|
||||
/** Class with documented members */
|
||||
class Client {
|
||||
/** Returns a greeting. */
|
||||
fun greet(name: String): String = "hi " + name
|
||||
}
|
||||
```
|
||||
|
||||
See a runnable sample file in `docs/samples/definitions.lyng.d`.
|
||||
|
||||
Notes:
|
||||
- Use real bodies if the declaration is not `extern` or `abstract`.
|
||||
- If you need purely declarative stubs, prefer `extern` members (see `embedding.md`).
|
||||
|
||||
## Doc Comment Format
|
||||
|
||||
Doc comments are picked up when they immediately precede a declaration.
|
||||
|
||||
```lyng
|
||||
/**
|
||||
* A sample function.
|
||||
* @param name user name
|
||||
* @return greeting string
|
||||
*/
|
||||
fun greet(name: String): String = "hi " + name
|
||||
```
|
||||
|
||||
## Generating `.lyng.d` Files
|
||||
|
||||
You can generate `.lyng.d` as part of your build. A common approach is to write a Gradle task that emits a file from a
|
||||
template or a Kotlin data model.
|
||||
|
||||
Example (pseudo-code):
|
||||
|
||||
```kotlin
|
||||
tasks.register("generateLyngDefs") {
|
||||
doLast {
|
||||
val out = file("src/main/lyng/api.lyng.d")
|
||||
out.writeText(
|
||||
"""
|
||||
/** Generated API */
|
||||
fun ping(): Int
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Place the generated file in your source tree, and the IDE will load it automatically.
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
Before kotlin 2.0, there was an excellent library, kotlinx.datetime, which was widely used everywhere, also in Lyng and its dependencies.
|
||||
|
||||
When Kotlin 2.0 was released, or soon after, JetBrains made a perplexing decision to remove `Instant` and `Clock` from kotlinx.datetime and replace it with _yet experimental_ analogs in `kotlin.time`.
|
||||
When kotlin 2.0 was released, or soon after, JetBrains made an exptic decision to remove `Instant` and `Clock` from kotlinx.datetime and replace it with _yet experimental_ analogs in `kotlin.time`.
|
||||
|
||||
The problem is, these were not quite the same (these weren't `@Serializable`!), so people didn't migrate with ease. Okay, then JetBrains decided to not only deprecate it but also make them unusable on Apple targets. It sort of split auditories of many published libraries to those who hate JetBrains and Apple and continue to use 1.9-2.0 compatible versions that no longer work with Kotlin 2.2 on Apple targets (but work pretty well with earlier Kotlin or on other platforms).
|
||||
|
||||
@ -12,14 +12,14 @@ Later JetBrains added serializers for their new `Instant` and `Clock` types, but
|
||||
|
||||
## Solution
|
||||
|
||||
We hereby publish a new version of Lyng, 1.0.8-SNAPSHOT, which uses `kotlin.time.Instant` and `kotlin.time.Clock` instead of `kotlinx.datetime.Instant` and `kotlinx.datetime.Clock`. It is in other aspects compatible also with Lynon encoded binaries. You might need to migrate your code to use `kotlin.time` types. (LocalDateTime/TimeZone still come from `kotlinx.datetime`.)
|
||||
We hereby publish a new version of Lyng, 1.0.8-SNAPSHOT, which uses `ktlin.time.Instant` and `kotlin.time.Clock` instead of `kotlinx.datetime.Instant` and `kotlinx.datetime.Clock; it is in other aspects compatible also with Lynon encoded binaries. Still you might need to migrate your code to use `kotlinx.datetime` types.
|
||||
|
||||
So, if you are getting errors with new version, please do:
|
||||
So, if you are getting errors with new version, plase do:
|
||||
|
||||
- upgrade to Kotlin 2.2
|
||||
- upgrade to Lyng 1.0.8-SNAPSHOT
|
||||
- replace in your code imports (or other uses) of `kotlinx.datetime.Clock` to `kotlin.time.Clock` and `kotlinx.datetime.Instant` to `kotlin.time.Instant`.
|
||||
- replace in your code imports (or other uses) of`kotlinx.datetime.Clock` to `kotlin.time.Clock` and `kotlinx.datetime.Instant` to `kotlin.time.Instant`.
|
||||
|
||||
This should solve the problem and hopefully we'll see no more such "brilliant" ideas from IDEA ideologspersons.
|
||||
This should solve the problem and hopefully we'll see no more suh a brillant ideas from IDEA ideologspersons.
|
||||
|
||||
Sorry for inconvenience and send a ray of hate to JetBrains ;)
|
||||
Sorry for inconvenicence and send a ray of hate to JetBrains ;)
|
||||
@ -49,7 +49,7 @@ Suppose we have a resource, that could be used concurrently, a counter in our ca
|
||||
delay(100)
|
||||
counter = c + 1
|
||||
}
|
||||
}.forEach { (it as Deferred).await() }
|
||||
}.forEach { it.await() }
|
||||
assert(counter < 50) { "counter is "+counter }
|
||||
>>> void
|
||||
|
||||
@ -64,12 +64,13 @@ Using [Mutex] makes it all working:
|
||||
launch {
|
||||
// slow increment:
|
||||
mutex.withLock {
|
||||
val c = counter ?: 0
|
||||
val c = counter
|
||||
delay(10)
|
||||
counter = c + 1
|
||||
}
|
||||
}
|
||||
}.forEach { (it as Deferred).await() }
|
||||
assert(counter in 1..4)
|
||||
}.forEach { it.await() }
|
||||
assertEquals(4, counter)
|
||||
>>> void
|
||||
|
||||
now everything works as expected: `mutex.withLock` makes them all be executed in sequence, not in parallel.
|
||||
@ -223,14 +224,17 @@ Future work: introduce thread‑safe pooling (e.g., per‑thread pools or confin
|
||||
|
||||
### Closures inside coroutine helpers (launch/flow)
|
||||
|
||||
Closures executed by `launch { ... }` and `flow { ... }` use **compile‑time resolution** just like any other Lyng code:
|
||||
Closures executed by `launch { ... }` and `flow { ... }` resolve names using the `ClosureScope` rules:
|
||||
|
||||
- **Captured locals are slots**: outer locals are resolved at compile time and captured as frame‑slot references, so they remain visible across suspension points.
|
||||
- **Members are statically resolved**: member access requires a statically known receiver type or an explicit cast (except `Object` members).
|
||||
- **No runtime fallbacks**: there is no dynamic name lookup or “search parent scopes” at runtime for missing symbols.
|
||||
1. **Current frame locals and arguments**: Variables defined within the current closure execution.
|
||||
2. **Captured lexical ancestry**: Outer local variables captured at the site where the closure was defined (the "lexical environment").
|
||||
3. **Captured receiver members**: If the closure was defined within a class or explicitly bound to an object, it checks members of that object (`this`), following MRO and respecting visibility.
|
||||
4. **Caller environment**: Falls back to the calling context (e.g., the caller's `this` or local variables).
|
||||
5. **Global/Module fallbacks**: Final check for module-level constants and global functions.
|
||||
|
||||
Implications:
|
||||
- Global helpers like `delay(ms)` and `yield()` must be imported/known at compile time.
|
||||
- If you need dynamic access, use explicit helpers (e.g., `dynamic { ... }`) rather than relying on scope resolution.
|
||||
- Outer locals (e.g., `counter`) stay visible across suspension points.
|
||||
- Global helpers like `delay(ms)` and `yield()` are available from inside closures.
|
||||
- If you write your own async helpers, execute user lambdas under `ClosureScope(callScope, capturedCreatorScope)` and avoid manual ancestry walking.
|
||||
|
||||
See also: [Scopes and Closures: compile-time resolution](scopes_and_closures.md)
|
||||
See also: [Scopes and Closures: resolution and safety](scopes_and_closures.md)
|
||||
|
||||
@ -1,65 +0,0 @@
|
||||
/**
|
||||
* Sample .lyng.d file for IDE support.
|
||||
* Demonstrates declarations and doc comments.
|
||||
*/
|
||||
|
||||
/** Simple function with default and named parameters. */
|
||||
extern fun connect(url: String, timeoutMs: Int = 5000): Client
|
||||
|
||||
/** Type alias with generics. */
|
||||
type NameMap = Map<String, String>
|
||||
|
||||
/** Multiple inheritance via interfaces. */
|
||||
interface A { abstract fun a(): Int }
|
||||
interface B { abstract fun b(): Int }
|
||||
|
||||
/** A concrete class implementing both. */
|
||||
class Multi(name: String) : A, B {
|
||||
/** Public field. */
|
||||
val id: Int = 0
|
||||
|
||||
/** Mutable property with accessors. */
|
||||
var size: Int
|
||||
get() = 0
|
||||
set(v) { }
|
||||
|
||||
/** Instance method. */
|
||||
fun a(): Int = 1
|
||||
fun b(): Int = 2
|
||||
}
|
||||
|
||||
/** Nullable and dynamic types. */
|
||||
extern val dynValue: dynamic
|
||||
extern var dynVar: dynamic?
|
||||
|
||||
/** Delegated property provider. */
|
||||
class LazyBox(val create) {
|
||||
fun getValue(thisRef, name) = create()
|
||||
}
|
||||
|
||||
/** Delegated property using provider. */
|
||||
val cached by LazyBox { 42 }
|
||||
|
||||
/** Delegated function. */
|
||||
object RpcDelegate {
|
||||
fun invoke(thisRef, name, args...) = Unset
|
||||
}
|
||||
|
||||
/** Remote function proxy. */
|
||||
fun remoteCall by RpcDelegate
|
||||
|
||||
/** Singleton object. */
|
||||
object Settings {
|
||||
/** Version string. */
|
||||
val version: String = "1.0"
|
||||
}
|
||||
|
||||
/**
|
||||
* Client API entry.
|
||||
* @param name user name
|
||||
* @return greeting string
|
||||
*/
|
||||
class Client {
|
||||
/** Returns a greeting. */
|
||||
fun greet(name: String): String = "hi " + name
|
||||
}
|
||||
@ -1,23 +1,23 @@
|
||||
// Sample: Operator Overloading in Lyng
|
||||
|
||||
class Vector<T>(val x: T, val y: T) {
|
||||
class Vector(val x, val y) {
|
||||
// Overload +
|
||||
fun plus(other: Vector<U>) = Vector(x + other.x, y + other.y)
|
||||
fun plus(other) = Vector(x + other.x, y + other.y)
|
||||
|
||||
// Overload -
|
||||
fun minus(other: Vector<U>) = Vector(x - other.x, y - other.y)
|
||||
fun minus(other) = Vector(x - other.x, y - other.y)
|
||||
|
||||
// Overload unary -
|
||||
fun negate() = Vector(-x, -y)
|
||||
|
||||
// Overload ==
|
||||
fun equals(other) {
|
||||
if (other is Vector<U>) x == other.x && y == other.y
|
||||
if (other is Vector) x == other.x && y == other.y
|
||||
else false
|
||||
}
|
||||
|
||||
// Overload * (scalar multiplication)
|
||||
fun mul(scalar: Int | Real) = Vector(x * scalar, y * scalar)
|
||||
fun mul(scalar) = Vector(x * scalar, y * scalar)
|
||||
|
||||
override fun toString() = "Vector(${x}, ${y})"
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ fun findSumLimit(f) {
|
||||
println("limit reached after "+n+" rounds")
|
||||
break sum
|
||||
}
|
||||
n++
|
||||
}
|
||||
else {
|
||||
println("limit not reached")
|
||||
|
||||
@ -1,18 +1,94 @@
|
||||
# Scopes and Closures: compile-time resolution
|
||||
# Scopes and Closures: resolution and safety
|
||||
|
||||
Attention to AI: name lookup in runtime `Scope` is legacy. The bytecode compiler uses **compile-time name/member resolution only**.
|
||||
Attention to AI: name lookup is ibsolete and must not be used with bytecode compiler
|
||||
|
||||
This page documents the **current** rules: static name resolution, closure captures, and the limited role of runtime `Scope` in Kotlin interop and explicit dynamic helpers.
|
||||
This page documents how name resolution works with `ClosureScope`, how to avoid recursion pitfalls, and how to safely capture and execute callbacks that need access to outer locals.
|
||||
|
||||
## Current rules (bytecode compiler)
|
||||
- **All names resolve at compile time**: locals, parameters, captures, members, imports, and module globals must be known when compiling. Missing symbols are compile-time errors.
|
||||
- **No runtime fallbacks**: there is no dynamic name lookup, no fallback opcodes, and no “search parent scopes” at runtime for missing names.
|
||||
- **Object members on unknown types only**: `toString`, `toInspectString`, `let`, `also`, `apply`, `run` are allowed on unknown types; all other members require a statically known receiver type or an explicit cast.
|
||||
- **Closures capture slots**: lambdas and nested functions capture **frame slots** directly. Captures are resolved at compile time and compiled to slot references.
|
||||
- **Scope is a reflection facade**: `Scope` is used only for Kotlin interop or explicit dynamic helpers. It must **not** be used for general symbol resolution in compiled Lyng code.
|
||||
## Why this matters
|
||||
Name lookup across nested scopes and closures can accidentally form recursive resolution paths or hide expected symbols (outer locals, module/global functions). The rules below ensure predictable resolution and prevent infinite recursion.
|
||||
|
||||
## Explicit dynamic access (opt-in only)
|
||||
Dynamic name access is available only via explicit helpers (e.g., `dynamic { get { name -> ... } }`). It is **not** a fallback for normal member or variable access.
|
||||
## Resolution order in ClosureScope
|
||||
When evaluating an identifier `name` inside a closure, `ClosureScope.get(name)` resolves in this order:
|
||||
|
||||
## Legacy interpreter behavior (reference only)
|
||||
The old runtime `Scope`-based resolution order (locals → captured → `this` → caller → globals) is obsolete for bytecode compilation. Keep it only for legacy interpreter paths and tooling that explicitly opts into it.
|
||||
1. **Current frame locals and arguments**: Variables defined within the current closure execution.
|
||||
2. **Captured lexical ancestry**: Outer local variables captured at the site where the closure was defined (the "lexical environment").
|
||||
3. **Captured receiver members**: If the closure was defined within a class or explicitly bound to an object, it checks members of that object (`this`). This includes both instance fields/methods and class-level static members, following the MRO (C3) and respecting visibility rules (private members are only visible if the closure was defined in their class).
|
||||
4. **Caller environment**: If not found lexically, it falls back to the calling context (e.g., the DSL's `this` or the caller's local variables).
|
||||
5. **Global/Module fallbacks**: Final check for module-level constants and global functions.
|
||||
|
||||
This ensures that closures primarily interact with their defining environment (lexical capture) while still being able to participate in DSL-style calling contexts.
|
||||
|
||||
## Use raw‑chain helpers for ancestry walks
|
||||
When authoring new scope types or advanced lookups, avoid calling virtual `get` while walking parents. Instead, use the non‑dispatch helpers on `Scope`:
|
||||
|
||||
- `chainLookupIgnoreClosure(name)`
|
||||
- Walk raw `parent` chain and check only per‑frame locals/bindings/slots.
|
||||
- Ignores overridden `get` (e.g., in `ClosureScope`). Cycle‑safe.
|
||||
- `chainLookupWithMembers(name)`
|
||||
- Like above, but after locals/bindings it also checks each frame’s `thisObj` members.
|
||||
- Ignores overridden `get`. Cycle‑safe.
|
||||
- `baseGetIgnoreClosure(name)`
|
||||
- For the current frame only: check locals/bindings, then walk raw parents (locals/bindings), then fallback to this frame’s `thisObj` members.
|
||||
|
||||
These helpers avoid ping‑pong recursion and make structural cycles harmless (lookups terminate).
|
||||
|
||||
## Preventing structural cycles
|
||||
- Don’t construct parent chains that can point back to a descendant.
|
||||
- A debug‑time guard throws if assigning a parent would create a cycle; keep it enabled for development builds.
|
||||
- Even with a cycle, chain helpers break out via a small `visited` set keyed by `frameId`.
|
||||
|
||||
## Capturing lexical environments for callbacks
|
||||
For dynamic objects or custom builders, capture the creator’s lexical scope so callbacks can see outer locals/parameters:
|
||||
|
||||
1. Use `snapshotForClosure()` on the caller scope to capture locals/bindings/slots and parent.
|
||||
2. Store this snapshot and run callbacks under `ClosureScope(callScope, captured)`.
|
||||
|
||||
Kotlin sketch:
|
||||
```kotlin
|
||||
val captured = scope.snapshotForClosure()
|
||||
val execScope = ClosureScope(currentCallScope, captured)
|
||||
callback.execute(execScope)
|
||||
```
|
||||
|
||||
This ensures expressions like `contractName` used inside dynamic `get { name -> ... }` resolve to outer variables defined at the creation site.
|
||||
|
||||
## Closures in coroutines (launch/flow)
|
||||
- The closure frame still prioritizes its own locals/args.
|
||||
- Outer locals declared before suspension points remain visible through slot‑aware ancestry lookups.
|
||||
- Global functions like `delay(ms)` and `yield()` are resolved via module/root fallbacks from within closures.
|
||||
|
||||
Tip: If a closure unexpectedly cannot see an outer local, check whether an intermediate runtime helper introduced an extra call frame; the built‑in lookup already traverses caller ancestry, so prefer the standard helpers rather than custom dispatch.
|
||||
|
||||
## Local variable references and missing symbols
|
||||
- Unqualified identifier resolution first prefers locals/bindings/slots before falling back to `this` members.
|
||||
- If neither locals nor members contain the symbol, missing field lookups map to `SymbolNotFound` (compatibility alias for `SymbolNotDefinedException`).
|
||||
|
||||
## Performance notes
|
||||
- The `visited` sets used for cycle detection are tiny and short‑lived; in typical scripts the overhead is negligible.
|
||||
- If profiling shows hotspots, consider limiting ancestry depth in your custom helpers or using small fixed arrays instead of hash sets—only for extremely hot code paths.
|
||||
|
||||
## Practical Example: `cached`
|
||||
|
||||
The `cached` function (defined in `lyng.stdlib`) is a classic example of using closures to maintain state. It wraps a builder into a zero-argument function that computes once and remembers the result:
|
||||
|
||||
```lyng
|
||||
fun cached(builder) {
|
||||
var calculated = false
|
||||
var value = null
|
||||
{ // This lambda captures `calculated`, `value`, and `builder`
|
||||
if( !calculated ) {
|
||||
value = builder()
|
||||
calculated = true
|
||||
}
|
||||
value
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Because Lyng now correctly isolates closures for each evaluation of a lambda literal, using `cached` inside a class instance works as expected: each instance maintains its own private `calculated` and `value` state, even if they share the same property declaration.
|
||||
|
||||
## Dos and Don’ts
|
||||
- Do use `chainLookupIgnoreClosure` / `chainLookupWithMembers` for ancestry traversals.
|
||||
- Do maintain the resolution order above for predictable behavior.
|
||||
- Don’t call virtual `get` while walking parents; it risks recursion across scope types.
|
||||
- Don’t attach instance scopes to transient/pool frames; bind to a stable parent scope instead.
|
||||
|
||||
@ -17,7 +17,7 @@ It is as simple as:
|
||||
assertEquals( text, Lynon.decode(encodedBits) )
|
||||
|
||||
// compression was used automatically
|
||||
assert( text.length > (encodedBits.toBuffer() as Buffer).size )
|
||||
assert( text.length > encodedBits.toBuffer().size )
|
||||
>>> void
|
||||
|
||||
Any class you create is serializable by default; lynon serializes first constructor fields, then any `var` member fields.
|
||||
|
||||
@ -229,8 +229,9 @@ Naturally, assignment returns its value:
|
||||
rvalue means you cant assign the result if the assignment
|
||||
|
||||
var x
|
||||
// compile-time error: can't assign to rvalue
|
||||
(x = 11) = 5
|
||||
assertThrows { (x = 11) = 5 }
|
||||
void
|
||||
>>> void
|
||||
|
||||
This also prevents chain assignments so use parentheses:
|
||||
|
||||
@ -248,24 +249,18 @@ When the value is `null`, it might throws `NullReferenceException`, the name is
|
||||
one can check it against null or use _null coalescing_. The null coalescing means, if the operand (left) is null,
|
||||
the operation won't be performed and the result will be null. Here is the difference:
|
||||
|
||||
class Sample {
|
||||
var field = 1
|
||||
fun method() { 2 }
|
||||
var list = [1, 2, 3]
|
||||
}
|
||||
|
||||
val ref: Sample? = null
|
||||
val list: List<Int>? = null
|
||||
// direct access throws NullReferenceException:
|
||||
// ref.field
|
||||
// ref.method()
|
||||
// ref.list[1]
|
||||
// list[1]
|
||||
|
||||
val ref = null
|
||||
assertThrows { ref.field }
|
||||
assertThrows { ref.method() }
|
||||
assertThrows { ref.array[1] }
|
||||
assertThrows { ref[1] }
|
||||
assertThrows { ref() }
|
||||
|
||||
assert( ref?.field == null )
|
||||
assert( ref?.method() == null )
|
||||
assert( ref?.list?[1] == null )
|
||||
assert( list?[1] == null )
|
||||
assert( ref?.array?[1] == null )
|
||||
assert( ref?[1] == null )
|
||||
assert( ref?() == null )
|
||||
>>> void
|
||||
|
||||
Note: `?.` is still a typed operation. The receiver must have a compile-time type that declares the member; if the
|
||||
@ -327,8 +322,8 @@ Much like let, but it does not alter returned value:
|
||||
|
||||
While it is not altering return value, the source object could be changed:
|
||||
also
|
||||
class Point(var x: Int, var y: Int)
|
||||
val p: Point = Point(1,2).also { it.x++ }
|
||||
class Point(x,y)
|
||||
val p = Point(1,2).also { it.x++ }
|
||||
assertEquals(p.x, 2)
|
||||
>>> void
|
||||
|
||||
@ -336,9 +331,9 @@ also
|
||||
|
||||
It works much like `also`, but is executed in the context of the source object:
|
||||
|
||||
class Point(var x: Int, var y: Int)
|
||||
class Point(x,y)
|
||||
// see the difference: apply changes this to newly created Point:
|
||||
val p = Point(1,2).apply { this@Point.x++; this@Point.y++ }
|
||||
val p = Point(1,2).apply { x++; y++ }
|
||||
assertEquals(p, Point(2,3))
|
||||
>>> void
|
||||
|
||||
@ -346,7 +341,7 @@ It works much like `also`, but is executed in the context of the source object:
|
||||
|
||||
Sets `this` to the first argument and executes the block. Returns the value returned by the block:
|
||||
|
||||
class Point(var x: Int, var y: Int)
|
||||
class Point(x,y)
|
||||
val p = Point(1,2)
|
||||
val sum = with(p) { x + y }
|
||||
assertEquals(3, sum)
|
||||
@ -518,13 +513,6 @@ Examples:
|
||||
fun inc(x=0) = x + 1 // (Int)->Int
|
||||
fun maybe(flag) { if(flag) 1 else null } // ()->Int?
|
||||
|
||||
Function types are written as `(T1, T2, ...)->R`. You can include ellipsis in function *types* to
|
||||
express a variadic position:
|
||||
|
||||
var fmt: (String, Object...)->String
|
||||
var f: (Int, Object..., String)->Real
|
||||
var anyArgs: (...)->Int // shorthand for (Object...)->Int
|
||||
|
||||
Untyped locals are allowed, but their type is fixed on the first assignment:
|
||||
|
||||
var x
|
||||
@ -647,9 +635,8 @@ There are default parameters in Lyng:
|
||||
It is possible to define also vararg using ellipsis:
|
||||
|
||||
fun sum(args...) {
|
||||
val list = args as List
|
||||
var result = list[0]
|
||||
for( i in 1 ..< list.size ) result += list[i]
|
||||
var result = args[0]
|
||||
for( i in 1 ..< args.size ) result += args[i]
|
||||
}
|
||||
sum(10,20,30)
|
||||
>>> 60
|
||||
@ -742,11 +729,6 @@ one could be with ellipsis that means "the rest pf arguments as List":
|
||||
assert( { a, b...-> [a,...b] }(100, 1, 2, 3) == [100, 1, 2, 3])
|
||||
void
|
||||
|
||||
Type-annotated lambdas can use variadic *function types* as well:
|
||||
|
||||
val f: (Int, Object..., String)->Real = { a, rest..., b -> 0.0 }
|
||||
val anyArgs: (...)->Int = { -> 0 }
|
||||
|
||||
### Using lambda as the parameter
|
||||
|
||||
See also: [Testing and Assertions](Testing.md)
|
||||
@ -793,7 +775,7 @@ Lists can contain any type of objects, lists too:
|
||||
assert( list is Array ) // general interface
|
||||
assert(list.size == 3)
|
||||
// second element is a list too:
|
||||
assert((list[1] as List).size == 2)
|
||||
assert(list[1].size == 2)
|
||||
>>> void
|
||||
|
||||
Notice usage of indexing. You can use negative indexes to offset from the end of the list; see more in [Lists](List.md).
|
||||
@ -1241,8 +1223,8 @@ ends normally, without breaks. It allows override loop result value, for example
|
||||
to not calculate it in every iteration. For example, consider this naive prime number
|
||||
test function (remember function return it's last expression result):
|
||||
|
||||
fun naive_is_prime(candidate: Int) {
|
||||
val x = candidate
|
||||
fun naive_is_prime(candidate) {
|
||||
val x = if( candidate !is Int) candidate.toInt() else candidate
|
||||
var divisor = 1
|
||||
while( ++divisor < x/2 || divisor == 2 ) {
|
||||
if( x % divisor == 0 ) break false
|
||||
@ -1317,9 +1299,8 @@ For loop are intended to traverse collections, and all other objects that suppor
|
||||
size and index access, like lists:
|
||||
|
||||
var letters = 0
|
||||
val words: List<String> = ["hello", "world"]
|
||||
for( w in words) {
|
||||
letters += (w as String).length
|
||||
for( w in ["hello", "wolrd"]) {
|
||||
letters += w.length
|
||||
}
|
||||
"total letters: "+letters
|
||||
>>> "total letters: 10"
|
||||
@ -1643,13 +1624,13 @@ Concatenation is a `+`: `"hello " + name` works as expected. No confusion. There
|
||||
|
||||
Extraction:
|
||||
|
||||
("abcd42def"[ "\d+".re ] as RegexMatch).value
|
||||
"abcd42def"[ "\d+".re ].value
|
||||
>>> "42"
|
||||
|
||||
Part match:
|
||||
|
||||
assert( "abc foo def" =~ "f[oO]+".re )
|
||||
assert( "foo" == ($~ as RegexMatch).value )
|
||||
assert( "foo" == $~.value )
|
||||
>>> void
|
||||
|
||||
Repeating the fragment:
|
||||
@ -1900,7 +1881,7 @@ You can add new methods and properties to existing classes without modifying the
|
||||
|
||||
### Extension properties
|
||||
|
||||
val Int.isEven get() = this % 2 == 0
|
||||
val Int.isEven = this % 2 == 0
|
||||
4.isEven
|
||||
>>> true
|
||||
|
||||
|
||||
@ -22,7 +22,7 @@ The API is fixed and will be kept with further Lyng core changes. It is now the
|
||||
|
||||
- **Deep inference**: The compiler analyzes types of symbols along the execution path and in many cases eliminates unnecessary casts or type specifications.
|
||||
- **Union and intersection types**: `A & B`, `A | B`.
|
||||
- **Generics**: Generic types are first-class citizens with support for [bounds and variance](generics.md). Type params are erased by default and are reified only when needed (e.g., `T::class`, `T is ...`, `as T`, or in extern-facing APIs), which enables checks like `A in T` when `T` is reified.
|
||||
- **Generics**: Generic types are first-class citizens with support for [bounds and variance](generics.md). No type erasure: in a generic function you can, for example, check `A in T`, where T is the generic type.
|
||||
- **Inner classes and enums**: Full support for nested declarations, including [Enums with lifting](OOP.md#lifted-enum-entries).
|
||||
|
||||
## Other highlights
|
||||
|
||||
@ -35,7 +35,13 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import net.sergeych.lyng.ExecutionError
|
||||
import net.sergeych.lyng.Script
|
||||
import net.sergeych.lyng.Source
|
||||
import net.sergeych.lyng.requireScope
|
||||
import net.sergeych.lyng.idea.LyngIcons
|
||||
import net.sergeych.lyng.obj.ObjVoid
|
||||
import net.sergeych.lyng.obj.getLyngExceptionMessageWithStackTrace
|
||||
|
||||
class RunLyngScriptAction : AnAction(LyngIcons.FILE) {
|
||||
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
@ -53,9 +59,7 @@ class RunLyngScriptAction : AnAction(LyngIcons.FILE) {
|
||||
val isLyng = psiFile?.name?.endsWith(".lyng") == true
|
||||
e.presentation.isEnabledAndVisible = isLyng
|
||||
if (isLyng) {
|
||||
e.presentation.isEnabled = false
|
||||
e.presentation.text = "Run '${psiFile.name}' (disabled)"
|
||||
e.presentation.description = "Running scripts from the IDE is disabled; use the CLI."
|
||||
e.presentation.text = "Run '${psiFile.name}'"
|
||||
} else {
|
||||
e.presentation.text = "Run Lyng Script"
|
||||
}
|
||||
@ -64,6 +68,7 @@ class RunLyngScriptAction : AnAction(LyngIcons.FILE) {
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val project = e.project ?: return
|
||||
val psiFile = getPsiFile(e) ?: return
|
||||
val text = psiFile.text
|
||||
val fileName = psiFile.name
|
||||
|
||||
val (console, toolWindow) = getConsoleAndToolWindow(project)
|
||||
@ -71,9 +76,40 @@ class RunLyngScriptAction : AnAction(LyngIcons.FILE) {
|
||||
|
||||
toolWindow.show {
|
||||
scope.launch {
|
||||
console.print("--- Run is disabled ---\n", ConsoleViewContentType.SYSTEM_OUTPUT)
|
||||
console.print("Lyng now runs in bytecode-only mode; the IDE no longer evaluates scripts.\n", ConsoleViewContentType.NORMAL_OUTPUT)
|
||||
console.print("Use the CLI to run scripts, e.g. `lyng run $fileName`.\n", ConsoleViewContentType.NORMAL_OUTPUT)
|
||||
try {
|
||||
val lyngScope = Script.newScope()
|
||||
lyngScope.addFn("print") {
|
||||
val sb = StringBuilder()
|
||||
for ((i, arg) in args.list.withIndex()) {
|
||||
if (i > 0) sb.append(" ")
|
||||
sb.append(arg.toString(requireScope()).value)
|
||||
}
|
||||
console.print(sb.toString(), ConsoleViewContentType.NORMAL_OUTPUT)
|
||||
ObjVoid
|
||||
}
|
||||
lyngScope.addFn("println") {
|
||||
val sb = StringBuilder()
|
||||
for ((i, arg) in args.list.withIndex()) {
|
||||
if (i > 0) sb.append(" ")
|
||||
sb.append(arg.toString(requireScope()).value)
|
||||
}
|
||||
console.print(sb.toString() + "\n", ConsoleViewContentType.NORMAL_OUTPUT)
|
||||
ObjVoid
|
||||
}
|
||||
|
||||
console.print("--- Running $fileName ---\n", ConsoleViewContentType.SYSTEM_OUTPUT)
|
||||
val result = lyngScope.eval(Source(fileName, text))
|
||||
console.print("\n--- Finished with result: ${result.inspect(lyngScope)} ---\n", ConsoleViewContentType.SYSTEM_OUTPUT)
|
||||
} catch (t: Throwable) {
|
||||
console.print("\n--- Error ---\n", ConsoleViewContentType.ERROR_OUTPUT)
|
||||
if( t is ExecutionError ) {
|
||||
val m = t.errorObject.getLyngExceptionMessageWithStackTrace()
|
||||
console.print(m, ConsoleViewContentType.ERROR_OUTPUT)
|
||||
}
|
||||
else
|
||||
console.print(t.message ?: t.toString(), ConsoleViewContentType.ERROR_OUTPUT)
|
||||
console.print("\n", ConsoleViewContentType.ERROR_OUTPUT)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,7 +24,6 @@ import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.util.TextRange
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiFile
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.highlight.offsetOf
|
||||
import net.sergeych.lyng.idea.LyngLanguage
|
||||
import net.sergeych.lyng.idea.util.LyngAstManager
|
||||
@ -76,51 +75,7 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
|
||||
// Single-source quick doc lookup
|
||||
LyngLanguageTools.docAt(analysis, offset)?.let { info ->
|
||||
val enriched = if (info.doc == null) {
|
||||
findDocInDeclarationFiles(file, info.target.containerName, info.target.name)
|
||||
?.let { info.copy(doc = it) } ?: info
|
||||
} else {
|
||||
info
|
||||
}
|
||||
renderDocFromInfo(enriched)?.let { return it }
|
||||
}
|
||||
|
||||
// Fallback: resolve references against merged MiniAst (including .lyng.d) when binder cannot
|
||||
run {
|
||||
val dotPos = DocLookupUtils.findDotLeft(text, idRange.startOffset)
|
||||
if (dotPos != null) {
|
||||
val receiverClass = DocLookupUtils.guessReceiverClassViaMini(mini, text, dotPos, imported, analysis.binding)
|
||||
?: DocLookupUtils.guessReceiverClass(text, dotPos, imported, mini)
|
||||
if (receiverClass != null) {
|
||||
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, receiverClass, ident, mini)
|
||||
if (resolved != null) {
|
||||
val owner = resolved.first
|
||||
val member = resolved.second
|
||||
val withDoc = if (member.doc == null) {
|
||||
findDocInDeclarationFiles(file, owner, member.name)?.let { doc ->
|
||||
when (member) {
|
||||
is MiniMemberFunDecl -> member.copy(doc = doc)
|
||||
is MiniMemberValDecl -> member.copy(doc = doc)
|
||||
is MiniMemberTypeAliasDecl -> member.copy(doc = doc)
|
||||
else -> member
|
||||
}
|
||||
} ?: member
|
||||
} else {
|
||||
member
|
||||
}
|
||||
return when (withDoc) {
|
||||
is MiniMemberFunDecl -> renderMemberFunDoc(owner, withDoc)
|
||||
is MiniMemberValDecl -> renderMemberValDoc(owner, withDoc)
|
||||
is MiniMemberTypeAliasDecl -> renderMemberTypeAliasDoc(owner, withDoc)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mini.declarations.firstOrNull { it.name == ident }?.let { decl ->
|
||||
return renderDeclDoc(decl, text, mini, imported)
|
||||
}
|
||||
}
|
||||
renderDocFromInfo(info)?.let { return it }
|
||||
}
|
||||
|
||||
// Try resolve to: function param at position, function/class/val declaration at position
|
||||
@ -615,55 +570,6 @@ class LyngDocumentationProvider : AbstractDocumentationProvider() {
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun findDocInDeclarationFiles(file: PsiFile, container: String?, name: String): MiniDoc? {
|
||||
val declFiles = LyngAstManager.getDeclarationFiles(file)
|
||||
if (declFiles.isEmpty()) return null
|
||||
|
||||
fun findInMini(mini: MiniScript): MiniDoc? {
|
||||
if (container == null) {
|
||||
mini.declarations.firstOrNull { it.name == name }?.let { return it.doc }
|
||||
return null
|
||||
}
|
||||
val cls = mini.declarations.filterIsInstance<MiniClassDecl>().firstOrNull { it.name == container } ?: return null
|
||||
cls.members.firstOrNull { it.name == name }?.let { return it.doc }
|
||||
cls.ctorFields.firstOrNull { it.name == name }?.let { return null }
|
||||
cls.classFields.firstOrNull { it.name == name }?.let { return null }
|
||||
return null
|
||||
}
|
||||
|
||||
for (df in declFiles) {
|
||||
val mini = LyngAstManager.getMiniAst(df)
|
||||
?: run {
|
||||
try {
|
||||
val res = runBlocking {
|
||||
LyngLanguageTools.analyze(df.text, df.name)
|
||||
}
|
||||
res.mini
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
if (mini != null) {
|
||||
val doc = findInMini(mini)
|
||||
if (doc != null) return doc
|
||||
}
|
||||
// Text fallback: parse preceding doc comment for the symbol
|
||||
val parsed = parseDocFromText(df.text, name)
|
||||
if (parsed != null) return parsed
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun parseDocFromText(text: String, name: String): MiniDoc? {
|
||||
if (text.isBlank()) return null
|
||||
val pattern = Regex("/\\*\\*([\\s\\S]*?)\\*/\\s*(?:public|private|protected|static|abstract|extern|open|closed|override\\s+)*\\s*(?:fun|val|var|class|interface|enum|type)\\s+$name\\b")
|
||||
val m = pattern.find(text) ?: return null
|
||||
val raw = m.groupValues.getOrNull(1)?.trim() ?: return null
|
||||
if (raw.isBlank()) return null
|
||||
val src = net.sergeych.lyng.Source("<doc>", raw)
|
||||
return MiniDoc.parse(MiniRange(src.startPos, src.startPos), raw.lines())
|
||||
}
|
||||
|
||||
private fun renderParamDoc(fn: MiniFunDecl, p: MiniParam): String {
|
||||
val title = "parameter ${p.name}${typeOf(p.type)} in ${fn.name}${signatureOf(fn)}"
|
||||
val sb = StringBuilder()
|
||||
|
||||
@ -20,18 +20,12 @@ package net.sergeych.lyng.idea.navigation
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.TextRange
|
||||
import com.intellij.psi.*
|
||||
import com.intellij.psi.search.FileTypeIndex
|
||||
import com.intellij.psi.search.FilenameIndex
|
||||
import com.intellij.psi.search.GlobalSearchScope
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.highlight.offsetOf
|
||||
import net.sergeych.lyng.idea.LyngFileType
|
||||
import net.sergeych.lyng.idea.util.LyngAstManager
|
||||
import net.sergeych.lyng.idea.util.TextCtx
|
||||
import net.sergeych.lyng.miniast.*
|
||||
import net.sergeych.lyng.tools.IdeLenientImportProvider
|
||||
import net.sergeych.lyng.tools.LyngAnalysisRequest
|
||||
import net.sergeych.lyng.tools.LyngLanguageTools
|
||||
|
||||
class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiElement>(element, TextRange(0, element.textLength)) {
|
||||
|
||||
@ -64,7 +58,7 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
|
||||
|
||||
// We need to find the actual PSI element for this member
|
||||
val targetFile = findFileForClass(file.project, owner) ?: file
|
||||
val targetMini = loadMini(targetFile)
|
||||
val targetMini = LyngAstManager.getMiniAst(targetFile)
|
||||
if (targetMini != null) {
|
||||
val targetSrc = targetMini.range.start.source
|
||||
val off = targetSrc.offsetOf(member.nameStart)
|
||||
@ -129,37 +123,24 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
|
||||
}
|
||||
|
||||
private fun findFileForClass(project: Project, className: String): PsiFile? {
|
||||
// 1. Try file with matching name first (optimization)
|
||||
val scope = GlobalSearchScope.projectScope(project)
|
||||
val psiManager = PsiManager.getInstance(project)
|
||||
val matchingFiles = FileTypeIndex.getFiles(LyngFileType, scope)
|
||||
.asSequence()
|
||||
.filter { it.name == "$className.lyng" }
|
||||
.mapNotNull { psiManager.findFile(it) }
|
||||
.toList()
|
||||
val matchingDeclFiles = FileTypeIndex.getFiles(LyngFileType, scope)
|
||||
.asSequence()
|
||||
.filter { it.name == "$className.lyng.d" }
|
||||
.mapNotNull { psiManager.findFile(it) }
|
||||
.toList()
|
||||
|
||||
// 1. Try file with matching name first (optimization)
|
||||
val matchingFiles = FilenameIndex.getFilesByName(project, "$className.lyng", GlobalSearchScope.projectScope(project))
|
||||
for (file in matchingFiles) {
|
||||
val mini = loadMini(file) ?: continue
|
||||
if (mini.declarations.any { isLocalDecl(mini, it) && ((it is MiniClassDecl && it.name == className) || (it is MiniEnumDecl && it.name == className)) }) {
|
||||
return file
|
||||
}
|
||||
}
|
||||
for (file in matchingDeclFiles) {
|
||||
val mini = loadMini(file) ?: continue
|
||||
if (mini.declarations.any { isLocalDecl(mini, it) && ((it is MiniClassDecl && it.name == className) || (it is MiniEnumDecl && it.name == className)) }) {
|
||||
val mini = LyngAstManager.getMiniAst(file) ?: continue
|
||||
if (mini.declarations.any { (it is MiniClassDecl && it.name == className) || (it is MiniEnumDecl && it.name == className) }) {
|
||||
return file
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback to full project scan
|
||||
for (file in collectLyngFiles(project)) {
|
||||
if (matchingFiles.contains(file) || matchingDeclFiles.contains(file)) continue // already checked
|
||||
val mini = loadMini(file) ?: continue
|
||||
if (mini.declarations.any { isLocalDecl(mini, it) && ((it is MiniClassDecl && it.name == className) || (it is MiniEnumDecl && it.name == className)) }) {
|
||||
val allFiles = FilenameIndex.getAllFilesByExt(project, "lyng", GlobalSearchScope.projectScope(project))
|
||||
for (vFile in allFiles) {
|
||||
val file = psiManager.findFile(vFile) ?: continue
|
||||
if (matchingFiles.contains(file)) continue // already checked
|
||||
val mini = LyngAstManager.getMiniAst(file) ?: continue
|
||||
if (mini.declarations.any { (it is MiniClassDecl && it.name == className) || (it is MiniEnumDecl && it.name == className) }) {
|
||||
return file
|
||||
}
|
||||
}
|
||||
@ -167,7 +148,7 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
|
||||
}
|
||||
|
||||
private fun getPackageName(file: PsiFile): String? {
|
||||
val mini = loadMini(file) ?: return null
|
||||
val mini = LyngAstManager.getMiniAst(file) ?: return null
|
||||
return try {
|
||||
val pkg = mini.range.start.source.extractPackageName()
|
||||
if (pkg.startsWith("lyng.")) pkg else "lyng.$pkg"
|
||||
@ -191,19 +172,19 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
|
||||
|
||||
private fun resolveGlobally(project: Project, name: String, membersOnly: Boolean = false, allowedPackages: Set<String>? = null): List<ResolveResult> {
|
||||
val results = mutableListOf<ResolveResult>()
|
||||
val files = FilenameIndex.getAllFilesByExt(project, "lyng", GlobalSearchScope.projectScope(project))
|
||||
val psiManager = PsiManager.getInstance(project)
|
||||
|
||||
for (file in collectLyngFiles(project)) {
|
||||
for (vFile in files) {
|
||||
val file = psiManager.findFile(vFile) ?: continue
|
||||
|
||||
// Filter by package if requested
|
||||
if (allowedPackages != null) {
|
||||
val pkg = getPackageName(file)
|
||||
if (pkg == null) {
|
||||
if (!file.name.endsWith(".lyng.d")) continue
|
||||
} else if (pkg !in allowedPackages) continue
|
||||
if (pkg == null || pkg !in allowedPackages) continue
|
||||
}
|
||||
|
||||
val mini = loadMini(file) ?: continue
|
||||
val mini = LyngAstManager.getMiniAst(file) ?: continue
|
||||
val src = mini.range.start.source
|
||||
|
||||
fun addIfMatch(dName: String, nameStart: net.sergeych.lyng.Pos, dKind: String) {
|
||||
@ -216,7 +197,6 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
|
||||
}
|
||||
|
||||
for (d in mini.declarations) {
|
||||
if (!isLocalDecl(mini, d)) continue
|
||||
if (!membersOnly) {
|
||||
val dKind = when(d) {
|
||||
is net.sergeych.lyng.miniast.MiniFunDecl -> "Function"
|
||||
@ -236,7 +216,6 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
|
||||
}
|
||||
|
||||
for (m in members) {
|
||||
if (m.range.start.source != src) continue
|
||||
val mKind = when(m) {
|
||||
is net.sergeych.lyng.miniast.MiniMemberFunDecl -> "Function"
|
||||
is net.sergeych.lyng.miniast.MiniMemberValDecl -> if (m.mutable) "Variable" else "Value"
|
||||
@ -250,42 +229,5 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
|
||||
return results
|
||||
}
|
||||
|
||||
private fun collectLyngFiles(project: Project): List<PsiFile> {
|
||||
val scope = GlobalSearchScope.projectScope(project)
|
||||
val psiManager = PsiManager.getInstance(project)
|
||||
val out = LinkedHashSet<PsiFile>()
|
||||
|
||||
val lyngFiles = FilenameIndex.getAllFilesByExt(project, "lyng", scope)
|
||||
for (vFile in lyngFiles) {
|
||||
psiManager.findFile(vFile)?.let { out.add(it) }
|
||||
}
|
||||
|
||||
// Include declaration files (*.lyng.d) which are indexed as extension "d".
|
||||
val dFiles = FilenameIndex.getAllFilesByExt(project, "d", scope)
|
||||
for (vFile in dFiles) {
|
||||
if (!vFile.name.endsWith(".lyng.d")) continue
|
||||
psiManager.findFile(vFile)?.let { out.add(it) }
|
||||
}
|
||||
|
||||
return out.toList()
|
||||
}
|
||||
|
||||
private fun loadMini(file: PsiFile): MiniScript? {
|
||||
LyngAstManager.getMiniAst(file)?.let { return it }
|
||||
return try {
|
||||
val provider = IdeLenientImportProvider.create()
|
||||
runBlocking {
|
||||
LyngLanguageTools.analyze(
|
||||
LyngAnalysisRequest(text = file.text, fileName = file.name, importProvider = provider)
|
||||
)
|
||||
}.mini
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun isLocalDecl(mini: MiniScript, decl: MiniDecl): Boolean =
|
||||
decl.range.start.source == mini.range.start.source
|
||||
|
||||
override fun getVariants(): Array<Any> = emptyArray()
|
||||
}
|
||||
|
||||
@ -21,22 +21,14 @@ import com.intellij.openapi.application.runReadAction
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.psi.PsiManager
|
||||
import com.intellij.psi.search.FileTypeIndex
|
||||
import com.intellij.psi.search.FilenameIndex
|
||||
import com.intellij.psi.search.GlobalSearchScope
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.binding.BindingSnapshot
|
||||
import net.sergeych.lyng.miniast.BuiltinDocRegistry
|
||||
import net.sergeych.lyng.miniast.DocLookupUtils
|
||||
import net.sergeych.lyng.miniast.MiniEnumDecl
|
||||
import net.sergeych.lyng.miniast.MiniRange
|
||||
import net.sergeych.lyng.miniast.MiniScript
|
||||
import net.sergeych.lyng.tools.IdeLenientImportProvider
|
||||
import net.sergeych.lyng.tools.LyngAnalysisRequest
|
||||
import net.sergeych.lyng.tools.LyngAnalysisResult
|
||||
import net.sergeych.lyng.tools.LyngDiagnostic
|
||||
import net.sergeych.lyng.tools.LyngLanguageTools
|
||||
import net.sergeych.lyng.idea.LyngFileType
|
||||
|
||||
object LyngAstManager {
|
||||
private val MINI_KEY = Key.create<MiniScript>("lyng.mini.cache")
|
||||
@ -60,65 +52,22 @@ object LyngAstManager {
|
||||
|
||||
private fun collectDeclarationFiles(file: PsiFile): List<PsiFile> = runReadAction {
|
||||
val psiManager = PsiManager.getInstance(file.project)
|
||||
var current = file.virtualFile?.parent
|
||||
val seen = mutableSetOf<String>()
|
||||
val result = mutableListOf<PsiFile>()
|
||||
|
||||
var currentDir = file.containingDirectory
|
||||
while (currentDir != null) {
|
||||
for (child in currentDir.files) {
|
||||
if (child.name.endsWith(".lyng.d") && child != file && seen.add(child.virtualFile.path)) {
|
||||
result.add(child)
|
||||
while (current != null) {
|
||||
for (child in current.children) {
|
||||
if (child.name.endsWith(".lyng.d") && child != file.virtualFile && seen.add(child.path)) {
|
||||
val psiD = psiManager.findFile(child) ?: continue
|
||||
result.add(psiD)
|
||||
}
|
||||
}
|
||||
currentDir = currentDir.parentDirectory
|
||||
}
|
||||
|
||||
if (result.isNotEmpty()) return@runReadAction result
|
||||
|
||||
// Fallback for virtual/light files without a stable parent chain (e.g., tests)
|
||||
val basePath = file.virtualFile?.path ?: return@runReadAction result
|
||||
val scope = GlobalSearchScope.projectScope(file.project)
|
||||
val dFiles = FilenameIndex.getAllFilesByExt(file.project, "d", scope)
|
||||
for (vFile in dFiles) {
|
||||
if (!vFile.name.endsWith(".lyng.d")) continue
|
||||
if (vFile.path == basePath) continue
|
||||
val parentPath = vFile.parent?.path ?: continue
|
||||
if (basePath == parentPath || basePath.startsWith(parentPath.trimEnd('/') + "/")) {
|
||||
if (seen.add(vFile.path)) {
|
||||
psiManager.findFile(vFile)?.let { result.add(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isNotEmpty()) return@runReadAction result
|
||||
|
||||
// Fallback: scan all Lyng files in project index and filter by .lyng.d
|
||||
val lyngFiles = FileTypeIndex.getFiles(LyngFileType, scope)
|
||||
for (vFile in lyngFiles) {
|
||||
if (!vFile.name.endsWith(".lyng.d")) continue
|
||||
if (vFile.path == basePath) continue
|
||||
if (seen.add(vFile.path)) {
|
||||
psiManager.findFile(vFile)?.let { result.add(it) }
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isNotEmpty()) return@runReadAction result
|
||||
|
||||
// Final fallback: include all .lyng.d files in project scope
|
||||
for (vFile in dFiles) {
|
||||
if (!vFile.name.endsWith(".lyng.d")) continue
|
||||
if (vFile.path == basePath) continue
|
||||
if (seen.add(vFile.path)) {
|
||||
psiManager.findFile(vFile)?.let { result.add(it) }
|
||||
}
|
||||
current = current.parent
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fun getDeclarationFiles(file: PsiFile): List<PsiFile> = runReadAction {
|
||||
collectDeclarationFiles(file)
|
||||
}
|
||||
|
||||
fun getBinding(file: PsiFile): BindingSnapshot? = runReadAction {
|
||||
getAnalysis(file)?.binding
|
||||
}
|
||||
@ -143,38 +92,20 @@ object LyngAstManager {
|
||||
}
|
||||
|
||||
if (built != null) {
|
||||
val isDecl = file.name.endsWith(".lyng.d")
|
||||
val merged = if (!isDecl && built.mini == null) {
|
||||
MiniScript(MiniRange(built.source.startPos, built.source.startPos))
|
||||
} else {
|
||||
built.mini
|
||||
}
|
||||
if (merged != null && !isDecl) {
|
||||
val merged = built.mini
|
||||
if (merged != null && !file.name.endsWith(".lyng.d")) {
|
||||
val dFiles = collectDeclarationFiles(file)
|
||||
for (df in dFiles) {
|
||||
val dMini = getAnalysis(df)?.mini ?: run {
|
||||
val dText = df.viewProvider.contents.toString()
|
||||
try {
|
||||
val provider = IdeLenientImportProvider.create()
|
||||
runBlocking {
|
||||
LyngLanguageTools.analyze(
|
||||
LyngAnalysisRequest(text = dText, fileName = df.name, importProvider = provider)
|
||||
)
|
||||
}.mini
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
} ?: continue
|
||||
val dAnalysis = getAnalysis(df)
|
||||
val dMini = dAnalysis?.mini ?: continue
|
||||
merged.declarations.addAll(dMini.declarations)
|
||||
merged.imports.addAll(dMini.imports)
|
||||
}
|
||||
}
|
||||
val finalAnalysis = if (merged != null) {
|
||||
val mergedImports = DocLookupUtils.canonicalImportedModules(merged, text)
|
||||
built.copy(
|
||||
mini = merged,
|
||||
importedModules = mergedImports,
|
||||
diagnostics = filterDiagnostics(built.diagnostics, merged, text, mergedImports)
|
||||
importedModules = DocLookupUtils.canonicalImportedModules(merged, text)
|
||||
)
|
||||
} else {
|
||||
built
|
||||
@ -187,45 +118,4 @@ object LyngAstManager {
|
||||
}
|
||||
null
|
||||
}
|
||||
|
||||
private fun filterDiagnostics(
|
||||
diagnostics: List<LyngDiagnostic>,
|
||||
merged: MiniScript,
|
||||
text: String,
|
||||
importedModules: List<String>
|
||||
): List<LyngDiagnostic> {
|
||||
if (diagnostics.isEmpty()) return diagnostics
|
||||
val declaredTopLevel = merged.declarations.map { it.name }.toSet()
|
||||
|
||||
val declaredMembers = linkedSetOf<String>()
|
||||
val aggregatedClasses = DocLookupUtils.aggregateClasses(importedModules, merged)
|
||||
for (cls in aggregatedClasses.values) {
|
||||
cls.members.forEach { declaredMembers.add(it.name) }
|
||||
cls.ctorFields.forEach { declaredMembers.add(it.name) }
|
||||
cls.classFields.forEach { declaredMembers.add(it.name) }
|
||||
}
|
||||
merged.declarations.filterIsInstance<MiniEnumDecl>().forEach { en ->
|
||||
DocLookupUtils.enumToSyntheticClass(en).members.forEach { declaredMembers.add(it.name) }
|
||||
}
|
||||
|
||||
val builtinTopLevel = linkedSetOf<String>()
|
||||
for (mod in importedModules) {
|
||||
BuiltinDocRegistry.docsForModule(mod).forEach { builtinTopLevel.add(it.name) }
|
||||
}
|
||||
|
||||
return diagnostics.filterNot { diag ->
|
||||
val msg = diag.message
|
||||
if (msg.startsWith("unresolved name: ")) {
|
||||
val name = msg.removePrefix("unresolved name: ").trim()
|
||||
name in declaredTopLevel || name in builtinTopLevel
|
||||
} else if (msg.startsWith("unresolved member: ")) {
|
||||
val name = msg.removePrefix("unresolved member: ").trim()
|
||||
val range = diag.range
|
||||
val dotLeft = if (range != null) DocLookupUtils.findDotLeft(text, range.start) else null
|
||||
dotLeft != null && name in declaredMembers
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,127 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng.idea.definitions
|
||||
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.idea.docs.LyngDocumentationProvider
|
||||
import net.sergeych.lyng.idea.navigation.LyngPsiReference
|
||||
import net.sergeych.lyng.idea.settings.LyngFormatterSettings
|
||||
import net.sergeych.lyng.idea.util.LyngAstManager
|
||||
import net.sergeych.lyng.miniast.CompletionEngineLight
|
||||
|
||||
class LyngDefinitionFilesTest : BasePlatformTestCase() {
|
||||
|
||||
override fun getTestDataPath(): String = ""
|
||||
|
||||
private fun enableCompletion() {
|
||||
LyngFormatterSettings.getInstance(project).enableLyngCompletionExperimental = true
|
||||
}
|
||||
|
||||
private fun addDefinitionsFile() {
|
||||
val defs = """
|
||||
/** Utilities exposed via .lyng.d */
|
||||
class Declared(val name: String) {
|
||||
/** Size property */
|
||||
val size: Int = 0
|
||||
|
||||
/** Returns greeting. */
|
||||
fun greet(who: String): String = "hi " + who
|
||||
}
|
||||
|
||||
/** Top-level function. */
|
||||
fun topFun(x: Int): Int = x + 1
|
||||
""".trimIndent()
|
||||
myFixture.addFileToProject("api.lyng.d", defs)
|
||||
}
|
||||
|
||||
fun test_CompletionsIncludeDefinitions() {
|
||||
addDefinitionsFile()
|
||||
enableCompletion()
|
||||
run {
|
||||
val code = """
|
||||
val v = top<caret>
|
||||
""".trimIndent()
|
||||
myFixture.configureByText("main.lyng", code)
|
||||
val text = myFixture.editor.document.text
|
||||
val caret = myFixture.caretOffset
|
||||
val analysis = LyngAstManager.getAnalysis(myFixture.file)
|
||||
val engine = runBlocking { CompletionEngineLight.completeSuspend(text, caret, analysis?.mini, analysis?.binding).map { it.name } }
|
||||
assertTrue("Expected topFun from .lyng.d; got=$engine", engine.contains("topFun"))
|
||||
}
|
||||
run {
|
||||
val code = """
|
||||
<caret>
|
||||
""".trimIndent()
|
||||
myFixture.configureByText("other.lyng", code)
|
||||
val text = myFixture.editor.document.text
|
||||
val caret = myFixture.caretOffset
|
||||
val analysis = LyngAstManager.getAnalysis(myFixture.file)
|
||||
val engine = runBlocking { CompletionEngineLight.completeSuspend(text, caret, analysis?.mini, analysis?.binding).map { it.name } }
|
||||
assertTrue("Expected Declared from .lyng.d; got=$engine", engine.contains("Declared"))
|
||||
}
|
||||
}
|
||||
|
||||
fun test_GotoDefinitionResolvesToDefinitionFile() {
|
||||
addDefinitionsFile()
|
||||
val code = """
|
||||
val x = topFun(1)
|
||||
val y = Declared("x")
|
||||
y.gre<caret>et("me")
|
||||
""".trimIndent()
|
||||
myFixture.configureByText("main.lyng", code)
|
||||
val offset = myFixture.caretOffset
|
||||
val element = myFixture.file.findElementAt(offset) ?: myFixture.file.findElementAt((offset - 1).coerceAtLeast(0))
|
||||
assertNotNull("Expected element at caret for resolve", element)
|
||||
val ref = LyngPsiReference(element!!)
|
||||
val resolved = ref.resolve()
|
||||
assertNotNull("Expected reference to resolve", resolved)
|
||||
assertTrue("Expected .lyng.d target; got=${resolved!!.containingFile.name}", resolved.containingFile.name.endsWith(".lyng.d"))
|
||||
}
|
||||
|
||||
fun test_QuickDocUsesDefinitionDocs() {
|
||||
addDefinitionsFile()
|
||||
val code = """
|
||||
val y = Declared("x")
|
||||
y.gre<caret>et("me")
|
||||
""".trimIndent()
|
||||
myFixture.configureByText("main.lyng", code)
|
||||
val provider = LyngDocumentationProvider()
|
||||
val offset = myFixture.caretOffset
|
||||
val element = myFixture.file.findElementAt(offset) ?: myFixture.file.findElementAt((offset - 1).coerceAtLeast(0))
|
||||
assertNotNull("Expected element at caret for doc", element)
|
||||
val doc = provider.generateDoc(element, element)
|
||||
assertNotNull("Expected Quick Doc", doc)
|
||||
assertTrue("Doc should include summary; got=$doc", doc!!.contains("Returns greeting"))
|
||||
}
|
||||
|
||||
fun test_DiagnosticsIgnoreDefinitionSymbols() {
|
||||
addDefinitionsFile()
|
||||
val code = """
|
||||
val x = topFun(1)
|
||||
val y = Declared("x")
|
||||
y.greet("me")
|
||||
""".trimIndent()
|
||||
myFixture.configureByText("main.lyng", code)
|
||||
val analysis = LyngAstManager.getAnalysis(myFixture.file)
|
||||
val messages = analysis?.diagnostics?.map { it.message } ?: emptyList()
|
||||
assertTrue("Should not report unresolved name for topFun", messages.none { it.contains("unresolved name: topFun") })
|
||||
assertTrue("Should not report unresolved name for Declared", messages.none { it.contains("unresolved name: Declared") })
|
||||
assertTrue("Should not report unresolved member for greet", messages.none { it.contains("unresolved member: greet") })
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* Copyright 2025 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.
|
||||
@ -24,10 +24,10 @@ package net.sergeych.lyng.io.fs
|
||||
import net.sergeych.lyng.ModuleScope
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.ScopeFacade
|
||||
import net.sergeych.lyng.requireScope
|
||||
import net.sergeych.lyng.miniast.*
|
||||
import net.sergeych.lyng.obj.*
|
||||
import net.sergeych.lyng.pacman.ImportManager
|
||||
import net.sergeych.lyng.requireScope
|
||||
import net.sergeych.lyngio.fs.LyngFS
|
||||
import net.sergeych.lyngio.fs.LyngFs
|
||||
import net.sergeych.lyngio.fs.LyngPath
|
||||
@ -191,7 +191,7 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) {
|
||||
fsGuard {
|
||||
val self = this.thisObj as ObjPath
|
||||
val m = self.ensureMetadata()
|
||||
m.modifiedAtMillis?.let { ObjInstant(kotlin.time.Instant.fromEpochMilliseconds(it)) } ?: ObjNull
|
||||
m.modifiedAtMillis?.let { ObjInstant(kotlinx.datetime.Instant.fromEpochMilliseconds(it)) } ?: ObjNull
|
||||
}
|
||||
}
|
||||
// modifiedAtMillis(): Int? — milliseconds since epoch or null
|
||||
@ -314,9 +314,9 @@ private suspend fun buildFsModule(module: ModuleScope, policy: FsAccessPolicy) {
|
||||
ObjMap(mutableMapOf(
|
||||
ObjString("isFile") to ObjBool(m.isRegularFile),
|
||||
ObjString("isDirectory") to ObjBool(m.isDirectory),
|
||||
ObjString("size") to (m.size ?: 0L).toObj(),
|
||||
ObjString("createdAtMillis") to (m.createdAtMillis ?: 0L).toObj(),
|
||||
ObjString("modifiedAtMillis") to (m.modifiedAtMillis ?: 0L).toObj(),
|
||||
ObjString("size") to (m.size?.toLong() ?: 0L).toObj(),
|
||||
ObjString("createdAtMillis") to ((m.createdAtMillis ?: 0L)).toObj(),
|
||||
ObjString("modifiedAtMillis") to ((m.modifiedAtMillis ?: 0L)).toObj(),
|
||||
ObjString("isSymlink") to ObjBool(m.isSymlink),
|
||||
))
|
||||
}
|
||||
@ -478,7 +478,7 @@ private suspend inline fun ScopeFacade.fsGuard(crossinline block: suspend () ->
|
||||
return try {
|
||||
block()
|
||||
} catch (e: AccessDeniedException) {
|
||||
raiseIllegalOperation(e.reasonDetail ?: "access denied")
|
||||
raiseError(ObjIllegalOperationException(requireScope(), e.reasonDetail ?: "access denied"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -21,10 +21,10 @@ import kotlinx.coroutines.flow.Flow
|
||||
import net.sergeych.lyng.ModuleScope
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.ScopeFacade
|
||||
import net.sergeych.lyng.requireScope
|
||||
import net.sergeych.lyng.miniast.*
|
||||
import net.sergeych.lyng.obj.*
|
||||
import net.sergeych.lyng.pacman.ImportManager
|
||||
import net.sergeych.lyng.requireScope
|
||||
import net.sergeych.lyngio.process.*
|
||||
import net.sergeych.lyngio.process.security.ProcessAccessDeniedException
|
||||
import net.sergeych.lyngio.process.security.ProcessAccessPolicy
|
||||
@ -210,9 +210,9 @@ private suspend inline fun ScopeFacade.processGuard(crossinline block: suspend (
|
||||
return try {
|
||||
block()
|
||||
} catch (e: ProcessAccessDeniedException) {
|
||||
raiseIllegalOperation(e.reasonDetail ?: "process access denied")
|
||||
raiseError(ObjIllegalOperationException(requireScope(), e.reasonDetail ?: "process access denied"))
|
||||
} catch (e: Exception) {
|
||||
raiseIllegalOperation(e.message ?: "process error")
|
||||
raiseError(ObjIllegalOperationException(requireScope(), e.message ?: "process error"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -36,19 +36,10 @@ class BytecodeClosureScope(
|
||||
val desired = preferredThisType?.let { typeName ->
|
||||
callScope.thisVariants.firstOrNull { it.objClass.className == typeName }
|
||||
}
|
||||
val primaryThis = when {
|
||||
callScope is ApplyScope -> callScope.thisObj
|
||||
desired != null -> desired
|
||||
else -> closureScope.thisObj
|
||||
}
|
||||
val merged = ArrayList<Obj>(callScope.thisVariants.size + closureScope.thisVariants.size + 3)
|
||||
val primaryThis = closureScope.thisObj
|
||||
val merged = ArrayList<Obj>(callScope.thisVariants.size + closureScope.thisVariants.size + 1)
|
||||
desired?.let { merged.add(it) }
|
||||
merged.add(callScope.thisObj)
|
||||
merged.addAll(callScope.thisVariants)
|
||||
if (callScope is ApplyScope) {
|
||||
merged.add(callScope.applied.thisObj)
|
||||
merged.addAll(callScope.applied.thisVariants)
|
||||
}
|
||||
merged.addAll(closureScope.thisVariants)
|
||||
setThisVariants(primaryThis, merged)
|
||||
this.currentClassCtx = closureScope.currentClassCtx ?: callScope.currentClassCtx
|
||||
@ -56,20 +47,10 @@ class BytecodeClosureScope(
|
||||
}
|
||||
|
||||
class ApplyScope(val callScope: Scope, val applied: Scope) :
|
||||
Scope(applied, callScope.args, callScope.pos, callScope.thisObj) {
|
||||
|
||||
init {
|
||||
// Merge applied receiver variants with the caller variants so qualified this@Type
|
||||
// can see both the applied receiver and outer receivers.
|
||||
val merged = ArrayList<Obj>(applied.thisVariants.size + callScope.thisVariants.size + 1)
|
||||
merged.addAll(applied.thisVariants)
|
||||
merged.addAll(callScope.thisVariants)
|
||||
setThisVariants(callScope.thisObj, merged)
|
||||
this.currentClassCtx = applied.currentClassCtx ?: callScope.currentClassCtx
|
||||
}
|
||||
Scope(callScope, thisObj = applied.thisObj) {
|
||||
|
||||
override fun get(name: String): ObjRecord? {
|
||||
return applied.get(name) ?: callScope.get(name)
|
||||
return applied.get(name) ?: super.get(name)
|
||||
}
|
||||
|
||||
override fun applyClosure(closure: Scope, preferredThisType: String?): Scope {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -31,7 +31,7 @@ class DelegatedVarDeclStatement(
|
||||
) : Statement() {
|
||||
override val pos: Pos = startPos
|
||||
|
||||
override suspend fun execute(scope: Scope): Obj {
|
||||
return bytecodeOnly(scope, "delegated var declaration")
|
||||
override suspend fun execute(context: Scope): Obj {
|
||||
return bytecodeOnly(context, "delegated var declaration")
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@ class DestructuringVarDeclStatement(
|
||||
val isTransient: Boolean,
|
||||
override val pos: Pos,
|
||||
) : Statement() {
|
||||
override suspend fun execute(scope: Scope): Obj {
|
||||
return bytecodeOnly(scope, "destructuring declaration")
|
||||
override suspend fun execute(context: Scope): Obj {
|
||||
return bytecodeOnly(context, "destructuring declaration")
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@ class ExtensionPropertyDeclStatement(
|
||||
) : Statement() {
|
||||
override val pos: Pos = startPos
|
||||
|
||||
override suspend fun execute(scope: Scope): Obj {
|
||||
return bytecodeOnly(scope, "extension property declaration")
|
||||
override suspend fun execute(context: Scope): Obj {
|
||||
return bytecodeOnly(context, "extension property declaration")
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,29 +56,10 @@ class FrameSlotRef(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun callOn(scope: Scope): Obj {
|
||||
val resolved = read()
|
||||
if (resolved === this) {
|
||||
scope.raiseNotImplemented("call on unresolved frame slot")
|
||||
}
|
||||
return resolved.callOn(scope)
|
||||
}
|
||||
|
||||
internal fun refersTo(frame: FrameAccess, slot: Int): Boolean {
|
||||
return this.frame === frame && this.slot == slot
|
||||
}
|
||||
|
||||
internal fun peekValue(): Obj? {
|
||||
val bytecodeFrame = frame as? net.sergeych.lyng.bytecode.BytecodeFrame ?: return read()
|
||||
val raw = bytecodeFrame.getRawObj(slot) ?: return null
|
||||
if (raw is FrameSlotRef && raw.refersTo(bytecodeFrame, slot)) return null
|
||||
return when (raw) {
|
||||
is FrameSlotRef -> raw.peekValue()
|
||||
is RecordSlotRef -> raw.peekValue()
|
||||
else -> raw
|
||||
}
|
||||
}
|
||||
|
||||
fun write(value: Obj) {
|
||||
when (value) {
|
||||
is ObjInt -> frame.setInt(slot, value.value)
|
||||
@ -89,62 +70,6 @@ class FrameSlotRef(
|
||||
}
|
||||
}
|
||||
|
||||
class ScopeSlotRef(
|
||||
private val scope: Scope,
|
||||
private val slot: Int,
|
||||
private val name: String? = null,
|
||||
) : net.sergeych.lyng.obj.Obj() {
|
||||
override suspend fun compareTo(scope: Scope, other: Obj): Int {
|
||||
val resolvedOther = when (other) {
|
||||
is FrameSlotRef -> other.read()
|
||||
is RecordSlotRef -> other.read()
|
||||
is ScopeSlotRef -> other.read()
|
||||
else -> other
|
||||
}
|
||||
return read().compareTo(scope, resolvedOther)
|
||||
}
|
||||
|
||||
fun read(): Obj {
|
||||
val record = scope.getSlotRecord(slot)
|
||||
val direct = record.value
|
||||
if (direct is FrameSlotRef) return direct.read()
|
||||
if (direct is RecordSlotRef) return direct.read()
|
||||
if (direct is ScopeSlotRef) return direct.read()
|
||||
if (direct !== ObjUnset) {
|
||||
return direct
|
||||
}
|
||||
if (name == null) return record.value
|
||||
val resolved = scope.get(name) ?: return record.value
|
||||
if (resolved.value !== ObjUnset) {
|
||||
scope.updateSlotFor(name, resolved)
|
||||
}
|
||||
return resolved.value
|
||||
}
|
||||
|
||||
internal fun peekValue(): Obj? {
|
||||
val record = scope.getSlotRecord(slot)
|
||||
val direct = record.value
|
||||
return when (direct) {
|
||||
is FrameSlotRef -> direct.peekValue()
|
||||
is RecordSlotRef -> direct.peekValue()
|
||||
is ScopeSlotRef -> direct.peekValue()
|
||||
else -> direct
|
||||
}
|
||||
}
|
||||
|
||||
fun write(value: Obj) {
|
||||
scope.setSlotValue(slot, value)
|
||||
}
|
||||
|
||||
override suspend fun callOn(scope: Scope): Obj {
|
||||
val resolved = read()
|
||||
if (resolved === this) {
|
||||
scope.raiseNotImplemented("call on unresolved scope slot")
|
||||
}
|
||||
return resolved.callOn(scope)
|
||||
}
|
||||
}
|
||||
|
||||
class RecordSlotRef(
|
||||
private val record: ObjRecord,
|
||||
) : net.sergeych.lyng.obj.Obj() {
|
||||
@ -152,7 +77,6 @@ class RecordSlotRef(
|
||||
val resolvedOther = when (other) {
|
||||
is FrameSlotRef -> other.read()
|
||||
is RecordSlotRef -> other.read()
|
||||
is ScopeSlotRef -> other.read()
|
||||
else -> other
|
||||
}
|
||||
return read().compareTo(scope, resolvedOther)
|
||||
@ -160,37 +84,10 @@ class RecordSlotRef(
|
||||
|
||||
fun read(): Obj {
|
||||
val direct = record.value
|
||||
return when (direct) {
|
||||
is FrameSlotRef -> direct.read()
|
||||
is ScopeSlotRef -> direct.read()
|
||||
else -> direct
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun callOn(scope: Scope): Obj {
|
||||
val resolved = read()
|
||||
if (resolved === this) {
|
||||
scope.raiseNotImplemented("call on unresolved record slot")
|
||||
}
|
||||
return resolved.callOn(scope)
|
||||
}
|
||||
|
||||
internal fun peekValue(): Obj? {
|
||||
val direct = record.value
|
||||
return when (direct) {
|
||||
is FrameSlotRef -> direct.peekValue()
|
||||
is RecordSlotRef -> direct.peekValue()
|
||||
is ScopeSlotRef -> direct.peekValue()
|
||||
else -> direct
|
||||
}
|
||||
return if (direct is FrameSlotRef) direct.read() else direct
|
||||
}
|
||||
|
||||
fun write(value: Obj) {
|
||||
val direct = record.value
|
||||
if (direct is ScopeSlotRef) {
|
||||
direct.write(value)
|
||||
} else {
|
||||
record.value = value
|
||||
}
|
||||
record.value = value
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* Copyright 2025 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.
|
||||
@ -44,28 +44,11 @@ class ModuleScope(
|
||||
|
||||
internal fun ensureModuleFrame(fn: CmdFunction): BytecodeFrame {
|
||||
val current = moduleFrame
|
||||
val frame = if (current == null) {
|
||||
val frame = if (current == null || moduleFrameLocalCount != fn.localCount) {
|
||||
BytecodeFrame(fn.localCount, 0).also {
|
||||
moduleFrame = it
|
||||
moduleFrameLocalCount = fn.localCount
|
||||
}
|
||||
} else if (fn.localCount > moduleFrameLocalCount) {
|
||||
val next = BytecodeFrame(fn.localCount, 0)
|
||||
current.copyTo(next)
|
||||
moduleFrame = next
|
||||
moduleFrameLocalCount = fn.localCount
|
||||
// Retarget frame-based locals to the new frame instance.
|
||||
val localNames = fn.localSlotNames
|
||||
for (i in localNames.indices) {
|
||||
val name = localNames[i] ?: continue
|
||||
val record = objects[name] ?: localBindings[name] ?: continue
|
||||
val value = record.value
|
||||
if (value is FrameSlotRef && value.refersTo(current, i)) {
|
||||
record.value = FrameSlotRef(next, i)
|
||||
updateSlotFor(name, record)
|
||||
}
|
||||
}
|
||||
next
|
||||
} else {
|
||||
current
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -19,8 +19,7 @@ package net.sergeych.lyng
|
||||
|
||||
data class Pos(val source: Source, val line: Int, val column: Int) {
|
||||
override fun toString(): String {
|
||||
val col = if (column >= 0) column + 1 else column
|
||||
return "${source.fileName}:${line+1}:$col"
|
||||
return "${source.fileName}:${line+1}:${column}"
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
|
||||
@ -635,16 +635,10 @@ open class Scope(
|
||||
objects[name]?.let {
|
||||
if( !it.isMutable )
|
||||
raiseIllegalAssignment("symbol is readonly: $name")
|
||||
when (val current = it.value) {
|
||||
is FrameSlotRef -> current.write(value)
|
||||
is RecordSlotRef -> current.write(value)
|
||||
else -> it.value = value
|
||||
}
|
||||
it.value = value
|
||||
// keep local binding index consistent within the frame
|
||||
localBindings[name] = it
|
||||
bumpClassLayoutIfNeeded(name, value, recordType)
|
||||
updateSlotFor(name, it)
|
||||
syncModuleFrameSlot(name, value)
|
||||
it
|
||||
} ?: addItem(name, true, value, visibility, writeVisibility, recordType, isAbstract = isAbstract, isClosed = isClosed, isOverride = isOverride)
|
||||
|
||||
@ -661,7 +655,6 @@ open class Scope(
|
||||
isOverride: Boolean = false,
|
||||
isTransient: Boolean = false,
|
||||
callSignature: CallSignature? = null,
|
||||
typeDecl: TypeDecl? = null,
|
||||
fieldId: Int? = null,
|
||||
methodId: Int? = null
|
||||
): ObjRecord {
|
||||
@ -674,7 +667,6 @@ open class Scope(
|
||||
isOverride = isOverride,
|
||||
isTransient = isTransient,
|
||||
callSignature = callSignature,
|
||||
typeDecl = typeDecl,
|
||||
memberName = name,
|
||||
fieldId = fieldId,
|
||||
methodId = methodId
|
||||
@ -711,7 +703,6 @@ open class Scope(
|
||||
slots[idx] = rec
|
||||
}
|
||||
}
|
||||
syncModuleFrameSlot(name, value)
|
||||
return rec
|
||||
}
|
||||
|
||||
@ -759,48 +750,7 @@ open class Scope(
|
||||
|
||||
// --- removed doc-aware overloads to keep runtime lean ---
|
||||
|
||||
fun addConst(name: String, value: Obj): ObjRecord {
|
||||
val existing = objects[name]
|
||||
if (existing != null) {
|
||||
when (val current = existing.value) {
|
||||
is FrameSlotRef -> current.write(value)
|
||||
is RecordSlotRef -> current.write(value)
|
||||
else -> existing.value = value
|
||||
}
|
||||
bumpClassLayoutIfNeeded(name, value, existing.type)
|
||||
updateSlotFor(name, existing)
|
||||
syncModuleFrameSlot(name, value)
|
||||
return existing
|
||||
}
|
||||
val slotIndex = getSlotIndexOf(name)
|
||||
if (slotIndex != null) {
|
||||
val record = getSlotRecord(slotIndex)
|
||||
when (val current = record.value) {
|
||||
is FrameSlotRef -> current.write(value)
|
||||
is RecordSlotRef -> current.write(value)
|
||||
else -> record.value = value
|
||||
}
|
||||
bumpClassLayoutIfNeeded(name, value, record.type)
|
||||
updateSlotFor(name, record)
|
||||
syncModuleFrameSlot(name, value)
|
||||
return record
|
||||
}
|
||||
val record = addItem(name, false, value)
|
||||
syncModuleFrameSlot(name, value)
|
||||
return record
|
||||
}
|
||||
|
||||
private fun syncModuleFrameSlot(name: String, value: Obj) {
|
||||
val module = this as? ModuleScope ?: return
|
||||
val frame = module.moduleFrame ?: return
|
||||
val localNames = module.moduleFrameLocalSlotNames
|
||||
if (localNames.isEmpty()) return
|
||||
for (i in localNames.indices) {
|
||||
if (localNames[i] == name) {
|
||||
frame.setObj(i, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
fun addConst(name: String, value: Obj) = addItem(name, false, value)
|
||||
|
||||
|
||||
suspend fun eval(code: String): Obj =
|
||||
@ -914,7 +864,7 @@ open class Scope(
|
||||
val receiver = rec.receiver ?: thisObj
|
||||
val del = rec.delegate ?: run {
|
||||
if (receiver is ObjInstance) {
|
||||
receiver.writeField(this, name, newValue)
|
||||
(receiver as ObjInstance).writeField(this, name, newValue)
|
||||
return
|
||||
}
|
||||
raiseError("Internal error: delegated property $name has no delegate")
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* Copyright 2026 Sergey S. Chernov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -12,12 +12,13 @@
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng
|
||||
|
||||
import net.sergeych.lyng.obj.*
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjRecord
|
||||
import net.sergeych.lyng.obj.ObjString
|
||||
|
||||
/**
|
||||
* Limited facade for Kotlin bridge callables.
|
||||
@ -107,29 +108,3 @@ inline fun <reified T : Obj> ScopeFacade.thisAs(): T {
|
||||
|
||||
fun ScopeFacade.requireScope(): Scope =
|
||||
(this as? ScopeBridge)?.scope ?: raiseIllegalState("ScopeFacade requires ScopeBridge")
|
||||
|
||||
fun ScopeFacade.raiseNPE(): Nothing = requireScope().raiseNPE()
|
||||
|
||||
fun ScopeFacade.raiseIndexOutOfBounds(message: String = "Index out of bounds"): Nothing =
|
||||
requireScope().raiseIndexOutOfBounds(message)
|
||||
|
||||
fun ScopeFacade.raiseIllegalAssignment(message: String): Nothing =
|
||||
requireScope().raiseIllegalAssignment(message)
|
||||
|
||||
fun ScopeFacade.raiseUnset(message: String = "property is unset (not initialized)"): Nothing =
|
||||
requireScope().raiseUnset(message)
|
||||
|
||||
fun ScopeFacade.raiseNotFound(message: String = "not found"): Nothing =
|
||||
requireScope().raiseNotFound(message)
|
||||
|
||||
fun ScopeFacade.raiseError(obj: Obj, pos: Pos = this.pos, message: String): Nothing =
|
||||
requireScope().raiseError(obj, pos, message)
|
||||
|
||||
fun ScopeFacade.raiseAssertionFailed(message: String): Nothing =
|
||||
raiseError(ObjAssertionFailedException(requireScope(), message))
|
||||
|
||||
fun ScopeFacade.raiseIllegalOperation(message: String = "Operation is illegal"): Nothing =
|
||||
raiseError(ObjIllegalOperationException(requireScope(), message))
|
||||
|
||||
fun ScopeFacade.raiseIterationFinished(): Nothing =
|
||||
raiseError(ObjIterationFinishedException(requireScope()))
|
||||
|
||||
@ -26,6 +26,7 @@ import net.sergeych.lyng.miniast.*
|
||||
import net.sergeych.lyng.obj.*
|
||||
import net.sergeych.lyng.pacman.ImportManager
|
||||
import net.sergeych.lyng.stdlib_included.rootLyng
|
||||
import net.sergeych.lyng.bridge.LyngClassBridge
|
||||
import net.sergeych.lynon.ObjLynonClass
|
||||
import net.sergeych.mp_tools.globalDefer
|
||||
import kotlin.math.*
|
||||
@ -42,12 +43,13 @@ class Script(
|
||||
// private val catchReturn: Boolean = false,
|
||||
) : Statement() {
|
||||
fun statements(): List<Statement> = statements
|
||||
|
||||
override suspend fun execute(scope: Scope): Obj {
|
||||
scope.pos = pos
|
||||
val execScope = resolveModuleScope(scope) ?: scope
|
||||
val isModuleScope = execScope is ModuleScope
|
||||
val shouldSeedModule = isModuleScope || execScope.thisObj === ObjVoid
|
||||
val moduleTarget = (execScope as? ModuleScope) ?: execScope.parent as? ModuleScope ?: execScope
|
||||
val moduleTarget = execScope
|
||||
if (shouldSeedModule) {
|
||||
seedModuleSlots(moduleTarget, scope)
|
||||
}
|
||||
@ -55,15 +57,9 @@ class Script(
|
||||
if (execScope is ModuleScope) {
|
||||
execScope.ensureModuleFrame(fn)
|
||||
}
|
||||
var execFrame: net.sergeych.lyng.bytecode.CmdFrame? = null
|
||||
val result = CmdVm().execute(fn, execScope, scope.args) { frame, _ ->
|
||||
execFrame = frame
|
||||
return CmdVm().execute(fn, execScope, scope.args) { frame, _ ->
|
||||
seedModuleLocals(frame, moduleTarget, scope)
|
||||
}
|
||||
if (execScope !is ModuleScope) {
|
||||
execFrame?.let { syncFrameLocalsToScope(it, execScope) }
|
||||
}
|
||||
return result
|
||||
}
|
||||
if (statements.isNotEmpty()) {
|
||||
scope.raiseIllegalState("bytecode-only execution is required; missing module bytecode")
|
||||
@ -74,13 +70,6 @@ class Script(
|
||||
private suspend fun seedModuleSlots(scope: Scope, seedScope: Scope) {
|
||||
if (importBindings.isEmpty() && importedModules.isEmpty()) return
|
||||
seedImportBindings(scope, seedScope)
|
||||
if (moduleSlotPlan.isNotEmpty()) {
|
||||
scope.applySlotPlan(moduleSlotPlan)
|
||||
for (name in moduleSlotPlan.keys) {
|
||||
val record = scope.objects[name] ?: scope.localBindings[name] ?: continue
|
||||
scope.updateSlotFor(name, record)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun seedModuleLocals(
|
||||
@ -99,43 +88,12 @@ class Script(
|
||||
val value = if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property) {
|
||||
scope.resolve(record, name)
|
||||
} else {
|
||||
val raw = record.value
|
||||
when (raw) {
|
||||
is FrameSlotRef -> {
|
||||
if (raw.refersTo(frame.frame, i)) {
|
||||
raw.peekValue() ?: continue
|
||||
} else if (seedScope !is ModuleScope) {
|
||||
raw
|
||||
} else {
|
||||
raw.read()
|
||||
}
|
||||
}
|
||||
is RecordSlotRef -> {
|
||||
if (seedScope !is ModuleScope) raw else raw.read()
|
||||
}
|
||||
else -> raw
|
||||
}
|
||||
record.value
|
||||
}
|
||||
frame.setObjUnchecked(base + i, value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun syncFrameLocalsToScope(frame: net.sergeych.lyng.bytecode.CmdFrame, scope: Scope) {
|
||||
val localNames = frame.fn.localSlotNames
|
||||
if (localNames.isEmpty()) return
|
||||
for (i in localNames.indices) {
|
||||
val name = localNames[i] ?: continue
|
||||
val record = scope.getLocalRecordDirect(name) ?: scope.localBindings[name] ?: scope.objects[name] ?: continue
|
||||
val value = frame.readLocalObj(i)
|
||||
when (val current = record.value) {
|
||||
is FrameSlotRef -> current.write(value)
|
||||
is RecordSlotRef -> current.write(value)
|
||||
else -> record.value = value
|
||||
}
|
||||
scope.updateSlotFor(name, record)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun seedImportBindings(scope: Scope, seedScope: Scope) {
|
||||
val provider = scope.currentImportProvider
|
||||
val importedModules = LinkedHashSet<ModuleScope>()
|
||||
@ -145,9 +103,6 @@ class Script(
|
||||
if (scope is ModuleScope) {
|
||||
scope.importedModules = importedModules.toList()
|
||||
}
|
||||
for (module in importedModules) {
|
||||
module.importInto(scope, null)
|
||||
}
|
||||
for ((name, binding) in importBindings) {
|
||||
val record = when (val source = binding.source) {
|
||||
is ImportBindingSource.Module -> {
|
||||
@ -210,8 +165,8 @@ class Script(
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Create new scope using a standard safe set of modules, using [defaultImportManager]. It is
|
||||
* suspended as first time invocation requires compilation of standard library or other
|
||||
* Create new scope using standard safe set of modules, using [defaultImportManager]. It is
|
||||
* suspended as first time calls requires compilation of standard library or other
|
||||
* asynchronous initialization.
|
||||
*/
|
||||
suspend fun newScope(pos: Pos = Pos.builtIn) = defaultImportManager.newStdScope(pos)
|
||||
@ -368,10 +323,10 @@ class Script(
|
||||
addVoidFn("assert") {
|
||||
val cond = requiredArg<ObjBool>(0)
|
||||
val message = if (args.size > 1)
|
||||
": " + toStringOf(call(args[1])).value
|
||||
": " + toStringOf(call(args[1] as Obj)).value
|
||||
else ""
|
||||
if (!cond.value == true)
|
||||
raiseAssertionFailed("Assertion failed$message")
|
||||
raiseError(ObjAssertionFailedException(requireScope(), "Assertion failed$message"))
|
||||
}
|
||||
|
||||
fun unwrapCompareArg(value: Obj): Obj {
|
||||
@ -387,7 +342,12 @@ class Script(
|
||||
val a = unwrapCompareArg(requiredArg(0))
|
||||
val b = unwrapCompareArg(requiredArg(1))
|
||||
if (a.compareTo(requireScope(), b) != 0) {
|
||||
raiseAssertionFailed("Assertion failed: ${inspect(a)} == ${inspect(b)}")
|
||||
raiseError(
|
||||
ObjAssertionFailedException(
|
||||
requireScope(),
|
||||
"Assertion failed: ${inspect(a)} == ${inspect(b)}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
// alias used in tests
|
||||
@ -395,13 +355,23 @@ class Script(
|
||||
val a = unwrapCompareArg(requiredArg(0))
|
||||
val b = unwrapCompareArg(requiredArg(1))
|
||||
if (a.compareTo(requireScope(), b) != 0)
|
||||
raiseAssertionFailed("Assertion failed: ${inspect(a)} == ${inspect(b)}")
|
||||
raiseError(
|
||||
ObjAssertionFailedException(
|
||||
requireScope(),
|
||||
"Assertion failed: ${inspect(a)} == ${inspect(b)}"
|
||||
)
|
||||
)
|
||||
}
|
||||
addVoidFn("assertNotEquals") {
|
||||
val a = unwrapCompareArg(requiredArg(0))
|
||||
val b = unwrapCompareArg(requiredArg(1))
|
||||
if (a.compareTo(requireScope(), b) == 0)
|
||||
raiseAssertionFailed("Assertion failed: ${inspect(a)} != ${inspect(b)}")
|
||||
raiseError(
|
||||
ObjAssertionFailedException(
|
||||
requireScope(),
|
||||
"Assertion failed: ${inspect(a)} != ${inspect(b)}"
|
||||
)
|
||||
)
|
||||
}
|
||||
addFnDoc(
|
||||
"assertThrows",
|
||||
@ -439,7 +409,12 @@ class Script(
|
||||
} catch (_: ScriptError) {
|
||||
ObjNull
|
||||
}
|
||||
if (result == null) raiseAssertionFailed("Expected exception but nothing was thrown")
|
||||
if (result == null) raiseError(
|
||||
ObjAssertionFailedException(
|
||||
requireScope(),
|
||||
"Expected exception but nothing was thrown"
|
||||
)
|
||||
)
|
||||
expectedClass?.let {
|
||||
if (!result.isInstanceOf(it)) {
|
||||
val actual = if (result is ObjException) result.exceptionClass else result.objClass
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* Copyright 2025 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.
|
||||
@ -30,10 +30,6 @@ sealed class TypeDecl(val isNullable:Boolean = false) {
|
||||
val returnType: TypeDecl,
|
||||
val nullable: Boolean = false
|
||||
) : TypeDecl(nullable)
|
||||
data class Ellipsis(
|
||||
val elementType: TypeDecl,
|
||||
val nullable: Boolean = false
|
||||
) : TypeDecl(nullable)
|
||||
data class TypeVar(val name: String, val nullable: Boolean = false) : TypeDecl(nullable)
|
||||
data class Union(val options: List<TypeDecl>, val nullable: Boolean = false) : TypeDecl(nullable)
|
||||
data class Intersection(val options: List<TypeDecl>, val nullable: Boolean = false) : TypeDecl(nullable)
|
||||
|
||||
@ -12,7 +12,6 @@
|
||||
* 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
|
||||
@ -26,7 +25,6 @@ class VarDeclStatement(
|
||||
val visibility: Visibility,
|
||||
val initializer: Statement?,
|
||||
val isTransient: Boolean,
|
||||
val typeDecl: TypeDecl?,
|
||||
val slotIndex: Int?,
|
||||
val scopeId: Int?,
|
||||
private val startPos: Pos,
|
||||
@ -34,7 +32,7 @@ class VarDeclStatement(
|
||||
) : Statement() {
|
||||
override val pos: Pos = startPos
|
||||
|
||||
override suspend fun execute(scope: Scope): Obj {
|
||||
return bytecodeOnly(scope, "var declaration")
|
||||
override suspend fun execute(context: Scope): Obj {
|
||||
return bytecodeOnly(context, "var declaration")
|
||||
}
|
||||
}
|
||||
|
||||
@ -122,51 +122,6 @@ object LyngClassBridge {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry point for Kotlin bindings to declared Lyng objects (singleton instances).
|
||||
*
|
||||
* Works similarly to [LyngClassBridge], but targets an already created object instance.
|
||||
*/
|
||||
object LyngObjectBridge {
|
||||
/**
|
||||
* Resolve a Lyng object by [objectName] and bind Kotlin implementations.
|
||||
*
|
||||
* @param module module name used for resolution (required when [module] scope is not provided)
|
||||
* @param importManager import manager used to resolve the module
|
||||
*/
|
||||
suspend fun bind(
|
||||
objectName: String,
|
||||
module: String? = null,
|
||||
importManager: ImportManager = Script.defaultImportManager,
|
||||
block: ClassBridgeBinder.() -> Unit
|
||||
): ObjInstance {
|
||||
val obj = resolveObject(objectName, module, null, importManager)
|
||||
return bind(obj, block)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a Lyng object within an existing [moduleScope] and bind Kotlin implementations.
|
||||
*/
|
||||
suspend fun bind(
|
||||
moduleScope: ModuleScope,
|
||||
objectName: String,
|
||||
block: ClassBridgeBinder.() -> Unit
|
||||
): ObjInstance {
|
||||
val obj = resolveObject(objectName, null, moduleScope, Script.defaultImportManager)
|
||||
return bind(obj, block)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind Kotlin implementations directly to an already resolved object [instance].
|
||||
*/
|
||||
suspend fun bind(instance: ObjInstance, block: ClassBridgeBinder.() -> Unit): ObjInstance {
|
||||
val binder = ObjectBridgeBinderImpl(instance)
|
||||
binder.block()
|
||||
binder.commit()
|
||||
return instance
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sugar for [LyngClassBridge.bind] on a module scope.
|
||||
*
|
||||
@ -177,16 +132,6 @@ suspend fun ModuleScope.bind(
|
||||
block: ClassBridgeBinder.() -> Unit
|
||||
): ObjClass = LyngClassBridge.bind(this, className, block)
|
||||
|
||||
/**
|
||||
* Sugar for [LyngObjectBridge.bind] on a module scope.
|
||||
*
|
||||
* Bound members must be declared as `extern` in Lyng.
|
||||
*/
|
||||
suspend fun ModuleScope.bindObject(
|
||||
objectName: String,
|
||||
block: ClassBridgeBinder.() -> Unit
|
||||
): ObjInstance = LyngObjectBridge.bind(this, objectName, block)
|
||||
|
||||
/** Kotlin-side data slot attached to a Lyng instance. */
|
||||
var ObjInstance.data: Any?
|
||||
get() = kotlinInstanceData
|
||||
@ -382,214 +327,6 @@ private class ClassBridgeBinderImpl(
|
||||
}
|
||||
}
|
||||
|
||||
private class ObjectBridgeBinderImpl(
|
||||
private val instance: ObjInstance
|
||||
) : ClassBridgeBinder {
|
||||
private val cls: ObjClass = instance.objClass
|
||||
private val initHooks = mutableListOf<suspend (ScopeFacade, ObjInstance) -> Unit>()
|
||||
|
||||
override var classData: Any?
|
||||
get() = cls.kotlinClassData
|
||||
set(value) { cls.kotlinClassData = value }
|
||||
|
||||
override fun init(block: suspend BridgeInstanceContext.(ScopeFacade) -> Unit) {
|
||||
initHooks.add { scope, inst ->
|
||||
val ctx = BridgeInstanceContextImpl(inst)
|
||||
ctx.block(scope)
|
||||
}
|
||||
}
|
||||
|
||||
override fun initWithInstance(block: suspend (ScopeFacade, Obj) -> Unit) {
|
||||
initHooks.add { scope, inst ->
|
||||
block(scope, inst)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addFun(name: String, impl: suspend (ScopeFacade, Obj, Arguments) -> Obj) {
|
||||
val target = findMember(name)
|
||||
val callable = ObjExternCallable.fromBridge {
|
||||
impl(this, thisObj, args)
|
||||
}
|
||||
val methodId = cls.ensureMethodIdForBridge(name, target.record)
|
||||
val newRecord = target.record.copy(
|
||||
value = callable,
|
||||
type = ObjRecord.Type.Fun,
|
||||
methodId = methodId
|
||||
)
|
||||
replaceMember(target, newRecord)
|
||||
updateInstanceMember(target, newRecord)
|
||||
}
|
||||
|
||||
override fun addVal(name: String, impl: suspend (ScopeFacade, Obj) -> Obj) {
|
||||
val target = findMember(name)
|
||||
if (target.record.isMutable) {
|
||||
throw ScriptError(Pos.builtIn, "extern val $name is mutable in class ${cls.className}")
|
||||
}
|
||||
val getter = ObjExternCallable.fromBridge {
|
||||
impl(this, thisObj)
|
||||
}
|
||||
val prop = ObjProperty(name, getter, null)
|
||||
val isFieldLike = target.record.type == ObjRecord.Type.Field ||
|
||||
target.record.type == ObjRecord.Type.ConstructorField
|
||||
val newRecord = if (isFieldLike) {
|
||||
removeFieldInitializersFor(name)
|
||||
target.record.copy(
|
||||
value = prop,
|
||||
type = target.record.type,
|
||||
fieldId = target.record.fieldId,
|
||||
methodId = target.record.methodId
|
||||
)
|
||||
} else {
|
||||
val methodId = cls.ensureMethodIdForBridge(name, target.record)
|
||||
target.record.copy(
|
||||
value = prop,
|
||||
type = ObjRecord.Type.Property,
|
||||
methodId = methodId,
|
||||
fieldId = null
|
||||
)
|
||||
}
|
||||
replaceMember(target, newRecord)
|
||||
updateInstanceMember(target, newRecord)
|
||||
}
|
||||
|
||||
override fun addVar(
|
||||
name: String,
|
||||
get: suspend (ScopeFacade, Obj) -> Obj,
|
||||
set: suspend (ScopeFacade, Obj, Obj) -> Unit
|
||||
) {
|
||||
val target = findMember(name)
|
||||
if (!target.record.isMutable) {
|
||||
throw ScriptError(Pos.builtIn, "extern var $name is readonly in class ${cls.className}")
|
||||
}
|
||||
val getter = ObjExternCallable.fromBridge {
|
||||
get(this, thisObj)
|
||||
}
|
||||
val setter = ObjExternCallable.fromBridge {
|
||||
val value = requiredArg<Obj>(0)
|
||||
set(this, thisObj, value)
|
||||
ObjVoid
|
||||
}
|
||||
val prop = ObjProperty(name, getter, setter)
|
||||
val isFieldLike = target.record.type == ObjRecord.Type.Field ||
|
||||
target.record.type == ObjRecord.Type.ConstructorField
|
||||
val newRecord = if (isFieldLike) {
|
||||
removeFieldInitializersFor(name)
|
||||
target.record.copy(
|
||||
value = prop,
|
||||
type = target.record.type,
|
||||
fieldId = target.record.fieldId,
|
||||
methodId = target.record.methodId
|
||||
)
|
||||
} else {
|
||||
val methodId = cls.ensureMethodIdForBridge(name, target.record)
|
||||
target.record.copy(
|
||||
value = prop,
|
||||
type = ObjRecord.Type.Property,
|
||||
methodId = methodId,
|
||||
fieldId = null
|
||||
)
|
||||
}
|
||||
replaceMember(target, newRecord)
|
||||
updateInstanceMember(target, newRecord)
|
||||
}
|
||||
|
||||
suspend fun commit() {
|
||||
if (initHooks.isNotEmpty()) {
|
||||
val target = cls.bridgeInitHooks ?: mutableListOf<suspend (ScopeFacade, ObjInstance) -> Unit>().also {
|
||||
cls.bridgeInitHooks = it
|
||||
}
|
||||
target.addAll(initHooks)
|
||||
val facade = instance.instanceScope.asFacade()
|
||||
for (hook in initHooks) {
|
||||
hook(facade, instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun replaceMember(target: MemberTarget, newRecord: ObjRecord) {
|
||||
when (target.kind) {
|
||||
MemberKind.Instance -> {
|
||||
cls.replaceMemberForBridge(target.name, newRecord)
|
||||
if (target.mirrorClassScope && cls.classScope?.objects?.containsKey(target.name) == true) {
|
||||
cls.replaceClassScopeMemberForBridge(target.name, newRecord)
|
||||
}
|
||||
}
|
||||
MemberKind.Static -> cls.replaceClassScopeMemberForBridge(target.name, newRecord)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateInstanceMember(target: MemberTarget, newRecord: ObjRecord) {
|
||||
val key = instanceStorageKey(target, newRecord) ?: return
|
||||
ensureInstanceSlotCapacity()
|
||||
instance.instanceScope.objects[key] = newRecord
|
||||
cls.fieldSlotForKey(key)?.let { slot ->
|
||||
instance.setFieldSlotRecord(slot.slot, newRecord)
|
||||
}
|
||||
cls.methodSlotForKey(key)?.let { slot ->
|
||||
instance.setMethodSlotRecord(slot.slot, newRecord)
|
||||
}
|
||||
}
|
||||
|
||||
private fun instanceStorageKey(target: MemberTarget, rec: ObjRecord): String? = when (target.kind) {
|
||||
MemberKind.Instance -> {
|
||||
if (rec.visibility == Visibility.Private ||
|
||||
rec.type == ObjRecord.Type.Field ||
|
||||
rec.type == ObjRecord.Type.ConstructorField ||
|
||||
rec.type == ObjRecord.Type.Delegated) {
|
||||
cls.mangledName(target.name)
|
||||
} else {
|
||||
target.name
|
||||
}
|
||||
}
|
||||
MemberKind.Static -> {
|
||||
if (rec.type != ObjRecord.Type.Fun &&
|
||||
rec.type != ObjRecord.Type.Property &&
|
||||
rec.type != ObjRecord.Type.Delegated) {
|
||||
null
|
||||
} else if (rec.visibility == Visibility.Private || rec.type == ObjRecord.Type.Delegated) {
|
||||
cls.mangledName(target.name)
|
||||
} else {
|
||||
target.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureInstanceSlotCapacity() {
|
||||
val fieldCount = cls.fieldSlotCount()
|
||||
if (instance.fieldSlots.size < fieldCount) {
|
||||
val newSlots = arrayOfNulls<ObjRecord>(fieldCount)
|
||||
instance.fieldSlots.copyInto(newSlots, 0, 0, instance.fieldSlots.size)
|
||||
instance.fieldSlots = newSlots
|
||||
}
|
||||
val methodCount = cls.methodSlotCount()
|
||||
if (instance.methodSlots.size < methodCount) {
|
||||
val newSlots = arrayOfNulls<ObjRecord>(methodCount)
|
||||
instance.methodSlots.copyInto(newSlots, 0, 0, instance.methodSlots.size)
|
||||
instance.methodSlots = newSlots
|
||||
}
|
||||
}
|
||||
|
||||
private fun findMember(name: String): MemberTarget {
|
||||
val inst = cls.members[name]
|
||||
val stat = cls.classScope?.objects?.get(name)
|
||||
if (inst != null) {
|
||||
return MemberTarget(name, inst, MemberKind.Instance, mirrorClassScope = stat != null)
|
||||
}
|
||||
if (stat != null) return MemberTarget(name, stat, MemberKind.Static)
|
||||
throw ScriptError(Pos.builtIn, "extern member $name not found in class ${cls.className}")
|
||||
}
|
||||
|
||||
private fun removeFieldInitializersFor(name: String) {
|
||||
if (cls.instanceInitializers.isEmpty()) return
|
||||
val storageName = cls.mangledName(name)
|
||||
cls.instanceInitializers.removeAll { init ->
|
||||
val stmt = init as? Statement ?: return@removeAll false
|
||||
val original = (stmt as? BytecodeStatement)?.original ?: stmt
|
||||
original is InstanceFieldInitStatement && original.storageName == storageName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun resolveClass(
|
||||
className: String,
|
||||
module: String?,
|
||||
@ -612,26 +349,3 @@ private suspend fun resolveClass(
|
||||
}
|
||||
throw ScriptError(Pos.builtIn, "class $className not found in module ${scope.packageName}")
|
||||
}
|
||||
|
||||
private suspend fun resolveObject(
|
||||
objectName: String,
|
||||
module: String?,
|
||||
moduleScope: ModuleScope?,
|
||||
importManager: ImportManager
|
||||
): ObjInstance {
|
||||
val scope = moduleScope ?: run {
|
||||
if (module == null) {
|
||||
throw ScriptError(Pos.builtIn, "module is required to resolve $objectName")
|
||||
}
|
||||
importManager.createModuleScope(Pos.builtIn, module)
|
||||
}
|
||||
val rec = scope.get(objectName)
|
||||
val direct = rec?.value as? ObjInstance
|
||||
if (direct != null) return direct
|
||||
if (objectName.contains('.')) {
|
||||
val resolved = scope.resolveQualifiedIdentifier(objectName)
|
||||
val inst = resolved as? ObjInstance
|
||||
if (inst != null) return inst
|
||||
}
|
||||
throw ScriptError(Pos.builtIn, "object $objectName not found in module ${scope.packageName}")
|
||||
}
|
||||
|
||||
@ -35,7 +35,6 @@ class BytecodeCompiler(
|
||||
private val slotTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(),
|
||||
private val slotTypeDeclByScopeId: Map<Int, Map<Int, TypeDecl>> = emptyMap(),
|
||||
private val knownNameObjClass: Map<String, ObjClass> = emptyMap(),
|
||||
private val knownClassNames: Set<String> = emptySet(),
|
||||
private val knownObjectNames: Set<String> = emptySet(),
|
||||
private val classFieldTypesByName: Map<String, Map<String, ObjClass>> = emptyMap(),
|
||||
private val enumEntriesByName: Map<String, List<String>> = emptyMap(),
|
||||
@ -79,6 +78,7 @@ class BytecodeCompiler(
|
||||
private val stableObjSlots = mutableSetOf<Int>()
|
||||
private val nameObjClass = knownNameObjClass.toMutableMap()
|
||||
private val listElementClassBySlot = mutableMapOf<Int, ObjClass>()
|
||||
private val knownClassNames = knownNameObjClass.keys.toSet()
|
||||
private val slotInitClassByKey = mutableMapOf<ScopeSlotKey, ObjClass>()
|
||||
private val intLoopVarNames = LinkedHashSet<String>()
|
||||
private val valueFnRefs = LinkedHashSet<ValueFnRef>()
|
||||
@ -522,7 +522,7 @@ class BytecodeCompiler(
|
||||
}
|
||||
}
|
||||
if (resolved == SlotType.UNKNOWN) {
|
||||
val inferred = if (knownClassNames.contains(ref.name)) null else slotTypeFromClass(nameObjClass[ref.name])
|
||||
val inferred = slotTypeFromClass(nameObjClass[ref.name])
|
||||
if (inferred != null) {
|
||||
updateSlotType(mapped, inferred)
|
||||
resolved = inferred
|
||||
@ -1638,7 +1638,7 @@ class BytecodeCompiler(
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
//else -> null
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@ -3268,10 +3268,9 @@ class BytecodeCompiler(
|
||||
if (ref.name.isBlank()) {
|
||||
return compileRefWithFallback(ref.target, null, Pos.builtIn)
|
||||
}
|
||||
val pos = callSitePos()
|
||||
val receiverClass = resolveReceiverClass(ref.target) ?: ObjDynamic.type
|
||||
if (receiverClass == ObjDynamic.type) {
|
||||
val receiver = compileRefWithFallback(ref.target, null, pos) ?: return null
|
||||
val receiver = compileRefWithFallback(ref.target, null, Pos.builtIn) ?: return null
|
||||
val dst = allocSlot()
|
||||
val nameId = builder.addConst(BytecodeConst.StringVal(ref.name))
|
||||
if (!ref.isOptional) {
|
||||
@ -3297,7 +3296,7 @@ class BytecodeCompiler(
|
||||
return CompiledValue(dst, SlotType.OBJ)
|
||||
}
|
||||
if (receiverClass is ObjInstanceClass && !isThisReceiver(ref.target)) {
|
||||
val receiver = compileRefWithFallback(ref.target, null, pos) ?: return null
|
||||
val receiver = compileRefWithFallback(ref.target, null, Pos.builtIn) ?: return null
|
||||
val dst = allocSlot()
|
||||
val nameId = builder.addConst(BytecodeConst.StringVal(ref.name))
|
||||
if (!ref.isOptional) {
|
||||
@ -3324,7 +3323,7 @@ class BytecodeCompiler(
|
||||
}
|
||||
val resolvedMember = receiverClass.resolveInstanceMember(ref.name)
|
||||
if (resolvedMember?.declaringClass?.className == "Obj") {
|
||||
val receiver = compileRefWithFallback(ref.target, null, pos) ?: return null
|
||||
val receiver = compileRefWithFallback(ref.target, null, Pos.builtIn) ?: return null
|
||||
val dst = allocSlot()
|
||||
val nameId = builder.addConst(BytecodeConst.StringVal(ref.name))
|
||||
if (!ref.isOptional) {
|
||||
@ -3353,7 +3352,7 @@ class BytecodeCompiler(
|
||||
val methodId = if (resolvedMember != null) receiverClass.instanceMethodIdMap(includeAbstract = true)[ref.name] else null
|
||||
val encodedFieldId = encodeMemberId(receiverClass, fieldId)
|
||||
val encodedMethodId = encodeMemberId(receiverClass, methodId)
|
||||
val receiver = compileRefWithFallback(ref.target, null, pos) ?: return null
|
||||
val receiver = compileRefWithFallback(ref.target, null, Pos.builtIn) ?: return null
|
||||
val dst = allocSlot()
|
||||
if (fieldId == null && methodId == null && (isKnownClassReceiver(ref.target) || isClassNameRef(ref.target, receiverClass)) &&
|
||||
(isClassSlot(receiver.slot) || receiverClass == ObjClassType)
|
||||
@ -5091,7 +5090,9 @@ class BytecodeCompiler(
|
||||
lifted = stmt.lifted
|
||||
)
|
||||
)
|
||||
val dst = resolveDirectNameSlot(stmt.declaredName)?.slot ?: allocSlot()
|
||||
val dst = stmt.declaredName?.let { name ->
|
||||
resolveDirectNameSlot(name)?.slot
|
||||
} ?: allocSlot()
|
||||
builder.emit(Opcode.DECL_ENUM, constId, dst)
|
||||
updateSlotType(dst, SlotType.OBJ)
|
||||
return CompiledValue(dst, SlotType.OBJ)
|
||||
@ -5356,6 +5357,7 @@ class BytecodeCompiler(
|
||||
is net.sergeych.lyng.ContinueStatement -> compileContinue(target)
|
||||
is net.sergeych.lyng.ReturnStatement -> compileReturn(target)
|
||||
is net.sergeych.lyng.ThrowStatement -> compileThrow(target)
|
||||
is net.sergeych.lyng.TryStatement -> emitTry(target, false)
|
||||
is net.sergeych.lyng.NopStatement -> {
|
||||
val slot = allocSlot()
|
||||
val voidId = builder.addConst(BytecodeConst.ObjRef(ObjVoid))
|
||||
@ -5731,8 +5733,7 @@ class BytecodeCompiler(
|
||||
stmt.name,
|
||||
stmt.isMutable,
|
||||
stmt.visibility,
|
||||
stmt.isTransient,
|
||||
stmt.typeDecl
|
||||
stmt.isTransient
|
||||
)
|
||||
)
|
||||
builder.emit(Opcode.DECL_LOCAL, declId, localSlot)
|
||||
@ -5759,8 +5760,7 @@ class BytecodeCompiler(
|
||||
stmt.name,
|
||||
stmt.isMutable,
|
||||
stmt.visibility,
|
||||
stmt.isTransient,
|
||||
stmt.typeDecl
|
||||
stmt.isTransient
|
||||
)
|
||||
)
|
||||
builder.emit(Opcode.DECL_LOCAL, declId, scopeSlot)
|
||||
@ -5778,8 +5778,7 @@ class BytecodeCompiler(
|
||||
stmt.name,
|
||||
stmt.isMutable,
|
||||
stmt.visibility,
|
||||
stmt.isTransient,
|
||||
stmt.typeDecl
|
||||
stmt.isTransient
|
||||
)
|
||||
)
|
||||
builder.emit(Opcode.DECL_LOCAL, declId, value.slot)
|
||||
@ -6948,6 +6947,7 @@ class BytecodeCompiler(
|
||||
is LocalVarRef -> ref.name
|
||||
is FastLocalVarRef -> ref.name
|
||||
is LocalSlotRef -> ref.name
|
||||
else -> "unknown"
|
||||
}
|
||||
val refKind = ref::class.simpleName ?: "LocalRef"
|
||||
val loopKeys = loopSlotOverrides.keys.sorted().joinToString(prefix = "[", postfix = "]")
|
||||
@ -7695,28 +7695,14 @@ class BytecodeCompiler(
|
||||
for (ref in valueFnRefs) {
|
||||
val entries = lambdaCaptureEntriesByRef[ref] ?: continue
|
||||
for (entry in entries) {
|
||||
if (entry.ownerKind == CaptureOwnerFrameKind.LOCAL) {
|
||||
val key = ScopeSlotKey(entry.ownerScopeId, entry.ownerSlotId)
|
||||
if (!localSlotInfoMap.containsKey(key)) {
|
||||
localSlotInfoMap[key] = LocalSlotInfo(
|
||||
entry.ownerName,
|
||||
entry.ownerIsMutable,
|
||||
entry.ownerIsDelegated
|
||||
)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (entry.ownerKind == CaptureOwnerFrameKind.MODULE) {
|
||||
val key = ScopeSlotKey(entry.ownerScopeId, entry.ownerSlotId)
|
||||
if (!scopeSlotMap.containsKey(key)) {
|
||||
scopeSlotMap[key] = scopeSlotMap.size
|
||||
}
|
||||
if (!scopeSlotNameMap.containsKey(key)) {
|
||||
scopeSlotNameMap[key] = entry.ownerName
|
||||
}
|
||||
if (!scopeSlotMutableMap.containsKey(key)) {
|
||||
scopeSlotMutableMap[key] = entry.ownerIsMutable
|
||||
}
|
||||
if (entry.ownerKind != CaptureOwnerFrameKind.LOCAL) continue
|
||||
val key = ScopeSlotKey(entry.ownerScopeId, entry.ownerSlotId)
|
||||
if (!localSlotInfoMap.containsKey(key)) {
|
||||
localSlotInfoMap[key] = LocalSlotInfo(
|
||||
entry.ownerName,
|
||||
entry.ownerIsMutable,
|
||||
entry.ownerIsDelegated
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -8185,8 +8171,7 @@ class BytecodeCompiler(
|
||||
}
|
||||
|
||||
private fun isModuleSlot(scopeId: Int, name: String?): Boolean {
|
||||
if (moduleScopeId != null && scopeId != moduleScopeId) return false
|
||||
val scopeNames = allowedScopeNames ?: scopeSlotNameSet
|
||||
val scopeNames = scopeSlotNameSet ?: allowedScopeNames
|
||||
if (scopeNames == null || name == null) return false
|
||||
return scopeNames.contains(name)
|
||||
}
|
||||
@ -8460,6 +8445,9 @@ class BytecodeCompiler(
|
||||
is StatementRef -> {
|
||||
collectScopeSlots(ref.statement)
|
||||
}
|
||||
is ImplicitThisMethodCallRef -> {
|
||||
collectScopeSlotsArgs(ref.arguments())
|
||||
}
|
||||
is ThisMethodSlotCallRef -> {
|
||||
collectScopeSlotsArgs(ref.arguments())
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* Copyright 2026 Sergey S. Chernov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -12,14 +12,12 @@
|
||||
* 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.bytecode
|
||||
|
||||
import net.sergeych.lyng.ArgsDeclaration
|
||||
import net.sergeych.lyng.Pos
|
||||
import net.sergeych.lyng.TypeDecl
|
||||
import net.sergeych.lyng.Visibility
|
||||
import net.sergeych.lyng.obj.ListLiteralRef
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
@ -70,7 +68,6 @@ sealed class BytecodeConst {
|
||||
val isMutable: Boolean,
|
||||
val visibility: Visibility,
|
||||
val isTransient: Boolean,
|
||||
val typeDecl: TypeDecl?,
|
||||
) : BytecodeConst()
|
||||
data class DelegatedDecl(
|
||||
val name: String,
|
||||
|
||||
@ -34,17 +34,6 @@ class BytecodeFrame(
|
||||
private val realSlots: DoubleArray = DoubleArray(slotCount)
|
||||
private val boolSlots: BooleanArray = BooleanArray(slotCount)
|
||||
|
||||
internal fun copyTo(target: BytecodeFrame) {
|
||||
val limit = minOf(slotCount, target.slotCount)
|
||||
for (i in 0 until limit) {
|
||||
target.slotTypes[i] = slotTypes[i]
|
||||
target.objSlots[i] = objSlots[i]
|
||||
target.intSlots[i] = intSlots[i]
|
||||
target.realSlots[i] = realSlots[i]
|
||||
target.boolSlots[i] = boolSlots[i]
|
||||
}
|
||||
}
|
||||
|
||||
fun getSlotType(slot: Int): SlotType = SlotType.values().first { it.code == slotTypes[slot] }
|
||||
override fun getSlotTypeCode(slot: Int): Byte = slotTypes[slot]
|
||||
fun setSlotType(slot: Int, type: SlotType) {
|
||||
|
||||
@ -20,7 +20,6 @@ package net.sergeych.lyng.bytecode
|
||||
import net.sergeych.lyng.*
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjClass
|
||||
import net.sergeych.lyng.obj.ObjRecord
|
||||
import net.sergeych.lyng.obj.ValueFnRef
|
||||
|
||||
class BytecodeStatement private constructor(
|
||||
@ -31,34 +30,7 @@ class BytecodeStatement private constructor(
|
||||
|
||||
override suspend fun execute(scope: Scope): Obj {
|
||||
scope.pos = pos
|
||||
val declaredNames = function.constants
|
||||
.mapNotNull { it as? BytecodeConst.LocalDecl }
|
||||
.mapTo(mutableSetOf()) { it.name }
|
||||
val binder: suspend (CmdFrame, Arguments) -> Unit = { frame, _ ->
|
||||
val localNames = frame.fn.localSlotNames
|
||||
for (i in localNames.indices) {
|
||||
val name = localNames[i] ?: continue
|
||||
if (declaredNames.contains(name)) continue
|
||||
val slotType = frame.getLocalSlotTypeCode(i)
|
||||
if (slotType != SlotType.UNKNOWN.code && slotType != SlotType.OBJ.code) {
|
||||
continue
|
||||
}
|
||||
if (slotType == SlotType.OBJ.code && frame.frame.getRawObj(i) != null) {
|
||||
continue
|
||||
}
|
||||
val record = scope.getLocalRecordDirect(name)
|
||||
?: scope.parent?.get(name)
|
||||
?: scope.get(name)
|
||||
?: continue
|
||||
val value = if (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property) {
|
||||
scope.resolve(record, name)
|
||||
} else {
|
||||
record.value
|
||||
}
|
||||
frame.frame.setObj(i, value)
|
||||
}
|
||||
}
|
||||
return CmdVm().execute(function, scope, scope.args, binder)
|
||||
return CmdVm().execute(function, scope, scope.args)
|
||||
}
|
||||
|
||||
internal fun bytecodeFunction(): CmdFunction = function
|
||||
@ -80,7 +52,6 @@ class BytecodeStatement private constructor(
|
||||
globalSlotScopeId: Int? = null,
|
||||
slotTypeByScopeId: Map<Int, Map<Int, ObjClass>> = emptyMap(),
|
||||
knownNameObjClass: Map<String, ObjClass> = emptyMap(),
|
||||
knownClassNames: Set<String> = emptySet(),
|
||||
knownObjectNames: Set<String> = emptySet(),
|
||||
classFieldTypesByName: Map<String, Map<String, ObjClass>> = emptyMap(),
|
||||
enumEntriesByName: Map<String, List<String>> = emptyMap(),
|
||||
@ -115,7 +86,6 @@ class BytecodeStatement private constructor(
|
||||
slotTypeByScopeId = slotTypeByScopeId,
|
||||
slotTypeDeclByScopeId = slotTypeDeclByScopeId,
|
||||
knownNameObjClass = knownNameObjClass,
|
||||
knownClassNames = knownClassNames,
|
||||
knownObjectNames = knownObjectNames,
|
||||
classFieldTypesByName = classFieldTypesByName,
|
||||
enumEntriesByName = enumEntriesByName,
|
||||
@ -238,7 +208,6 @@ class BytecodeStatement private constructor(
|
||||
stmt.visibility,
|
||||
stmt.initializer?.let { unwrapDeep(it) },
|
||||
stmt.isTransient,
|
||||
stmt.typeDecl,
|
||||
stmt.slotIndex,
|
||||
stmt.scopeId,
|
||||
stmt.pos,
|
||||
|
||||
@ -502,6 +502,7 @@ object CmdDisassembler {
|
||||
is CmdIterPush -> Opcode.ITER_PUSH to intArrayOf(cmd.iterSlot)
|
||||
is CmdIterPop -> Opcode.ITER_POP to intArrayOf()
|
||||
is CmdIterCancel -> Opcode.ITER_CANCEL to intArrayOf()
|
||||
else -> error("Unsupported cmd in disassembler: ${cmd::class.simpleName}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -229,29 +229,8 @@ class CmdLoadThisVariant(
|
||||
val typeConst = frame.fn.constants.getOrNull(typeId) as? BytecodeConst.StringVal
|
||||
?: error("LOAD_THIS_VARIANT expects StringVal at $typeId")
|
||||
val typeName = typeConst.value
|
||||
val scope = frame.ensureScope()
|
||||
if (scope.thisVariants.isEmpty() || scope.thisVariants.firstOrNull() !== scope.thisObj) {
|
||||
scope.setThisVariants(scope.thisObj, scope.thisVariants)
|
||||
}
|
||||
val receiver = scope.thisVariants.firstOrNull { it.isInstanceOf(typeName) }
|
||||
?: run {
|
||||
if (scope.thisObj.isInstanceOf(typeName)) return@run scope.thisObj
|
||||
val typeClass = scope[typeName]?.value as? net.sergeych.lyng.obj.ObjClass
|
||||
var s: Scope? = scope
|
||||
while (s != null) {
|
||||
val candidate = s.thisObj
|
||||
if (candidate.isInstanceOf(typeName)) return@run candidate
|
||||
if (typeClass != null) {
|
||||
val inst = candidate as? net.sergeych.lyng.obj.ObjInstance
|
||||
if (inst != null && (inst.objClass === typeClass || inst.objClass.allParentsSet.contains(typeClass))) {
|
||||
return@run inst
|
||||
}
|
||||
}
|
||||
s = s.parent
|
||||
}
|
||||
val variants = scope.thisVariants.joinToString { it.objClass.className }
|
||||
scope.raiseClassCastError("Cannot cast ${scope.thisObj.objClass.className} to $typeName (variants: $variants)")
|
||||
}
|
||||
val receiver = frame.ensureScope().thisVariants.firstOrNull { it.isInstanceOf(typeName) }
|
||||
?: frame.ensureScope().raiseClassCastError("Cannot cast ${frame.ensureScope().thisObj.objClass.className} to $typeName")
|
||||
frame.setObj(dst, receiver)
|
||||
return
|
||||
}
|
||||
@ -2401,8 +2380,7 @@ class CmdDeclLocal(internal val constId: Int, internal val slot: Int) : Cmd() {
|
||||
decl.isMutable,
|
||||
decl.visibility,
|
||||
isTransient = decl.isTransient,
|
||||
type = ObjRecord.Type.Other,
|
||||
typeDecl = decl.typeDecl
|
||||
type = ObjRecord.Type.Other
|
||||
)
|
||||
)
|
||||
return
|
||||
@ -2414,15 +2392,16 @@ class CmdDeclLocal(internal val constId: Int, internal val slot: Int) : Cmd() {
|
||||
decl.isMutable,
|
||||
decl.visibility,
|
||||
isTransient = decl.isTransient,
|
||||
type = ObjRecord.Type.Other,
|
||||
typeDecl = decl.typeDecl
|
||||
type = ObjRecord.Type.Other
|
||||
)
|
||||
val moduleScope = frame.scope as? ModuleScope
|
||||
if (moduleScope != null) {
|
||||
moduleScope.updateSlotFor(decl.name, record)
|
||||
moduleScope.objects[decl.name] = record
|
||||
moduleScope.localBindings[decl.name] = record
|
||||
} else if (frame.fn.name == "<script>") {
|
||||
val target = frame.ensureScope()
|
||||
target.updateSlotFor(decl.name, record)
|
||||
target.objects[decl.name] = record
|
||||
target.localBindings[decl.name] = record
|
||||
}
|
||||
@ -2559,12 +2538,6 @@ private fun buildFunctionCaptureRecords(frame: CmdFrame, captureNames: List<Stri
|
||||
it.delegate = delegate
|
||||
}
|
||||
} else {
|
||||
val raw = frame.frame.getRawObj(localIndex)
|
||||
val scoped = frame.scope.chainLookupIgnoreClosure(name, followClosure = true) ?: frame.scope.get(name)
|
||||
if (scoped != null && scoped.value !== ObjUnset) {
|
||||
records += scoped
|
||||
continue
|
||||
}
|
||||
records += ObjRecord(FrameSlotRef(frame.frame, localIndex), isMutable)
|
||||
}
|
||||
continue
|
||||
@ -2588,11 +2561,6 @@ private fun buildFunctionCaptureRecords(frame: CmdFrame, captureNames: List<Stri
|
||||
}
|
||||
}
|
||||
}
|
||||
val scoped = frame.scope.chainLookupIgnoreClosure(name, followClosure = true) ?: frame.scope.get(name)
|
||||
if (scoped != null) {
|
||||
records += ObjRecord(RecordSlotRef(scoped), isMutable = scoped.isMutable)
|
||||
continue
|
||||
}
|
||||
frame.ensureScope().raiseSymbolNotFound("capture $name not found")
|
||||
}
|
||||
return records
|
||||
@ -2609,17 +2577,6 @@ class CmdDeclClass(internal val constId: Int, internal val slot: Int) : Cmd() {
|
||||
val bodyCaptureRecords = buildFunctionCaptureRecords(frame, bodyCaptureNames)
|
||||
val result = executeClassDecl(frame.ensureScope(), decl.spec, bodyCaptureRecords, bodyCaptureNames)
|
||||
frame.setObjUnchecked(slot, result)
|
||||
val name = decl.spec.declaredName ?: return
|
||||
val moduleScope = frame.scope as? ModuleScope ?: return
|
||||
val record = ObjRecord(
|
||||
result,
|
||||
isMutable = false,
|
||||
visibility = net.sergeych.lyng.Visibility.Public,
|
||||
type = ObjRecord.Type.Other
|
||||
)
|
||||
moduleScope.updateSlotFor(name, record)
|
||||
moduleScope.objects[name] = record
|
||||
moduleScope.localBindings[name] = record
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -3270,7 +3227,6 @@ class CmdGetMemberSlot(
|
||||
internal val dst: Int,
|
||||
) : Cmd() {
|
||||
override suspend fun perform(frame: CmdFrame) {
|
||||
val scope = frame.ensureScope()
|
||||
val receiver = frame.slotToObj(recvSlot)
|
||||
val inst = receiver as? ObjInstance
|
||||
val cls = receiver as? ObjClass
|
||||
@ -3293,26 +3249,7 @@ class CmdGetMemberSlot(
|
||||
else -> receiver.objClass.methodRecordForId(methodIdResolved)
|
||||
}
|
||||
} else null
|
||||
} ?: run {
|
||||
val receiverClass = when {
|
||||
cls != null && fieldOnObjClass -> cls.objClass
|
||||
cls != null -> cls
|
||||
else -> receiver.objClass
|
||||
}
|
||||
val fieldName = if (fieldIdResolved >= 0) {
|
||||
receiverClass.fieldSlotMap().entries.firstOrNull { it.value.slot == fieldIdResolved }?.key
|
||||
} else null
|
||||
val methodName = if (methodIdResolved >= 0) {
|
||||
receiverClass.methodSlotMap().entries.firstOrNull { it.value.slot == methodIdResolved }?.key
|
||||
} else null
|
||||
val memberName = fieldName ?: methodName
|
||||
val message = if (memberName != null) {
|
||||
"no such member: $memberName on ${receiverClass.className}"
|
||||
} else {
|
||||
"no such member slot (fieldId=$fieldIdResolved, methodId=$methodIdResolved) on ${receiverClass.className}"
|
||||
}
|
||||
scope.raiseError(message)
|
||||
}
|
||||
} ?: frame.ensureScope().raiseSymbolNotFound("member")
|
||||
val rawName = rec.memberName ?: "<member>"
|
||||
val name = if (receiver is ObjInstance && rawName.contains("::")) {
|
||||
rawName.substringAfterLast("::")
|
||||
@ -3344,7 +3281,6 @@ class CmdSetMemberSlot(
|
||||
internal val valueSlot: Int,
|
||||
) : Cmd() {
|
||||
override suspend fun perform(frame: CmdFrame) {
|
||||
val scope = frame.ensureScope()
|
||||
val receiver = frame.slotToObj(recvSlot)
|
||||
val inst = receiver as? ObjInstance
|
||||
val cls = receiver as? ObjClass
|
||||
@ -3367,26 +3303,7 @@ class CmdSetMemberSlot(
|
||||
else -> receiver.objClass.methodRecordForId(methodIdResolved)
|
||||
}
|
||||
} else null
|
||||
} ?: run {
|
||||
val receiverClass = when {
|
||||
cls != null && fieldOnObjClass -> cls.objClass
|
||||
cls != null -> cls
|
||||
else -> receiver.objClass
|
||||
}
|
||||
val fieldName = if (fieldIdResolved >= 0) {
|
||||
receiverClass.fieldSlotMap().entries.firstOrNull { it.value.slot == fieldIdResolved }?.key
|
||||
} else null
|
||||
val methodName = if (methodIdResolved >= 0) {
|
||||
receiverClass.methodSlotMap().entries.firstOrNull { it.value.slot == methodIdResolved }?.key
|
||||
} else null
|
||||
val memberName = fieldName ?: methodName
|
||||
val message = if (memberName != null) {
|
||||
"no such member: $memberName on ${receiverClass.className}"
|
||||
} else {
|
||||
"no such member slot (fieldId=$fieldIdResolved, methodId=$methodIdResolved) on ${receiverClass.className}"
|
||||
}
|
||||
scope.raiseError(message)
|
||||
}
|
||||
} ?: frame.ensureScope().raiseSymbolNotFound("member")
|
||||
val rawName = rec.memberName ?: "<member>"
|
||||
val name = if (receiver is ObjInstance && rawName.contains("::")) {
|
||||
rawName.substringAfterLast("::")
|
||||
@ -3649,20 +3566,6 @@ class BytecodeLambdaCallable(
|
||||
private val returnLabels: Set<String>,
|
||||
override val pos: Pos,
|
||||
) : Statement(), BytecodeCallable {
|
||||
fun rebindClosure(newClosureScope: Scope): BytecodeLambdaCallable {
|
||||
return BytecodeLambdaCallable(
|
||||
fn = fn,
|
||||
closureScope = newClosureScope,
|
||||
captureRecords = captureRecords,
|
||||
captureNames = captureNames,
|
||||
paramSlotPlan = paramSlotPlan,
|
||||
argsDeclaration = argsDeclaration,
|
||||
preferredThisType = preferredThisType,
|
||||
returnLabels = returnLabels,
|
||||
pos = pos
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun execute(scope: Scope): Obj {
|
||||
val context = scope.applyClosureForBytecode(closureScope, preferredThisType).also {
|
||||
it.args = scope.args
|
||||
@ -3810,7 +3713,6 @@ class CmdFrame(
|
||||
}
|
||||
|
||||
internal fun getLocalSlotTypeCode(localIndex: Int): Byte = frame.getSlotTypeCode(localIndex)
|
||||
internal fun readLocalObj(localIndex: Int): Obj = localSlotToObj(localIndex)
|
||||
internal fun isFastLocalSlot(slot: Int): Boolean {
|
||||
if (slot < fn.scopeSlotCount) return false
|
||||
val localIndex = slot - fn.scopeSlotCount
|
||||
@ -3946,26 +3848,10 @@ class CmdFrame(
|
||||
}
|
||||
}
|
||||
CaptureOwnerFrameKind.MODULE -> {
|
||||
val slotId = entry.slotIndex
|
||||
val target = moduleScope
|
||||
val name = captureNames?.getOrNull(index)
|
||||
if (name != null) {
|
||||
target.tryGetLocalRecord(target, name, target.currentClassCtx)?.let { return@mapIndexed it }
|
||||
target.getSlotIndexOf(name)?.let { return@mapIndexed target.getSlotRecord(it) }
|
||||
target.get(name)?.let { return@mapIndexed it }
|
||||
// Fallback to current scope in case the module scope isn't in the parent chain
|
||||
// or doesn't carry the imported symbol yet.
|
||||
scope.tryGetLocalRecord(scope, name, scope.currentClassCtx)?.let { return@mapIndexed it }
|
||||
scope.get(name)?.let { return@mapIndexed it }
|
||||
}
|
||||
if (slotId < target.slotCount) {
|
||||
return@mapIndexed target.getSlotRecord(slotId)
|
||||
}
|
||||
if (name != null) {
|
||||
target.applySlotPlan(mapOf(name to slotId))
|
||||
return@mapIndexed target.getSlotRecord(slotId)
|
||||
}
|
||||
error("Missing module capture slot $slotId")
|
||||
val slot = entry.slotIndex
|
||||
val target = scopeTarget(slot)
|
||||
val index = fn.scopeSlotIndices[slot]
|
||||
target.getSlotRecord(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3989,15 +3875,10 @@ class CmdFrame(
|
||||
.firstOrNull { fn.scopeSlotIsModule.getOrNull(it) == true }
|
||||
?.let { fn.scopeSlotNames[it] }
|
||||
if (moduleSlotName != null) {
|
||||
findModuleScope(scope)?.let { return it }
|
||||
val bySlot = findScopeWithSlot(scope, moduleSlotName)
|
||||
if (bySlot is ModuleScope) return bySlot
|
||||
val bySlotParent = bySlot?.parent
|
||||
if (bySlotParent is ModuleScope) return bySlotParent
|
||||
bySlot?.let { return it }
|
||||
val byRecord = findScopeWithRecord(scope, moduleSlotName)
|
||||
if (byRecord is ModuleScope) return byRecord
|
||||
val byRecordParent = byRecord?.parent
|
||||
if (byRecordParent is ModuleScope) return byRecordParent
|
||||
byRecord?.let { return it }
|
||||
return scope
|
||||
}
|
||||
findModuleScope(scope)?.let { return it }
|
||||
@ -4048,7 +3929,7 @@ class CmdFrame(
|
||||
val current = queue.removeFirst()
|
||||
if (!visited.add(current)) continue
|
||||
if (current is ModuleScope) return current
|
||||
if (current.parent is ModuleScope) return current.parent
|
||||
if (current.parent is ModuleScope) return current
|
||||
current.parent?.let { queue.add(it) }
|
||||
if (current is BytecodeClosureScope) {
|
||||
queue.add(current.closureScope)
|
||||
@ -4231,18 +4112,16 @@ class CmdFrame(
|
||||
} else {
|
||||
val localIndex = slot - fn.scopeSlotCount
|
||||
ensureLocalMutable(localIndex)
|
||||
if (shouldWriteThroughLocal(localIndex)) {
|
||||
when (val existing = frame.getRawObj(localIndex)) {
|
||||
is FrameSlotRef -> {
|
||||
existing.write(value)
|
||||
return
|
||||
}
|
||||
is RecordSlotRef -> {
|
||||
existing.write(value)
|
||||
return
|
||||
}
|
||||
else -> {}
|
||||
when (val existing = frame.getRawObj(localIndex)) {
|
||||
is FrameSlotRef -> {
|
||||
existing.write(value)
|
||||
return
|
||||
}
|
||||
is RecordSlotRef -> {
|
||||
existing.write(value)
|
||||
return
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
frame.setObj(localIndex, value)
|
||||
}
|
||||
@ -4289,18 +4168,16 @@ class CmdFrame(
|
||||
target.setSlotValue(index, ObjInt.of(value))
|
||||
} else {
|
||||
val localIndex = slot - fn.scopeSlotCount
|
||||
if (shouldWriteThroughLocal(localIndex)) {
|
||||
when (val existing = frame.getRawObj(localIndex)) {
|
||||
is FrameSlotRef -> {
|
||||
existing.write(ObjInt.of(value))
|
||||
return
|
||||
}
|
||||
is RecordSlotRef -> {
|
||||
existing.write(ObjInt.of(value))
|
||||
return
|
||||
}
|
||||
else -> {}
|
||||
when (val existing = frame.getRawObj(localIndex)) {
|
||||
is FrameSlotRef -> {
|
||||
existing.write(ObjInt.of(value))
|
||||
return
|
||||
}
|
||||
is RecordSlotRef -> {
|
||||
existing.write(ObjInt.of(value))
|
||||
return
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
frame.setInt(localIndex, value)
|
||||
}
|
||||
@ -4319,18 +4196,16 @@ class CmdFrame(
|
||||
} else {
|
||||
val localIndex = slot - fn.scopeSlotCount
|
||||
ensureLocalMutable(localIndex)
|
||||
if (shouldWriteThroughLocal(localIndex)) {
|
||||
when (val existing = frame.getRawObj(localIndex)) {
|
||||
is FrameSlotRef -> {
|
||||
existing.write(ObjInt.of(value))
|
||||
return
|
||||
}
|
||||
is RecordSlotRef -> {
|
||||
existing.write(ObjInt.of(value))
|
||||
return
|
||||
}
|
||||
else -> {}
|
||||
when (val existing = frame.getRawObj(localIndex)) {
|
||||
is FrameSlotRef -> {
|
||||
existing.write(ObjInt.of(value))
|
||||
return
|
||||
}
|
||||
is RecordSlotRef -> {
|
||||
existing.write(ObjInt.of(value))
|
||||
return
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
frame.setInt(localIndex, value)
|
||||
}
|
||||
@ -4379,18 +4254,16 @@ class CmdFrame(
|
||||
} else {
|
||||
val localIndex = slot - fn.scopeSlotCount
|
||||
ensureLocalMutable(localIndex)
|
||||
if (shouldWriteThroughLocal(localIndex)) {
|
||||
when (val existing = frame.getRawObj(localIndex)) {
|
||||
is FrameSlotRef -> {
|
||||
existing.write(ObjReal.of(value))
|
||||
return
|
||||
}
|
||||
is RecordSlotRef -> {
|
||||
existing.write(ObjReal.of(value))
|
||||
return
|
||||
}
|
||||
else -> {}
|
||||
when (val existing = frame.getRawObj(localIndex)) {
|
||||
is FrameSlotRef -> {
|
||||
existing.write(ObjReal.of(value))
|
||||
return
|
||||
}
|
||||
is RecordSlotRef -> {
|
||||
existing.write(ObjReal.of(value))
|
||||
return
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
frame.setReal(localIndex, value)
|
||||
}
|
||||
@ -4403,18 +4276,16 @@ class CmdFrame(
|
||||
target.setSlotValue(index, ObjReal.of(value))
|
||||
} else {
|
||||
val localIndex = slot - fn.scopeSlotCount
|
||||
if (shouldWriteThroughLocal(localIndex)) {
|
||||
when (val existing = frame.getRawObj(localIndex)) {
|
||||
is FrameSlotRef -> {
|
||||
existing.write(ObjReal.of(value))
|
||||
return
|
||||
}
|
||||
is RecordSlotRef -> {
|
||||
existing.write(ObjReal.of(value))
|
||||
return
|
||||
}
|
||||
else -> {}
|
||||
when (val existing = frame.getRawObj(localIndex)) {
|
||||
is FrameSlotRef -> {
|
||||
existing.write(ObjReal.of(value))
|
||||
return
|
||||
}
|
||||
is RecordSlotRef -> {
|
||||
existing.write(ObjReal.of(value))
|
||||
return
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
frame.setReal(localIndex, value)
|
||||
}
|
||||
@ -4457,18 +4328,16 @@ class CmdFrame(
|
||||
} else {
|
||||
val localIndex = slot - fn.scopeSlotCount
|
||||
ensureLocalMutable(localIndex)
|
||||
if (shouldWriteThroughLocal(localIndex)) {
|
||||
when (val existing = frame.getRawObj(localIndex)) {
|
||||
is FrameSlotRef -> {
|
||||
existing.write(if (value) ObjTrue else ObjFalse)
|
||||
return
|
||||
}
|
||||
is RecordSlotRef -> {
|
||||
existing.write(if (value) ObjTrue else ObjFalse)
|
||||
return
|
||||
}
|
||||
else -> {}
|
||||
when (val existing = frame.getRawObj(localIndex)) {
|
||||
is FrameSlotRef -> {
|
||||
existing.write(if (value) ObjTrue else ObjFalse)
|
||||
return
|
||||
}
|
||||
is RecordSlotRef -> {
|
||||
existing.write(if (value) ObjTrue else ObjFalse)
|
||||
return
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
frame.setBool(localIndex, value)
|
||||
}
|
||||
@ -4481,18 +4350,16 @@ class CmdFrame(
|
||||
target.setSlotValue(index, if (value) ObjTrue else ObjFalse)
|
||||
} else {
|
||||
val localIndex = slot - fn.scopeSlotCount
|
||||
if (shouldWriteThroughLocal(localIndex)) {
|
||||
when (val existing = frame.getRawObj(localIndex)) {
|
||||
is FrameSlotRef -> {
|
||||
existing.write(if (value) ObjTrue else ObjFalse)
|
||||
return
|
||||
}
|
||||
is RecordSlotRef -> {
|
||||
existing.write(if (value) ObjTrue else ObjFalse)
|
||||
return
|
||||
}
|
||||
else -> {}
|
||||
when (val existing = frame.getRawObj(localIndex)) {
|
||||
is FrameSlotRef -> {
|
||||
existing.write(if (value) ObjTrue else ObjFalse)
|
||||
return
|
||||
}
|
||||
is RecordSlotRef -> {
|
||||
existing.write(if (value) ObjTrue else ObjFalse)
|
||||
return
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
frame.setBool(localIndex, value)
|
||||
}
|
||||
@ -4581,30 +4448,7 @@ class CmdFrame(
|
||||
val index = ensureScopeSlot(target, slot)
|
||||
target.setSlotValue(index, value)
|
||||
} else {
|
||||
val localIndex = slot - fn.scopeSlotCount
|
||||
if (shouldWriteThroughLocal(localIndex)) {
|
||||
when (val existing = frame.getRawObj(localIndex)) {
|
||||
is FrameSlotRef -> {
|
||||
existing.write(value)
|
||||
return
|
||||
}
|
||||
is RecordSlotRef -> {
|
||||
existing.write(value)
|
||||
return
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
frame.setObj(localIndex, value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldWriteThroughLocal(localIndex: Int): Boolean {
|
||||
if (localIndex < fn.localSlotCaptures.size && fn.localSlotCaptures[localIndex]) return true
|
||||
if (localIndex < fn.localSlotDelegated.size && fn.localSlotDelegated[localIndex]) return true
|
||||
return when (frame.getRawObj(localIndex)) {
|
||||
is FrameSlotRef, is RecordSlotRef -> true
|
||||
else -> false
|
||||
frame.setObj(slot - fn.scopeSlotCount, value)
|
||||
}
|
||||
}
|
||||
|
||||
@ -4745,19 +4589,6 @@ class CmdFrame(
|
||||
if (direct is FrameSlotRef) return direct.read()
|
||||
if (direct is RecordSlotRef) return direct.read()
|
||||
val name = fn.scopeSlotNames[slot]
|
||||
if (name != null && record.memberName != null && record.memberName != name) {
|
||||
val resolved = target.get(name)
|
||||
if (resolved != null) {
|
||||
val resolvedValue = resolved.value
|
||||
if (resolved.type == ObjRecord.Type.Delegated || resolved.type == ObjRecord.Type.Property || resolvedValue is ObjProperty) {
|
||||
return target.resolve(resolved, name)
|
||||
}
|
||||
if (resolvedValue !== ObjUnset) {
|
||||
target.updateSlotFor(name, resolved)
|
||||
}
|
||||
return resolvedValue
|
||||
}
|
||||
}
|
||||
if (name != null && (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || direct is ObjProperty)) {
|
||||
return target.resolve(record, name)
|
||||
}
|
||||
@ -4781,19 +4612,6 @@ class CmdFrame(
|
||||
if (direct is RecordSlotRef) return direct.read()
|
||||
val slotId = addrScopeSlots[addrSlot]
|
||||
val name = fn.scopeSlotNames.getOrNull(slotId)
|
||||
if (name != null && record.memberName != null && record.memberName != name) {
|
||||
val resolved = target.get(name)
|
||||
if (resolved != null) {
|
||||
val resolvedValue = resolved.value
|
||||
if (resolved.type == ObjRecord.Type.Delegated || resolved.type == ObjRecord.Type.Property || resolvedValue is ObjProperty) {
|
||||
return target.resolve(resolved, name)
|
||||
}
|
||||
if (resolvedValue !== ObjUnset) {
|
||||
target.updateSlotFor(name, resolved)
|
||||
}
|
||||
return resolvedValue
|
||||
}
|
||||
}
|
||||
if (name != null && (record.type == ObjRecord.Type.Delegated || record.type == ObjRecord.Type.Property || direct is ObjProperty)) {
|
||||
return target.resolve(record, name)
|
||||
}
|
||||
|
||||
@ -178,7 +178,6 @@ object CompletionEngineLight {
|
||||
is MiniMemberDecl -> node.range
|
||||
else -> return
|
||||
}
|
||||
if (range.start.source != src || range.end.source != src) return
|
||||
val start = src.offsetOf(range.start)
|
||||
val end = src.offsetOf(range.end).coerceAtMost(text.length)
|
||||
|
||||
@ -373,12 +372,9 @@ object CompletionEngineLight {
|
||||
val src = Source("<engine>", text)
|
||||
val provider = LenientImportProvider.create()
|
||||
Compiler.compileWithMini(src, provider, sink)
|
||||
sink.build() ?: MiniScript(MiniRange(src.startPos, src.startPos))
|
||||
sink.build()
|
||||
} catch (_: Throwable) {
|
||||
sink.build() ?: run {
|
||||
val src = Source("<engine>", text)
|
||||
MiniScript(MiniRange(src.startPos, src.startPos))
|
||||
}
|
||||
sink.build()
|
||||
}
|
||||
}
|
||||
|
||||
@ -391,7 +387,6 @@ object CompletionEngineLight {
|
||||
|
||||
// Text helpers
|
||||
private fun prefixAt(text: String, offset: Int): String {
|
||||
if (text.isEmpty()) return ""
|
||||
val off = offset.coerceIn(0, text.length)
|
||||
var i = (off - 1).coerceAtLeast(0)
|
||||
while (i >= 0 && DocLookupUtils.isIdentChar(text[i])) i--
|
||||
|
||||
@ -1163,7 +1163,6 @@ object DocLookupUtils {
|
||||
}
|
||||
|
||||
fun findDotLeft(text: String, offset: Int): Int? {
|
||||
if (text.isEmpty()) return null
|
||||
var i = (offset - 1).coerceAtLeast(0)
|
||||
while (i >= 0 && text[i].isWhitespace()) i--
|
||||
return if (i >= 0 && text[i] == '.') i else null
|
||||
|
||||
@ -256,13 +256,6 @@ open class Obj {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sugar to use it with [ScopeFacade]
|
||||
*/
|
||||
suspend fun toString(facade: ScopeFacade) = toString(facade.requireScope())
|
||||
|
||||
|
||||
|
||||
open suspend fun defaultToString(scope: Scope): ObjString = ObjString(this.toString())
|
||||
|
||||
/**
|
||||
@ -887,7 +880,7 @@ open class Obj {
|
||||
is Enum<*> -> ObjString(obj.name)
|
||||
Unit -> ObjVoid
|
||||
null -> ObjNull
|
||||
is Iterator<*> -> ObjKotlinIterator(obj)
|
||||
is Iterator<*> -> ObjKotlinIterator(obj as Iterator<Any?>)
|
||||
else -> throw IllegalArgumentException("cannot convert to Obj: $obj")
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* Copyright 2025 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.
|
||||
@ -39,7 +39,7 @@ class ObjArrayIterator(val array: Obj) : Obj() {
|
||||
val self = thisAs<ObjArrayIterator>()
|
||||
if (self.nextIndex < self.lastIndex) {
|
||||
self.array.invokeInstanceMethod(requireScope(), "getAt", (self.nextIndex++).toObj())
|
||||
} else raiseIterationFinished()
|
||||
} else raiseError(ObjIterationFinishedException(requireScope()))
|
||||
}
|
||||
addFn("hasNext") {
|
||||
val self = thisAs<ObjArrayIterator>()
|
||||
|
||||
@ -1196,22 +1196,7 @@ open class ObjClass(
|
||||
return JsonObject(result)
|
||||
}
|
||||
|
||||
open suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
|
||||
val meta = constructorMeta
|
||||
?: scope.raiseError("can't deserialize non-serializable object (no constructor meta)")
|
||||
val params = meta.params.filter { !it.isTransient }
|
||||
val values = decoder.decodeAnyList(scope)
|
||||
if (values.size > params.size) {
|
||||
scope.raiseIllegalArgument(
|
||||
"serialized params has bigger size ${values.size} than constructor params (${params.size}): " +
|
||||
values.joinToString(",")
|
||||
)
|
||||
}
|
||||
val instance = callWithArgs(scope, *values.toTypedArray())
|
||||
if (instance is ObjInstance) {
|
||||
instance.deserializeStateVars(scope, decoder)
|
||||
}
|
||||
return instance
|
||||
}
|
||||
open suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
|
||||
scope.raiseNotImplemented()
|
||||
|
||||
}
|
||||
|
||||
@ -17,16 +17,7 @@
|
||||
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import kotlinx.datetime.DateTimeUnit
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.UtcOffset
|
||||
import kotlinx.datetime.asTimeZone
|
||||
import kotlinx.datetime.isoDayNumber
|
||||
import kotlinx.datetime.number
|
||||
import kotlinx.datetime.plus
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import kotlinx.datetime.*
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import net.sergeych.lyng.Scope
|
||||
@ -37,13 +28,12 @@ import net.sergeych.lyng.miniast.type
|
||||
import net.sergeych.lynon.LynonDecoder
|
||||
import net.sergeych.lynon.LynonEncoder
|
||||
import net.sergeych.lynon.LynonType
|
||||
import kotlin.time.Instant
|
||||
|
||||
class ObjDateTime(val instant: Instant, val timeZone: TimeZone) : Obj() {
|
||||
override val objClass: ObjClass get() = type
|
||||
|
||||
val localDateTime: LocalDateTime by lazy {
|
||||
with(timeZone) { instant.toLocalDateTime() }
|
||||
instant.toLocalDateTime(timeZone)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
@ -187,11 +177,11 @@ class ObjDateTime(val instant: Instant, val timeZone: TimeZone) : Obj() {
|
||||
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.month.number.toObj() })
|
||||
getter = { thisAs<ObjDateTime>().localDateTime.monthNumber.toObj() })
|
||||
addPropertyDoc("dayOfMonth", "The day of month component.", type("lyng.Int"), moduleName = "lyng.time",
|
||||
getter = { thisAs<ObjDateTime>().localDateTime.day.toObj() })
|
||||
getter = { thisAs<ObjDateTime>().localDateTime.dayOfMonth.toObj() })
|
||||
addPropertyDoc("day", "Alias to dayOfMonth.", type("lyng.Int"), moduleName = "lyng.time",
|
||||
getter = { thisAs<ObjDateTime>().localDateTime.day.toObj() })
|
||||
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",
|
||||
@ -267,7 +257,7 @@ class ObjDateTime(val instant: Instant, val timeZone: TimeZone) : Obj() {
|
||||
returns = type("lyng.DateTime"),
|
||||
moduleName = "lyng.time") {
|
||||
val s = (args.firstAndOnly() as ObjString).value
|
||||
// kotlin.time's Instant.parse handles RFC3339
|
||||
// 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.
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -19,7 +19,6 @@ package net.sergeych.lyng.obj
|
||||
|
||||
import net.sergeych.lyng.Arguments
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.bytecode.BytecodeLambdaCallable
|
||||
|
||||
class ObjDynamicContext(val delegate: ObjDynamic) : Obj() {
|
||||
override val objClass: ObjClass get() = type
|
||||
@ -30,8 +29,7 @@ class ObjDynamicContext(val delegate: ObjDynamic) : Obj() {
|
||||
val d = thisAs<ObjDynamicContext>().delegate
|
||||
if (d.readCallback != null)
|
||||
raiseIllegalState("get already defined")
|
||||
val callback = requireOnlyArg<Obj>()
|
||||
d.readCallback = d.rebindCallback(requireScope(), callback)
|
||||
d.readCallback = requireOnlyArg()
|
||||
ObjVoid
|
||||
}
|
||||
|
||||
@ -39,8 +37,7 @@ class ObjDynamicContext(val delegate: ObjDynamic) : Obj() {
|
||||
val d = thisAs<ObjDynamicContext>().delegate
|
||||
if (d.writeCallback != null)
|
||||
raiseIllegalState("set already defined")
|
||||
val callback = requireOnlyArg<Obj>()
|
||||
d.writeCallback = d.rebindCallback(requireScope(), callback)
|
||||
d.writeCallback = requireOnlyArg()
|
||||
ObjVoid
|
||||
}
|
||||
|
||||
@ -58,11 +55,6 @@ open class ObjDynamic(var readCallback: Obj? = null, var writeCallback: Obj? = n
|
||||
override val objClass: ObjClass get() = type
|
||||
// Capture the lexical scope used to build this dynamic so callbacks can see outer locals
|
||||
internal var builderScope: Scope? = null
|
||||
internal fun rebindCallback(contextScope: Scope, callback: Obj): Obj {
|
||||
val snapshot = builderScope ?: return callback
|
||||
val context = Scope(snapshot, contextScope.args, contextScope.pos, contextScope.thisObj)
|
||||
return (callback as? BytecodeLambdaCallable)?.rebindClosure(context) ?: callback
|
||||
}
|
||||
|
||||
/**
|
||||
* Use read callback to dynamically resolve the field name. Note that it does not work
|
||||
@ -121,9 +113,8 @@ open class ObjDynamic(var readCallback: Obj? = null, var writeCallback: Obj? = n
|
||||
// Capture the function's lexical scope (scope) so callbacks can see outer locals like parameters.
|
||||
// Build the dynamic in a child scope purely to set `this` to context, but keep captured closure at parent.
|
||||
val buildScope = scope.createChildScope(newThisObj = context)
|
||||
// Snapshot the caller scope to capture locals/args even if the runtime pools/reuses frames.
|
||||
// Module scope should stay late-bound to allow extern class rebinding and similar updates.
|
||||
delegate.builderScope = if (scope is net.sergeych.lyng.ModuleScope) null else scope.snapshotForClosure()
|
||||
// Snapshot the caller scope to capture locals/args even if the runtime pools/reuses frames
|
||||
delegate.builderScope = scope.snapshotForClosure()
|
||||
builder.callOn(buildScope)
|
||||
return delegate
|
||||
}
|
||||
|
||||
@ -17,13 +17,7 @@
|
||||
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.UtcOffset
|
||||
import kotlinx.datetime.asTimeZone
|
||||
import kotlinx.datetime.number
|
||||
import kotlinx.datetime.toInstant
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import kotlinx.datetime.*
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import net.sergeych.lyng.Scope
|
||||
@ -33,7 +27,6 @@ import net.sergeych.lynon.LynonEncoder
|
||||
import net.sergeych.lynon.LynonSettings
|
||||
import net.sergeych.lynon.LynonType
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
import kotlin.time.isDistantFuture
|
||||
import kotlin.time.isDistantPast
|
||||
|
||||
@ -203,8 +196,8 @@ class ObjInstant(val instant: Instant,val truncateMode: LynonSettings.InstantTru
|
||||
) {
|
||||
val t = thisAs<ObjInstant>().instant
|
||||
val tz = TimeZone.UTC
|
||||
val dt = with(tz) { t.toLocalDateTime() }
|
||||
val truncated = LocalDateTime(dt.year, dt.month.number, dt.day, dt.hour, dt.minute, 0, 0)
|
||||
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(
|
||||
|
||||
@ -166,7 +166,7 @@ class ObjInt(val value: Long, override val isConst: Boolean = false) : Obj(), Nu
|
||||
internal const val CACHE_LOW: Long = -1024L
|
||||
internal const val CACHE_HIGH: Long = 1023L
|
||||
private val cache = Array((CACHE_HIGH - CACHE_LOW + 1).toInt()) {
|
||||
ObjInt(it + CACHE_LOW, true)
|
||||
ObjInt((it + CACHE_LOW).toLong(), true)
|
||||
}
|
||||
|
||||
fun of(value: Long): ObjInt {
|
||||
|
||||
@ -22,7 +22,6 @@ import net.sergeych.lyng.miniast.TypeGenericDoc
|
||||
import net.sergeych.lyng.miniast.addFnDoc
|
||||
import net.sergeych.lyng.miniast.addPropertyDoc
|
||||
import net.sergeych.lyng.miniast.type
|
||||
import net.sergeych.lyng.requireScope
|
||||
|
||||
class ObjRange(
|
||||
val start: Obj?,
|
||||
|
||||
@ -39,7 +39,6 @@ data class ObjRecord(
|
||||
/** The receiver object to resolve this member against (for instance fields/methods). */
|
||||
var receiver: Obj? = null,
|
||||
val callSignature: net.sergeych.lyng.CallSignature? = null,
|
||||
val typeDecl: net.sergeych.lyng.TypeDecl? = null,
|
||||
val memberName: String? = null,
|
||||
val fieldId: Int? = null,
|
||||
val methodId: Int? = null,
|
||||
|
||||
@ -17,12 +17,14 @@
|
||||
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import net.sergeych.lyng.FrameSlotRef
|
||||
import net.sergeych.lyng.PerfFlags
|
||||
import net.sergeych.lyng.Pos
|
||||
import net.sergeych.lyng.RecordSlotRef
|
||||
import net.sergeych.lyng.RegexCache
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.miniast.*
|
||||
import net.sergeych.lyng.requireScope
|
||||
import net.sergeych.lyng.miniast.*
|
||||
|
||||
class ObjRegex(val regex: Regex) : Obj() {
|
||||
override val objClass get() = type
|
||||
@ -31,10 +33,18 @@ class ObjRegex(val regex: Regex) : Obj() {
|
||||
return regex.find(other.cast<ObjString>(scope).value)?.let {
|
||||
val match = ObjRegexMatch(it)
|
||||
val record = scope.chainLookupIgnoreClosure("$~", followClosure = true)
|
||||
if (record != null && !record.isMutable) {
|
||||
scope.raiseIllegalAssignment("symbol is readonly: $~")
|
||||
if (record != null) {
|
||||
if (!record.isMutable) {
|
||||
scope.raiseIllegalAssignment("symbol is readonly: $~")
|
||||
}
|
||||
when (val value = record.value) {
|
||||
is FrameSlotRef -> value.write(match)
|
||||
is RecordSlotRef -> value.write(match)
|
||||
else -> record.value = match
|
||||
}
|
||||
} else {
|
||||
scope.addOrUpdateItem("$~", match)
|
||||
}
|
||||
scope.addOrUpdateItem("$~", match)
|
||||
ObjTrue
|
||||
} ?: ObjFalse
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* Copyright 2026 Sergey S. Chernov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -12,7 +12,6 @@
|
||||
* 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
|
||||
@ -64,7 +63,6 @@ internal fun matchesTypeDecl(scope: Scope, value: Obj, typeDecl: TypeDecl): Bool
|
||||
if (cls != null) value.isInstanceOf(cls) else value.isInstanceOf(typeDecl.name.substringAfterLast('.'))
|
||||
}
|
||||
is TypeDecl.Function -> value.isInstanceOf("Callable")
|
||||
is TypeDecl.Ellipsis -> matchesTypeDecl(scope, value, typeDecl.elementType)
|
||||
is TypeDecl.Union -> typeDecl.options.any { matchesTypeDecl(scope, value, it) }
|
||||
is TypeDecl.Intersection -> typeDecl.options.all { matchesTypeDecl(scope, value, it) }
|
||||
}
|
||||
@ -92,11 +90,10 @@ internal fun typeDeclIsSubtype(scope: Scope, left: TypeDecl, right: TypeDecl): B
|
||||
return when (l) {
|
||||
is TypeDecl.Union -> l.options.all { typeDeclIsSubtype(scope, it, r) }
|
||||
is TypeDecl.Intersection -> l.options.any { typeDeclIsSubtype(scope, it, r) }
|
||||
is TypeDecl.Ellipsis -> typeDeclIsSubtype(scope, l.elementType, r)
|
||||
else -> when (r) {
|
||||
is TypeDecl.Union -> r.options.any { typeDeclIsSubtype(scope, l, it) }
|
||||
is TypeDecl.Intersection -> r.options.all { typeDeclIsSubtype(scope, l, it) }
|
||||
is TypeDecl.Simple, is TypeDecl.Generic, is TypeDecl.Function, is TypeDecl.Ellipsis -> {
|
||||
is TypeDecl.Simple, is TypeDecl.Generic, is TypeDecl.Function -> {
|
||||
val leftClass = resolveTypeDeclClass(scope, l) ?: return false
|
||||
val rightClass = resolveTypeDeclClass(scope, r) ?: return false
|
||||
leftClass == rightClass || leftClass.allParentsSet.contains(rightClass)
|
||||
@ -174,7 +171,6 @@ private fun stripNullable(type: TypeDecl): TypeDecl {
|
||||
} else {
|
||||
when (type) {
|
||||
is TypeDecl.Function -> type.copy(nullable = false)
|
||||
is TypeDecl.Ellipsis -> type.copy(nullable = false)
|
||||
is TypeDecl.TypeVar -> type.copy(nullable = false)
|
||||
is TypeDecl.Union -> type.copy(nullable = false)
|
||||
is TypeDecl.Intersection -> type.copy(nullable = false)
|
||||
@ -190,7 +186,6 @@ private fun makeNullable(type: TypeDecl): TypeDecl {
|
||||
TypeDecl.TypeAny -> TypeDecl.TypeNullableAny
|
||||
TypeDecl.TypeNullableAny -> type
|
||||
is TypeDecl.Function -> type.copy(nullable = true)
|
||||
is TypeDecl.Ellipsis -> type.copy(nullable = true)
|
||||
is TypeDecl.TypeVar -> type.copy(nullable = true)
|
||||
is TypeDecl.Union -> type.copy(nullable = true)
|
||||
is TypeDecl.Intersection -> type.copy(nullable = true)
|
||||
@ -205,7 +200,6 @@ private fun typeDeclKey(type: TypeDecl): String = when (type) {
|
||||
is TypeDecl.Simple -> "S:${type.name}"
|
||||
is TypeDecl.Generic -> "G:${type.name}<${type.args.joinToString(",") { typeDeclKey(it) }}>"
|
||||
is TypeDecl.Function -> "F:(${type.params.joinToString(",") { typeDeclKey(it) }})->${typeDeclKey(type.returnType)}"
|
||||
is TypeDecl.Ellipsis -> "E:${typeDeclKey(type.elementType)}"
|
||||
is TypeDecl.TypeVar -> "V:${type.name}"
|
||||
is TypeDecl.Union -> "U:${type.options.joinToString("|") { typeDeclKey(it) }}"
|
||||
is TypeDecl.Intersection -> "I:${type.options.joinToString("&") { typeDeclKey(it) }}"
|
||||
@ -222,7 +216,6 @@ private fun resolveTypeDeclClass(scope: Scope, type: TypeDecl): ObjClass? {
|
||||
direct ?: scope[type.name.substringAfterLast('.')]?.value as? ObjClass
|
||||
}
|
||||
is TypeDecl.Function -> scope["Callable"]?.value as? ObjClass
|
||||
is TypeDecl.Ellipsis -> resolveTypeDeclClass(scope, type.elementType)
|
||||
is TypeDecl.TypeVar -> {
|
||||
val bound = scope[type.name]?.value
|
||||
when (bound) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* Copyright 2026 Sergey S. Chernov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -12,28 +12,17 @@
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import net.sergeych.lyng.Pos
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.ScopeFacade
|
||||
import net.sergeych.lyng.raiseAssertionFailed as coreRaiseAssertionFailed
|
||||
import net.sergeych.lyng.raiseError as coreRaiseError
|
||||
import net.sergeych.lyng.raiseIllegalAssignment as coreRaiseIllegalAssignment
|
||||
import net.sergeych.lyng.raiseIllegalOperation as coreRaiseIllegalOperation
|
||||
import net.sergeych.lyng.raiseIndexOutOfBounds as coreRaiseIndexOutOfBounds
|
||||
import net.sergeych.lyng.raiseIterationFinished as coreRaiseIterationFinished
|
||||
import net.sergeych.lyng.raiseNPE as coreRaiseNPE
|
||||
import net.sergeych.lyng.raiseNotFound as coreRaiseNotFound
|
||||
import net.sergeych.lyng.raiseUnset as coreRaiseUnset
|
||||
import net.sergeych.lyng.requireExactCount as coreRequireExactCount
|
||||
import net.sergeych.lyng.requireNoArgs as coreRequireNoArgs
|
||||
import net.sergeych.lyng.requireOnlyArg as coreRequireOnlyArg
|
||||
import net.sergeych.lyng.requireScope as coreRequireScope
|
||||
import net.sergeych.lyng.requiredArg as coreRequiredArg
|
||||
import net.sergeych.lyng.requireScope as coreRequireScope
|
||||
import net.sergeych.lyng.thisAs as coreThisAs
|
||||
|
||||
inline fun <reified T : Obj> ScopeFacade.requiredArg(index: Int): T = coreRequiredArg(index)
|
||||
@ -47,29 +36,3 @@ fun ScopeFacade.requireNoArgs() = coreRequireNoArgs()
|
||||
inline fun <reified T : Obj> ScopeFacade.thisAs(): T = coreThisAs()
|
||||
|
||||
internal fun ScopeFacade.requireScope(): Scope = coreRequireScope()
|
||||
|
||||
fun ScopeFacade.raiseNPE(): Nothing = coreRaiseNPE()
|
||||
|
||||
fun ScopeFacade.raiseIndexOutOfBounds(message: String = "Index out of bounds"): Nothing =
|
||||
coreRaiseIndexOutOfBounds(message)
|
||||
|
||||
fun ScopeFacade.raiseIllegalAssignment(message: String): Nothing =
|
||||
coreRaiseIllegalAssignment(message)
|
||||
|
||||
fun ScopeFacade.raiseUnset(message: String = "property is unset (not initialized)"): Nothing =
|
||||
coreRaiseUnset(message)
|
||||
|
||||
fun ScopeFacade.raiseNotFound(message: String = "not found"): Nothing =
|
||||
coreRaiseNotFound(message)
|
||||
|
||||
fun ScopeFacade.raiseError(obj: Obj, pos: Pos = this.pos, message: String): Nothing =
|
||||
coreRaiseError(obj, pos, message)
|
||||
|
||||
fun ScopeFacade.raiseAssertionFailed(message: String): Nothing =
|
||||
coreRaiseAssertionFailed(message)
|
||||
|
||||
fun ScopeFacade.raiseIllegalOperation(message: String = "Operation is illegal"): Nothing =
|
||||
coreRaiseIllegalOperation(message)
|
||||
|
||||
fun ScopeFacade.raiseIterationFinished(): Nothing =
|
||||
coreRaiseIterationFinished()
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* Copyright 2025 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.
|
||||
@ -69,10 +69,10 @@ abstract class ImportProvider(
|
||||
val plan: Map<String, Int>
|
||||
)
|
||||
|
||||
private var cachedStdScopeSeed = CachedExpression<StdScopeSeed>()
|
||||
private var cachedStdScope = CachedExpression<StdScopeSeed>()
|
||||
|
||||
suspend fun newStdScope(pos: Pos = Pos.builtIn): ModuleScope {
|
||||
val seed = cachedStdScopeSeed.get {
|
||||
suspend fun newStdScope(pos: Pos = Pos.builtIn): Scope {
|
||||
val seed = cachedStdScope.get {
|
||||
val stdlib = prepareImport(pos, "lyng.stdlib", null)
|
||||
val plan = LinkedHashMap<String, Int>()
|
||||
for ((name, record) in stdlib.objects) {
|
||||
@ -84,8 +84,7 @@ abstract class ImportProvider(
|
||||
val module = newModuleAt(pos)
|
||||
if (seed.plan.isNotEmpty()) module.applySlotPlan(seed.plan)
|
||||
seed.stdlib.importInto(module, null)
|
||||
// Predeclare regex match result slot ($~) in every module scope.
|
||||
module.addOrUpdateItem("$~", net.sergeych.lyng.obj.ObjNull)
|
||||
return module
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -23,9 +23,6 @@ package net.sergeych.lyng
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.lyng.binding.Binder
|
||||
import net.sergeych.lyng.miniast.MiniAstBuilder
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjClass
|
||||
import net.sergeych.lyng.obj.ObjString
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
@ -132,56 +129,4 @@ class BindingTest {
|
||||
val refs = snap.references.count { it.symbolId == xField.id }
|
||||
assertEquals(1, refs)
|
||||
}
|
||||
|
||||
class ObjA: Obj() {
|
||||
override val objClass = type
|
||||
|
||||
companion object {
|
||||
val type = ObjClass("ObjA").apply {
|
||||
addFn("get1") {
|
||||
ObjString("get1")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testShortFormMethod() = runTest {
|
||||
eval("""
|
||||
class A {
|
||||
fun get1() = "1"
|
||||
fun get2() = get1() + "-2"
|
||||
fun get3(): String = get2() + "-3"
|
||||
override fun toString() = "!"+get3()+"!"
|
||||
}
|
||||
assert(A().get3() == "1-2-3")
|
||||
assert(A().toString() == "!1-2-3!")
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLateGlobalBinding() = runTest {
|
||||
val ms = Script.newScope()
|
||||
ms.eval("""
|
||||
extern class A {
|
||||
fun get1(): String
|
||||
}
|
||||
|
||||
extern fun getA(): A
|
||||
|
||||
fun getB(a: A) = a.get1() + "-2"
|
||||
""".trimIndent())
|
||||
|
||||
ms.addFn("getA") {
|
||||
ObjA()
|
||||
}
|
||||
ms.addConst("A", ObjA.type)
|
||||
ms.eval("""
|
||||
assert(A() is A)
|
||||
assert(getA() is A)
|
||||
assertEquals(getB(getA()), "get1-2")
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -1,32 +1,13 @@
|
||||
/*
|
||||
* 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 kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.lyng.bridge.LyngClassBridge
|
||||
import net.sergeych.lyng.bridge.bindObject
|
||||
import net.sergeych.lyng.bridge.data
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjClass
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import net.sergeych.lyng.obj.ObjString
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class BridgeBindingTest {
|
||||
private data class CounterState(var count: Long)
|
||||
@ -164,87 +145,4 @@ class BridgeBindingTest {
|
||||
}
|
||||
assertTrue(bindFailed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExternObjectBinding() = runTest {
|
||||
val im = Script.defaultImportManager.copy()
|
||||
im.addPackage("bridge.obj") { scope ->
|
||||
scope.eval(
|
||||
"""
|
||||
extern object HostObject {
|
||||
extern fun add(a: Int, b: Int): Int
|
||||
extern val status: String
|
||||
extern var count: Int
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
scope.bindObject("HostObject") {
|
||||
classData = "OK"
|
||||
init { _ ->
|
||||
data = CounterState(5)
|
||||
}
|
||||
addFun("add") { _, _, args ->
|
||||
val a = (args.list[0] as ObjInt).value
|
||||
val b = (args.list[1] as ObjInt).value
|
||||
ObjInt.of(a + b)
|
||||
}
|
||||
addVal("status") { _, _ -> ObjString(classData as String) }
|
||||
addVar(
|
||||
"count",
|
||||
get = { _, instance ->
|
||||
val st = (instance as net.sergeych.lyng.obj.ObjInstance).data as CounterState
|
||||
ObjInt.of(st.count)
|
||||
},
|
||||
set = { _, instance, value ->
|
||||
val st = (instance as net.sergeych.lyng.obj.ObjInstance).data as CounterState
|
||||
st.count = (value as ObjInt).value
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val scope = im.newStdScope()
|
||||
scope.eval(
|
||||
"""
|
||||
import bridge.obj
|
||||
assertEquals(42, HostObject.add(10, 32))
|
||||
assertEquals("OK", HostObject.status)
|
||||
assertEquals(5, HostObject.count)
|
||||
HostObject.count = HostObject.count + 1
|
||||
assertEquals(6, HostObject.count)
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
class ObjA: Obj() {
|
||||
companion object {
|
||||
val type = ObjClass("A").apply {
|
||||
addProperty("field",{
|
||||
ObjInt.of(42)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBindExternClass() = runTest {
|
||||
val ms = Script.newScope()
|
||||
ms.eval("""
|
||||
extern class A {
|
||||
val field: Int
|
||||
}
|
||||
|
||||
fun test(a: A) = a.field
|
||||
|
||||
val prop = dynamic {
|
||||
get { name ->
|
||||
name + "=" + A().field
|
||||
}
|
||||
}
|
||||
|
||||
""".trimIndent())
|
||||
ms.addConst("A", ObjA.type)
|
||||
ms.eval("assertEquals(42, test(A()))")
|
||||
ms.eval("assertEquals(\"test=42\", prop.test)")
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,7 +77,7 @@ class NestedRangeBenchmarkTest {
|
||||
}
|
||||
var depth = 1
|
||||
while (current is BytecodeStatement && current.original is ForInStatement) {
|
||||
val original = current.original
|
||||
val original = current.original as ForInStatement
|
||||
println(
|
||||
"[DEBUG_LOG] [BENCH] nested-happy loop depth=$depth " +
|
||||
"constRange=${original.constRange} canBreak=${original.canBreak} " +
|
||||
|
||||
@ -70,10 +70,9 @@ class OOTest {
|
||||
|
||||
@Test
|
||||
fun testDynamicGet() = runTest {
|
||||
val ms = Script.newScope()
|
||||
ms.eval(
|
||||
eval(
|
||||
"""
|
||||
val accessor: String = dynamic {
|
||||
val accessor: Delegate = dynamic {
|
||||
get { name ->
|
||||
if( name == "foo" ) "bar" else null
|
||||
}
|
||||
@ -85,10 +84,6 @@ class OOTest {
|
||||
|
||||
""".trimIndent()
|
||||
)
|
||||
ms.eval("""
|
||||
assertEquals("bar", accessor.foo)
|
||||
assertEquals(null, accessor.bad)
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -911,57 +906,4 @@ class OOTest {
|
||||
assertEquals(5, t.x)
|
||||
""".trimIndent())
|
||||
}
|
||||
@Test
|
||||
fun testDynamicToDynamic() = runTest {
|
||||
val ms = Script.newScope()
|
||||
ms.eval("""
|
||||
|
||||
class A(prefix) {
|
||||
val da = dynamic {
|
||||
get { name -> "a:"+prefix+":"+name }
|
||||
}
|
||||
}
|
||||
|
||||
val B: A = dynamic {
|
||||
get { p -> A(p) }
|
||||
}
|
||||
assertEquals(A("bar").da.foo, "a:bar:foo")
|
||||
assertEquals( B.buzz.da.foo, "a:buzz:foo" )
|
||||
|
||||
val C = dynamic {
|
||||
get { p -> A(p).da }
|
||||
}
|
||||
|
||||
assertEquals(C.buzz.foo, "a:buzz:foo")
|
||||
""".trimIndent())
|
||||
ms.eval("""
|
||||
""")
|
||||
}
|
||||
@Test
|
||||
fun testDynamicToDynamicFun() = runTest {
|
||||
val ms = Script.newScope()
|
||||
ms.eval("""
|
||||
|
||||
class A(prefix) {
|
||||
val da = dynamic {
|
||||
get { name -> { x -> "a:"+prefix+":"+name+"/"+x } }
|
||||
}
|
||||
}
|
||||
|
||||
val B: A = dynamic {
|
||||
get { p -> A(p) }
|
||||
}
|
||||
assertEquals(A("bar").da.foo("buzz"), "a:bar:foo/buzz")
|
||||
assertEquals( B.buzz.da.foo("42"), "a:buzz:foo/42" )
|
||||
|
||||
val C = dynamic {
|
||||
get { p -> A(p).da }
|
||||
}
|
||||
|
||||
assertEquals(C.buzz.foo("one"), "a:buzz:foo/one")
|
||||
""".trimIndent())
|
||||
ms.eval("""
|
||||
""")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -16,8 +16,6 @@
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.lyng.Script
|
||||
import net.sergeych.lyng.ScriptError
|
||||
import net.sergeych.lyng.eval
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFailsWith
|
||||
@ -304,61 +302,4 @@ class TypesTest {
|
||||
}
|
||||
""")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLambdaTypes1() = runTest {
|
||||
val scope = Script.newScope()
|
||||
// declare: ok
|
||||
scope.eval("""
|
||||
var l1: (Int,String)->String
|
||||
""".trimIndent())
|
||||
// this should be Lyng compile time exception
|
||||
assertFailsWith<ScriptError> {
|
||||
scope.eval("""
|
||||
fun test() {
|
||||
// compiler should detect that l1 us called with arguments that does not match
|
||||
// declare type (Int,String)->String:
|
||||
l1()
|
||||
}
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLambdaTypesEllipsis() = runTest {
|
||||
val scope = Script.newScope()
|
||||
scope.eval("""
|
||||
var l2: (Int,Object...,String)->Real
|
||||
var l4: (Int,String...,String)->Real
|
||||
var l3: (...)->Int
|
||||
""".trimIndent())
|
||||
assertFailsWith<ScriptError> {
|
||||
scope.eval("""
|
||||
fun testTooFew() {
|
||||
l2(1)
|
||||
}
|
||||
""".trimIndent())
|
||||
}
|
||||
assertFailsWith<ScriptError> {
|
||||
scope.eval("""
|
||||
fun testWrongHead() {
|
||||
l2("x", "y")
|
||||
}
|
||||
""".trimIndent())
|
||||
}
|
||||
assertFailsWith<ScriptError> {
|
||||
scope.eval("""
|
||||
fun testWrongEllipsis() {
|
||||
l4(1, 2, "x")
|
||||
}
|
||||
""".trimIndent())
|
||||
}
|
||||
scope.eval("""
|
||||
fun testOk1() { l2(1, "x") }
|
||||
fun testOk2() { l2(1, 2, 3, "x") }
|
||||
fun testOk3() { l3() }
|
||||
fun testOk4() { l3(1, true, "x") }
|
||||
fun testOk5() { l4(1, "a", "b", "x") }
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,7 +55,7 @@ class WebsiteSamplesTest {
|
||||
squares
|
||||
""".trimIndent())
|
||||
assertTrue(result is ObjList)
|
||||
val list = result.list.map { (it as ObjInt).value }
|
||||
val list = (result as ObjList).list.map { (it as ObjInt).value }
|
||||
assertEquals(listOf(4L, 16L, 36L, 64L, 100L), list)
|
||||
}
|
||||
|
||||
@ -75,7 +75,7 @@ class WebsiteSamplesTest {
|
||||
[intBox.get(), realBox.get()]
|
||||
""".trimIndent())
|
||||
assertTrue(result is ObjList)
|
||||
val l = result.list
|
||||
val l = (result as ObjList).list
|
||||
assertEquals(42L, (l[0] as ObjInt).value)
|
||||
assertEquals(3.14, (l[1] as ObjReal).value)
|
||||
}
|
||||
@ -108,7 +108,7 @@ class WebsiteSamplesTest {
|
||||
full
|
||||
""".trimIndent())
|
||||
assertTrue(result is ObjMap)
|
||||
val m = result.map
|
||||
val m = (result as ObjMap).map
|
||||
assertEquals(101L, (m[ObjString("id")] as ObjInt).value)
|
||||
assertEquals("Lyng", (m[ObjString("name")] as ObjString).value)
|
||||
assertEquals("1.5.0-SNAPSHOT", (m[ObjString("version")] as ObjString).value)
|
||||
@ -138,7 +138,7 @@ class WebsiteSamplesTest {
|
||||
[first, middle, last]
|
||||
""".trimIndent())
|
||||
assertTrue(result is ObjList)
|
||||
val rl = result.list
|
||||
val rl = (result as ObjList).list
|
||||
assertEquals(1L, (rl[0] as ObjInt).value)
|
||||
val middle = rl[1] as ObjList
|
||||
assertEquals(listOf(2L, 3L, 4L, 5L), middle.list.map { (it as ObjInt).value })
|
||||
@ -178,7 +178,7 @@ class WebsiteSamplesTest {
|
||||
["hello".shout(), [10, 20, 30].second]
|
||||
""".trimIndent())
|
||||
assertTrue(result is ObjList)
|
||||
val el = result.list
|
||||
val el = (result as ObjList).list
|
||||
assertEquals("HELLO!!!", (el[0] as ObjString).value)
|
||||
assertEquals(20L, (el[1] as ObjInt).value)
|
||||
}
|
||||
@ -253,7 +253,7 @@ class WebsiteSamplesTest {
|
||||
[d1.await(), d2.await()]
|
||||
""".trimIndent())
|
||||
assertTrue(result is ObjList)
|
||||
val dl = result.list
|
||||
val dl = (result as ObjList).list
|
||||
assertEquals("Task A finished", (dl[0] as ObjString).value)
|
||||
assertEquals("Task B finished", (dl[1] as ObjString).value)
|
||||
}
|
||||
|
||||
95
lynglib/src/jvmTest/kotlin/ArithmeticBenchmarkTest.kt
Normal file
95
lynglib/src/jvmTest/kotlin/ArithmeticBenchmarkTest.kt
Normal file
@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* JVM micro-benchmarks for primitive arithmetic and comparison fast paths.
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.PerfFlags
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.Ignore
|
||||
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
class ArithmeticBenchmarkTest {
|
||||
@Test
|
||||
fun benchmarkIntArithmeticAndComparisons() = runBlocking {
|
||||
val n = 400_000
|
||||
val sumScript = """
|
||||
var s = 0
|
||||
var i = 0
|
||||
while (i < $n) {
|
||||
s = s + i
|
||||
i = i + 1
|
||||
}
|
||||
s
|
||||
""".trimIndent()
|
||||
|
||||
// Baseline: disable primitive fast ops
|
||||
PerfFlags.PRIMITIVE_FASTOPS = false
|
||||
val scope1 = Scope()
|
||||
val t0 = System.nanoTime()
|
||||
val r1 = (scope1.eval(sumScript) as ObjInt).value
|
||||
val t1 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] int-sum x$n [PRIMITIVE_FASTOPS=OFF]: ${(t1 - t0)/1_000_000.0} ms")
|
||||
|
||||
// Optimized
|
||||
PerfFlags.PRIMITIVE_FASTOPS = true
|
||||
val scope2 = Scope()
|
||||
val t2 = System.nanoTime()
|
||||
val r2 = (scope2.eval(sumScript) as ObjInt).value
|
||||
val t3 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] int-sum x$n [PRIMITIVE_FASTOPS=ON]: ${(t3 - t2)/1_000_000.0} ms")
|
||||
|
||||
val expected = (n.toLong() - 1L) * n / 2L
|
||||
assertEquals(expected, r1)
|
||||
assertEquals(expected, r2)
|
||||
|
||||
// Comparison heavy (branchy) loop
|
||||
val cmpScript = """
|
||||
var s = 0
|
||||
var i = 0
|
||||
while (i < $n) {
|
||||
if (i % 2 == 0) s = s + 1 else s = s + 2
|
||||
i = i + 1
|
||||
}
|
||||
s
|
||||
""".trimIndent()
|
||||
|
||||
PerfFlags.PRIMITIVE_FASTOPS = false
|
||||
val scope3 = Scope()
|
||||
val t4 = System.nanoTime()
|
||||
val c1 = (scope3.eval(cmpScript) as ObjInt).value
|
||||
val t5 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] int-cmp x$n [PRIMITIVE_FASTOPS=OFF]: ${(t5 - t4)/1_000_000.0} ms")
|
||||
|
||||
PerfFlags.PRIMITIVE_FASTOPS = true
|
||||
val scope4 = Scope()
|
||||
val t6 = System.nanoTime()
|
||||
val c2 = (scope4.eval(cmpScript) as ObjInt).value
|
||||
val t7 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] int-cmp x$n [PRIMITIVE_FASTOPS=ON]: ${(t7 - t6)/1_000_000.0} ms")
|
||||
|
||||
// Expected: half of n even add 1, half odd add 2 (n even assumed)
|
||||
val expectedCmp = (n / 2) * 1L + (n - n / 2) * 2L
|
||||
assertEquals(expectedCmp, c1)
|
||||
assertEquals(expectedCmp, c2)
|
||||
}
|
||||
}
|
||||
350
lynglib/src/jvmTest/kotlin/BookAllocationProfileTest.kt
Normal file
350
lynglib/src/jvmTest/kotlin/BookAllocationProfileTest.kt
Normal file
@ -0,0 +1,350 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.PerfFlags
|
||||
import java.io.File
|
||||
import java.lang.management.GarbageCollectorMXBean
|
||||
import java.lang.management.ManagementFactory
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import kotlin.io.path.extension
|
||||
import kotlin.random.Random
|
||||
import kotlin.system.measureNanoTime
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.Ignore
|
||||
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
class BookAllocationProfileTest {
|
||||
|
||||
private fun outFile(): File = File("lynglib/build/book_alloc_profile.txt")
|
||||
|
||||
private fun writeHeader(f: File) {
|
||||
if (!f.parentFile.exists()) f.parentFile.mkdirs()
|
||||
f.writeText("[DEBUG_LOG] Book allocation/time profiling (JVM)\n")
|
||||
f.appendText("[DEBUG_LOG] All sizes in bytes; time in ns (lower is better).\n")
|
||||
}
|
||||
|
||||
private fun appendLine(f: File, s: String) { f.appendText(s + "\n") }
|
||||
|
||||
// Optional STDERR filter to hide benign warnings during profiling runs
|
||||
private inline fun <T> withFilteredStderr(vararg suppressContains: String, block: () -> T): T {
|
||||
val orig = System.err
|
||||
val filtering = java.io.PrintStream(object : java.io.OutputStream() {
|
||||
private val buf = StringBuilder()
|
||||
override fun write(b: Int) {
|
||||
if (b == '\n'.code) {
|
||||
val line = buf.toString()
|
||||
val suppress = suppressContains.any { line.contains(it) }
|
||||
if (!suppress) orig.println(line)
|
||||
buf.setLength(0)
|
||||
} else buf.append(b.toChar())
|
||||
}
|
||||
})
|
||||
return try {
|
||||
System.setErr(filtering)
|
||||
block()
|
||||
} finally {
|
||||
System.setErr(orig)
|
||||
}
|
||||
}
|
||||
|
||||
private fun forceGc() {
|
||||
// Best-effort GC to stabilize measurements
|
||||
repeat(3) {
|
||||
System.gc()
|
||||
try { Thread.sleep(25) } catch (_: InterruptedException) {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun usedHeap(): Long {
|
||||
val mem = ManagementFactory.getMemoryMXBean().heapMemoryUsage
|
||||
return mem.used
|
||||
}
|
||||
|
||||
private suspend fun runDocTestsNonFailing(file: String, bookMode: Boolean = true) {
|
||||
try {
|
||||
runDocTests(file, bookMode)
|
||||
} catch (t: Throwable) {
|
||||
// Profiling should not fail because of documentation snippet issues.
|
||||
println("[DEBUG_LOG] [PROFILE] Skipping failing doc: $file: ${t.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun runBooksOnce(): Unit = runBlocking {
|
||||
// Mirror BookTest set, but run in bookMode to avoid strict assertions and allow shared context
|
||||
// Profiling should not fail on documentation snippet mismatches.
|
||||
runDocTestsNonFailing("../docs/tutorial.md", bookMode = true)
|
||||
runDocTestsNonFailing("../docs/math.md", bookMode = true)
|
||||
runDocTestsNonFailing("../docs/advanced_topics.md", bookMode = true)
|
||||
runDocTestsNonFailing("../docs/OOP.md", bookMode = true)
|
||||
runDocTestsNonFailing("../docs/Real.md", bookMode = true)
|
||||
runDocTestsNonFailing("../docs/List.md", bookMode = true)
|
||||
runDocTestsNonFailing("../docs/Range.md", bookMode = true)
|
||||
runDocTestsNonFailing("../docs/Set.md", bookMode = true)
|
||||
runDocTestsNonFailing("../docs/Map.md", bookMode = true)
|
||||
runDocTestsNonFailing("../docs/Buffer.md", bookMode = true)
|
||||
runDocTestsNonFailing("../docs/when.md", bookMode = true)
|
||||
// Samples folder, bookMode=true
|
||||
for (bt in Files.list(Paths.get("../docs/samples")).toList()) {
|
||||
if (bt.extension == "md") runDocTestsNonFailing(bt.toString(), bookMode = true)
|
||||
}
|
||||
runDocTestsNonFailing("../docs/declaring_arguments.md", bookMode = true)
|
||||
runDocTestsNonFailing("../docs/exceptions_handling.md", bookMode = true)
|
||||
runDocTestsNonFailing("../docs/time.md", bookMode = true)
|
||||
runDocTestsNonFailing("../docs/parallelism.md", bookMode = true)
|
||||
runDocTestsNonFailing("../docs/RingBuffer.md", bookMode = true)
|
||||
runDocTestsNonFailing("../docs/Iterable.md", bookMode = true)
|
||||
}
|
||||
|
||||
private data class ProfileResult(val timeNs: Long, val allocBytes: Long)
|
||||
|
||||
private suspend fun profileRun(): ProfileResult {
|
||||
forceGc()
|
||||
val before = usedHeap()
|
||||
val elapsed = measureNanoTime {
|
||||
withFilteredStderr("ScriptFlowIsNoMoreCollected") {
|
||||
runBooksOnce()
|
||||
}
|
||||
}
|
||||
forceGc()
|
||||
val after = usedHeap()
|
||||
val alloc = (after - before).coerceAtLeast(0)
|
||||
return ProfileResult(elapsed, alloc)
|
||||
}
|
||||
|
||||
private data class GcSnapshot(val count: Long, val timeMs: Long)
|
||||
private fun gcSnapshot(): GcSnapshot {
|
||||
var c = 0L
|
||||
var t = 0L
|
||||
for (gc: GarbageCollectorMXBean in ManagementFactory.getGarbageCollectorMXBeans()) {
|
||||
c += (gc.collectionCount.takeIf { it >= 0 } ?: 0)
|
||||
t += (gc.collectionTime.takeIf { it >= 0 } ?: 0)
|
||||
}
|
||||
return GcSnapshot(c, t)
|
||||
}
|
||||
|
||||
// --- Optional JFR support via reflection (works only on JDKs with Flight Recorder) ---
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
private class JfrHandle(val rec: Any, val dump: (File) -> Unit, val stop: () -> Unit)
|
||||
|
||||
private fun jfrStartIfRequested(name: String): JfrHandle? {
|
||||
val enabled = System.getProperty("lyng.jfr")?.toBoolean() == true
|
||||
if (!enabled) return null
|
||||
return try {
|
||||
val recCl = Class.forName("jdk.jfr.Recording")
|
||||
val ctor = recCl.getDeclaredConstructor()
|
||||
val rec = ctor.newInstance()
|
||||
val setName = recCl.methods.firstOrNull { it.name == "setName" && it.parameterTypes.size == 1 }
|
||||
setName?.invoke(rec, "Lyng-$name")
|
||||
val start = recCl.methods.first { it.name == "start" && it.parameterTypes.isEmpty() }
|
||||
start.invoke(rec)
|
||||
val stop = recCl.methods.first { it.name == "stop" && it.parameterTypes.isEmpty() }
|
||||
val dump = recCl.methods.firstOrNull { it.name == "dump" && it.parameterTypes.size == 1 }
|
||||
val dumper: (File) -> Unit = if (dump != null) {
|
||||
{ f -> dump.invoke(rec, f.toPath()) }
|
||||
} else {
|
||||
{ _ -> }
|
||||
}
|
||||
JfrHandle(rec, dumper) { stop.invoke(rec) }
|
||||
} catch (e: Throwable) {
|
||||
// JFR requested but not available; note once via stdout and proceed without it
|
||||
try {
|
||||
println("[DEBUG_LOG] JFR not available on this JVM; run with Oracle/OpenJDK 11+ to enable -Dlyng.jfr=true")
|
||||
} catch (_: Throwable) {}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun intProp(name: String, def: Int): Int =
|
||||
System.getProperty(name)?.toIntOrNull() ?: def
|
||||
|
||||
private fun boolProp(name: String, def: Boolean): Boolean =
|
||||
System.getProperty(name)?.toBoolean() ?: def
|
||||
|
||||
private data class FlagSnapshot(
|
||||
val RVAL_FASTPATH: Boolean,
|
||||
val PRIMITIVE_FASTOPS: Boolean,
|
||||
val ARG_BUILDER: Boolean,
|
||||
val ARG_SMALL_ARITY_12: Boolean,
|
||||
val FIELD_PIC: Boolean,
|
||||
val METHOD_PIC: Boolean,
|
||||
val FIELD_PIC_SIZE_4: Boolean,
|
||||
val METHOD_PIC_SIZE_4: Boolean,
|
||||
val INDEX_PIC: Boolean,
|
||||
val INDEX_PIC_SIZE_4: Boolean,
|
||||
val SCOPE_POOL: Boolean,
|
||||
val PIC_DEBUG_COUNTERS: Boolean,
|
||||
) {
|
||||
fun restore() {
|
||||
PerfFlags.RVAL_FASTPATH = RVAL_FASTPATH
|
||||
PerfFlags.PRIMITIVE_FASTOPS = PRIMITIVE_FASTOPS
|
||||
PerfFlags.ARG_BUILDER = ARG_BUILDER
|
||||
PerfFlags.ARG_SMALL_ARITY_12 = ARG_SMALL_ARITY_12
|
||||
PerfFlags.FIELD_PIC = FIELD_PIC
|
||||
PerfFlags.METHOD_PIC = METHOD_PIC
|
||||
PerfFlags.FIELD_PIC_SIZE_4 = FIELD_PIC_SIZE_4
|
||||
PerfFlags.METHOD_PIC_SIZE_4 = METHOD_PIC_SIZE_4
|
||||
PerfFlags.INDEX_PIC = INDEX_PIC
|
||||
PerfFlags.INDEX_PIC_SIZE_4 = INDEX_PIC_SIZE_4
|
||||
PerfFlags.SCOPE_POOL = SCOPE_POOL
|
||||
PerfFlags.PIC_DEBUG_COUNTERS = PIC_DEBUG_COUNTERS
|
||||
}
|
||||
}
|
||||
|
||||
private fun snapshotFlags() = FlagSnapshot(
|
||||
RVAL_FASTPATH = PerfFlags.RVAL_FASTPATH,
|
||||
PRIMITIVE_FASTOPS = PerfFlags.PRIMITIVE_FASTOPS,
|
||||
ARG_BUILDER = PerfFlags.ARG_BUILDER,
|
||||
ARG_SMALL_ARITY_12 = PerfFlags.ARG_SMALL_ARITY_12,
|
||||
FIELD_PIC = PerfFlags.FIELD_PIC,
|
||||
METHOD_PIC = PerfFlags.METHOD_PIC,
|
||||
FIELD_PIC_SIZE_4 = PerfFlags.FIELD_PIC_SIZE_4,
|
||||
METHOD_PIC_SIZE_4 = PerfFlags.METHOD_PIC_SIZE_4,
|
||||
INDEX_PIC = PerfFlags.INDEX_PIC,
|
||||
INDEX_PIC_SIZE_4 = PerfFlags.INDEX_PIC_SIZE_4,
|
||||
SCOPE_POOL = PerfFlags.SCOPE_POOL,
|
||||
PIC_DEBUG_COUNTERS = PerfFlags.PIC_DEBUG_COUNTERS,
|
||||
)
|
||||
|
||||
private fun median(values: List<Long>): Long {
|
||||
if (values.isEmpty()) return 0
|
||||
val s = values.sorted()
|
||||
val mid = s.size / 2
|
||||
return if (s.size % 2 == 1) s[mid] else ((s[mid - 1] + s[mid]) / 2)
|
||||
}
|
||||
|
||||
private suspend fun runScenario(
|
||||
name: String,
|
||||
prepare: () -> Unit,
|
||||
repeats: Int = 3,
|
||||
out: (String) -> Unit
|
||||
): ProfileResult {
|
||||
val warmup = intProp("lyng.profile.warmup", 1)
|
||||
val reps = intProp("lyng.profile.repeats", repeats)
|
||||
// JFR
|
||||
val jfr = jfrStartIfRequested(name)
|
||||
if (System.getProperty("lyng.jfr")?.toBoolean() == true && jfr == null) {
|
||||
out("[DEBUG_LOG] JFR: requested but not available on this JVM")
|
||||
}
|
||||
// Warm-up before GC snapshot (some profilers prefer this)
|
||||
prepare()
|
||||
repeat(warmup) { profileRun() }
|
||||
// GC baseline
|
||||
val gc0 = gcSnapshot()
|
||||
val times = ArrayList<Long>(repeats)
|
||||
val allocs = ArrayList<Long>(repeats)
|
||||
repeat(reps) {
|
||||
val r = profileRun()
|
||||
times += r.timeNs
|
||||
allocs += r.allocBytes
|
||||
}
|
||||
val pr = ProfileResult(median(times), median(allocs))
|
||||
val gc1 = gcSnapshot()
|
||||
val gcCountDelta = (gc1.count - gc0.count).coerceAtLeast(0)
|
||||
val gcTimeDelta = (gc1.timeMs - gc0.timeMs).coerceAtLeast(0)
|
||||
out("[DEBUG_LOG] time=${pr.timeNs} ns, alloc=${pr.allocBytes} B (median of ${reps}), GC(count=${gcCountDelta}, timeMs=${gcTimeDelta})")
|
||||
// Stop and dump JFR if enabled
|
||||
if (jfr != null) {
|
||||
try {
|
||||
jfr.stop()
|
||||
val dumpFile = File("lynglib/build/jfr_${name}.jfr")
|
||||
jfr.dump(dumpFile)
|
||||
out("[DEBUG_LOG] JFR dumped: ${dumpFile.path}")
|
||||
} catch (_: Throwable) {}
|
||||
}
|
||||
return pr
|
||||
}
|
||||
|
||||
@Test
|
||||
fun profile_books_allocations_and_time() = runTestBlocking {
|
||||
val f = outFile()
|
||||
writeHeader(f)
|
||||
|
||||
fun log(s: String) = appendLine(f, s)
|
||||
|
||||
val saved = snapshotFlags()
|
||||
try {
|
||||
data class Scenario(val label: String, val title: String, val prep: () -> Unit)
|
||||
val scenarios = mutableListOf<Scenario>()
|
||||
// Baseline A
|
||||
scenarios += Scenario("A", "JVM defaults") {
|
||||
saved.restore(); PerfFlags.PIC_DEBUG_COUNTERS = false
|
||||
}
|
||||
// Most flags OFF B
|
||||
scenarios += Scenario("B", "most perf flags OFF") {
|
||||
saved.restore(); PerfFlags.PIC_DEBUG_COUNTERS = false
|
||||
PerfFlags.RVAL_FASTPATH = false
|
||||
PerfFlags.PRIMITIVE_FASTOPS = false
|
||||
PerfFlags.ARG_BUILDER = false
|
||||
PerfFlags.ARG_SMALL_ARITY_12 = false
|
||||
PerfFlags.FIELD_PIC = false
|
||||
PerfFlags.METHOD_PIC = false
|
||||
PerfFlags.FIELD_PIC_SIZE_4 = false
|
||||
PerfFlags.METHOD_PIC_SIZE_4 = false
|
||||
PerfFlags.INDEX_PIC = false
|
||||
PerfFlags.INDEX_PIC_SIZE_4 = false
|
||||
PerfFlags.SCOPE_POOL = false
|
||||
}
|
||||
// Defaults with INDEX_PIC size 2 C
|
||||
scenarios += Scenario("C", "defaults except INDEX_PIC_SIZE_4=false") {
|
||||
saved.restore(); PerfFlags.PIC_DEBUG_COUNTERS = false
|
||||
PerfFlags.INDEX_PIC = true; PerfFlags.INDEX_PIC_SIZE_4 = false
|
||||
}
|
||||
|
||||
// One-flag toggles relative to A
|
||||
scenarios += Scenario("D", "A with RVAL_FASTPATH=false") {
|
||||
saved.restore(); PerfFlags.PIC_DEBUG_COUNTERS = false; PerfFlags.RVAL_FASTPATH = false
|
||||
}
|
||||
scenarios += Scenario("E", "A with PRIMITIVE_FASTOPS=false") {
|
||||
saved.restore(); PerfFlags.PIC_DEBUG_COUNTERS = false; PerfFlags.PRIMITIVE_FASTOPS = false
|
||||
}
|
||||
scenarios += Scenario("F", "A with INDEX_PIC=false") {
|
||||
saved.restore(); PerfFlags.PIC_DEBUG_COUNTERS = false; PerfFlags.INDEX_PIC = false
|
||||
}
|
||||
scenarios += Scenario("G", "A with SCOPE_POOL=false") {
|
||||
saved.restore(); PerfFlags.PIC_DEBUG_COUNTERS = false; PerfFlags.SCOPE_POOL = false
|
||||
}
|
||||
|
||||
val shuffle = boolProp("lyng.profile.shuffle", true)
|
||||
val order = if (shuffle) scenarios.shuffled(Random(System.nanoTime())) else scenarios
|
||||
|
||||
val results = mutableMapOf<String, ProfileResult>()
|
||||
for (sc in order) {
|
||||
log("[DEBUG_LOG] Scenario ${sc.label}: ${sc.title}")
|
||||
results[sc.label] = runScenario(sc.label, prepare = sc.prep, out = ::log)
|
||||
}
|
||||
|
||||
// Summary vs A if measured
|
||||
val a = results["A"]
|
||||
if (a != null) {
|
||||
log("[DEBUG_LOG] Summary deltas vs A (medians):")
|
||||
fun deltaLine(name: String, r: ProfileResult) = "[DEBUG_LOG] ${name} - A: time=${r.timeNs - a.timeNs} ns, alloc=${r.allocBytes - a.allocBytes} B"
|
||||
listOf("B","C","D","E","F","G").forEach { k ->
|
||||
results[k]?.let { r -> log(deltaLine(k, r)) }
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
saved.restore()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Minimal runBlocking bridge to avoid extra test deps here
|
||||
private fun runTestBlocking(block: suspend () -> Unit) {
|
||||
kotlinx.coroutines.runBlocking { block() }
|
||||
}
|
||||
@ -30,6 +30,7 @@ import java.nio.file.Files.readAllLines
|
||||
import java.nio.file.Paths
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.io.path.extension
|
||||
import kotlin.test.Ignore
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.fail
|
||||
@ -247,6 +248,7 @@ suspend fun runDocTests(fileName: String, bookMode: Boolean = false) {
|
||||
println("tests passed: $count")
|
||||
}
|
||||
|
||||
@Ignore("TODO(bytecode-only): uses fallback")
|
||||
class BookTest {
|
||||
|
||||
@Test
|
||||
|
||||
158
lynglib/src/jvmTest/kotlin/CallArgPipelineABTest.kt
Normal file
158
lynglib/src/jvmTest/kotlin/CallArgPipelineABTest.kt
Normal file
@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
package net.sergeych.lyng
|
||||
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import java.io.File
|
||||
import kotlin.system.measureNanoTime
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.Ignore
|
||||
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
class CallArgPipelineABTest {
|
||||
|
||||
private fun outFile(): File = File("lynglib/build/call_ab_results.txt")
|
||||
|
||||
private fun writeHeader(f: File) {
|
||||
if (!f.parentFile.exists()) f.parentFile.mkdirs()
|
||||
f.writeText("[DEBUG_LOG] Call/Arg pipeline A/B results\n")
|
||||
}
|
||||
|
||||
private fun appendLine(f: File, s: String) {
|
||||
f.appendText(s + "\n")
|
||||
}
|
||||
|
||||
private suspend fun buildScriptForCalls(arity: Int, iters: Int): Script {
|
||||
val argsDecl = (0 until arity).joinToString(",") { "a$it" }
|
||||
val argsUse = (0 until arity).joinToString(" + ") { "a$it" }.ifEmpty { "0" }
|
||||
val callArgs = (0 until arity).joinToString(",") { (it + 1).toString() }
|
||||
val src = """
|
||||
var sum = 0
|
||||
fun f($argsDecl) { $argsUse }
|
||||
for(i in 0..${iters - 1}) {
|
||||
sum += f($callArgs)
|
||||
}
|
||||
sum
|
||||
""".trimIndent()
|
||||
return Compiler.compile(Source("<calls-$arity>", src), Script.defaultImportManager)
|
||||
}
|
||||
|
||||
private suspend fun benchCallsOnce(arity: Int, iters: Int): Long {
|
||||
val script = buildScriptForCalls(arity, iters)
|
||||
val scope = Script.newScope()
|
||||
var result: Obj? = null
|
||||
val t = measureNanoTime {
|
||||
result = script.execute(scope)
|
||||
}
|
||||
// Basic correctness check so JIT doesn’t elide
|
||||
val expected = (0 until iters).fold(0L) { acc, _ ->
|
||||
(acc + (1L + 2L + 3L + 4L + 5L + 6L + 7L + 8L).let { if (arity <= 8) it - (8 - arity) * 0L else it })
|
||||
}
|
||||
// We only rely that it runs; avoid strict equals as function may compute differently for arities < 8
|
||||
if (result !is ObjInt) {
|
||||
// ensure use to prevent DCE
|
||||
println("[DEBUG_LOG] Result class=${result?.javaClass?.simpleName}")
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
private suspend fun benchOptionalCallShortCircuit(iters: Int): Long {
|
||||
val src = """
|
||||
var side = 0
|
||||
fun inc() { side += 1 }
|
||||
var o = null
|
||||
for(i in 0..${iters - 1}) {
|
||||
o?.foo(inc())
|
||||
}
|
||||
side
|
||||
""".trimIndent()
|
||||
val script = Compiler.compile(Source("<opt-call>", src), Script.defaultImportManager)
|
||||
val scope = Script.newScope()
|
||||
var result: Obj? = null
|
||||
val t = measureNanoTime { result = script.execute(scope) }
|
||||
// Ensure short-circuit actually happened
|
||||
require((result as? ObjInt)?.value == 0L) { "optional-call short-circuit failed; side=${(result as? ObjInt)?.value}" }
|
||||
return t
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ab_call_pipeline() = runTestBlocking {
|
||||
val f = outFile()
|
||||
writeHeader(f)
|
||||
|
||||
val savedArgBuilder = PerfFlags.ARG_BUILDER
|
||||
val savedScopePool = PerfFlags.SCOPE_POOL
|
||||
|
||||
try {
|
||||
val iters = 50_000
|
||||
val aritiesBase = listOf(0, 1, 2, 3, 4, 5, 6, 7, 8)
|
||||
|
||||
// A/B for ARG_BUILDER (0..8)
|
||||
PerfFlags.ARG_BUILDER = false
|
||||
val offTimes = mutableListOf<Long>()
|
||||
for (a in aritiesBase) offTimes += benchCallsOnce(a, iters)
|
||||
PerfFlags.ARG_BUILDER = true
|
||||
val onTimes = mutableListOf<Long>()
|
||||
for (a in aritiesBase) onTimes += benchCallsOnce(a, iters)
|
||||
|
||||
appendLine(f, "[DEBUG_LOG] ARG_BUILDER A/B (iters=$iters):")
|
||||
aritiesBase.forEachIndexed { idx, a ->
|
||||
appendLine(f, "[DEBUG_LOG] arity=$a OFF=${offTimes[idx]} ns, ON=${onTimes[idx]} ns, delta=${offTimes[idx] - onTimes[idx]} ns")
|
||||
}
|
||||
|
||||
// A/B for ARG_SMALL_ARITY_12 (9..12)
|
||||
val aritiesExtended = listOf(9, 10, 11, 12)
|
||||
val savedSmall = PerfFlags.ARG_SMALL_ARITY_12
|
||||
try {
|
||||
PerfFlags.ARG_BUILDER = true // base builder on
|
||||
PerfFlags.ARG_SMALL_ARITY_12 = false
|
||||
val offExt = mutableListOf<Long>()
|
||||
for (a in aritiesExtended) offExt += benchCallsOnce(a, iters)
|
||||
PerfFlags.ARG_SMALL_ARITY_12 = true
|
||||
val onExt = mutableListOf<Long>()
|
||||
for (a in aritiesExtended) onExt += benchCallsOnce(a, iters)
|
||||
appendLine(f, "[DEBUG_LOG] ARG_SMALL_ARITY_12 A/B (iters=$iters):")
|
||||
aritiesExtended.forEachIndexed { idx, a ->
|
||||
appendLine(f, "[DEBUG_LOG] arity=$a OFF=${offExt[idx]} ns, ON=${onExt[idx]} ns, delta=${offExt[idx] - onExt[idx]} ns")
|
||||
}
|
||||
} finally {
|
||||
PerfFlags.ARG_SMALL_ARITY_12 = savedSmall
|
||||
}
|
||||
|
||||
// Optional call short-circuit sanity timing (does not A/B a flag currently; implementation short-circuits before args)
|
||||
val tOpt = benchOptionalCallShortCircuit(100_000)
|
||||
appendLine(f, "[DEBUG_LOG] Optional-call short-circuit sanity: ${tOpt} ns for 100k iterations (side-effect arg not evaluated).")
|
||||
|
||||
// A/B for SCOPE_POOL
|
||||
PerfFlags.SCOPE_POOL = false
|
||||
val tPoolOff = benchCallsOnce(5, iters)
|
||||
PerfFlags.SCOPE_POOL = true
|
||||
val tPoolOn = benchCallsOnce(5, iters)
|
||||
appendLine(f, "[DEBUG_LOG] SCOPE_POOL A/B (arity=5, iters=$iters): OFF=${tPoolOff} ns, ON=${tPoolOn} ns, delta=${tPoolOff - tPoolOn} ns")
|
||||
|
||||
} finally {
|
||||
PerfFlags.ARG_BUILDER = savedArgBuilder
|
||||
PerfFlags.SCOPE_POOL = savedScopePool
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Minimal runBlocking for common jvmTest without depending on kotlinx.coroutines test artifacts here
|
||||
private fun runTestBlocking(block: suspend () -> Unit) {
|
||||
kotlinx.coroutines.runBlocking { block() }
|
||||
}
|
||||
119
lynglib/src/jvmTest/kotlin/CallBenchmarkTest.kt
Normal file
119
lynglib/src/jvmTest/kotlin/CallBenchmarkTest.kt
Normal file
@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* JVM micro-benchmarks for function/method call overhead and argument building.
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.PerfFlags
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.Ignore
|
||||
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
class CallBenchmarkTest {
|
||||
@Test
|
||||
fun benchmarkSimpleFunctionCalls() = runBlocking {
|
||||
val n = 300_000 // keep it fast for CI
|
||||
|
||||
// A tiny script with 0, 1, 2 arg functions and a loop using them
|
||||
val script = """
|
||||
fun f0() { 1 }
|
||||
fun f1(a) { a }
|
||||
fun f2(a,b) { a + b }
|
||||
|
||||
var s = 0
|
||||
var i = 0
|
||||
while (i < $n) {
|
||||
s = s + f0()
|
||||
s = s + f1(1)
|
||||
s = s + f2(1, 1)
|
||||
i = i + 1
|
||||
}
|
||||
s
|
||||
""".trimIndent()
|
||||
|
||||
// Disable ARG_BUILDER for baseline
|
||||
PerfFlags.ARG_BUILDER = false
|
||||
val scope1 = Scope()
|
||||
val t0 = System.nanoTime()
|
||||
val r1 = (scope1.eval(script) as ObjInt).value
|
||||
val t1 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] calls x$n [ARG_BUILDER=OFF]: ${(t1 - t0)/1_000_000.0} ms")
|
||||
|
||||
// Enable ARG_BUILDER for optimized run
|
||||
PerfFlags.ARG_BUILDER = true
|
||||
val scope2 = Scope()
|
||||
val t2 = System.nanoTime()
|
||||
val r2 = (scope2.eval(script) as ObjInt).value
|
||||
val t3 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] calls x$n [ARG_BUILDER=ON]: ${(t3 - t2)/1_000_000.0} ms")
|
||||
|
||||
// Correctness: each loop adds 1 + 1 + (1+1) = 4
|
||||
val expected = 4L * n
|
||||
assertEquals(expected, r1)
|
||||
assertEquals(expected, r2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun benchmarkMixedArityCalls() = runBlocking {
|
||||
val n = 200_000
|
||||
val script = """
|
||||
fun f0() { 1 }
|
||||
fun f1(a) { a }
|
||||
fun f2(a,b) { a + b }
|
||||
fun f3(a,b,c) { a + b + c }
|
||||
fun f4(a,b,c,d) { a + b + c + d }
|
||||
|
||||
var s = 0
|
||||
var i = 0
|
||||
while (i < $n) {
|
||||
s = s + f0()
|
||||
s = s + f1(1)
|
||||
s = s + f2(1, 1)
|
||||
s = s + f3(1, 1, 1)
|
||||
s = s + f4(1, 1, 1, 1)
|
||||
i = i + 1
|
||||
}
|
||||
s
|
||||
""".trimIndent()
|
||||
|
||||
// Baseline
|
||||
PerfFlags.ARG_BUILDER = false
|
||||
val scope1 = Scope()
|
||||
val t0 = System.nanoTime()
|
||||
val r1 = (scope1.eval(script) as ObjInt).value
|
||||
val t1 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] mixed-arity x$n [ARG_BUILDER=OFF]: ${(t1 - t0)/1_000_000.0} ms")
|
||||
|
||||
// Optimized
|
||||
PerfFlags.ARG_BUILDER = true
|
||||
val scope2 = Scope()
|
||||
val t2 = System.nanoTime()
|
||||
val r2 = (scope2.eval(script) as ObjInt).value
|
||||
val t3 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] mixed-arity x$n [ARG_BUILDER=ON]: ${(t3 - t2)/1_000_000.0} ms")
|
||||
|
||||
// Each loop: 1 + 1 + 2 + 3 + 4 = 11
|
||||
val expected = 11L * n
|
||||
assertEquals(expected, r1)
|
||||
assertEquals(expected, r2)
|
||||
}
|
||||
}
|
||||
83
lynglib/src/jvmTest/kotlin/CallMixedArityBenchmarkTest.kt
Normal file
83
lynglib/src/jvmTest/kotlin/CallMixedArityBenchmarkTest.kt
Normal file
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* JVM micro-benchmark for mixed-arity function calls and ARG_BUILDER.
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.PerfFlags
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import java.io.File
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.Ignore
|
||||
|
||||
private fun appendBenchLog(name: String, variant: String, ms: Double) {
|
||||
val f = File("lynglib/build/benchlogs/log.csv")
|
||||
f.parentFile.mkdirs()
|
||||
f.appendText("$name,$variant,$ms\n")
|
||||
}
|
||||
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
class CallMixedArityBenchmarkTest {
|
||||
@Test
|
||||
fun benchmarkMixedArityCalls() = runBlocking {
|
||||
val n = 200_000
|
||||
val script = """
|
||||
fun f0() { 1 }
|
||||
fun f1(a) { a }
|
||||
fun f2(a,b) { a + b }
|
||||
fun f3(a,b,c) { a + b + c }
|
||||
fun f4(a,b,c,d) { a + b + c + d }
|
||||
|
||||
var s = 0
|
||||
var i = 0
|
||||
while (i < $n) {
|
||||
s = s + f0()
|
||||
s = s + f1(1)
|
||||
s = s + f2(1, 1)
|
||||
s = s + f3(1, 1, 1)
|
||||
s = s + f4(1, 1, 1, 1)
|
||||
i = i + 1
|
||||
}
|
||||
s
|
||||
""".trimIndent()
|
||||
|
||||
// Baseline
|
||||
PerfFlags.ARG_BUILDER = false
|
||||
val scope1 = Scope()
|
||||
val t0 = System.nanoTime()
|
||||
val r1 = (scope1.eval(script) as ObjInt).value
|
||||
val t1 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] mixed-arity x$n [ARG_BUILDER=OFF]: ${(t1 - t0)/1_000_000.0} ms")
|
||||
|
||||
// Optimized
|
||||
PerfFlags.ARG_BUILDER = true
|
||||
val scope2 = Scope()
|
||||
val t2 = System.nanoTime()
|
||||
val r2 = (scope2.eval(script) as ObjInt).value
|
||||
val t3 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] mixed-arity x$n [ARG_BUILDER=ON]: ${(t3 - t2)/1_000_000.0} ms")
|
||||
|
||||
// Each loop: 1 + 1 + 2 + 3 + 4 = 11
|
||||
val expected = 11L * n
|
||||
assertEquals(expected, r1)
|
||||
assertEquals(expected, r2)
|
||||
}
|
||||
}
|
||||
72
lynglib/src/jvmTest/kotlin/CallPoolingBenchmarkTest.kt
Normal file
72
lynglib/src/jvmTest/kotlin/CallPoolingBenchmarkTest.kt
Normal file
@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* JVM micro-benchmark for Scope frame pooling impact on call-heavy code paths.
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.PerfFlags
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.Ignore
|
||||
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
class CallPoolingBenchmarkTest {
|
||||
@Test
|
||||
fun benchmarkScopePoolingOnFunctionCalls() = runBlocking {
|
||||
val n = 300_000
|
||||
val script = """
|
||||
fun inc1(a) { a + 1 }
|
||||
fun inc2(a) { inc1(a) + 1 }
|
||||
fun inc3(a) { inc2(a) + 1 }
|
||||
var s = 0
|
||||
var i = 0
|
||||
while (i < $n) {
|
||||
s = inc3(s)
|
||||
i = i + 1
|
||||
}
|
||||
s
|
||||
""".trimIndent()
|
||||
|
||||
// Baseline: pooling OFF
|
||||
PerfFlags.SCOPE_POOL = false
|
||||
val scope1 = Scope()
|
||||
val t0 = System.nanoTime()
|
||||
val r1 = (scope1.eval(script) as ObjInt).value
|
||||
val t1 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] call-pooling x$n [SCOPE_POOL=OFF]: ${(t1 - t0)/1_000_000.0} ms")
|
||||
|
||||
// Optimized: pooling ON
|
||||
PerfFlags.SCOPE_POOL = true
|
||||
val scope2 = Scope()
|
||||
val t2 = System.nanoTime()
|
||||
val r2 = (scope2.eval(script) as ObjInt).value
|
||||
val t3 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] call-pooling x$n [SCOPE_POOL=ON]: ${(t3 - t2)/1_000_000.0} ms")
|
||||
|
||||
// Each inc3 performs 3 increments per loop
|
||||
val expected = 3L * n
|
||||
assertEquals(expected, r1)
|
||||
assertEquals(expected, r2)
|
||||
|
||||
// Reset flag to default (OFF) to avoid affecting other tests unintentionally
|
||||
PerfFlags.SCOPE_POOL = false
|
||||
}
|
||||
}
|
||||
74
lynglib/src/jvmTest/kotlin/CallSplatBenchmarkTest.kt
Normal file
74
lynglib/src/jvmTest/kotlin/CallSplatBenchmarkTest.kt
Normal file
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* JVM micro-benchmark for calls with splat (spread) arguments.
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.PerfFlags
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.Ignore
|
||||
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
class CallSplatBenchmarkTest {
|
||||
@Test
|
||||
fun benchmarkCallsWithSplatArgs() = runBlocking {
|
||||
val n = 120_000
|
||||
val script = """
|
||||
fun sum4(a,b,c,d) { a + b + c + d }
|
||||
val base = [1,1,1,1]
|
||||
var s = 0
|
||||
var i = 0
|
||||
while (i < $n) {
|
||||
// two direct, one splat per iteration
|
||||
s = s + sum4(1,1,1,1)
|
||||
s = s + sum4(1,1,1,1)
|
||||
s = s + sum4(base[0], base[1], base[2], base[3])
|
||||
i = i + 1
|
||||
}
|
||||
s
|
||||
""".trimIndent()
|
||||
|
||||
// Baseline
|
||||
PerfFlags.ARG_BUILDER = false
|
||||
val scope1 = Scope()
|
||||
val t0 = System.nanoTime()
|
||||
val r1 = (scope1.eval(script) as ObjInt).value
|
||||
val t1 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] splat-calls x$n [ARG_BUILDER=OFF]: ${(t1 - t0)/1_000_000.0} ms")
|
||||
|
||||
// Optimized
|
||||
PerfFlags.ARG_BUILDER = true
|
||||
val scope2 = Scope()
|
||||
val t2 = System.nanoTime()
|
||||
val r2 = (scope2.eval(script) as ObjInt).value
|
||||
val t3 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] splat-calls x$n [ARG_BUILDER=ON]: ${(t3 - t2)/1_000_000.0} ms")
|
||||
|
||||
// Each loop adds (4 + 4 + 4) = 12
|
||||
val expected = 12L * n
|
||||
assertEquals(expected, r1)
|
||||
assertEquals(expected, r2)
|
||||
|
||||
// Reset to default
|
||||
PerfFlags.ARG_BUILDER = true
|
||||
}
|
||||
}
|
||||
85
lynglib/src/jvmTest/kotlin/ConcurrencyCallBenchmarkTest.kt
Normal file
85
lynglib/src/jvmTest/kotlin/ConcurrencyCallBenchmarkTest.kt
Normal file
@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* Multithreaded benchmark to quantify SCOPE_POOL speedup on JVM.
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import net.sergeych.lyng.PerfFlags
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.Ignore
|
||||
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
class ConcurrencyCallBenchmarkTest {
|
||||
|
||||
private suspend fun parallelEval(workers: Int, script: String): List<Long> = coroutineScope {
|
||||
(0 until workers).map { async { (Scope().eval(script) as ObjInt).value } }.awaitAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun benchmark_multithread_calls_off_on() = runBlocking {
|
||||
val cpu = Runtime.getRuntime().availableProcessors()
|
||||
val workers = min(max(2, cpu), 8)
|
||||
val iterations = 15_000 // per worker; keep CI fast
|
||||
val script = """
|
||||
fun f0() { 1 }
|
||||
fun f1(a) { a }
|
||||
fun f2(a,b) { a + b }
|
||||
fun f3(a,b,c) { a + b + c }
|
||||
fun f4(a,b,c,d) { a + b + c + d }
|
||||
var s = 0
|
||||
var i = 0
|
||||
while (i < $iterations) {
|
||||
s = s + f0()
|
||||
s = s + f1(1)
|
||||
s = s + f2(1, 1)
|
||||
s = s + f3(1, 1, 1)
|
||||
s = s + f4(1, 1, 1, 1)
|
||||
i = i + 1
|
||||
}
|
||||
s
|
||||
""".trimIndent()
|
||||
val expected = (1 + 1 + 2 + 3 + 4).toLong() * iterations
|
||||
|
||||
// OFF
|
||||
PerfFlags.SCOPE_POOL = false
|
||||
val t0 = System.nanoTime()
|
||||
val off = withContext(Dispatchers.Default) { parallelEval(workers, script) }
|
||||
val t1 = System.nanoTime()
|
||||
// ON
|
||||
PerfFlags.SCOPE_POOL = true
|
||||
val t2 = System.nanoTime()
|
||||
val on = withContext(Dispatchers.Default) { parallelEval(workers, script) }
|
||||
val t3 = System.nanoTime()
|
||||
// reset
|
||||
PerfFlags.SCOPE_POOL = false
|
||||
|
||||
off.forEach { assertEquals(expected, it) }
|
||||
on.forEach { assertEquals(expected, it) }
|
||||
|
||||
val offMs = (t1 - t0) / 1_000_000.0
|
||||
val onMs = (t3 - t2) / 1_000_000.0
|
||||
val speedup = offMs / onMs
|
||||
println("[DEBUG_LOG] [BENCH] ConcurrencyCallBenchmark workers=$workers iters=$iterations each: OFF=${"%.3f".format(offMs)} ms, ON=${"%.3f".format(onMs)} ms, speedup=${"%.2f".format(speedup)}x")
|
||||
}
|
||||
}
|
||||
95
lynglib/src/jvmTest/kotlin/DeepPoolingStressJvmTest.kt
Normal file
95
lynglib/src/jvmTest/kotlin/DeepPoolingStressJvmTest.kt
Normal file
@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* JVM stress tests for scope frame pooling (deep nesting and recursion).
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.PerfFlags
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.Ignore
|
||||
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
class DeepPoolingStressJvmTest {
|
||||
@Test
|
||||
fun deepNestedCalls_noLeak_and_correct_with_and_without_pooling() = runBlocking {
|
||||
val depth = 200
|
||||
val script = """
|
||||
fun f0(x) { x + 1 }
|
||||
fun f1(x) { f0(x) + 1 }
|
||||
fun f2(x) { f1(x) + 1 }
|
||||
fun f3(x) { f2(x) + 1 }
|
||||
fun f4(x) { f3(x) + 1 }
|
||||
fun f5(x) { f4(x) + 1 }
|
||||
|
||||
fun chain(x, d) {
|
||||
var i = 0
|
||||
var s = x
|
||||
while (i < d) {
|
||||
// 5 nested calls per iteration
|
||||
s = f5(s)
|
||||
i = i + 1
|
||||
}
|
||||
s
|
||||
}
|
||||
chain(0, $depth)
|
||||
""".trimIndent()
|
||||
|
||||
// Pool OFF
|
||||
PerfFlags.SCOPE_POOL = false
|
||||
val scope1 = Scope()
|
||||
val r1 = (scope1.eval(script) as ObjInt).value
|
||||
// Pool ON
|
||||
PerfFlags.SCOPE_POOL = true
|
||||
val scope2 = Scope()
|
||||
val r2 = (scope2.eval(script) as ObjInt).value
|
||||
// Each loop adds 6 (f0..f5 adds 6)
|
||||
val expected = 6L * depth
|
||||
assertEquals(expected, r1)
|
||||
assertEquals(expected, r2)
|
||||
// Reset
|
||||
PerfFlags.SCOPE_POOL = false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun recursion_factorial_correct_with_and_without_pooling() = runBlocking {
|
||||
val n = 10
|
||||
val script = """
|
||||
fun fact(x) {
|
||||
if (x <= 1) 1 else x * fact(x - 1)
|
||||
}
|
||||
fact($n)
|
||||
""".trimIndent()
|
||||
// OFF
|
||||
PerfFlags.SCOPE_POOL = false
|
||||
val scope1 = Scope()
|
||||
val r1 = (scope1.eval(script) as ObjInt).value
|
||||
// ON
|
||||
PerfFlags.SCOPE_POOL = true
|
||||
val scope2 = Scope()
|
||||
val r2 = (scope2.eval(script) as ObjInt).value
|
||||
// 10! = 3628800
|
||||
val expected = 3628800L
|
||||
assertEquals(expected, r1)
|
||||
assertEquals(expected, r2)
|
||||
PerfFlags.SCOPE_POOL = false
|
||||
}
|
||||
}
|
||||
156
lynglib/src/jvmTest/kotlin/ExpressionBenchmarkTest.kt
Normal file
156
lynglib/src/jvmTest/kotlin/ExpressionBenchmarkTest.kt
Normal file
@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* JVM micro-benchmark for expression evaluation with RVAL_FASTPATH.
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.PerfFlags
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.Ignore
|
||||
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
class ExpressionBenchmarkTest {
|
||||
@Test
|
||||
fun benchmarkExpressionChains() = runBlocking {
|
||||
val n = 350_000
|
||||
val script = """
|
||||
// arithmetic + elvis + logical chains
|
||||
val maybe = null
|
||||
var s = 0
|
||||
var i = 0
|
||||
while (i < $n) {
|
||||
// exercise elvis on a null
|
||||
s = s + (maybe ?: 0)
|
||||
// branch using booleans without coercion to int
|
||||
if ((i % 3 == 0 && true) || false) { s = s + 1 } else { s = s + 2 }
|
||||
// parity via arithmetic only (avoid adding booleans)
|
||||
s = s + (i - (i / 2) * 2)
|
||||
i = i + 1
|
||||
}
|
||||
s
|
||||
""".trimIndent()
|
||||
|
||||
// OFF
|
||||
PerfFlags.RVAL_FASTPATH = false
|
||||
val scope1 = Scope()
|
||||
val t0 = System.nanoTime()
|
||||
val r1 = (scope1.eval(script) as ObjInt).value
|
||||
val t1 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] expr-chain x$n [RVAL_FASTPATH=OFF]: ${(t1 - t0)/1_000_000.0} ms")
|
||||
|
||||
// ON
|
||||
PerfFlags.RVAL_FASTPATH = true
|
||||
val scope2 = Scope()
|
||||
val t2 = System.nanoTime()
|
||||
val r2 = (scope2.eval(script) as ObjInt).value
|
||||
val t3 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] expr-chain x$n [RVAL_FASTPATH=ON]: ${(t3 - t2)/1_000_000.0} ms")
|
||||
|
||||
// correctness: compute expected with simple kotlin logic mirroring the loop
|
||||
var s = 0L
|
||||
var i = 0
|
||||
while (i < n) {
|
||||
if ((i % 3 == 0 && true) || false) s += 1 else s += 2
|
||||
// parity via arithmetic only, matches script's single parity addition
|
||||
s += i - (i / 2) * 2
|
||||
i += 1
|
||||
}
|
||||
assertEquals(s, r1)
|
||||
assertEquals(s, r2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun benchmarkListIndexReads() = runBlocking {
|
||||
val n = 350_000
|
||||
val script = """
|
||||
val list = (1..10).toList()
|
||||
var s = 0
|
||||
var i = 0
|
||||
while (i < $n) {
|
||||
// exercise fast index path on ObjList + ObjInt index
|
||||
s = s + list[3]
|
||||
s = s + list[7]
|
||||
i = i + 1
|
||||
}
|
||||
s
|
||||
""".trimIndent()
|
||||
|
||||
// OFF
|
||||
PerfFlags.RVAL_FASTPATH = false
|
||||
val scope1 = Scope()
|
||||
val t0 = System.nanoTime()
|
||||
val r1 = (scope1.eval(script) as ObjInt).value
|
||||
val t1 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] list-index x$n [RVAL_FASTPATH=OFF]: ${(t1 - t0)/1_000_000.0} ms")
|
||||
|
||||
// ON
|
||||
PerfFlags.RVAL_FASTPATH = true
|
||||
val scope2 = Scope()
|
||||
val t2 = System.nanoTime()
|
||||
val r2 = (scope2.eval(script) as ObjInt).value
|
||||
val t3 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] list-index x$n [RVAL_FASTPATH=ON]: ${(t3 - t2)/1_000_000.0} ms")
|
||||
|
||||
// correctness: list = [1..10]; each loop adds list[3]+list[7] = 4 + 8 = 12
|
||||
val expected = 12L * n
|
||||
assertEquals(expected, r1)
|
||||
assertEquals(expected, r2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun benchmarkFieldReadPureReceiver() = runBlocking {
|
||||
val n = 300_000
|
||||
val script = """
|
||||
class C(){ var x = 1; var y = 2 }
|
||||
val c = C()
|
||||
var s = 0
|
||||
var i = 0
|
||||
while (i < $n) {
|
||||
// repeated reads on the same monomorphic receiver
|
||||
s = s + c.x
|
||||
s = s + c.y
|
||||
i = i + 1
|
||||
}
|
||||
s
|
||||
""".trimIndent()
|
||||
|
||||
// OFF
|
||||
PerfFlags.RVAL_FASTPATH = false
|
||||
val scope1 = Scope()
|
||||
val t0 = System.nanoTime()
|
||||
val r1 = (scope1.eval(script) as ObjInt).value
|
||||
val t1 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] field-read x$n [RVAL_FASTPATH=OFF]: ${(t1 - t0)/1_000_000.0} ms")
|
||||
|
||||
// ON
|
||||
PerfFlags.RVAL_FASTPATH = true
|
||||
val scope2 = Scope()
|
||||
val t2 = System.nanoTime()
|
||||
val r2 = (scope2.eval(script) as ObjInt).value
|
||||
val t3 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] field-read x$n [RVAL_FASTPATH=ON]: ${(t3 - t2)/1_000_000.0} ms")
|
||||
|
||||
val expected = (1L + 2L) * n
|
||||
assertEquals(expected, r1)
|
||||
assertEquals(expected, r2)
|
||||
}
|
||||
}
|
||||
139
lynglib/src/jvmTest/kotlin/IndexPicABTest.kt
Normal file
139
lynglib/src/jvmTest/kotlin/IndexPicABTest.kt
Normal file
@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
package net.sergeych.lyng
|
||||
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import java.io.File
|
||||
import kotlin.system.measureNanoTime
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.Ignore
|
||||
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
class IndexPicABTest {
|
||||
|
||||
private fun outFile(): File = File("lynglib/build/index_pic_ab_results.txt")
|
||||
|
||||
private fun writeHeader(f: File) {
|
||||
if (!f.parentFile.exists()) f.parentFile.mkdirs()
|
||||
f.writeText("[DEBUG_LOG] Index PIC A/B results\n")
|
||||
}
|
||||
|
||||
private fun appendLine(f: File, s: String) { f.appendText(s + "\n") }
|
||||
|
||||
private suspend fun buildStringIndexScript(len: Int, iters: Int): Script {
|
||||
// Build a long string and index it by cycling positions
|
||||
val content = (0 until len).joinToString("") { i ->
|
||||
val ch = 'a' + (i % 26)
|
||||
ch.toString()
|
||||
}
|
||||
val src = """
|
||||
val s = "$content"
|
||||
var acc = 0
|
||||
for(i in 0..${iters - 1}) {
|
||||
val j = i % ${len}
|
||||
// Compare to a 1-char string to avoid needing Char.toInt(); still exercises indexing path
|
||||
if (s[j] == "a") { acc += 1 } else { acc += 0 }
|
||||
}
|
||||
acc
|
||||
""".trimIndent()
|
||||
return Compiler.compile(Source("<idx-string>", src), Script.defaultImportManager)
|
||||
}
|
||||
|
||||
private suspend fun buildMapIndexScript(keys: Int, iters: Int): Script {
|
||||
// Build a map of ("kX" -> X) and repeatedly access by key cycling
|
||||
val entries = (0 until keys).joinToString(", ") { i -> "\"k$i\" => $i" }
|
||||
val src = """
|
||||
// Build via Map(entry1, entry2, ...), not a list literal
|
||||
val m = Map($entries)
|
||||
var acc = 0
|
||||
for(i in 0..${iters - 1}) {
|
||||
val k = "k" + (i % ${keys})
|
||||
acc += (m[k] ?: 0)
|
||||
}
|
||||
acc
|
||||
""".trimIndent()
|
||||
return Compiler.compile(Source("<idx-map>", src), Script.defaultImportManager)
|
||||
}
|
||||
|
||||
private suspend fun runOnce(script: Script): Long {
|
||||
val scope = Script.newScope()
|
||||
var result: Obj? = null
|
||||
val t = measureNanoTime { result = script.execute(scope) }
|
||||
if (result !is ObjInt) println("[DEBUG_LOG] result=${result?.javaClass?.simpleName}")
|
||||
return t
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ab_index_pic_and_size() = runTestBlocking {
|
||||
val f = outFile()
|
||||
writeHeader(f)
|
||||
|
||||
val savedIndexPic = PerfFlags.INDEX_PIC
|
||||
val savedIndexSize4 = PerfFlags.INDEX_PIC_SIZE_4
|
||||
val savedCounters = PerfFlags.PIC_DEBUG_COUNTERS
|
||||
|
||||
try {
|
||||
val iters = 300_000
|
||||
val sLen = 512
|
||||
val mapKeys = 256
|
||||
val sScript = buildStringIndexScript(sLen, iters)
|
||||
val mScript = buildMapIndexScript(mapKeys, iters)
|
||||
|
||||
fun header(which: String) { appendLine(f, "[DEBUG_LOG] A/B on $which (iters=$iters)") }
|
||||
|
||||
// Baseline OFF
|
||||
PerfFlags.PIC_DEBUG_COUNTERS = true
|
||||
PerfStats.resetAll()
|
||||
PerfFlags.INDEX_PIC = false
|
||||
PerfFlags.INDEX_PIC_SIZE_4 = false
|
||||
header("String[Int], INDEX_PIC=OFF")
|
||||
val tSOff = runOnce(sScript)
|
||||
header("Map[String], INDEX_PIC=OFF")
|
||||
val tMOff = runOnce(mScript)
|
||||
appendLine(f, "[DEBUG_LOG] OFF counters: indexHit=${PerfStats.indexPicHit} indexMiss=${PerfStats.indexPicMiss}")
|
||||
|
||||
// PIC ON, size 2
|
||||
PerfStats.resetAll()
|
||||
PerfFlags.INDEX_PIC = true
|
||||
PerfFlags.INDEX_PIC_SIZE_4 = false
|
||||
val tSOn2 = runOnce(sScript)
|
||||
val tMOn2 = runOnce(mScript)
|
||||
appendLine(f, "[DEBUG_LOG] ON size=2 counters: indexHit=${PerfStats.indexPicHit} indexMiss=${PerfStats.indexPicMiss}")
|
||||
|
||||
// PIC ON, size 4
|
||||
PerfStats.resetAll()
|
||||
PerfFlags.INDEX_PIC = true
|
||||
PerfFlags.INDEX_PIC_SIZE_4 = true
|
||||
val tSOn4 = runOnce(sScript)
|
||||
val tMOn4 = runOnce(mScript)
|
||||
appendLine(f, "[DEBUG_LOG] ON size=4 counters: indexHit=${PerfStats.indexPicHit} indexMiss=${PerfStats.indexPicMiss}")
|
||||
|
||||
// Report
|
||||
appendLine(f, "[DEBUG_LOG] String[Int] OFF=${tSOff} ns, ON(2)=${tSOn2} ns, ON(4)=${tSOn4} ns")
|
||||
appendLine(f, "[DEBUG_LOG] Map[String] OFF=${tMOff} ns, ON(2)=${tMOn2} ns, ON(4)=${tMOn4} ns")
|
||||
} finally {
|
||||
PerfFlags.INDEX_PIC = savedIndexPic
|
||||
PerfFlags.INDEX_PIC_SIZE_4 = savedIndexSize4
|
||||
PerfFlags.PIC_DEBUG_COUNTERS = savedCounters
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun runTestBlocking(block: suspend () -> Unit) {
|
||||
kotlinx.coroutines.runBlocking { block() }
|
||||
}
|
||||
141
lynglib/src/jvmTest/kotlin/IndexWritePathABTest.kt
Normal file
141
lynglib/src/jvmTest/kotlin/IndexWritePathABTest.kt
Normal file
@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng
|
||||
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import java.io.File
|
||||
import kotlin.system.measureNanoTime
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.Ignore
|
||||
|
||||
/**
|
||||
* A/B micro-benchmark for index WRITE paths (Map[String] put, List[Int] set).
|
||||
* Measures OFF vs ON for INDEX_PIC and then size 2 vs 4 (INDEX_PIC_SIZE_4).
|
||||
* Produces [DEBUG_LOG] output in lynglib/build/index_write_ab_results.txt
|
||||
*/
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
class IndexWritePathABTest {
|
||||
|
||||
private fun outFile(): File = File("lynglib/build/index_write_ab_results.txt")
|
||||
|
||||
private fun writeHeader(f: File) {
|
||||
if (!f.parentFile.exists()) f.parentFile.mkdirs()
|
||||
f.writeText("[DEBUG_LOG] Index WRITE PIC A/B results\n")
|
||||
}
|
||||
|
||||
private fun appendLine(f: File, s: String) { f.appendText(s + "\n") }
|
||||
|
||||
private suspend fun buildMapWriteScript(keys: Int, iters: Int): Script {
|
||||
// Construct map with keys k0..k{keys-1} and then perform writes in a tight loop
|
||||
val initEntries = (0 until keys).joinToString(", ") { i -> "\"k$i\" => $i" }
|
||||
val src = """
|
||||
var acc = 0
|
||||
val m = Map($initEntries)
|
||||
for(i in 0..${iters - 1}) {
|
||||
val k = "k" + (i % $keys)
|
||||
m[k] = i
|
||||
acc += (m[k] ?: 0)
|
||||
}
|
||||
acc
|
||||
""".trimIndent()
|
||||
return Compiler.compile(Source("<idx-map-write>", src), Script.defaultImportManager)
|
||||
}
|
||||
|
||||
private suspend fun buildListWriteScript(len: Int, iters: Int): Script {
|
||||
val initList = (0 until len).joinToString(", ") { i -> i.toString() }
|
||||
val src = """
|
||||
var acc = 0
|
||||
val a = [$initList]
|
||||
for(i in 0..${iters - 1}) {
|
||||
val j = i % $len
|
||||
a[j] = i
|
||||
acc += a[j]
|
||||
}
|
||||
acc
|
||||
""".trimIndent()
|
||||
return Compiler.compile(Source("<idx-list-write>", src), Script.defaultImportManager)
|
||||
}
|
||||
|
||||
private suspend fun runOnce(script: Script): Long {
|
||||
val scope = Script.newScope()
|
||||
var result: Obj? = null
|
||||
val t = measureNanoTime { result = script.execute(scope) }
|
||||
if (result !is ObjInt) println("[DEBUG_LOG] result=${result?.javaClass?.simpleName}")
|
||||
return t
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ab_index_write_paths() = runTestBlocking {
|
||||
val f = outFile()
|
||||
writeHeader(f)
|
||||
|
||||
val savedIndexPic = PerfFlags.INDEX_PIC
|
||||
val savedIndexSize4 = PerfFlags.INDEX_PIC_SIZE_4
|
||||
val savedCounters = PerfFlags.PIC_DEBUG_COUNTERS
|
||||
|
||||
try {
|
||||
val iters = 250_000
|
||||
val mapKeys = 256
|
||||
val listLen = 1024
|
||||
val mScript = buildMapWriteScript(mapKeys, iters)
|
||||
val lScript = buildListWriteScript(listLen, iters)
|
||||
|
||||
fun header(which: String) { appendLine(f, "[DEBUG_LOG] A/B on $which (iters=$iters)") }
|
||||
|
||||
// Baseline OFF
|
||||
PerfFlags.PIC_DEBUG_COUNTERS = true
|
||||
PerfStats.resetAll()
|
||||
PerfFlags.INDEX_PIC = false
|
||||
PerfFlags.INDEX_PIC_SIZE_4 = false
|
||||
header("Map[String] write, INDEX_PIC=OFF")
|
||||
val tMOff = runOnce(mScript)
|
||||
header("List[Int] write, INDEX_PIC=OFF")
|
||||
val tLOff = runOnce(lScript)
|
||||
appendLine(f, "[DEBUG_LOG] OFF counters: indexHit=${PerfStats.indexPicHit} indexMiss=${PerfStats.indexPicMiss}")
|
||||
|
||||
// PIC ON, size 2
|
||||
PerfStats.resetAll()
|
||||
PerfFlags.INDEX_PIC = true
|
||||
PerfFlags.INDEX_PIC_SIZE_4 = false
|
||||
val tMOn2 = runOnce(mScript)
|
||||
val tLOn2 = runOnce(lScript)
|
||||
appendLine(f, "[DEBUG_LOG] ON size=2 counters: indexHit=${PerfStats.indexPicHit} indexMiss=${PerfStats.indexPicMiss}")
|
||||
|
||||
// PIC ON, size 4
|
||||
PerfStats.resetAll()
|
||||
PerfFlags.INDEX_PIC = true
|
||||
PerfFlags.INDEX_PIC_SIZE_4 = true
|
||||
val tMOn4 = runOnce(mScript)
|
||||
val tLOn4 = runOnce(lScript)
|
||||
appendLine(f, "[DEBUG_LOG] ON size=4 counters: indexHit=${PerfStats.indexPicHit} indexMiss=${PerfStats.indexPicMiss}")
|
||||
|
||||
// Report
|
||||
appendLine(f, "[DEBUG_LOG] Map[String] WRITE OFF=$tMOff ns, ON(2)=$tMOn2 ns, ON(4)=$tMOn4 ns")
|
||||
appendLine(f, "[DEBUG_LOG] List[Int] WRITE OFF=$tLOff ns, ON(2)=$tLOn2 ns, ON(4)=$tLOn4 ns")
|
||||
} finally {
|
||||
PerfFlags.INDEX_PIC = savedIndexPic
|
||||
PerfFlags.INDEX_PIC_SIZE_4 = savedIndexSize4
|
||||
PerfFlags.PIC_DEBUG_COUNTERS = savedCounters
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun runTestBlocking(block: suspend () -> Unit) {
|
||||
kotlinx.coroutines.runBlocking { block() }
|
||||
}
|
||||
103
lynglib/src/jvmTest/kotlin/ListOpsBenchmarkTest.kt
Normal file
103
lynglib/src/jvmTest/kotlin/ListOpsBenchmarkTest.kt
Normal file
@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* JVM micro-benchmark for list operations specialization under PRIMITIVE_FASTOPS.
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.PerfFlags
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.Ignore
|
||||
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
class ListOpsBenchmarkTest {
|
||||
@Test
|
||||
fun benchmarkSumInts() = runBlocking {
|
||||
val n = 200_000
|
||||
val script = """
|
||||
val list = (1..10).toList()
|
||||
var s = 0
|
||||
var i = 0
|
||||
while (i < $n) {
|
||||
// list.sum() should return 55 for [1..10]
|
||||
s = s + list.sum()
|
||||
i = i + 1
|
||||
}
|
||||
s
|
||||
""".trimIndent()
|
||||
|
||||
// OFF
|
||||
PerfFlags.PRIMITIVE_FASTOPS = false
|
||||
val scope1 = Scope()
|
||||
val t0 = System.nanoTime()
|
||||
val r1 = (scope1.eval(script) as ObjInt).value
|
||||
val t1 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] list-sum x$n [PRIMITIVE_FASTOPS=OFF]: ${(t1 - t0)/1_000_000.0} ms")
|
||||
|
||||
// ON
|
||||
PerfFlags.PRIMITIVE_FASTOPS = true
|
||||
val scope2 = Scope()
|
||||
val t2 = System.nanoTime()
|
||||
val r2 = (scope2.eval(script) as ObjInt).value
|
||||
val t3 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] list-sum x$n [PRIMITIVE_FASTOPS=ON]: ${(t3 - t2)/1_000_000.0} ms")
|
||||
|
||||
val expected = 55L * n
|
||||
assertEquals(expected, r1)
|
||||
assertEquals(expected, r2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun benchmarkContainsInts() = runBlocking {
|
||||
val n = 1_000_000
|
||||
val script = """
|
||||
val list = (1..10).toList()
|
||||
var s = 0
|
||||
var i = 0
|
||||
while (i < $n) {
|
||||
if (7 in list) { s = s + 1 }
|
||||
i = i + 1
|
||||
}
|
||||
s
|
||||
""".trimIndent()
|
||||
|
||||
// OFF
|
||||
PerfFlags.PRIMITIVE_FASTOPS = false
|
||||
val scope1 = Scope()
|
||||
val t0 = System.nanoTime()
|
||||
val r1 = (scope1.eval(script) as ObjInt).value
|
||||
val t1 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] list-contains x$n [PRIMITIVE_FASTOPS=OFF]: ${(t1 - t0)/1_000_000.0} ms")
|
||||
|
||||
// ON
|
||||
PerfFlags.PRIMITIVE_FASTOPS = true
|
||||
val scope2 = Scope()
|
||||
val t2 = System.nanoTime()
|
||||
val r2 = (scope2.eval(script) as ObjInt).value
|
||||
val t3 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] list-contains x$n [PRIMITIVE_FASTOPS=ON]: ${(t3 - t2)/1_000_000.0} ms")
|
||||
|
||||
// 7 in [1..10] is always true
|
||||
val expected = 1L * n
|
||||
assertEquals(expected, r1)
|
||||
assertEquals(expected, r2)
|
||||
}
|
||||
}
|
||||
77
lynglib/src/jvmTest/kotlin/LocalVarBenchmarkTest.kt
Normal file
77
lynglib/src/jvmTest/kotlin/LocalVarBenchmarkTest.kt
Normal file
@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* JVM micro-benchmark focused on local variable access paths:
|
||||
* - LOCAL_SLOT_PIC (per-frame slot PIC in LocalVarRef)
|
||||
* - EMIT_FAST_LOCAL_REFS (compiler-emitted fast locals)
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.PerfFlags
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.Ignore
|
||||
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
class LocalVarBenchmarkTest {
|
||||
@Test
|
||||
fun benchmarkLocalReadsWrites_off_on() = runBlocking {
|
||||
val iterations = 400_000
|
||||
val script = """
|
||||
fun hot(n){
|
||||
var a = 0
|
||||
var b = 1
|
||||
var c = 2
|
||||
var s = 0
|
||||
var i = 0
|
||||
while(i < n){
|
||||
a = a + 1
|
||||
b = b + a
|
||||
c = c + b
|
||||
s = s + a + b + c
|
||||
i = i + 1
|
||||
}
|
||||
s
|
||||
}
|
||||
hot($iterations)
|
||||
""".trimIndent()
|
||||
|
||||
// Baseline: disable both fast paths
|
||||
PerfFlags.LOCAL_SLOT_PIC = false
|
||||
PerfFlags.EMIT_FAST_LOCAL_REFS = false
|
||||
val scope1 = Scope()
|
||||
val t0 = System.nanoTime()
|
||||
val r1 = (scope1.eval(script) as ObjInt).value
|
||||
val t1 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] locals x$iterations [PIC=OFF, FAST_LOCAL=OFF]: ${(t1 - t0)/1_000_000.0} ms")
|
||||
|
||||
// Optimized: enable both
|
||||
PerfFlags.LOCAL_SLOT_PIC = true
|
||||
PerfFlags.EMIT_FAST_LOCAL_REFS = true
|
||||
val scope2 = Scope()
|
||||
val t2 = System.nanoTime()
|
||||
val r2 = (scope2.eval(script) as ObjInt).value
|
||||
val t3 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] locals x$iterations [PIC=ON, FAST_LOCAL=ON]: ${(t3 - t2)/1_000_000.0} ms")
|
||||
|
||||
// Correctness: both runs produce the same result
|
||||
assertEquals(r1, r2)
|
||||
}
|
||||
}
|
||||
@ -25,13 +25,13 @@ import net.sergeych.lyng.obj.*
|
||||
import net.sergeych.lynon.*
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import kotlin.time.Instant
|
||||
import kotlin.test.Ignore
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
class LynonTests {
|
||||
|
||||
@Test
|
||||
@ -330,11 +330,13 @@ class LynonTests {
|
||||
eval(
|
||||
"""
|
||||
import lyng.serialization
|
||||
import lyng.stdlib
|
||||
fun testEncode(value) {
|
||||
val encoded = Lynon.encode(value)
|
||||
val decoded = Lynon.decode(encoded)
|
||||
assertEquals(value, decoded)
|
||||
println(encoded.toDump())
|
||||
println("Encoded size %d: %s"(encoded.size, value))
|
||||
Lynon.decode(encoded).also {
|
||||
assertEquals( value, it )
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
@ -353,45 +355,27 @@ class LynonTests {
|
||||
|
||||
@Test
|
||||
fun testSimpleTypes() = runTest {
|
||||
val scope = Scope()
|
||||
suspend fun roundTrip(obj: Obj) {
|
||||
val encoded = ObjLynonClass.encodeAny(scope, obj)
|
||||
val decoded = ObjLynonClass.decodeAny(scope, encoded)
|
||||
assertTrue(obj.equals(scope, decoded))
|
||||
}
|
||||
testScope().eval(
|
||||
"""
|
||||
testEncode(null)
|
||||
testEncode(0)
|
||||
testEncode(47)
|
||||
testEncode(-21)
|
||||
testEncode(true)
|
||||
testEncode(false)
|
||||
testEncode(1.22345)
|
||||
testEncode(-π)
|
||||
|
||||
roundTrip(ObjNull)
|
||||
roundTrip(ObjInt.Zero)
|
||||
roundTrip(ObjInt(47))
|
||||
roundTrip(ObjInt(-21))
|
||||
roundTrip(ObjTrue)
|
||||
roundTrip(ObjFalse)
|
||||
roundTrip(ObjReal(1.22345))
|
||||
roundTrip(ObjReal(-Math.PI))
|
||||
import lyng.time
|
||||
testEncode(Instant.now().truncateToSecond())
|
||||
testEncode(Instant.now().truncateToMillisecond())
|
||||
testEncode(Instant.now().truncateToMicrosecond())
|
||||
|
||||
val now = Instant.fromEpochMilliseconds(System.currentTimeMillis())
|
||||
roundTrip(ObjInstant(Instant.fromEpochSeconds(now.epochSeconds), LynonSettings.InstantTruncateMode.Second))
|
||||
roundTrip(
|
||||
ObjInstant(
|
||||
Instant.fromEpochSeconds(
|
||||
now.epochSeconds,
|
||||
now.nanosecondsOfSecond / 1_000_000 * 1_000_000
|
||||
),
|
||||
LynonSettings.InstantTruncateMode.Millisecond
|
||||
)
|
||||
testEncode("Hello, world".encodeUtf8())
|
||||
testEncode("Hello, world")
|
||||
|
||||
""".trimIndent()
|
||||
)
|
||||
roundTrip(
|
||||
ObjInstant(
|
||||
Instant.fromEpochSeconds(
|
||||
now.epochSeconds,
|
||||
now.nanosecondsOfSecond / 1_000 * 1_000
|
||||
),
|
||||
LynonSettings.InstantTruncateMode.Microsecond
|
||||
)
|
||||
)
|
||||
|
||||
roundTrip(ObjBuffer("Hello, world".encodeToByteArray().toUByteArray()))
|
||||
roundTrip(ObjString("Hello, world"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -585,6 +569,7 @@ class LynonTests {
|
||||
val s = testScope()
|
||||
s.eval("""
|
||||
testEncode( Map("one" => 1, "two" => 2) )
|
||||
testEncode( Map() )
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@ -594,16 +579,18 @@ class LynonTests {
|
||||
s.eval("""
|
||||
testEncode(["one", 2])
|
||||
testEncode([1, "2"])
|
||||
testEncode({ "one": 1, "two": "2" })
|
||||
testEncode( Map("one" => 1, 2 => 2) )
|
||||
testEncode( Map("one" => 1, 2 => "2") )
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSetSerialization() = runTest {
|
||||
testScope().eval("""
|
||||
testEncode([ "one", "two" ].toSet())
|
||||
testEncode([ 1, "one", false ].toSet())
|
||||
testEncode([ true, true, false ].toSet())
|
||||
testEncode( Set("one", "two") )
|
||||
testEncode( Set() )
|
||||
testEncode( Set(1, "one", false) )
|
||||
testEncode( Set(true, true, false) )
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@ -693,11 +680,13 @@ class Wallet( id, ownerKey, balance=0, createdAt=Instant.now().truncateToSecond(
|
||||
// it is not random, but it _is_ unique, and we use it as a walletId.
|
||||
val newId = Buffer("testid")
|
||||
|
||||
val w: Wallet = Wallet(newId, ownerKey)
|
||||
val w = Wallet(newId, ownerKey)
|
||||
println(w)
|
||||
println(w.balance)
|
||||
|
||||
val t: Wallet = Lynon.decode(Lynon.encode(Wallet(newId, ownerKey))) as Wallet
|
||||
val x = Lynon.encode(Wallet(newId, ownerKey) ).toBuffer()
|
||||
val t = Lynon.decode(x.toBitInput())
|
||||
println(x)
|
||||
println(t)
|
||||
assertEquals(w.balance, t.balance)
|
||||
w
|
||||
@ -715,52 +704,82 @@ class Wallet( id, ownerKey, balance=0, createdAt=Instant.now().truncateToSecond(
|
||||
|
||||
|
||||
@Test
|
||||
@Ignore("TODO(bytecode-only): MI serialization fallback")
|
||||
fun testMISerialization() = runTest {
|
||||
val scope = testScope()
|
||||
scope.eval(
|
||||
"""
|
||||
val s = testScope()
|
||||
s.eval("""
|
||||
import lyng.serialization
|
||||
|
||||
class Point(x,y)
|
||||
class Color(r,g,b)
|
||||
class ColoredPoint(px, py, cr, cg, cb): Point(px,py), Color(cr,cg,cb)
|
||||
""".trimIndent()
|
||||
)
|
||||
val cp = scope.eval("ColoredPoint(1,2,30,40,50)") as ObjInstance
|
||||
val decoded = ObjLynonClass.decodeAny(scope, ObjLynonClass.encodeAny(scope, cp))
|
||||
val decodedInstance = decoded as ObjInstance
|
||||
assertEquals(cp.objClass.className, decodedInstance.objClass.className)
|
||||
|
||||
class ColoredPoint(x, y, r, g, b): Point(x,y), Color(r,g,b)
|
||||
|
||||
val cp = ColoredPoint(1,2,30,40,50)
|
||||
val d = testEncode( cp )
|
||||
assert(d is ColoredPoint)
|
||||
assert(d is Point)
|
||||
assert(d is Color)
|
||||
val p = d as Point
|
||||
val c = d as Color
|
||||
val cp2 = ColoredPoint(p.x, p.y, c.r, c.g, c.b)
|
||||
assertEquals(cp, cp2)
|
||||
""")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testClassSerializationSizes() = runTest {
|
||||
val scope = testScope()
|
||||
scope.eval(
|
||||
"""
|
||||
testScope().eval("""
|
||||
class Point(x=0,y=0)
|
||||
// 1 bit - nonnull
|
||||
// 4 bits type record
|
||||
// 8 bits size (5)
|
||||
// 1 bit uncompressed
|
||||
// 40 bits "Point"
|
||||
// 54 total:
|
||||
assertEquals( 54, Lynon.encode("Point").size )
|
||||
assertEquals( 7, Lynon.encode("Point").toBuffer().size )
|
||||
|
||||
// 1 bit - nonnull
|
||||
// 4 bits type record
|
||||
assertEquals( 5, Lynon.encode(0).size )
|
||||
|
||||
class Empty()
|
||||
class Poin2(x=0,y=0) { val z = x + y }
|
||||
class Poin3(x=0,y=0) { var z = x + y }
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
suspend fun encBits(obj: Obj): Long = ObjLynonClass.encodeAny(scope, obj).bitArray.size
|
||||
suspend fun encBytes(obj: Obj): Long = ObjLynonClass.encodeAny(scope, obj).bitArray.asUByteArray().size.toLong()
|
||||
|
||||
assertEquals(54L, encBits(ObjString("Point")))
|
||||
assertEquals(7L, encBytes(ObjString("Point")))
|
||||
assertEquals(5L, encBits(ObjInt.Zero))
|
||||
|
||||
val empty = scope.eval("Empty()")
|
||||
assertEquals(64L, encBits(empty))
|
||||
val emptyDecoded = ObjLynonClass.decodeAny(scope, ObjLynonClass.encodeAny(scope, empty))
|
||||
assertTrue(empty.equals(scope, emptyDecoded))
|
||||
|
||||
assertEquals(70L, encBits(scope.eval("Point()")))
|
||||
assertEquals(86L, encBits(scope.eval("Point(1,1)")))
|
||||
assertEquals(86L, encBits(scope.eval("Point(1,2)")))
|
||||
assertEquals(86L, encBits(scope.eval("Poin2(1,2)")))
|
||||
assertEquals(27L, encBits(scope.eval("[3]")))
|
||||
assertTrue(encBits(scope.eval("Poin3(1,2)")) <= 110L)
|
||||
// 1 bit non-null
|
||||
// 4 bits type record
|
||||
// 54 bits "Empty"
|
||||
// 4 bits list size
|
||||
// dont know where 1 bit for not cached
|
||||
assertEquals( 64, Lynon.encode(Empty()).size )
|
||||
assertEquals( Empty(), Lynon.decode(Lynon.encode(Empty())) )
|
||||
|
||||
// Here the situation is dofferent: we have 2 in zeroes plus int size, but cache shrinks it
|
||||
assertEquals( 70, Lynon.encode(Point()).size )
|
||||
// two 1's added 16 bit (each short int is 8 bits)
|
||||
assertEquals( 86, Lynon.encode(Point(1,1)).size )
|
||||
assertEquals( 86, Lynon.encode(Point(1,2)).size )
|
||||
|
||||
// Now let's make it more complex: we add 1 var to save:
|
||||
class Poin2(x=0,y=0) {
|
||||
val z = x + y
|
||||
}
|
||||
// val must not be serialized so no change here:
|
||||
assertEquals( 86, Lynon.encode(Poin2(1,2)).size )
|
||||
|
||||
// lets check size of homogenous list of one small int
|
||||
// 8 bits 3
|
||||
// 4 bits type
|
||||
// 8 bits list size
|
||||
// 2 bits not cached and not null
|
||||
// 4 bits element type
|
||||
assertEquals( 27, Lynon.encode([3]).size)
|
||||
|
||||
class Poin3(x=0,y=0) {
|
||||
var z = x + y
|
||||
}
|
||||
// var must be serialized, but caching could reduce size:
|
||||
assert( Lynon.encode(Poin3(1,2)).size <= 110)
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
69
lynglib/src/jvmTest/kotlin/MethodPoolingBenchmarkTest.kt
Normal file
69
lynglib/src/jvmTest/kotlin/MethodPoolingBenchmarkTest.kt
Normal file
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* JVM micro-benchmark for scope frame pooling on instance method calls.
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.PerfFlags
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.Ignore
|
||||
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
class MethodPoolingBenchmarkTest {
|
||||
@Test
|
||||
fun benchmarkInstanceMethodCallsWithPooling() = runBlocking {
|
||||
val n = 300_000
|
||||
val script = """
|
||||
class C() {
|
||||
var x = 0
|
||||
fun add1() { x = x + 1 }
|
||||
fun get() { x }
|
||||
}
|
||||
val c = C()
|
||||
var i = 0
|
||||
while (i < $n) {
|
||||
c.add1()
|
||||
i = i + 1
|
||||
}
|
||||
c.get()
|
||||
""".trimIndent()
|
||||
|
||||
// Pool OFF
|
||||
PerfFlags.SCOPE_POOL = false
|
||||
val scope1 = Scope()
|
||||
val t0 = System.nanoTime()
|
||||
val r1 = (scope1.eval(script) as ObjInt).value
|
||||
val t1 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] method-loop x$n [SCOPE_POOL=OFF]: ${(t1 - t0)/1_000_000.0} ms")
|
||||
|
||||
// Pool ON
|
||||
PerfFlags.SCOPE_POOL = true
|
||||
val scope2 = Scope()
|
||||
val t2 = System.nanoTime()
|
||||
val r2 = (scope2.eval(script) as ObjInt).value
|
||||
val t3 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] method-loop x$n [SCOPE_POOL=ON]: ${(t3 - t2)/1_000_000.0} ms")
|
||||
|
||||
assertEquals(n.toLong(), r1)
|
||||
assertEquals(n.toLong(), r2)
|
||||
}
|
||||
}
|
||||
100
lynglib/src/jvmTest/kotlin/MixedBenchmarkTest.kt
Normal file
100
lynglib/src/jvmTest/kotlin/MixedBenchmarkTest.kt
Normal file
@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* JVM mixed workload micro-benchmark to exercise multiple hot paths together.
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.PerfFlags
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.Ignore
|
||||
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
class MixedBenchmarkTest {
|
||||
@Test
|
||||
fun benchmarkMixedWorkloadRvalFastpath() = runBlocking {
|
||||
// Keep iterations moderate to avoid CI timeouts
|
||||
val n = 250_000
|
||||
val script = """
|
||||
class Acc() {
|
||||
var x = 0
|
||||
fun add(v) { x = x + v }
|
||||
fun get() { x }
|
||||
}
|
||||
val acc = Acc()
|
||||
val maybe = null
|
||||
var s = 0
|
||||
var i = 0
|
||||
while (i < $n) {
|
||||
// exercise locals + primitive ops
|
||||
s = s + i
|
||||
// elvis on null
|
||||
s = s + (maybe ?: 0)
|
||||
// boolean logic (short-circuit + primitive fast path)
|
||||
if ((i % 3 == 0 && true) || false) { s = s + 1 } else { s = s + 2 }
|
||||
// instance field/method with PIC
|
||||
acc.add(1)
|
||||
// simple index with list building every 1024 steps (rare path)
|
||||
if (i % 1024 == 0) {
|
||||
val lst = [0,1,2,3]
|
||||
s = s + lst[2]
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
s + acc.get()
|
||||
""".trimIndent()
|
||||
|
||||
// OFF
|
||||
PerfFlags.RVAL_FASTPATH = false
|
||||
val scope1 = Scope()
|
||||
val t0 = System.nanoTime()
|
||||
val r1 = (scope1.eval(script) as ObjInt).value
|
||||
val t1 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] mixed x$n [RVAL_FASTPATH=OFF]: ${(t1 - t0)/1_000_000.0} ms")
|
||||
|
||||
// ON
|
||||
PerfFlags.RVAL_FASTPATH = true
|
||||
val scope2 = Scope()
|
||||
val t2 = System.nanoTime()
|
||||
val r2 = (scope2.eval(script) as ObjInt).value
|
||||
val t3 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] mixed x$n [RVAL_FASTPATH=ON]: ${(t3 - t2)/1_000_000.0} ms")
|
||||
|
||||
// Compute expected value in Kotlin to ensure correctness
|
||||
var s = 0L
|
||||
var i = 0
|
||||
var acc = 0L
|
||||
while (i < n) {
|
||||
s += i
|
||||
s += 0 // (maybe ?: 0)
|
||||
if ((i % 3 == 0 && true) || false) s += 1 else s += 2
|
||||
acc += 1
|
||||
if (i % 1024 == 0) s += 2
|
||||
i += 1
|
||||
}
|
||||
val expected = s + acc
|
||||
assertEquals(expected, r1)
|
||||
assertEquals(expected, r2)
|
||||
|
||||
// Reset flag for other tests
|
||||
PerfFlags.RVAL_FASTPATH = false
|
||||
}
|
||||
}
|
||||
116
lynglib/src/jvmTest/kotlin/MultiThreadPoolingStressJvmTest.kt
Normal file
116
lynglib/src/jvmTest/kotlin/MultiThreadPoolingStressJvmTest.kt
Normal file
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* Multithreaded stress tests for ScopePool on JVM.
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import net.sergeych.lyng.PerfFlags
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.Ignore
|
||||
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
class MultiThreadPoolingStressJvmTest {
|
||||
|
||||
private suspend fun parallelEval(workers: Int, block: suspend (Int) -> Long): List<Long> = coroutineScope {
|
||||
(0 until workers).map { w -> async { block(w) } }.awaitAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parallel_shallow_calls_correct_off_on() = runBlocking {
|
||||
val cpu = Runtime.getRuntime().availableProcessors()
|
||||
val workers = min(max(2, cpu), 8)
|
||||
val iterations = 25_000 // keep CI reasonable
|
||||
val script = """
|
||||
fun f0(a){ a }
|
||||
fun f1(a,b){ a + b }
|
||||
fun f2(a,b,c){ a + b + c }
|
||||
var s = 0
|
||||
var i = 0
|
||||
while(i < $iterations){
|
||||
s = s + f0(1)
|
||||
s = s + f1(1,1)
|
||||
s = s + f2(1,1,1)
|
||||
i = i + 1
|
||||
}
|
||||
s
|
||||
""".trimIndent()
|
||||
|
||||
fun expected() = (1 + 2 + 3).toLong() * iterations
|
||||
|
||||
// OFF
|
||||
PerfFlags.SCOPE_POOL = false
|
||||
val offResults = withContext(Dispatchers.Default) {
|
||||
parallelEval(workers) {
|
||||
val r = (Scope().eval(script) as ObjInt).value
|
||||
r
|
||||
}
|
||||
}
|
||||
// ON
|
||||
PerfFlags.SCOPE_POOL = true
|
||||
val onResults = withContext(Dispatchers.Default) {
|
||||
parallelEval(workers) {
|
||||
val r = (Scope().eval(script) as ObjInt).value
|
||||
r
|
||||
}
|
||||
}
|
||||
// reset
|
||||
PerfFlags.SCOPE_POOL = false
|
||||
|
||||
val exp = expected()
|
||||
offResults.forEach { assertEquals(exp, it) }
|
||||
onResults.forEach { assertEquals(exp, it) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parallel_recursion_correct_off_on() = runBlocking {
|
||||
val cpu = Runtime.getRuntime().availableProcessors()
|
||||
val workers = min(max(2, cpu), 8)
|
||||
val depth = 12
|
||||
val script = """
|
||||
fun fact(x){ if(x <= 1) 1 else x * fact(x-1) }
|
||||
fact($depth)
|
||||
""".trimIndent()
|
||||
val expected = (1..depth).fold(1L){a,b->a*b}
|
||||
|
||||
// OFF
|
||||
PerfFlags.SCOPE_POOL = false
|
||||
val offResults = withContext(Dispatchers.Default) {
|
||||
parallelEval(workers) {
|
||||
(Scope().eval(script) as ObjInt).value
|
||||
}
|
||||
}
|
||||
// ON
|
||||
PerfFlags.SCOPE_POOL = true
|
||||
val onResults = withContext(Dispatchers.Default) {
|
||||
parallelEval(workers) {
|
||||
(Scope().eval(script) as ObjInt).value
|
||||
}
|
||||
}
|
||||
// reset
|
||||
PerfFlags.SCOPE_POOL = false
|
||||
|
||||
offResults.forEach { assertEquals(expected, it) }
|
||||
onResults.forEach { assertEquals(expected, it) }
|
||||
}
|
||||
}
|
||||
104
lynglib/src/jvmTest/kotlin/OtherTests.kt
Normal file
104
lynglib/src/jvmTest/kotlin/OtherTests.kt
Normal file
@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*
|
||||
*/
|
||||
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.ModuleScope
|
||||
import net.sergeych.lyng.Source
|
||||
import net.sergeych.lyng.eval
|
||||
import net.sergeych.lyng.pacman.InlineSourcesImportProvider
|
||||
import net.sergeych.lyng.toSource
|
||||
import net.sergeych.lynon.BitArray
|
||||
import net.sergeych.lynon.BitList
|
||||
import kotlin.test.Ignore
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertNotEquals
|
||||
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
class OtherTests {
|
||||
@Test
|
||||
fun testImports3() = runBlocking {
|
||||
val foosrc = """
|
||||
package lyng.foo
|
||||
|
||||
import lyng.bar
|
||||
|
||||
fun foo() { "foo1" }
|
||||
""".trimIndent()
|
||||
val barsrc = """
|
||||
package lyng.bar
|
||||
|
||||
fun bar() { "bar1" }
|
||||
""".trimIndent()
|
||||
val pm = InlineSourcesImportProvider(
|
||||
listOf(
|
||||
Source("foosrc", foosrc),
|
||||
Source("barsrc", barsrc),
|
||||
))
|
||||
|
||||
val src = """
|
||||
import lyng.foo
|
||||
|
||||
foo() + " / " + bar()
|
||||
""".trimIndent().toSource("test")
|
||||
|
||||
val scope = ModuleScope(pm, src)
|
||||
assertEquals("foo1 / bar1", scope.eval(src).toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInstantTruncation() = runBlocking {
|
||||
eval("""
|
||||
import lyng.time
|
||||
val t1 = Instant()
|
||||
val t2 = Instant()
|
||||
// assert( t1 != t2 )
|
||||
println(t1 - t2)
|
||||
""".trimIndent())
|
||||
Unit
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBitArrayEqAndHash() {
|
||||
|
||||
val b1 = BitArray.ofBits(1, 0, 1, 1)
|
||||
val b11 = BitArray.ofBits(1, 0, 1, 1)
|
||||
val b2 = BitArray.ofBits(1, 1, 1, 1)
|
||||
val b3 = BitArray.ofBits(1, 0, 1, 1, 0)
|
||||
|
||||
assert( b3 > b1 )
|
||||
assert( b2 > b1)
|
||||
assert( b11.compareTo(b1) == 0)
|
||||
|
||||
assertEquals(b1, b11)
|
||||
assertNotEquals(b1, b2)
|
||||
assertNotEquals(b1, b3)
|
||||
|
||||
assert( b1.hashCode() == b11.hashCode() )
|
||||
|
||||
val x = mutableMapOf<BitList,String>()
|
||||
x[b1] = "wrong"
|
||||
x[b11] = "OK"
|
||||
x[b2] = "other"
|
||||
|
||||
assertEquals("OK", x[b11])
|
||||
assertEquals("OK", x[b1])
|
||||
assertEquals("other", x[b2])
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
78
lynglib/src/jvmTest/kotlin/PerfProfilesTest.kt
Normal file
78
lynglib/src/jvmTest/kotlin/PerfProfilesTest.kt
Normal file
@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2025 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 kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.Ignore
|
||||
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
class PerfProfilesTest {
|
||||
|
||||
@Test
|
||||
fun apply_and_restore_presets() {
|
||||
val before = PerfProfiles.snapshot()
|
||||
|
||||
try {
|
||||
// BENCH preset expectations
|
||||
val snapAfterBench = PerfProfiles.apply(PerfProfiles.Preset.BENCH)
|
||||
// Expect some key flags ON for benches
|
||||
assertTrue(PerfFlags.ARG_BUILDER)
|
||||
assertTrue(PerfFlags.SCOPE_POOL)
|
||||
assertTrue(PerfFlags.FIELD_PIC)
|
||||
assertTrue(PerfFlags.METHOD_PIC)
|
||||
assertTrue(PerfFlags.INDEX_PIC)
|
||||
assertTrue(PerfFlags.INDEX_PIC_SIZE_4)
|
||||
assertTrue(PerfFlags.PRIMITIVE_FASTOPS)
|
||||
assertTrue(PerfFlags.RVAL_FASTPATH)
|
||||
// Restore via snapshot returned by apply
|
||||
PerfProfiles.restore(snapAfterBench)
|
||||
|
||||
// BOOKS preset expectations
|
||||
val snapAfterBooks = PerfProfiles.apply(PerfProfiles.Preset.BOOKS)
|
||||
// Expect simpler paths enabled/disabled accordingly
|
||||
assertEquals(false, PerfFlags.ARG_BUILDER)
|
||||
assertEquals(false, PerfFlags.SCOPE_POOL)
|
||||
assertEquals(false, PerfFlags.FIELD_PIC)
|
||||
assertEquals(false, PerfFlags.METHOD_PIC)
|
||||
assertEquals(false, PerfFlags.INDEX_PIC)
|
||||
assertEquals(false, PerfFlags.INDEX_PIC_SIZE_4)
|
||||
assertEquals(false, PerfFlags.PRIMITIVE_FASTOPS)
|
||||
assertEquals(false, PerfFlags.RVAL_FASTPATH)
|
||||
// Restore via snapshot returned by apply
|
||||
PerfProfiles.restore(snapAfterBooks)
|
||||
|
||||
// BASELINE preset should match PerfDefaults
|
||||
val snapAfterBaseline = PerfProfiles.apply(PerfProfiles.Preset.BASELINE)
|
||||
assertEquals(PerfDefaults.ARG_BUILDER, PerfFlags.ARG_BUILDER)
|
||||
assertEquals(PerfDefaults.SCOPE_POOL, PerfFlags.SCOPE_POOL)
|
||||
assertEquals(PerfDefaults.FIELD_PIC, PerfFlags.FIELD_PIC)
|
||||
assertEquals(PerfDefaults.METHOD_PIC, PerfFlags.METHOD_PIC)
|
||||
assertEquals(PerfDefaults.INDEX_PIC_SIZE_4, PerfFlags.INDEX_PIC_SIZE_4)
|
||||
assertEquals(PerfDefaults.PRIMITIVE_FASTOPS, PerfFlags.PRIMITIVE_FASTOPS)
|
||||
assertEquals(PerfDefaults.RVAL_FASTPATH, PerfFlags.RVAL_FASTPATH)
|
||||
// Restore baseline snapshot
|
||||
PerfProfiles.restore(snapAfterBaseline)
|
||||
|
||||
} finally {
|
||||
// Finally, restore very original snapshot
|
||||
PerfProfiles.restore(before)
|
||||
}
|
||||
}
|
||||
}
|
||||
156
lynglib/src/jvmTest/kotlin/PicAdaptiveABTest.kt
Normal file
156
lynglib/src/jvmTest/kotlin/PicAdaptiveABTest.kt
Normal file
@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
package net.sergeych.lyng
|
||||
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import java.io.File
|
||||
import kotlin.system.measureNanoTime
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.Ignore
|
||||
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
class PicAdaptiveABTest {
|
||||
|
||||
private fun outFile(): File = File("lynglib/build/pic_adaptive_ab_results.txt")
|
||||
|
||||
private fun writeHeader(f: File) {
|
||||
if (!f.parentFile.exists()) f.parentFile.mkdirs()
|
||||
f.writeText("[DEBUG_LOG] PIC Adaptive 2→4 A/B results\n")
|
||||
}
|
||||
|
||||
private fun appendLine(f: File, s: String) { f.appendText(s + "\n") }
|
||||
|
||||
private suspend fun buildScriptForMethodShapes(shapes: Int, iters: Int): Script {
|
||||
// Define N classes C0..C{shapes-1} each with method f() { 1 }
|
||||
val classes = (0 until shapes).joinToString("\n") { i ->
|
||||
"class C$i { fun f() { $i } var x = 0 }"
|
||||
}
|
||||
val inits = (0 until shapes).joinToString(", ") { i -> "C$i()" }
|
||||
val calls = buildString {
|
||||
append("var s = 0\n")
|
||||
append("val a = [${inits}]\n")
|
||||
append("for(i in 0..${iters - 1}) {\n")
|
||||
append(" val o = a[i % ${shapes}]\n")
|
||||
append(" s += o.f()\n")
|
||||
append("}\n")
|
||||
append("s\n")
|
||||
}
|
||||
val src = classes + "\n" + calls
|
||||
return Compiler.compile(Source("<pic-method-shapes>", src), Script.defaultImportManager)
|
||||
}
|
||||
|
||||
private suspend fun buildScriptForFieldShapes(shapes: Int, iters: Int): Script {
|
||||
// Each class has a mutable field x initialized to 0; read and write it
|
||||
val classes = (0 until shapes).joinToString("\n") { i ->
|
||||
"class F$i { var x = 0 }"
|
||||
}
|
||||
val inits = (0 until shapes).joinToString(", ") { i -> "F$i()" }
|
||||
val body = buildString {
|
||||
append("var s = 0\n")
|
||||
append("val a = [${inits}]\n")
|
||||
append("for(i in 0..${iters - 1}) {\n")
|
||||
append(" val o = a[i % ${shapes}]\n")
|
||||
append(" s += o.x\n")
|
||||
append(" o.x = o.x + 1\n")
|
||||
append("}\n")
|
||||
append("s\n")
|
||||
}
|
||||
val src = classes + "\n" + body
|
||||
return Compiler.compile(Source("<pic-field-shapes>", src), Script.defaultImportManager)
|
||||
}
|
||||
|
||||
private suspend fun runOnce(script: Script): Long {
|
||||
val scope = Script.newScope()
|
||||
var result: Obj? = null
|
||||
val t = measureNanoTime { result = script.execute(scope) }
|
||||
if (result !is ObjInt) println("[DEBUG_LOG] result=${result?.javaClass?.simpleName}")
|
||||
return t
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ab_adaptive_pic() = runTestBlocking {
|
||||
val f = outFile()
|
||||
writeHeader(f)
|
||||
|
||||
val savedAdaptive = PerfFlags.PIC_ADAPTIVE_2_TO_4
|
||||
val savedCounters = PerfFlags.PIC_DEBUG_COUNTERS
|
||||
val savedFieldPic = PerfFlags.FIELD_PIC
|
||||
val savedMethodPic = PerfFlags.METHOD_PIC
|
||||
val savedFieldPicSize4 = PerfFlags.FIELD_PIC_SIZE_4
|
||||
val savedMethodPicSize4 = PerfFlags.METHOD_PIC_SIZE_4
|
||||
|
||||
try {
|
||||
// Ensure baseline PICs are enabled and fixed-size flags OFF to isolate adaptivity
|
||||
PerfFlags.FIELD_PIC = true
|
||||
PerfFlags.METHOD_PIC = true
|
||||
PerfFlags.FIELD_PIC_SIZE_4 = false
|
||||
PerfFlags.METHOD_PIC_SIZE_4 = false
|
||||
|
||||
// Prepare workloads with 3 and 4 receiver shapes
|
||||
val iters = 200_000
|
||||
val meth3 = buildScriptForMethodShapes(3, iters)
|
||||
val meth4 = buildScriptForMethodShapes(4, iters)
|
||||
val fld3 = buildScriptForFieldShapes(3, iters)
|
||||
val fld4 = buildScriptForFieldShapes(4, iters)
|
||||
|
||||
fun header(which: String) {
|
||||
appendLine(f, "[DEBUG_LOG] A/B Adaptive PIC on $which (iters=$iters)")
|
||||
}
|
||||
|
||||
// OFF pass
|
||||
PerfFlags.PIC_DEBUG_COUNTERS = true
|
||||
PerfStats.resetAll()
|
||||
PerfFlags.PIC_ADAPTIVE_2_TO_4 = false
|
||||
header("methods-3")
|
||||
val tM3Off = runOnce(meth3)
|
||||
header("methods-4")
|
||||
val tM4Off = runOnce(meth4)
|
||||
header("fields-3")
|
||||
val tF3Off = runOnce(fld3)
|
||||
header("fields-4")
|
||||
val tF4Off = runOnce(fld4)
|
||||
appendLine(f, "[DEBUG_LOG] OFF counters: methodHit=${PerfStats.methodPicHit} methodMiss=${PerfStats.methodPicMiss} fieldHit=${PerfStats.fieldPicHit} fieldMiss=${PerfStats.fieldPicMiss}")
|
||||
|
||||
// ON pass
|
||||
PerfStats.resetAll()
|
||||
PerfFlags.PIC_ADAPTIVE_2_TO_4 = true
|
||||
val tM3On = runOnce(meth3)
|
||||
val tM4On = runOnce(meth4)
|
||||
val tF3On = runOnce(fld3)
|
||||
val tF4On = runOnce(fld4)
|
||||
appendLine(f, "[DEBUG_LOG] ON counters: methodHit=${PerfStats.methodPicHit} methodMiss=${PerfStats.methodPicMiss} fieldHit=${PerfStats.fieldPicHit} fieldMiss=${PerfStats.fieldPicMiss}")
|
||||
|
||||
// Report
|
||||
appendLine(f, "[DEBUG_LOG] methods-3 OFF=${tM3Off} ns, ON=${tM3On} ns, delta=${tM3Off - tM3On} ns")
|
||||
appendLine(f, "[DEBUG_LOG] methods-4 OFF=${tM4Off} ns, ON=${tM4On} ns, delta=${tM4Off - tM4On} ns")
|
||||
appendLine(f, "[DEBUG_LOG] fields-3 OFF=${tF3Off} ns, ON=${tF3On} ns, delta=${tF3Off - tF3On} ns")
|
||||
appendLine(f, "[DEBUG_LOG] fields-4 OFF=${tF4Off} ns, ON=${tF4On} ns, delta=${tF4Off - tF4On} ns")
|
||||
} finally {
|
||||
PerfFlags.PIC_ADAPTIVE_2_TO_4 = savedAdaptive
|
||||
PerfFlags.PIC_DEBUG_COUNTERS = savedCounters
|
||||
PerfFlags.FIELD_PIC = savedFieldPic
|
||||
PerfFlags.METHOD_PIC = savedMethodPic
|
||||
PerfFlags.FIELD_PIC_SIZE_4 = savedFieldPicSize4
|
||||
PerfFlags.METHOD_PIC_SIZE_4 = savedMethodPicSize4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun runTestBlocking(block: suspend () -> Unit) {
|
||||
kotlinx.coroutines.runBlocking { block() }
|
||||
}
|
||||
146
lynglib/src/jvmTest/kotlin/PicBenchmarkTest.kt
Normal file
146
lynglib/src/jvmTest/kotlin/PicBenchmarkTest.kt
Normal file
@ -0,0 +1,146 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* JVM micro-benchmarks for FieldRef and MethodCallRef PICs.
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.PerfFlags
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.Ignore
|
||||
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
class PicBenchmarkTest {
|
||||
@Test
|
||||
fun benchmarkFieldGetSetPic() = runBlocking {
|
||||
val iterations = 300_000
|
||||
val script = """
|
||||
class C() {
|
||||
var x = 0
|
||||
fun add1() { x = x + 1 }
|
||||
fun getX() { x }
|
||||
}
|
||||
val c = C()
|
||||
var i = 0
|
||||
while(i < $iterations) {
|
||||
c.x = c.x + 1
|
||||
i = i + 1
|
||||
}
|
||||
c.x
|
||||
""".trimIndent()
|
||||
|
||||
// PIC OFF
|
||||
PerfFlags.FIELD_PIC = false
|
||||
val scope1 = Scope()
|
||||
val t0 = System.nanoTime()
|
||||
val r1 = (scope1.eval(script) as ObjInt).value
|
||||
val t1 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] Field PIC=OFF: ${(t1 - t0) / 1_000_000.0} ms")
|
||||
assertEquals(iterations.toLong(), r1)
|
||||
|
||||
// PIC ON
|
||||
PerfFlags.FIELD_PIC = true
|
||||
val scope2 = Scope()
|
||||
val t2 = System.nanoTime()
|
||||
val r2 = (scope2.eval(script) as ObjInt).value
|
||||
val t3 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] Field PIC=ON: ${(t3 - t2) / 1_000_000.0} ms")
|
||||
assertEquals(iterations.toLong(), r2)
|
||||
if (PerfFlags.PIC_DEBUG_COUNTERS) {
|
||||
println("[DEBUG_LOG] [PIC] field get hit=${net.sergeych.lyng.PerfStats.fieldPicHit} miss=${net.sergeych.lyng.PerfStats.fieldPicMiss}")
|
||||
println("[DEBUG_LOG] [PIC] field set hit=${net.sergeych.lyng.PerfStats.fieldPicSetHit} miss=${net.sergeych.lyng.PerfStats.fieldPicSetMiss}")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun benchmarkMethodPic() = runBlocking {
|
||||
val iterations = 200_000
|
||||
val script = """
|
||||
class C() {
|
||||
var x = 0
|
||||
fun add(v) { x = x + v }
|
||||
fun get() { x }
|
||||
}
|
||||
val c = C()
|
||||
var i = 0
|
||||
while(i < $iterations) {
|
||||
c.add(1)
|
||||
i = i + 1
|
||||
}
|
||||
c.get()
|
||||
""".trimIndent()
|
||||
|
||||
// PIC OFF
|
||||
PerfFlags.METHOD_PIC = false
|
||||
PerfFlags.SCOPE_POOL = false
|
||||
val scope1 = Scope()
|
||||
val t0 = System.nanoTime()
|
||||
val r1 = (scope1.eval(script) as ObjInt).value
|
||||
val t1 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] Method PIC=OFF, POOL=OFF: ${(t1 - t0) / 1_000_000.0} ms")
|
||||
assertEquals(iterations.toLong(), r1)
|
||||
|
||||
// PIC ON
|
||||
PerfFlags.METHOD_PIC = true
|
||||
PerfFlags.SCOPE_POOL = true
|
||||
val scope2 = Scope()
|
||||
val t2 = System.nanoTime()
|
||||
val r2 = (scope2.eval(script) as ObjInt).value
|
||||
val t3 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] Method PIC=ON, POOL=ON: ${(t3 - t2) / 1_000_000.0} ms")
|
||||
assertEquals(iterations.toLong(), r2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun benchmarkLoopScopePooling() = runBlocking {
|
||||
val iterations = 500_000
|
||||
val script = """
|
||||
var x = 0
|
||||
var i = 0
|
||||
while(i < $iterations) {
|
||||
if(true) {
|
||||
var y = 1
|
||||
x = x + y
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
x
|
||||
""".trimIndent()
|
||||
|
||||
// POOL OFF
|
||||
PerfFlags.SCOPE_POOL = false
|
||||
val scope1 = Scope()
|
||||
val t0 = System.nanoTime()
|
||||
val r1 = (scope1.eval(script) as ObjInt).value
|
||||
val t1 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] Loop Pool=OFF: ${(t1 - t0) / 1_000_000.0} ms")
|
||||
assertEquals(iterations.toLong(), r1)
|
||||
|
||||
// POOL ON
|
||||
PerfFlags.SCOPE_POOL = true
|
||||
val scope2 = Scope()
|
||||
val t2 = System.nanoTime()
|
||||
val r2 = (scope2.eval(script) as ObjInt).value
|
||||
val t3 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] Loop Pool=ON: ${(t3 - t2) / 1_000_000.0} ms")
|
||||
assertEquals(iterations.toLong(), r2)
|
||||
}
|
||||
}
|
||||
124
lynglib/src/jvmTest/kotlin/PicInvalidationJvmTest.kt
Normal file
124
lynglib/src/jvmTest/kotlin/PicInvalidationJvmTest.kt
Normal file
@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.PerfFlags
|
||||
import net.sergeych.lyng.PerfStats
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.obj.ObjClass
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import kotlin.test.Ignore
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
class PicInvalidationJvmTest {
|
||||
@Test
|
||||
fun fieldPicInvalidatesOnClassLayoutChange() = runBlocking {
|
||||
// Enable counters and PICs
|
||||
PerfFlags.FIELD_PIC = true
|
||||
PerfFlags.PIC_DEBUG_COUNTERS = true
|
||||
PerfStats.resetAll()
|
||||
|
||||
val scope = Scope()
|
||||
// Declare a class and warm up field access
|
||||
val script = """
|
||||
class C() {
|
||||
var x = 0
|
||||
fun getX() { x }
|
||||
}
|
||||
val c = C()
|
||||
var i = 0
|
||||
while (i < 1000) {
|
||||
// warm read path
|
||||
val t = c.x
|
||||
i = i + 1
|
||||
}
|
||||
c.getX()
|
||||
""".trimIndent()
|
||||
val r1 = (scope.eval(script) as ObjInt).value
|
||||
assertEquals(0L, r1)
|
||||
val hitsBefore = PerfStats.fieldPicHit
|
||||
val missesBefore = PerfStats.fieldPicMiss
|
||||
assertTrue(hitsBefore >= 1, "Expected some PIC hits after warm-up")
|
||||
|
||||
// Mutate class layout from Kotlin side to bump layoutVersion and invalidate PIC
|
||||
val cls = (scope["C"]!!.value as ObjClass)
|
||||
cls.createClassField("yy", ObjInt(1), isMutable = false)
|
||||
|
||||
// Access the same field again; first access after version bump should miss PIC
|
||||
val r2 = (scope.eval("c.x") as ObjInt).value
|
||||
assertEquals(0L, r2)
|
||||
val missesAfter = PerfStats.fieldPicMiss
|
||||
assertTrue(missesAfter >= missesBefore + 1, "Expected PIC miss after class layout change")
|
||||
|
||||
// Optional summary when counters enabled
|
||||
if (PerfFlags.PIC_DEBUG_COUNTERS) {
|
||||
println("[DEBUG_LOG] [PIC] field get hit=${PerfStats.fieldPicHit} miss=${PerfStats.fieldPicMiss}")
|
||||
println("[DEBUG_LOG] [PIC] field set hit=${PerfStats.fieldPicSetHit} miss=${PerfStats.fieldPicSetMiss}")
|
||||
}
|
||||
|
||||
// Disable counters to avoid affecting other tests
|
||||
PerfFlags.PIC_DEBUG_COUNTERS = false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun methodPicInvalidatesOnClassLayoutChange() = runBlocking {
|
||||
PerfFlags.METHOD_PIC = true
|
||||
PerfFlags.PIC_DEBUG_COUNTERS = true
|
||||
PerfStats.resetAll()
|
||||
|
||||
val scope = Scope()
|
||||
val script = """
|
||||
class D() {
|
||||
var x = 0
|
||||
fun inc() { x = x + 1 }
|
||||
fun get() { x }
|
||||
}
|
||||
val d = D()
|
||||
var i = 0
|
||||
while (i < 1000) {
|
||||
d.inc()
|
||||
i = i + 1
|
||||
}
|
||||
d.get()
|
||||
""".trimIndent()
|
||||
val r1 = (scope.eval(script) as ObjInt).value
|
||||
assertEquals(1000L, r1)
|
||||
val mhBefore = PerfStats.methodPicHit
|
||||
val mmBefore = PerfStats.methodPicMiss
|
||||
assertTrue(mhBefore >= 1, "Expected method PIC hits after warm-up")
|
||||
|
||||
// Bump layout by adding a new class field
|
||||
val cls = (scope["D"]!!.value as ObjClass)
|
||||
cls.createClassField("zz", ObjInt(0), isMutable = false)
|
||||
|
||||
// Next invocation should miss and then re-fill
|
||||
val r2 = (scope.eval("d.get()") as ObjInt).value
|
||||
assertEquals(1000L, r2)
|
||||
val mmAfter = PerfStats.methodPicMiss
|
||||
assertTrue(mmAfter >= mmBefore + 1, "Expected method PIC miss after class layout change")
|
||||
|
||||
// Optional summary when counters enabled
|
||||
if (PerfFlags.PIC_DEBUG_COUNTERS) {
|
||||
println("[DEBUG_LOG] [PIC] method hit=${PerfStats.methodPicHit} miss=${PerfStats.methodPicMiss}")
|
||||
}
|
||||
|
||||
PerfFlags.PIC_DEBUG_COUNTERS = false
|
||||
}
|
||||
}
|
||||
134
lynglib/src/jvmTest/kotlin/PicMethodsOnlyAdaptiveABTest.kt
Normal file
134
lynglib/src/jvmTest/kotlin/PicMethodsOnlyAdaptiveABTest.kt
Normal file
@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng
|
||||
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import java.io.File
|
||||
import kotlin.system.measureNanoTime
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.Ignore
|
||||
|
||||
/**
|
||||
* A/B micro-benchmark to compare methods-only adaptive PIC OFF vs ON.
|
||||
* Ensures fixed PIC sizes (2-entry) and only toggles PIC_ADAPTIVE_METHODS_ONLY.
|
||||
* Writes a summary to lynglib/build/pic_methods_only_adaptive_ab_results.txt
|
||||
*/
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
class PicMethodsOnlyAdaptiveABTest {
|
||||
|
||||
private fun outFile(): File = File("lynglib/build/pic_methods_only_adaptive_ab_results.txt")
|
||||
|
||||
private fun writeHeader(f: File) {
|
||||
if (!f.parentFile.exists()) f.parentFile.mkdirs()
|
||||
f.writeText("[DEBUG_LOG] PIC Adaptive (methods-only) 2→4 A/B results\n")
|
||||
}
|
||||
|
||||
private fun appendLine(f: File, s: String) { f.appendText(s + "\n") }
|
||||
|
||||
private suspend fun buildScriptForMethodShapes(shapes: Int, iters: Int): Script {
|
||||
// Define N classes C0..C{shapes-1} each with method f() { i }
|
||||
val classes = (0 until shapes).joinToString("\n") { i ->
|
||||
"class MC$i { fun f() { $i } }"
|
||||
}
|
||||
val inits = (0 until shapes).joinToString(", ") { i -> "MC$i()" }
|
||||
val body = buildString {
|
||||
append("var s = 0\n")
|
||||
append("val a = [${inits}]\n")
|
||||
append("for(i in 0..${iters - 1}) {\n")
|
||||
append(" val o = a[i % ${shapes}]\n")
|
||||
append(" s += o.f()\n")
|
||||
append("}\n")
|
||||
append("s\n")
|
||||
}
|
||||
val src = classes + "\n" + body
|
||||
return Compiler.compile(Source("<pic-meth-only-shapes>", src), Script.defaultImportManager)
|
||||
}
|
||||
|
||||
private suspend fun runOnce(script: Script): Long {
|
||||
val scope = Script.newScope()
|
||||
var result: Obj? = null
|
||||
val t = measureNanoTime { result = script.execute(scope) }
|
||||
if (result !is ObjInt) println("[DEBUG_LOG] result=${result?.javaClass?.simpleName}")
|
||||
return t
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ab_methods_only_adaptive_pic() = runTestBlocking {
|
||||
val f = outFile()
|
||||
writeHeader(f)
|
||||
|
||||
// Save flags
|
||||
val savedAdaptive2To4 = PerfFlags.PIC_ADAPTIVE_2_TO_4
|
||||
val savedAdaptiveMethodsOnly = PerfFlags.PIC_ADAPTIVE_METHODS_ONLY
|
||||
val savedFieldPic = PerfFlags.FIELD_PIC
|
||||
val savedMethodPic = PerfFlags.METHOD_PIC
|
||||
val savedFieldSize4 = PerfFlags.FIELD_PIC_SIZE_4
|
||||
val savedMethodSize4 = PerfFlags.METHOD_PIC_SIZE_4
|
||||
val savedCounters = PerfFlags.PIC_DEBUG_COUNTERS
|
||||
|
||||
try {
|
||||
// Fixed-size 2-entry PICs, enable PICs, disable global adaptivity
|
||||
PerfFlags.FIELD_PIC = true
|
||||
PerfFlags.METHOD_PIC = true
|
||||
PerfFlags.FIELD_PIC_SIZE_4 = false
|
||||
PerfFlags.METHOD_PIC_SIZE_4 = false
|
||||
PerfFlags.PIC_ADAPTIVE_2_TO_4 = false
|
||||
|
||||
val iters = 200_000
|
||||
val meth3 = buildScriptForMethodShapes(3, iters)
|
||||
val meth4 = buildScriptForMethodShapes(4, iters)
|
||||
|
||||
fun header(which: String) { appendLine(f, "[DEBUG_LOG] A/B Methods-only adaptive on $which (iters=$iters)") }
|
||||
|
||||
// OFF pass
|
||||
PerfFlags.PIC_DEBUG_COUNTERS = true
|
||||
PerfStats.resetAll()
|
||||
PerfFlags.PIC_ADAPTIVE_METHODS_ONLY = false
|
||||
header("methods-3")
|
||||
val tM3Off = runOnce(meth3)
|
||||
header("methods-4")
|
||||
val tM4Off = runOnce(meth4)
|
||||
appendLine(f, "[DEBUG_LOG] OFF counters: methodHit=${PerfStats.methodPicHit} methodMiss=${PerfStats.methodPicMiss}")
|
||||
|
||||
// ON pass
|
||||
PerfStats.resetAll()
|
||||
PerfFlags.PIC_ADAPTIVE_METHODS_ONLY = true
|
||||
val tM3On = runOnce(meth3)
|
||||
val tM4On = runOnce(meth4)
|
||||
appendLine(f, "[DEBUG_LOG] ON counters: methodHit=${PerfStats.methodPicHit} methodMiss=${PerfStats.methodPicMiss}")
|
||||
|
||||
// Report
|
||||
appendLine(f, "[DEBUG_LOG] methods-3 OFF=${tM3Off} ns, ON=${tM3On} ns, delta=${tM3Off - tM3On} ns")
|
||||
appendLine(f, "[DEBUG_LOG] methods-4 OFF=${tM4Off} ns, ON=${tM4On} ns, delta=${tM4Off - tM4On} ns")
|
||||
} finally {
|
||||
// Restore
|
||||
PerfFlags.PIC_ADAPTIVE_2_TO_4 = savedAdaptive2To4
|
||||
PerfFlags.PIC_ADAPTIVE_METHODS_ONLY = savedAdaptiveMethodsOnly
|
||||
PerfFlags.FIELD_PIC = savedFieldPic
|
||||
PerfFlags.METHOD_PIC = savedMethodPic
|
||||
PerfFlags.FIELD_PIC_SIZE_4 = savedFieldSize4
|
||||
PerfFlags.METHOD_PIC_SIZE_4 = savedMethodSize4
|
||||
PerfFlags.PIC_DEBUG_COUNTERS = savedCounters
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun runTestBlocking(block: suspend () -> Unit) {
|
||||
kotlinx.coroutines.runBlocking { block() }
|
||||
}
|
||||
114
lynglib/src/jvmTest/kotlin/PrimitiveFastOpsABTest.kt
Normal file
114
lynglib/src/jvmTest/kotlin/PrimitiveFastOpsABTest.kt
Normal file
@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* A/B micro-benchmark to compare PerfFlags.PRIMITIVE_FASTOPS OFF vs ON.
|
||||
* JVM-only quick check using simple arithmetic/logic loops.
|
||||
*/
|
||||
package net.sergeych.lyng
|
||||
|
||||
import java.io.File
|
||||
import kotlin.system.measureNanoTime
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.Ignore
|
||||
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
class PrimitiveFastOpsABTest {
|
||||
|
||||
private fun outFile(): File = File("lynglib/build/primitive_ab_results.txt")
|
||||
|
||||
private fun writeHeader(f: File) {
|
||||
if (!f.parentFile.exists()) f.parentFile.mkdirs()
|
||||
f.writeText("[DEBUG_LOG] Primitive FastOps A/B results\n")
|
||||
}
|
||||
|
||||
private fun appendLine(f: File, s: String) {
|
||||
f.appendText(s + "\n")
|
||||
}
|
||||
|
||||
private fun benchIntArithmeticIters(iters: Int): Long {
|
||||
var acc = 0L
|
||||
val t = measureNanoTime {
|
||||
var a = 1L
|
||||
var b = 2L
|
||||
var c = 3L
|
||||
repeat(iters) {
|
||||
// mimic mix of +, -, *, /, %, shifts and comparisons
|
||||
a = (a + b) xor c
|
||||
b = (b * 3L + a) and 0x7FFF_FFFFL
|
||||
if ((b and 1L) == 0L) c = c + 1L else c = c - 1L
|
||||
acc = acc + (a and b) + (c or a)
|
||||
}
|
||||
}
|
||||
// use acc to prevent DCE
|
||||
if (acc == 42L) println("[DEBUG_LOG] impossible")
|
||||
return t
|
||||
}
|
||||
|
||||
private fun benchBoolLogicIters(iters: Int): Long {
|
||||
var acc = 0
|
||||
val t = measureNanoTime {
|
||||
var a = true
|
||||
var b = false
|
||||
repeat(iters) {
|
||||
a = a || b
|
||||
b = !b && a
|
||||
if (a == b) acc++ else acc--
|
||||
}
|
||||
}
|
||||
if (acc == Int.MIN_VALUE) println("[DEBUG_LOG] impossible2")
|
||||
return t
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ab_compare_primitive_fastops() {
|
||||
// Save current settings
|
||||
val savedFast = PerfFlags.PRIMITIVE_FASTOPS
|
||||
val savedCounters = PerfFlags.PIC_DEBUG_COUNTERS
|
||||
val f = outFile()
|
||||
writeHeader(f)
|
||||
|
||||
try {
|
||||
val iters = 500_000
|
||||
|
||||
// OFF pass
|
||||
PerfFlags.PIC_DEBUG_COUNTERS = true
|
||||
PerfStats.resetAll()
|
||||
PerfFlags.PRIMITIVE_FASTOPS = false
|
||||
val tArithOff = benchIntArithmeticIters(iters)
|
||||
val tLogicOff = benchBoolLogicIters(iters)
|
||||
|
||||
// ON pass
|
||||
PerfStats.resetAll()
|
||||
PerfFlags.PRIMITIVE_FASTOPS = true
|
||||
val tArithOn = benchIntArithmeticIters(iters)
|
||||
val tLogicOn = benchBoolLogicIters(iters)
|
||||
|
||||
println("[DEBUG_LOG] A/B PrimitiveFastOps (iters=$iters):")
|
||||
println("[DEBUG_LOG] Arithmetic OFF: ${'$'}tArithOff ns, ON: ${'$'}tArithOn ns, delta: ${'$'}{tArithOff - tArithOn} ns")
|
||||
println("[DEBUG_LOG] Bool logic OFF: ${'$'}tLogicOff ns, ON: ${'$'}tLogicOn ns, delta: ${'$'}{tLogicOff - tLogicOn} ns")
|
||||
|
||||
appendLine(f, "[DEBUG_LOG] A/B PrimitiveFastOps (iters=$iters):")
|
||||
appendLine(f, "[DEBUG_LOG] Arithmetic OFF: ${'$'}tArithOff ns, ON: ${'$'}tArithOn ns, delta: ${'$'}{tArithOff - tArithOn} ns")
|
||||
appendLine(f, "[DEBUG_LOG] Bool logic OFF: ${'$'}tLogicOff ns, ON: ${'$'}tLogicOn ns, delta: ${'$'}{tLogicOff - tLogicOn} ns")
|
||||
} finally {
|
||||
// restore
|
||||
PerfFlags.PRIMITIVE_FASTOPS = savedFast
|
||||
PerfFlags.PIC_DEBUG_COUNTERS = savedCounters
|
||||
}
|
||||
}
|
||||
}
|
||||
67
lynglib/src/jvmTest/kotlin/RangeBenchmarkTest.kt
Normal file
67
lynglib/src/jvmTest/kotlin/RangeBenchmarkTest.kt
Normal file
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* JVM micro-benchmark for range for-in lowering under PRIMITIVE_FASTOPS.
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.PerfFlags
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.Ignore
|
||||
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
class RangeBenchmarkTest {
|
||||
@Test
|
||||
fun benchmarkIntRangeForIn() = runBlocking {
|
||||
val n = 5_000 // outer repetitions
|
||||
val script = """
|
||||
var s = 0
|
||||
var i = 0
|
||||
while (i < $n) {
|
||||
// Hot inner counted loop over int range
|
||||
for (x in 0..999) { s = s + x }
|
||||
i = i + 1
|
||||
}
|
||||
s
|
||||
""".trimIndent()
|
||||
|
||||
// OFF
|
||||
PerfFlags.PRIMITIVE_FASTOPS = false
|
||||
val scope1 = Scope()
|
||||
val t0 = System.nanoTime()
|
||||
val r1 = (scope1.eval(script) as ObjInt).value
|
||||
val t1 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] range-for-in x$n (inner 0..999) [PRIMITIVE_FASTOPS=OFF]: ${(t1 - t0)/1_000_000.0} ms")
|
||||
|
||||
// ON
|
||||
PerfFlags.PRIMITIVE_FASTOPS = true
|
||||
val scope2 = Scope()
|
||||
val t2 = System.nanoTime()
|
||||
val r2 = (scope2.eval(script) as ObjInt).value
|
||||
val t3 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] range-for-in x$n (inner 0..999) [PRIMITIVE_FASTOPS=ON]: ${(t3 - t2)/1_000_000.0} ms")
|
||||
|
||||
// Each inner loop sums 0..999 => 999*1000/2 = 499500; repeated n times
|
||||
val expected = 499_500L * n
|
||||
assertEquals(expected, r1)
|
||||
assertEquals(expected, r2)
|
||||
}
|
||||
}
|
||||
173
lynglib/src/jvmTest/kotlin/RangeIterationBenchmarkTest.kt
Normal file
173
lynglib/src/jvmTest/kotlin/RangeIterationBenchmarkTest.kt
Normal file
@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng
|
||||
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import java.io.File
|
||||
import kotlin.system.measureNanoTime
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.Ignore
|
||||
|
||||
/**
|
||||
* Baseline range iteration benchmark. It measures for-loops over integer ranges under
|
||||
* current implementation and records timings. When RANGE_FAST_ITER is implemented,
|
||||
* this test will also serve for OFF vs ON A/B.
|
||||
*/
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
class RangeIterationBenchmarkTest {
|
||||
|
||||
private fun outFile(): File = File("lynglib/build/range_iter_bench.txt")
|
||||
|
||||
private fun writeHeader(f: File) {
|
||||
if (!f.parentFile.exists()) f.parentFile.mkdirs()
|
||||
f.writeText("[DEBUG_LOG] Range iteration benchmark results\n")
|
||||
}
|
||||
|
||||
private fun appendLine(f: File, s: String) { f.appendText(s + "\n") }
|
||||
|
||||
private suspend fun buildSumScriptInclusive(n: Int, iters: Int): Script {
|
||||
// Sum 0..n repeatedly to stress iteration
|
||||
val src = """
|
||||
var total = 0
|
||||
for (k in 0..${iters - 1}) {
|
||||
var s = 0
|
||||
for (i in 0..$n) { s += i }
|
||||
total += s
|
||||
}
|
||||
total
|
||||
""".trimIndent()
|
||||
return Compiler.compile(Source("<range-inc>", src), Script.defaultImportManager)
|
||||
}
|
||||
|
||||
private suspend fun buildSumScriptExclusive(n: Int, iters: Int): Script {
|
||||
val src = """
|
||||
var total = 0
|
||||
for (k in 0..${iters - 1}) {
|
||||
var s = 0
|
||||
for (i in 0..<$n) { s += i }
|
||||
total += s
|
||||
}
|
||||
total
|
||||
""".trimIndent()
|
||||
return Compiler.compile(Source("<range-exc>", src), Script.defaultImportManager)
|
||||
}
|
||||
|
||||
private suspend fun buildSumScriptReversed(n: Int, iters: Int): Script {
|
||||
val src = """
|
||||
var total = 0
|
||||
for (k in 0..${iters - 1}) {
|
||||
var s = 0
|
||||
// reversed-like loop using countdown range (n..0)
|
||||
for (i in $n..0) { s += i }
|
||||
total += s
|
||||
}
|
||||
total
|
||||
""".trimIndent()
|
||||
return Compiler.compile(Source("<range-rev>", src), Script.defaultImportManager)
|
||||
}
|
||||
|
||||
private suspend fun buildSumScriptNegative(n: Int, iters: Int): Script {
|
||||
// Sum -n..n repeatedly
|
||||
val src = """
|
||||
var total = 0
|
||||
for (k in 0..${iters - 1}) {
|
||||
var s = 0
|
||||
for (i in -$n..$n) { s += (i < 0 ? -i : i) }
|
||||
total += s
|
||||
}
|
||||
total
|
||||
""".trimIndent()
|
||||
return Compiler.compile(Source("<range-neg>", src), Script.defaultImportManager)
|
||||
}
|
||||
|
||||
private suspend fun buildSumScriptEmpty(iters: Int): Script {
|
||||
// Empty range 1..0 should not iterate
|
||||
val src = """
|
||||
var total = 0
|
||||
for (k in 0..${iters - 1}) {
|
||||
var s = 0
|
||||
for (i in 1..0) { s += 1 }
|
||||
total += s
|
||||
}
|
||||
total
|
||||
""".trimIndent()
|
||||
return Compiler.compile(Source("<range-empty>", src), Script.defaultImportManager)
|
||||
}
|
||||
|
||||
private suspend fun runOnce(script: Script): Long {
|
||||
val scope = Script.newScope()
|
||||
var result: Obj? = null
|
||||
val t = measureNanoTime { result = script.execute(scope) }
|
||||
if (result !is ObjInt) println("[DEBUG_LOG] result=${result?.javaClass?.simpleName}")
|
||||
return t
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bench_range_iteration_baseline() = runTestBlocking {
|
||||
val f = outFile()
|
||||
writeHeader(f)
|
||||
|
||||
val savedFlag = PerfFlags.RANGE_FAST_ITER
|
||||
try {
|
||||
val n = 1000
|
||||
val iters = 500
|
||||
|
||||
// Baseline with current flag (OFF by default)
|
||||
PerfFlags.RANGE_FAST_ITER = false
|
||||
val sIncOff = buildSumScriptInclusive(n, iters)
|
||||
val tIncOff = runOnce(sIncOff)
|
||||
val sExcOff = buildSumScriptExclusive(n, iters)
|
||||
val tExcOff = runOnce(sExcOff)
|
||||
appendLine(f, "[DEBUG_LOG] OFF inclusive=${tIncOff} ns, exclusive=${tExcOff} ns")
|
||||
|
||||
// Also record ON times
|
||||
PerfFlags.RANGE_FAST_ITER = true
|
||||
val sIncOn = buildSumScriptInclusive(n, iters)
|
||||
val tIncOn = runOnce(sIncOn)
|
||||
val sExcOn = buildSumScriptExclusive(n, iters)
|
||||
val tExcOn = runOnce(sExcOn)
|
||||
appendLine(f, "[DEBUG_LOG] ON inclusive=${tIncOn} ns, exclusive=${tExcOn} ns")
|
||||
|
||||
// Additional scenarios: reversed, negative, empty
|
||||
PerfFlags.RANGE_FAST_ITER = false
|
||||
val sRevOff = buildSumScriptReversed(n, iters)
|
||||
val tRevOff = runOnce(sRevOff)
|
||||
val sNegOff = buildSumScriptNegative(n, iters)
|
||||
val tNegOff = runOnce(sNegOff)
|
||||
val sEmptyOff = buildSumScriptEmpty(iters)
|
||||
val tEmptyOff = runOnce(sEmptyOff)
|
||||
appendLine(f, "[DEBUG_LOG] OFF reversed=${tRevOff} ns, negative=${tNegOff} ns, empty=${tEmptyOff} ns")
|
||||
|
||||
PerfFlags.RANGE_FAST_ITER = true
|
||||
val sRevOn = buildSumScriptReversed(n, iters)
|
||||
val tRevOn = runOnce(sRevOn)
|
||||
val sNegOn = buildSumScriptNegative(n, iters)
|
||||
val tNegOn = runOnce(sNegOn)
|
||||
val sEmptyOn = buildSumScriptEmpty(iters)
|
||||
val tEmptyOn = runOnce(sEmptyOn)
|
||||
appendLine(f, "[DEBUG_LOG] ON reversed=${tRevOn} ns, negative=${tNegOn} ns, empty=${tEmptyOn} ns")
|
||||
} finally {
|
||||
PerfFlags.RANGE_FAST_ITER = savedFlag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun runTestBlocking(block: suspend () -> Unit) {
|
||||
kotlinx.coroutines.runBlocking { block() }
|
||||
}
|
||||
111
lynglib/src/jvmTest/kotlin/RegexBenchmarkTest.kt
Normal file
111
lynglib/src/jvmTest/kotlin/RegexBenchmarkTest.kt
Normal file
@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2025 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* JVM micro-benchmark for regex caching under REGEX_CACHE.
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.PerfFlags
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.Ignore
|
||||
|
||||
@Ignore("TODO(compile-time-res): legacy tests disabled")
|
||||
class RegexBenchmarkTest {
|
||||
@Test
|
||||
fun benchmarkLiteralPatternMatches() = runBlocking {
|
||||
val n = 500_000
|
||||
val text = "abc123def"
|
||||
val pattern = ".*\\d{3}.*" // substring contains three digits
|
||||
val script = """
|
||||
val text = "$text"
|
||||
val pat = "$pattern"
|
||||
var s = 0
|
||||
var i = 0
|
||||
while (i < $n) {
|
||||
if (text.matches(pat)) { s = s + 1 }
|
||||
i = i + 1
|
||||
}
|
||||
s
|
||||
""".trimIndent()
|
||||
|
||||
// OFF
|
||||
PerfFlags.REGEX_CACHE = false
|
||||
val scope1 = Scope()
|
||||
val t0 = System.nanoTime()
|
||||
val r1 = (scope1.eval(script) as ObjInt).value
|
||||
val t1 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] regex-literal x$n [REGEX_CACHE=OFF]: ${(t1 - t0)/1_000_000.0} ms")
|
||||
|
||||
// ON
|
||||
PerfFlags.REGEX_CACHE = true
|
||||
val scope2 = Scope()
|
||||
val t2 = System.nanoTime()
|
||||
val r2 = (scope2.eval(script) as ObjInt).value
|
||||
val t3 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] regex-literal x$n [REGEX_CACHE=ON]: ${(t3 - t2)/1_000_000.0} ms")
|
||||
|
||||
// "abc123def" matches \\d{3}
|
||||
val expected = 1L * n
|
||||
assertEquals(expected, r1)
|
||||
assertEquals(expected, r2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun benchmarkDynamicPatternMatches() = runBlocking {
|
||||
val n = 300_000
|
||||
val text = "foo-123-XYZ"
|
||||
val patterns = listOf("foo-\\d{3}-XYZ", "bar-\\d{3}-XYZ")
|
||||
val script = """
|
||||
val text = "$text"
|
||||
val patterns = ["foo-\\d{3}-XYZ","bar-\\d{3}-XYZ"]
|
||||
var s = 0
|
||||
var i = 0
|
||||
while (i < $n) {
|
||||
// Alternate patterns to exercise cache
|
||||
val p = if (i % 2 == 0) patterns[0] else patterns[1]
|
||||
if (text.matches(p)) { s = s + 1 }
|
||||
i = i + 1
|
||||
}
|
||||
s
|
||||
""".trimIndent()
|
||||
|
||||
// OFF
|
||||
PerfFlags.REGEX_CACHE = false
|
||||
val scope1 = Scope()
|
||||
val t0 = System.nanoTime()
|
||||
val r1 = (scope1.eval(script) as ObjInt).value
|
||||
val t1 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] regex-dynamic x$n [REGEX_CACHE=OFF]: ${(t1 - t0)/1_000_000.0} ms")
|
||||
|
||||
// ON
|
||||
PerfFlags.REGEX_CACHE = true
|
||||
val scope2 = Scope()
|
||||
val t2 = System.nanoTime()
|
||||
val r2 = (scope2.eval(script) as ObjInt).value
|
||||
val t3 = System.nanoTime()
|
||||
println("[DEBUG_LOG] [BENCH] regex-dynamic x$n [REGEX_CACHE=ON]: ${(t3 - t2)/1_000_000.0} ms")
|
||||
|
||||
// Only the first pattern matches; alternates every other iteration
|
||||
val expected = (n / 2).toLong()
|
||||
assertEquals(expected, r1)
|
||||
assertEquals(expected, r2)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user