Extensions methods and properties now are correctly isolated, respect visibility rules and allow adding class properties and class vals.
This commit is contained in:
1 parent
3b6504d3b1
commit
cd2b1a9cb7
26 files changed
+606
-163
No files matched your search
@@ -21,7 +21,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
group = "net.sergeych"
|
||||
version = "1.1.0-beta1"
|
||||
version = "1.1.0-SNAPSHOT"
|
||||
|
||||
// Removed legacy buildscript classpath declarations; plugins are applied via the plugins DSL below
|
||||
|
||||
|
||||
@@ -49,12 +49,14 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
|
||||
arguments: Arguments = scope.args,
|
||||
defaultAccessType: AccessType = AccessType.Var,
|
||||
defaultVisibility: Visibility = Visibility.Public,
|
||||
declaringClass: net.sergeych.lyng.obj.ObjClass? = scope.currentClassCtx
|
||||
) {
|
||||
fun assign(a: Item, value: Obj) {
|
||||
scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable,
|
||||
value.byValueCopy(),
|
||||
a.visibility ?: defaultVisibility,
|
||||
recordType = ObjRecord.Type.Argument)
|
||||
recordType = ObjRecord.Type.Argument,
|
||||
declaringClass = declaringClass)
|
||||
}
|
||||
|
||||
// Prepare positional args and parameter count, handle tail-block binding
|
||||
|
||||
@@ -58,17 +58,25 @@ class ClosureScope(val callScope: Scope, val closureScope: Scope) :
|
||||
?.instanceScope
|
||||
?.objects
|
||||
?.get(name)
|
||||
?.let { return it }
|
||||
closureScope.thisObj.objClass.getInstanceMemberOrNull(name)?.let { return it }
|
||||
?.let { rec ->
|
||||
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) return rec
|
||||
}
|
||||
findExtension(closureScope.thisObj.objClass, name)?.let { return it }
|
||||
closureScope.thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec ->
|
||||
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) return rec
|
||||
}
|
||||
|
||||
// 3) Closure scope chain (locals/parents + members), ignore ClosureScope overrides to prevent recursion
|
||||
closureScope.chainLookupWithMembers(name)?.let { return it }
|
||||
closureScope.chainLookupWithMembers(name, currentClassCtx)?.let { return it }
|
||||
|
||||
// 4) Caller `this` members
|
||||
callScope.thisObj.objClass.getInstanceMemberOrNull(name)?.let { return it }
|
||||
findExtension(callScope.thisObj.objClass, name)?.let { return it }
|
||||
callScope.thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec ->
|
||||
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) return rec
|
||||
}
|
||||
|
||||
// 5) Caller chain (locals/parents + members)
|
||||
callScope.chainLookupWithMembers(name)?.let { return it }
|
||||
callScope.chainLookupWithMembers(name, currentClassCtx)?.let { return it }
|
||||
|
||||
// 6) Module pseudo-symbols (e.g., __PACKAGE__) — walk caller ancestry and query ModuleScope directly
|
||||
if (name.startsWith("__")) {
|
||||
|
||||
@@ -2366,6 +2366,7 @@ class Compiler(
|
||||
throw ScriptError(t.pos, "Expected identifier after 'fun'")
|
||||
else t.value
|
||||
var nameStartPos: Pos = t.pos
|
||||
var receiverMini: MiniTypeRef? = null
|
||||
|
||||
val annotation = lastAnnotation
|
||||
val parentContext = codeContexts.last()
|
||||
@@ -2374,6 +2375,12 @@ class Compiler(
|
||||
// Is extension?
|
||||
if (t.type == Token.Type.DOT) {
|
||||
extTypeName = name
|
||||
val receiverEnd = Pos(start.source, start.line, start.column + name.length)
|
||||
receiverMini = MiniTypeName(
|
||||
range = MiniRange(start, receiverEnd),
|
||||
segments = listOf(MiniTypeName.Segment(name, MiniRange(start, receiverEnd))),
|
||||
nullable = false
|
||||
)
|
||||
t = cc.next()
|
||||
if (t.type != Token.Type.ID)
|
||||
throw ScriptError(t.pos, "illegal extension format: expected function name")
|
||||
@@ -2417,7 +2424,8 @@ class Compiler(
|
||||
returnType = returnTypeMini,
|
||||
body = null,
|
||||
doc = declDocLocal,
|
||||
nameStart = nameStartPos
|
||||
nameStart = nameStartPos,
|
||||
receiver = receiverMini
|
||||
)
|
||||
miniSink?.onFunDecl(node)
|
||||
pendingDeclDoc = null
|
||||
@@ -2486,7 +2494,7 @@ class Compiler(
|
||||
// class extension method
|
||||
val type = context[typeName]?.value ?: context.raiseSymbolNotFound("class $typeName not found")
|
||||
if (type !is ObjClass) context.raiseClassCastError("$typeName is not the class instance")
|
||||
type.addFn(name, isOpen = true, visibility = visibility) {
|
||||
val stmt = statement {
|
||||
// ObjInstance has a fixed instance scope, so we need to build a closure
|
||||
(thisObj as? ObjInstance)?.let { i ->
|
||||
annotatedFnBody.execute(ClosureScope(this, i.instanceScope))
|
||||
@@ -2494,6 +2502,7 @@ class Compiler(
|
||||
// other classes can create one-time scope for this rare case:
|
||||
?: annotatedFnBody.execute(thisObj.autoInstanceScope(this))
|
||||
}
|
||||
context.addExtension(type, name, ObjRecord(stmt, isMutable = false, visibility = visibility, declaringClass = null))
|
||||
}
|
||||
// regular function/method
|
||||
?: run {
|
||||
@@ -2640,7 +2649,27 @@ class Compiler(
|
||||
|
||||
if (nextToken.type != Token.Type.ID)
|
||||
throw ScriptError(nextToken.pos, "Expected identifier or [ here")
|
||||
val name = nextToken.value
|
||||
var name = nextToken.value
|
||||
var extTypeName: String? = null
|
||||
var nameStartPos: Pos = nextToken.pos
|
||||
var receiverMini: MiniTypeRef? = null
|
||||
|
||||
if (cc.peekNextNonWhitespace().type == Token.Type.DOT) {
|
||||
cc.skipWsTokens()
|
||||
cc.next() // consume dot
|
||||
extTypeName = name
|
||||
val receiverEnd = Pos(nextToken.pos.source, nextToken.pos.line, nextToken.pos.column + name.length)
|
||||
receiverMini = MiniTypeName(
|
||||
range = MiniRange(nextToken.pos, receiverEnd),
|
||||
segments = listOf(MiniTypeName.Segment(name, MiniRange(nextToken.pos, receiverEnd))),
|
||||
nullable = false
|
||||
)
|
||||
val nameToken = cc.next()
|
||||
if (nameToken.type != Token.Type.ID)
|
||||
throw ScriptError(nameToken.pos, "Expected identifier after dot in extension declaration")
|
||||
name = nameToken.value
|
||||
nameStartPos = nameToken.pos
|
||||
}
|
||||
|
||||
// Optional explicit type annotation
|
||||
val varTypeMini: MiniTypeRef? = if (cc.current().type == Token.Type.COLON) {
|
||||
@@ -2648,19 +2677,22 @@ class Compiler(
|
||||
} else null
|
||||
|
||||
val markBeforeEq = cc.savePos()
|
||||
cc.skipWsTokens()
|
||||
val eqToken = cc.next()
|
||||
var setNull = false
|
||||
var isProperty = false
|
||||
|
||||
val declaringClassNameCaptured = (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.name
|
||||
|
||||
if (declaringClassNameCaptured != null) {
|
||||
if (declaringClassNameCaptured != null || extTypeName != null) {
|
||||
val mark = cc.savePos()
|
||||
cc.restorePos(markBeforeEq)
|
||||
cc.skipWsTokens()
|
||||
val next = cc.peekNextNonWhitespace()
|
||||
if (next.isId("get") || next.isId("set")) {
|
||||
isProperty = true
|
||||
cc.restorePos(markBeforeEq)
|
||||
cc.skipWsTokens()
|
||||
} else {
|
||||
cc.restorePos(mark)
|
||||
}
|
||||
@@ -2673,10 +2705,11 @@ class Compiler(
|
||||
true
|
||||
} else {
|
||||
if (!isProperty && eqToken.type != Token.Type.ASSIGN) {
|
||||
if (!isMutable && (declaringClassNameCaptured == null))
|
||||
if (!isMutable && (declaringClassNameCaptured == null) && (extTypeName == null))
|
||||
throw ScriptError(start, "val must be initialized")
|
||||
else {
|
||||
cc.restorePos(markBeforeEq)
|
||||
cc.skipWsTokens()
|
||||
setNull = true
|
||||
}
|
||||
}
|
||||
@@ -2698,7 +2731,8 @@ class Compiler(
|
||||
type = varTypeMini,
|
||||
initRange = initR,
|
||||
doc = pendingDeclDoc,
|
||||
nameStart = start
|
||||
nameStart = nameStartPos,
|
||||
receiver = receiverMini
|
||||
)
|
||||
miniSink?.onValDecl(node)
|
||||
pendingDeclDoc = null
|
||||
@@ -2722,7 +2756,7 @@ class Compiler(
|
||||
// Check for accessors if it is a class member
|
||||
var getter: Statement? = null
|
||||
var setter: Statement? = null
|
||||
if (declaringClassNameCaptured != null) {
|
||||
if (declaringClassNameCaptured != null || extTypeName != null) {
|
||||
while (true) {
|
||||
val t = cc.peekNextNonWhitespace()
|
||||
if (t.isId("get")) {
|
||||
@@ -2785,6 +2819,22 @@ class Compiler(
|
||||
}
|
||||
|
||||
return statement(start) { context ->
|
||||
if (extTypeName != null) {
|
||||
val prop = if (getter != null || setter != null) {
|
||||
ObjProperty(name, getter, setter)
|
||||
} else {
|
||||
// Simple val extension with initializer
|
||||
val initExpr = initialExpression ?: throw ScriptError(start, "Extension val must be initialized")
|
||||
ObjProperty(name, statement(initExpr.pos) { scp -> initExpr.execute(scp) }, null)
|
||||
}
|
||||
|
||||
val type = context[extTypeName]?.value ?: context.raiseSymbolNotFound("class $extTypeName not found")
|
||||
if (type !is ObjClass) context.raiseClassCastError("$extTypeName is not the class instance")
|
||||
|
||||
context.addExtension(type, name, ObjRecord(prop, isMutable = false, visibility = visibility, declaringClass = null, type = ObjRecord.Type.Property))
|
||||
|
||||
return@statement prop
|
||||
}
|
||||
// In true class bodies (not inside a function), store fields under a class-qualified key to support MI collisions
|
||||
// Do NOT infer declaring class from runtime thisObj here; only the compile-time captured
|
||||
// ClassBody qualifies for class-field storage. Otherwise, this is a plain local.
|
||||
|
||||
@@ -45,19 +45,36 @@ class ModuleScope(
|
||||
if (record.visibility.isPublic) {
|
||||
val newName = symbols?.let { ss: Map<String, String> ->
|
||||
ss[symbol]
|
||||
?.also { symbolsToImport!!.remove(it) }
|
||||
?: scope.raiseError("internal error: symbol $symbol not found though the module is cached")
|
||||
} ?: symbol
|
||||
val existing = scope.objects[newName]
|
||||
if (existing != null ) {
|
||||
if (existing.importedFrom != record.importedFrom)
|
||||
scope.raiseError("symbol ${existing.importedFrom?.packageName}.$newName already exists, redefinition on import is not allowed")
|
||||
// already imported
|
||||
?.also { symbolsToImport!!.remove(symbol) }
|
||||
?: return@let null
|
||||
} ?: if (symbols == null) symbol else null
|
||||
|
||||
if (newName != null) {
|
||||
val existing = scope.objects[newName]
|
||||
if (existing != null) {
|
||||
if (existing.importedFrom != record.importedFrom)
|
||||
scope.raiseError("symbol ${existing.importedFrom?.packageName}.$newName already exists, redefinition on import is not allowed")
|
||||
// already imported
|
||||
} else {
|
||||
// when importing records, we keep track of its package (not otherwise needed)
|
||||
if (record.importedFrom == null) record.importedFrom = this
|
||||
scope.objects[newName] = record
|
||||
}
|
||||
}
|
||||
else {
|
||||
// when importing records, we keep track of its package (not otherwise needed)
|
||||
if (record.importedFrom == null) record.importedFrom = this
|
||||
scope.objects[newName] = record
|
||||
}
|
||||
}
|
||||
for ((cls, map) in this.extensions) {
|
||||
for ((symbol, record) in map) {
|
||||
if (record.visibility.isPublic) {
|
||||
val newName = symbols?.let { ss: Map<String, String> ->
|
||||
ss[symbol]
|
||||
?.also { symbolsToImport!!.remove(symbol) }
|
||||
?: return@let null
|
||||
} ?: if (symbols == null) symbol else null
|
||||
|
||||
if (newName != null) {
|
||||
scope.addExtension(cls, newName, record)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,30 @@ open class Scope(
|
||||
*/
|
||||
internal val localBindings: MutableMap<String, ObjRecord> = mutableMapOf()
|
||||
|
||||
internal val extensions: MutableMap<ObjClass, MutableMap<String, ObjRecord>> = mutableMapOf()
|
||||
|
||||
fun addExtension(cls: ObjClass, name: String, record: ObjRecord) {
|
||||
extensions.getOrPut(cls) { mutableMapOf() }[name] = record
|
||||
}
|
||||
|
||||
internal fun findExtension(receiverClass: ObjClass, name: String): ObjRecord? {
|
||||
var s: Scope? = this
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) break
|
||||
// Proximity rule: check all extensions in the current scope before going to parent.
|
||||
// Priority within scope: more specific class in MRO wins.
|
||||
for (cls in receiverClass.mro) {
|
||||
s.extensions[cls]?.get(name)?.let { return it }
|
||||
}
|
||||
if (s is ClosureScope) {
|
||||
s.closureScope.findExtension(receiverClass, name)?.let { return it }
|
||||
}
|
||||
s = s.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** Debug helper: ensure assigning [candidateParent] does not create a structural cycle. */
|
||||
private fun ensureNoCycle(candidateParent: Scope?) {
|
||||
if (candidateParent == null) return
|
||||
@@ -82,10 +106,17 @@ open class Scope(
|
||||
* 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.
|
||||
*/
|
||||
private fun tryGetLocalRecord(s: Scope, name: String): ObjRecord? {
|
||||
s.objects[name]?.let { return it }
|
||||
s.localBindings[name]?.let { return it }
|
||||
s.getSlotIndexOf(name)?.let { return s.getSlotRecord(it) }
|
||||
private fun tryGetLocalRecord(s: Scope, name: String, caller: net.sergeych.lyng.obj.ObjClass?): ObjRecord? {
|
||||
s.objects[name]?.let { rec ->
|
||||
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec
|
||||
}
|
||||
s.localBindings[name]?.let { rec ->
|
||||
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec
|
||||
}
|
||||
s.getSlotIndexOf(name)?.let { idx ->
|
||||
val rec = s.getSlotRecord(idx)
|
||||
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -95,7 +126,7 @@ open class Scope(
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) return null
|
||||
tryGetLocalRecord(s, name)?.let { return it }
|
||||
tryGetLocalRecord(s, name, currentClassCtx)?.let { return it }
|
||||
s = s.parent
|
||||
}
|
||||
return null
|
||||
@@ -110,17 +141,22 @@ open class Scope(
|
||||
*/
|
||||
internal fun baseGetIgnoreClosure(name: String): ObjRecord? {
|
||||
// 1) locals/bindings in this frame
|
||||
tryGetLocalRecord(this, name)?.let { return it }
|
||||
tryGetLocalRecord(this, name, currentClassCtx)?.let { return it }
|
||||
// 2) walk parents for plain locals/bindings only
|
||||
var s = parent
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) return null
|
||||
tryGetLocalRecord(s, name)?.let { return it }
|
||||
tryGetLocalRecord(s, name, currentClassCtx)?.let { return it }
|
||||
s = s.parent
|
||||
}
|
||||
// 3) fallback to instance/class members of this frame's thisObj
|
||||
return thisObj.objClass.getInstanceMemberOrNull(name)
|
||||
for (cls in thisObj.objClass.mro) {
|
||||
this.extensions[cls]?.get(name)?.let { return it }
|
||||
}
|
||||
return thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec ->
|
||||
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) rec else null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,13 +166,18 @@ open class Scope(
|
||||
* This completely avoids invoking overridden `get` implementations, preventing
|
||||
* ping-pong recursion between `ClosureScope` frames.
|
||||
*/
|
||||
internal fun chainLookupWithMembers(name: String): ObjRecord? {
|
||||
internal fun chainLookupWithMembers(name: String, caller: net.sergeych.lyng.obj.ObjClass? = currentClassCtx): ObjRecord? {
|
||||
var s: Scope? = this
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) return null
|
||||
tryGetLocalRecord(s, name)?.let { return it }
|
||||
s.thisObj.objClass.getInstanceMemberOrNull(name)?.let { return it }
|
||||
tryGetLocalRecord(s, name, caller)?.let { return it }
|
||||
for (cls in s.thisObj.objClass.mro) {
|
||||
s.extensions[cls]?.get(name)?.let { return it }
|
||||
}
|
||||
s.thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec ->
|
||||
if (canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec
|
||||
}
|
||||
s = s.parent
|
||||
}
|
||||
return null
|
||||
@@ -152,6 +193,10 @@ open class Scope(
|
||||
// copy locals and bindings
|
||||
snap.objects.putAll(this.objects)
|
||||
snap.localBindings.putAll(this.localBindings)
|
||||
// copy extensions
|
||||
for ((cls, map) in extensions) {
|
||||
snap.extensions[cls] = map.toMutableMap()
|
||||
}
|
||||
// copy slots map preserving indices
|
||||
if (this.slotCount() > 0) {
|
||||
var i = 0
|
||||
@@ -273,13 +318,19 @@ open class Scope(
|
||||
if (name == "this") thisObj.asReadonly
|
||||
else {
|
||||
// Prefer direct locals/bindings declared in this frame
|
||||
(objects[name]
|
||||
(objects[name]?.let { rec ->
|
||||
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) rec else null
|
||||
}
|
||||
// Then, check known local bindings in this frame (helps after suspension)
|
||||
?: localBindings[name]
|
||||
?: 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)
|
||||
?: thisObj.objClass.getInstanceMemberOrNull(name)?.let { rec ->
|
||||
if (canAccessMember(rec.visibility, rec.declaringClass, currentClassCtx)) rec else null
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -317,6 +368,7 @@ open class Scope(
|
||||
slots.clear()
|
||||
nameToSlot.clear()
|
||||
localBindings.clear()
|
||||
extensions.clear()
|
||||
// Now safe to validate and re-parent
|
||||
ensureNoCycle(parent)
|
||||
this.parent = parent
|
||||
@@ -400,9 +452,10 @@ open class Scope(
|
||||
isMutable: Boolean,
|
||||
value: Obj,
|
||||
visibility: Visibility = Visibility.Public,
|
||||
recordType: ObjRecord.Type = ObjRecord.Type.Other
|
||||
recordType: ObjRecord.Type = ObjRecord.Type.Other,
|
||||
declaringClass: net.sergeych.lyng.obj.ObjClass? = currentClassCtx
|
||||
): ObjRecord {
|
||||
val rec = ObjRecord(value, isMutable, visibility, declaringClass = currentClassCtx, type = recordType)
|
||||
val rec = ObjRecord(value, isMutable, visibility, declaringClass = declaringClass, type = recordType)
|
||||
objects[name] = rec
|
||||
// Index this binding within the current frame to help resolve locals across suspension
|
||||
localBindings[name] = rec
|
||||
|
||||
@@ -26,7 +26,7 @@ enum class Visibility {
|
||||
|
||||
/** MI-aware visibility check: whether [caller] can access a member declared in [decl] with [visibility]. */
|
||||
fun canAccessMember(visibility: Visibility, decl: net.sergeych.lyng.obj.ObjClass?, caller: net.sergeych.lyng.obj.ObjClass?): Boolean {
|
||||
return when (visibility) {
|
||||
val res = when (visibility) {
|
||||
Visibility.Public -> true
|
||||
Visibility.Private -> (decl != null && caller === decl)
|
||||
Visibility.Protected -> when {
|
||||
@@ -36,4 +36,5 @@ fun canAccessMember(visibility: Visibility, decl: net.sergeych.lyng.obj.ObjClass
|
||||
else -> (caller.allParentsSet.contains(decl))
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -105,20 +105,24 @@ object BuiltinDocRegistry : BuiltinDocSource {
|
||||
}
|
||||
|
||||
/**
|
||||
* List names of extension-like methods defined for [className] in the stdlib text (`root.lyng`).
|
||||
* We do a lightweight regex scan like: `fun ClassName.methodName(` and collect distinct names.
|
||||
* List names of extension members defined for [className] in the stdlib text (`root.lyng`).
|
||||
* We do a lightweight regex scan like: `fun ClassName.methodName(` or `val ClassName.propName`
|
||||
* and collect distinct names.
|
||||
*/
|
||||
fun extensionMethodNamesFor(className: String): List<String> {
|
||||
fun extensionMemberNamesFor(className: String): List<String> {
|
||||
val src = try { rootLyng } catch (_: Throwable) { null } ?: return emptyList()
|
||||
val out = LinkedHashSet<String>()
|
||||
// Match lines like: fun String.trim(...)
|
||||
val re = Regex("(?m)^\\s*fun\\s+${className}\\.([A-Za-z_][A-Za-z0-9_]*)\\s*\\(")
|
||||
// Match lines like: fun String.trim(...) or val Int.isEven = ...
|
||||
val re = Regex("(?m)^\\s*(?:fun|val|var)\\s+${className}\\.([A-Za-z_][A-Za-z0-9_]*)\\b")
|
||||
re.findAll(src).forEach { m ->
|
||||
val name = m.groupValues.getOrNull(1)?.trim()
|
||||
if (!name.isNullOrEmpty()) out.add(name)
|
||||
}
|
||||
return out.toList()
|
||||
}
|
||||
|
||||
@Deprecated("Use extensionMemberNamesFor", ReplaceWith("extensionMemberNamesFor(className)"))
|
||||
fun extensionMethodNamesFor(className: String): List<String> = extensionMemberNamesFor(className)
|
||||
}
|
||||
|
||||
// ---------------- Builders ----------------
|
||||
@@ -367,8 +371,8 @@ private object StdlibInlineDocIndex {
|
||||
else -> {
|
||||
// Non-comment, non-blank: try to match a declaration just after comments
|
||||
if (buf.isNotEmpty()) {
|
||||
// fun Class.name( ... )
|
||||
val mExt = Regex("^fun\\s+([A-Za-z_][A-Za-z0-9_]*)\\.([A-Za-z_][A-Za-z0-9_]*)\\s*\\(").find(line)
|
||||
// fun/val/var Class.name( ... )
|
||||
val mExt = Regex("^(?:fun|val|var)\\s+([A-Za-z_][A-Za-z0-9_]*)\\.([A-Za-z_][A-Za-z0-9_]*)\\b").find(line)
|
||||
if (mExt != null) {
|
||||
val (cls, name) = mExt.destructured
|
||||
flushTo(Key.Method(cls, name))
|
||||
|
||||
@@ -208,10 +208,10 @@ object CompletionEngineLight {
|
||||
emitGroup(directMap)
|
||||
emitGroup(inheritedMap)
|
||||
|
||||
// Supplement with stdlib extension-like methods defined in root.lyng (e.g., fun String.re(...))
|
||||
// Supplement with stdlib extension members defined in root.lyng (e.g., fun String.re(...))
|
||||
run {
|
||||
val already = (directMap.keys + inheritedMap.keys).toMutableSet()
|
||||
val ext = BuiltinDocRegistry.extensionMethodNamesFor(className)
|
||||
val ext = BuiltinDocRegistry.extensionMemberNamesFor(className)
|
||||
for (name in ext) {
|
||||
if (already.contains(name)) continue
|
||||
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, className, name)
|
||||
|
||||
@@ -100,7 +100,7 @@ fun ObjClass.addFnDoc(
|
||||
code: suspend Scope.() -> Obj
|
||||
) {
|
||||
// Register runtime method
|
||||
addFn(name, isOpen, visibility, code)
|
||||
addFn(name, isOpen, visibility, code = code)
|
||||
// Register docs for the member under this class
|
||||
BuiltinDocRegistry.module(moduleName ?: ownerModuleNameFromClassOrUnknown()) {
|
||||
classDoc(this@addFnDoc.className, doc = "") {
|
||||
|
||||
@@ -109,7 +109,8 @@ data class MiniFunDecl(
|
||||
val returnType: MiniTypeRef?,
|
||||
val body: MiniBlock?,
|
||||
override val doc: MiniDoc?,
|
||||
override val nameStart: Pos
|
||||
override val nameStart: Pos,
|
||||
val receiver: MiniTypeRef? = null
|
||||
) : MiniDecl
|
||||
|
||||
data class MiniValDecl(
|
||||
@@ -119,7 +120,8 @@ data class MiniValDecl(
|
||||
val type: MiniTypeRef?,
|
||||
val initRange: MiniRange?,
|
||||
override val doc: MiniDoc?,
|
||||
override val nameStart: Pos
|
||||
override val nameStart: Pos,
|
||||
val receiver: MiniTypeRef? = null
|
||||
) : MiniDecl
|
||||
|
||||
data class MiniClassDecl(
|
||||
|
||||
@@ -85,21 +85,50 @@ open class Obj {
|
||||
args: Arguments = Arguments.EMPTY,
|
||||
onNotFoundResult: (suspend () -> Obj?)? = null
|
||||
): Obj {
|
||||
val rec = objClass.getInstanceMemberOrNull(name)
|
||||
if (rec != null) {
|
||||
val decl = rec.declaringClass ?: objClass.findDeclaringClassOf(name)
|
||||
val caller = scope.currentClassCtx
|
||||
if (!canAccessMember(rec.visibility, decl, caller))
|
||||
scope.raiseError(ObjAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})"))
|
||||
// Propagate declaring class as current class context during method execution
|
||||
val saved = scope.currentClassCtx
|
||||
scope.currentClassCtx = decl
|
||||
try {
|
||||
return rec.value.invoke(scope, this, args)
|
||||
} finally {
|
||||
scope.currentClassCtx = saved
|
||||
// 1. Hierarchy members (excluding root fallback)
|
||||
for (cls in objClass.mro) {
|
||||
if (cls.className == "Obj") break
|
||||
val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
|
||||
if (rec != null) {
|
||||
val decl = rec.declaringClass ?: cls
|
||||
val caller = scope.currentClassCtx
|
||||
if (!canAccessMember(rec.visibility, decl, caller))
|
||||
scope.raiseError(ObjAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})"))
|
||||
val saved = scope.currentClassCtx
|
||||
scope.currentClassCtx = decl
|
||||
try {
|
||||
return rec.value.invoke(scope, this, args)
|
||||
} finally {
|
||||
scope.currentClassCtx = saved
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Extensions in scope
|
||||
val extension = scope.findExtension(objClass, name)
|
||||
if (extension != null) {
|
||||
return extension.value.invoke(scope, this, args)
|
||||
}
|
||||
|
||||
// 3. Root object fallback
|
||||
for (cls in objClass.mro) {
|
||||
if (cls.className == "Obj") {
|
||||
cls.members[name]?.let { rec ->
|
||||
val decl = rec.declaringClass ?: cls
|
||||
val caller = scope.currentClassCtx
|
||||
if (!canAccessMember(rec.visibility, decl, caller))
|
||||
scope.raiseError(ObjAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})"))
|
||||
val saved = scope.currentClassCtx
|
||||
scope.currentClassCtx = decl
|
||||
try {
|
||||
return rec.value.invoke(scope, this, args)
|
||||
} finally {
|
||||
scope.currentClassCtx = saved
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return onNotFoundResult?.invoke()
|
||||
?: scope.raiseError(
|
||||
"no such member: $name on ${objClass.className}. Considered order: ${objClass.renderLinearization(true)}. " +
|
||||
@@ -313,34 +342,83 @@ open class Obj {
|
||||
// suspend fun <T> sync(block: () -> T): T = monitor.withLock { block() }
|
||||
|
||||
open suspend fun readField(scope: Scope, name: String): ObjRecord {
|
||||
// could be property or class field:
|
||||
val obj = objClass.getInstanceMemberOrNull(name) ?: scope.raiseError(
|
||||
// 1. Hierarchy members (excluding root fallback)
|
||||
for (cls in objClass.mro) {
|
||||
if (cls.className == "Obj") break
|
||||
cls.members[name]?.let { return resolveRecord(scope, it, name, it.declaringClass) }
|
||||
cls.classScope?.objects?.get(name)?.let { return resolveRecord(scope, it, name, it.declaringClass) }
|
||||
}
|
||||
|
||||
// 2. Extensions
|
||||
val extension = scope.findExtension(objClass, name)
|
||||
if (extension != null) {
|
||||
return resolveRecord(scope, extension, name, extension.declaringClass)
|
||||
}
|
||||
|
||||
// 3. Root fallback
|
||||
for (cls in objClass.mro) {
|
||||
if (cls.className == "Obj") {
|
||||
cls.members[name]?.let {
|
||||
val decl = it.declaringClass ?: cls
|
||||
return resolveRecord(scope, it, name, decl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.raiseError(
|
||||
"no such field: $name on ${objClass.className}. Considered order: ${objClass.renderLinearization(true)}"
|
||||
)
|
||||
val decl = obj.declaringClass ?: objClass.findDeclaringClassOf(name)
|
||||
}
|
||||
|
||||
protected suspend fun resolveRecord(scope: Scope, obj: ObjRecord, name: String, decl: ObjClass?): ObjRecord {
|
||||
val value = obj.value
|
||||
if (value is ObjProperty) {
|
||||
return ObjRecord(value.callGetter(scope, this, decl), obj.isMutable)
|
||||
}
|
||||
if (value is Statement && decl != null) {
|
||||
return ObjRecord(value.execute(scope.createChildScope(scope.pos, newThisObj = this)), obj.isMutable)
|
||||
}
|
||||
val caller = scope.currentClassCtx
|
||||
// Check visibility for non-property members here if they weren't checked before
|
||||
if (!canAccessMember(obj.visibility, decl, caller))
|
||||
scope.raiseError(ObjAccessException(scope, "can't access field ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})"))
|
||||
return when (val value = obj.value) {
|
||||
is Statement -> {
|
||||
ObjRecord(value.execute(scope.createChildScope(scope.pos, newThisObj = this)), obj.isMutable)
|
||||
}
|
||||
// could be writable property naturally
|
||||
// null -> ObjNull.asReadonly
|
||||
else -> obj
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
open suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
||||
willMutate(scope)
|
||||
val field = objClass.getInstanceMemberOrNull(name) ?: scope.raiseError(
|
||||
var field: ObjRecord? = null
|
||||
// 1. Hierarchy members (excluding root fallback)
|
||||
for (cls in objClass.mro) {
|
||||
if (cls.className == "Obj") break
|
||||
field = cls.members[name] ?: cls.classScope?.objects?.get(name)
|
||||
if (field != null) break
|
||||
}
|
||||
// 2. Extensions
|
||||
if (field == null) {
|
||||
field = scope.findExtension(objClass, name)
|
||||
}
|
||||
// 3. Root fallback
|
||||
if (field == null) {
|
||||
for (cls in objClass.mro) {
|
||||
if (cls.className == "Obj") {
|
||||
field = cls.members[name]
|
||||
if (field != null) break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (field == null) scope.raiseError(
|
||||
"no such field: $name on ${objClass.className}. Considered order: ${objClass.renderLinearization(true)}"
|
||||
)
|
||||
val decl = field.declaringClass ?: objClass.findDeclaringClassOf(name)
|
||||
|
||||
val decl = field.declaringClass
|
||||
val caller = scope.currentClassCtx
|
||||
if (!canAccessMember(field.visibility, decl, caller))
|
||||
scope.raiseError(ObjAccessException(scope, "can't assign field ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})"))
|
||||
if (field.isMutable) field.value = newValue else scope.raiseError("can't assign to read-only field: $name")
|
||||
if (field.value is ObjProperty) {
|
||||
(field.value as ObjProperty).callSetter(scope, this, newValue, decl)
|
||||
} else if (field.isMutable) field.value = newValue else scope.raiseError("can't assign to read-only field: $name")
|
||||
}
|
||||
|
||||
open suspend fun getAt(scope: Scope, index: Obj): Obj {
|
||||
|
||||
@@ -53,6 +53,12 @@ class ObjChar(val value: Char): Obj() {
|
||||
returns = type("lyng.Int"),
|
||||
moduleName = "lyng.stdlib"
|
||||
) { ObjInt(thisAs<ObjChar>().value.code.toLong()) }
|
||||
addFn("isDigit") {
|
||||
thisAs<ObjChar>().value.isDigit().toObj()
|
||||
}
|
||||
addFn("isSpace") {
|
||||
thisAs<ObjChar>().value.isWhitespace().toObj()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,7 +175,11 @@ open class ObjClass(
|
||||
}
|
||||
|
||||
/** Full C3 MRO including this class at index 0. */
|
||||
val mro: List<ObjClass> by lazy { c3Linearize(this, mutableMapOf()) }
|
||||
val mro: List<ObjClass> by lazy {
|
||||
val base = c3Linearize(this, mutableMapOf())
|
||||
if (this.className == "Obj" || base.any { it.className == "Obj" }) base
|
||||
else base + rootObjectType
|
||||
}
|
||||
|
||||
/** Parents in C3 order (no self). */
|
||||
val mroParents: List<ObjClass> by lazy { mro.drop(1) }
|
||||
@@ -216,6 +220,7 @@ open class ObjClass(
|
||||
// remains stable even when call frames are pooled and reused.
|
||||
val stableParent = classScope ?: scope.parent
|
||||
instance.instanceScope = Scope(stableParent, scope.args, scope.pos, instance)
|
||||
instance.instanceScope.currentClassCtx = null
|
||||
// Expose instance methods (and other callable members) directly in the instance scope for fast lookup
|
||||
// This mirrors Obj.autoInstanceScope behavior for ad-hoc scopes and makes fb.method() resolution robust
|
||||
// 1) members-defined methods
|
||||
@@ -268,7 +273,7 @@ open class ObjClass(
|
||||
c.constructorMeta?.let { meta ->
|
||||
val argsHere = argsForThis ?: Arguments.EMPTY
|
||||
// Assign constructor params into instance scope (unmangled)
|
||||
meta.assignToContext(instance.instanceScope, argsHere)
|
||||
meta.assignToContext(instance.instanceScope, argsHere, declaringClass = c)
|
||||
// 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.
|
||||
for (p in meta.params) {
|
||||
@@ -297,7 +302,7 @@ open class ObjClass(
|
||||
// parameters even if they were shadowed/overwritten by parent class initialization.
|
||||
c.constructorMeta?.let { meta ->
|
||||
val argsHere = argsForThis ?: Arguments.EMPTY
|
||||
meta.assignToContext(instance.instanceScope, argsHere)
|
||||
meta.assignToContext(instance.instanceScope, argsHere, declaringClass = c)
|
||||
// Re-sync mangled names to point to the fresh records to keep them consistent
|
||||
for (p in meta.params) {
|
||||
val rec = instance.instanceScope.objects[p.name]
|
||||
@@ -340,14 +345,15 @@ open class ObjClass(
|
||||
initialValue: Obj,
|
||||
isMutable: Boolean = false,
|
||||
visibility: Visibility = Visibility.Public,
|
||||
pos: Pos = Pos.builtIn
|
||||
pos: Pos = Pos.builtIn,
|
||||
declaringClass: ObjClass? = this
|
||||
) {
|
||||
// Allow overriding ancestors: only prevent redefinition if THIS class already defines an immutable member
|
||||
val existingInSelf = members[name]
|
||||
if (existingInSelf != null && existingInSelf.isMutable == false)
|
||||
throw ScriptError(pos, "$name is already defined in $objClass")
|
||||
// Install/override in this class
|
||||
members[name] = ObjRecord(initialValue, isMutable, visibility, declaringClass = this)
|
||||
members[name] = ObjRecord(initialValue, isMutable, visibility, declaringClass = declaringClass)
|
||||
// Structural change: bump layout version for PIC invalidation
|
||||
layoutVersion += 1
|
||||
}
|
||||
@@ -377,10 +383,11 @@ open class ObjClass(
|
||||
name: String,
|
||||
isOpen: Boolean = false,
|
||||
visibility: Visibility = Visibility.Public,
|
||||
declaringClass: ObjClass? = this,
|
||||
code: suspend Scope.() -> Obj
|
||||
) {
|
||||
val stmt = statement { code() }
|
||||
createField(name, stmt, isOpen, visibility)
|
||||
createField(name, stmt, isOpen, visibility, Pos.builtIn, declaringClass)
|
||||
}
|
||||
|
||||
fun addConst(name: String, value: Obj) = createField(name, value, isMutable = false)
|
||||
|
||||
@@ -33,9 +33,10 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
override suspend fun readField(scope: Scope, name: String): ObjRecord {
|
||||
// Direct (unmangled) lookup first
|
||||
instanceScope[name]?.let { rec ->
|
||||
val decl = rec.declaringClass ?: objClass.findDeclaringClassOf(name)
|
||||
val decl = rec.declaringClass
|
||||
// Allow unconditional access when accessing through `this` of the same instance
|
||||
if (scope.thisObj !== this) {
|
||||
// BUT only if we are in the class context (not extension)
|
||||
if (scope.thisObj !== this || scope.currentClassCtx == null) {
|
||||
val caller = scope.currentClassCtx
|
||||
if (!canAccessMember(rec.visibility, decl, caller))
|
||||
scope.raiseError(
|
||||
@@ -47,7 +48,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
}
|
||||
if (rec.type == ObjRecord.Type.Property) {
|
||||
val prop = rec.value as ObjProperty
|
||||
return rec.copy(value = prop.callGetter(scope, this))
|
||||
return rec.copy(value = prop.callGetter(scope, this, decl))
|
||||
}
|
||||
return rec
|
||||
}
|
||||
@@ -70,7 +71,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
instanceScope.objects.containsKey("${cls.className}::$name") -> cls
|
||||
else -> cls.mroParents.firstOrNull { instanceScope.objects.containsKey("${it.className}::$name") }
|
||||
}
|
||||
if (scope.thisObj !== this) {
|
||||
if (scope.thisObj !== this || scope.currentClassCtx == null) {
|
||||
val caller = scope.currentClassCtx
|
||||
if (!canAccessMember(rec.visibility, declaring, caller))
|
||||
scope.raiseError(
|
||||
@@ -82,7 +83,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
}
|
||||
if (rec.type == ObjRecord.Type.Property) {
|
||||
val prop = rec.value as ObjProperty
|
||||
return rec.copy(value = prop.callGetter(scope, this))
|
||||
return rec.copy(value = prop.callGetter(scope, this, declaring))
|
||||
}
|
||||
return rec
|
||||
}
|
||||
@@ -93,8 +94,8 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
||||
// Direct (unmangled) first
|
||||
instanceScope[name]?.let { f ->
|
||||
val decl = f.declaringClass ?: objClass.findDeclaringClassOf(name)
|
||||
if (scope.thisObj !== this) {
|
||||
val decl = f.declaringClass
|
||||
if (scope.thisObj !== this || scope.currentClassCtx == null) {
|
||||
val caller = scope.currentClassCtx
|
||||
if (!canAccessMember(f.visibility, decl, caller))
|
||||
ObjIllegalAssignmentException(
|
||||
@@ -104,7 +105,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
}
|
||||
if (f.type == ObjRecord.Type.Property) {
|
||||
val prop = f.value as ObjProperty
|
||||
prop.callSetter(scope, this, newValue)
|
||||
prop.callSetter(scope, this, newValue, decl)
|
||||
return
|
||||
}
|
||||
if (!f.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
||||
@@ -128,7 +129,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
instanceScope.objects.containsKey("${cls.className}::$name") -> cls
|
||||
else -> cls.mroParents.firstOrNull { instanceScope.objects.containsKey("${it.className}::$name") }
|
||||
}
|
||||
if (scope.thisObj !== this) {
|
||||
if (scope.thisObj !== this || scope.currentClassCtx == null) {
|
||||
val caller = scope.currentClassCtx
|
||||
if (!canAccessMember(rec.visibility, declaring, caller))
|
||||
ObjIllegalAssignmentException(
|
||||
@@ -138,7 +139,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
}
|
||||
if (rec.type == ObjRecord.Type.Property) {
|
||||
val prop = rec.value as ObjProperty
|
||||
prop.callSetter(scope, this, newValue)
|
||||
prop.callSetter(scope, this, newValue, declaring)
|
||||
return
|
||||
}
|
||||
if (!rec.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise()
|
||||
@@ -154,7 +155,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
onNotFoundResult: (suspend () -> Obj?)?
|
||||
): Obj =
|
||||
instanceScope[name]?.let { rec ->
|
||||
val decl = rec.declaringClass ?: objClass.findDeclaringClassOf(name)
|
||||
val decl = rec.declaringClass
|
||||
val caller = scope.currentClassCtx ?: if (scope.thisObj === this) objClass else null
|
||||
if (!canAccessMember(rec.visibility, decl, caller))
|
||||
scope.raiseError(
|
||||
@@ -163,23 +164,16 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
"can't invoke method $name (declared in ${decl?.className ?: "?"})"
|
||||
)
|
||||
)
|
||||
// execute with lexical class context propagated to declaring class
|
||||
val saved = instanceScope.currentClassCtx
|
||||
instanceScope.currentClassCtx = decl
|
||||
try {
|
||||
rec.value.invoke(
|
||||
instanceScope,
|
||||
this,
|
||||
args
|
||||
)
|
||||
} finally {
|
||||
instanceScope.currentClassCtx = saved
|
||||
}
|
||||
rec.value.invoke(
|
||||
instanceScope,
|
||||
this,
|
||||
args
|
||||
)
|
||||
}
|
||||
?: run {
|
||||
// fallback: class-scope function (registered during class body execution)
|
||||
objClass.classScope?.objects?.get(name)?.let { rec ->
|
||||
val decl = rec.declaringClass ?: objClass.findDeclaringClassOf(name)
|
||||
val decl = rec.declaringClass
|
||||
val caller = scope.currentClassCtx ?: if (scope.thisObj === this) objClass else null
|
||||
if (!canAccessMember(rec.visibility, decl, caller))
|
||||
scope.raiseError(
|
||||
@@ -188,13 +182,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
|
||||
"can't invoke method $name (declared in ${decl?.className ?: "?"})"
|
||||
)
|
||||
)
|
||||
val saved = instanceScope.currentClassCtx
|
||||
instanceScope.currentClassCtx = decl
|
||||
try {
|
||||
rec.value.invoke(instanceScope, this, args)
|
||||
} finally {
|
||||
instanceScope.currentClassCtx = saved
|
||||
}
|
||||
rec.value.invoke(instanceScope, this, args)
|
||||
}
|
||||
}
|
||||
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng.obj
|
||||
|
||||
import net.sergeych.lyng.Arguments
|
||||
import net.sergeych.lyng.ClosureScope
|
||||
import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.Statement
|
||||
|
||||
@@ -14,16 +32,24 @@ class ObjProperty(
|
||||
val setter: Statement?
|
||||
) : Obj() {
|
||||
|
||||
suspend fun callGetter(scope: Scope, instance: ObjInstance): Obj {
|
||||
suspend fun callGetter(scope: Scope, instance: Obj, declaringClass: ObjClass? = null): Obj {
|
||||
val g = getter ?: scope.raiseError("property $name has no getter")
|
||||
// Execute getter in a child scope of the instance with 'this' properly set
|
||||
return g.execute(instance.instanceScope.createChildScope(newThisObj = instance))
|
||||
// Use ClosureScope to match extension function behavior (access to instance scope + call scope)
|
||||
val instanceScope = (instance as? ObjInstance)?.instanceScope ?: instance.autoInstanceScope(scope)
|
||||
val execScope = ClosureScope(scope, instanceScope).createChildScope(newThisObj = instance)
|
||||
execScope.currentClassCtx = declaringClass
|
||||
return g.execute(execScope)
|
||||
}
|
||||
|
||||
suspend fun callSetter(scope: Scope, instance: ObjInstance, value: Obj) {
|
||||
suspend fun callSetter(scope: Scope, instance: Obj, value: Obj, declaringClass: ObjClass? = null) {
|
||||
val s = setter ?: scope.raiseError("property $name has no setter")
|
||||
// Execute setter in a child scope of the instance with 'this' properly set and the value as an argument
|
||||
s.execute(instance.instanceScope.createChildScope(args = Arguments(value), newThisObj = instance))
|
||||
// Use ClosureScope to match extension function behavior
|
||||
val instanceScope = (instance as? ObjInstance)?.instanceScope ?: instance.autoInstanceScope(scope)
|
||||
val execScope = ClosureScope(scope, instanceScope).createChildScope(args = Arguments(value), newThisObj = instance)
|
||||
execScope.currentClassCtx = declaringClass
|
||||
s.execute(execScope)
|
||||
}
|
||||
|
||||
override fun toString(): String = "Property($name)"
|
||||
|
||||
@@ -1178,18 +1178,25 @@ class MethodCallRef(
|
||||
when (base) {
|
||||
is ObjInstance -> {
|
||||
// Prefer resolved class member to avoid per-call lookup on hit
|
||||
val member = base.objClass.getInstanceMemberOrNull(name)
|
||||
if (member != null) {
|
||||
val visibility = member.visibility
|
||||
val callable = member.value
|
||||
// BUT only if it's NOT a root object member (which can be shadowed by extensions)
|
||||
var hierarchyMember: ObjRecord? = null
|
||||
for (cls in base.objClass.mro) {
|
||||
if (cls.className == "Obj") break
|
||||
hierarchyMember = cls.members[name] ?: cls.classScope?.objects?.get(name)
|
||||
if (hierarchyMember != null) break
|
||||
}
|
||||
|
||||
if (hierarchyMember != null) {
|
||||
val visibility = hierarchyMember.visibility
|
||||
val callable = hierarchyMember.value
|
||||
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a ->
|
||||
val inst = obj as ObjInstance
|
||||
if (!visibility.isPublic)
|
||||
if (!visibility.isPublic && !canAccessMember(visibility, hierarchyMember.declaringClass ?: inst.objClass, sc.currentClassCtx))
|
||||
sc.raiseError(ObjAccessException(sc, "can't invoke non-public method $name"))
|
||||
callable.invoke(inst.instanceScope, inst, a)
|
||||
}
|
||||
} else {
|
||||
// Fallback to name-based lookup per call (uncommon)
|
||||
// Fallback to name-based lookup per call (handles extensions and root members)
|
||||
mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a -> obj.invokeInstanceMethod(sc, name, a) }
|
||||
}
|
||||
}
|
||||
@@ -1281,10 +1288,15 @@ class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef {
|
||||
val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
|
||||
val slot = if (hit) cachedSlot else resolveSlot(scope)
|
||||
if (slot >= 0) {
|
||||
if (PerfFlags.PIC_DEBUG_COUNTERS) {
|
||||
if (hit) PerfStats.localVarPicHit++ else PerfStats.localVarPicMiss++
|
||||
val rec = scope.getSlotRecord(slot)
|
||||
if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) {
|
||||
// Not visible via slot, fallback to other lookups
|
||||
} else {
|
||||
if (PerfFlags.PIC_DEBUG_COUNTERS) {
|
||||
if (hit) PerfStats.localVarPicHit++ else PerfStats.localVarPicMiss++
|
||||
}
|
||||
return rec
|
||||
}
|
||||
return scope.getSlotRecord(slot)
|
||||
}
|
||||
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.localVarPicMiss++
|
||||
// 2) Fallback name in scope or field on `this`
|
||||
@@ -1336,7 +1348,11 @@ class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef {
|
||||
}
|
||||
val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
|
||||
val slot = if (hit) cachedSlot else resolveSlot(scope)
|
||||
if (slot >= 0) return scope.getSlotRecord(slot).value
|
||||
if (slot >= 0) {
|
||||
val rec = scope.getSlotRecord(slot)
|
||||
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx))
|
||||
return rec.value
|
||||
}
|
||||
// Fallback name in scope or field on `this`
|
||||
scope[name]?.let { return it.value }
|
||||
run {
|
||||
@@ -1400,9 +1416,11 @@ class LocalVarRef(val name: String, private val atPos: Pos) : ObjRef {
|
||||
val slot = if (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount()) cachedSlot else resolveSlot(scope)
|
||||
if (slot >= 0) {
|
||||
val rec = scope.getSlotRecord(slot)
|
||||
if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value")
|
||||
rec.value = newValue
|
||||
return
|
||||
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) {
|
||||
if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value")
|
||||
rec.value = newValue
|
||||
return
|
||||
}
|
||||
}
|
||||
scope[name]?.let { stored ->
|
||||
if (stored.isMutable) stored.value = newValue
|
||||
@@ -1444,17 +1462,25 @@ class BoundLocalVarRef(
|
||||
) : ObjRef {
|
||||
override suspend fun get(scope: Scope): ObjRecord {
|
||||
scope.pos = atPos
|
||||
return scope.getSlotRecord(slot)
|
||||
val rec = scope.getSlotRecord(slot)
|
||||
if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx))
|
||||
scope.raiseError(ObjAccessException(scope, "private field access"))
|
||||
return rec
|
||||
}
|
||||
|
||||
override suspend fun evalValue(scope: Scope): Obj {
|
||||
scope.pos = atPos
|
||||
return scope.getSlotRecord(slot).value
|
||||
val rec = scope.getSlotRecord(slot)
|
||||
if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx))
|
||||
scope.raiseError(ObjAccessException(scope, "private field access"))
|
||||
return rec.value
|
||||
}
|
||||
|
||||
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
|
||||
scope.pos = atPos
|
||||
val rec = scope.getSlotRecord(slot)
|
||||
if (rec.declaringClass != null && !canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx))
|
||||
scope.raiseError(ObjAccessException(scope, "private field access"))
|
||||
if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value")
|
||||
rec.value = newValue
|
||||
}
|
||||
@@ -1518,10 +1544,13 @@ class FastLocalVarRef(
|
||||
val slot = if (ownerValid && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope)
|
||||
val actualOwner = cachedOwnerScope
|
||||
if (slot >= 0 && actualOwner != null) {
|
||||
if (PerfFlags.PIC_DEBUG_COUNTERS) {
|
||||
if (ownerValid) PerfStats.fastLocalHit++ else PerfStats.fastLocalMiss++
|
||||
val rec = actualOwner.getSlotRecord(slot)
|
||||
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) {
|
||||
if (PerfFlags.PIC_DEBUG_COUNTERS) {
|
||||
if (ownerValid) PerfStats.fastLocalHit++ else PerfStats.fastLocalMiss++
|
||||
}
|
||||
return rec
|
||||
}
|
||||
return actualOwner.getSlotRecord(slot)
|
||||
}
|
||||
// Try per-frame local binding maps in the ancestry first (locals declared in frames)
|
||||
run {
|
||||
@@ -1558,7 +1587,11 @@ class FastLocalVarRef(
|
||||
val ownerValid = isOwnerValidFor(scope)
|
||||
val slot = if (ownerValid && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope)
|
||||
val actualOwner = cachedOwnerScope
|
||||
if (slot >= 0 && actualOwner != null) return actualOwner.getSlotRecord(slot).value
|
||||
if (slot >= 0 && actualOwner != null) {
|
||||
val rec = actualOwner.getSlotRecord(slot)
|
||||
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx))
|
||||
return rec.value
|
||||
}
|
||||
// Try per-frame local binding maps in the ancestry first
|
||||
run {
|
||||
var s: Scope? = scope
|
||||
@@ -1595,9 +1628,11 @@ class FastLocalVarRef(
|
||||
val actualOwner = cachedOwnerScope
|
||||
if (slot >= 0 && actualOwner != null) {
|
||||
val rec = actualOwner.getSlotRecord(slot)
|
||||
if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value")
|
||||
rec.value = newValue
|
||||
return
|
||||
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) {
|
||||
if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value")
|
||||
rec.value = newValue
|
||||
return
|
||||
}
|
||||
}
|
||||
// Try per-frame local binding maps in the ancestry first
|
||||
run {
|
||||
|
||||
@@ -122,6 +122,12 @@ class ObjRingBuffer(val capacity: Int) : Obj() {
|
||||
returns = type("lyng.Void"),
|
||||
moduleName = "lyng.stdlib"
|
||||
) { thisAs<ObjRingBuffer>().apply { buffer.add(requireOnlyArg<Obj>()) } }
|
||||
addFnDoc(
|
||||
name = "first",
|
||||
doc = "Return the oldest element in the buffer.",
|
||||
returns = type("lyng.Any"),
|
||||
moduleName = "lyng.stdlib"
|
||||
) { thisAs<ObjRingBuffer>().buffer.first() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import net.sergeych.lyng.obj.ObjInstance
|
||||
import net.sergeych.lyng.obj.ObjList
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFails
|
||||
|
||||
class OOTest {
|
||||
@Test
|
||||
@@ -343,4 +344,56 @@ class OOTest {
|
||||
val b2 = list.list[1] as ObjInstance
|
||||
assertEquals(0, b1.compareTo(scope, b2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPropAsExtension() = runTest {
|
||||
val scope = Script.newScope()
|
||||
scope.eval("""
|
||||
class A(x) {
|
||||
private val privateVal = 100
|
||||
val p1 get() = x + 1
|
||||
}
|
||||
assertEquals(2, A(1).p1)
|
||||
|
||||
fun A.f() = x + 5
|
||||
assertEquals(7, A(2).f())
|
||||
|
||||
// The same, we should be able to add member values to a class;
|
||||
// notice it should access to the class public instance members,
|
||||
// somewhat like it is declared in the class body
|
||||
val A.simple = x + 3
|
||||
|
||||
assertEquals(5, A(2).simple)
|
||||
|
||||
// it should also work with properties:
|
||||
val A.p10 get() = x * 10
|
||||
assertEquals(20, A(2).p10)
|
||||
""".trimIndent())
|
||||
|
||||
// important is that such extensions should not be able to access private members
|
||||
// and thus remove privateness:
|
||||
assertFails {
|
||||
scope.eval("val A.exportPrivateVal = privateVal; A(1).exportPrivateVal")
|
||||
}
|
||||
assertFails {
|
||||
scope.eval("val A.exportPrivateValProp get() = privateVal; A(1).exportPrivateValProp")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExtensionsAreScopeIsolated() = runTest {
|
||||
val scope1 = Script.newScope()
|
||||
scope1.eval("""
|
||||
val String.totalDigits get() {
|
||||
// notice using `this`:
|
||||
this.characters.filter{ it.isDigit() }.size()
|
||||
}
|
||||
assertEquals(2, "answer is 42".totalDigits)
|
||||
""")
|
||||
val scope2 = Script.newScope()
|
||||
scope2.eval("""
|
||||
// in scope2 we didn't override `totalDigits` extension:
|
||||
assertThrows { "answer is 42".totalDigits }
|
||||
""".trimIndent())
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ fun cached(builder) {
|
||||
}
|
||||
}
|
||||
/* Filter elements of this iterable using the provided predicate. */
|
||||
fun Iterable.filter(predicate) {
|
||||
fun Iterable.filterFlow(predicate) {
|
||||
val list = this
|
||||
flow {
|
||||
for( item in list ) {
|
||||
@@ -27,10 +27,20 @@ fun Iterable.filter(predicate) {
|
||||
}
|
||||
}
|
||||
|
||||
fun Iterable.filter(predicate) {
|
||||
val result = []
|
||||
for( item in this ) if( predicate(item) ) result.add(item)
|
||||
result
|
||||
}
|
||||
|
||||
/*
|
||||
filter out all null elements from this collection (Iterable); collection of
|
||||
non-null elements is returned
|
||||
*/
|
||||
fun Iterable.filterFlowNotNull() {
|
||||
filterFlow { it != null }
|
||||
}
|
||||
|
||||
fun Iterable.filterNotNull() {
|
||||
filter { it != null }
|
||||
}
|
||||
@@ -42,10 +52,10 @@ fun Iterable.drop(n) {
|
||||
}
|
||||
|
||||
/* Return the first element or throw if the iterable is empty. */
|
||||
fun Iterable.first() {
|
||||
val Iterable.first get() {
|
||||
val i = iterator()
|
||||
if( !i.hasNext() ) throw NoSuchElementException()
|
||||
i.next().also { i.cancelIteration() }
|
||||
i.next().also { i.cancelIteration() }
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -73,7 +83,7 @@ fun Iterable.findFirstOrNull(predicate) {
|
||||
|
||||
|
||||
/* Return the last element or throw if the iterable is empty. */
|
||||
fun Iterable.last() {
|
||||
val Iterable.last get() {
|
||||
var found = false
|
||||
var element = null
|
||||
for( i in this ) {
|
||||
@@ -81,7 +91,7 @@ fun Iterable.last() {
|
||||
found = true
|
||||
}
|
||||
if( !found ) throw NoSuchElementException()
|
||||
element
|
||||
element
|
||||
}
|
||||
|
||||
/* Emit all but the last N elements of this iterable. */
|
||||
@@ -247,5 +257,5 @@ fun Exception.printStackTrace() {
|
||||
}
|
||||
|
||||
/* Compile this string into a regular expression. */
|
||||
fun String.re(): Regex { Regex(this) }
|
||||
val String.re get() = Regex(this)
|
||||
|
||||
Reference in new issue
Block a user