1.2.0-SNAPSHOT started bug core refactor to support all new future and more strict rules (namely, fundamental difference between properties, functions and delegates)

This commit is contained in:
Sergey Chernov 2026-01-10 02:57:05 +01:00
parent 10b7cb2db2
commit b9831a422a
33 changed files with 1322 additions and 1078 deletions

1
.gitignore vendored
View File

@ -26,3 +26,4 @@ debug.log
/check_output.txt /check_output.txt
/compile_jvm_output.txt /compile_jvm_output.txt
/compile_metadata_output.txt /compile_metadata_output.txt
test_output*.txt

View File

@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
group = "net.sergeych" group = "net.sergeych"
version = "1.1.1-SNAPSHOT" version = "1.2.-SNAPSHOT"
// Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below // Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below

View File

@ -17,6 +17,8 @@
package net.sergeych.lyng package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjClass
import net.sergeych.lyng.obj.ObjRecord import net.sergeych.lyng.obj.ObjRecord
/** /**
@ -38,138 +40,34 @@ class ClosureScope(val callScope: Scope, val closureScope: Scope) :
} }
override fun get(name: String): ObjRecord? { override fun get(name: String): ObjRecord? {
// Fast-path built-ins
if (name == "this") return thisObj.asReadonly if (name == "this") return thisObj.asReadonly
// Priority: // 1. Current frame locals (parameters, local variables)
// 1) Locals and arguments declared in this lambda frame (including values defined before suspension) tryGetLocalRecord(this, name, currentClassCtx)?.let { return it }
// 2) Instance/class members of the captured receiver (`closureScope.thisObj`)
// 3) Symbols from the captured closure scope chain (locals/parents), ignoring nested ClosureScope overrides
// 4) Instance members of the caller's `this` (e.g., FlowBuilder.emit)
// 5) Symbols from the caller chain (locals/parents), ignoring nested ClosureScope overrides
// 6) Special fallback for module pseudo-symbols (e.g., __PACKAGE__)
// 1) Locals/arguments in this closure frame // 2. Lexical environment (captured locals from entire ancestry)
super.objects[name]?.let { return it }
super.localBindings[name]?.let { return it }
// 1a) Priority: if we are in a class context, prefer our own private members to support
// non-virtual private dispatch. This prevents a subclass from accidentally
// capturing a private member call from a base class.
// We only return non-field/non-property members (methods) here; fields must
// be resolved via instance storage in priority 2.
currentClassCtx?.let { ctx ->
ctx.members[name]?.let { rec ->
if (rec.visibility == Visibility.Private &&
rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Field &&
rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Property) return rec
}
}
// 1b) Captured locals from the entire closure ancestry. This ensures that parameters
// and local variables shadow members of captured receivers, matching standard
// lexical scoping rules.
closureScope.chainLookupIgnoreClosure(name, followClosure = true)?.let { return it } closureScope.chainLookupIgnoreClosure(name, followClosure = true)?.let { return it }
// 2) Members on the captured receiver instance // 3. Lexical this members (captured receiver)
(closureScope.thisObj as? net.sergeych.lyng.obj.ObjInstance)?.let { inst -> val receiver = thisObj
// Check direct locals in instance scope (unmangled) val effectiveClass = receiver as? ObjClass ?: receiver.objClass
inst.instanceScope.objects[name]?.let { rec -> for (cls in effectiveClass.mro) {
if (rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Property && val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) return rec if (rec != null && !rec.isAbstract) {
} if (canAccessMember(rec.visibility, rec.declaringClass ?: cls, currentClassCtx)) {
// Check mangled names for fields along MRO return rec.copy(receiver = receiver)
for (cls in inst.objClass.mro) {
inst.instanceScope.objects["${cls.className}::$name"]?.let { rec ->
if (rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Property &&
canAccessMember(rec.visibility, rec.declaringClass ?: cls, currentClassCtx)) return rec
} }
} }
} }
// Finally, root object fallback
findExtension(closureScope.thisObj.objClass, name)?.let { return it } Obj.rootObjectType.members[name]?.let { rec ->
closureScope.thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec ->
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) { if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) {
// Return only non-field/non-property members (methods) from class-level records. return rec.copy(receiver = receiver)
// Fields and properties must be resolved via instance storage (mangled) or readField.
if (rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Field &&
rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Property &&
!rec.isAbstract) return rec
} }
} }
// 3) Closure scope chain (locals/parents + members), ignore ClosureScope overrides to prevent recursion // 4. Call environment (caller locals, caller this, and global fallback)
closureScope.chainLookupWithMembers(name, currentClassCtx, followClosure = true)?.let { return it } return callScope.get(name)
// 4) Caller `this` members
(callScope.thisObj as? net.sergeych.lyng.obj.ObjInstance)?.let { inst ->
// Check direct locals in instance scope (unmangled)
inst.instanceScope.objects[name]?.let { rec ->
if (rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Property &&
canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) return rec
}
// Check mangled names for fields along MRO
for (cls in inst.objClass.mro) {
inst.instanceScope.objects["${cls.className}::$name"]?.let { rec ->
if (rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Property &&
canAccessMember(rec.visibility, rec.declaringClass ?: cls, currentClassCtx)) return rec
}
}
}
findExtension(callScope.thisObj.objClass, name)?.let { return it }
callScope.thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec ->
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) {
if (rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Field &&
rec.type != net.sergeych.lyng.obj.ObjRecord.Type.Property &&
!rec.isAbstract) return rec
}
}
// 5) Caller chain (locals/parents + members)
callScope.chainLookupWithMembers(name, currentClassCtx)?.let { return it }
// 6) Module pseudo-symbols (e.g., __PACKAGE__) — walk caller ancestry and query ModuleScope directly
if (name.startsWith("__")) {
var s: Scope? = callScope
val visited = HashSet<Long>(4)
while (s != null) {
if (!visited.add(s.frameId)) break
if (s is ModuleScope) return s.get(name)
s = s.parent
}
}
// 7) Direct module/global fallback: try to locate nearest ModuleScope and check its own locals
fun lookupInModuleAncestry(from: Scope): ObjRecord? {
var s: Scope? = from
val visited = HashSet<Long>(4)
while (s != null) {
if (!visited.add(s.frameId)) break
if (s is ModuleScope) {
s.objects[name]?.let { return it }
s.localBindings[name]?.let { return it }
// check immediate parent (root scope) locals/constants for globals like `delay`
val p = s.parent
if (p != null) {
p.objects[name]?.let { return it }
p.localBindings[name]?.let { return it }
p.thisObj.objClass.getInstanceMemberOrNull(name)?.let { return it }
}
return null
}
s = s.parent
}
return null
}
lookupInModuleAncestry(closureScope)?.let { return it }
lookupInModuleAncestry(callScope)?.let { return it }
// 8) Global root scope constants/functions (e.g., delay, yield) via current import provider
runCatching { this.currentImportProvider.rootScope.objects[name] }.getOrNull()?.let { return it }
// Final safe fallback: base scope lookup from this frame walking raw parents
return baseGetIgnoreClosure(name)
} }
} }

View File

@ -667,19 +667,25 @@ class Compiler(
"lambda must have either valid arguments declaration with '->' or no arguments" "lambda must have either valid arguments declaration with '->' or no arguments"
) )
val paramNames = argsDeclaration?.params?.map { it.name } ?: emptyList()
label?.let { cc.labels.add(it) } label?.let { cc.labels.add(it) }
val body = parseBlock(skipLeadingBrace = true) val body = inCodeContext(CodeContext.Function("<lambda>")) {
withLocalNames(paramNames.toSet()) {
parseBlock(skipLeadingBrace = true)
}
}
label?.let { cc.labels.remove(it) } label?.let { cc.labels.remove(it) }
return ValueFnRef { closureScope -> return ValueFnRef { closureScope ->
statement { statement(body.pos) { scope ->
// and the source closure of the lambda which might have other thisObj. // and the source closure of the lambda which might have other thisObj.
val context = this.applyClosure(closureScope) val context = scope.applyClosure(closureScope)
// Execute lambda body in a closure-aware context. Blocks inside the lambda // Execute lambda body in a closure-aware context. Blocks inside the lambda
// will create child scopes as usual, so re-declarations inside loops work. // will create child scopes as usual, so re-declarations inside loops work.
if (argsDeclaration == null) { if (argsDeclaration == null) {
// no args: automatic var 'it' // no args: automatic var 'it'
val l = args.list val l = scope.args.list
val itValue: Obj = when (l.size) { val itValue: Obj = when (l.size) {
// no args: it == void // no args: it == void
0 -> ObjVoid 0 -> ObjVoid
@ -2192,20 +2198,6 @@ class Compiler(
for (s in initScope) for (s in initScope)
s.execute(classScope) s.execute(classScope)
} }
// Fallback: ensure any functions declared in class scope are also present as instance methods
// (defensive in case some paths skipped cls.addFn during parsing/execution ordering)
for ((k, rec) in classScope.objects) {
val v = rec.value
if (v is Statement) {
if (newClass.members[k] == null) {
newClass.addFn(k, isMutable = true, pos = rec.importedFrom?.pos ?: nameToken.pos) {
(thisObj as? ObjInstance)?.let { i ->
v.execute(ClosureScope(this, i.instanceScope))
} ?: v.execute(thisObj.autoInstanceScope(this))
}
}
}
}
newClass.checkAbstractSatisfaction(nameToken.pos) newClass.checkAbstractSatisfaction(nameToken.pos)
// Debug summary: list registered instance methods and class-scope functions for this class // Debug summary: list registered instance methods and class-scope functions for this class
newClass newClass
@ -2292,7 +2284,7 @@ class Compiler(
} else if (sourceObj.isInstanceOf(ObjIterable)) { } else if (sourceObj.isInstanceOf(ObjIterable)) {
loopIterable(forContext, sourceObj, loopSO, body, elseStatement, label, canBreak) loopIterable(forContext, sourceObj, loopSO, body, elseStatement, label, canBreak)
} else { } else {
val size = runCatching { sourceObj.invokeInstanceMethod(forContext, "size").toInt() } val size = runCatching { sourceObj.readField(forContext, "size").value.toInt() }
.getOrElse { .getOrElse {
throw ScriptError( throw ScriptError(
tOp.pos, tOp.pos,
@ -3079,31 +3071,88 @@ class Compiler(
val mark = cc.savePos() val mark = cc.savePos()
cc.restorePos(markBeforeEq) cc.restorePos(markBeforeEq)
cc.skipWsTokens() cc.skipWsTokens()
val next = cc.peekNextNonWhitespace()
if (next.isId("get") || next.isId("set") || next.isId("private") || next.isId("protected")) { // Heuristic: if we see 'get(' or 'set(' or 'private set(' or 'protected set(',
// look ahead for a body.
fun hasAccessorWithBody(): Boolean {
val t = cc.peekNextNonWhitespace()
if (t.isId("get") || t.isId("set")) {
val saved = cc.savePos()
cc.next() // consume get/set
val nextToken = cc.peekNextNonWhitespace()
if (nextToken.type == Token.Type.LPAREN) {
cc.next() // consume (
var depth = 1
while (cc.hasNext() && depth > 0) {
val tt = cc.next()
if (tt.type == Token.Type.LPAREN) depth++
else if (tt.type == Token.Type.RPAREN) depth--
}
val next = cc.peekNextNonWhitespace()
if (next.type == Token.Type.LBRACE || next.type == Token.Type.ASSIGN) {
cc.restorePos(saved)
return true
}
} else if (nextToken.type == Token.Type.LBRACE || nextToken.type == Token.Type.ASSIGN) {
cc.restorePos(saved)
return true
}
cc.restorePos(saved)
} else if (t.isId("private") || t.isId("protected")) {
val saved = cc.savePos()
cc.next() // consume modifier
if (cc.skipWsTokens().isId("set")) {
cc.next() // consume set
val nextToken = cc.peekNextNonWhitespace()
if (nextToken.type == Token.Type.LPAREN) {
cc.next() // consume (
var depth = 1
while (cc.hasNext() && depth > 0) {
val tt = cc.next()
if (tt.type == Token.Type.LPAREN) depth++
else if (tt.type == Token.Type.RPAREN) depth--
}
val next = cc.peekNextNonWhitespace()
if (next.type == Token.Type.LBRACE || next.type == Token.Type.ASSIGN) {
cc.restorePos(saved)
return true
}
} else if (nextToken.type == Token.Type.LBRACE || nextToken.type == Token.Type.ASSIGN) {
cc.restorePos(saved)
return true
}
}
cc.restorePos(saved)
}
return false
}
if (hasAccessorWithBody()) {
isProperty = true isProperty = true
cc.restorePos(markBeforeEq) cc.restorePos(markBeforeEq)
cc.skipWsTokens() // Do not consume eqToken if it's an accessor keyword
} else { } else {
cc.restorePos(mark) cc.restorePos(mark)
} }
} }
val effectiveEqToken = if (isProperty) null else eqToken
// Register the local name at compile time so that subsequent identifiers can be emitted as fast locals // Register the local name at compile time so that subsequent identifiers can be emitted as fast locals
if (!isStatic) declareLocalName(name) if (!isStatic) declareLocalName(name)
val isDelegate = if (isAbstract || actualExtern) { val isDelegate = if (isAbstract || actualExtern) {
if (!isProperty && (eqToken.type == Token.Type.ASSIGN || eqToken.type == Token.Type.BY)) if (!isProperty && (effectiveEqToken?.type == Token.Type.ASSIGN || effectiveEqToken?.type == Token.Type.BY))
throw ScriptError(eqToken.pos, "${if (isAbstract) "abstract" else "extern"} variable $name cannot have an initializer or delegate") throw ScriptError(effectiveEqToken.pos, "${if (isAbstract) "abstract" else "extern"} variable $name cannot have an initializer or delegate")
// Abstract or extern variables don't have initializers // Abstract or extern variables don't have initializers
cc.restorePos(markBeforeEq) cc.restorePos(markBeforeEq)
cc.skipWsTokens() cc.skipWsTokens()
setNull = true setNull = true
false false
} else if (!isProperty && eqToken.type == Token.Type.BY) { } else if (!isProperty && effectiveEqToken?.type == Token.Type.BY) {
true true
} else { } else {
if (!isProperty && eqToken.type != Token.Type.ASSIGN) { if (!isProperty && effectiveEqToken?.type != Token.Type.ASSIGN) {
if (!isMutable && (declaringClassNameCaptured == null) && (extTypeName == null)) if (!isMutable && (declaringClassNameCaptured == null) && (extTypeName == null))
throw ScriptError(start, "val must be initialized") throw ScriptError(start, "val must be initialized")
else if (!isMutable && declaringClassNameCaptured != null && extTypeName == null) { else if (!isMutable && declaringClassNameCaptured != null && extTypeName == null) {
@ -3123,12 +3172,12 @@ class Compiler(
val initialExpression = if (setNull || isProperty) null val initialExpression = if (setNull || isProperty) null
else parseStatement(true) else parseStatement(true)
?: throw ScriptError(eqToken.pos, "Expected initializer expression") ?: throw ScriptError(effectiveEqToken!!.pos, "Expected initializer expression")
// Emit MiniValDecl for this declaration (before execution wiring), attach doc if any // Emit MiniValDecl for this declaration (before execution wiring), attach doc if any
run { run {
val declRange = MiniRange(pendingDeclStart ?: start, cc.currentPos()) val declRange = MiniRange(pendingDeclStart ?: start, cc.currentPos())
val initR = if (setNull || isProperty) null else MiniRange(eqToken.pos, cc.currentPos()) val initR = if (setNull || isProperty) null else MiniRange(effectiveEqToken!!.pos, cc.currentPos())
val node = MiniValDecl( val node = MiniValDecl(
range = declRange, range = declRange,
name = name, name = name,
@ -3193,17 +3242,24 @@ class Compiler(
if (t.isId("get")) { if (t.isId("get")) {
val getStart = cc.currentPos() val getStart = cc.currentPos()
cc.next() // consume 'get' cc.next() // consume 'get'
cc.requireToken(Token.Type.LPAREN) if (cc.peekNextNonWhitespace().type == Token.Type.LPAREN) {
cc.requireToken(Token.Type.RPAREN) cc.next() // consume (
cc.requireToken(Token.Type.RPAREN)
}
miniSink?.onEnterFunction(null) miniSink?.onEnterFunction(null)
getter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) { getter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
cc.skipWsTokens() cc.skipWsTokens()
parseBlock() inCodeContext(CodeContext.Function("<getter>")) {
parseBlock()
}
} else if (cc.peekNextNonWhitespace().type == Token.Type.ASSIGN) { } else if (cc.peekNextNonWhitespace().type == Token.Type.ASSIGN) {
cc.skipWsTokens() cc.skipWsTokens()
cc.next() // consume '=' cc.next() // consume '='
val expr = parseExpression() ?: throw ScriptError(cc.current().pos, "Expected getter expression") inCodeContext(CodeContext.Function("<getter>")) {
expr val expr = parseExpression()
?: throw ScriptError(cc.current().pos, "Expected getter expression")
expr
}
} else { } else {
throw ScriptError(cc.current().pos, "Expected { or = after get()") throw ScriptError(cc.current().pos, "Expected { or = after get()")
} }
@ -3211,26 +3267,34 @@ class Compiler(
} else if (t.isId("set")) { } else if (t.isId("set")) {
val setStart = cc.currentPos() val setStart = cc.currentPos()
cc.next() // consume 'set' cc.next() // consume 'set'
cc.requireToken(Token.Type.LPAREN) var setArgName = "it"
val setArg = cc.requireToken(Token.Type.ID, "Expected setter argument name") if (cc.peekNextNonWhitespace().type == Token.Type.LPAREN) {
cc.requireToken(Token.Type.RPAREN) cc.next() // consume (
setArgName = cc.requireToken(Token.Type.ID, "Expected setter argument name").value
cc.requireToken(Token.Type.RPAREN)
}
miniSink?.onEnterFunction(null) miniSink?.onEnterFunction(null)
setter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) { setter = if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
cc.skipWsTokens() cc.skipWsTokens()
val body = parseBlock() val body = inCodeContext(CodeContext.Function("<setter>")) {
parseBlock()
}
statement(body.pos) { scope -> statement(body.pos) { scope ->
val value = scope.args.list.firstOrNull() ?: ObjNull val value = scope.args.list.firstOrNull() ?: ObjNull
scope.addItem(setArg.value, true, value, recordType = ObjRecord.Type.Argument) scope.addItem(setArgName, true, value, recordType = ObjRecord.Type.Argument)
body.execute(scope) body.execute(scope)
} }
} else if (cc.peekNextNonWhitespace().type == Token.Type.ASSIGN) { } else if (cc.peekNextNonWhitespace().type == Token.Type.ASSIGN) {
cc.skipWsTokens() cc.skipWsTokens()
cc.next() // consume '=' cc.next() // consume '='
val expr = parseExpression() ?: throw ScriptError(cc.current().pos, "Expected setter expression") val expr = inCodeContext(CodeContext.Function("<setter>")) {
parseExpression()
?: throw ScriptError(cc.current().pos, "Expected setter expression")
}
val st = expr val st = expr
statement(st.pos) { scope -> statement(st.pos) { scope ->
val value = scope.args.list.firstOrNull() ?: ObjNull val value = scope.args.list.firstOrNull() ?: ObjNull
scope.addItem(setArg.value, true, value, recordType = ObjRecord.Type.Argument) scope.addItem(setArgName, true, value, recordType = ObjRecord.Type.Argument)
st.execute(scope) st.execute(scope)
} }
} else { } else {
@ -3274,6 +3338,8 @@ class Compiler(
throw ScriptError(cc.current().pos, "Expected { or = after set(...)") throw ScriptError(cc.current().pos, "Expected { or = after set(...)")
} }
miniSink?.onExitFunction(cc.currentPos()) miniSink?.onExitFunction(cc.currentPos())
} else {
// private set without body: setter remains null, visibility is restricted
} }
} else { } else {
cc.restorePos(mark) cc.restorePos(mark)
@ -3525,7 +3591,7 @@ class Compiler(
} else { } else {
// Not in class body: regular local/var declaration // Not in class body: regular local/var declaration
val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull val initValue = initialExpression?.execute(context)?.byValueCopy() ?: ObjNull
context.addItem(name, isMutable, initValue, visibility, recordType = ObjRecord.Type.Field) context.addItem(name, isMutable, initValue, visibility, recordType = ObjRecord.Type.Other)
initValue initValue
} }
} }

View File

@ -106,7 +106,7 @@ open class Scope(
* intertwined closure frames. They traverse the plain parent chain and consult only locals * intertwined closure frames. They traverse the plain parent chain and consult only locals
* and bindings of each frame. Instance/class member fallback must be decided by the caller. * and bindings of each frame. Instance/class member fallback must be decided by the caller.
*/ */
private fun tryGetLocalRecord(s: Scope, name: String, caller: net.sergeych.lyng.obj.ObjClass?): ObjRecord? { internal fun tryGetLocalRecord(s: Scope, name: String, caller: net.sergeych.lyng.obj.ObjClass?): ObjRecord? {
s.objects[name]?.let { rec -> s.objects[name]?.let { rec ->
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec
} }
@ -330,28 +330,38 @@ open class Scope(
internal val objects = mutableMapOf<String, ObjRecord>() internal val objects = mutableMapOf<String, ObjRecord>()
open operator fun get(name: String): ObjRecord? = open operator fun get(name: String): ObjRecord? {
if (name == "this") thisObj.asReadonly if (name == "this") return thisObj.asReadonly
else {
// Prefer direct locals/bindings declared in this frame // 1. Prefer direct locals/bindings declared in this frame
(objects[name]?.let { rec -> objects[name]?.let { rec ->
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) rec else null if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) return rec
}
// Then, check known local bindings in this frame (helps after suspension)
?: localBindings[name]?.let { rec ->
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) rec else null
}
// Walk up ancestry
?: parent?.get(name)
// Finally, fallback to class members on thisObj
?: thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec ->
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) {
if (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property || rec.isAbstract) null
else rec
} else null
}
)
} }
localBindings[name]?.let { rec ->
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) return rec
}
// 2. Then, check members of thisObj
val receiver = thisObj
val effectiveClass = receiver as? ObjClass ?: receiver.objClass
for (cls in effectiveClass.mro) {
val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
if (rec != null && !rec.isAbstract) {
if (canAccessMember(rec.visibility, rec.declaringClass ?: cls, currentClassCtx)) {
return rec.copy(receiver = receiver)
}
}
}
// Finally, root object fallback
Obj.rootObjectType.members[name]?.let { rec ->
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) {
return rec.copy(receiver = receiver)
}
}
// 3. Finally, walk up ancestry
return parent?.get(name)
}
// Slot fast-path API // Slot fast-path API
fun getSlotRecord(index: Int): ObjRecord = slots[index] fun getSlotRecord(index: Int): ObjRecord = slots[index]
@ -380,6 +390,7 @@ open class Scope(
// that could interact badly with the new parent and produce a cycle. // that could interact badly with the new parent and produce a cycle.
this.parent = null this.parent = null
this.skipScopeCreation = false this.skipScopeCreation = false
this.currentClassCtx = parent?.currentClassCtx
// fresh identity for PIC caches // fresh identity for PIC caches
this.frameId = nextFrameId() this.frameId = nextFrameId()
// clear locals and slot maps // clear locals and slot maps
@ -388,6 +399,7 @@ open class Scope(
nameToSlot.clear() nameToSlot.clear()
localBindings.clear() localBindings.clear()
extensions.clear() extensions.clear()
this.currentClassCtx = parent?.currentClassCtx
// Now safe to validate and re-parent // Now safe to validate and re-parent
ensureNoCycle(parent) ensureNoCycle(parent)
this.parent = parent this.parent = parent
@ -628,46 +640,34 @@ open class Scope(
} }
suspend fun resolve(rec: ObjRecord, name: String): Obj { suspend fun resolve(rec: ObjRecord, name: String): Obj {
if (rec.type == ObjRecord.Type.Delegated) { val receiver = rec.receiver ?: thisObj
val del = rec.delegate ?: run { return receiver.resolveRecord(this, rec, name, rec.declaringClass).value
if (thisObj is ObjInstance) {
val res = (thisObj as ObjInstance).resolveRecord(this, rec, name, rec.declaringClass).value
rec.value = res
return res
}
raiseError("Internal error: delegated property $name has no delegate")
}
val th = if (thisObj === ObjVoid) ObjNull else thisObj
val res = del.invokeInstanceMethod(this, "getValue", Arguments(th, ObjString(name)), onNotFoundResult = {
// If getValue not found, return a wrapper that calls invoke
object : Statement() {
override val pos: Pos = Pos.builtIn
override suspend fun execute(scope: Scope): Obj {
val th2 = if (scope.thisObj === ObjVoid) ObjNull else scope.thisObj
val allArgs = (listOf(th2, ObjString(name)) + scope.args.list).toTypedArray()
return del.invokeInstanceMethod(scope, "invoke", Arguments(*allArgs))
}
}
})
rec.value = res
return res
}
return rec.value
} }
suspend fun assign(rec: ObjRecord, name: String, newValue: Obj) { suspend fun assign(rec: ObjRecord, name: String, newValue: Obj) {
if (rec.type == ObjRecord.Type.Delegated) { if (rec.type == ObjRecord.Type.Delegated) {
val receiver = rec.receiver ?: thisObj
val del = rec.delegate ?: run { val del = rec.delegate ?: run {
if (thisObj is ObjInstance) { if (receiver is ObjInstance) {
(thisObj as ObjInstance).writeField(this, name, newValue) (receiver as ObjInstance).writeField(this, name, newValue)
return return
} }
raiseError("Internal error: delegated property $name has no delegate") raiseError("Internal error: delegated property $name has no delegate")
} }
val th = if (thisObj === ObjVoid) ObjNull else thisObj val th = if (receiver === ObjVoid) ObjNull else receiver
del.invokeInstanceMethod(this, "setValue", Arguments(th, ObjString(name), newValue)) del.invokeInstanceMethod(this, "setValue", Arguments(th, ObjString(name), newValue))
return return
} }
if (rec.value is ObjProperty) {
(rec.value as ObjProperty).callSetter(this, rec.receiver ?: thisObj, newValue, rec.declaringClass)
return
}
// If it's a member (explicitly tracked by receiver or declaringClass), use writeField.
// Important: locals have receiver == null and declaringClass == null (enforced in addItem).
if (rec.receiver != null || (rec.declaringClass != null && (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property))) {
(rec.receiver ?: thisObj).writeField(this, name, newValue)
return
}
if (!rec.isMutable && rec.value !== ObjUnset) raiseIllegalAssignment("can't reassign val $name") if (!rec.isMutable && rec.value !== ObjUnset) raiseIllegalAssignment("can't reassign val $name")
rec.value = newValue rec.value = newValue
} }

View File

@ -293,10 +293,14 @@ class Script(
ObjVoid ObjVoid
} }
// Delay in milliseconds (plain numeric). For time-aware variants use lyng.time.Duration API.
addVoidFn("delay") { addVoidFn("delay") {
val ms = (this.args.firstAndOnly().toDouble()).roundToLong() val a = args.firstAndOnly()
delay(ms) when (a) {
is ObjInt -> delay(a.value)
is ObjReal -> delay((a.value * 1000).roundToLong())
is ObjDuration -> delay(a.duration)
else -> raiseIllegalArgument("Expected Int, Real or Duration, got ${a.inspect(this)}")
}
} }
addConst("Object", rootObjectType) addConst("Object", rootObjectType)

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -145,6 +145,23 @@ fun ObjClass.addClassFnDoc(
} }
} }
fun ObjClass.addPropertyDoc(
name: String,
doc: String,
type: TypeDoc? = null,
visibility: Visibility = Visibility.Public,
moduleName: String? = null,
getter: (suspend Scope.() -> Obj)? = null,
setter: (suspend Scope.(Obj) -> Unit)? = null
) {
addProperty(name, getter, setter, visibility)
BuiltinDocRegistry.module(moduleName ?: ownerModuleNameFromClassOrUnknown()) {
classDoc(this@addPropertyDoc.className, doc = "") {
field(name = name, doc = doc, type = type, mutable = setter != null)
}
}
}
fun ObjClass.addClassConstDoc( fun ObjClass.addClassConstDoc(
name: String, name: String,
value: Obj, value: Obj,

View File

@ -93,23 +93,45 @@ open class Obj {
args: Arguments = Arguments.EMPTY, args: Arguments = Arguments.EMPTY,
onNotFoundResult: (suspend () -> Obj?)? = null onNotFoundResult: (suspend () -> Obj?)? = null
): Obj { ): Obj {
// 0. Prefer private member of current class context
scope.currentClassCtx?.let { caller ->
caller.members[name]?.let { rec ->
if (rec.visibility == Visibility.Private && !rec.isAbstract) {
if (rec.type == ObjRecord.Type.Property) {
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, caller)
} else if (rec.type != ObjRecord.Type.Delegated) {
return rec.value.invoke(scope, this, args, caller)
}
}
}
}
// 1. Hierarchy members (excluding root fallback) // 1. Hierarchy members (excluding root fallback)
for (cls in objClass.mro) { for (cls in objClass.mro) {
if (cls.className == "Obj") break if (cls.className == "Obj") break
val rec = cls.members[name] ?: cls.classScope?.objects?.get(name) val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
if (rec != null && !rec.isAbstract && rec.type != ObjRecord.Type.Property) { if (rec != null && !rec.isAbstract) {
val decl = rec.declaringClass ?: cls val decl = rec.declaringClass ?: cls
val caller = scope.currentClassCtx val caller = scope.currentClassCtx
if (!canAccessMember(rec.visibility, decl, caller)) if (!canAccessMember(rec.visibility, decl, caller))
scope.raiseError(ObjIllegalAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})")) scope.raiseError(ObjIllegalAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})"))
return rec.value.invoke(scope, this, args, decl)
if (rec.type == ObjRecord.Type.Property) {
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
} else if (rec.type != ObjRecord.Type.Delegated) {
return rec.value.invoke(scope, this, args, decl)
}
} }
} }
// 2. Extensions in scope // 2. Extensions in scope
val extension = scope.findExtension(objClass, name) val extension = scope.findExtension(objClass, name)
if (extension != null) { if (extension != null) {
return extension.value.invoke(scope, this, args) if (extension.type == ObjRecord.Type.Property) {
if (args.isEmpty()) return (extension.value as ObjProperty).callGetter(scope, this, extension.declaringClass)
} else if (extension.type != ObjRecord.Type.Delegated) {
return extension.value.invoke(scope, this, args)
}
} }
// 3. Root object fallback // 3. Root object fallback
@ -120,7 +142,12 @@ open class Obj {
val caller = scope.currentClassCtx val caller = scope.currentClassCtx
if (!canAccessMember(rec.visibility, decl, caller)) if (!canAccessMember(rec.visibility, decl, caller))
scope.raiseError(ObjIllegalAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})")) scope.raiseError(ObjIllegalAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})"))
return rec.value.invoke(scope, this, args, decl)
if (rec.type == ObjRecord.Type.Property) {
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
} else if (rec.type != ObjRecord.Type.Delegated) {
return rec.value.invoke(scope, this, args, decl)
}
} }
} }
} }
@ -415,29 +442,52 @@ open class Obj {
// suspend fun <T> sync(block: () -> T): T = monitor.withLock { block() } // suspend fun <T> sync(block: () -> T): T = monitor.withLock { block() }
open suspend fun readField(scope: Scope, name: String): ObjRecord { open suspend fun readField(scope: Scope, name: String): ObjRecord {
// 0. Prefer private member of current class context
scope.currentClassCtx?.let { caller ->
caller.members[name]?.let { rec ->
if (rec.visibility == Visibility.Private && !rec.isAbstract) {
val resolved = resolveRecord(scope, rec, name, caller)
if (resolved.type == ObjRecord.Type.Fun && resolved.value is Statement)
return resolved.copy(value = resolved.value.invoke(scope, this, Arguments.EMPTY, caller))
return resolved
}
}
}
// 1. Hierarchy members (excluding root fallback) // 1. Hierarchy members (excluding root fallback)
for (cls in objClass.mro) { for (cls in objClass.mro) {
if (cls.className == "Obj") break if (cls.className == "Obj") break
val rec = cls.members[name] ?: cls.classScope?.objects?.get(name) val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
if (rec != null) { if (rec != null && !rec.isAbstract) {
if (!rec.isAbstract) { val decl = rec.declaringClass ?: cls
return resolveRecord(scope, rec, name, rec.declaringClass) val resolved = resolveRecord(scope, rec, name, decl)
} if (resolved.type == ObjRecord.Type.Fun && resolved.value is Statement)
return resolved.copy(value = resolved.value.invoke(scope, this, Arguments.EMPTY, decl))
return resolved
} }
} }
// 2. Extensions // 2. Extensions
val extension = scope.findExtension(objClass, name) val extension = scope.findExtension(objClass, name)
if (extension != null) { if (extension != null) {
return resolveRecord(scope, extension, name, extension.declaringClass) val resolved = resolveRecord(scope, extension, name, extension.declaringClass)
if (resolved.type == ObjRecord.Type.Fun && resolved.value is Statement)
return resolved.copy(value = resolved.value.invoke(scope, this, Arguments.EMPTY, extension.declaringClass))
return resolved
} }
// 3. Root fallback // 3. Root fallback
for (cls in objClass.mro) { for (cls in objClass.mro) {
if (cls.className == "Obj") { if (cls.className == "Obj") {
cls.members[name]?.let { cls.members[name]?.let { rec ->
val decl = it.declaringClass ?: cls val decl = rec.declaringClass ?: cls
return resolveRecord(scope, it, name, decl) val caller = scope.currentClassCtx
if (!canAccessMember(rec.visibility, decl, caller))
scope.raiseError(ObjIllegalAccessException(scope, "can't access field ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})"))
val resolved = resolveRecord(scope, rec, name, decl)
if (resolved.type == ObjRecord.Type.Fun && resolved.value is Statement)
return resolved.copy(value = resolved.value.invoke(scope, this, Arguments.EMPTY, decl))
return resolved
} }
} }
} }
@ -450,17 +500,29 @@ open class Obj {
open suspend fun resolveRecord(scope: Scope, obj: ObjRecord, name: String, decl: ObjClass?): ObjRecord { open suspend fun resolveRecord(scope: Scope, obj: ObjRecord, name: String, decl: ObjClass?): ObjRecord {
if (obj.type == ObjRecord.Type.Delegated) { if (obj.type == ObjRecord.Type.Delegated) {
val del = obj.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate") val del = obj.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate")
val th = if (this === ObjVoid) ObjNull else this
val res = del.invokeInstanceMethod(scope, "getValue", Arguments(th, ObjString(name)), onNotFoundResult = {
// If getValue not found, return a wrapper that calls invoke
object : Statement() {
override val pos: Pos = Pos.builtIn
override suspend fun execute(s: Scope): Obj {
val th2 = if (s.thisObj === ObjVoid) ObjNull else s.thisObj
val allArgs = (listOf(th2, ObjString(name)) + s.args.list).toTypedArray()
return del.invokeInstanceMethod(s, "invoke", Arguments(*allArgs))
}
}
})
return obj.copy( return obj.copy(
value = del.invokeInstanceMethod(scope, "getValue", Arguments(this, ObjString(name))), value = res,
type = ObjRecord.Type.Other type = ObjRecord.Type.Other
) )
} }
val value = obj.value val value = obj.value
if (value is ObjProperty) { if (value is ObjProperty || obj.type == ObjRecord.Type.Property) {
return ObjRecord(value.callGetter(scope, this, decl), obj.isMutable) val prop = if (value is ObjProperty) value else (value as? Statement)?.execute(scope.createChildScope(scope.pos, newThisObj = this)) as? ObjProperty
} ?: scope.raiseError("Expected ObjProperty for property member $name, got ${value::class}")
if (value is Statement && decl != null) { val res = prop.callGetter(scope, this, decl)
return ObjRecord(value.execute(scope.createChildScope(scope.pos, newThisObj = this)), obj.isMutable) return ObjRecord(res, obj.isMutable)
} }
val caller = scope.currentClassCtx val caller = scope.currentClassCtx
// Check visibility for non-property members here if they weren't checked before // Check visibility for non-property members here if they weren't checked before
@ -472,13 +534,24 @@ open class Obj {
open suspend fun writeField(scope: Scope, name: String, newValue: Obj) { open suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
willMutate(scope) willMutate(scope)
var field: ObjRecord? = null var field: ObjRecord? = null
// 0. Prefer private member of current class context
scope.currentClassCtx?.let { caller ->
caller.members[name]?.let { rec ->
if (rec.visibility == Visibility.Private && !rec.isAbstract) {
field = rec
}
}
}
// 1. Hierarchy members (excluding root fallback) // 1. Hierarchy members (excluding root fallback)
for (cls in objClass.mro) { if (field == null) {
if (cls.className == "Obj") break for (cls in objClass.mro) {
val rec = cls.members[name] ?: cls.classScope?.objects?.get(name) if (cls.className == "Obj") break
if (rec != null && !rec.isAbstract) { val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
field = rec if (rec != null && !rec.isAbstract) {
break field = rec
break
}
} }
} }
// 2. Extensions // 2. Extensions
@ -512,12 +585,19 @@ open class Obj {
} }
open suspend fun getAt(scope: Scope, index: Obj): Obj { open suspend fun getAt(scope: Scope, index: Obj): Obj {
if (index is ObjString) {
return readField(scope, index.value).value
}
scope.raiseNotImplemented("indexing") scope.raiseNotImplemented("indexing")
} }
suspend fun getAt(scope: Scope, index: Int): Obj = getAt(scope, ObjInt(index.toLong())) suspend fun getAt(scope: Scope, index: Int): Obj = getAt(scope, ObjInt(index.toLong()))
open suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) { open suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) {
if (index is ObjString) {
writeField(scope, index.value, newValue)
return
}
scope.raiseNotImplemented("indexing") scope.raiseNotImplemented("indexing")
} }
@ -683,9 +763,7 @@ open class Obj {
} }
@Suppress("NOTHING_TO_INLINE") fun from(obj: Any?): Obj {
inline fun from(obj: Any?): Obj {
@Suppress("UNCHECKED_CAST")
return when (obj) { return when (obj) {
is Obj -> obj is Obj -> obj
is Double -> ObjReal(obj) is Double -> ObjReal(obj)
@ -694,16 +772,16 @@ open class Obj {
is Long -> ObjInt.of(obj) is Long -> ObjInt.of(obj)
is String -> ObjString(obj) is String -> ObjString(obj)
is CharSequence -> ObjString(obj.toString()) is CharSequence -> ObjString(obj.toString())
is Char -> ObjChar(obj)
is Boolean -> ObjBool(obj) is Boolean -> ObjBool(obj)
is Set<*> -> ObjSet((obj as Set<Obj>).toMutableSet()) is Set<*> -> ObjSet(obj.map { from(it) }.toMutableSet())
is List<*> -> ObjList(obj.map { from(it) }.toMutableList())
is Map<*, *> -> ObjMap(obj.entries.associate { from(it.key) to from(it.value) }.toMutableMap())
is Map.Entry<*, *> -> ObjMapEntry(from(obj.key), from(obj.value))
is Enum<*> -> ObjString(obj.name)
Unit -> ObjVoid Unit -> ObjVoid
null -> ObjNull null -> ObjNull
is Iterator<*> -> ObjKotlinIterator(obj) is Iterator<*> -> ObjKotlinIterator(obj as Iterator<Any?>)
is Map.Entry<*, *> -> {
obj as MutableMap.MutableEntry<Obj, Obj>
ObjMapEntry(obj.key, obj.value)
}
else -> throw IllegalArgumentException("cannot convert to Obj: $obj") else -> throw IllegalArgumentException("cannot convert to Obj: $obj")
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,10 +17,7 @@
package net.sergeych.lyng.obj package net.sergeych.lyng.obj
import net.sergeych.lyng.miniast.ParamDoc import net.sergeych.lyng.miniast.*
import net.sergeych.lyng.miniast.TypeGenericDoc
import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.type
val ObjArray by lazy { val ObjArray by lazy {
@ -52,32 +49,35 @@ val ObjArray by lazy {
ObjFalse ObjFalse
} }
addFnDoc( addPropertyDoc(
name = "last", name = "last",
doc = "The last element of this array.", doc = "The last element of this array.",
returns = type("lyng.Any"), type = type("lyng.Any"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { getter = {
thisObj.invokeInstanceMethod( this.thisObj.invokeInstanceMethod(
this, this,
"getAt", "getAt",
(thisObj.invokeInstanceMethod(this, "size").toInt() - 1).toObj() (this.thisObj.invokeInstanceMethod(this, "size").toInt() - 1).toObj()
) )
} }
)
addFnDoc( addPropertyDoc(
name = "lastIndex", name = "lastIndex",
doc = "Index of the last element (size - 1).", doc = "Index of the last element (size - 1).",
returns = type("lyng.Int"), type = type("lyng.Int"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { (thisObj.invokeInstanceMethod(this, "size").toInt() - 1).toObj() } getter = { (this.thisObj.invokeInstanceMethod(this, "size").toInt() - 1).toObj() }
)
addFnDoc( addPropertyDoc(
name = "indices", name = "indices",
doc = "Range of valid indices for this array.", doc = "Range of valid indices for this array.",
returns = type("lyng.Range"), type = type("lyng.Range"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { ObjRange(0.toObj(), thisObj.invokeInstanceMethod(this, "size"), false) } getter = { ObjRange(0.toObj(), this.thisObj.invokeInstanceMethod(this, "size"), false) }
)
addFnDoc( addFnDoc(
name = "binarySearch", name = "binarySearch",

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -19,6 +19,8 @@ package net.sergeych.lyng.obj
import net.sergeych.bintools.toDump import net.sergeych.bintools.toDump
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.miniast.addPropertyDoc
import net.sergeych.lyng.miniast.type
import net.sergeych.lynon.BitArray import net.sergeych.lynon.BitArray
class ObjBitBuffer(val bitArray: BitArray) : Obj() { class ObjBitBuffer(val bitArray: BitArray) : Obj() {
@ -43,12 +45,20 @@ class ObjBitBuffer(val bitArray: BitArray) : Obj() {
thisAs<ObjBitBuffer>().bitArray.asUByteArray().toDump() thisAs<ObjBitBuffer>().bitArray.asUByteArray().toDump()
) )
} }
addFn("size") { addPropertyDoc(
thisAs<ObjBitBuffer>().bitArray.size.toObj() name = "size",
} doc = "Size of the bit buffer in bits.",
addFn("sizeInBytes") { type = type("lyng.Int"),
ObjInt((thisAs<ObjBitBuffer>().bitArray.size + 7) shr 3) moduleName = "lyng.stdlib",
} getter = { thisAs<ObjBitBuffer>().bitArray.size.toObj() }
)
addPropertyDoc(
name = "sizeInBytes",
doc = "Size of the bit buffer in full bytes (rounded up).",
type = type("lyng.Int"),
moduleName = "lyng.stdlib",
getter = { ObjInt((thisAs<ObjBitBuffer>().bitArray.size + 7) shr 3) }
)
} }
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -23,7 +23,8 @@ import net.sergeych.bintools.decodeHex
import net.sergeych.bintools.encodeToHex import net.sergeych.bintools.encodeToHex
import net.sergeych.bintools.toDump import net.sergeych.bintools.toDump
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.statement import net.sergeych.lyng.miniast.addPropertyDoc
import net.sergeych.lyng.miniast.type
import net.sergeych.lynon.BitArray import net.sergeych.lynon.BitArray
import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder import net.sergeych.lynon.LynonEncoder
@ -174,20 +175,26 @@ open class ObjBuffer(val byteArray: UByteArray) : Obj() {
addClassFn("decodeHex") { addClassFn("decodeHex") {
ObjBuffer(requireOnlyArg<Obj>().toString().decodeHex().asUByteArray()) ObjBuffer(requireOnlyArg<Obj>().toString().decodeHex().asUByteArray())
} }
createField("size", addPropertyDoc(
statement { name = "size",
(thisObj as ObjBuffer).byteArray.size.toObj() doc = "Number of bytes in this buffer.",
} type = type("lyng.Int"),
moduleName = "lyng.stdlib",
getter = { (this.thisObj as ObjBuffer).byteArray.size.toObj() }
) )
createField("hex", addPropertyDoc(
statement { name = "hex",
thisAs<ObjBuffer>().hex.toObj() doc = "Hexadecimal string representation of the buffer.",
} type = type("lyng.String"),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjBuffer>().hex.toObj() }
) )
createField("base64", addPropertyDoc(
statement { name = "base64",
thisAs<ObjBuffer>().base64.toObj() doc = "Base64 (URL-safe) string representation of the buffer.",
} type = type("lyng.String"),
moduleName = "lyng.stdlib",
getter = { thisAs<ObjBuffer>().base64.toObj() }
) )
addFn("decodeUtf8") { addFn("decodeUtf8") {
ObjString( ObjString(

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -18,7 +18,7 @@
package net.sergeych.lyng.obj package net.sergeych.lyng.obj
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.miniast.addFnDoc import net.sergeych.lyng.miniast.addPropertyDoc
import net.sergeych.lyng.miniast.type import net.sergeych.lyng.miniast.type
class ObjChar(val value: Char): Obj() { class ObjChar(val value: Char): Obj() {
@ -47,12 +47,13 @@ class ObjChar(val value: Char): Obj() {
companion object { companion object {
val type = ObjClass("Char").apply { val type = ObjClass("Char").apply {
addFnDoc( addPropertyDoc(
name = "code", name = "code",
doc = "Unicode code point (UTF-16 code unit) of this character.", doc = "Unicode code point (UTF-16 code unit) of this character.",
returns = type("lyng.Int"), type = type("lyng.Int"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { ObjInt(thisAs<ObjChar>().value.code.toLong()) } getter = { ObjInt((this.thisObj as ObjChar).value.code.toLong()) }
)
addFn("isDigit") { addFn("isDigit") {
thisAs<ObjChar>().value.isDigit().toObj() thisAs<ObjChar>().value.isDigit().toObj()
} }

View File

@ -18,10 +18,7 @@
package net.sergeych.lyng.obj package net.sergeych.lyng.obj
import net.sergeych.lyng.* import net.sergeych.lyng.*
import net.sergeych.lyng.miniast.ParamDoc import net.sergeych.lyng.miniast.*
import net.sergeych.lyng.miniast.TypeGenericDoc
import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.type
import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonType import net.sergeych.lynon.LynonType
@ -30,47 +27,56 @@ private object ClassIdGen { var c: Long = 1L; fun nextId(): Long = c++ }
val ObjClassType by lazy { val ObjClassType by lazy {
ObjClass("Class").apply { ObjClass("Class").apply {
addProperty("className", getter = { thisAs<ObjClass>().classNameObj }) addPropertyDoc(
addFnDoc( name = "className",
doc = "Full name of this class including package if available.",
type = type("lyng.String"),
moduleName = "lyng.stdlib",
getter = { (this.thisObj as ObjClass).classNameObj }
)
addPropertyDoc(
name = "name", name = "name",
doc = "Simple name of this class (without package).", doc = "Simple name of this class (without package).",
returns = type("lyng.String"), type = type("lyng.String"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { thisAs<ObjClass>().classNameObj } getter = { (this.thisObj as ObjClass).classNameObj }
)
addFnDoc( addPropertyDoc(
name = "fields", name = "fields",
doc = "Declared instance fields of this class and its ancestors (C3 order), without duplicates.", doc = "Declared instance fields of this class and its ancestors (C3 order), without duplicates.",
returns = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.String"))), type = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.String"))),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { getter = {
val cls = thisAs<ObjClass>() val cls = this.thisObj as ObjClass
val seen = hashSetOf<String>() val seen = hashSetOf<String>()
val names = mutableListOf<Obj>() val names = mutableListOf<Obj>()
for (c in cls.mro) { for (c in cls.mro) {
for ((n, rec) in c.members) { for ((n, rec) in c.members) {
if (rec.value !is Statement && seen.add(n)) names += ObjString(n) if (rec.value !is Statement && seen.add(n)) names += ObjString(n)
}
} }
ObjList(names.toMutableList())
} }
ObjList(names.toMutableList()) )
}
addFnDoc( addPropertyDoc(
name = "methods", name = "methods",
doc = "Declared instance methods of this class and its ancestors (C3 order), without duplicates.", doc = "Declared instance methods of this class and its ancestors (C3 order), without duplicates.",
returns = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.String"))), type = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.String"))),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { getter = {
val cls = thisAs<ObjClass>() val cls = this.thisObj as ObjClass
val seen = hashSetOf<String>() val seen = hashSetOf<String>()
val names = mutableListOf<Obj>() val names = mutableListOf<Obj>()
for (c in cls.mro) { for (c in cls.mro) {
for ((n, rec) in c.members) { for ((n, rec) in c.members) {
if (rec.value is Statement && seen.add(n)) names += ObjString(n) if (rec.value is Statement && seen.add(n)) names += ObjString(n)
}
} }
ObjList(names.toMutableList())
} }
ObjList(names.toMutableList()) )
}
addFnDoc( addFnDoc(
name = "get", name = "get",
@ -247,21 +253,29 @@ open class ObjClass(
// remains stable even when call frames are pooled and reused. // remains stable even when call frames are pooled and reused.
val stableParent = classScope ?: scope.parent val stableParent = classScope ?: scope.parent
instance.instanceScope = Scope(stableParent, scope.args, scope.pos, instance) instance.instanceScope = Scope(stableParent, scope.args, scope.pos, instance)
// println("[DEBUG_LOG] createInstance: created $instance scope@${instance.instanceScope.hashCode()}")
instance.instanceScope.currentClassCtx = null instance.instanceScope.currentClassCtx = null
// Expose instance methods (and other callable members) directly in the instance scope for fast lookup // 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 // This mirrors Obj.autoInstanceScope behavior for ad-hoc scopes and makes fb.method() resolution robust
// 1) members-defined methods
for ((k, v) in members) { for (cls in mro) {
if (v.value is Statement || v.type == ObjRecord.Type.Delegated) { // 1) members-defined methods
instance.instanceScope.objects[k] = if (v.type == ObjRecord.Type.Delegated) v.copy() else v for ((k, v) in cls.members) {
if (v.value is Statement || v.type == ObjRecord.Type.Delegated) {
val key = if (v.visibility == Visibility.Private) "${cls.className}::$k" else k
if (!instance.instanceScope.objects.containsKey(key)) {
instance.instanceScope.objects[key] = if (v.type == ObjRecord.Type.Delegated) v.copy() else v
}
}
} }
} // 2) class-scope methods registered during class-body execution
// 2) class-scope methods registered during class-body execution cls.classScope?.objects?.forEach { (k, rec) ->
classScope?.objects?.forEach { (k, rec) -> if (rec.value is Statement || rec.type == ObjRecord.Type.Delegated) {
if (rec.value is Statement || rec.type == ObjRecord.Type.Delegated) { val key = if (rec.visibility == Visibility.Private) "${cls.className}::$k" else k
// if not already present, copy reference for dispatch // if not already present, copy reference for dispatch
if (!instance.instanceScope.objects.containsKey(k)) { if (!instance.instanceScope.objects.containsKey(key)) {
instance.instanceScope.objects[k] = if (rec.type == ObjRecord.Type.Delegated) rec.copy() else rec instance.instanceScope.objects[key] = if (rec.type == ObjRecord.Type.Delegated) rec.copy() else rec
}
} }
} }
} }
@ -300,7 +314,13 @@ open class ObjClass(
c.constructorMeta?.let { meta -> c.constructorMeta?.let { meta ->
val argsHere = argsForThis ?: Arguments.EMPTY val argsHere = argsForThis ?: Arguments.EMPTY
// Assign constructor params into instance scope (unmangled) // Assign constructor params into instance scope (unmangled)
meta.assignToContext(instance.instanceScope, argsHere, declaringClass = c) val savedCtx = instance.instanceScope.currentClassCtx
instance.instanceScope.currentClassCtx = c
try {
meta.assignToContext(instance.instanceScope, argsHere, declaringClass = c)
} finally {
instance.instanceScope.currentClassCtx = savedCtx
}
// Also expose them under MI-mangled storage keys `${Class}::name` so qualified views can access them // Also expose them under MI-mangled storage keys `${Class}::name` so qualified views can access them
// and so that base-class casts like `(obj as Base).field` work. // and so that base-class casts like `(obj as Base).field` work.
for (p in meta.params) { for (p in meta.params) {
@ -329,7 +349,13 @@ open class ObjClass(
// parameters even if they were shadowed/overwritten by parent class initialization. // parameters even if they were shadowed/overwritten by parent class initialization.
c.constructorMeta?.let { meta -> c.constructorMeta?.let { meta ->
val argsHere = argsForThis ?: Arguments.EMPTY val argsHere = argsForThis ?: Arguments.EMPTY
meta.assignToContext(instance.instanceScope, argsHere, declaringClass = c) val savedCtx = instance.instanceScope.currentClassCtx
instance.instanceScope.currentClassCtx = c
try {
meta.assignToContext(instance.instanceScope, argsHere, declaringClass = c)
} finally {
instance.instanceScope.currentClassCtx = savedCtx
}
// Re-sync mangled names to point to the fresh records to keep them consistent // Re-sync mangled names to point to the fresh records to keep them consistent
for (p in meta.params) { for (p in meta.params) {
val rec = instance.instanceScope.objects[p.name] val rec = instance.instanceScope.objects[p.name]
@ -382,7 +408,8 @@ open class ObjClass(
): ObjRecord { ): ObjRecord {
// Validation of override rules: only for non-system declarations // Validation of override rules: only for non-system declarations
if (pos != Pos.builtIn) { if (pos != Pos.builtIn) {
val existing = getInstanceMemberOrNull(name) // Only consider TRUE instance members from ancestors for overrides
val existing = getInstanceMemberOrNull(name, includeAbstract = true, includeStatic = false)
var actualOverride = false var actualOverride = false
if (existing != null && existing.declaringClass != this) { if (existing != null && existing.declaringClass != this) {
// If the existing member is private in the ancestor, it's not visible for overriding. // If the existing member is private in the ancestor, it's not visible for overriding.
@ -505,14 +532,21 @@ open class ObjClass(
/** /**
* Get instance member traversing the hierarchy if needed. Its meaning is different for different objects. * Get instance member traversing the hierarchy if needed. Its meaning is different for different objects.
*/ */
fun getInstanceMemberOrNull(name: String): ObjRecord? { fun getInstanceMemberOrNull(name: String, includeAbstract: Boolean = false, includeStatic: Boolean = true): ObjRecord? {
// Unified traversal in strict C3 order: self, then each ancestor, checking members before classScope // Unified traversal in strict C3 order: self, then each ancestor, checking members before classScope
for (cls in mro) { for (cls in mro) {
cls.members[name]?.let { return it } cls.members[name]?.let {
cls.classScope?.objects?.get(name)?.let { return it } if (includeAbstract || !it.isAbstract) return it
}
if (includeStatic) {
cls.classScope?.objects?.get(name)?.let {
if (includeAbstract || !it.isAbstract) return it
}
}
} }
// Finally, allow root object fallback (rare; mostly built-ins like toString) // Finally, allow root object fallback (rare; mostly built-ins like toString)
return rootObjectType.members[name] val rootRec = rootObjectType.members[name]
return if (rootRec != null && (includeAbstract || !rootRec.isAbstract)) rootRec else null
} }
/** Find the declaring class where a member with [name] is defined, starting from this class along MRO. */ /** Find the declaring class where a member with [name] is defined, starting from this class along MRO. */

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -21,5 +21,6 @@ package net.sergeych.lyng.obj
* Collection is an iterator with `size` * Collection is an iterator with `size`
*/ */
val ObjCollection = ObjClass("Collection", ObjIterable).apply { val ObjCollection = ObjClass("Collection", ObjIterable).apply {
addProperty("size", isAbstract = true)
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -20,6 +20,7 @@ package net.sergeych.lyng.obj
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.miniast.addFnDoc import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.addPropertyDoc
import net.sergeych.lyng.miniast.type import net.sergeych.lyng.miniast.type
open class ObjDeferred(val deferred: Deferred<Obj>): Obj() { open class ObjDeferred(val deferred: Deferred<Obj>): Obj() {
@ -38,28 +39,30 @@ open class ObjDeferred(val deferred: Deferred<Obj>): Obj() {
returns = type("lyng.Any"), returns = type("lyng.Any"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib"
) { thisAs<ObjDeferred>().deferred.await() } ) { thisAs<ObjDeferred>().deferred.await() }
addFnDoc( addPropertyDoc(
name = "isCompleted", name = "isCompleted",
doc = "Whether this deferred has completed (successfully or with an error).", doc = "Whether this deferred has completed (successfully or with an error).",
returns = type("lyng.Bool"), type = type("lyng.Bool"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { thisAs<ObjDeferred>().deferred.isCompleted.toObj() } getter = { thisAs<ObjDeferred>().deferred.isCompleted.toObj() }
addFnDoc( )
addPropertyDoc(
name = "isActive", name = "isActive",
doc = "Whether this deferred is currently active (not completed and not cancelled).", doc = "Whether this deferred is currently active (not completed and not cancelled).",
returns = type("lyng.Bool"), type = type("lyng.Bool"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { getter = {
val d = thisAs<ObjDeferred>().deferred val d = thisAs<ObjDeferred>().deferred
(d.isActive || (!d.isCompleted && !d.isCancelled)).toObj() (d.isActive || (!d.isCompleted && !d.isCancelled)).toObj()
} }
addFnDoc( )
addPropertyDoc(
name = "isCancelled", name = "isCancelled",
doc = "Whether this deferred was cancelled.", doc = "Whether this deferred was cancelled.",
returns = type("lyng.Bool"), type = type("lyng.Bool"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { thisAs<ObjDeferred>().deferred.isCancelled.toObj() } getter = { thisAs<ObjDeferred>().deferred.isCancelled.toObj() }
)
} }
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -18,7 +18,7 @@
package net.sergeych.lyng.obj package net.sergeych.lyng.obj
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.miniast.addFnDoc import net.sergeych.lyng.miniast.addPropertyDoc
import net.sergeych.lyng.miniast.type import net.sergeych.lyng.miniast.type
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.days
@ -74,169 +74,195 @@ class ObjDuration(val duration: Duration) : Obj() {
) )
} }
}.apply { }.apply {
addFnDoc( addPropertyDoc(
name = "days", name = "days",
doc = "Return this duration as a real number of days.", doc = "Return this duration as a real number of days.",
returns = type("lyng.Real"), type = type("lyng.Real"),
moduleName = "lyng.time" moduleName = "lyng.time",
) { thisAs<ObjDuration>().duration.toDouble(DurationUnit.DAYS).toObj() } getter = { thisAs<ObjDuration>().duration.toDouble(DurationUnit.DAYS).toObj() }
addFnDoc( )
addPropertyDoc(
name = "hours", name = "hours",
doc = "Return this duration as a real number of hours.", doc = "Return this duration as a real number of hours.",
returns = type("lyng.Real"), type = type("lyng.Real"),
moduleName = "lyng.time" moduleName = "lyng.time",
) { thisAs<ObjDuration>().duration.toDouble(DurationUnit.HOURS).toObj() } getter = { thisAs<ObjDuration>().duration.toDouble(DurationUnit.HOURS).toObj() }
addFnDoc( )
addPropertyDoc(
name = "minutes", name = "minutes",
doc = "Return this duration as a real number of minutes.", doc = "Return this duration as a real number of minutes.",
returns = type("lyng.Real"), type = type("lyng.Real"),
moduleName = "lyng.time" moduleName = "lyng.time",
) { thisAs<ObjDuration>().duration.toDouble(DurationUnit.MINUTES).toObj() } getter = { thisAs<ObjDuration>().duration.toDouble(DurationUnit.MINUTES).toObj() }
addFnDoc( )
addPropertyDoc(
name = "seconds", name = "seconds",
doc = "Return this duration as a real number of seconds.", doc = "Return this duration as a real number of seconds.",
returns = type("lyng.Real"), type = type("lyng.Real"),
moduleName = "lyng.time" moduleName = "lyng.time",
) { thisAs<ObjDuration>().duration.toDouble(DurationUnit.SECONDS).toObj() } getter = { thisAs<ObjDuration>().duration.toDouble(DurationUnit.SECONDS).toObj() }
addFnDoc( )
addPropertyDoc(
name = "milliseconds", name = "milliseconds",
doc = "Return this duration as a real number of milliseconds.", doc = "Return this duration as a real number of milliseconds.",
returns = type("lyng.Real"), type = type("lyng.Real"),
moduleName = "lyng.time" moduleName = "lyng.time",
) { thisAs<ObjDuration>().duration.toDouble(DurationUnit.MILLISECONDS).toObj() } getter = { thisAs<ObjDuration>().duration.toDouble(DurationUnit.MILLISECONDS).toObj() }
addFnDoc( )
addPropertyDoc(
name = "microseconds", name = "microseconds",
doc = "Return this duration as a real number of microseconds.", doc = "Return this duration as a real number of microseconds.",
returns = type("lyng.Real"), type = type("lyng.Real"),
moduleName = "lyng.time" moduleName = "lyng.time",
) { thisAs<ObjDuration>().duration.toDouble(DurationUnit.MICROSECONDS).toObj() } getter = { thisAs<ObjDuration>().duration.toDouble(DurationUnit.MICROSECONDS).toObj() }
)
// extensions // extensions
ObjInt.type.addFnDoc( ObjInt.type.addPropertyDoc(
name = "seconds", name = "seconds",
doc = "Construct a `Duration` equal to this integer number of seconds.", doc = "Construct a `Duration` equal to this integer number of seconds.",
returns = type("lyng.Duration"), type = type("lyng.Duration"),
moduleName = "lyng.time" moduleName = "lyng.time",
) { ObjDuration(thisAs<ObjInt>().value.seconds) } getter = { ObjDuration(thisAs<ObjInt>().value.seconds) }
)
ObjInt.type.addFnDoc( ObjInt.type.addPropertyDoc(
name = "second", name = "second",
doc = "Construct a `Duration` equal to this integer number of seconds.", doc = "Construct a `Duration` equal to this integer number of seconds.",
returns = type("lyng.Duration"), type = type("lyng.Duration"),
moduleName = "lyng.time" moduleName = "lyng.time",
) { ObjDuration(thisAs<ObjInt>().value.seconds) } getter = { ObjDuration(thisAs<ObjInt>().value.seconds) }
ObjInt.type.addFnDoc( )
ObjInt.type.addPropertyDoc(
name = "milliseconds", name = "milliseconds",
doc = "Construct a `Duration` equal to this integer number of milliseconds.", doc = "Construct a `Duration` equal to this integer number of milliseconds.",
returns = type("lyng.Duration"), type = type("lyng.Duration"),
moduleName = "lyng.time" moduleName = "lyng.time",
) { ObjDuration(thisAs<ObjInt>().value.milliseconds) } getter = { ObjDuration(thisAs<ObjInt>().value.milliseconds) }
)
ObjInt.type.addFnDoc( ObjInt.type.addPropertyDoc(
name = "millisecond", name = "millisecond",
doc = "Construct a `Duration` equal to this integer number of milliseconds.", doc = "Construct a `Duration` equal to this integer number of milliseconds.",
returns = type("lyng.Duration"), type = type("lyng.Duration"),
moduleName = "lyng.time" moduleName = "lyng.time",
) { ObjDuration(thisAs<ObjInt>().value.milliseconds) } getter = { ObjDuration(thisAs<ObjInt>().value.milliseconds) }
ObjReal.type.addFnDoc( )
ObjReal.type.addPropertyDoc(
name = "seconds", name = "seconds",
doc = "Construct a `Duration` equal to this real number of seconds.", doc = "Construct a `Duration` equal to this real number of seconds.",
returns = type("lyng.Duration"), type = type("lyng.Duration"),
moduleName = "lyng.time" moduleName = "lyng.time",
) { ObjDuration(thisAs<ObjReal>().value.seconds) } getter = { ObjDuration(thisAs<ObjReal>().value.seconds) }
)
ObjReal.type.addFnDoc( ObjReal.type.addPropertyDoc(
name = "second", name = "second",
doc = "Construct a `Duration` equal to this real number of seconds.", doc = "Construct a `Duration` equal to this real number of seconds.",
returns = type("lyng.Duration"), type = type("lyng.Duration"),
moduleName = "lyng.time" moduleName = "lyng.time",
) { ObjDuration(thisAs<ObjReal>().value.seconds) } getter = { ObjDuration(thisAs<ObjReal>().value.seconds) }
)
ObjReal.type.addFnDoc( ObjReal.type.addPropertyDoc(
name = "milliseconds", name = "milliseconds",
doc = "Construct a `Duration` equal to this real number of milliseconds.", doc = "Construct a `Duration` equal to this real number of milliseconds.",
returns = type("lyng.Duration"), type = type("lyng.Duration"),
moduleName = "lyng.time" moduleName = "lyng.time",
) { ObjDuration(thisAs<ObjReal>().value.milliseconds) } getter = { ObjDuration(thisAs<ObjReal>().value.milliseconds) }
ObjReal.type.addFnDoc( )
ObjReal.type.addPropertyDoc(
name = "millisecond", name = "millisecond",
doc = "Construct a `Duration` equal to this real number of milliseconds.", doc = "Construct a `Duration` equal to this real number of milliseconds.",
returns = type("lyng.Duration"), type = type("lyng.Duration"),
moduleName = "lyng.time" moduleName = "lyng.time",
) { ObjDuration(thisAs<ObjReal>().value.milliseconds) } getter = { ObjDuration(thisAs<ObjReal>().value.milliseconds) }
)
ObjInt.type.addFnDoc( ObjInt.type.addPropertyDoc(
name = "minutes", name = "minutes",
doc = "Construct a `Duration` equal to this integer number of minutes.", doc = "Construct a `Duration` equal to this integer number of minutes.",
returns = type("lyng.Duration"), type = type("lyng.Duration"),
moduleName = "lyng.time" moduleName = "lyng.time",
) { ObjDuration(thisAs<ObjInt>().value.minutes) } getter = { ObjDuration(thisAs<ObjInt>().value.minutes) }
ObjReal.type.addFnDoc( )
ObjReal.type.addPropertyDoc(
name = "minutes", name = "minutes",
doc = "Construct a `Duration` equal to this real number of minutes.", doc = "Construct a `Duration` equal to this real number of minutes.",
returns = type("lyng.Duration"), type = type("lyng.Duration"),
moduleName = "lyng.time" moduleName = "lyng.time",
) { ObjDuration(thisAs<ObjReal>().value.minutes) } getter = { ObjDuration(thisAs<ObjReal>().value.minutes) }
ObjInt.type.addFnDoc( )
ObjInt.type.addPropertyDoc(
name = "minute", name = "minute",
doc = "Construct a `Duration` equal to this integer number of minutes.", doc = "Construct a `Duration` equal to this integer number of minutes.",
returns = type("lyng.Duration"), type = type("lyng.Duration"),
moduleName = "lyng.time" moduleName = "lyng.time",
) { ObjDuration(thisAs<ObjInt>().value.minutes) } getter = { ObjDuration(thisAs<ObjInt>().value.minutes) }
ObjReal.type.addFnDoc( )
ObjReal.type.addPropertyDoc(
name = "minute", name = "minute",
doc = "Construct a `Duration` equal to this real number of minutes.", doc = "Construct a `Duration` equal to this real number of minutes.",
returns = type("lyng.Duration"), type = type("lyng.Duration"),
moduleName = "lyng.time" moduleName = "lyng.time",
) { ObjDuration(thisAs<ObjReal>().value.minutes) } getter = { ObjDuration(thisAs<ObjReal>().value.minutes) }
ObjInt.type.addFnDoc( )
ObjInt.type.addPropertyDoc(
name = "hours", name = "hours",
doc = "Construct a `Duration` equal to this integer number of hours.", doc = "Construct a `Duration` equal to this integer number of hours.",
returns = type("lyng.Duration"), type = type("lyng.Duration"),
moduleName = "lyng.time" moduleName = "lyng.time",
) { ObjDuration(thisAs<ObjInt>().value.hours) } getter = { ObjDuration(thisAs<ObjInt>().value.hours) }
ObjReal.type.addFnDoc( )
ObjReal.type.addPropertyDoc(
name = "hours", name = "hours",
doc = "Construct a `Duration` equal to this real number of hours.", doc = "Construct a `Duration` equal to this real number of hours.",
returns = type("lyng.Duration"), type = type("lyng.Duration"),
moduleName = "lyng.time" moduleName = "lyng.time",
) { ObjDuration(thisAs<ObjReal>().value.hours) } getter = { ObjDuration(thisAs<ObjReal>().value.hours) }
ObjInt.type.addFnDoc( )
ObjInt.type.addPropertyDoc(
name = "hour", name = "hour",
doc = "Construct a `Duration` equal to this integer number of hours.", doc = "Construct a `Duration` equal to this integer number of hours.",
returns = type("lyng.Duration"), type = type("lyng.Duration"),
moduleName = "lyng.time" moduleName = "lyng.time",
) { ObjDuration(thisAs<ObjInt>().value.hours) } getter = { ObjDuration(thisAs<ObjInt>().value.hours) }
ObjReal.type.addFnDoc( )
ObjReal.type.addPropertyDoc(
name = "hour", name = "hour",
doc = "Construct a `Duration` equal to this real number of hours.", doc = "Construct a `Duration` equal to this real number of hours.",
returns = type("lyng.Duration"), type = type("lyng.Duration"),
moduleName = "lyng.time" moduleName = "lyng.time",
) { ObjDuration(thisAs<ObjReal>().value.hours) } getter = { ObjDuration(thisAs<ObjReal>().value.hours) }
ObjInt.type.addFnDoc( )
ObjInt.type.addPropertyDoc(
name = "days", name = "days",
doc = "Construct a `Duration` equal to this integer number of days.", doc = "Construct a `Duration` equal to this integer number of days.",
returns = type("lyng.Duration"), type = type("lyng.Duration"),
moduleName = "lyng.time" moduleName = "lyng.time",
) { ObjDuration(thisAs<ObjInt>().value.days) } getter = { ObjDuration(thisAs<ObjInt>().value.days) }
ObjReal.type.addFnDoc( )
ObjReal.type.addPropertyDoc(
name = "days", name = "days",
doc = "Construct a `Duration` equal to this real number of days.", doc = "Construct a `Duration` equal to this real number of days.",
returns = type("lyng.Duration"), type = type("lyng.Duration"),
moduleName = "lyng.time" moduleName = "lyng.time",
) { ObjDuration(thisAs<ObjReal>().value.days) } getter = { ObjDuration(thisAs<ObjReal>().value.days) }
ObjInt.type.addFnDoc( )
ObjInt.type.addPropertyDoc(
name = "day", name = "day",
doc = "Construct a `Duration` equal to this integer number of days.", doc = "Construct a `Duration` equal to this integer number of days.",
returns = type("lyng.Duration"), type = type("lyng.Duration"),
moduleName = "lyng.time" moduleName = "lyng.time",
) { ObjDuration(thisAs<ObjInt>().value.days) } getter = { ObjDuration(thisAs<ObjInt>().value.days) }
ObjReal.type.addFnDoc( )
ObjReal.type.addPropertyDoc(
name = "day", name = "day",
doc = "Construct a `Duration` equal to this real number of days.", doc = "Construct a `Duration` equal to this real number of days.",
returns = type("lyng.Duration"), type = type("lyng.Duration"),
moduleName = "lyng.time" moduleName = "lyng.time",
) { ObjDuration(thisAs<ObjReal>().value.days) } getter = { ObjDuration(thisAs<ObjReal>().value.days) }
)
// addFn("epochSeconds") { // addFn("epochSeconds") {

View File

@ -35,6 +35,8 @@ package net.sergeych.lyng.obj/*
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonPrimitive
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.miniast.addPropertyDoc
import net.sergeych.lyng.miniast.type
import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType import net.sergeych.lynon.LynonType
@ -76,8 +78,8 @@ class ObjEnumClass(val name: String) : ObjClass(name, EnumBase) {
val name = requireOnlyArg<ObjString>() val name = requireOnlyArg<ObjString>()
byName[name] ?: raiseSymbolNotFound("does not exists: enum ${className}.$name") byName[name] ?: raiseSymbolNotFound("does not exists: enum ${className}.$name")
} }
addFn("name") { thisAs<ObjEnumEntry>().name } addPropertyDoc("name", doc = "Entry name as string", type = type("lyng.String"), getter = { thisAs<ObjEnumEntry>().name })
addFn("ordinal") { thisAs<ObjEnumEntry>().ordinal } addPropertyDoc("ordinal", doc = "Entry ordinal position", type = type("lyng.Int"), getter = { thisAs<ObjEnumEntry>().ordinal })
} }

View File

@ -17,11 +17,10 @@
package net.sergeych.lyng.obj package net.sergeych.lyng.obj
import net.sergeych.bintools.encodeToHex
import net.sergeych.lyng.* import net.sergeych.lyng.*
import net.sergeych.lyng.miniast.TypeGenericDoc import net.sergeych.lyng.miniast.TypeGenericDoc
import net.sergeych.lyng.miniast.addConstDoc
import net.sergeych.lyng.miniast.addFnDoc import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.addPropertyDoc
import net.sergeych.lyng.miniast.type import net.sergeych.lyng.miniast.type
import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder import net.sergeych.lynon.LynonEncoder
@ -133,7 +132,7 @@ open class ObjException(
return ObjException(this, scope, message) return ObjException(this, scope, message)
} }
override fun toString(): String = "ExceptionClass[$name]@${hashCode().encodeToHex()}" override fun toString(): String = name
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj { override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj {
return try { return try {
@ -170,43 +169,44 @@ open class ObjException(
ObjVoid ObjVoid
}) })
instanceConstructor = statement { ObjVoid } instanceConstructor = statement { ObjVoid }
addConstDoc( addPropertyDoc(
name = "message", name = "message",
value = statement { doc = "Human‑readable error message.",
when (val t = thisObj) { type = type("lyng.String"),
moduleName = "lyng.stdlib",
getter = {
when (val t = this.thisObj) {
is ObjException -> t.message is ObjException -> t.message
is ObjInstance -> t.instanceScope.get("Exception::message")?.value ?: ObjNull is ObjInstance -> t.instanceScope.get("Exception::message")?.value ?: ObjNull
else -> ObjNull else -> ObjNull
} }
}, }
doc = "Human‑readable error message.",
type = type("lyng.String"),
moduleName = "lyng.stdlib"
) )
addConstDoc( addPropertyDoc(
name = "extraData", name = "extraData",
value = statement { doc = "Extra data associated with the exception.",
when (val t = thisObj) { type = type("lyng.Any", nullable = true),
moduleName = "lyng.stdlib",
getter = {
when (val t = this.thisObj) {
is ObjException -> t.extraData is ObjException -> t.extraData
else -> ObjNull else -> ObjNull
} }
}, }
doc = "Extra data associated with the exception.",
type = type("lyng.Any", nullable = true),
moduleName = "lyng.stdlib"
) )
addFnDoc( addPropertyDoc(
name = "stackTrace", name = "stackTrace",
doc = "Stack trace captured at throw site as a list of `StackTraceEntry`.", doc = "Stack trace captured at throw site as a list of `StackTraceEntry`.",
returns = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.StackTraceEntry"))), type = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.StackTraceEntry"))),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { getter = {
when (val t = thisObj) { when (val t = this.thisObj) {
is ObjException -> t.getStackTrace() is ObjException -> t.getStackTrace()
is ObjInstance -> t.instanceScope.get("Exception::stackTrace")?.value as? ObjList ?: ObjList() is ObjInstance -> t.instanceScope.get("Exception::stackTrace")?.value as? ObjList ?: ObjList()
else -> ObjList() else -> ObjList()
}
} }
} )
addFnDoc( addFnDoc(
name = "toString", name = "toString",
doc = "Human‑readable string representation of the error.", doc = "Human‑readable string representation of the error.",

View File

@ -21,6 +21,7 @@ import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import net.sergeych.lyng.Arguments import net.sergeych.lyng.Arguments
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.Visibility
import net.sergeych.lyng.canAccessMember import net.sergeych.lyng.canAccessMember
import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder import net.sergeych.lynon.LynonEncoder
@ -31,50 +32,35 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
internal lateinit var instanceScope: Scope internal lateinit var instanceScope: Scope
override suspend fun readField(scope: Scope, name: String): ObjRecord { override suspend fun readField(scope: Scope, name: String): ObjRecord {
// 1. Direct (unmangled) lookup first val caller = scope.currentClassCtx
instanceScope[name]?.let { rec ->
// 0. Private mangled of current class context
caller?.let { c ->
val mangled = "${c.className}::$name"
instanceScope.objects[mangled]?.let { rec ->
if (rec.visibility == Visibility.Private) {
return resolveRecord(scope, rec, name, c)
}
}
}
// 1. MRO mangled storage
for (cls in objClass.mro) {
if (cls.className == "Obj") break
val mangled = "${cls.className}::$name"
instanceScope.objects[mangled]?.let { rec ->
if ((scope.thisObj === this && caller != null) || canAccessMember(rec.visibility, cls, caller)) {
return resolveRecord(scope, rec, name, cls)
}
}
}
// 2. Unmangled storage
instanceScope.objects[name]?.let { rec ->
val decl = rec.declaringClass val decl = rec.declaringClass
// Allow unconditional access when accessing through `this` of the same instance // Unmangled access is only allowed if it's public OR we are inside the same instance's method
// BUT only if we are in the class context (not extension) if ((scope.thisObj === this && caller != null) || canAccessMember(rec.visibility, decl, caller))
if (scope.thisObj !== this || scope.currentClassCtx == null) { return resolveRecord(scope, rec, name, decl)
val caller = scope.currentClassCtx
if (!canAccessMember(rec.visibility, decl, caller))
scope.raiseError(
ObjIllegalAccessException(
scope,
"can't access field $name (declared in ${decl?.className ?: "?"})"
)
)
}
return resolveRecord(scope, rec, name, decl)
}
// 2. MI-mangled instance scope lookup
val cls = objClass
fun findMangledInRead(): ObjRecord? {
instanceScope.objects["${cls.className}::$name"]?.let { return it }
for (p in cls.mroParents) {
instanceScope.objects["${p.className}::$name"]?.let { return it }
}
return null
}
findMangledInRead()?.let { rec ->
val declaring = when {
instanceScope.objects.containsKey("${cls.className}::$name") -> cls
else -> cls.mroParents.firstOrNull { instanceScope.objects.containsKey("${it.className}::$name") }
}
if (scope.thisObj !== this || scope.currentClassCtx == null) {
val caller = scope.currentClassCtx
if (!canAccessMember(rec.visibility, declaring, caller))
scope.raiseError(
ObjIllegalAccessException(
scope,
"can't access field $name (declared in ${declaring?.className ?: "?"})"
)
)
}
return resolveRecord(scope, rec, name, declaring)
} }
// 3. Fall back to super (handles class members and extensions) // 3. Fall back to super (handles class members and extensions)
@ -83,6 +69,90 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
override suspend fun resolveRecord(scope: Scope, obj: ObjRecord, name: String, decl: ObjClass?): ObjRecord { override suspend fun resolveRecord(scope: Scope, obj: ObjRecord, name: String, decl: ObjClass?): ObjRecord {
if (obj.type == ObjRecord.Type.Delegated) { if (obj.type == ObjRecord.Type.Delegated) {
val d = decl ?: obj.declaringClass
val storageName = "${d?.className}::$name"
var del = instanceScope[storageName]?.delegate ?: obj.delegate
if (del == null) {
for (c in objClass.mro) {
del = instanceScope["${c.className}::$name"]?.delegate
if (del != null) break
}
}
del = del ?: scope.raiseError("Internal error: delegated property $name has no delegate")
val res = del.invokeInstanceMethod(scope, "getValue", Arguments(this, ObjString(name)))
obj.value = res
return obj
}
// Map member template to instance storage if applicable
var targetRec = obj
val d = decl ?: obj.declaringClass
if (d != null) {
val mangled = "${d.className}::$name"
instanceScope.objects[mangled]?.let {
targetRec = it
}
}
if (targetRec === obj) {
instanceScope.objects[name]?.let { rec ->
// Check if this record in instanceScope is the one we want.
// For members, it must match the declaring class.
// Arguments are also preferred.
if (rec.type == ObjRecord.Type.Argument || rec.declaringClass == d || rec.declaringClass == null) {
targetRec = rec
}
}
}
return super.resolveRecord(scope, targetRec, name, d)
}
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
willMutate(scope)
val caller = scope.currentClassCtx
// 0. Private mangled of current class context
caller?.let { c ->
val mangled = "${c.className}::$name"
instanceScope.objects[mangled]?.let { rec ->
if (rec.visibility == Visibility.Private) {
updateRecord(scope, rec, name, newValue, c)
return
}
}
}
// 1. MRO mangled storage
for (cls in objClass.mro) {
if (cls.className == "Obj") break
val mangled = "${cls.className}::$name"
instanceScope.objects[mangled]?.let { rec ->
if ((scope.thisObj === this && caller != null) || canAccessMember(rec.effectiveWriteVisibility, cls, caller)) {
updateRecord(scope, rec, name, newValue, cls)
return
}
}
}
// 2. Unmangled storage
instanceScope.objects[name]?.let { rec ->
val decl = rec.declaringClass
if ((scope.thisObj === this && caller != null) || canAccessMember(rec.effectiveWriteVisibility, decl, caller)) {
updateRecord(scope, rec, name, newValue, decl)
return
}
}
super.writeField(scope, name, newValue)
}
private suspend fun updateRecord(scope: Scope, rec: ObjRecord, name: String, newValue: Obj, decl: ObjClass?) {
if (rec.type == ObjRecord.Type.Property) {
val prop = rec.value as ObjProperty
prop.callSetter(scope, this, newValue, decl)
return
}
if (rec.type == ObjRecord.Type.Delegated) {
val storageName = "${decl?.className}::$name" val storageName = "${decl?.className}::$name"
var del = instanceScope[storageName]?.delegate var del = instanceScope[storageName]?.delegate
if (del == null) { if (del == null) {
@ -91,136 +161,80 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
if (del != null) break if (del != null) break
} }
} }
del = del ?: obj.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate (tried $storageName)") del = del ?: rec.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate (tried $storageName)")
val res = del.invokeInstanceMethod(scope, "getValue", Arguments(this, ObjString(name))) del.invokeInstanceMethod(scope, "setValue", Arguments(this, ObjString(name), newValue))
obj.value = res
return obj
}
return super.resolveRecord(scope, obj, name, decl)
}
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
// Direct (unmangled) first
instanceScope[name]?.let { f ->
val decl = f.declaringClass
if (scope.thisObj !== this || scope.currentClassCtx == null) {
val caller = scope.currentClassCtx
if (!canAccessMember(f.effectiveWriteVisibility, decl, caller))
ObjIllegalAccessException(
scope,
"can't assign to field $name (declared in ${decl?.className ?: "?"})"
).raise()
}
if (f.type == ObjRecord.Type.Property) {
val prop = f.value as ObjProperty
prop.callSetter(scope, this, newValue, decl)
return
}
if (f.type == ObjRecord.Type.Delegated) {
val storageName = "${decl?.className}::$name"
var del = instanceScope[storageName]?.delegate
if (del == null) {
for (c in objClass.mro) {
del = instanceScope["${c.className}::$name"]?.delegate
if (del != null) break
}
}
del = del ?: f.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate (tried $storageName)")
del.invokeInstanceMethod(scope, "setValue", Arguments(this, ObjString(name), newValue))
return
}
if (!f.isMutable && f.value !== ObjUnset) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
if (f.value.assign(scope, newValue) == null)
f.value = newValue
return return
} }
// Try MI-mangled resolution along linearization (C3 MRO) if (!rec.isMutable && rec.value !== ObjUnset) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
val cls = objClass if (rec.value.assign(scope, newValue) == null)
fun findMangled(): ObjRecord? { rec.value = newValue
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") }
}
if (scope.thisObj !== this || scope.currentClassCtx == null) {
val caller = scope.currentClassCtx
if (!canAccessMember(rec.effectiveWriteVisibility, declaring, caller))
ObjIllegalAccessException(
scope,
"can't assign to field $name (declared in ${declaring?.className ?: "?"})"
).raise()
}
if (rec.type == ObjRecord.Type.Property) {
val prop = rec.value as ObjProperty
prop.callSetter(scope, this, newValue, declaring)
return
}
if (rec.type == ObjRecord.Type.Delegated) {
val storageName = "${declaring?.className}::$name"
var del = instanceScope[storageName]?.delegate
if (del == null) {
for (c in objClass.mro) {
del = instanceScope["${c.className}::$name"]?.delegate
if (del != null) break
}
}
del = del ?: rec.delegate ?: scope.raiseError("Internal error: delegated property $name has no delegate (tried $storageName)")
del.invokeInstanceMethod(scope, "setValue", Arguments(this, ObjString(name), newValue))
return
}
if (!rec.isMutable && rec.value !== ObjUnset) 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( override suspend fun invokeInstanceMethod(
scope: Scope, name: String, args: Arguments, scope: Scope, name: String, args: Arguments,
onNotFoundResult: (suspend () -> Obj?)? onNotFoundResult: (suspend () -> Obj?)?
): Obj { ): Obj {
// 0. Prefer private member of current class context
scope.currentClassCtx?.let { caller ->
val mangled = "${caller.className}::$name"
instanceScope.objects[mangled]?.let { rec ->
if (rec.visibility == Visibility.Private && !rec.isAbstract) {
if (rec.type == ObjRecord.Type.Property) {
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, caller)
} else if (rec.type == ObjRecord.Type.Fun) {
return rec.value.invoke(instanceScope, this, args, caller)
}
}
}
caller.members[name]?.let { rec ->
if (rec.visibility == Visibility.Private && !rec.isAbstract) {
if (rec.type == ObjRecord.Type.Property) {
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, caller)
} else if (rec.type == ObjRecord.Type.Fun) {
return rec.value.invoke(instanceScope, this, args, caller)
}
}
}
}
// 1. Walk MRO to find member, handling delegation // 1. Walk MRO to find member, handling delegation
for (cls in objClass.mro) { for (cls in objClass.mro) {
if (cls.className == "Obj") break if (cls.className == "Obj") break
val rec = cls.members[name] ?: cls.classScope?.objects?.get(name) val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
if (rec != null) { if (rec != null && !rec.isAbstract) {
if (rec.type == ObjRecord.Type.Delegated) { if (rec.type == ObjRecord.Type.Delegated) {
val storageName = "${cls.className}::$name" val storageName = "${cls.className}::$name"
val del = instanceScope[storageName]?.delegate ?: rec.delegate val del = instanceScope[storageName]?.delegate ?: rec.delegate
?: scope.raiseError("Internal error: delegated member $name has no delegate (tried $storageName)") ?: scope.raiseError("Internal error: delegated member $name has no delegate (tried $storageName)")
// For delegated member, try 'invoke' first if it's a function-like call
val allArgs = (listOf(this, ObjString(name)) + args.list).toTypedArray() val allArgs = (listOf(this, ObjString(name)) + args.list).toTypedArray()
return del.invokeInstanceMethod(scope, "invoke", Arguments(*allArgs), onNotFoundResult = { return del.invokeInstanceMethod(scope, "invoke", Arguments(*allArgs), onNotFoundResult = {
// Fallback: property delegation // Fallback: property delegation (getValue then call result)
val propVal = del.invokeInstanceMethod(scope, "getValue", Arguments(this, ObjString(name))) val propVal = del.invokeInstanceMethod(scope, "getValue", Arguments(this, ObjString(name)))
propVal.invoke(scope, this, args, rec.declaringClass ?: cls) propVal.invoke(scope, this, args, rec.declaringClass ?: cls)
}) })
} }
if (rec.type == ObjRecord.Type.Fun && !rec.isAbstract) { val decl = rec.declaringClass ?: cls
val decl = rec.declaringClass ?: cls val caller = scope.currentClassCtx ?: if (scope.thisObj === this) objClass else null
val caller = scope.currentClassCtx ?: if (scope.thisObj === this) objClass else null if (!canAccessMember(rec.visibility, decl, caller))
if (!canAccessMember(rec.visibility, decl, caller)) scope.raiseError(
scope.raiseError( ObjIllegalAccessException(
ObjIllegalAccessException( scope,
scope, "can't invoke method $name (declared in ${decl.className})"
"can't invoke method $name (declared in ${decl.className})"
)
) )
)
if (rec.type == ObjRecord.Type.Property) {
if (args.isEmpty()) return (rec.value as ObjProperty).callGetter(scope, this, decl)
} else if (rec.type == ObjRecord.Type.Fun) {
return rec.value.invoke( return rec.value.invoke(
instanceScope, instanceScope,
this, this,
args, args,
decl decl
) )
} else if ((rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.Property) && !rec.isAbstract) { } else if (rec.type == ObjRecord.Type.Field || rec.type == ObjRecord.Type.ConstructorField || rec.type == ObjRecord.Type.Argument) {
val resolved = readField(scope, name) val resolved = readField(scope, name)
return resolved.value.invoke(scope, this, args, resolved.declaringClass) return resolved.value.invoke(scope, this, args, resolved.declaringClass)
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -20,6 +20,9 @@ package net.sergeych.lyng.obj
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonPrimitive
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.addPropertyDoc
import net.sergeych.lyng.miniast.type
import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonSettings import net.sergeych.lynon.LynonSettings
@ -148,34 +151,71 @@ class ObjInstant(val instant: Instant,val truncateMode: LynonSettings.InstantTru
} }
}.apply { }.apply {
addFn("epochSeconds") { addPropertyDoc(
val instant = thisAs<ObjInstant>().instant name = "epochSeconds",
ObjReal(instant.epochSeconds + instant.nanosecondsOfSecond * 1e-9) doc = "Return the number of seconds since the Unix epoch as a real number (including fractions).",
} type = type("lyng.Real"),
addFn("isDistantFuture") { moduleName = "lyng.time",
thisAs<ObjInstant>().instant.isDistantFuture.toObj() getter = {
} val instant = thisAs<ObjInstant>().instant
addFn("isDistantPast") { ObjReal(instant.epochSeconds + instant.nanosecondsOfSecond * 1e-9)
thisAs<ObjInstant>().instant.isDistantPast.toObj() }
} )
addFn("epochWholeSeconds") { addPropertyDoc(
ObjInt(thisAs<ObjInstant>().instant.epochSeconds) name = "isDistantFuture",
} doc = "Whether this instant represents the distant future.",
addFn("nanosecondsOfSecond") { type = type("lyng.Bool"),
ObjInt(thisAs<ObjInstant>().instant.nanosecondsOfSecond.toLong()) moduleName = "lyng.time",
} getter = { thisAs<ObjInstant>().instant.isDistantFuture.toObj() }
addFn("truncateToSecond") { )
addPropertyDoc(
name = "isDistantPast",
doc = "Whether this instant represents the distant past.",
type = type("lyng.Bool"),
moduleName = "lyng.time",
getter = { thisAs<ObjInstant>().instant.isDistantPast.toObj() }
)
addPropertyDoc(
name = "epochWholeSeconds",
doc = "Return the number of full seconds since the Unix epoch.",
type = type("lyng.Int"),
moduleName = "lyng.time",
getter = { ObjInt(thisAs<ObjInstant>().instant.epochSeconds) }
)
addPropertyDoc(
name = "nanosecondsOfSecond",
doc = "The number of nanoseconds within the current second.",
type = type("lyng.Int"),
moduleName = "lyng.time",
getter = { ObjInt(thisAs<ObjInstant>().instant.nanosecondsOfSecond.toLong()) }
)
addFnDoc(
name = "truncateToSecond",
doc = "Truncate this instant to the nearest second.",
returns = type("lyng.Instant"),
moduleName = "lyng.time"
) {
val t = thisAs<ObjInstant>().instant val t = thisAs<ObjInstant>().instant
ObjInstant(Instant.fromEpochSeconds(t.epochSeconds), LynonSettings.InstantTruncateMode.Second) ObjInstant(Instant.fromEpochSeconds(t.epochSeconds), LynonSettings.InstantTruncateMode.Second)
} }
addFn("truncateToMillisecond") { addFnDoc(
name = "truncateToMillisecond",
doc = "Truncate this instant to the nearest millisecond.",
returns = type("lyng.Instant"),
moduleName = "lyng.time"
) {
val t = thisAs<ObjInstant>().instant val t = thisAs<ObjInstant>().instant
ObjInstant( ObjInstant(
Instant.fromEpochSeconds(t.epochSeconds, t.nanosecondsOfSecond / 1_000_000 * 1_000_000), Instant.fromEpochSeconds(t.epochSeconds, t.nanosecondsOfSecond / 1_000_000 * 1_000_000),
LynonSettings.InstantTruncateMode.Millisecond LynonSettings.InstantTruncateMode.Millisecond
) )
} }
addFn("truncateToMicrosecond") { addFnDoc(
name = "truncateToMicrosecond",
doc = "Truncate this instant to the nearest microsecond.",
returns = type("lyng.Instant"),
moduleName = "lyng.time"
) {
val t = thisAs<ObjInstant>().instant val t = thisAs<ObjInstant>().instant
ObjInstant( ObjInstant(
Instant.fromEpochSeconds(t.epochSeconds, t.nanosecondsOfSecond / 1_000 * 1_000), Instant.fromEpochSeconds(t.epochSeconds, t.nanosecondsOfSecond / 1_000 * 1_000),

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -21,6 +21,7 @@ import net.sergeych.lyng.Arguments
import net.sergeych.lyng.Statement import net.sergeych.lyng.Statement
import net.sergeych.lyng.miniast.ParamDoc import net.sergeych.lyng.miniast.ParamDoc
import net.sergeych.lyng.miniast.addFnDoc import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.addPropertyDoc
import net.sergeych.lyng.miniast.type import net.sergeych.lyng.miniast.type
/** /**
@ -29,19 +30,20 @@ import net.sergeych.lyng.miniast.type
val ObjIterable by lazy { val ObjIterable by lazy {
ObjClass("Iterable").apply { ObjClass("Iterable").apply {
addFnDoc( addPropertyDoc(
name = "toList", name = "toList",
doc = "Collect elements of this iterable into a new list.", doc = "Collect elements of this iterable into a new list.",
returns = type("lyng.List"), type = type("lyng.List"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { getter = {
val result = mutableListOf<Obj>() val result = mutableListOf<Obj>()
val iterator = thisObj.invokeInstanceMethod(this, "iterator") val it = this.thisObj.invokeInstanceMethod(this, "iterator")
while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
while (iterator.invokeInstanceMethod(this, "hasNext").toBool()) result.add(it.invokeInstanceMethod(this, "next"))
result += iterator.invokeInstanceMethod(this, "next") }
ObjList(result) ObjList(result)
} }
)
// it is not effective, but it is open: // it is not effective, but it is open:
addFnDoc( addFnDoc(
@ -55,7 +57,7 @@ val ObjIterable by lazy {
val obj = args.firstAndOnly() val obj = args.firstAndOnly()
val it = thisObj.invokeInstanceMethod(this, "iterator") val it = thisObj.invokeInstanceMethod(this, "iterator")
while (it.invokeInstanceMethod(this, "hasNext").toBool()) { while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
if (obj.compareTo(this, it.invokeInstanceMethod(this, "next")) == 0) if (obj.equals(this, it.invokeInstanceMethod(this, "next")))
return@addFnDoc ObjTrue return@addFnDoc ObjTrue
} }
ObjFalse ObjFalse
@ -73,46 +75,49 @@ val ObjIterable by lazy {
var index = 0 var index = 0
val it = thisObj.invokeInstanceMethod(this, "iterator") val it = thisObj.invokeInstanceMethod(this, "iterator")
while (it.invokeInstanceMethod(this, "hasNext").toBool()) { while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
if (obj.compareTo(this, it.invokeInstanceMethod(this, "next")) == 0) if (obj.equals(this, it.invokeInstanceMethod(this, "next")))
return@addFnDoc ObjInt(index.toLong()) return@addFnDoc ObjInt(index.toLong())
index++ index++
} }
ObjInt(-1L) ObjInt(-1L)
} }
addFnDoc( addPropertyDoc(
name = "toSet", name = "toSet",
doc = "Collect elements of this iterable into a new set.", doc = "Collect elements of this iterable into a new set.",
returns = type("lyng.Set"), type = type("lyng.Set"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { getter = {
if( thisObj.isInstanceOf(ObjSet.type) ) if( this.thisObj.isInstanceOf(ObjSet.type) )
thisObj this.thisObj
else { else {
val result = mutableSetOf<Obj>() val result = mutableSetOf<Obj>()
val it = thisObj.invokeInstanceMethod(this, "iterator") val it = this.thisObj.invokeInstanceMethod(this, "iterator")
while (it.invokeInstanceMethod(this, "hasNext").toBool()) { while (it.invokeInstanceMethod(this, "hasNext").toBool()) {
result += it.invokeInstanceMethod(this, "next") result.add(it.invokeInstanceMethod(this, "next"))
}
ObjSet(result)
} }
ObjSet(result)
} }
} )
addFnDoc( addPropertyDoc(
name = "toMap", name = "toMap",
doc = "Collect pairs into a map using [0] as key and [1] as value for each element.", doc = "Collect pairs into a map using [0] as key and [1] as value for each element.",
returns = type("lyng.Map"), type = type("lyng.Map"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { getter = {
val result = mutableMapOf<Obj, Obj>() val result = mutableMapOf<Obj, Obj>()
thisObj.toFlow(this).collect { pair -> this.thisObj.enumerate(this) { pair ->
when (pair) { when (pair) {
is ObjMapEntry -> result[pair.key] = pair.value is ObjMapEntry -> result[pair.key] = pair.value
else -> result[pair.getAt(this, 0)] = pair.getAt(this, 1) else -> result[pair.getAt(this, 0)] = pair.getAt(this, 1)
}
true
} }
ObjMap(result)
} }
ObjMap(result) )
}
addFnDoc( addFnDoc(
name = "associateBy", name = "associateBy",
@ -156,7 +161,7 @@ val ObjIterable by lazy {
val fn = requiredArg<Statement>(0) val fn = requiredArg<Statement>(0)
val result = mutableListOf<Obj>() val result = mutableListOf<Obj>()
thisObj.toFlow(this).collect { thisObj.toFlow(this).collect {
result += fn.call(this, it) result.add(fn.call(this, it))
} }
ObjList(result) ObjList(result)
} }
@ -173,7 +178,7 @@ val ObjIterable by lazy {
val result = mutableListOf<Obj>() val result = mutableListOf<Obj>()
thisObj.toFlow(this).collect { thisObj.toFlow(this).collect {
val transformed = fn.call(this, it) val transformed = fn.call(this, it)
if( transformed != ObjNull) result += transformed if( transformed != ObjNull) result.add(transformed)
} }
ObjList(result) ObjList(result)
} }
@ -189,25 +194,26 @@ val ObjIterable by lazy {
val result = mutableListOf<Obj>() val result = mutableListOf<Obj>()
if (n > 0) { if (n > 0) {
thisObj.enumerate(this) { thisObj.enumerate(this) {
result += it result.add(it)
--n > 0 --n > 0
} }
} }
ObjList(result) ObjList(result)
} }
addFnDoc( addPropertyDoc(
name = "isEmpty", name = "isEmpty",
doc = "Whether the iterable has no elements.", doc = "Whether the iterable has no elements.",
returns = type("lyng.Bool"), type = type("lyng.Bool"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { getter = {
ObjBool( ObjBool(
thisObj.invokeInstanceMethod(this, "iterator") this.thisObj.invokeInstanceMethod(this, "iterator")
.invokeInstanceMethod(this, "hasNext").toBool() .invokeInstanceMethod(this, "hasNext").toBool()
.not() .not()
) )
} }
)
addFnDoc( addFnDoc(
name = "sortedWith", name = "sortedWith",

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -22,16 +22,30 @@ import kotlinx.serialization.json.JsonElement
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.Statement import net.sergeych.lyng.Statement
import net.sergeych.lyng.miniast.ParamDoc import net.sergeych.lyng.miniast.ParamDoc
import net.sergeych.lyng.miniast.addConstDoc
import net.sergeych.lyng.miniast.addFnDoc import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.addPropertyDoc
import net.sergeych.lyng.miniast.type import net.sergeych.lyng.miniast.type
import net.sergeych.lyng.statement
import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType import net.sergeych.lynon.LynonType
class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() { class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
override suspend fun equals(scope: Scope, other: Obj): Boolean {
if (this === other) return true
if (other !is ObjList) {
if (other.isInstanceOf(ObjIterable)) {
return compareTo(scope, other) == 0
}
return false
}
if (list.size != other.list.size) return false
for (i in 0..<list.size) {
if (!list[i].equals(scope, other.list[i])) return false
}
return true
}
override suspend fun getAt(scope: Scope, index: Obj): Obj { override suspend fun getAt(scope: Scope, index: Obj): Obj {
return when (index) { return when (index) {
is ObjInt -> { is ObjInt -> {
@ -77,21 +91,35 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
} }
override suspend fun compareTo(scope: Scope, other: Obj): Int { override suspend fun compareTo(scope: Scope, other: Obj): Int {
if (other !is ObjList) return -2 if (other is ObjList) {
val mySize = list.size val mySize = list.size
val otherSize = other.list.size val otherSize = other.list.size
val commonSize = minOf(mySize, otherSize) val commonSize = minOf(mySize, otherSize)
for (i in 0..<commonSize) { for (i in 0..<commonSize) {
if (list[i].compareTo(scope, other.list[i]) != 0) { val d = list[i].compareTo(scope, other.list[i])
return list[i].compareTo(scope, other.list[i]) if (d != 0) {
return d
}
} }
val res = mySize.compareTo(otherSize)
return res
} }
// equal so far, longer is greater: if (other.isInstanceOf(ObjIterable)) {
return when { val it1 = this.list.iterator()
mySize < otherSize -> -1 val it2 = other.invokeInstanceMethod(scope, "iterator")
mySize > otherSize -> 1 val hasNext2 = it2.getInstanceMethod(scope, "hasNext")
else -> 0 val next2 = it2.getInstanceMethod(scope, "next")
while (it1.hasNext()) {
if (!hasNext2.invoke(scope, it2).toBool()) return 1 // I'm longer
val v1 = it1.next()
val v2 = next2.invoke(scope, it2)
val d = v1.compareTo(scope, v2)
if (d != 0) return d
}
return if (hasNext2.invoke(scope, it2).toBool()) -1 else 0
} }
return -2
} }
override suspend fun plus(scope: Scope, other: Obj): Obj = override suspend fun plus(scope: Scope, other: Obj): Obj =
@ -99,27 +127,28 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
other is ObjList -> other is ObjList ->
ObjList((list + other.list).toMutableList()) ObjList((list + other.list).toMutableList())
other.isInstanceOf(ObjIterable) -> { other.isInstanceOf(ObjIterable) && other !is ObjString && other !is ObjBuffer -> {
val l = other.callMethod<ObjList>(scope, "toList") val l = other.callMethod<ObjList>(scope, "toList")
ObjList((list + l.list).toMutableList()) ObjList((list + l.list).toMutableList())
} }
else -> else -> {
scope.raiseError("'+': can't concatenate ${this.toString(scope)} with ${other.toString(scope)}") val newList = list.toMutableList()
newList.add(other)
ObjList(newList)
}
} }
override suspend fun plusAssign(scope: Scope, other: Obj): Obj { override suspend fun plusAssign(scope: Scope, other: Obj): Obj {
// optimization
if (other is ObjList) { if (other is ObjList) {
list += other.list list.addAll(other.list)
return this } else if (other.isInstanceOf(ObjIterable) && other !is ObjString && other !is ObjBuffer) {
val otherList = (other.invokeInstanceMethod(scope, "toList") as ObjList).list
list.addAll(otherList)
} else {
list.add(other)
} }
if (other.isInstanceOf(ObjIterable)) {
val otherList = other.invokeInstanceMethod(scope, "toList") as ObjList
list += otherList.list
} else
list += other
return this return this
} }
@ -222,26 +251,25 @@ class ObjList(val list: MutableList<Obj> = mutableListOf()) : Obj() {
return ObjList(decoder.decodeAnyList(scope)) return ObjList(decoder.decodeAnyList(scope))
} }
}.apply { }.apply {
addConstDoc( addPropertyDoc(
name = "size", name = "size",
value = statement {
(thisObj as ObjList).list.size.toObj()
},
doc = "Number of elements in this list.", doc = "Number of elements in this list.",
type = type("lyng.Int"), type = type("lyng.Int"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
getter = {
val s = (this.thisObj as ObjList).list.size
s.toObj()
}
) )
addConstDoc( addFnDoc(
name = "add", name = "add",
value = statement {
val l = thisAs<ObjList>().list
for (a in args) l.add(a)
ObjVoid
},
doc = "Append one or more elements to the end of this list.", doc = "Append one or more elements to the end of this list.",
type = type("lyng.Callable"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib"
) ) {
val l = thisAs<ObjList>().list
for (a in args) l.add(a)
ObjVoid
}
addFnDoc( addFnDoc(
name = "insertAt", name = "insertAt",
doc = "Insert elements starting at the given index.", doc = "Insert elements starting at the given index.",

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -21,10 +21,7 @@ import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.Statement import net.sergeych.lyng.Statement
import net.sergeych.lyng.miniast.ParamDoc import net.sergeych.lyng.miniast.*
import net.sergeych.lyng.miniast.TypeGenericDoc
import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.type
import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType import net.sergeych.lynon.LynonType
@ -76,24 +73,27 @@ class ObjMapEntry(val key: Obj, val value: Obj) : Obj() {
) )
} }
}.apply { }.apply {
addFnDoc( addPropertyDoc(
name = "key", name = "key",
doc = "Key component of this map entry.", doc = "Key component of this map entry.",
returns = type("lyng.Any"), type = type("lyng.Any"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { thisAs<ObjMapEntry>().key } getter = { thisAs<ObjMapEntry>().key }
addFnDoc( )
addPropertyDoc(
name = "value", name = "value",
doc = "Value component of this map entry.", doc = "Value component of this map entry.",
returns = type("lyng.Any"), type = type("lyng.Any"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { thisAs<ObjMapEntry>().value } getter = { thisAs<ObjMapEntry>().value }
addFnDoc( )
addPropertyDoc(
name = "size", name = "size",
doc = "Number of components in this entry (always 2).", doc = "Number of components in this entry (always 2).",
returns = type("lyng.Int"), type = type("lyng.Int"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { 2.toObj() } getter = { 2.toObj() }
)
} }
} }
@ -106,6 +106,18 @@ class ObjMapEntry(val key: Obj, val value: Obj) : Obj() {
class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() { class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
override suspend fun equals(scope: Scope, other: Obj): Boolean {
if (this === other) return true
if (other !is ObjMap) return false
if (map.size != other.map.size) return false
for ((k, v) in map) {
val otherV = other.getAt(scope, k)
if (otherV === ObjNull && !other.contains(scope, k)) return false
if (!v.equals(scope, otherV)) return false
}
return true
}
override val objClass get() = type override val objClass get() = type
override suspend fun getAt(scope: Scope, index: Obj): Obj = override suspend fun getAt(scope: Scope, index: Obj): Obj =
@ -120,7 +132,13 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
} }
override suspend fun compareTo(scope: Scope, other: Obj): Int { override suspend fun compareTo(scope: Scope, other: Obj): Int {
if( other is ObjMap && other.map == map) return 0 if (other is ObjMap) {
if (map == other.map) return 0
if (map.size != other.map.size) return map.size.compareTo(other.map.size)
// for same size, if they are not equal, we don't have a stable order
// but let's try to be consistent
return map.toString().compareTo(other.map.toString())
}
return -1 return -1
} }
@ -247,14 +265,13 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
lambda.execute(this) lambda.execute(this)
} }
} }
addFnDoc( addPropertyDoc(
name = "size", name = "size",
doc = "Number of entries in the map.", doc = "Number of entries in the map.",
returns = type("lyng.Int"), type = type("lyng.Int"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { getter = { (this.thisObj as ObjMap).map.size.toObj() }
thisAs<ObjMap>().map.size.toObj() )
}
addFnDoc( addFnDoc(
name = "remove", name = "remove",
doc = "Remove the entry by key and return the previous value or null if absent.", doc = "Remove the entry by key and return the previous value or null if absent.",
@ -273,22 +290,20 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
thisAs<ObjMap>().map.clear() thisAs<ObjMap>().map.clear()
thisObj thisObj
} }
addFnDoc( addPropertyDoc(
name = "keys", name = "keys",
doc = "List of keys in this map.", doc = "List of keys in this map.",
returns = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.Any"))), type = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.Any"))),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { getter = { thisAs<ObjMap>().map.keys.toObj() }
thisAs<ObjMap>().map.keys.toObj() )
} addPropertyDoc(
addFnDoc(
name = "values", name = "values",
doc = "List of values in this map.", doc = "List of values in this map.",
returns = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.Any"))), type = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.Any"))),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { getter = { ObjList(thisAs<ObjMap>().map.values.toMutableList()) }
ObjList(thisAs<ObjMap>().map.values.toMutableList()) )
}
addFnDoc( addFnDoc(
name = "iterator", name = "iterator",
doc = "Iterator over map entries as MapEntry objects.", doc = "Iterator over map entries as MapEntry objects.",
@ -328,7 +343,14 @@ class ObjMap(val map: MutableMap<Obj, Obj> = mutableMapOf()) : Obj() {
for (e in other.list) { for (e in other.list) {
val entry = when (e) { val entry = when (e) {
is ObjMapEntry -> e is ObjMapEntry -> e
else -> scope.raiseIllegalArgument("map can only be merged with MapEntry elements; got $e") else -> {
if (e.isInstanceOf(ObjArray)) {
if (e.invokeInstanceMethod(scope, "size").toInt() != 2)
scope.raiseIllegalArgument("Array element to merge into map must have 2 elements, got $e")
ObjMapEntry(e.getAt(scope, 0), e.getAt(scope, 1))
} else
scope.raiseIllegalArgument("map can only be merged with MapEntry elements; got $e")
}
} }
map[entry.key] = entry.value map[entry.key] = entry.value
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -20,6 +20,7 @@ package net.sergeych.lyng.obj
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.miniast.TypeGenericDoc import net.sergeych.lyng.miniast.TypeGenericDoc
import net.sergeych.lyng.miniast.addFnDoc import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.addPropertyDoc
import net.sergeych.lyng.miniast.type import net.sergeych.lyng.miniast.type
class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Obj() { class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Obj() {
@ -174,54 +175,48 @@ class ObjRange(val start: Obj?, val end: Obj?, val isEndInclusive: Boolean) : Ob
companion object { companion object {
val type = ObjClass("Range", ObjIterable).apply { val type = ObjClass("Range", ObjIterable).apply {
addFnDoc( addPropertyDoc(
name = "start", name = "start",
doc = "Start bound of the range or null if open.", doc = "Start bound of the range or null if open.",
returns = type("lyng.Any", nullable = true), type = type("lyng.Any", nullable = true),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { getter = { thisAs<ObjRange>().start ?: ObjNull }
thisAs<ObjRange>().start ?: ObjNull )
} addPropertyDoc(
addFnDoc(
name = "end", name = "end",
doc = "End bound of the range or null if open.", doc = "End bound of the range or null if open.",
returns = type("lyng.Any", nullable = true), type = type("lyng.Any", nullable = true),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { getter = { thisAs<ObjRange>().end ?: ObjNull }
thisAs<ObjRange>().end ?: ObjNull )
} addPropertyDoc(
addFnDoc(
name = "isOpen", name = "isOpen",
doc = "Whether the range is open on either side (no start or no end).", doc = "Whether the range is open on either side (no start or no end).",
returns = type("lyng.Bool"), type = type("lyng.Bool"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { getter = { thisAs<ObjRange>().let { it.start == null || it.end == null }.toObj() }
thisAs<ObjRange>().let { it.start == null || it.end == null }.toObj() )
} addPropertyDoc(
addFnDoc(
name = "isIntRange", name = "isIntRange",
doc = "True if both bounds are Int values.", doc = "True if both bounds are Int values.",
returns = type("lyng.Bool"), type = type("lyng.Bool"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { getter = { thisAs<ObjRange>().isIntRange.toObj() }
thisAs<ObjRange>().isIntRange.toObj() )
} addPropertyDoc(
addFnDoc(
name = "isCharRange", name = "isCharRange",
doc = "True if both bounds are Char values.", doc = "True if both bounds are Char values.",
returns = type("lyng.Bool"), type = type("lyng.Bool"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { getter = { thisAs<ObjRange>().isCharRange.toObj() }
thisAs<ObjRange>().isCharRange.toObj() )
} addPropertyDoc(
addFnDoc(
name = "isEndInclusive", name = "isEndInclusive",
doc = "Whether the end bound is inclusive.", doc = "Whether the end bound is inclusive.",
returns = type("lyng.Bool"), type = type("lyng.Bool"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { getter = { thisAs<ObjRange>().isEndInclusive.toObj() }
thisAs<ObjRange>().isEndInclusive.toObj() )
}
addFnDoc( addFnDoc(
name = "iterator", name = "iterator",
doc = "Iterator over elements in this range (optimized for Int ranges).", doc = "Iterator over elements in this range (optimized for Int ranges).",

View File

@ -36,6 +36,8 @@ data class ObjRecord(
val isClosed: Boolean = false, val isClosed: Boolean = false,
val isOverride: Boolean = false, val isOverride: Boolean = false,
var delegate: Obj? = null, var delegate: Obj? = null,
/** The receiver object to resolve this member against (for instance fields/methods). */
var receiver: Obj? = null,
) { ) {
val effectiveWriteVisibility: Visibility get() = writeVisibility ?: visibility val effectiveWriteVisibility: Visibility get() = writeVisibility ?: visibility
enum class Type(val comparable: Boolean = false,val serializable: Boolean = false) { enum class Type(val comparable: Boolean = false,val serializable: Boolean = false) {

View File

@ -34,7 +34,15 @@ sealed interface ObjRef {
*/ */
suspend fun evalValue(scope: Scope): Obj { suspend fun evalValue(scope: Scope): Obj {
val rec = get(scope) val rec = get(scope)
if (rec.type == ObjRecord.Type.Delegated) return scope.resolve(rec, "unknown") if (rec.type == ObjRecord.Type.Delegated) {
val receiver = rec.receiver ?: scope.thisObj
// Use resolve to handle delegated property logic
return scope.resolve(rec, "unknown")
}
// Template record: must map to instance storage
if (rec.receiver != null && rec.declaringClass != null) {
return rec.receiver!!.resolveRecord(scope, rec, "unknown", rec.declaringClass).value
}
return rec.value return rec.value
} }
suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) { suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
@ -383,7 +391,7 @@ class IncDecRef(
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
val rec = target.get(scope) val rec = target.get(scope)
if (!rec.isMutable) scope.raiseError("Cannot ${if (isIncrement) "increment" else "decrement"} immutable value") if (!rec.isMutable) scope.raiseError("Cannot ${if (isIncrement) "increment" else "decrement"} immutable value")
val v = scope.resolve(rec, "unknown") val v = target.evalValue(scope)
val one = ObjInt.One val one = ObjInt.One
// We now treat numbers as immutable and always perform write-back via setAt. // We now treat numbers as immutable and always perform write-back via setAt.
// This avoids issues where literals are shared and mutated in-place. // This avoids issues where literals are shared and mutated in-place.
@ -947,65 +955,63 @@ class IndexRef(
} }
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) { override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
val fastRval = PerfFlags.RVAL_FASTPATH val base = target.evalValue(scope)
val base = if (fastRval) target.evalValue(scope) else target.get(scope).value
if (base == ObjNull && isOptional) { if (base == ObjNull && isOptional) {
// no-op on null receiver for optional chaining assignment // no-op on null receiver for optional chaining assignment
return return
} }
val idx = if (fastRval) index.evalValue(scope) else index.get(scope).value val idx = index.evalValue(scope)
if (fastRval) {
// Mirror read fast-path with direct write for ObjList + ObjInt index // Mirror read fast-path with direct write for ObjList + ObjInt index
if (base is ObjList && idx is ObjInt) { if (base is ObjList && idx is ObjInt) {
val i = idx.toInt() val i = idx.toInt()
base.list[i] = newValue base.list[i] = newValue
return return
}
// Direct write fast path for ObjMap + ObjString
if (base is ObjMap && idx is ObjString) {
base.map[idx] = newValue
return
}
if (PerfFlags.RVAL_FASTPATH && PerfFlags.INDEX_PIC) {
// Polymorphic inline cache for index write
val (key, ver) = when (base) {
is ObjInstance -> base.objClass.classId to base.objClass.layoutVersion
is ObjClass -> base.classId to base.layoutVersion
else -> 0L to -1
} }
// Direct write fast path for ObjMap + ObjString if (key != 0L) {
if (base is ObjMap && idx is ObjString) { wSetter1?.let { s -> if (key == wKey1 && ver == wVer1) { s(base, scope, idx, newValue); return } }
base.map[idx] = newValue wSetter2?.let { s -> if (key == wKey2 && ver == wVer2) {
return val tk = wKey2; val tv = wVer2; val ts = wSetter2
}
if (PerfFlags.INDEX_PIC) {
// Polymorphic inline cache for index write
val (key, ver) = when (base) {
is ObjInstance -> base.objClass.classId to base.objClass.layoutVersion
is ObjClass -> base.classId to base.layoutVersion
else -> 0L to -1
}
if (key != 0L) {
wSetter1?.let { s -> if (key == wKey1 && ver == wVer1) { s(base, scope, idx, newValue); return } }
wSetter2?.let { s -> if (key == wKey2 && ver == wVer2) {
val tk = wKey2; val tv = wVer2; val ts = wSetter2
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
wKey1 = tk; wVer1 = tv; wSetter1 = ts
s(base, scope, idx, newValue); return
} }
if (PerfFlags.INDEX_PIC_SIZE_4) wSetter3?.let { s -> if (key == wKey3 && ver == wVer3) {
val tk = wKey3; val tv = wVer3; val ts = wSetter3
wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
wKey1 = tk; wVer1 = tv; wSetter1 = ts
s(base, scope, idx, newValue); return
} }
if (PerfFlags.INDEX_PIC_SIZE_4) wSetter4?.let { s -> if (key == wKey4 && ver == wVer4) {
val tk = wKey4; val tv = wVer4; val ts = wSetter4
wKey4 = wKey3; wVer4 = wVer3; wSetter4 = wSetter3
wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
wKey1 = tk; wVer1 = tv; wSetter1 = ts
s(base, scope, idx, newValue); return
} }
// Miss: perform write and install generic handler
base.putAt(scope, idx, newValue)
if (PerfFlags.INDEX_PIC_SIZE_4) {
wKey4 = wKey3; wVer4 = wVer3; wSetter4 = wSetter3
wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2
}
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1 wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, ix, v -> obj.putAt(sc, ix, v) } wKey1 = tk; wVer1 = tv; wSetter1 = ts
return s(base, scope, idx, newValue); return
} }
if (PerfFlags.INDEX_PIC_SIZE_4) wSetter3?.let { s -> if (key == wKey3 && ver == wVer3) {
val tk = wKey3; val tv = wVer3; val ts = wSetter3
wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
wKey1 = tk; wVer1 = tv; wSetter1 = ts
s(base, scope, idx, newValue); return
} }
if (PerfFlags.INDEX_PIC_SIZE_4) wSetter4?.let { s -> if (key == wKey4 && ver == wVer4) {
val tk = wKey4; val tv = wVer4; val ts = wSetter4
wKey4 = wKey3; wVer4 = wVer3; wSetter4 = wSetter3
wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
wKey1 = tk; wVer1 = tv; wSetter1 = ts
s(base, scope, idx, newValue); return
} }
// Miss: perform write and install generic handler
base.putAt(scope, idx, newValue)
if (PerfFlags.INDEX_PIC_SIZE_4) {
wKey4 = wKey3; wVer4 = wVer3; wSetter4 = wSetter3
wKey3 = wKey2; wVer3 = wVer2; wSetter3 = wSetter2
} }
wKey2 = wKey1; wVer2 = wVer1; wSetter2 = wSetter1
wKey1 = key; wVer1 = ver; wSetter1 = { obj, sc, ix, v -> obj.putAt(sc, ix, v) }
return
} }
} }
base.putAt(scope, idx, newValue) base.putAt(scope, idx, newValue)
@ -1266,20 +1272,6 @@ class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef {
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.localVarPicMiss++ if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.localVarPicMiss++
// 2) Fallback to current-scope object or field on `this` // 2) Fallback to current-scope object or field on `this`
scope[name]?.let { return it } scope[name]?.let { return it }
// 2a) Try nearest ClosureScope's closure ancestry explicitly
run {
var s: Scope? = scope
val visited = HashSet<Long>(4)
while (s != null) {
if (!visited.add(s.frameId)) break
if (s is ClosureScope) {
s.closureScope.chainLookupWithMembers(name)?.let { return it }
}
s = s.parent
}
}
// 2b) Try raw ancestry local/binding lookup (cycle-safe), including slots in parents
scope.chainLookupIgnoreClosure(name)?.let { return it }
try { try {
return scope.thisObj.readField(scope, name) return scope.thisObj.readField(scope, name)
} catch (e: ExecutionError) { } catch (e: ExecutionError) {
@ -1305,18 +1297,6 @@ class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef {
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.localVarPicMiss++ if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.localVarPicMiss++
// 2) Fallback name in scope or field on `this` // 2) Fallback name in scope or field on `this`
scope[name]?.let { return it } scope[name]?.let { return it }
run {
var s: Scope? = scope
val visited = HashSet<Long>(4)
while (s != null) {
if (!visited.add(s.frameId)) break
if (s is ClosureScope) {
s.closureScope.chainLookupWithMembers(name)?.let { return it }
}
s = s.parent
}
}
scope.chainLookupIgnoreClosure(name)?.let { return it }
try { try {
return scope.thisObj.readField(scope, name) return scope.thisObj.readField(scope, name)
} catch (e: ExecutionError) { } catch (e: ExecutionError) {
@ -1327,56 +1307,11 @@ class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef {
override suspend fun evalValue(scope: Scope): Obj { override suspend fun evalValue(scope: Scope): Obj {
scope.pos = atPos scope.pos = atPos
if (!PerfFlags.LOCAL_SLOT_PIC) { scope.getSlotIndexOf(name)?.let { return scope.resolve(scope.getSlotRecord(it), name) }
scope.getSlotIndexOf(name)?.let { return scope.resolve(scope.getSlotRecord(it), name) } // fallback to current-scope object or field on `this`
// fallback to current-scope object or field on `this` scope[name]?.let { return scope.resolve(it, name) }
scope[name]?.let { return scope.resolve(it, name) }
run {
var s: Scope? = scope
val visited = HashSet<Long>(4)
while (s != null) {
if (!visited.add(s.frameId)) break
if (s is ClosureScope) {
s.closureScope.chainLookupWithMembers(name)?.let { return s.resolve(it, name) }
}
s = s.parent
}
}
scope.chainLookupIgnoreClosure(name)?.let { return scope.resolve(it, name) }
return try {
scope.thisObj.readField(scope, name).value
} catch (e: ExecutionError) {
if ((e.message ?: "").contains("no such field: $name")) scope.raiseSymbolNotFound(name)
throw e
}
}
val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
val slot = if (hit) cachedSlot else resolveSlot(scope)
if (slot >= 0) {
val rec = scope.getSlotRecord(slot)
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) {
return scope.resolve(rec, name)
}
}
// Fallback name in scope or field on `this`
scope[name]?.let {
return scope.resolve(it, name)
}
run {
var s: Scope? = scope
val visited = HashSet<Long>(4)
while (s != null) {
if (!visited.add(s.frameId)) break
if (s is ClosureScope) {
s.closureScope.chainLookupWithMembers(name)?.let { return s.resolve(it, name) }
}
s = s.parent
}
}
scope.chainLookupIgnoreClosure(name)?.let { return scope.resolve(it, name) }
return try { return try {
val res = scope.thisObj.readField(scope, name).value scope.thisObj.readField(scope, name).value
res
} catch (e: ExecutionError) { } catch (e: ExecutionError) {
if ((e.message ?: "").contains("no such field: $name")) scope.raiseSymbolNotFound(name) if ((e.message ?: "").contains("no such field: $name")) scope.raiseSymbolNotFound(name)
throw e throw e
@ -1395,24 +1330,6 @@ class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef {
scope.assign(stored, name, newValue) scope.assign(stored, name, newValue)
return return
} }
run {
var s: Scope? = scope
val visited = HashSet<Long>(4)
while (s != null) {
if (!visited.add(s.frameId)) break
if (s is ClosureScope) {
s.closureScope.chainLookupWithMembers(name)?.let { stored ->
s.assign(stored, name, newValue)
return
}
}
s = s.parent
}
}
scope.chainLookupIgnoreClosure(name)?.let { stored ->
scope.assign(stored, name, newValue)
return
}
// Fallback: write to field on `this` // Fallback: write to field on `this`
scope.thisObj.writeField(scope, name, newValue) scope.thisObj.writeField(scope, name, newValue)
return return
@ -1429,24 +1346,6 @@ class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef {
scope.assign(stored, name, newValue) scope.assign(stored, name, newValue)
return return
} }
run {
var s: Scope? = scope
val visited = HashSet<Long>(4)
while (s != null) {
if (!visited.add(s.frameId)) break
if (s is ClosureScope) {
s.closureScope.chainLookupWithMembers(name)?.let { stored ->
s.assign(stored, name, newValue)
return
}
}
s = s.parent
}
}
scope.chainLookupIgnoreClosure(name)?.let { stored ->
scope.assign(stored, name, newValue)
return
}
scope.thisObj.writeField(scope, name, newValue) scope.thisObj.writeField(scope, name, newValue)
return return
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -20,10 +20,7 @@ package net.sergeych.lyng.obj
import net.sergeych.lyng.PerfFlags import net.sergeych.lyng.PerfFlags
import net.sergeych.lyng.RegexCache import net.sergeych.lyng.RegexCache
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.miniast.ParamDoc import net.sergeych.lyng.miniast.*
import net.sergeych.lyng.miniast.TypeGenericDoc
import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.type
class ObjRegex(val regex: Regex) : Obj() { class ObjRegex(val regex: Regex) : Obj() {
override val objClass get() = type override val objClass get() = type
@ -123,30 +120,27 @@ class ObjRegexMatch(val match: MatchResult) : Obj() {
scope.raiseError("RegexMatch can't be constructed directly") scope.raiseError("RegexMatch can't be constructed directly")
} }
}.apply { }.apply {
addFnDoc( addPropertyDoc(
name = "groups", name = "groups",
doc = "List of captured groups with index 0 as the whole match.", doc = "List of captured groups with index 0 as the whole match.",
returns = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.String"))), type = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.String"))),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { getter = { thisAs<ObjRegexMatch>().objGroups }
thisAs<ObjRegexMatch>().objGroups )
} addPropertyDoc(
addFnDoc(
name = "value", name = "value",
doc = "The matched substring.", doc = "The matched substring.",
returns = type("lyng.String"), type = type("lyng.String"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { getter = { thisAs<ObjRegexMatch>().objValue }
thisAs<ObjRegexMatch>().objValue )
} addPropertyDoc(
addFnDoc(
name = "range", name = "range",
doc = "Range of the match in the input (end-exclusive).", doc = "Range of the match in the input (end-exclusive).",
returns = type("lyng.Range"), type = type("lyng.Range"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { getter = { thisAs<ObjRegexMatch>().objRange }
thisAs<ObjRegexMatch>().objRange )
}
} }
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -18,10 +18,7 @@
package net.sergeych.lyng.obj package net.sergeych.lyng.obj
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.miniast.ParamDoc import net.sergeych.lyng.miniast.*
import net.sergeych.lyng.miniast.TypeGenericDoc
import net.sergeych.lyng.miniast.addFnDoc
import net.sergeych.lyng.miniast.type
class RingBuffer<T>(val maxSize: Int) : Iterable<T> { class RingBuffer<T>(val maxSize: Int) : Iterable<T> {
private val data = arrayOfNulls<Any>(maxSize) private val data = arrayOfNulls<Any>(maxSize)
@ -94,18 +91,20 @@ class ObjRingBuffer(val capacity: Int) : Obj() {
return ObjRingBuffer(scope.requireOnlyArg<ObjInt>().toInt()) return ObjRingBuffer(scope.requireOnlyArg<ObjInt>().toInt())
} }
}.apply { }.apply {
addFnDoc( addPropertyDoc(
name = "capacity", name = "capacity",
doc = "Maximum number of elements the buffer can hold.", doc = "Maximum number of elements the buffer can hold.",
returns = type("lyng.Int"), type = type("lyng.Int"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { thisAs<ObjRingBuffer>().capacity.toObj() } getter = { thisAs<ObjRingBuffer>().capacity.toObj() }
addFnDoc( )
addPropertyDoc(
name = "size", name = "size",
doc = "Current number of elements in the buffer.", doc = "Current number of elements in the buffer.",
returns = type("lyng.Int"), type = type("lyng.Int"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { thisAs<ObjRingBuffer>().buffer.size.toObj() } getter = { thisAs<ObjRingBuffer>().buffer.size.toObj() }
)
addFnDoc( addFnDoc(
name = "iterator", name = "iterator",
doc = "Iterator over elements in insertion order (oldest to newest).", doc = "Iterator over elements in insertion order (oldest to newest).",
@ -122,12 +121,16 @@ class ObjRingBuffer(val capacity: Int) : Obj() {
returns = type("lyng.Void"), returns = type("lyng.Void"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib"
) { thisAs<ObjRingBuffer>().apply { buffer.add(requireOnlyArg<Obj>()) } } ) { thisAs<ObjRingBuffer>().apply { buffer.add(requireOnlyArg<Obj>()) } }
addFnDoc( addPropertyDoc(
name = "first", name = "first",
doc = "Return the oldest element in the buffer.", doc = "Return the oldest element in the buffer.",
returns = type("lyng.Any"), type = type("lyng.Any"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { thisAs<ObjRingBuffer>().buffer.first() } getter = {
val buffer = (this.thisObj as ObjRingBuffer).buffer
if (buffer.size == 0) ObjNull else buffer.first()
}
)
} }
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -28,6 +28,18 @@ import net.sergeych.lynon.LynonType
class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() { class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
override suspend fun equals(scope: Scope, other: Obj): Boolean {
if (this === other) return true
if (other !is ObjSet) return false
if (set.size != other.set.size) return false
// Sets are equal if all my elements are in other and vice versa
// contains() in ObjSet uses equals(scope, ...), so we need to be careful
for (e in set) {
if (!other.contains(scope, e)) return false
}
return true
}
override val objClass get() = type override val objClass get() = type
override suspend fun contains(scope: Scope, other: Obj): Boolean { override suspend fun contains(scope: Scope, other: Obj): Boolean {
@ -113,11 +125,12 @@ class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
} }
override suspend fun compareTo(scope: Scope, other: Obj): Int { override suspend fun compareTo(scope: Scope, other: Obj): Int {
return if (other !is ObjSet) -1 if (other is ObjSet) {
else { if (set == other.set) return 0
if (set == other.set) 0 if (set.size != other.set.size) return set.size.compareTo(other.set.size)
else -1 return set.toString().compareTo(other.set.toString())
} }
return -2
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -126,10 +139,7 @@ class ObjSet(val set: MutableSet<Obj> = mutableSetOf()) : Obj() {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other == null || this::class != other::class) return false if (other !is ObjSet) return false
other as ObjSet
return set == other.set return set == other.set
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -25,7 +25,6 @@ import net.sergeych.lyng.PerfFlags
import net.sergeych.lyng.RegexCache import net.sergeych.lyng.RegexCache
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.miniast.* import net.sergeych.lyng.miniast.*
import net.sergeych.lyng.statement
import net.sergeych.lynon.LynonDecoder import net.sergeych.lynon.LynonDecoder
import net.sergeych.lynon.LynonEncoder import net.sergeych.lynon.LynonEncoder
import net.sergeych.lynon.LynonType import net.sergeych.lynon.LynonType
@ -124,10 +123,16 @@ data class ObjString(val value: String) : Obj() {
} }
companion object { companion object {
val type = object : ObjClass("String") { val type = object : ObjClass("String", ObjCollection) {
override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj = override suspend fun deserialize(scope: Scope, decoder: LynonDecoder, lynonType: LynonType?): Obj =
ObjString(decoder.unpackBinaryData().decodeToString()) ObjString(decoder.unpackBinaryData().decodeToString())
}.apply { }.apply {
addFnDoc(
name = "iterator",
doc = "Iterator over characters of this string.",
returns = TypeGenericDoc(type("lyng.Iterator"), listOf(type("lyng.Char"))),
moduleName = "lyng.stdlib"
) { ObjKotlinIterator(thisAs<ObjString>().value.iterator()) }
addFnDoc( addFnDoc(
name = "toInt", name = "toInt",
doc = "Parse this string as an integer or throw if it is not a valid integer.", doc = "Parse this string as an integer or throw if it is not a valid integer.",
@ -157,12 +162,12 @@ data class ObjString(val value: String) : Obj() {
) { ) {
ObjBool(thisAs<ObjString>().value.endsWith(requiredArg<ObjString>(0).value)) ObjBool(thisAs<ObjString>().value.endsWith(requiredArg<ObjString>(0).value))
} }
addConstDoc( addPropertyDoc(
name = "length", name = "length",
value = statement { ObjInt.of(thisAs<ObjString>().value.length.toLong()) },
doc = "Number of UTF-16 code units in this string.", doc = "Number of UTF-16 code units in this string.",
type = type("lyng.Int"), type = type("lyng.Int"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
getter = { ObjInt.of((this.thisObj as ObjString).value.length.toLong()) }
) )
addFnDoc( addFnDoc(
name = "takeLast", name = "takeLast",
@ -240,16 +245,17 @@ data class ObjString(val value: String) : Obj() {
) { ) {
thisAs<ObjString>().value.uppercase().let(::ObjString) thisAs<ObjString>().value.uppercase().let(::ObjString)
} }
addFnDoc( addPropertyDoc(
name = "characters", name = "characters",
doc = "List of characters of this string.", doc = "List of characters of this string.",
returns = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.Char"))), type = TypeGenericDoc(type("lyng.List"), listOf(type("lyng.Char"))),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { getter = {
ObjList( ObjList(
thisAs<ObjString>().value.map { ObjChar(it) }.toMutableList() (this.thisObj as ObjString).value.map { ObjChar(it) }.toMutableList()
) )
} }
)
addFnDoc( addFnDoc(
name = "last", name = "last",
doc = "The last character of this string or throw if the string is empty.", doc = "The last character of this string or throw if the string is empty.",
@ -264,12 +270,13 @@ data class ObjString(val value: String) : Obj() {
returns = type("lyng.Buffer"), returns = type("lyng.Buffer"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib"
) { ObjBuffer(thisAs<ObjString>().value.encodeToByteArray().asUByteArray()) } ) { ObjBuffer(thisAs<ObjString>().value.encodeToByteArray().asUByteArray()) }
addFnDoc( addPropertyDoc(
name = "size", name = "size",
doc = "Alias for length: the number of characters (code units) in this string.", doc = "Alias for length: the number of characters (code units) in this string.",
returns = type("lyng.Int"), type = type("lyng.Int"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib",
) { ObjInt.of(thisAs<ObjString>().value.length.toLong()) } getter = { ObjInt.of((this.thisObj as ObjString).value.length.toLong()) }
)
addFnDoc( addFnDoc(
name = "toReal", name = "toReal",
doc = "Parse this string as a real number (floating point).", doc = "Parse this string as a real number (floating point).",

View File

@ -1994,12 +1994,12 @@ class ScriptTest {
""" """
class Foo { class Foo {
// this means last is lambda: // this means last is lambda:
fun f(e=1, f) { fun test_f(e=1, f_param) {
"e="+e+"f="+f() "e="+e+"f="+f_param()
} }
} }
val f = Foo() val f_obj = Foo()
assertEquals("e=1f=xx", f.f { "xx" }) assertEquals("e=1f=xx", f_obj.test_f { "xx" })
""".trimIndent() """.trimIndent()
) )
@ -2011,13 +2011,13 @@ class ScriptTest {
""" """
class Foo { class Foo {
// this means last is lambda: // this means last is lambda:
fun f(e..., f) { fun test_f_ellipsis(e..., f_param) {
"e="+e+"f="+f() "e="+e+"f="+f_param()
} }
} }
val f = Foo() val f_obj = Foo()
assertEquals("e=[]f=xx", f.f { "xx" }) assertEquals("e=[]f=xx", f_obj.test_f_ellipsis { "xx" })
assertEquals("e=[1,2]f=xx", f.f(1,2) { "xx" }) assertEquals("e=[1,2]f=xx", f_obj.test_f_ellipsis(1,2) { "xx" })
""".trimIndent() """.trimIndent()
) )
@ -4645,7 +4645,8 @@ class ScriptTest {
null null
} catch { it } } catch { it }
assert(caught != null) assert(caught != null)
assert(caught.message.contains("Expected DerivedEx, got MyEx")) assertEquals("Expected DerivedEx, got MyEx", caught.message)
assert(caught.message == "Expected DerivedEx, got MyEx")
""".trimIndent()) """.trimIndent())
} }
@ -4703,7 +4704,7 @@ class ScriptTest {
// 61755f07-630c-4181-8d50-1b044d96e1f4 // 61755f07-630c-4181-8d50-1b044d96e1f4
class T { class T {
static var f1 = null static var f1 = null
static fun t(name=null) { static fun testCapture(name=null) {
run { run {
// I expect it will catch the 'name' from // I expect it will catch the 'name' from
// param? // param?
@ -4714,11 +4715,86 @@ class ScriptTest {
assert(T.f1 == null) assert(T.f1 == null)
println("-- "+T.f1::class) println("-- "+T.f1::class)
println("-- "+T.f1) println("-- "+T.f1)
T.t("foo") T.testCapture("foo")
println("2- "+T.f1::class) println("2- "+T.f1::class)
println("2- "+T.f1) println("2- "+T.f1)
assert(T.f1 == "foo") assert(T.f1 == "foo")
""".trimIndent()) """.trimIndent())
} }
@Test
fun testLazyLocals() = runTest() {
eval("""
class T {
val x by lazy {
val c = "c"
c + "!"
}
}
val t = T()
assertEquals("c!", t.x)
assertEquals("c!", t.x)
""".trimIndent())
}
@Test
fun testGetterLocals() = runTest() {
eval("""
class T {
val x get() {
val c = "c"
c + "!"
}
}
val t = T()
assertEquals("c!", t.x)
assertEquals("c!", t.x)
""".trimIndent())
}
@Test
fun testMethodLocals() = runTest() {
eval("""
class T {
fun x() {
val c = "c"
c + "!"
}
}
val t = T()
assertEquals("c!", t.x())
assertEquals("c!", t.x())
""".trimIndent())
}
@Test
fun testContrcuctorMagicIdBug() = runTest() {
eval("""
interface SomeI {
abstract fun x()
}
class T(id): SomeI {
override fun x() {
val c = id
c + "!"
}
}
val t = T("c")
assertEquals("c!", t.x())
assertEquals("c!", t.x())
""".trimIndent())
}
@Test
fun testLambdaLocals() = runTest() {
eval("""
class T {
val l = { x ->
val c = x + ":"
c + x
}
}
assertEquals("r:r", T().l("r"))
""".trimIndent())
}
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.

View File

@ -211,7 +211,7 @@ fun Iterable.sortedBy(predicate) {
/* Return a shuffled copy of the iterable as a list. */ /* Return a shuffled copy of the iterable as a list. */
fun Iterable.shuffled() { fun Iterable.shuffled() {
toList().apply { shuffle() } toList.apply { shuffle() }
} }
/* /*
@ -267,7 +267,7 @@ class StackTraceEntry(
fun Exception.printStackTrace() { fun Exception.printStackTrace() {
println(this) println(this)
var lastEntry = null var lastEntry = null
for( entry in stackTrace() ) { for( entry in stackTrace ) {
if( lastEntry == null || lastEntry !is StackTraceEntry || lastEntry.line != entry.line ) if( lastEntry == null || lastEntry !is StackTraceEntry || lastEntry.line != entry.line )
println("\tat "+entry.toString()) println("\tat "+entry.toString())
lastEntry = entry lastEntry = entry