added Multiple Inheritance

This commit is contained in:
sergeych committed 2025-11-17 00:28:07 +01:00
1 parent 882df67909
commit aeeec2d417
33 files changed
+3302 -130

No files matched your search

@@ -29,17 +29,23 @@ class ClosureScope(val callScope: Scope, val closureScope: Scope) :
// we captured, not to the caller's `this` (e.g., FlowBuilder).
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? {
// 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`
// 3) Symbols from the captured closure scope (its locals and parents)
// 4) Instance members of the caller's `this` (e.g., FlowBuilder.emit)
// 5) Fallback to the standard chain (this frame -> parent (callScope) -> class members)
// note using super, not callScope, as arguments are assigned by the constructor
// and are not yet exposed via callScope.get at this point:
super.objects[name]?.let { if (it.type.isArgument) return it }
// First, prefer locals/arguments bound in this frame
super.objects[name]?.let { return it }
// Prefer instance fields/methods declared on the captured receiver:
// First, resolve real instance fields stored in the instance scope (constructor vars like `coll`, `factor`)
@@ -479,6 +479,8 @@ class Compiler(
val callStatement = statement {
// and the source closure of the lambda which might have other thisObj.
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) {
// no args: automatic var 'it'
val l = args.list
@@ -700,6 +702,37 @@ class Compiler(
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(
left: ObjRef,
@@ -750,7 +783,18 @@ class Compiler(
}
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)
"null" -> ConstRef(ObjNull.asReadonly)
"true" -> ConstRef(ObjTrue.asReadonly)
@@ -818,6 +862,40 @@ class Compiler(
private suspend fun parseKeywordStatement(id: Token): Statement? = when (id.value) {
"val" -> parseVarDeclaration(false, 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()
"do" -> parseDoWhileStatement()
"for" -> parseForStatement()
@@ -1162,19 +1240,40 @@ class Compiler(
"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)
val t = cc.next()
pushInitScope()
val bodyInit: Statement? = if (t.type == Token.Type.LBRACE) {
// parse body
parseScript().also {
cc.skipTokens(Token.Type.RBRACE)
// Robust body detection: peek next non-whitespace token; if it's '{', consume and parse the body
val bodyInit: Statement? = run {
val saved = cc.savePos()
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()
@@ -1191,31 +1290,60 @@ class Compiler(
val constructorCode = statement {
// constructor code is registered with class instance and is called over
// new `thisObj` already set by class to ObjInstance.instanceContext
thisObj as ObjInstance
// the context now is a "class creation context", we must use its args to initialize
// fields. Note that 'this' is already set by class
constructorArgsDeclaration?.assignToContext(this)
bodyInit?.execute(this)
thisObj
val instance = thisObj as ObjInstance
// Constructor parameters have been assigned to instance scope by ObjClass.callOn before
// invoking parent/child constructors.
// IMPORTANT: do not execute class body here; class body was executed once in the class scope
// to register methods and prepare initializers. Instance constructor should be empty unless
// we later add explicit constructor body syntax.
instance
}
// inheritance must alter this code:
val newClass = ObjInstanceClass(className).apply {
instanceConstructor = constructorCode
constructorMeta = constructorArgsDeclaration
}
statement {
// the main statement should create custom ObjClass instance with field
// 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)
// 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()) {
val classScope = createChildScope(newThisObj = newClass)
newClass.classScope = classScope
for (s in initScope)
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
}
}
@@ -1686,6 +1814,7 @@ class Compiler(
}
fnStatements.execute(context)
}
val enclosingCtx = parentContext
val fnCreateStatement = statement(start) { context ->
// we added fn in the context. now we must save closure
// for the function, unless we're in the class scope:
@@ -1699,7 +1828,7 @@ class Compiler(
// class extension method
val type = context[typeName]?.value ?: context.raiseSymbolNotFound("class $typeName not found")
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
(thisObj as? ObjInstance)?.let { i ->
annotatedFnBody.execute(ClosureScope(this, i.instanceScope))
@@ -1709,7 +1838,31 @@ class Compiler(
}
}
// 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
// saved the proper context in the closure
annotatedFnBody
@@ -1791,9 +1944,18 @@ class Compiler(
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 ->
if (context.containsLocal(name))
throw ScriptError(nameToken.pos, "Variable $name is already defined")
// In true class bodies (not inside a function), store fields under a class-qualified key to support MI collisions
// 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
if (!isStatic) declareLocalName(name)
@@ -1818,11 +1980,32 @@ class Compiler(
// context.addItem(name, isMutable, it, visibility, recordType = ObjRecord.Type.Field)
// }
} else {
// init value could be a val; when we initialize by-value type var with it, we need to
// create a separate copy:
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
context.addItem(name, isMutable, initValue, visibility, recordType = ObjRecord.Type.Field)
initValue
if (declaringClassName != null && !isStatic) {
val storageName = "$declaringClassName::$name"
// If we are in class scope now (defining instance field), defer initialization to instance time
val isClassScope = context.thisObj is ObjClass && (context.thisObj !is ObjInstance)
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 ->
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 ->
ElvisRef(a, b)
@@ -342,6 +342,11 @@ private class Parser(fromPos: Pos) {
when (text) {
"in" -> Token("in", from, Token.Type.IN)
"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
@@ -0,0 +1,221 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.sergeych.lyng
import net.sergeych.lyng.PerfProfiles.restore
/**
* Helper to quickly apply groups of [PerfFlags] for different workload profiles and restore them back.
* JVM-first defaults; safe to use on all targets. Applying a preset returns a [Snapshot] that can be
* passed to [restore] to revert the change.
*/
object PerfProfiles {
data class Snapshot(
val LOCAL_SLOT_PIC: Boolean,
val EMIT_FAST_LOCAL_REFS: Boolean,
val ARG_BUILDER: Boolean,
val ARG_SMALL_ARITY_12: Boolean,
val SKIP_ARGS_ON_NULL_RECEIVER: Boolean,
val SCOPE_POOL: Boolean,
val FIELD_PIC: Boolean,
val METHOD_PIC: Boolean,
val FIELD_PIC_SIZE_4: Boolean,
val METHOD_PIC_SIZE_4: Boolean,
val PIC_ADAPTIVE_2_TO_4: Boolean,
val PIC_ADAPTIVE_METHODS_ONLY: Boolean,
val PIC_ADAPTIVE_HEURISTIC: Boolean,
val INDEX_PIC: Boolean,
val INDEX_PIC_SIZE_4: Boolean,
val PIC_DEBUG_COUNTERS: Boolean,
val PRIMITIVE_FASTOPS: Boolean,
val RVAL_FASTPATH: Boolean,
val REGEX_CACHE: Boolean,
val RANGE_FAST_ITER: Boolean,
)
fun snapshot(): Snapshot = Snapshot(
LOCAL_SLOT_PIC = PerfFlags.LOCAL_SLOT_PIC,
EMIT_FAST_LOCAL_REFS = PerfFlags.EMIT_FAST_LOCAL_REFS,
ARG_BUILDER = PerfFlags.ARG_BUILDER,
ARG_SMALL_ARITY_12 = PerfFlags.ARG_SMALL_ARITY_12,
SKIP_ARGS_ON_NULL_RECEIVER = PerfFlags.SKIP_ARGS_ON_NULL_RECEIVER,
SCOPE_POOL = PerfFlags.SCOPE_POOL,
FIELD_PIC = PerfFlags.FIELD_PIC,
METHOD_PIC = PerfFlags.METHOD_PIC,
FIELD_PIC_SIZE_4 = PerfFlags.FIELD_PIC_SIZE_4,
METHOD_PIC_SIZE_4 = PerfFlags.METHOD_PIC_SIZE_4,
PIC_ADAPTIVE_2_TO_4 = PerfFlags.PIC_ADAPTIVE_2_TO_4,
PIC_ADAPTIVE_METHODS_ONLY = PerfFlags.PIC_ADAPTIVE_METHODS_ONLY,
PIC_ADAPTIVE_HEURISTIC = PerfFlags.PIC_ADAPTIVE_HEURISTIC,
INDEX_PIC = PerfFlags.INDEX_PIC,
INDEX_PIC_SIZE_4 = PerfFlags.INDEX_PIC_SIZE_4,
PIC_DEBUG_COUNTERS = PerfFlags.PIC_DEBUG_COUNTERS,
PRIMITIVE_FASTOPS = PerfFlags.PRIMITIVE_FASTOPS,
RVAL_FASTPATH = PerfFlags.RVAL_FASTPATH,
REGEX_CACHE = PerfFlags.REGEX_CACHE,
RANGE_FAST_ITER = PerfFlags.RANGE_FAST_ITER,
)
fun restore(s: Snapshot) {
PerfFlags.LOCAL_SLOT_PIC = s.LOCAL_SLOT_PIC
PerfFlags.EMIT_FAST_LOCAL_REFS = s.EMIT_FAST_LOCAL_REFS
PerfFlags.ARG_BUILDER = s.ARG_BUILDER
PerfFlags.ARG_SMALL_ARITY_12 = s.ARG_SMALL_ARITY_12
PerfFlags.SKIP_ARGS_ON_NULL_RECEIVER = s.SKIP_ARGS_ON_NULL_RECEIVER
PerfFlags.SCOPE_POOL = s.SCOPE_POOL
PerfFlags.FIELD_PIC = s.FIELD_PIC
PerfFlags.METHOD_PIC = s.METHOD_PIC
PerfFlags.FIELD_PIC_SIZE_4 = s.FIELD_PIC_SIZE_4
PerfFlags.METHOD_PIC_SIZE_4 = s.METHOD_PIC_SIZE_4
PerfFlags.PIC_ADAPTIVE_2_TO_4 = s.PIC_ADAPTIVE_2_TO_4
PerfFlags.PIC_ADAPTIVE_METHODS_ONLY = s.PIC_ADAPTIVE_METHODS_ONLY
PerfFlags.PIC_ADAPTIVE_HEURISTIC = s.PIC_ADAPTIVE_HEURISTIC
PerfFlags.INDEX_PIC = s.INDEX_PIC
PerfFlags.INDEX_PIC_SIZE_4 = s.INDEX_PIC_SIZE_4
PerfFlags.PIC_DEBUG_COUNTERS = s.PIC_DEBUG_COUNTERS
PerfFlags.PRIMITIVE_FASTOPS = s.PRIMITIVE_FASTOPS
PerfFlags.RVAL_FASTPATH = s.RVAL_FASTPATH
PerfFlags.REGEX_CACHE = s.REGEX_CACHE
PerfFlags.RANGE_FAST_ITER = s.RANGE_FAST_ITER
}
enum class Preset { BASELINE, BENCH, BOOKS }
/** Apply a preset and return a [Snapshot] of the previous state to allow restoring later. */
fun apply(preset: Preset): Snapshot {
val saved = snapshot()
when (preset) {
Preset.BASELINE -> applyBaseline()
Preset.BENCH -> applyBench()
Preset.BOOKS -> applyBooks()
}
return saved
}
private fun applyBaseline() {
// Restore platform defaults. Note: INDEX_PIC follows FIELD_PIC parity by convention.
PerfFlags.LOCAL_SLOT_PIC = PerfDefaults.LOCAL_SLOT_PIC
PerfFlags.EMIT_FAST_LOCAL_REFS = PerfDefaults.EMIT_FAST_LOCAL_REFS
PerfFlags.ARG_BUILDER = PerfDefaults.ARG_BUILDER
PerfFlags.ARG_SMALL_ARITY_12 = PerfDefaults.ARG_SMALL_ARITY_12
PerfFlags.SKIP_ARGS_ON_NULL_RECEIVER = PerfDefaults.SKIP_ARGS_ON_NULL_RECEIVER
PerfFlags.SCOPE_POOL = PerfDefaults.SCOPE_POOL
PerfFlags.FIELD_PIC = PerfDefaults.FIELD_PIC
PerfFlags.METHOD_PIC = PerfDefaults.METHOD_PIC
PerfFlags.FIELD_PIC_SIZE_4 = PerfDefaults.FIELD_PIC_SIZE_4
PerfFlags.METHOD_PIC_SIZE_4 = PerfDefaults.METHOD_PIC_SIZE_4
PerfFlags.PIC_ADAPTIVE_2_TO_4 = PerfDefaults.PIC_ADAPTIVE_2_TO_4
PerfFlags.PIC_ADAPTIVE_METHODS_ONLY = PerfDefaults.PIC_ADAPTIVE_METHODS_ONLY
PerfFlags.PIC_ADAPTIVE_HEURISTIC = PerfDefaults.PIC_ADAPTIVE_HEURISTIC
PerfFlags.INDEX_PIC = PerfFlags.FIELD_PIC
PerfFlags.INDEX_PIC_SIZE_4 = PerfDefaults.INDEX_PIC_SIZE_4
PerfFlags.PIC_DEBUG_COUNTERS = PerfDefaults.PIC_DEBUG_COUNTERS
PerfFlags.PRIMITIVE_FASTOPS = PerfDefaults.PRIMITIVE_FASTOPS
PerfFlags.RVAL_FASTPATH = PerfDefaults.RVAL_FASTPATH
PerfFlags.REGEX_CACHE = PerfDefaults.REGEX_CACHE
PerfFlags.RANGE_FAST_ITER = PerfDefaults.RANGE_FAST_ITER
}
private fun applyBench() {
// Expression-heavy micro-bench focus (JVM-first assumptions)
PerfFlags.LOCAL_SLOT_PIC = true
PerfFlags.EMIT_FAST_LOCAL_REFS = true
PerfFlags.ARG_BUILDER = true
PerfFlags.ARG_SMALL_ARITY_12 = false
PerfFlags.SKIP_ARGS_ON_NULL_RECEIVER = true
PerfFlags.SCOPE_POOL = true
PerfFlags.FIELD_PIC = true
PerfFlags.METHOD_PIC = true
PerfFlags.FIELD_PIC_SIZE_4 = false
PerfFlags.METHOD_PIC_SIZE_4 = false
PerfFlags.PIC_ADAPTIVE_2_TO_4 = false
PerfFlags.PIC_ADAPTIVE_METHODS_ONLY = false
PerfFlags.PIC_ADAPTIVE_HEURISTIC = false
PerfFlags.INDEX_PIC = true
PerfFlags.INDEX_PIC_SIZE_4 = true
PerfFlags.PIC_DEBUG_COUNTERS = false
PerfFlags.PRIMITIVE_FASTOPS = true
PerfFlags.RVAL_FASTPATH = true
// Keep regex cache/platform setting; enable on JVM typically
PerfFlags.REGEX_CACHE = PerfDefaults.REGEX_CACHE
PerfFlags.RANGE_FAST_ITER = false
}
private fun applyBooks() {
// Documentation/book workload focus based on profiling observations.
// Favor simpler paths when hot expression paths are not dominant.
PerfFlags.LOCAL_SLOT_PIC = PerfDefaults.LOCAL_SLOT_PIC
PerfFlags.EMIT_FAST_LOCAL_REFS = PerfDefaults.EMIT_FAST_LOCAL_REFS
PerfFlags.ARG_BUILDER = false
PerfFlags.ARG_SMALL_ARITY_12 = false
PerfFlags.SKIP_ARGS_ON_NULL_RECEIVER = true
PerfFlags.SCOPE_POOL = false
PerfFlags.FIELD_PIC = false
PerfFlags.METHOD_PIC = false
PerfFlags.FIELD_PIC_SIZE_4 = false
PerfFlags.METHOD_PIC_SIZE_4 = false
PerfFlags.PIC_ADAPTIVE_2_TO_4 = false
PerfFlags.PIC_ADAPTIVE_METHODS_ONLY = false
PerfFlags.PIC_ADAPTIVE_HEURISTIC = false
PerfFlags.INDEX_PIC = false
PerfFlags.INDEX_PIC_SIZE_4 = false
PerfFlags.PIC_DEBUG_COUNTERS = false
PerfFlags.PRIMITIVE_FASTOPS = false
PerfFlags.RVAL_FASTPATH = false
// Keep regex cache/platform default; ON on JVM usually helps string APIs in books
PerfFlags.REGEX_CACHE = PerfDefaults.REGEX_CACHE
PerfFlags.RANGE_FAST_ITER = false
}
}
@@ -46,6 +46,8 @@ open class Scope(
var thisObj: Obj = ObjVoid,
var 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.
var frameId: Long = nextFrameId()
@@ -53,6 +55,12 @@ open class Scope(
// Enabled by default for child scopes; module/class scopes can ignore it.
private val slots: MutableList<ObjRecord> = mutableListOf()
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.
@@ -162,11 +170,15 @@ open class Scope(
open operator fun get(name: String): ObjRecord? =
if (name == "this") thisObj.asReadonly
else {
// Prefer direct locals/bindings declared in this frame
(objects[name]
// Then, check known local bindings in this frame (helps after suspension)
?: localBindings[name]
// Walk up ancestry
?: parent?.get(name)
?: thisObj.objClass
.getInstanceMemberOrNull(name)
)
// Finally, fallback to class members on thisObj
?: thisObj.objClass.getInstanceMemberOrNull(name)
)
}
// Slot fast-path API
@@ -199,6 +211,7 @@ open class Scope(
objects.clear()
slots.clear()
nameToSlot.clear()
localBindings.clear()
// Pre-size local slots for upcoming parameter assignment where possible
reserveLocalCapacity(args.list.size + 4)
}
@@ -258,6 +271,13 @@ open class Scope(
if( !it.isMutable )
raiseIllegalAssignment("symbol is readonly: $name")
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
} ?: addItem(name, true, value, visibility, recordType)
@@ -268,8 +288,23 @@ open class Scope(
visibility: Visibility = Visibility.Public,
recordType: ObjRecord.Type = ObjRecord.Type.Other
): ObjRecord {
val rec = ObjRecord(value, isMutable, visibility, type = recordType)
val rec = ObjRecord(value, isMutable, visibility, declaringClass = currentClassCtx, type = recordType)
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)
if (getSlotIndexOf(name) == null) {
allocateSlotFor(name, rec)
@@ -43,6 +43,8 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
SHL, SHR,
SINLGE_LINE_COMMENT, MULTILINE_COMMENT,
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,
ELLIPSIS, DOTDOT, DOTDOTLT,
NEWLINE,
@@ -22,4 +22,18 @@ enum class Visibility {
val isPublic by lazy { this == Public }
@Suppress("unused")
val isProtected by lazy { this == Protected }
}
/** MI-aware visibility check: whether [caller] can access a member declared in [decl] with [visibility]. */
fun canAccessMember(visibility: Visibility, decl: net.sergeych.lyng.obj.ObjClass?, caller: net.sergeych.lyng.obj.ObjClass?): Boolean {
return when (visibility) {
Visibility.Public -> true
Visibility.Private -> (decl != null && caller === decl)
Visibility.Protected -> when {
decl == null -> false
caller == null -> false
caller === decl -> true
else -> (caller.allParentsSet.contains(decl))
}
}
}
@@ -80,13 +80,26 @@ open class Obj {
args: Arguments = Arguments.EMPTY,
onNotFoundResult: (() -> Obj?)? = null
): Obj {
return objClass.getInstanceMemberOrNull(name)?.value?.invoke(
scope,
this,
args
)
?: onNotFoundResult?.invoke()
?: scope.raiseSymbolNotFound(name)
val rec = objClass.getInstanceMemberOrNull(name)
if (rec != null) {
val decl = rec.declaringClass ?: objClass.findDeclaringClassOf(name)
val caller = scope.currentClassCtx
if (!canAccessMember(rec.visibility, decl, caller))
scope.raiseError(ObjAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})"))
// 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(
@@ -258,7 +271,13 @@ open class Obj {
open suspend fun readField(scope: Scope, name: String): ObjRecord {
// 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) {
is Statement -> {
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) {
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")
}
@@ -40,6 +40,12 @@ open class ObjClass(
var constructorMeta: ArgsDeclaration? = 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.
*
@@ -50,10 +56,77 @@ open class ObjClass(
*/
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> =
parents.flatMap {
listOf(it) + it.allParentsSet
}.toMutableSet()
buildSet {
fun collect(c: ObjClass) {
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 }
@@ -73,9 +146,70 @@ open class ObjClass(
// remains stable even when call frames are pooled and reused.
val stableParent = scope.parent
instance.instanceScope = Scope(stableParent, scope.args, scope.pos, instance)
if (instanceConstructor != null) {
instanceConstructor!!.execute(instance.instanceScope)
// Expose instance methods (and other callable members) directly in the instance scope for fast lookup
// 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
}
@@ -91,10 +225,12 @@ open class ObjClass(
visibility: Visibility = Visibility.Public,
pos: Pos = Pos.builtIn
) {
val existing = members[name] ?: allParentsSet.firstNotNullOfOrNull { it.members[name] }
if (existing?.isMutable == false)
throw ScriptError(pos, "$name is already defined in $objClass or one of its supertypes")
members[name] = ObjRecord(initialValue, isMutable, visibility)
// Allow overriding ancestors: only prevent redefinition if THIS class already defines an immutable member
val existingInSelf = members[name]
if (existingInSelf != null && existingInSelf.isMutable == false)
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
layoutVersion += 1
}
@@ -120,8 +256,14 @@ open class ObjClass(
layoutVersion += 1
}
fun addFn(name: String, isOpen: Boolean = false, code: suspend Scope.() -> Obj) {
createField(name, statement { code() }, isOpen)
fun addFn(
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)
@@ -135,15 +277,46 @@ open class ObjClass(
* Get instance member traversing the hierarchy if needed. Its meaning is different for different objects.
*/
fun getInstanceMemberOrNull(name: String): ObjRecord? {
members[name]?.let { return it }
allParentsSet.forEach { parent -> parent.getInstanceMemberOrNull(name)?.let { return it } }
// Unified traversal in strict C3 order: self, then each ancestor, checking members before classScope
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]
}
/** 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 =
getInstanceMemberOrNull(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 {
classScope?.objects?.get(name)?.let {
if (it.visibility.isPublic) return it
@@ -19,6 +19,7 @@ package net.sergeych.lyng.obj
import net.sergeych.lyng.Arguments
import net.sergeych.lyng.Scope
import net.sergeych.lyng.canAccessMember
import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType
@@ -28,40 +29,124 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
internal lateinit var instanceScope: Scope
override suspend fun readField(scope: Scope, name: String): ObjRecord {
return instanceScope[name]?.let {
if (it.visibility.isPublic)
it
else
scope.raiseError(ObjAccessException(scope, "can't access non-public field $name"))
// Direct (unmangled) lookup first
instanceScope[name]?.let {
val decl = it.declaringClass ?: objClass.findDeclaringClassOf(name)
val caller = scope.currentClassCtx ?: instanceScope.currentClassCtx
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) {
// Direct (unmangled) first
instanceScope[name]?.let { f ->
if (!f.visibility.isPublic)
ObjIllegalAssignmentException(scope, "can't assign to non-public field $name")
val decl = f.declaringClass ?: objClass.findDeclaringClassOf(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.value.assign(scope, newValue) == null)
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,
onNotFoundResult: (()->Obj?)?): Obj =
instanceScope[name]?.let {
if (it.visibility.isPublic)
it.value.invoke(
instanceScope[name]?.let { rec ->
val decl = rec.declaringClass ?: objClass.findDeclaringClassOf(name)
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,
this,
args)
else
scope.raiseError(ObjAccessException(scope, "can't invoke non-public method $name"))
} finally {
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)
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 {
val fields = publicFields.map { "${it.key}=${it.value.value}" }.joinToString(",")
@@ -120,4 +205,117 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
}
return 0
}
}
/**
* A qualified view over an [ObjInstance] that resolves members starting from a specific ancestor class.
* It does not change identity; it only affects lookup precedence for fields and methods.
*/
class ObjQualifiedView(val instance: ObjInstance, private val startClass: ObjClass) : Obj() {
override val objClass: ObjClass get() = instance.objClass
private fun memberFromAncestor(name: String): ObjRecord? =
instance.objClass.getInstanceMemberFromAncestor(startClass, name)
override suspend fun readField(scope: Scope, name: String): ObjRecord {
// Qualified field access: prefer mangled storage for the qualified ancestor
val mangled = "${startClass.className}::$name"
instance.instanceScope.objects[mangled]?.let { rec ->
// Visibility: declaring class is the qualified ancestor for mangled storage
val decl = rec.declaringClass ?: startClass
val caller = scope.currentClassCtx
if (!net.sergeych.lyng.canAccessMember(rec.visibility, decl, caller))
scope.raiseError(ObjAccessException(scope, "can't access field $name (declared in ${decl.className})"))
return rec
}
// Then try instance locals (unmangled) only if startClass is the dynamic class itself
if (startClass === instance.objClass) {
instance.instanceScope[name]?.let { rec ->
val decl = rec.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
val caller = scope.currentClassCtx
if (!net.sergeych.lyng.canAccessMember(rec.visibility, decl, caller))
scope.raiseError(ObjAccessException(scope, "can't access field $name (declared in ${decl?.className ?: "?"})"))
return rec
}
}
// Finally try methods/properties starting from ancestor
val r = memberFromAncestor(name) ?: scope.raiseError("no such field: $name")
val decl = r.declaringClass ?: startClass
val caller = scope.currentClassCtx
if (!net.sergeych.lyng.canAccessMember(r.visibility, decl, caller))
scope.raiseError(ObjAccessException(scope, "can't access field $name (declared in ${decl.className})"))
return when (val value = r.value) {
is net.sergeych.lyng.Statement -> ObjRecord(value.execute(instance.instanceScope.createChildScope(scope.pos, newThisObj = instance)), r.isMutable)
else -> r
}
}
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
// Qualified write: target mangled storage for the ancestor
val mangled = "${startClass.className}::$name"
instance.instanceScope.objects[mangled]?.let { f ->
val decl = f.declaringClass ?: startClass
val caller = scope.currentClassCtx
if (!net.sergeych.lyng.canAccessMember(f.visibility, decl, caller))
ObjIllegalAssignmentException(scope, "can't assign to field $name (declared in ${decl.className})").raise()
if (!f.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
if (f.value.assign(scope, newValue) == null) f.value = newValue
return
}
// If start is dynamic class, allow unmangled
if (startClass === instance.objClass) {
instance.instanceScope[name]?.let { f ->
val decl = f.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
val caller = scope.currentClassCtx
if (!net.sergeych.lyng.canAccessMember(f.visibility, decl, caller))
ObjIllegalAssignmentException(scope, "can't assign to field $name (declared in ${decl?.className ?: "?"})").raise()
if (!f.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
if (f.value.assign(scope, newValue) == null) f.value = newValue
return
}
}
val r = memberFromAncestor(name) ?: scope.raiseError("no such field: $name")
val decl = r.declaringClass ?: startClass
val caller = scope.currentClassCtx
if (!net.sergeych.lyng.canAccessMember(r.visibility, decl, caller))
ObjIllegalAssignmentException(scope, "can't assign to field $name (declared in ${decl.className})").raise()
if (!r.isMutable) scope.raiseError("can't assign to read-only field: $name")
if (r.value.assign(scope, newValue) == null) r.value = newValue
}
override suspend fun invokeInstanceMethod(scope: Scope, name: String, args: Arguments, onNotFoundResult: (() -> Obj?)?): Obj {
// Qualified method dispatch must start from the specified ancestor, not from the instance scope.
memberFromAncestor(name)?.let { rec ->
val decl = rec.declaringClass ?: startClass
val caller = scope.currentClassCtx
if (!net.sergeych.lyng.canAccessMember(rec.visibility, decl, caller))
scope.raiseError(ObjAccessException(scope, "can't invoke method $name (declared in ${decl.className})"))
val saved = instance.instanceScope.currentClassCtx
instance.instanceScope.currentClassCtx = decl
try {
return rec.value.invoke(instance.instanceScope, instance, args)
} finally {
instance.instanceScope.currentClassCtx = saved
}
}
// If the qualifier is the dynamic class itself, allow instance-scope methods as a fallback
if (startClass === instance.objClass) {
instance.instanceScope[name]?.let { rec ->
val decl = rec.declaringClass ?: instance.objClass.findDeclaringClassOf(name)
val caller = scope.currentClassCtx
if (!net.sergeych.lyng.canAccessMember(rec.visibility, decl, caller))
scope.raiseError(ObjAccessException(scope, "can't invoke method $name (declared in ${decl?.className ?: "?"})"))
val saved = instance.instanceScope.currentClassCtx
instance.instanceScope.currentClassCtx = decl
try {
return rec.value.invoke(instance.instanceScope, instance, args)
} finally {
instance.instanceScope.currentClassCtx = saved
}
}
}
return onNotFoundResult?.invoke() ?: scope.raiseSymbolNotFound(name)
}
override fun toString(): String = instance.toString()
}
@@ -25,7 +25,7 @@ import net.sergeych.lynon.LynonType
/**
* Special variant of [ObjClass] to be used in [ObjInstance], e.g. for Lyng compiled classes
*/
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 {
val args = decoder.decodeAnyList(scope)
@@ -41,7 +41,6 @@ class ObjInstanceClass(val name: String) : ObjClass(name) {
init {
addFn("toString", true) {
println("-------------- tos! --------------")
ObjString(thisObj.toString())
}
}
@@ -33,6 +33,9 @@ class ObjMutex(val mutex: Mutex): Obj() {
}.apply {
addFn("withLock") {
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) }
}
}
@@ -27,6 +27,8 @@ data class ObjRecord(
var value: Obj,
val isMutable: Boolean,
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,
val isTransient: Boolean = false,
val type: Type = Type.Other
@@ -253,6 +253,49 @@ class BinaryOpRef(private val op: BinOp, private val left: ObjRef, private val r
}
}
/** Cast operator reference: left `as` rightType or `as?` (nullable). */
class CastRef(
private val valueRef: ObjRef,
private val typeRef: ObjRef,
private val isNullable: Boolean,
private val atPos: Pos,
) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord {
val v0 = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) valueRef.evalValue(scope) else valueRef.get(scope).value
val t = if (net.sergeych.lyng.PerfFlags.RVAL_FASTPATH) typeRef.evalValue(scope) else typeRef.get(scope).value
val target = (t as? ObjClass) ?: scope.raiseClassCastError("${'$'}t is not the class instance")
// unwrap qualified views
val v = when (v0) {
is ObjQualifiedView -> v0.instance
else -> v0
}
return if (v.isInstanceOf(target)) {
// For instances, return a qualified view to enforce ancestor-start dispatch
if (v is ObjInstance) ObjQualifiedView(v, target).asReadonly else v.asReadonly
} else {
if (isNullable) ObjNull.asReadonly else scope.raiseClassCastError(
"Cannot cast ${'$'}{(v as? Obj)?.objClass?.className ?: v::class.simpleName} to ${'$'}{target.className}"
)
}
}
}
/** Qualified `this@Type`: resolves to a view of current `this` starting dispatch from the ancestor Type. */
class QualifiedThisRef(private val typeName: String, private val atPos: Pos) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord {
val thisObj = scope.thisObj
val t = scope[typeName]?.value as? ObjClass
?: scope.raiseError("unknown type $typeName")
val inst = (thisObj as? ObjInstance)
?: scope.raiseClassCastError("this is not an instance")
if (!inst.objClass.allParentsSet.contains(t) && inst.objClass !== t)
scope.raiseClassCastError(
"Qualifier ${'$'}{t.className} is not an ancestor of ${'$'}{inst.objClass.className} (order: ${'$'}{inst.objClass.renderLinearization(true)})"
)
return ObjQualifiedView(inst, t).asReadonly
}
}
/** Assignment compound op: target op= value */
class AssignOpRef(
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 {
scope.pos = atPos
// 1) Try fast slot/local
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++
return scope.getSlotRecord(it)
return scope.getSlotRecord(it)
}
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 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)
}
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 {
scope.pos = atPos
if (!PerfFlags.LOCAL_SLOT_PIC) {
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 slot = if (hit) cachedSlot else resolveSlot(scope)
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) {
@@ -1209,10 +1277,18 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
rec.value = newValue
return
}
val stored = scope[name] ?: scope.raiseError("symbol not defined: '$name'")
if (stored.isMutable) stored.value = newValue
else scope.raiseError("Cannot assign to immutable value")
return
scope[name]?.let { stored ->
if (stored.isMutable) stored.value = newValue
else scope.raiseError("Cannot assign to immutable value")
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)
if (slot >= 0) {
@@ -1221,9 +1297,17 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
rec.value = newValue
return
}
val stored = scope[name] ?: scope.raiseError("symbol not defined: '$name'")
if (stored.isMutable) stored.value = newValue
else scope.raiseError("Cannot assign to immutable value")
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("symbol not defined: '$name'")
}
}
@@ -1298,11 +1382,32 @@ class FastLocalVarRef(
val ownerValid = isOwnerValidFor(scope)
val slot = if (ownerValid && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope)
val actualOwner = cachedOwnerScope
if (slot < 0 || actualOwner == null) scope.raiseError("local '$name' is not available in this scope")
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) {
if (ownerValid) net.sergeych.lyng.PerfStats.fastLocalHit++ else net.sergeych.lyng.PerfStats.fastLocalMiss++
if (slot >= 0 && actualOwner != null) {
if (net.sergeych.lyng.PerfFlags.PIC_DEBUG_COUNTERS) {
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 {
@@ -1310,8 +1415,26 @@ class FastLocalVarRef(
val ownerValid = isOwnerValidFor(scope)
val slot = if (ownerValid && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope)
val actualOwner = cachedOwnerScope
if (slot < 0 || actualOwner == null) scope.raiseError("local '$name' is not available in this scope")
return actualOwner.getSlotRecord(slot).value
if (slot >= 0 && actualOwner != null) 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) {
@@ -1319,10 +1442,37 @@ class FastLocalVarRef(
val owner = if (isOwnerValidFor(scope)) cachedOwnerScope else null
val slot = if (owner != null && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope)
val actualOwner = cachedOwnerScope
if (slot < 0 || actualOwner == null) scope.raiseError("local '$name' is not available in this scope")
val rec = actualOwner.getSlotRecord(slot)
if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value")
rec.value = newValue
if (slot >= 0 && actualOwner != null) {
val rec = actualOwner.getSlotRecord(slot)
if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value")
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
}
}
// (duplicate LocalVarRef removed; the canonical implementation is defined earlier in this 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()
)
}
}
@@ -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")
}
}
@@ -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()
)
}
}
@@ -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) }
}
}
@@ -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()
)
}
}
@@ -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())
}
}
@@ -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() }
}
@@ -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() }
}
@@ -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() }
}
@@ -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() }
}
@@ -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)
}
}
}
@@ -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() }
}
@@ -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() }
}
@@ -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
}
}
}
@@ -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() }
}