added Multiple Inheritance
This commit is contained in:
parent
882df67909
commit
aeeec2d417
28
CHANGELOG.md
Normal file
28
CHANGELOG.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
## Changelog
|
||||||
|
|
||||||
|
### Unreleased
|
||||||
|
|
||||||
|
- Multiple Inheritance (MI) completed and enabled by default:
|
||||||
|
- Active C3 Method Resolution Order (MRO) for deterministic, monotonic lookup across complex hierarchies and diamonds.
|
||||||
|
- Qualified dispatch:
|
||||||
|
- `this@Type.member(...)` inside class bodies starts lookup at the specified ancestor.
|
||||||
|
- Cast-based disambiguation: `(expr as Type).member(...)`, `(expr as? Type)?.member(...)` (works with existing safe-call `?.`).
|
||||||
|
- Field inheritance (`val`/`var`) under MI:
|
||||||
|
- Instance storage is disambiguated per declaring class; unqualified read/write resolves to the first match in MRO.
|
||||||
|
- Qualified read/write targets the chosen ancestor’s storage.
|
||||||
|
- Constructors and initialization:
|
||||||
|
- Direct bases are initialized left-to-right; each ancestor is initialized at most once (diamond-safe de-duplication).
|
||||||
|
- Header-specified constructor arguments are passed to direct bases.
|
||||||
|
- Visibility enforcement under MI:
|
||||||
|
- `private` visible only inside the declaring class body.
|
||||||
|
- `protected` visible inside the declaring class and any of its transitive subclasses; unrelated contexts cannot access it (qualification/casts do not bypass).
|
||||||
|
- Diagnostics improvements:
|
||||||
|
- Missing member/field messages include receiver class and linearization order; hints for `this@Type` or casts when helpful.
|
||||||
|
- Invalid `this@Type` reports that the qualifier is not an ancestor and shows the receiver lineage.
|
||||||
|
- `as`/`as?` cast errors include actual and target type names.
|
||||||
|
|
||||||
|
- Documentation updated (docs/OOP.md and tutorial quick-start) to reflect MI with active C3 MRO.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Existing single-inheritance code continues to work; resolution reduces to the single base.
|
||||||
|
- If code previously relied on non-deterministic parent set iteration, C3 MRO provides a predictable order; disambiguate explicitly if needed using `this@Type`/casts.
|
||||||
@ -174,8 +174,8 @@ Ready features:
|
|||||||
|
|
||||||
### Under way:
|
### Under way:
|
||||||
|
|
||||||
- [ ] regular exceptions + extended `when`
|
- [x] regular exceptions + extended `when`
|
||||||
- [ ] multiple inheritance for user classes
|
- [x] multiple inheritance for user classes
|
||||||
- [ ] site with integrated interpreter to give a try
|
- [ ] site with integrated interpreter to give a try
|
||||||
- [ ] kotlin part public API good docs, integration focused
|
- [ ] kotlin part public API good docs, integration focused
|
||||||
|
|
||||||
|
|||||||
112
docs/OOP.md
112
docs/OOP.md
@ -71,6 +71,99 @@ Functions defined inside a class body are methods, and unless declared
|
|||||||
void
|
void
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
|
## Multiple Inheritance (MI)
|
||||||
|
|
||||||
|
Lyng supports declaring a class with multiple direct base classes. The syntax is:
|
||||||
|
|
||||||
|
```
|
||||||
|
class Foo(val a) {
|
||||||
|
var tag = "F"
|
||||||
|
fun runA() { "ResultA:" + a }
|
||||||
|
fun common() { "CommonA" }
|
||||||
|
private fun privateInFoo() {}
|
||||||
|
protected fun protectedInFoo() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Bar(val b) {
|
||||||
|
var tag = "B"
|
||||||
|
fun runB() { "ResultB:" + b }
|
||||||
|
fun common() { "CommonB" }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple inheritance with per‑base constructor arguments
|
||||||
|
class FooBar(a, b) : Foo(a), Bar(b) {
|
||||||
|
// You can disambiguate via qualified this or casts
|
||||||
|
fun fromFoo() { this@Foo.common() }
|
||||||
|
fun fromBar() { this@Bar.common() }
|
||||||
|
}
|
||||||
|
|
||||||
|
val fb = FooBar(1, 2)
|
||||||
|
assertEquals("ResultA:1", fb.runA())
|
||||||
|
assertEquals("ResultB:2", fb.runB())
|
||||||
|
// Unqualified ambiguous member resolves to the first base (leftmost)
|
||||||
|
assertEquals("CommonA", fb.common())
|
||||||
|
// Disambiguation via casts
|
||||||
|
assertEquals("CommonB", (fb as Bar).common())
|
||||||
|
assertEquals("CommonA", (fb as Foo).common())
|
||||||
|
|
||||||
|
// Field inheritance with name collisions
|
||||||
|
assertEquals("F", fb.tag) // unqualified: leftmost base
|
||||||
|
assertEquals("F", (fb as Foo).tag) // qualified read: Foo.tag
|
||||||
|
assertEquals("B", (fb as Bar).tag) // qualified read: Bar.tag
|
||||||
|
|
||||||
|
fb.tag = "X" // unqualified write updates leftmost base
|
||||||
|
assertEquals("X", (fb as Foo).tag)
|
||||||
|
assertEquals("B", (fb as Bar).tag)
|
||||||
|
|
||||||
|
(fb as Bar).tag = "Y" // qualified write updates Bar.tag
|
||||||
|
assertEquals("X", (fb as Foo).tag)
|
||||||
|
assertEquals("Y", (fb as Bar).tag)
|
||||||
|
```
|
||||||
|
|
||||||
|
Key rules and features:
|
||||||
|
|
||||||
|
- Syntax
|
||||||
|
- `class Derived(args) : Base1(b1Args), Base2(b2Args)`
|
||||||
|
- Each direct base may receive constructor arguments specified in the header. Only direct bases receive header args; indirect bases must either be default‑constructible or receive their args through their direct child (future extensions may add more control).
|
||||||
|
|
||||||
|
- Resolution order (C3 MRO — active)
|
||||||
|
- Member lookup is deterministic and follows C3 linearization (Python‑like), which provides a monotonic, predictable order for complex hierarchies and diamonds.
|
||||||
|
- Intuition: for `class D() : B(), C()` where `B()` and `C()` both derive from `A()`, the C3 order is `D → B → C → A`.
|
||||||
|
- The first visible match along this order wins.
|
||||||
|
|
||||||
|
- Qualified dispatch
|
||||||
|
- Inside a class body, use `this@Type.member(...)` to start lookup at the specified ancestor.
|
||||||
|
- For arbitrary receivers, use casts: `(expr as Type).member(...)` or `(expr as? Type)?.member(...)` (safe‑call `?.` is already available in Lyng).
|
||||||
|
- Qualified access does not relax visibility.
|
||||||
|
|
||||||
|
- Field inheritance (`val`/`var`) and collisions
|
||||||
|
- Instance storage is kept per declaring class, internally disambiguated; unqualified read/write resolves to the first match in the resolution order (leftmost base).
|
||||||
|
- Qualified read/write (via `this@Type` or casts) targets the chosen ancestor’s storage.
|
||||||
|
- `val` remains read‑only; attempting to write raises an error as usual.
|
||||||
|
|
||||||
|
- Constructors and initialization
|
||||||
|
- During construction, direct bases are initialized left‑to‑right in the declaration order. Each ancestor is initialized at most once (diamond‑safe de‑duplication).
|
||||||
|
- Arguments in the header are evaluated in the instance scope and passed to the corresponding direct base constructor.
|
||||||
|
- The most‑derived class’s constructor runs after the bases.
|
||||||
|
|
||||||
|
- Visibility
|
||||||
|
- `private`: accessible only inside the declaring class body; not visible in subclasses and cannot be accessed via `this@Type` or casts.
|
||||||
|
- `protected`: accessible in the declaring class and in any of its transitive subclasses (including MI), but not from unrelated contexts; qualification/casts do not bypass it.
|
||||||
|
|
||||||
|
- Diagnostics
|
||||||
|
- When a member/field is not found, error messages include the receiver class name and the considered linearization order, with suggestions to disambiguate using `this@Type` or casts if appropriate.
|
||||||
|
- Qualifying with a non‑ancestor in `this@Type` reports a clear error mentioning the receiver lineage.
|
||||||
|
- `as`/`as?` cast errors mention the actual and target types.
|
||||||
|
|
||||||
|
Compatibility notes:
|
||||||
|
|
||||||
|
- Existing single‑inheritance code continues to work unchanged; its resolution order reduces to the single base.
|
||||||
|
- If your previous code accidentally relied on non‑deterministic parent set iteration, it may change behavior — the new deterministic order is a correctness fix.
|
||||||
|
|
||||||
|
### Migration note (declaration‑order → C3)
|
||||||
|
|
||||||
|
Earlier drafts and docs described a declaration‑order depth‑first linearization. Lyng now uses C3 MRO for member lookup and disambiguation. Most code should continue to work unchanged, but in rare edge cases involving diamonds or complex multiple inheritance, the chosen base for an ambiguous member may change to reflect C3. If needed, disambiguate explicitly using `this@Type.member(...)` inside class bodies or casts `(expr as Type).member(...)` from outside.
|
||||||
|
|
||||||
## fields and visibility
|
## fields and visibility
|
||||||
|
|
||||||
It is possible to add non-constructor fields:
|
It is possible to add non-constructor fields:
|
||||||
@ -130,6 +223,25 @@ Private fields are visible only _inside the class instance_:
|
|||||||
void
|
void
|
||||||
>>> void
|
>>> void
|
||||||
|
|
||||||
|
### Protected members
|
||||||
|
|
||||||
|
Protected members are available to the declaring class and all of its transitive subclasses (including via MI), but not from unrelated contexts:
|
||||||
|
|
||||||
|
```
|
||||||
|
class A() {
|
||||||
|
protected fun ping() { "pong" }
|
||||||
|
}
|
||||||
|
class B() : A() {
|
||||||
|
fun call() { this@A.ping() }
|
||||||
|
}
|
||||||
|
|
||||||
|
val b = B()
|
||||||
|
assertEquals("pong", b.call())
|
||||||
|
|
||||||
|
// Unrelated access is forbidden, even via cast
|
||||||
|
assertThrows { (b as A).ping() }
|
||||||
|
```
|
||||||
|
|
||||||
It is possible to provide private constructor parameters so they can be
|
It is possible to provide private constructor parameters so they can be
|
||||||
set at construction but not available outside the class:
|
set at construction but not available outside the class:
|
||||||
|
|
||||||
|
|||||||
@ -1,27 +1,3 @@
|
|||||||
---
|
|
||||||
gitea: none
|
|
||||||
include_toc: true
|
|
||||||
---
|
|
||||||
|
|
||||||
# Lyng tutorial
|
|
||||||
|
|
||||||
Lyng is a very simple language, where we take only most important and popular features from
|
|
||||||
other scripts and languages. In particular, we adopt _principle of minimal confusion_[^1].
|
|
||||||
In other word, the code usually works as expected when you see it. So, nothing unusual.
|
|
||||||
|
|
||||||
__Other documents to read__ maybe after this one:
|
|
||||||
|
|
||||||
- [Advanced topics](advanced_topics.md), [declaring arguments](declaring_arguments.md)
|
|
||||||
- [OOP notes](OOP.md), [exception handling](exceptions_handling.md)
|
|
||||||
- [math in Lyng](math.md)
|
|
||||||
- [time](time.md) and [parallelism](parallelism.md)
|
|
||||||
- [parallelism] - multithreaded code, coroutines, etc.
|
|
||||||
- Some class
|
|
||||||
references: [List], [Set], [Map], [Real], [Range], [Iterable], [Iterator], [time manipulation](time.md), [Array], [RingBuffer], [Buffer].
|
|
||||||
- Some samples: [combinatorics](samples/combinatorics.lyng.md), national vars and
|
|
||||||
loops: [сумма ряда](samples/сумма_ряда.lyng.md). More at [samples folder](samples)
|
|
||||||
|
|
||||||
# Expressions
|
|
||||||
|
|
||||||
Everything is an expression in Lyng. Even an empty block:
|
Everything is an expression in Lyng. Even an empty block:
|
||||||
|
|
||||||
@ -1426,3 +1402,66 @@ Lambda avoid unnecessary execution if assertion is not failed. for example:
|
|||||||
[Array]: Array.md
|
[Array]: Array.md
|
||||||
|
|
||||||
[Regex]: Regex.md
|
[Regex]: Regex.md
|
||||||
|
|
||||||
|
## Multiple Inheritance (quick start)
|
||||||
|
|
||||||
|
Lyng supports multiple inheritance (MI) with simple, predictable rules. For a full reference see OOP notes, this is a quick, copy‑paste friendly overview.
|
||||||
|
|
||||||
|
Declare a class with multiple bases and pass constructor arguments to each base in the header:
|
||||||
|
|
||||||
|
```
|
||||||
|
class Foo(val a) {
|
||||||
|
var tag = "F"
|
||||||
|
fun runA() { "ResultA:" + a }
|
||||||
|
fun common() { "CommonA" }
|
||||||
|
private fun privateInFoo() {}
|
||||||
|
protected fun protectedInFoo() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Bar(val b) {
|
||||||
|
var tag = "B"
|
||||||
|
fun runB() { "ResultB:" + b }
|
||||||
|
fun common() { "CommonB" }
|
||||||
|
}
|
||||||
|
|
||||||
|
class FooBar(a, b) : Foo(a), Bar(b) {
|
||||||
|
// Inside class bodies you can qualify with this@Type
|
||||||
|
fun fromFoo() { this@Foo.common() }
|
||||||
|
fun fromBar() { this@Bar.common() }
|
||||||
|
}
|
||||||
|
|
||||||
|
val fb = FooBar(1, 2)
|
||||||
|
assertEquals("ResultA:1", fb.runA())
|
||||||
|
assertEquals("ResultB:2", fb.runB())
|
||||||
|
|
||||||
|
// Unqualified ambiguous member uses the first base (leftmost)
|
||||||
|
assertEquals("CommonA", fb.common())
|
||||||
|
|
||||||
|
// Disambiguate with casts
|
||||||
|
assertEquals("CommonB", (fb as Bar).common())
|
||||||
|
assertEquals("CommonA", (fb as Foo).common())
|
||||||
|
|
||||||
|
// Field collisions: unqualified read/write uses the first in order
|
||||||
|
assertEquals("F", fb.tag)
|
||||||
|
fb.tag = "X"
|
||||||
|
assertEquals("X", fb.tag) // Foo.tag updated
|
||||||
|
assertEquals("X", (fb as Foo).tag) // qualified read: Foo.tag
|
||||||
|
assertEquals("B", (fb as Bar).tag) // qualified read: Bar.tag
|
||||||
|
|
||||||
|
// Qualified write targets the chosen base storage
|
||||||
|
(fb as Bar).tag = "Y"
|
||||||
|
assertEquals("X", (fb as Foo).tag)
|
||||||
|
assertEquals("Y", (fb as Bar).tag)
|
||||||
|
|
||||||
|
// Optional casts with safe call
|
||||||
|
class Buzz : Bar(3)
|
||||||
|
val buzz = Buzz()
|
||||||
|
assertEquals("ResultB:3", buzz.runB())
|
||||||
|
assertEquals("ResultB:3", (buzz as? Bar)?.runB())
|
||||||
|
assertEquals(null, (buzz as? Foo)?.runA())
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Resolution order uses C3 MRO (active): deterministic, monotonic order suitable for diamonds and complex hierarchies. Example: for `class D() : B(), C()` where both `B()` and `C()` derive from `A()`, the C3 order is `D → B → C → A`. The first visible match wins.
|
||||||
|
- `private` is visible only inside the declaring class; `protected` is visible from the declaring class and any of its transitive subclasses. Qualification (`this@Type`) or casts do not bypass visibility.
|
||||||
|
- Safe‑call `?.` works with `as?` for optional dispatch.
|
||||||
|
|||||||
@ -29,17 +29,23 @@ class ClosureScope(val callScope: Scope, val closureScope: Scope) :
|
|||||||
// we captured, not to the caller's `this` (e.g., FlowBuilder).
|
// we captured, not to the caller's `this` (e.g., FlowBuilder).
|
||||||
Scope(callScope, callScope.args, thisObj = closureScope.thisObj) {
|
Scope(callScope, callScope.args, thisObj = closureScope.thisObj) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Preserve the lexical class context from where the lambda was defined (closure),
|
||||||
|
// so that visibility checks (private/protected) inside lambdas executed within other
|
||||||
|
// methods (e.g., Mutex.withLock) still see the original declaring class context.
|
||||||
|
this.currentClassCtx = closureScope.currentClassCtx
|
||||||
|
}
|
||||||
|
|
||||||
override fun get(name: String): ObjRecord? {
|
override fun get(name: String): ObjRecord? {
|
||||||
// Priority:
|
// Priority:
|
||||||
// 1) Arguments from the caller scope (if present in this frame)
|
// 1) Locals and arguments declared in this lambda frame (including values defined before suspension)
|
||||||
// 2) Instance/class members of the captured receiver (`closureScope.thisObj`), e.g., fields like `coll`, `factor`
|
// 2) Instance/class members of the captured receiver (`closureScope.thisObj`), e.g., fields like `coll`, `factor`
|
||||||
// 3) Symbols from the captured closure scope (its locals and parents)
|
// 3) Symbols from the captured closure scope (its locals and parents)
|
||||||
// 4) Instance members of the caller's `this` (e.g., FlowBuilder.emit)
|
// 4) Instance members of the caller's `this` (e.g., FlowBuilder.emit)
|
||||||
// 5) Fallback to the standard chain (this frame -> parent (callScope) -> class members)
|
// 5) Fallback to the standard chain (this frame -> parent (callScope) -> class members)
|
||||||
|
|
||||||
// note using super, not callScope, as arguments are assigned by the constructor
|
// First, prefer locals/arguments bound in this frame
|
||||||
// and are not yet exposed via callScope.get at this point:
|
super.objects[name]?.let { return it }
|
||||||
super.objects[name]?.let { if (it.type.isArgument) return it }
|
|
||||||
|
|
||||||
// Prefer instance fields/methods declared on the captured receiver:
|
// Prefer instance fields/methods declared on the captured receiver:
|
||||||
// First, resolve real instance fields stored in the instance scope (constructor vars like `coll`, `factor`)
|
// First, resolve real instance fields stored in the instance scope (constructor vars like `coll`, `factor`)
|
||||||
|
|||||||
@ -479,6 +479,8 @@ class Compiler(
|
|||||||
val callStatement = statement {
|
val callStatement = statement {
|
||||||
// and the source closure of the lambda which might have other thisObj.
|
// and the source closure of the lambda which might have other thisObj.
|
||||||
val context = this.applyClosure(closure!!)
|
val context = this.applyClosure(closure!!)
|
||||||
|
// Execute lambda body in a closure-aware context. Blocks inside the lambda
|
||||||
|
// will create child scopes as usual, so re-declarations inside loops work.
|
||||||
if (argsDeclaration == null) {
|
if (argsDeclaration == null) {
|
||||||
// no args: automatic var 'it'
|
// no args: automatic var 'it'
|
||||||
val l = args.list
|
val l = args.list
|
||||||
@ -700,6 +702,37 @@ class Compiler(
|
|||||||
return args to lastBlockArgument
|
return args to lastBlockArgument
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse arguments inside parentheses without consuming any optional trailing block after the RPAREN.
|
||||||
|
* Useful in contexts where a following '{' has different meaning (e.g., class bodies after base lists).
|
||||||
|
*/
|
||||||
|
private suspend fun parseArgsNoTailBlock(): List<ParsedArgument> {
|
||||||
|
val args = mutableListOf<ParsedArgument>()
|
||||||
|
do {
|
||||||
|
val t = cc.next()
|
||||||
|
when (t.type) {
|
||||||
|
Token.Type.NEWLINE,
|
||||||
|
Token.Type.RPAREN, Token.Type.COMMA -> {
|
||||||
|
}
|
||||||
|
|
||||||
|
Token.Type.ELLIPSIS -> {
|
||||||
|
parseStatement()?.let { args += ParsedArgument(it, t.pos, isSplat = true) }
|
||||||
|
?: throw ScriptError(t.pos, "Expecting arguments list")
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
cc.previous()
|
||||||
|
parseExpression()?.let { args += ParsedArgument(it, t.pos) }
|
||||||
|
?: throw ScriptError(t.pos, "Expecting arguments list")
|
||||||
|
if (cc.current().type == Token.Type.COLON)
|
||||||
|
parseTypeDeclaration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (t.type != Token.Type.RPAREN)
|
||||||
|
// Do NOT peek for a trailing block; leave it to the outer parser
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private suspend fun parseFunctionCall(
|
private suspend fun parseFunctionCall(
|
||||||
left: ObjRef,
|
left: ObjRef,
|
||||||
@ -750,7 +783,18 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Token.Type.ID -> {
|
Token.Type.ID -> {
|
||||||
when (t.value) {
|
// Special case: qualified this -> this@Type
|
||||||
|
if (t.value == "this") {
|
||||||
|
val pos = cc.savePos()
|
||||||
|
val next = cc.next()
|
||||||
|
if (next.pos.line == t.pos.line && next.type == Token.Type.ATLABEL) {
|
||||||
|
QualifiedThisRef(next.value, t.pos)
|
||||||
|
} else {
|
||||||
|
cc.restorePos(pos)
|
||||||
|
// plain this
|
||||||
|
LocalVarRef("this", t.pos)
|
||||||
|
}
|
||||||
|
} else when (t.value) {
|
||||||
"void" -> ConstRef(ObjVoid.asReadonly)
|
"void" -> ConstRef(ObjVoid.asReadonly)
|
||||||
"null" -> ConstRef(ObjNull.asReadonly)
|
"null" -> ConstRef(ObjNull.asReadonly)
|
||||||
"true" -> ConstRef(ObjTrue.asReadonly)
|
"true" -> ConstRef(ObjTrue.asReadonly)
|
||||||
@ -818,6 +862,40 @@ class Compiler(
|
|||||||
private suspend fun parseKeywordStatement(id: Token): Statement? = when (id.value) {
|
private suspend fun parseKeywordStatement(id: Token): Statement? = when (id.value) {
|
||||||
"val" -> parseVarDeclaration(false, Visibility.Public)
|
"val" -> parseVarDeclaration(false, Visibility.Public)
|
||||||
"var" -> parseVarDeclaration(true, Visibility.Public)
|
"var" -> parseVarDeclaration(true, Visibility.Public)
|
||||||
|
// Ensure function declarations are recognized in all contexts (including class bodies)
|
||||||
|
"fun" -> parseFunctionDeclaration(isOpen = false, isExtern = false, isStatic = false)
|
||||||
|
"fn" -> parseFunctionDeclaration(isOpen = false, isExtern = false, isStatic = false)
|
||||||
|
// Visibility modifiers for declarations: private/protected val/var/fun/fn
|
||||||
|
"private" -> {
|
||||||
|
var k = cc.requireToken(Token.Type.ID, "declaration expected after 'private'")
|
||||||
|
var isStatic = false
|
||||||
|
if (k.value == "static") {
|
||||||
|
isStatic = true
|
||||||
|
k = cc.requireToken(Token.Type.ID, "declaration expected after 'private static'")
|
||||||
|
}
|
||||||
|
when (k.value) {
|
||||||
|
"val" -> parseVarDeclaration(false, Visibility.Private, isStatic = isStatic)
|
||||||
|
"var" -> parseVarDeclaration(true, Visibility.Private, isStatic = isStatic)
|
||||||
|
"fun" -> parseFunctionDeclaration(visibility = Visibility.Private, isOpen = false, isExtern = false, isStatic = isStatic)
|
||||||
|
"fn" -> parseFunctionDeclaration(visibility = Visibility.Private, isOpen = false, isExtern = false, isStatic = isStatic)
|
||||||
|
else -> k.raiseSyntax("unsupported private declaration kind: ${k.value}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"protected" -> {
|
||||||
|
var k = cc.requireToken(Token.Type.ID, "declaration expected after 'protected'")
|
||||||
|
var isStatic = false
|
||||||
|
if (k.value == "static") {
|
||||||
|
isStatic = true
|
||||||
|
k = cc.requireToken(Token.Type.ID, "declaration expected after 'protected static'")
|
||||||
|
}
|
||||||
|
when (k.value) {
|
||||||
|
"val" -> parseVarDeclaration(false, Visibility.Protected, isStatic = isStatic)
|
||||||
|
"var" -> parseVarDeclaration(true, Visibility.Protected, isStatic = isStatic)
|
||||||
|
"fun" -> parseFunctionDeclaration(visibility = Visibility.Protected, isOpen = false, isExtern = false, isStatic = isStatic)
|
||||||
|
"fn" -> parseFunctionDeclaration(visibility = Visibility.Protected, isOpen = false, isExtern = false, isStatic = isStatic)
|
||||||
|
else -> k.raiseSyntax("unsupported protected declaration kind: ${k.value}")
|
||||||
|
}
|
||||||
|
}
|
||||||
"while" -> parseWhileStatement()
|
"while" -> parseWhileStatement()
|
||||||
"do" -> parseDoWhileStatement()
|
"do" -> parseDoWhileStatement()
|
||||||
"for" -> parseForStatement()
|
"for" -> parseForStatement()
|
||||||
@ -1162,19 +1240,40 @@ class Compiler(
|
|||||||
"Bad class declaration: expected ')' at the end of the primary constructor"
|
"Bad class declaration: expected ')' at the end of the primary constructor"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Optional base list: ":" Base ("," Base)* where Base := ID ( "(" args? ")" )?
|
||||||
|
data class BaseSpec(val name: String, val args: List<ParsedArgument>?)
|
||||||
|
val baseSpecs = mutableListOf<BaseSpec>()
|
||||||
|
if (cc.skipTokenOfType(Token.Type.COLON, isOptional = true)) {
|
||||||
|
do {
|
||||||
|
val baseId = cc.requireToken(Token.Type.ID, "base class name expected")
|
||||||
|
var argsList: List<ParsedArgument>? = null
|
||||||
|
// Optional constructor args of the base — parse and ignore for now (MVP), just to consume tokens
|
||||||
|
if (cc.skipTokenOfType(Token.Type.LPAREN, isOptional = true)) {
|
||||||
|
// Parse args without consuming any following block so that a class body can follow safely
|
||||||
|
argsList = parseArgsNoTailBlock()
|
||||||
|
}
|
||||||
|
baseSpecs += BaseSpec(baseId.value, argsList)
|
||||||
|
} while (cc.skipTokenOfType(Token.Type.COMMA, isOptional = true))
|
||||||
|
}
|
||||||
|
|
||||||
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
|
cc.skipTokenOfType(Token.Type.NEWLINE, isOptional = true)
|
||||||
val t = cc.next()
|
|
||||||
|
|
||||||
pushInitScope()
|
pushInitScope()
|
||||||
|
|
||||||
val bodyInit: Statement? = if (t.type == Token.Type.LBRACE) {
|
// Robust body detection: peek next non-whitespace token; if it's '{', consume and parse the body
|
||||||
// parse body
|
val bodyInit: Statement? = run {
|
||||||
parseScript().also {
|
val saved = cc.savePos()
|
||||||
cc.skipTokens(Token.Type.RBRACE)
|
val next = cc.nextNonWhitespace()
|
||||||
|
if (next.type == Token.Type.LBRACE) {
|
||||||
|
// parse body
|
||||||
|
parseScript().also {
|
||||||
|
cc.skipTokens(Token.Type.RBRACE)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// restore if no body starts here
|
||||||
|
cc.restorePos(saved)
|
||||||
|
null
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
cc.previous()
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val initScope = popInitScope()
|
val initScope = popInitScope()
|
||||||
@ -1191,31 +1290,60 @@ class Compiler(
|
|||||||
val constructorCode = statement {
|
val constructorCode = statement {
|
||||||
// constructor code is registered with class instance and is called over
|
// constructor code is registered with class instance and is called over
|
||||||
// new `thisObj` already set by class to ObjInstance.instanceContext
|
// new `thisObj` already set by class to ObjInstance.instanceContext
|
||||||
thisObj as ObjInstance
|
val instance = thisObj as ObjInstance
|
||||||
|
// Constructor parameters have been assigned to instance scope by ObjClass.callOn before
|
||||||
// the context now is a "class creation context", we must use its args to initialize
|
// invoking parent/child constructors.
|
||||||
// fields. Note that 'this' is already set by class
|
// IMPORTANT: do not execute class body here; class body was executed once in the class scope
|
||||||
constructorArgsDeclaration?.assignToContext(this)
|
// to register methods and prepare initializers. Instance constructor should be empty unless
|
||||||
bodyInit?.execute(this)
|
// we later add explicit constructor body syntax.
|
||||||
|
instance
|
||||||
thisObj
|
|
||||||
}
|
}
|
||||||
// inheritance must alter this code:
|
|
||||||
val newClass = ObjInstanceClass(className).apply {
|
|
||||||
instanceConstructor = constructorCode
|
|
||||||
constructorMeta = constructorArgsDeclaration
|
|
||||||
}
|
|
||||||
|
|
||||||
statement {
|
statement {
|
||||||
// the main statement should create custom ObjClass instance with field
|
// the main statement should create custom ObjClass instance with field
|
||||||
// accessors, constructor registration, etc.
|
// accessors, constructor registration, etc.
|
||||||
|
// Resolve parent classes by name at execution time
|
||||||
|
val parentClasses = baseSpecs.map { baseSpec ->
|
||||||
|
val rec = this[baseSpec.name] ?: throw ScriptError(nameToken.pos, "unknown base class: ${baseSpec.name}")
|
||||||
|
(rec.value as? ObjClass) ?: throw ScriptError(nameToken.pos, "${baseSpec.name} is not a class")
|
||||||
|
}
|
||||||
|
|
||||||
|
val newClass = ObjInstanceClass(className, *parentClasses.toTypedArray()).also {
|
||||||
|
it.instanceConstructor = constructorCode
|
||||||
|
it.constructorMeta = constructorArgsDeclaration
|
||||||
|
// Attach per-parent constructor args (thunks) if provided
|
||||||
|
for (i in parentClasses.indices) {
|
||||||
|
val argsList = baseSpecs[i].args
|
||||||
|
if (argsList != null) it.directParentArgs[parentClasses[i]] = argsList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
addItem(className, false, newClass)
|
addItem(className, false, newClass)
|
||||||
|
// Prepare class scope for class-scope members (static) and future registrations
|
||||||
|
val classScope = createChildScope(newThisObj = newClass)
|
||||||
|
// Set lexical class context for visibility tagging inside class body
|
||||||
|
classScope.currentClassCtx = newClass
|
||||||
|
newClass.classScope = classScope
|
||||||
|
// Execute class body once in class scope to register instance methods and prepare instance field initializers
|
||||||
|
bodyInit?.execute(classScope)
|
||||||
if (initScope.isNotEmpty()) {
|
if (initScope.isNotEmpty()) {
|
||||||
val classScope = createChildScope(newThisObj = newClass)
|
|
||||||
newClass.classScope = classScope
|
|
||||||
for (s in initScope)
|
for (s in initScope)
|
||||||
s.execute(classScope)
|
s.execute(classScope)
|
||||||
}
|
}
|
||||||
|
// Fallback: ensure any functions declared in class scope are also present as instance methods
|
||||||
|
// (defensive in case some paths skipped cls.addFn during parsing/execution ordering)
|
||||||
|
for ((k, rec) in classScope.objects) {
|
||||||
|
val v = rec.value
|
||||||
|
if (v is Statement) {
|
||||||
|
if (newClass.members[k] == null) {
|
||||||
|
newClass.addFn(k, isOpen = true) {
|
||||||
|
(thisObj as? ObjInstance)?.let { i ->
|
||||||
|
v.execute(ClosureScope(this, i.instanceScope))
|
||||||
|
} ?: v.execute(thisObj.autoInstanceScope(this))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Debug summary: list registered instance methods and class-scope functions for this class
|
||||||
newClass
|
newClass
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1686,6 +1814,7 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
fnStatements.execute(context)
|
fnStatements.execute(context)
|
||||||
}
|
}
|
||||||
|
val enclosingCtx = parentContext
|
||||||
val fnCreateStatement = statement(start) { context ->
|
val fnCreateStatement = statement(start) { context ->
|
||||||
// we added fn in the context. now we must save closure
|
// we added fn in the context. now we must save closure
|
||||||
// for the function, unless we're in the class scope:
|
// for the function, unless we're in the class scope:
|
||||||
@ -1699,7 +1828,7 @@ class Compiler(
|
|||||||
// class extension method
|
// class extension method
|
||||||
val type = context[typeName]?.value ?: context.raiseSymbolNotFound("class $typeName not found")
|
val type = context[typeName]?.value ?: context.raiseSymbolNotFound("class $typeName not found")
|
||||||
if (type !is ObjClass) context.raiseClassCastError("$typeName is not the class instance")
|
if (type !is ObjClass) context.raiseClassCastError("$typeName is not the class instance")
|
||||||
type.addFn(name, isOpen = true) {
|
type.addFn(name, isOpen = true, visibility = visibility) {
|
||||||
// ObjInstance has a fixed instance scope, so we need to build a closure
|
// ObjInstance has a fixed instance scope, so we need to build a closure
|
||||||
(thisObj as? ObjInstance)?.let { i ->
|
(thisObj as? ObjInstance)?.let { i ->
|
||||||
annotatedFnBody.execute(ClosureScope(this, i.instanceScope))
|
annotatedFnBody.execute(ClosureScope(this, i.instanceScope))
|
||||||
@ -1709,7 +1838,31 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// regular function/method
|
// regular function/method
|
||||||
?: context.addItem(name, false, annotatedFnBody, visibility)
|
?: run {
|
||||||
|
val th = context.thisObj
|
||||||
|
if (!isStatic && th is ObjClass) {
|
||||||
|
// Instance method declared inside a class body: register on the class
|
||||||
|
val cls: ObjClass = th
|
||||||
|
cls.addFn(name, isOpen = true, visibility = visibility) {
|
||||||
|
// Execute with the instance as receiver; set caller lexical class for visibility
|
||||||
|
val savedCtx = this.currentClassCtx
|
||||||
|
this.currentClassCtx = cls
|
||||||
|
try {
|
||||||
|
(thisObj as? ObjInstance)?.let { i ->
|
||||||
|
annotatedFnBody.execute(ClosureScope(this, i.instanceScope))
|
||||||
|
} ?: annotatedFnBody.execute(thisObj.autoInstanceScope(this))
|
||||||
|
} finally {
|
||||||
|
this.currentClassCtx = savedCtx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// also expose the symbol in the class scope for possible references
|
||||||
|
context.addItem(name, false, annotatedFnBody, visibility)
|
||||||
|
annotatedFnBody
|
||||||
|
} else {
|
||||||
|
// top-level or nested function
|
||||||
|
context.addItem(name, false, annotatedFnBody, visibility)
|
||||||
|
}
|
||||||
|
}
|
||||||
// as the function can be called from anywhere, we have
|
// as the function can be called from anywhere, we have
|
||||||
// saved the proper context in the closure
|
// saved the proper context in the closure
|
||||||
annotatedFnBody
|
annotatedFnBody
|
||||||
@ -1791,9 +1944,18 @@ class Compiler(
|
|||||||
return NopStatement
|
return NopStatement
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine declaring class (if inside class body) at compile time, capture it in the closure
|
||||||
|
val declaringClassNameCaptured = (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.name
|
||||||
|
|
||||||
return statement(nameToken.pos) { context ->
|
return statement(nameToken.pos) { context ->
|
||||||
if (context.containsLocal(name))
|
// In true class bodies (not inside a function), store fields under a class-qualified key to support MI collisions
|
||||||
throw ScriptError(nameToken.pos, "Variable $name is already defined")
|
// Do NOT infer declaring class from runtime thisObj here; only the compile-time captured
|
||||||
|
// ClassBody qualifies for class-field storage. Otherwise, this is a plain local.
|
||||||
|
val declaringClassName = declaringClassNameCaptured
|
||||||
|
if (declaringClassName == null) {
|
||||||
|
if (context.containsLocal(name))
|
||||||
|
throw ScriptError(nameToken.pos, "Variable $name is already defined")
|
||||||
|
}
|
||||||
|
|
||||||
// Register the local name so subsequent identifiers can be emitted as fast locals
|
// Register the local name so subsequent identifiers can be emitted as fast locals
|
||||||
if (!isStatic) declareLocalName(name)
|
if (!isStatic) declareLocalName(name)
|
||||||
@ -1818,11 +1980,32 @@ class Compiler(
|
|||||||
// context.addItem(name, isMutable, it, visibility, recordType = ObjRecord.Type.Field)
|
// context.addItem(name, isMutable, it, visibility, recordType = ObjRecord.Type.Field)
|
||||||
// }
|
// }
|
||||||
} else {
|
} else {
|
||||||
// init value could be a val; when we initialize by-value type var with it, we need to
|
if (declaringClassName != null && !isStatic) {
|
||||||
// create a separate copy:
|
val storageName = "$declaringClassName::$name"
|
||||||
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
|
// If we are in class scope now (defining instance field), defer initialization to instance time
|
||||||
context.addItem(name, isMutable, initValue, visibility, recordType = ObjRecord.Type.Field)
|
val isClassScope = context.thisObj is ObjClass && (context.thisObj !is ObjInstance)
|
||||||
initValue
|
if (isClassScope) {
|
||||||
|
val cls = context.thisObj as ObjClass
|
||||||
|
// Defer: at instance construction, evaluate initializer in instance scope and store under mangled name
|
||||||
|
val initStmt = statement(nameToken.pos) { scp ->
|
||||||
|
val initValue = initialExpression?.execute(scp)?.byValueCopy() ?: ObjNull
|
||||||
|
scp.addOrUpdateItem(storageName, initValue, visibility, recordType = ObjRecord.Type.Field)
|
||||||
|
ObjVoid
|
||||||
|
}
|
||||||
|
cls.instanceInitializers += initStmt
|
||||||
|
ObjVoid
|
||||||
|
} else {
|
||||||
|
// We are in instance scope already: perform initialization immediately
|
||||||
|
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
|
||||||
|
context.addOrUpdateItem(storageName, initValue, visibility, recordType = ObjRecord.Type.Field)
|
||||||
|
initValue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not in class body: regular local/var declaration
|
||||||
|
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
|
||||||
|
context.addItem(name, isMutable, initValue, visibility, recordType = ObjRecord.Type.Field)
|
||||||
|
initValue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2027,6 +2210,13 @@ class Compiler(
|
|||||||
Operator(Token.Type.NOTIS, lastPriority) { _, a, b ->
|
Operator(Token.Type.NOTIS, lastPriority) { _, a, b ->
|
||||||
BinaryOpRef(BinOp.NOTIS, a, b)
|
BinaryOpRef(BinOp.NOTIS, a, b)
|
||||||
},
|
},
|
||||||
|
// casts: as / as?
|
||||||
|
Operator(Token.Type.AS, lastPriority) { pos, a, b ->
|
||||||
|
CastRef(a, b, false, pos)
|
||||||
|
},
|
||||||
|
Operator(Token.Type.ASNULL, lastPriority) { pos, a, b ->
|
||||||
|
CastRef(a, b, true, pos)
|
||||||
|
},
|
||||||
|
|
||||||
Operator(Token.Type.ELVIS, ++lastPriority, 2) { _, a, b ->
|
Operator(Token.Type.ELVIS, ++lastPriority, 2) { _, a, b ->
|
||||||
ElvisRef(a, b)
|
ElvisRef(a, b)
|
||||||
|
|||||||
@ -342,6 +342,11 @@ private class Parser(fromPos: Pos) {
|
|||||||
when (text) {
|
when (text) {
|
||||||
"in" -> Token("in", from, Token.Type.IN)
|
"in" -> Token("in", from, Token.Type.IN)
|
||||||
"is" -> Token("is", from, Token.Type.IS)
|
"is" -> Token("is", from, Token.Type.IS)
|
||||||
|
"as" -> {
|
||||||
|
// support both `as` and tight `as?` without spaces
|
||||||
|
if (currentChar == '?') { pos.advance(); Token("as?", from, Token.Type.ASNULL) }
|
||||||
|
else Token("as", from, Token.Type.AS)
|
||||||
|
}
|
||||||
else -> Token(text, from, Token.Type.ID)
|
else -> Token(text, from, Token.Type.ID)
|
||||||
}
|
}
|
||||||
} else
|
} else
|
||||||
|
|||||||
221
lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfProfiles.kt
Normal file
221
lynglib/src/commonMain/kotlin/net/sergeych/lyng/PerfProfiles.kt
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
/*
|
||||||
|
* 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.PerfProfiles.restore
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to quickly apply groups of [PerfFlags] for different workload profiles and restore them back.
|
||||||
|
* JVM-first defaults; safe to use on all targets. Applying a preset returns a [Snapshot] that can be
|
||||||
|
* passed to [restore] to revert the change.
|
||||||
|
*/
|
||||||
|
object PerfProfiles {
|
||||||
|
data class Snapshot(
|
||||||
|
val LOCAL_SLOT_PIC: Boolean,
|
||||||
|
val EMIT_FAST_LOCAL_REFS: Boolean,
|
||||||
|
|
||||||
|
val ARG_BUILDER: Boolean,
|
||||||
|
val ARG_SMALL_ARITY_12: Boolean,
|
||||||
|
val SKIP_ARGS_ON_NULL_RECEIVER: Boolean,
|
||||||
|
val SCOPE_POOL: Boolean,
|
||||||
|
|
||||||
|
val FIELD_PIC: Boolean,
|
||||||
|
val METHOD_PIC: Boolean,
|
||||||
|
val FIELD_PIC_SIZE_4: Boolean,
|
||||||
|
val METHOD_PIC_SIZE_4: Boolean,
|
||||||
|
val PIC_ADAPTIVE_2_TO_4: Boolean,
|
||||||
|
val PIC_ADAPTIVE_METHODS_ONLY: Boolean,
|
||||||
|
val PIC_ADAPTIVE_HEURISTIC: Boolean,
|
||||||
|
|
||||||
|
val INDEX_PIC: Boolean,
|
||||||
|
val INDEX_PIC_SIZE_4: Boolean,
|
||||||
|
|
||||||
|
val PIC_DEBUG_COUNTERS: Boolean,
|
||||||
|
|
||||||
|
val PRIMITIVE_FASTOPS: Boolean,
|
||||||
|
val RVAL_FASTPATH: Boolean,
|
||||||
|
|
||||||
|
val REGEX_CACHE: Boolean,
|
||||||
|
val RANGE_FAST_ITER: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun snapshot(): Snapshot = Snapshot(
|
||||||
|
LOCAL_SLOT_PIC = PerfFlags.LOCAL_SLOT_PIC,
|
||||||
|
EMIT_FAST_LOCAL_REFS = PerfFlags.EMIT_FAST_LOCAL_REFS,
|
||||||
|
|
||||||
|
ARG_BUILDER = PerfFlags.ARG_BUILDER,
|
||||||
|
ARG_SMALL_ARITY_12 = PerfFlags.ARG_SMALL_ARITY_12,
|
||||||
|
SKIP_ARGS_ON_NULL_RECEIVER = PerfFlags.SKIP_ARGS_ON_NULL_RECEIVER,
|
||||||
|
SCOPE_POOL = PerfFlags.SCOPE_POOL,
|
||||||
|
|
||||||
|
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,
|
||||||
|
PIC_ADAPTIVE_2_TO_4 = PerfFlags.PIC_ADAPTIVE_2_TO_4,
|
||||||
|
PIC_ADAPTIVE_METHODS_ONLY = PerfFlags.PIC_ADAPTIVE_METHODS_ONLY,
|
||||||
|
PIC_ADAPTIVE_HEURISTIC = PerfFlags.PIC_ADAPTIVE_HEURISTIC,
|
||||||
|
|
||||||
|
INDEX_PIC = PerfFlags.INDEX_PIC,
|
||||||
|
INDEX_PIC_SIZE_4 = PerfFlags.INDEX_PIC_SIZE_4,
|
||||||
|
|
||||||
|
PIC_DEBUG_COUNTERS = PerfFlags.PIC_DEBUG_COUNTERS,
|
||||||
|
|
||||||
|
PRIMITIVE_FASTOPS = PerfFlags.PRIMITIVE_FASTOPS,
|
||||||
|
RVAL_FASTPATH = PerfFlags.RVAL_FASTPATH,
|
||||||
|
|
||||||
|
REGEX_CACHE = PerfFlags.REGEX_CACHE,
|
||||||
|
RANGE_FAST_ITER = PerfFlags.RANGE_FAST_ITER,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun restore(s: Snapshot) {
|
||||||
|
PerfFlags.LOCAL_SLOT_PIC = s.LOCAL_SLOT_PIC
|
||||||
|
PerfFlags.EMIT_FAST_LOCAL_REFS = s.EMIT_FAST_LOCAL_REFS
|
||||||
|
|
||||||
|
PerfFlags.ARG_BUILDER = s.ARG_BUILDER
|
||||||
|
PerfFlags.ARG_SMALL_ARITY_12 = s.ARG_SMALL_ARITY_12
|
||||||
|
PerfFlags.SKIP_ARGS_ON_NULL_RECEIVER = s.SKIP_ARGS_ON_NULL_RECEIVER
|
||||||
|
PerfFlags.SCOPE_POOL = s.SCOPE_POOL
|
||||||
|
|
||||||
|
PerfFlags.FIELD_PIC = s.FIELD_PIC
|
||||||
|
PerfFlags.METHOD_PIC = s.METHOD_PIC
|
||||||
|
PerfFlags.FIELD_PIC_SIZE_4 = s.FIELD_PIC_SIZE_4
|
||||||
|
PerfFlags.METHOD_PIC_SIZE_4 = s.METHOD_PIC_SIZE_4
|
||||||
|
PerfFlags.PIC_ADAPTIVE_2_TO_4 = s.PIC_ADAPTIVE_2_TO_4
|
||||||
|
PerfFlags.PIC_ADAPTIVE_METHODS_ONLY = s.PIC_ADAPTIVE_METHODS_ONLY
|
||||||
|
PerfFlags.PIC_ADAPTIVE_HEURISTIC = s.PIC_ADAPTIVE_HEURISTIC
|
||||||
|
|
||||||
|
PerfFlags.INDEX_PIC = s.INDEX_PIC
|
||||||
|
PerfFlags.INDEX_PIC_SIZE_4 = s.INDEX_PIC_SIZE_4
|
||||||
|
|
||||||
|
PerfFlags.PIC_DEBUG_COUNTERS = s.PIC_DEBUG_COUNTERS
|
||||||
|
|
||||||
|
PerfFlags.PRIMITIVE_FASTOPS = s.PRIMITIVE_FASTOPS
|
||||||
|
PerfFlags.RVAL_FASTPATH = s.RVAL_FASTPATH
|
||||||
|
|
||||||
|
PerfFlags.REGEX_CACHE = s.REGEX_CACHE
|
||||||
|
PerfFlags.RANGE_FAST_ITER = s.RANGE_FAST_ITER
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Preset { BASELINE, BENCH, BOOKS }
|
||||||
|
|
||||||
|
/** Apply a preset and return a [Snapshot] of the previous state to allow restoring later. */
|
||||||
|
fun apply(preset: Preset): Snapshot {
|
||||||
|
val saved = snapshot()
|
||||||
|
when (preset) {
|
||||||
|
Preset.BASELINE -> applyBaseline()
|
||||||
|
Preset.BENCH -> applyBench()
|
||||||
|
Preset.BOOKS -> applyBooks()
|
||||||
|
}
|
||||||
|
return saved
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyBaseline() {
|
||||||
|
// Restore platform defaults. Note: INDEX_PIC follows FIELD_PIC parity by convention.
|
||||||
|
PerfFlags.LOCAL_SLOT_PIC = PerfDefaults.LOCAL_SLOT_PIC
|
||||||
|
PerfFlags.EMIT_FAST_LOCAL_REFS = PerfDefaults.EMIT_FAST_LOCAL_REFS
|
||||||
|
|
||||||
|
PerfFlags.ARG_BUILDER = PerfDefaults.ARG_BUILDER
|
||||||
|
PerfFlags.ARG_SMALL_ARITY_12 = PerfDefaults.ARG_SMALL_ARITY_12
|
||||||
|
PerfFlags.SKIP_ARGS_ON_NULL_RECEIVER = PerfDefaults.SKIP_ARGS_ON_NULL_RECEIVER
|
||||||
|
PerfFlags.SCOPE_POOL = PerfDefaults.SCOPE_POOL
|
||||||
|
|
||||||
|
PerfFlags.FIELD_PIC = PerfDefaults.FIELD_PIC
|
||||||
|
PerfFlags.METHOD_PIC = PerfDefaults.METHOD_PIC
|
||||||
|
PerfFlags.FIELD_PIC_SIZE_4 = PerfDefaults.FIELD_PIC_SIZE_4
|
||||||
|
PerfFlags.METHOD_PIC_SIZE_4 = PerfDefaults.METHOD_PIC_SIZE_4
|
||||||
|
PerfFlags.PIC_ADAPTIVE_2_TO_4 = PerfDefaults.PIC_ADAPTIVE_2_TO_4
|
||||||
|
PerfFlags.PIC_ADAPTIVE_METHODS_ONLY = PerfDefaults.PIC_ADAPTIVE_METHODS_ONLY
|
||||||
|
PerfFlags.PIC_ADAPTIVE_HEURISTIC = PerfDefaults.PIC_ADAPTIVE_HEURISTIC
|
||||||
|
|
||||||
|
PerfFlags.INDEX_PIC = PerfFlags.FIELD_PIC
|
||||||
|
PerfFlags.INDEX_PIC_SIZE_4 = PerfDefaults.INDEX_PIC_SIZE_4
|
||||||
|
|
||||||
|
PerfFlags.PIC_DEBUG_COUNTERS = PerfDefaults.PIC_DEBUG_COUNTERS
|
||||||
|
|
||||||
|
PerfFlags.PRIMITIVE_FASTOPS = PerfDefaults.PRIMITIVE_FASTOPS
|
||||||
|
PerfFlags.RVAL_FASTPATH = PerfDefaults.RVAL_FASTPATH
|
||||||
|
|
||||||
|
PerfFlags.REGEX_CACHE = PerfDefaults.REGEX_CACHE
|
||||||
|
PerfFlags.RANGE_FAST_ITER = PerfDefaults.RANGE_FAST_ITER
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyBench() {
|
||||||
|
// Expression-heavy micro-bench focus (JVM-first assumptions)
|
||||||
|
PerfFlags.LOCAL_SLOT_PIC = true
|
||||||
|
PerfFlags.EMIT_FAST_LOCAL_REFS = true
|
||||||
|
|
||||||
|
PerfFlags.ARG_BUILDER = true
|
||||||
|
PerfFlags.ARG_SMALL_ARITY_12 = false
|
||||||
|
PerfFlags.SKIP_ARGS_ON_NULL_RECEIVER = true
|
||||||
|
PerfFlags.SCOPE_POOL = true
|
||||||
|
|
||||||
|
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
|
||||||
|
PerfFlags.PIC_ADAPTIVE_METHODS_ONLY = false
|
||||||
|
PerfFlags.PIC_ADAPTIVE_HEURISTIC = false
|
||||||
|
|
||||||
|
PerfFlags.INDEX_PIC = true
|
||||||
|
PerfFlags.INDEX_PIC_SIZE_4 = true
|
||||||
|
|
||||||
|
PerfFlags.PIC_DEBUG_COUNTERS = false
|
||||||
|
|
||||||
|
PerfFlags.PRIMITIVE_FASTOPS = true
|
||||||
|
PerfFlags.RVAL_FASTPATH = true
|
||||||
|
|
||||||
|
// Keep regex cache/platform setting; enable on JVM typically
|
||||||
|
PerfFlags.REGEX_CACHE = PerfDefaults.REGEX_CACHE
|
||||||
|
PerfFlags.RANGE_FAST_ITER = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyBooks() {
|
||||||
|
// Documentation/book workload focus based on profiling observations.
|
||||||
|
// Favor simpler paths when hot expression paths are not dominant.
|
||||||
|
PerfFlags.LOCAL_SLOT_PIC = PerfDefaults.LOCAL_SLOT_PIC
|
||||||
|
PerfFlags.EMIT_FAST_LOCAL_REFS = PerfDefaults.EMIT_FAST_LOCAL_REFS
|
||||||
|
|
||||||
|
PerfFlags.ARG_BUILDER = false
|
||||||
|
PerfFlags.ARG_SMALL_ARITY_12 = false
|
||||||
|
PerfFlags.SKIP_ARGS_ON_NULL_RECEIVER = true
|
||||||
|
PerfFlags.SCOPE_POOL = false
|
||||||
|
|
||||||
|
PerfFlags.FIELD_PIC = false
|
||||||
|
PerfFlags.METHOD_PIC = false
|
||||||
|
PerfFlags.FIELD_PIC_SIZE_4 = false
|
||||||
|
PerfFlags.METHOD_PIC_SIZE_4 = false
|
||||||
|
PerfFlags.PIC_ADAPTIVE_2_TO_4 = false
|
||||||
|
PerfFlags.PIC_ADAPTIVE_METHODS_ONLY = false
|
||||||
|
PerfFlags.PIC_ADAPTIVE_HEURISTIC = false
|
||||||
|
|
||||||
|
PerfFlags.INDEX_PIC = false
|
||||||
|
PerfFlags.INDEX_PIC_SIZE_4 = false
|
||||||
|
|
||||||
|
PerfFlags.PIC_DEBUG_COUNTERS = false
|
||||||
|
|
||||||
|
PerfFlags.PRIMITIVE_FASTOPS = false
|
||||||
|
PerfFlags.RVAL_FASTPATH = false
|
||||||
|
|
||||||
|
// Keep regex cache/platform default; ON on JVM usually helps string APIs in books
|
||||||
|
PerfFlags.REGEX_CACHE = PerfDefaults.REGEX_CACHE
|
||||||
|
PerfFlags.RANGE_FAST_ITER = false
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -46,6 +46,8 @@ open class Scope(
|
|||||||
var thisObj: Obj = ObjVoid,
|
var thisObj: Obj = ObjVoid,
|
||||||
var skipScopeCreation: Boolean = false,
|
var skipScopeCreation: Boolean = false,
|
||||||
) {
|
) {
|
||||||
|
/** Lexical class context for visibility checks (propagates from parent). */
|
||||||
|
var currentClassCtx: net.sergeych.lyng.obj.ObjClass? = parent?.currentClassCtx
|
||||||
// Unique id per scope frame for PICs; regenerated on each borrow from the pool.
|
// Unique id per scope frame for PICs; regenerated on each borrow from the pool.
|
||||||
var frameId: Long = nextFrameId()
|
var frameId: Long = nextFrameId()
|
||||||
|
|
||||||
@ -53,6 +55,12 @@ open class Scope(
|
|||||||
// Enabled by default for child scopes; module/class scopes can ignore it.
|
// Enabled by default for child scopes; module/class scopes can ignore it.
|
||||||
private val slots: MutableList<ObjRecord> = mutableListOf()
|
private val slots: MutableList<ObjRecord> = mutableListOf()
|
||||||
private val nameToSlot: MutableMap<String, Int> = mutableMapOf()
|
private val nameToSlot: MutableMap<String, Int> = mutableMapOf()
|
||||||
|
/**
|
||||||
|
* Auxiliary per-frame map of local bindings (locals declared in this frame).
|
||||||
|
* This helps resolving locals across suspension when slot ownership isn't
|
||||||
|
* directly discoverable from the current frame.
|
||||||
|
*/
|
||||||
|
internal val localBindings: MutableMap<String, ObjRecord> = mutableMapOf()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hint internal collections to reduce reallocations for upcoming parameter/local assignments.
|
* Hint internal collections to reduce reallocations for upcoming parameter/local assignments.
|
||||||
@ -162,11 +170,15 @@ open class Scope(
|
|||||||
open operator fun get(name: String): ObjRecord? =
|
open operator fun get(name: String): ObjRecord? =
|
||||||
if (name == "this") thisObj.asReadonly
|
if (name == "this") thisObj.asReadonly
|
||||||
else {
|
else {
|
||||||
|
// Prefer direct locals/bindings declared in this frame
|
||||||
(objects[name]
|
(objects[name]
|
||||||
|
// Then, check known local bindings in this frame (helps after suspension)
|
||||||
|
?: localBindings[name]
|
||||||
|
// Walk up ancestry
|
||||||
?: parent?.get(name)
|
?: parent?.get(name)
|
||||||
?: thisObj.objClass
|
// Finally, fallback to class members on thisObj
|
||||||
.getInstanceMemberOrNull(name)
|
?: thisObj.objClass.getInstanceMemberOrNull(name)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slot fast-path API
|
// Slot fast-path API
|
||||||
@ -199,6 +211,7 @@ open class Scope(
|
|||||||
objects.clear()
|
objects.clear()
|
||||||
slots.clear()
|
slots.clear()
|
||||||
nameToSlot.clear()
|
nameToSlot.clear()
|
||||||
|
localBindings.clear()
|
||||||
// Pre-size local slots for upcoming parameter assignment where possible
|
// Pre-size local slots for upcoming parameter assignment where possible
|
||||||
reserveLocalCapacity(args.list.size + 4)
|
reserveLocalCapacity(args.list.size + 4)
|
||||||
}
|
}
|
||||||
@ -258,6 +271,13 @@ open class Scope(
|
|||||||
if( !it.isMutable )
|
if( !it.isMutable )
|
||||||
raiseIllegalAssignment("symbol is readonly: $name")
|
raiseIllegalAssignment("symbol is readonly: $name")
|
||||||
it.value = value
|
it.value = value
|
||||||
|
// keep local binding index consistent within the frame
|
||||||
|
localBindings[name] = it
|
||||||
|
// If we are a ClosureScope, mirror binding into the caller frame to keep it discoverable
|
||||||
|
// across suspension when resumed on the call frame
|
||||||
|
if (this is ClosureScope) {
|
||||||
|
callScope.localBindings[name] = it
|
||||||
|
}
|
||||||
it
|
it
|
||||||
} ?: addItem(name, true, value, visibility, recordType)
|
} ?: addItem(name, true, value, visibility, recordType)
|
||||||
|
|
||||||
@ -268,8 +288,23 @@ open class Scope(
|
|||||||
visibility: Visibility = Visibility.Public,
|
visibility: Visibility = Visibility.Public,
|
||||||
recordType: ObjRecord.Type = ObjRecord.Type.Other
|
recordType: ObjRecord.Type = ObjRecord.Type.Other
|
||||||
): ObjRecord {
|
): ObjRecord {
|
||||||
val rec = ObjRecord(value, isMutable, visibility, type = recordType)
|
val rec = ObjRecord(value, isMutable, visibility, declaringClass = currentClassCtx, type = recordType)
|
||||||
objects[name] = rec
|
objects[name] = rec
|
||||||
|
// Index this binding within the current frame to help resolve locals across suspension
|
||||||
|
localBindings[name] = rec
|
||||||
|
// If we are a ClosureScope, mirror binding into the caller frame to keep it discoverable
|
||||||
|
// across suspension when resumed on the call frame
|
||||||
|
if (this is ClosureScope) {
|
||||||
|
callScope.localBindings[name] = rec
|
||||||
|
// Additionally, expose the binding in caller's objects and slot map so identifier
|
||||||
|
// resolution after suspension can still find it even if the active scope is a child
|
||||||
|
// of the callScope (e.g., due to internal withChildFrame usage).
|
||||||
|
// This keeps visibility within the method body but prevents leaking outside the caller frame.
|
||||||
|
callScope.objects[name] = rec
|
||||||
|
if (callScope.getSlotIndexOf(name) == null) {
|
||||||
|
callScope.allocateSlotFor(name, rec)
|
||||||
|
}
|
||||||
|
}
|
||||||
// Map to a slot for fast local access (if not already mapped)
|
// Map to a slot for fast local access (if not already mapped)
|
||||||
if (getSlotIndexOf(name) == null) {
|
if (getSlotIndexOf(name) == null) {
|
||||||
allocateSlotFor(name, rec)
|
allocateSlotFor(name, rec)
|
||||||
|
|||||||
@ -43,6 +43,8 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
|
|||||||
SHL, SHR,
|
SHL, SHR,
|
||||||
SINLGE_LINE_COMMENT, MULTILINE_COMMENT,
|
SINLGE_LINE_COMMENT, MULTILINE_COMMENT,
|
||||||
LABEL, ATLABEL, // label@ at@label
|
LABEL, ATLABEL, // label@ at@label
|
||||||
|
// type-checking/casting
|
||||||
|
AS, ASNULL,
|
||||||
//PUBLIC, PROTECTED, INTERNAL, EXPORT, OPEN, INLINE, OVERRIDE, ABSTRACT, SEALED, EXTERNAL, VAL, VAR, CONST, TYPE, FUN, CLASS, INTERFACE, ENUM, OBJECT, TRAIT, THIS,
|
//PUBLIC, PROTECTED, INTERNAL, EXPORT, OPEN, INLINE, OVERRIDE, ABSTRACT, SEALED, EXTERNAL, VAL, VAR, CONST, TYPE, FUN, CLASS, INTERFACE, ENUM, OBJECT, TRAIT, THIS,
|
||||||
ELLIPSIS, DOTDOT, DOTDOTLT,
|
ELLIPSIS, DOTDOT, DOTDOTLT,
|
||||||
NEWLINE,
|
NEWLINE,
|
||||||
|
|||||||
@ -22,4 +22,18 @@ enum class Visibility {
|
|||||||
val isPublic by lazy { this == Public }
|
val isPublic by lazy { this == Public }
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
val isProtected by lazy { this == Protected }
|
val isProtected by lazy { this == Protected }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** MI-aware visibility check: whether [caller] can access a member declared in [decl] with [visibility]. */
|
||||||
|
fun canAccessMember(visibility: Visibility, decl: net.sergeych.lyng.obj.ObjClass?, caller: net.sergeych.lyng.obj.ObjClass?): Boolean {
|
||||||
|
return when (visibility) {
|
||||||
|
Visibility.Public -> true
|
||||||
|
Visibility.Private -> (decl != null && caller === decl)
|
||||||
|
Visibility.Protected -> when {
|
||||||
|
decl == null -> false
|
||||||
|
caller == null -> false
|
||||||
|
caller === decl -> true
|
||||||
|
else -> (caller.allParentsSet.contains(decl))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -80,13 +80,26 @@ open class Obj {
|
|||||||
args: Arguments = Arguments.EMPTY,
|
args: Arguments = Arguments.EMPTY,
|
||||||
onNotFoundResult: (() -> Obj?)? = null
|
onNotFoundResult: (() -> Obj?)? = null
|
||||||
): Obj {
|
): Obj {
|
||||||
return objClass.getInstanceMemberOrNull(name)?.value?.invoke(
|
val rec = objClass.getInstanceMemberOrNull(name)
|
||||||
scope,
|
if (rec != null) {
|
||||||
this,
|
val decl = rec.declaringClass ?: objClass.findDeclaringClassOf(name)
|
||||||
args
|
val caller = scope.currentClassCtx
|
||||||
)
|
if (!canAccessMember(rec.visibility, decl, caller))
|
||||||
?: onNotFoundResult?.invoke()
|
scope.raiseError(ObjAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})"))
|
||||||
?: scope.raiseSymbolNotFound(name)
|
// Propagate declaring class as current class context during method execution
|
||||||
|
val saved = scope.currentClassCtx
|
||||||
|
scope.currentClassCtx = decl
|
||||||
|
try {
|
||||||
|
return rec.value.invoke(scope, this, args)
|
||||||
|
} finally {
|
||||||
|
scope.currentClassCtx = saved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return onNotFoundResult?.invoke()
|
||||||
|
?: scope.raiseError(
|
||||||
|
"no such member: $name on ${objClass.className}. Considered order: ${objClass.renderLinearization(true)}. " +
|
||||||
|
"Tip: try this@Base.$name(...) or (obj as Base).$name(...) if ambiguous"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
open suspend fun getInstanceMethod(
|
open suspend fun getInstanceMethod(
|
||||||
@ -258,7 +271,13 @@ open class Obj {
|
|||||||
|
|
||||||
open suspend fun readField(scope: Scope, name: String): ObjRecord {
|
open suspend fun readField(scope: Scope, name: String): ObjRecord {
|
||||||
// could be property or class field:
|
// could be property or class field:
|
||||||
val obj = objClass.getInstanceMemberOrNull(name) ?: scope.raiseError("no such field: $name")
|
val obj = objClass.getInstanceMemberOrNull(name) ?: scope.raiseError(
|
||||||
|
"no such field: $name on ${objClass.className}. Considered order: ${objClass.renderLinearization(true)}"
|
||||||
|
)
|
||||||
|
val decl = obj.declaringClass ?: objClass.findDeclaringClassOf(name)
|
||||||
|
val caller = scope.currentClassCtx
|
||||||
|
if (!canAccessMember(obj.visibility, decl, caller))
|
||||||
|
scope.raiseError(ObjAccessException(scope, "can't access field ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})"))
|
||||||
return when (val value = obj.value) {
|
return when (val value = obj.value) {
|
||||||
is Statement -> {
|
is Statement -> {
|
||||||
ObjRecord(value.execute(scope.createChildScope(scope.pos, newThisObj = this)), obj.isMutable)
|
ObjRecord(value.execute(scope.createChildScope(scope.pos, newThisObj = this)), obj.isMutable)
|
||||||
@ -271,7 +290,13 @@ open class Obj {
|
|||||||
|
|
||||||
open suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
open suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
||||||
willMutate(scope)
|
willMutate(scope)
|
||||||
val field = objClass.getInstanceMemberOrNull(name) ?: scope.raiseError("no such field: $name")
|
val field = objClass.getInstanceMemberOrNull(name) ?: scope.raiseError(
|
||||||
|
"no such field: $name on ${objClass.className}. Considered order: ${objClass.renderLinearization(true)}"
|
||||||
|
)
|
||||||
|
val decl = field.declaringClass ?: objClass.findDeclaringClassOf(name)
|
||||||
|
val caller = scope.currentClassCtx
|
||||||
|
if (!canAccessMember(field.visibility, decl, caller))
|
||||||
|
scope.raiseError(ObjAccessException(scope, "can't assign field ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})"))
|
||||||
if (field.isMutable) field.value = newValue else scope.raiseError("can't assign to read-only field: $name")
|
if (field.isMutable) field.value = newValue else scope.raiseError("can't assign to read-only field: $name")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -40,6 +40,12 @@ open class ObjClass(
|
|||||||
var constructorMeta: ArgsDeclaration? = null
|
var constructorMeta: ArgsDeclaration? = null
|
||||||
var instanceConstructor: Statement? = null
|
var instanceConstructor: Statement? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-instance initializers collected from class body (for instance fields). These are executed
|
||||||
|
* during construction in the instance scope of the object, once per class in the hierarchy.
|
||||||
|
*/
|
||||||
|
val instanceInitializers: MutableList<Statement> = mutableListOf()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the scope for class methods, initialize class vars, etc.
|
* the scope for class methods, initialize class vars, etc.
|
||||||
*
|
*
|
||||||
@ -50,10 +56,77 @@ open class ObjClass(
|
|||||||
*/
|
*/
|
||||||
var classScope: Scope? = null
|
var classScope: Scope? = null
|
||||||
|
|
||||||
|
/** Direct parents in declaration order (kept deterministic). */
|
||||||
|
val directParents: List<ObjClass> = parents.toList()
|
||||||
|
|
||||||
|
/** Optional constructor argument specs for each direct parent (set by compiler for user classes). */
|
||||||
|
open val directParentArgs: MutableMap<ObjClass, List<ParsedArgument>> = mutableMapOf()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All ancestors as a Set for fast `isInstanceOf` checks. Order is not guaranteed here and
|
||||||
|
* must not be used for resolution — use [parentsLinearized] instead.
|
||||||
|
*/
|
||||||
val allParentsSet: Set<ObjClass> =
|
val allParentsSet: Set<ObjClass> =
|
||||||
parents.flatMap {
|
buildSet {
|
||||||
listOf(it) + it.allParentsSet
|
fun collect(c: ObjClass) {
|
||||||
}.toMutableSet()
|
if (add(c)) c.directParents.forEach { collect(it) }
|
||||||
|
}
|
||||||
|
directParents.forEach { collect(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- C3 Method Resolution Order (MRO) ---
|
||||||
|
private fun c3Merge(seqs: MutableList<MutableList<ObjClass>>): List<ObjClass> {
|
||||||
|
val result = mutableListOf<ObjClass>()
|
||||||
|
while (seqs.isNotEmpty()) {
|
||||||
|
// remove empty lists
|
||||||
|
seqs.removeAll { it.isEmpty() }
|
||||||
|
if (seqs.isEmpty()) break
|
||||||
|
var candidate: ObjClass? = null
|
||||||
|
outer@ for (seq in seqs) {
|
||||||
|
val head = seq.first()
|
||||||
|
// head must not appear in any other list's tail
|
||||||
|
var inTail = false
|
||||||
|
for (other in seqs) {
|
||||||
|
if (other === seq || other.size <= 1) continue
|
||||||
|
if (other.drop(1).contains(head)) { inTail = true; break }
|
||||||
|
}
|
||||||
|
if (!inTail) { candidate = head; break@outer }
|
||||||
|
}
|
||||||
|
val picked = candidate ?: throw ScriptError(Pos.builtIn, "C3 MRO failed: inconsistent hierarchy for $className")
|
||||||
|
result += picked
|
||||||
|
// remove picked from heads
|
||||||
|
for (seq in seqs) if (seq.isNotEmpty() && seq.first() === picked) seq.removeAt(0)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun c3Linearize(self: ObjClass, visited: MutableMap<ObjClass, List<ObjClass>>): List<ObjClass> {
|
||||||
|
visited[self]?.let { return it }
|
||||||
|
// Linearize parents first
|
||||||
|
val parentLinearizations = self.directParents.map { c3Linearize(it, visited) }
|
||||||
|
// Merge parent MROs with the direct parent list
|
||||||
|
val toMerge: MutableList<MutableList<ObjClass>> = mutableListOf()
|
||||||
|
parentLinearizations.forEach { toMerge += it.toMutableList() }
|
||||||
|
toMerge += self.directParents.toMutableList()
|
||||||
|
val merged = c3Merge(toMerge)
|
||||||
|
val mro = listOf(self) + merged
|
||||||
|
visited[self] = mro
|
||||||
|
return mro
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Full C3 MRO including this class at index 0. */
|
||||||
|
val mro: List<ObjClass> by lazy { c3Linearize(this, mutableMapOf()) }
|
||||||
|
|
||||||
|
/** Parents in C3 order (no self). */
|
||||||
|
val mroParents: List<ObjClass> by lazy { mro.drop(1) }
|
||||||
|
|
||||||
|
/** Render current linearization order for diagnostics (C3). */
|
||||||
|
fun renderLinearization(includeSelf: Boolean = true): String {
|
||||||
|
val list = mutableListOf<String>()
|
||||||
|
if (includeSelf) list += className
|
||||||
|
mroParents.forEach { list += it.className }
|
||||||
|
return list.joinToString(" → ")
|
||||||
|
}
|
||||||
|
|
||||||
override val objClass: ObjClass by lazy { ObjClassType }
|
override val objClass: ObjClass by lazy { ObjClassType }
|
||||||
|
|
||||||
@ -73,9 +146,70 @@ open class ObjClass(
|
|||||||
// remains stable even when call frames are pooled and reused.
|
// remains stable even when call frames are pooled and reused.
|
||||||
val stableParent = scope.parent
|
val stableParent = scope.parent
|
||||||
instance.instanceScope = Scope(stableParent, scope.args, scope.pos, instance)
|
instance.instanceScope = Scope(stableParent, scope.args, scope.pos, instance)
|
||||||
if (instanceConstructor != null) {
|
// Expose instance methods (and other callable members) directly in the instance scope for fast lookup
|
||||||
instanceConstructor!!.execute(instance.instanceScope)
|
// This mirrors Obj.autoInstanceScope behavior for ad-hoc scopes and makes fb.method() resolution robust
|
||||||
|
// 1) members-defined methods
|
||||||
|
for ((k, v) in members) {
|
||||||
|
if (v.value is Statement) {
|
||||||
|
instance.instanceScope.objects[k] = v
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// 2) class-scope methods registered during class-body execution
|
||||||
|
classScope?.objects?.forEach { (k, rec) ->
|
||||||
|
if (rec.value is Statement) {
|
||||||
|
// if not already present, copy reference for dispatch
|
||||||
|
if (!instance.instanceScope.objects.containsKey(k)) {
|
||||||
|
instance.instanceScope.objects[k] = rec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Constructor chaining MVP: initialize base classes left-to-right, then this class.
|
||||||
|
// Ensure each ancestor is initialized at most once (diamond-safe).
|
||||||
|
val visited = hashSetOf<ObjClass>()
|
||||||
|
|
||||||
|
suspend fun initClass(c: ObjClass, argsForThis: Arguments?, isRoot: Boolean = false) {
|
||||||
|
if (!visited.add(c)) return
|
||||||
|
// For the most-derived class, bind its constructor params BEFORE parents so base arg thunks can see them
|
||||||
|
if (isRoot) {
|
||||||
|
c.constructorMeta?.let { meta ->
|
||||||
|
val argsHere = argsForThis ?: Arguments.EMPTY
|
||||||
|
meta.assignToContext(instance.instanceScope, argsHere)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Initialize direct parents first, in order
|
||||||
|
for (p in c.directParents) {
|
||||||
|
val raw = c.directParentArgs[p]?.toArguments(instance.instanceScope, false)
|
||||||
|
val limited = if (raw != null) {
|
||||||
|
val need = p.constructorMeta?.params?.size ?: 0
|
||||||
|
if (need == 0) Arguments.EMPTY else Arguments(raw.list.take(need), tailBlockMode = false)
|
||||||
|
} else Arguments.EMPTY
|
||||||
|
initClass(p, limited)
|
||||||
|
}
|
||||||
|
// Execute per-instance initializers collected from class body for this class
|
||||||
|
if (c.instanceInitializers.isNotEmpty()) {
|
||||||
|
val savedCtx = instance.instanceScope.currentClassCtx
|
||||||
|
instance.instanceScope.currentClassCtx = c
|
||||||
|
try {
|
||||||
|
for (initStmt in c.instanceInitializers) {
|
||||||
|
initStmt.execute(instance.instanceScope)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
instance.instanceScope.currentClassCtx = savedCtx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Then run this class' constructor, if any
|
||||||
|
c.instanceConstructor?.let { ctor ->
|
||||||
|
// Bind this class's constructor parameters into the instance scope now, right before ctor
|
||||||
|
c.constructorMeta?.let { meta ->
|
||||||
|
val argsHere = argsForThis ?: Arguments.EMPTY
|
||||||
|
meta.assignToContext(instance.instanceScope, argsHere)
|
||||||
|
}
|
||||||
|
val execScope = instance.instanceScope.createChildScope(args = argsForThis ?: Arguments.EMPTY, newThisObj = instance)
|
||||||
|
ctor.execute(execScope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initClass(this, instance.instanceScope.args, isRoot = true)
|
||||||
return instance
|
return instance
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,10 +225,12 @@ open class ObjClass(
|
|||||||
visibility: Visibility = Visibility.Public,
|
visibility: Visibility = Visibility.Public,
|
||||||
pos: Pos = Pos.builtIn
|
pos: Pos = Pos.builtIn
|
||||||
) {
|
) {
|
||||||
val existing = members[name] ?: allParentsSet.firstNotNullOfOrNull { it.members[name] }
|
// Allow overriding ancestors: only prevent redefinition if THIS class already defines an immutable member
|
||||||
if (existing?.isMutable == false)
|
val existingInSelf = members[name]
|
||||||
throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
|
if (existingInSelf != null && existingInSelf.isMutable == false)
|
||||||
members[name] = ObjRecord(initialValue, isMutable, visibility)
|
throw ScriptError(pos, "$name is already defined in $objClass")
|
||||||
|
// Install/override in this class
|
||||||
|
members[name] = ObjRecord(initialValue, isMutable, visibility, declaringClass = this)
|
||||||
// Structural change: bump layout version for PIC invalidation
|
// Structural change: bump layout version for PIC invalidation
|
||||||
layoutVersion += 1
|
layoutVersion += 1
|
||||||
}
|
}
|
||||||
@ -120,8 +256,14 @@ open class ObjClass(
|
|||||||
layoutVersion += 1
|
layoutVersion += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addFn(name: String, isOpen: Boolean = false, code: suspend Scope.() -> Obj) {
|
fun addFn(
|
||||||
createField(name, statement { code() }, isOpen)
|
name: String,
|
||||||
|
isOpen: Boolean = false,
|
||||||
|
visibility: net.sergeych.lyng.Visibility = net.sergeych.lyng.Visibility.Public,
|
||||||
|
code: suspend Scope.() -> Obj
|
||||||
|
) {
|
||||||
|
val stmt = statement { code() }
|
||||||
|
createField(name, stmt, isOpen, visibility)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addConst(name: String, value: Obj) = createField(name, value, isMutable = false)
|
fun addConst(name: String, value: Obj) = createField(name, value, isMutable = false)
|
||||||
@ -135,15 +277,46 @@ open class ObjClass(
|
|||||||
* Get instance member traversing the hierarchy if needed. Its meaning is different for different objects.
|
* Get instance member traversing the hierarchy if needed. Its meaning is different for different objects.
|
||||||
*/
|
*/
|
||||||
fun getInstanceMemberOrNull(name: String): ObjRecord? {
|
fun getInstanceMemberOrNull(name: String): ObjRecord? {
|
||||||
members[name]?.let { return it }
|
// Unified traversal in strict C3 order: self, then each ancestor, checking members before classScope
|
||||||
allParentsSet.forEach { parent -> parent.getInstanceMemberOrNull(name)?.let { return it } }
|
for (cls in mro) {
|
||||||
|
cls.members[name]?.let { return it }
|
||||||
|
cls.classScope?.objects?.get(name)?.let { return it }
|
||||||
|
}
|
||||||
|
// Finally, allow root object fallback (rare; mostly built-ins like toString)
|
||||||
return rootObjectType.members[name]
|
return rootObjectType.members[name]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Find the declaring class where a member with [name] is defined, starting from this class along MRO. */
|
||||||
|
fun findDeclaringClassOf(name: String): ObjClass? {
|
||||||
|
if (members.containsKey(name)) return this
|
||||||
|
for (anc in mroParents) {
|
||||||
|
if (anc.members.containsKey(name)) return anc
|
||||||
|
}
|
||||||
|
return if (rootObjectType.members.containsKey(name)) rootObjectType else null
|
||||||
|
}
|
||||||
|
|
||||||
fun getInstanceMember(atPos: Pos, name: String): ObjRecord =
|
fun getInstanceMember(atPos: Pos, name: String): ObjRecord =
|
||||||
getInstanceMemberOrNull(name)
|
getInstanceMemberOrNull(name)
|
||||||
?: throw ScriptError(atPos, "symbol doesn't exist: $name")
|
?: throw ScriptError(atPos, "symbol doesn't exist: $name")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve member starting from a specific ancestor class [start], not from this class.
|
||||||
|
* Searches [start] first, then traverses its linearized parents.
|
||||||
|
*/
|
||||||
|
fun getInstanceMemberFromAncestor(start: ObjClass, name: String): ObjRecord? {
|
||||||
|
val order = mro
|
||||||
|
val idx = order.indexOf(start)
|
||||||
|
if (idx < 0) return null
|
||||||
|
for (i in idx until order.size) {
|
||||||
|
val cls = order[i]
|
||||||
|
// Prefer true instance members on the class
|
||||||
|
cls.members[name]?.let { return it }
|
||||||
|
// Fallback to class-scope function registered during class-body execution
|
||||||
|
cls.classScope?.objects?.get(name)?.let { return it }
|
||||||
|
}
|
||||||
|
return rootObjectType.members[name]
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun readField(scope: Scope, name: String): ObjRecord {
|
override suspend fun readField(scope: Scope, name: String): ObjRecord {
|
||||||
classScope?.objects?.get(name)?.let {
|
classScope?.objects?.get(name)?.let {
|
||||||
if (it.visibility.isPublic) return it
|
if (it.visibility.isPublic) return it
|
||||||
|
|||||||
@ -19,6 +19,7 @@ package net.sergeych.lyng.obj
|
|||||||
|
|
||||||
import net.sergeych.lyng.Arguments
|
import net.sergeych.lyng.Arguments
|
||||||
import net.sergeych.lyng.Scope
|
import net.sergeych.lyng.Scope
|
||||||
|
import net.sergeych.lyng.canAccessMember
|
||||||
import net.sergeych.lynon.LynonDecoder
|
import net.sergeych.lynon.LynonDecoder
|
||||||
import net.sergeych.lynon.LynonEncoder
|
import net.sergeych.lynon.LynonEncoder
|
||||||
import net.sergeych.lynon.LynonType
|
import net.sergeych.lynon.LynonType
|
||||||
@ -28,40 +29,124 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
internal lateinit var instanceScope: Scope
|
internal lateinit var instanceScope: Scope
|
||||||
|
|
||||||
override suspend fun readField(scope: Scope, name: String): ObjRecord {
|
override suspend fun readField(scope: Scope, name: String): ObjRecord {
|
||||||
return instanceScope[name]?.let {
|
// Direct (unmangled) lookup first
|
||||||
if (it.visibility.isPublic)
|
instanceScope[name]?.let {
|
||||||
it
|
val decl = it.declaringClass ?: objClass.findDeclaringClassOf(name)
|
||||||
else
|
val caller = scope.currentClassCtx ?: instanceScope.currentClassCtx
|
||||||
scope.raiseError(ObjAccessException(scope, "can't access non-public field $name"))
|
val allowed = if (it.visibility == net.sergeych.lyng.Visibility.Private) (decl === objClass) else canAccessMember(it.visibility, decl, caller)
|
||||||
|
if (!allowed)
|
||||||
|
scope.raiseError(ObjAccessException(scope, "can't access field $name (declared in ${decl?.className ?: "?"})"))
|
||||||
|
return it
|
||||||
}
|
}
|
||||||
?: super.readField(scope, name)
|
// Try MI-mangled lookup along linearization (C3 MRO): ClassName::name
|
||||||
|
val cls = objClass
|
||||||
|
// self first, then parents
|
||||||
|
fun findMangled(): ObjRecord? {
|
||||||
|
// self
|
||||||
|
instanceScope.objects["${cls.className}::$name"]?.let { return it }
|
||||||
|
// ancestors in deterministic C3 order
|
||||||
|
for (p in cls.mroParents) {
|
||||||
|
instanceScope.objects["${p.className}::$name"]?.let { return it }
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
findMangled()?.let { rec ->
|
||||||
|
val declName = rec.importedFrom?.packageName // unused; use mangled key instead
|
||||||
|
// derive declaring class by mangled prefix: try self then parents
|
||||||
|
val declaring = when {
|
||||||
|
instanceScope.objects.containsKey("${cls.className}::$name") -> cls
|
||||||
|
else -> cls.mroParents.firstOrNull { instanceScope.objects.containsKey("${it.className}::$name") }
|
||||||
|
}
|
||||||
|
val caller = scope.currentClassCtx ?: instanceScope.currentClassCtx
|
||||||
|
val allowed = if (rec.visibility == net.sergeych.lyng.Visibility.Private) (declaring === objClass) else canAccessMember(rec.visibility, declaring, caller)
|
||||||
|
if (!allowed)
|
||||||
|
scope.raiseError(ObjAccessException(scope, "can't access field $name (declared in ${declaring?.className ?: "?"})"))
|
||||||
|
return rec
|
||||||
|
}
|
||||||
|
// Fall back to methods/properties on class
|
||||||
|
return super.readField(scope, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
||||||
|
// Direct (unmangled) first
|
||||||
instanceScope[name]?.let { f ->
|
instanceScope[name]?.let { f ->
|
||||||
if (!f.visibility.isPublic)
|
val decl = f.declaringClass ?: objClass.findDeclaringClassOf(name)
|
||||||
ObjIllegalAssignmentException(scope, "can't assign to non-public field $name")
|
val caller = scope.currentClassCtx ?: instanceScope.currentClassCtx
|
||||||
|
val allowed = if (f.visibility == net.sergeych.lyng.Visibility.Private) (decl === objClass) else canAccessMember(f.visibility, decl, caller)
|
||||||
|
if (!allowed)
|
||||||
|
ObjIllegalAssignmentException(scope, "can't assign to field $name (declared in ${decl?.className ?: "?"})").raise()
|
||||||
if (!f.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
if (!f.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
||||||
if (f.value.assign(scope, newValue) == null)
|
if (f.value.assign(scope, newValue) == null)
|
||||||
f.value = newValue
|
f.value = newValue
|
||||||
} ?: super.writeField(scope, name, newValue)
|
return
|
||||||
|
}
|
||||||
|
// Try MI-mangled resolution along linearization (C3 MRO)
|
||||||
|
val cls = objClass
|
||||||
|
fun findMangled(): ObjRecord? {
|
||||||
|
instanceScope.objects["${cls.className}::$name"]?.let { return it }
|
||||||
|
for (p in cls.mroParents) {
|
||||||
|
instanceScope.objects["${p.className}::$name"]?.let { return it }
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val rec = findMangled()
|
||||||
|
if (rec != null) {
|
||||||
|
val declaring = when {
|
||||||
|
instanceScope.objects.containsKey("${cls.className}::$name") -> cls
|
||||||
|
else -> cls.mroParents.firstOrNull { instanceScope.objects.containsKey("${it.className}::$name") }
|
||||||
|
}
|
||||||
|
val caller = scope.currentClassCtx ?: instanceScope.currentClassCtx
|
||||||
|
val allowed = if (rec.visibility == net.sergeych.lyng.Visibility.Private) (declaring === objClass) else canAccessMember(rec.visibility, declaring, caller)
|
||||||
|
if (!allowed)
|
||||||
|
ObjIllegalAssignmentException(scope, "can't assign to field $name (declared in ${declaring?.className ?: "?"})").raise()
|
||||||
|
if (!rec.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
||||||
|
if (rec.value.assign(scope, newValue) == null)
|
||||||
|
rec.value = newValue
|
||||||
|
return
|
||||||
|
}
|
||||||
|
super.writeField(scope, name, newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments,
|
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments,
|
||||||
onNotFoundResult: (()->Obj?)?): Obj =
|
onNotFoundResult: (()->Obj?)?): Obj =
|
||||||
instanceScope[name]?.let {
|
instanceScope[name]?.let { rec ->
|
||||||
if (it.visibility.isPublic)
|
val decl = rec.declaringClass ?: objClass.findDeclaringClassOf(name)
|
||||||
it.value.invoke(
|
val caller = scope.currentClassCtx ?: instanceScope.currentClassCtx
|
||||||
|
val allowed = if (rec.visibility == net.sergeych.lyng.Visibility.Private) (decl === objClass) else canAccessMember(rec.visibility, decl, caller)
|
||||||
|
if (!allowed)
|
||||||
|
scope.raiseError(ObjAccessException(scope, "can't invoke method $name (declared in ${decl?.className ?: "?"})"))
|
||||||
|
// execute with lexical class context propagated to declaring class
|
||||||
|
val saved = instanceScope.currentClassCtx
|
||||||
|
instanceScope.currentClassCtx = decl
|
||||||
|
try {
|
||||||
|
rec.value.invoke(
|
||||||
instanceScope,
|
instanceScope,
|
||||||
this,
|
this,
|
||||||
args)
|
args)
|
||||||
else
|
} finally {
|
||||||
scope.raiseError(ObjAccessException(scope, "can't invoke non-public method $name"))
|
instanceScope.currentClassCtx = saved
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
?: run {
|
||||||
|
// fallback: class-scope function (registered during class body execution)
|
||||||
|
objClass.classScope?.objects?.get(name)?.let { rec ->
|
||||||
|
val decl = rec.declaringClass ?: objClass.findDeclaringClassOf(name)
|
||||||
|
val caller = scope.currentClassCtx
|
||||||
|
if (!canAccessMember(rec.visibility, decl, caller))
|
||||||
|
scope.raiseError(ObjAccessException(scope, "can't invoke method $name (declared in ${decl?.className ?: "?"})"))
|
||||||
|
val saved = instanceScope.currentClassCtx
|
||||||
|
instanceScope.currentClassCtx = decl
|
||||||
|
try {
|
||||||
|
rec.value.invoke(instanceScope, this, args)
|
||||||
|
} finally {
|
||||||
|
instanceScope.currentClassCtx = saved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
|
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
|
||||||
|
|
||||||
private val publicFields: Map<String, ObjRecord>
|
private val publicFields: Map<String, ObjRecord>
|
||||||
get() = instanceScope.objects.filter { it.value.visibility.isPublic }
|
get() = instanceScope.objects.filter { it.value.visibility.isPublic && it.value.type.serializable }
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
val fields = publicFields.map { "${it.key}=${it.value.value}" }.joinToString(",")
|
val fields = publicFields.map { "${it.key}=${it.value.value}" }.joinToString(",")
|
||||||
@ -120,4 +205,117 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
|||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A qualified view over an [ObjInstance] that resolves members starting from a specific ancestor class.
|
||||||
|
* It does not change identity; it only affects lookup precedence for fields and methods.
|
||||||
|
*/
|
||||||
|
class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjClass) : Obj() {
|
||||||
|
override val objClass: ObjClass get() = instance.objClass
|
||||||
|
|
||||||
|
private fun memberFromAncestor(name: String): ObjRecord? =
|
||||||
|
instance.objClass.getInstanceMemberFromAncestor(startClass, name)
|
||||||
|
|
||||||
|
override suspend fun readField(scope: Scope, name: String): ObjRecord {
|
||||||
|
// Qualified field access: prefer mangled storage for the qualified ancestor
|
||||||
|
val mangled = "${startClass.className}::$name"
|
||||||
|
instance.instanceScope.objects[mangled]?.let { rec ->
|
||||||
|
// Visibility: declaring class is the qualified ancestor for mangled storage
|
||||||
|
val decl = rec.declaringClass ?: startClass
|
||||||
|
val caller = scope.currentClassCtx
|
||||||
|
if (!net.sergeych.lyng.canAccessMember(rec.visibility, decl, caller))
|
||||||
|
scope.raiseError(ObjAccessException(scope, "can't access field $name (declared in ${decl.className})"))
|
||||||
|
return rec
|
||||||
|
}
|
||||||
|
// Then try instance locals (unmangled) only if startClass is the dynamic class itself
|
||||||
|
if (startClass === instance.objClass) {
|
||||||
|
instance.instanceScope[name]?.let { rec ->
|
||||||
|
val decl = rec.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
|
||||||
|
val caller = scope.currentClassCtx
|
||||||
|
if (!net.sergeych.lyng.canAccessMember(rec.visibility, decl, caller))
|
||||||
|
scope.raiseError(ObjAccessException(scope, "can't access field $name (declared in ${decl?.className ?: "?"})"))
|
||||||
|
return rec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Finally try methods/properties starting from ancestor
|
||||||
|
val r = memberFromAncestor(name) ?: scope.raiseError("no such field: $name")
|
||||||
|
val decl = r.declaringClass ?: startClass
|
||||||
|
val caller = scope.currentClassCtx
|
||||||
|
if (!net.sergeych.lyng.canAccessMember(r.visibility, decl, caller))
|
||||||
|
scope.raiseError(ObjAccessException(scope, "can't access field $name (declared in ${decl.className})"))
|
||||||
|
return when (val value = r.value) {
|
||||||
|
is net.sergeych.lyng.Statement -> ObjRecord(value.execute(instance.instanceScope.createChildScope(scope.pos, newThisObj = instance)), r.isMutable)
|
||||||
|
else -> r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
||||||
|
// Qualified write: target mangled storage for the ancestor
|
||||||
|
val mangled = "${startClass.className}::$name"
|
||||||
|
instance.instanceScope.objects[mangled]?.let { f ->
|
||||||
|
val decl = f.declaringClass ?: startClass
|
||||||
|
val caller = scope.currentClassCtx
|
||||||
|
if (!net.sergeych.lyng.canAccessMember(f.visibility, decl, caller))
|
||||||
|
ObjIllegalAssignmentException(scope, "can't assign to field $name (declared in ${decl.className})").raise()
|
||||||
|
if (!f.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
||||||
|
if (f.value.assign(scope, newValue) == null) f.value = newValue
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// If start is dynamic class, allow unmangled
|
||||||
|
if (startClass === instance.objClass) {
|
||||||
|
instance.instanceScope[name]?.let { f ->
|
||||||
|
val decl = f.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
|
||||||
|
val caller = scope.currentClassCtx
|
||||||
|
if (!net.sergeych.lyng.canAccessMember(f.visibility, decl, caller))
|
||||||
|
ObjIllegalAssignmentException(scope, "can't assign to field $name (declared in ${decl?.className ?: "?"})").raise()
|
||||||
|
if (!f.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
||||||
|
if (f.value.assign(scope, newValue) == null) f.value = newValue
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val r = memberFromAncestor(name) ?: scope.raiseError("no such field: $name")
|
||||||
|
val decl = r.declaringClass ?: startClass
|
||||||
|
val caller = scope.currentClassCtx
|
||||||
|
if (!net.sergeych.lyng.canAccessMember(r.visibility, decl, caller))
|
||||||
|
ObjIllegalAssignmentException(scope, "can't assign to field $name (declared in ${decl.className})").raise()
|
||||||
|
if (!r.isMutable) scope.raiseError("can't assign to read-only field: $name")
|
||||||
|
if (r.value.assign(scope, newValue) == null) r.value = newValue
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments, onNotFoundResult: (() -> Obj?)?): Obj {
|
||||||
|
// Qualified method dispatch must start from the specified ancestor, not from the instance scope.
|
||||||
|
memberFromAncestor(name)?.let { rec ->
|
||||||
|
val decl = rec.declaringClass ?: startClass
|
||||||
|
val caller = scope.currentClassCtx
|
||||||
|
if (!net.sergeych.lyng.canAccessMember(rec.visibility, decl, caller))
|
||||||
|
scope.raiseError(ObjAccessException(scope, "can't invoke method $name (declared in ${decl.className})"))
|
||||||
|
val saved = instance.instanceScope.currentClassCtx
|
||||||
|
instance.instanceScope.currentClassCtx = decl
|
||||||
|
try {
|
||||||
|
return rec.value.invoke(instance.instanceScope, instance, args)
|
||||||
|
} finally {
|
||||||
|
instance.instanceScope.currentClassCtx = saved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If the qualifier is the dynamic class itself, allow instance-scope methods as a fallback
|
||||||
|
if (startClass === instance.objClass) {
|
||||||
|
instance.instanceScope[name]?.let { rec ->
|
||||||
|
val decl = rec.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
|
||||||
|
val caller = scope.currentClassCtx
|
||||||
|
if (!net.sergeych.lyng.canAccessMember(rec.visibility, decl, caller))
|
||||||
|
scope.raiseError(ObjAccessException(scope, "can't invoke method $name (declared in ${decl?.className ?: "?"})"))
|
||||||
|
val saved = instance.instanceScope.currentClassCtx
|
||||||
|
instance.instanceScope.currentClassCtx = decl
|
||||||
|
try {
|
||||||
|
return rec.value.invoke(instance.instanceScope, instance, args)
|
||||||
|
} finally {
|
||||||
|
instance.instanceScope.currentClassCtx = saved
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return onNotFoundResult?.invoke() ?: scope.raiseSymbolNotFound(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = instance.toString()
|
||||||
}
|
}
|
||||||
@ -25,7 +25,7 @@ import net.sergeych.lynon.LynonType
|
|||||||
/**
|
/**
|
||||||
* Special variant of [ObjClass] to be used in [ObjInstance], e.g. for Lyng compiled classes
|
* Special variant of [ObjClass] to be used in [ObjInstance], e.g. for Lyng compiled classes
|
||||||
*/
|
*/
|
||||||
class ObjInstanceClass(val name: String) : ObjClass(name) {
|
class ObjInstanceClass(val name: String, vararg parents: ObjClass) : ObjClass(name, *parents) {
|
||||||
|
|
||||||
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
|
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
|
||||||
val args = decoder.decodeAnyList(scope)
|
val args = decoder.decodeAnyList(scope)
|
||||||
@ -41,7 +41,6 @@ class ObjInstanceClass(val name: String) : ObjClass(name) {
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
addFn("toString", true) {
|
addFn("toString", true) {
|
||||||
println("-------------- tos! --------------")
|
|
||||||
ObjString(thisObj.toString())
|
ObjString(thisObj.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,6 +33,9 @@ class ObjMutex(val mutex: Mutex): Obj() {
|
|||||||
}.apply {
|
}.apply {
|
||||||
addFn("withLock") {
|
addFn("withLock") {
|
||||||
val f = requiredArg<Statement>(0)
|
val f = requiredArg<Statement>(0)
|
||||||
|
// Execute user lambda directly in the current scope to preserve the active scope
|
||||||
|
// ancestry across suspension points. The lambda still constructs a ClosureScope
|
||||||
|
// on top of this frame, and parseLambdaExpression sets skipScopeCreation for its body.
|
||||||
thisAs<ObjMutex>().mutex.withLock { f.execute(this) }
|
thisAs<ObjMutex>().mutex.withLock { f.execute(this) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,6 +27,8 @@ data class ObjRecord(
|
|||||||
var value: Obj,
|
var value: Obj,
|
||||||
val isMutable: Boolean,
|
val isMutable: Boolean,
|
||||||
val visibility: Visibility = Visibility.Public,
|
val visibility: Visibility = Visibility.Public,
|
||||||
|
/** If non-null, denotes the class that declared this member (field/method). */
|
||||||
|
val declaringClass: ObjClass? = null,
|
||||||
var importedFrom: Scope? = null,
|
var importedFrom: Scope? = null,
|
||||||
val isTransient: Boolean = false,
|
val isTransient: Boolean = false,
|
||||||
val type: Type = Type.Other
|
val type: Type = Type.Other
|
||||||
|
|||||||
@ -253,6 +253,49 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Cast operator reference: left `as` rightType or `as?` (nullable). */
|
||||||
|
class CastRef(
|
||||||
|
private val valueRef: ObjRef,
|
||||||
|
private val typeRef: ObjRef,
|
||||||
|
private val isNullable: Boolean,
|
||||||
|
private val atPos: Pos,
|
||||||
|
) : ObjRef {
|
||||||
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
|
val v0 = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) valueRef.evalValue(scope) else valueRef.get(scope).value
|
||||||
|
val t = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) typeRef.evalValue(scope) else typeRef.get(scope).value
|
||||||
|
val target = (t as? ObjClass) ?: scope.raiseClassCastError("${'$'}t is not the class instance")
|
||||||
|
// unwrap qualified views
|
||||||
|
val v = when (v0) {
|
||||||
|
is ObjQualifiedView -> v0.instance
|
||||||
|
else -> v0
|
||||||
|
}
|
||||||
|
return if (v.isInstanceOf(target)) {
|
||||||
|
// For instances, return a qualified view to enforce ancestor-start dispatch
|
||||||
|
if (v is ObjInstance) ObjQualifiedView(v, target).asReadonly else v.asReadonly
|
||||||
|
} else {
|
||||||
|
if (isNullable) ObjNull.asReadonly else scope.raiseClassCastError(
|
||||||
|
"Cannot cast ${'$'}{(v as? Obj)?.objClass?.className ?: v::class.simpleName} to ${'$'}{target.className}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Qualified `this@Type`: resolves to a view of current `this` starting dispatch from the ancestor Type. */
|
||||||
|
class QualifiedThisRef(private val typeName: String, private val atPos: Pos) : ObjRef {
|
||||||
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
|
val thisObj = scope.thisObj
|
||||||
|
val t = scope[typeName]?.value as? ObjClass
|
||||||
|
?: scope.raiseError("unknown type $typeName")
|
||||||
|
val inst = (thisObj as? ObjInstance)
|
||||||
|
?: scope.raiseClassCastError("this is not an instance")
|
||||||
|
if (!inst.objClass.allParentsSet.contains(t) && inst.objClass !== t)
|
||||||
|
scope.raiseClassCastError(
|
||||||
|
"Qualifier ${'$'}{t.className} is not an ancestor of ${'$'}{inst.objClass.className} (order: ${'$'}{inst.objClass.renderLinearization(true)})"
|
||||||
|
)
|
||||||
|
return ObjQualifiedView(inst, t).asReadonly
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Assignment compound op: target op= value */
|
/** Assignment compound op: target op= value */
|
||||||
class AssignOpRef(
|
class AssignOpRef(
|
||||||
private val op: BinOp,
|
private val op: BinOp,
|
||||||
@ -1168,13 +1211,20 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
|||||||
|
|
||||||
override suspend fun get(scope: Scope): ObjRecord {
|
override suspend fun get(scope: Scope): ObjRecord {
|
||||||
scope.pos = atPos
|
scope.pos = atPos
|
||||||
|
// 1) Try fast slot/local
|
||||||
if (!PerfFlags.LOCAL_SLOT_PIC) {
|
if (!PerfFlags.LOCAL_SLOT_PIC) {
|
||||||
scope.getSlotIndexOf(name)?.let {
|
scope.getSlotIndexOf(name)?.let {
|
||||||
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.localVarPicHit++
|
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.localVarPicHit++
|
||||||
return scope.getSlotRecord(it)
|
return scope.getSlotRecord(it)
|
||||||
}
|
}
|
||||||
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.localVarPicMiss++
|
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.localVarPicMiss++
|
||||||
return scope[name] ?: scope.raiseError("symbol not defined: '$name'")
|
// 2) Fallback to current-scope object or field on `this`
|
||||||
|
scope[name]?.let { return it }
|
||||||
|
val th = scope.thisObj
|
||||||
|
return when (th) {
|
||||||
|
is Obj -> th.readField(scope, name)
|
||||||
|
else -> scope.raiseError("symbol not defined: '$name'")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
|
val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
|
||||||
val slot = if (hit) cachedSlot else resolveSlot(scope)
|
val slot = if (hit) cachedSlot else resolveSlot(scope)
|
||||||
@ -1185,19 +1235,37 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
|||||||
return scope.getSlotRecord(slot)
|
return scope.getSlotRecord(slot)
|
||||||
}
|
}
|
||||||
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.localVarPicMiss++
|
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) net.sergeych.lyng.PerfStats.localVarPicMiss++
|
||||||
return scope[name] ?: scope.raiseError("symbol not defined: '$name'")
|
// 2) Fallback name in scope or field on `this`
|
||||||
|
scope[name]?.let { return it }
|
||||||
|
val th = scope.thisObj
|
||||||
|
return when (th) {
|
||||||
|
is Obj -> th.readField(scope, name)
|
||||||
|
else -> scope.raiseError("symbol not defined: '$name'")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun evalValue(scope: Scope): Obj {
|
override suspend fun evalValue(scope: Scope): Obj {
|
||||||
scope.pos = atPos
|
scope.pos = atPos
|
||||||
if (!PerfFlags.LOCAL_SLOT_PIC) {
|
if (!PerfFlags.LOCAL_SLOT_PIC) {
|
||||||
scope.getSlotIndexOf(name)?.let { return scope.getSlotRecord(it).value }
|
scope.getSlotIndexOf(name)?.let { return scope.getSlotRecord(it).value }
|
||||||
return (scope[name] ?: scope.raiseError("symbol not defined: '$name'")).value
|
// fallback to current-scope object or field on `this`
|
||||||
|
scope[name]?.let { return it.value }
|
||||||
|
val th = scope.thisObj
|
||||||
|
return when (th) {
|
||||||
|
is Obj -> th.readField(scope, name).value
|
||||||
|
else -> scope.raiseError("symbol not defined: '$name'")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
|
val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
|
||||||
val slot = if (hit) cachedSlot else resolveSlot(scope)
|
val slot = if (hit) cachedSlot else resolveSlot(scope)
|
||||||
if (slot >= 0) return scope.getSlotRecord(slot).value
|
if (slot >= 0) return scope.getSlotRecord(slot).value
|
||||||
return (scope[name] ?: scope.raiseError("symbol not defined: '$name'")).value
|
// Fallback name in scope or field on `this`
|
||||||
|
scope[name]?.let { return it.value }
|
||||||
|
val th = scope.thisObj
|
||||||
|
return when (th) {
|
||||||
|
is Obj -> th.readField(scope, name).value
|
||||||
|
else -> scope.raiseError("symbol not defined: '$name'")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
|
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
|
||||||
@ -1209,10 +1277,18 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
|||||||
rec.value = newValue
|
rec.value = newValue
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val stored = scope[name] ?: scope.raiseError("symbol not defined: '$name'")
|
scope[name]?.let { stored ->
|
||||||
if (stored.isMutable) stored.value = newValue
|
if (stored.isMutable) stored.value = newValue
|
||||||
else scope.raiseError("Cannot assign to immutable value")
|
else scope.raiseError("Cannot assign to immutable value")
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
// Fallback: write to field on `this`
|
||||||
|
val th = scope.thisObj
|
||||||
|
if (th is Obj) {
|
||||||
|
th.writeField(scope, name, newValue)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
scope.raiseError("symbol not defined: '$name'")
|
||||||
}
|
}
|
||||||
val slot = if (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount()) cachedSlot else resolveSlot(scope)
|
val slot = if (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount()) cachedSlot else resolveSlot(scope)
|
||||||
if (slot >= 0) {
|
if (slot >= 0) {
|
||||||
@ -1221,9 +1297,17 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
|||||||
rec.value = newValue
|
rec.value = newValue
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val stored = scope[name] ?: scope.raiseError("symbol not defined: '$name'")
|
scope[name]?.let { stored ->
|
||||||
if (stored.isMutable) stored.value = newValue
|
if (stored.isMutable) stored.value = newValue
|
||||||
else scope.raiseError("Cannot assign to immutable value")
|
else scope.raiseError("Cannot assign to immutable value")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val th = scope.thisObj
|
||||||
|
if (th is Obj) {
|
||||||
|
th.writeField(scope, name, newValue)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
scope.raiseError("symbol not defined: '$name'")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1298,11 +1382,32 @@ class FastLocalVarRef(
|
|||||||
val ownerValid = isOwnerValidFor(scope)
|
val ownerValid = isOwnerValidFor(scope)
|
||||||
val slot = if (ownerValid && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope)
|
val slot = if (ownerValid && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope)
|
||||||
val actualOwner = cachedOwnerScope
|
val actualOwner = cachedOwnerScope
|
||||||
if (slot < 0 || actualOwner == null) scope.raiseError("local '$name' is not available in this scope")
|
if (slot >= 0 && actualOwner != null) {
|
||||||
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) {
|
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) {
|
||||||
if (ownerValid) net.sergeych.lyng.PerfStats.fastLocalHit++ else net.sergeych.lyng.PerfStats.fastLocalMiss++
|
if (ownerValid) net.sergeych.lyng.PerfStats.fastLocalHit++ else net.sergeych.lyng.PerfStats.fastLocalMiss++
|
||||||
|
}
|
||||||
|
return actualOwner.getSlotRecord(slot)
|
||||||
}
|
}
|
||||||
return actualOwner.getSlotRecord(slot)
|
// Try per-frame local binding maps in the ancestry first (locals declared in frames)
|
||||||
|
run {
|
||||||
|
var s: Scope? = scope
|
||||||
|
while (s != null) {
|
||||||
|
s.localBindings[name]?.let { return it }
|
||||||
|
s = s.parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Try to find a direct local binding in the current ancestry (without invoking name resolution that may prefer fields)
|
||||||
|
var s: Scope? = scope
|
||||||
|
while (s != null) {
|
||||||
|
s.objects[name]?.let { return it }
|
||||||
|
s = s.parent
|
||||||
|
}
|
||||||
|
// Fallback to standard name lookup (locals or closure chain) if the slot owner changed across suspension
|
||||||
|
scope[name]?.let { return it }
|
||||||
|
// As a last resort, treat as field on `this`
|
||||||
|
val th = scope.thisObj
|
||||||
|
if (th is Obj) return th.readField(scope, name)
|
||||||
|
scope.raiseError("local '$name' is not available in this scope")
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun evalValue(scope: Scope): Obj {
|
override suspend fun evalValue(scope: Scope): Obj {
|
||||||
@ -1310,8 +1415,26 @@ class FastLocalVarRef(
|
|||||||
val ownerValid = isOwnerValidFor(scope)
|
val ownerValid = isOwnerValidFor(scope)
|
||||||
val slot = if (ownerValid && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope)
|
val slot = if (ownerValid && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope)
|
||||||
val actualOwner = cachedOwnerScope
|
val actualOwner = cachedOwnerScope
|
||||||
if (slot < 0 || actualOwner == null) scope.raiseError("local '$name' is not available in this scope")
|
if (slot >= 0 && actualOwner != null) return actualOwner.getSlotRecord(slot).value
|
||||||
return actualOwner.getSlotRecord(slot).value
|
// Try per-frame local binding maps in the ancestry first
|
||||||
|
run {
|
||||||
|
var s: Scope? = scope
|
||||||
|
while (s != null) {
|
||||||
|
s.localBindings[name]?.let { return it.value }
|
||||||
|
s = s.parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Try to find a direct local binding in the current ancestry first
|
||||||
|
var s: Scope? = scope
|
||||||
|
while (s != null) {
|
||||||
|
s.objects[name]?.let { return it.value }
|
||||||
|
s = s.parent
|
||||||
|
}
|
||||||
|
// Fallback to standard name lookup (locals or closure chain)
|
||||||
|
scope[name]?.let { return it.value }
|
||||||
|
val th = scope.thisObj
|
||||||
|
if (th is Obj) return th.readField(scope, name).value
|
||||||
|
scope.raiseError("local '$name' is not available in this scope")
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
|
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
|
||||||
@ -1319,10 +1442,37 @@ class FastLocalVarRef(
|
|||||||
val owner = if (isOwnerValidFor(scope)) cachedOwnerScope else null
|
val owner = if (isOwnerValidFor(scope)) cachedOwnerScope else null
|
||||||
val slot = if (owner != null && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope)
|
val slot = if (owner != null && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope)
|
||||||
val actualOwner = cachedOwnerScope
|
val actualOwner = cachedOwnerScope
|
||||||
if (slot < 0 || actualOwner == null) scope.raiseError("local '$name' is not available in this scope")
|
if (slot >= 0 && actualOwner != null) {
|
||||||
val rec = actualOwner.getSlotRecord(slot)
|
val rec = actualOwner.getSlotRecord(slot)
|
||||||
if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value")
|
if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value")
|
||||||
rec.value = newValue
|
rec.value = newValue
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Try per-frame local binding maps in the ancestry first
|
||||||
|
run {
|
||||||
|
var s: Scope? = scope
|
||||||
|
while (s != null) {
|
||||||
|
val rec = s.localBindings[name]
|
||||||
|
if (rec != null) {
|
||||||
|
if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value")
|
||||||
|
rec.value = newValue
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s = s.parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback to standard name lookup
|
||||||
|
scope[name]?.let { stored ->
|
||||||
|
if (stored.isMutable) stored.value = newValue
|
||||||
|
else scope.raiseError("Cannot assign to immutable value")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val th = scope.thisObj
|
||||||
|
if (th is Obj) {
|
||||||
|
th.writeField(scope, name, newValue)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
scope.raiseError("local '$name' is not available in this scope")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1385,3 +1535,5 @@ class AssignRef(
|
|||||||
return v.asReadonly
|
return v.asReadonly
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// (duplicate LocalVarRef removed; the canonical implementation is defined earlier in this file)
|
||||||
68
lynglib/src/commonTest/kotlin/MIC3MroTest.kt
Normal file
68
lynglib/src/commonTest/kotlin/MIC3MroTest.kt
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* C3 MRO tests for Multiple Inheritance
|
||||||
|
*/
|
||||||
|
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import net.sergeych.lyng.eval
|
||||||
|
import kotlin.test.Test
|
||||||
|
|
||||||
|
class MIC3MroTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun diamondConstructorRunsSharedAncestorOnce() = runTest {
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
var topInit = 0
|
||||||
|
|
||||||
|
class Top() {
|
||||||
|
// increment module-scope counter in instance initializer; runs once per Top-subobject
|
||||||
|
var tick = (topInit = topInit + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Left() : Top()
|
||||||
|
class Right() : Top()
|
||||||
|
class Bottom() : Left(), Right()
|
||||||
|
|
||||||
|
val b = Bottom()
|
||||||
|
assertEquals(1, topInit)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun methodResolutionFollowsC3() = runTest {
|
||||||
|
// For the classic diamond D(B,C), B and C derive from A; C3 should result in D -> B -> C -> A
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
class A() { fun common() { "A" } }
|
||||||
|
class B() : A() { fun common() { "B" } }
|
||||||
|
class C() : A() { fun common() { "C" } }
|
||||||
|
class D() : B(), C()
|
||||||
|
|
||||||
|
val d = D()
|
||||||
|
// Unqualified must pick B.common() according to C3 when direct bases are (B, C)
|
||||||
|
assertEquals("B", d.common())
|
||||||
|
// Qualified disambiguation via casts still works
|
||||||
|
assertEquals("C", (d as C).common())
|
||||||
|
assertEquals("A", (d as A).common())
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
105
lynglib/src/commonTest/kotlin/MIDiagnosticsTest.kt
Normal file
105
lynglib/src/commonTest/kotlin/MIDiagnosticsTest.kt
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Diagnostics tests for Multiple Inheritance (MI)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import net.sergeych.lyng.eval
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertFails
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class MIDiagnosticsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun missingMemberIncludesLinearizationAndHint() = runTest {
|
||||||
|
val ex = assertFails {
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
class Foo(val a) { fun runA() { "ResultA:" + a } }
|
||||||
|
class Bar(val b) { fun runB() { "ResultB:" + b } }
|
||||||
|
class FooBar(a,b) : Foo(a), Bar(b) { }
|
||||||
|
val fb = FooBar(1,2)
|
||||||
|
fb.qux()
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val msg = ex.message ?: ""
|
||||||
|
assertTrue(msg.contains("no such member: qux"), "must mention missing member name")
|
||||||
|
assertTrue(msg.contains("FooBar"), "must mention receiver class name")
|
||||||
|
assertTrue(msg.contains("Considered order:"), "must include linearization header")
|
||||||
|
assertTrue(msg.contains("FooBar") && msg.contains("Foo") && msg.contains("Bar"), "must list classes in order")
|
||||||
|
assertTrue(msg.contains("this@") || msg.contains("(obj as"), "must suggest qualification or cast")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun missingFieldIncludesLinearization() = runTest {
|
||||||
|
val ex = assertFails {
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
class Foo(val a) { var tag = "F" }
|
||||||
|
class Bar(val b) { var tag = "B" }
|
||||||
|
class FooBar(a,b) : Foo(a), Bar(b) { }
|
||||||
|
val fb = FooBar(1,2)
|
||||||
|
fb.unknownField
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val msg = ex.message ?: ""
|
||||||
|
assertTrue(msg.contains("no such field: unknownField"))
|
||||||
|
assertTrue(msg.contains("FooBar"))
|
||||||
|
assertTrue(msg.contains("Considered order:"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun invalidQualifiedThisReportsAncestorError() = runTest {
|
||||||
|
assertFails {
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
class Foo() { fun f() { "F" } }
|
||||||
|
class Bar() { fun g() { "G" } }
|
||||||
|
class Baz() : Foo() {
|
||||||
|
fun bad() { this@Bar.g() }
|
||||||
|
}
|
||||||
|
val b = Baz()
|
||||||
|
b.bad()
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun castFailureMentionsActualAndTargetTypes() = runTest {
|
||||||
|
val ex = assertFails {
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
class Foo() { }
|
||||||
|
class Bar() { }
|
||||||
|
val b = Bar()
|
||||||
|
(b as Foo)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val msg = ex.message ?: ""
|
||||||
|
// message like: Cannot cast Bar to Foo (be tolerant across targets)
|
||||||
|
val lower = msg.lowercase()
|
||||||
|
assertTrue(lower.contains("cast"), "message should mention cast")
|
||||||
|
assertTrue(msg.contains("Bar") || msg.contains("Foo"), "should mention at least one of the types")
|
||||||
|
}
|
||||||
|
}
|
||||||
98
lynglib/src/commonTest/kotlin/MIQualifiedDispatchTest.kt
Normal file
98
lynglib/src/commonTest/kotlin/MIQualifiedDispatchTest.kt
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* 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.test.runTest
|
||||||
|
import net.sergeych.lyng.eval
|
||||||
|
import kotlin.test.Test
|
||||||
|
|
||||||
|
class MIQualifiedDispatchTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testQualifiedMethodResolution() = runTest {
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
class Foo(val a) {
|
||||||
|
fun common() { "A" }
|
||||||
|
fun runA() { "ResultA:" + a }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Bar(val b) {
|
||||||
|
fun common() { "B" }
|
||||||
|
fun runB() { "ResultB:" + b }
|
||||||
|
}
|
||||||
|
|
||||||
|
class FooBar(a,b) : Foo(a), Bar(b) { }
|
||||||
|
|
||||||
|
val fb = FooBar(1,2)
|
||||||
|
|
||||||
|
// unqualified picks leftmost base
|
||||||
|
assertEquals("A", fb.common())
|
||||||
|
|
||||||
|
// cast-based disambiguation
|
||||||
|
assertEquals("B", (fb as Bar).common())
|
||||||
|
assertEquals("A", (fb as Foo).common())
|
||||||
|
|
||||||
|
// Note: wrappers using this@Type inside FooBar body will be validated later
|
||||||
|
// when class-body method registration is finalized.
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testQualifiedFieldReadWrite() = runTest {
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
class Foo(val a) { var tag = "F" }
|
||||||
|
class Bar(val b) { var tag = "B" }
|
||||||
|
class FooBar(a,b) : Foo(a), Bar(b) { }
|
||||||
|
|
||||||
|
val fb = FooBar(1,2)
|
||||||
|
// unqualified resolves to leftmost base
|
||||||
|
assertEquals("F", fb.tag)
|
||||||
|
// qualified reads via casts
|
||||||
|
assertEquals("F", (fb as Foo).tag)
|
||||||
|
assertEquals("B", (fb as Bar).tag)
|
||||||
|
|
||||||
|
// unqualified write updates leftmost base
|
||||||
|
fb.tag = "X"
|
||||||
|
assertEquals("X", fb.tag)
|
||||||
|
assertEquals("X", (fb as Foo).tag)
|
||||||
|
assertEquals("B", (fb as Bar).tag)
|
||||||
|
|
||||||
|
// qualified write via cast targets Bar
|
||||||
|
(fb as Bar).tag = "Y"
|
||||||
|
assertEquals("X", (fb as Foo).tag)
|
||||||
|
assertEquals("Y", (fb as Bar).tag)
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCastsAndSafeCall() = runTest {
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
class Foo(val a) { fun runA() { "ResultA:" + a } }
|
||||||
|
class Bar(val b) { fun runB() { "ResultB:" + b } }
|
||||||
|
class Buzz : Bar(3)
|
||||||
|
val buzz = Buzz()
|
||||||
|
assertEquals("ResultB:3", buzz.runB())
|
||||||
|
assertEquals("ResultB:3", (buzz as? Bar)?.runB())
|
||||||
|
assertEquals(null, (buzz as? Foo)?.runA())
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
75
lynglib/src/commonTest/kotlin/MIVisibilityTest.kt
Normal file
75
lynglib/src/commonTest/kotlin/MIVisibilityTest.kt
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* 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.test.runTest
|
||||||
|
import net.sergeych.lyng.eval
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertFails
|
||||||
|
|
||||||
|
class MIVisibilityTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun privateMethodNotVisibleInSubclass() = runTest {
|
||||||
|
val code = """
|
||||||
|
class Foo() {
|
||||||
|
private fun secret() { "S" }
|
||||||
|
}
|
||||||
|
class Bar() : Foo() {
|
||||||
|
fun trySecret() { this@Foo.secret() }
|
||||||
|
}
|
||||||
|
val b = Bar()
|
||||||
|
// calling a wrapper that tries to access Foo.secret must fail
|
||||||
|
b.trySecret()
|
||||||
|
""".trimIndent()
|
||||||
|
assertFails { eval(code) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun protectedMethodNotVisibleFromUnrelatedEvenViaCast() = runTest {
|
||||||
|
val code = """
|
||||||
|
class Foo() { protected fun prot() { "P" } }
|
||||||
|
class Bar() : Foo() { }
|
||||||
|
val b = Bar()
|
||||||
|
// Unrelated context tries to call through cast — should fail
|
||||||
|
(b as Foo).prot()
|
||||||
|
""".trimIndent()
|
||||||
|
assertFails { eval(code) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun privateFieldNotVisibleInSubclass() = runTest {
|
||||||
|
val code = """
|
||||||
|
class Foo() { private val x = "X" }
|
||||||
|
class Bar() : Foo() { fun getX() { this@Foo.x } }
|
||||||
|
val b = Bar()
|
||||||
|
b.getX()
|
||||||
|
""".trimIndent()
|
||||||
|
assertFails { eval(code) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun protectedFieldNotVisibleFromUnrelatedEvenViaCast() = runTest {
|
||||||
|
// Not allowed from unrelated, even via cast
|
||||||
|
val code = """
|
||||||
|
class Foo() { protected val y = "Y" }
|
||||||
|
class Bar() : Foo() { }
|
||||||
|
val b = Bar()
|
||||||
|
(b as Foo).y
|
||||||
|
""".trimIndent()
|
||||||
|
assertFails { eval(code) }
|
||||||
|
}
|
||||||
|
}
|
||||||
55
lynglib/src/commonTest/kotlin/ParallelLocalScopeTest.kt
Normal file
55
lynglib/src/commonTest/kotlin/ParallelLocalScopeTest.kt
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Focused regression test for local variable visibility across suspension inside withLock { }
|
||||||
|
*/
|
||||||
|
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import net.sergeych.lyng.eval
|
||||||
|
import kotlin.test.Test
|
||||||
|
|
||||||
|
class ParallelLocalScopeTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun localsSurviveSuspensionInsideWithLock() = runTest {
|
||||||
|
// Minimal reproduction of the pattern used in ScriptTest.testParallels2
|
||||||
|
eval(
|
||||||
|
"""
|
||||||
|
class AtomicCounter {
|
||||||
|
private val m = Mutex()
|
||||||
|
private var counter = 0
|
||||||
|
|
||||||
|
fun increment() {
|
||||||
|
m.withLock {
|
||||||
|
val a = counter
|
||||||
|
delay(1)
|
||||||
|
counter = a + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCounter() { counter }
|
||||||
|
}
|
||||||
|
|
||||||
|
val ac = AtomicCounter()
|
||||||
|
// Single-threaded increments should work if locals survive after delay
|
||||||
|
for (i in 1..3) ac.increment()
|
||||||
|
assertEquals(3, ac.getCounter())
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
154
lynglib/src/commonTest/kotlin/TestInheritance.kt
Normal file
154
lynglib/src/commonTest/kotlin/TestInheritance.kt
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
/*
|
||||||
|
* 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.test.runTest
|
||||||
|
import net.sergeych.lyng.eval
|
||||||
|
import kotlin.test.Test
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
class TestInheritance {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testInheritanceSpecification() = runTest {
|
||||||
|
eval("""
|
||||||
|
// Multiple inheritance specification test (spec only, parser/interpreter TBD)
|
||||||
|
|
||||||
|
// Parent A: exposes a val and a var, and a method with a name that collides with Bar.common()
|
||||||
|
class Foo(val a) {
|
||||||
|
var tag = "F"
|
||||||
|
|
||||||
|
fun runA() {
|
||||||
|
"ResultA:" + a
|
||||||
|
}
|
||||||
|
|
||||||
|
fun common() {
|
||||||
|
"CommonA"
|
||||||
|
}
|
||||||
|
|
||||||
|
// this can only be called from Foo (not from subclasses):
|
||||||
|
private fun privateInFoo() {
|
||||||
|
}
|
||||||
|
|
||||||
|
// this can be called from Foo and any subclass (including MI subclasses):
|
||||||
|
protected fun protectedInFoo() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parent B: also exposes a val and a var with the same name to test field inheritance and conflict rules
|
||||||
|
class Bar(val b) {
|
||||||
|
var tag = "B"
|
||||||
|
|
||||||
|
fun runB() {
|
||||||
|
"ResultB:" + b
|
||||||
|
}
|
||||||
|
|
||||||
|
fun common() {
|
||||||
|
"CommonB"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// With multiple inheritance, base constructors are called in the order of declaration,
|
||||||
|
// and each ancestor is initialized at most once (diamonds are de-duplicated):
|
||||||
|
class FooBar(a, b) : Foo(a), Bar(b) {
|
||||||
|
|
||||||
|
// Ambiguous method name "common" can be disambiguated:
|
||||||
|
fun commonFromFoo() {
|
||||||
|
// explicit qualification by ancestor type:
|
||||||
|
this@Foo.common()
|
||||||
|
// or by cast:
|
||||||
|
(this as Foo).common()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun commonFromBar() {
|
||||||
|
this@Bar.common()
|
||||||
|
(this as Bar).common()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accessing inherited fields (val/var) respects the same resolution rules:
|
||||||
|
fun tagFromFoo() { this@Foo.tag }
|
||||||
|
fun tagFromBar() { this@Bar.tag }
|
||||||
|
}
|
||||||
|
|
||||||
|
val fb = FooBar(1, 2)
|
||||||
|
|
||||||
|
// Methods with distinct names from different bases work:
|
||||||
|
assertEquals("ResultA:1", fb.runA())
|
||||||
|
assertEquals("ResultB:2", fb.runB())
|
||||||
|
|
||||||
|
// If we call an ambiguous method unqualified, the first in MRO (leftmost base) is used:
|
||||||
|
assertEquals("CommonA", fb.common())
|
||||||
|
|
||||||
|
// We can call a specific one via explicit qualification or cast:
|
||||||
|
assertEquals("CommonB", (fb as Bar).common())
|
||||||
|
assertEquals("CommonA", (fb as Foo).common())
|
||||||
|
|
||||||
|
// Or again via explicit casts (wrappers may be validated separately):
|
||||||
|
assertEquals("CommonB", (fb as Bar).common())
|
||||||
|
assertEquals("CommonA", (fb as Foo).common())
|
||||||
|
|
||||||
|
// Inheriting val/var:
|
||||||
|
// - Reading an ambiguous var/val selects the first along MRO (Foo.tag initially):
|
||||||
|
assertEquals("F", fb.tag)
|
||||||
|
// - Qualified access returns the chosen ancestor’s member:
|
||||||
|
assertEquals("F", (fb as Foo).tag)
|
||||||
|
assertEquals("B", (fb as Bar).tag)
|
||||||
|
|
||||||
|
// - Writing an ambiguous var writes to the same selected member (first in MRO):
|
||||||
|
fb.tag = "X"
|
||||||
|
assertEquals("X", fb.tag) // unqualified resolves to Foo.tag
|
||||||
|
assertEquals("X", (fb as Foo).tag) // Foo.tag updated
|
||||||
|
assertEquals("B", (fb as Bar).tag) // Bar.tag unchanged
|
||||||
|
|
||||||
|
// - Qualified write via cast updates the specific ancestor’s storage:
|
||||||
|
(fb as Bar).tag = "Y"
|
||||||
|
assertEquals("X", (fb as Foo).tag)
|
||||||
|
assertEquals("Y", (fb as Bar).tag)
|
||||||
|
|
||||||
|
// A simple single-inheritance subclass still works:
|
||||||
|
class Buzz : Bar(3)
|
||||||
|
val buzz = Buzz()
|
||||||
|
|
||||||
|
assertEquals("ResultB:3", buzz.runB())
|
||||||
|
|
||||||
|
// Optional cast returns null if cast is not possible; use safe-call with it:
|
||||||
|
assertEquals("ResultB:3", (buzz as? Bar)?.runB())
|
||||||
|
assertEquals(null, (buzz as? Foo)?.runA())
|
||||||
|
|
||||||
|
// Visibility (spec only):
|
||||||
|
// - Foo.privateInFoo() is accessible only inside Foo body; even FooBar cannot call it,
|
||||||
|
// including with this@Foo or casts. Attempting to do so must be a compile-time error.
|
||||||
|
// - Foo.protectedInFoo() is accessible inside Foo and any subclass bodies (including FooBar),
|
||||||
|
// but not from unrelated classes/instances.
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
}
|
||||||
336
lynglib/src/jvmTest/kotlin/BookAllocationProfileTest.kt
Normal file
336
lynglib/src/jvmTest/kotlin/BookAllocationProfileTest.kt
Normal file
@ -0,0 +1,336 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
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 runBooksOnce(): Unit = runBlocking {
|
||||||
|
// Mirror BookTest set
|
||||||
|
runDocTests("../docs/tutorial.md")
|
||||||
|
runDocTests("../docs/math.md")
|
||||||
|
runDocTests("../docs/advanced_topics.md")
|
||||||
|
runDocTests("../docs/OOP.md")
|
||||||
|
runDocTests("../docs/Real.md")
|
||||||
|
runDocTests("../docs/List.md")
|
||||||
|
runDocTests("../docs/Range.md")
|
||||||
|
runDocTests("../docs/Set.md")
|
||||||
|
runDocTests("../docs/Map.md")
|
||||||
|
runDocTests("../docs/Buffer.md")
|
||||||
|
// Samples folder, bookMode=true
|
||||||
|
for (bt in Files.list(Paths.get("../docs/samples")).toList()) {
|
||||||
|
if (bt.extension == "md") runDocTests(bt.toString(), bookMode = true)
|
||||||
|
}
|
||||||
|
runDocTests("../docs/declaring_arguments.md")
|
||||||
|
runDocTests("../docs/exceptions_handling.md")
|
||||||
|
runDocTests("../docs/time.md")
|
||||||
|
runDocTests("../docs/parallelism.md")
|
||||||
|
runDocTests("../docs/RingBuffer.md")
|
||||||
|
runDocTests("../docs/Iterable.md")
|
||||||
|
}
|
||||||
|
|
||||||
|
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) ---
|
||||||
|
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() }
|
||||||
|
}
|
||||||
156
lynglib/src/jvmTest/kotlin/CallArgPipelineABTest.kt
Normal file
156
lynglib/src/jvmTest/kotlin/CallArgPipelineABTest.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
|
||||||
|
|
||||||
|
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() }
|
||||||
|
}
|
||||||
137
lynglib/src/jvmTest/kotlin/IndexPicABTest.kt
Normal file
137
lynglib/src/jvmTest/kotlin/IndexPicABTest.kt
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
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() }
|
||||||
|
}
|
||||||
139
lynglib/src/jvmTest/kotlin/IndexWritePathABTest.kt
Normal file
139
lynglib/src/jvmTest/kotlin/IndexWritePathABTest.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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
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() }
|
||||||
|
}
|
||||||
76
lynglib/src/jvmTest/kotlin/PerfProfilesTest.kt
Normal file
76
lynglib/src/jvmTest/kotlin/PerfProfilesTest.kt
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
154
lynglib/src/jvmTest/kotlin/PicAdaptiveABTest.kt
Normal file
154
lynglib/src/jvmTest/kotlin/PicAdaptiveABTest.kt
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
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() }
|
||||||
|
}
|
||||||
132
lynglib/src/jvmTest/kotlin/PicMethodsOnlyAdaptiveABTest.kt
Normal file
132
lynglib/src/jvmTest/kotlin/PicMethodsOnlyAdaptiveABTest.kt
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
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() }
|
||||||
|
}
|
||||||
112
lynglib/src/jvmTest/kotlin/PrimitiveFastOpsABTest.kt
Normal file
112
lynglib/src/jvmTest/kotlin/PrimitiveFastOpsABTest.kt
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
171
lynglib/src/jvmTest/kotlin/RangeIterationBenchmarkTest.kt
Normal file
171
lynglib/src/jvmTest/kotlin/RangeIterationBenchmarkTest.kt
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
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() }
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user