added Multiple Inheritance

This commit is contained in:
Sergey Chernov 2025-11-17 00:28:07 +01:00
parent 882df67909
commit aeeec2d417
33 changed files with 3302 additions and 130 deletions

28
CHANGELOG.md Normal file
View 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.

View File

@ -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

View File

@ -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:

View File

@ -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.

View File

@ -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`)

View File

@ -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)

View File

@ -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

View 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
}
}

View File

@ -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)

View File

@ -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,

View File

@ -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))
}
}
} }

View File

@ -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")
} }

View File

@ -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

View File

@ -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()
} }

View File

@ -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())
} }
} }

View File

@ -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) }
} }
} }

View File

@ -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

View File

@ -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)

View 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()
)
}
}

View 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")
}
}

View 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()
)
}
}

View 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) }
}
}

View 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()
)
}
}

View 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())
}
}

View 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() }
}

View 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() }
}

View 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() }
}

View 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() }
}

View 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)
}
}
}

View 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() }
}

View 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() }
}

View 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
}
}
}

View 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() }
}