Extensions methods and properties now are correctly isolated, respect visibility rules and allow adding class properties and class vals.

This commit is contained in:
Sergey Chernov 2025-12-24 00:29:10 +01:00
parent 3b6504d3b1
commit cd2b1a9cb7
26 changed files with 606 additions and 163 deletions

View File

@ -98,7 +98,17 @@ esac
die() { echo "ERROR: $*" 1>&2 ; exit 1; } die() { echo "ERROR: $*" 1>&2 ; exit 1; }
function refreshTextmateZip() {
echo "Refreshing distributables/lyng-textmate.zip from editors/..."
mkdir -p distributables
# We use -r for recursive and -q for quiet (optional)
# -j can be used if we want to junk paths, but the request says "contents of editors/"
# usually we want to preserve the structure inside editors/
(cd editors && zip -rq ../distributables/lyng-textmate.zip .)
}
# Update the IDEA plugin download link in docs (temporarily), then build, then restore the doc # Update the IDEA plugin download link in docs (temporarily), then build, then restore the doc
refreshTextmateZip
updateIdeaPluginDownloadLink || echo "WARN: proceeding without updating IDEA plugin download link" updateIdeaPluginDownloadLink || echo "WARN: proceeding without updating IDEA plugin download link"
./gradlew site:jsBrowserDistribution ./gradlew site:jsBrowserDistribution

View File

@ -491,9 +491,11 @@ As usual, private statics are not accessible from the outside:
# Extending classes # Extending classes
It sometimes happen that the class is missing some particular functionality that can be _added to it_ without rewriting its inner logic and using its private state. In this case _extension methods_ could be used, for example. we want to create an extension method It sometimes happen that the class is missing some particular functionality that can be _added to it_ without rewriting its inner logic and using its private state. In this case _extension members_ could be used.
that would test if some object of unknown type contains something that can be interpreted
as an integer. In this case we _extend_ class `Object`, as it is the parent class for any instance of any type: ## Extension methods
For example, we want to create an extension method that would test if some object of unknown type contains something that can be interpreted as an integer. In this case we _extend_ class `Object`, as it is the parent class for any instance of any type:
fun Object.isInteger() { fun Object.isInteger() {
when(this) { when(this) {
@ -518,10 +520,67 @@ as an integer. In this case we _extend_ class `Object`, as it is the parent clas
assert( ! "5.2".isInteger() ) assert( ! "5.2".isInteger() )
>>> void >>> void
__Important note__ as for version 0.6.9, extensions are in __global scope__. It means, that once applied to a global type (Int in our sample), they will be available for _all_ contexts, even new created, ## Extension properties
as they are modifying the type, not the context.
Beware of it. We might need to reconsider it later. Just like methods, you can extend existing classes with properties. These can be defined using simple initialization (for `val` only) or with custom accessors.
### Simple val extension
A read-only extension can be defined by assigning an expression:
```kotlin
val String.isLong = length > 10
val s = "Hello, world!"
assert(s.isLong)
```
### Properties with accessors
For more complex logic, use `get()` and `set()` blocks:
```kotlin
class Box(var value: Int)
var Box.doubledValue
get() = value * 2
set(v) = value = v / 2
val b = Box(10)
assertEquals(20, b.doubledValue)
b.doubledValue = 30
assertEquals(15, b.value)
```
Extension members are strictly barred from accessing private members of the class they extend, maintaining encapsulation.
### Extension Scoping and Isolation
Extensions in Lyng are **scope-isolated**. This means an extension is only visible within the scope where it is defined and its child scopes. This reduces the "attack surface" and prevents extensions from polluting the global space or other modules.
#### Scope Isolation Example
You can define different extensions with the same name in different scopes:
```kotlin
fun scopeA() {
val Int.description = "Number: " + toString()
assertEquals("Number: 42", 42.description)
}
fun scopeB() {
val Int.description = "Value: " + toString()
assertEquals("Value: 42", 42.description)
}
scopeA()
scopeB()
// Outside those scopes, Int.description is not defined
assertThrows { 42.description }
```
This isolation ensures that libraries can use extensions internally without worrying about name collisions with other libraries or the user's code. When a module is imported using `use`, its top-level extensions become available in the importing scope.
## dynamic symbols ## dynamic symbols

View File

@ -1608,4 +1608,28 @@ Notes:
- `private` is visible only inside the declaring class; `protected` is visible from the declaring class and any of its transitive subclasses. Qualialsofication (`this@Type`) or casts do not bypass visibility. - `private` is visible only inside the declaring class; `protected` is visible from the declaring class and any of its transitive subclasses. Qualialsofication (`this@Type`) or casts do not bypass visibility.
- Safe‑call `?.` works with `as?` for optional dispatch. - Safe‑call `?.` works with `as?` for optional dispatch.
To get details on OOP in Lyng, see [OOP notes](oop.md). ## Extension members
You can add new methods and properties to existing classes without modifying them.
### Extension functions
fun String.shout() = this.upper() + "!!!"
"hello".shout()
>>> "HELLO!!!"
### Extension properties
val Int.isEven = this % 2 == 0
4.isEven
>>> true
Example with custom accessors:
val String.firstChar get() = this[0]
"abc".firstChar
>>> 'a'
Extension members are **scope-isolated**: they are visible only in the scope where they are defined and its children. This prevents name collisions and improves security.
To get details on OOP in Lyng, see [OOP notes](OOP.md).

View File

@ -76,7 +76,7 @@
}, },
"labels": { "patterns": [ { "name": "entity.name.label.lyng", "match": "[\\p{L}_][\\p{L}\\p{N}_]*:" } ] }, "labels": { "patterns": [ { "name": "entity.name.label.lyng", "match": "[\\p{L}_][\\p{L}\\p{N}_]*:" } ] },
"directives": { "patterns": [ { "name": "meta.directive.lyng", "match": "^\\s*#[_A-Za-z][_A-Za-z0-9]*" } ] }, "directives": { "patterns": [ { "name": "meta.directive.lyng", "match": "^\\s*#[_A-Za-z][_A-Za-z0-9]*" } ] },
"declarations": { "patterns": [ { "name": "meta.function.declaration.lyng", "match": "\\b(?:fun|fn)\\s+([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "entity.name.function.lyng" } } }, { "name": "meta.type.declaration.lyng", "match": "\\b(?:class|enum)\\s+([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "entity.name.type.lyng" } } }, { "name": "meta.variable.declaration.lyng", "match": "\\b(?:val|var)\\s+([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "variable.other.declaration.lyng" } } } ] }, "declarations": { "patterns": [ { "name": "meta.function.declaration.lyng", "match": "\\b(fun|fn)\\s+(?:([\\p{L}_][\\p{L}\\p{N}_]*)\\.)?([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "keyword.declaration.lyng" }, "2": { "name": "entity.name.type.lyng" }, "3": { "name": "entity.name.function.lyng" } } }, { "name": "meta.type.declaration.lyng", "match": "\\b(?:class|enum)\\s+([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "entity.name.type.lyng" } } }, { "name": "meta.variable.declaration.lyng", "match": "\\b(val|var)\\s+(?:([\\p{L}_][\\p{L}\\p{N}_]*)\\.)?([\\p{L}_][\\p{L}\\p{N}_]*)", "captures": { "1": { "name": "keyword.declaration.lyng" }, "2": { "name": "entity.name.type.lyng" }, "3": { "name": "variable.other.declaration.lyng" } } } ] },
"keywords": { "patterns": [ { "name": "keyword.control.lyng", "match": "\\b(?:if|else|when|while|do|for|try|catch|finally|throw|return|break|continue)\\b" }, { "name": "keyword.declaration.lyng", "match": "\\b(?:fun|fn|class|enum|val|var|import|package|constructor|property|open|extern|private|protected|static|get|set)\\b" }, { "name": "keyword.operator.word.lyng", "match": "\\bnot\\s+(?:in|is)\\b" }, { "name": "keyword.operator.word.lyng", "match": "\\b(?:and|or|not|in|is|as|as\\?)\\b" } ] }, "keywords": { "patterns": [ { "name": "keyword.control.lyng", "match": "\\b(?:if|else|when|while|do|for|try|catch|finally|throw|return|break|continue)\\b" }, { "name": "keyword.declaration.lyng", "match": "\\b(?:fun|fn|class|enum|val|var|import|package|constructor|property|open|extern|private|protected|static|get|set)\\b" }, { "name": "keyword.operator.word.lyng", "match": "\\bnot\\s+(?:in|is)\\b" }, { "name": "keyword.operator.word.lyng", "match": "\\b(?:and|or|not|in|is|as|as\\?)\\b" } ] },
"constants": { "patterns": [ { "name": "constant.language.lyng", "match": "(?:\\b(?:true|false|null|this)\\b|π)" } ] }, "constants": { "patterns": [ { "name": "constant.language.lyng", "match": "(?:\\b(?:true|false|null|this)\\b|π)" } ] },
"types": { "patterns": [ { "name": "storage.type.lyng", "match": "\\b(?:Int|Real|String|Bool|Char|Regex)\\b" }, { "name": "entity.name.type.lyng", "match": "\\b[A-Z][A-Za-z0-9_]*\\b(?!\\s*\\()" } ] }, "types": { "patterns": [ { "name": "storage.type.lyng", "match": "\\b(?:Int|Real|String|Bool|Char|Regex)\\b" }, { "name": "entity.name.type.lyng", "match": "\\b[A-Z][A-Za-z0-9_]*\\b(?!\\s*\\()" } ] },

View File

@ -157,8 +157,12 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
is MiniFunDecl -> { is MiniFunDecl -> {
addTypeSegments(d.returnType) addTypeSegments(d.returnType)
d.params.forEach { addTypeSegments(it.type) } d.params.forEach { addTypeSegments(it.type) }
addTypeSegments(d.receiver)
}
is MiniValDecl -> {
addTypeSegments(d.type)
addTypeSegments(d.receiver)
} }
is MiniValDecl -> addTypeSegments(d.type)
is MiniClassDecl -> { is MiniClassDecl -> {
d.ctorFields.forEach { addTypeSegments(it.type) } d.ctorFields.forEach { addTypeSegments(it.type) }
d.classFields.forEach { addTypeSegments(it.type) } d.classFields.forEach { addTypeSegments(it.type) }

View File

@ -184,7 +184,7 @@ class LyngCompletionContributor : CompletionContributor() {
?: guessReturnClassAcrossKnownCallees(text, memberDotPos, imported) ?: guessReturnClassAcrossKnownCallees(text, memberDotPos, imported)
?: guessReceiverClass(text, memberDotPos, imported) ?: guessReceiverClass(text, memberDotPos, imported)
if (!inferredClass.isNullOrBlank()) { if (!inferredClass.isNullOrBlank()) {
val ext = BuiltinDocRegistry.extensionMethodNamesFor(inferredClass) val ext = BuiltinDocRegistry.extensionMemberNamesFor(inferredClass)
if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Post-engine extension check for $inferredClass: ${'$'}{ext}") if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Post-engine extension check for $inferredClass: ${'$'}{ext}")
for (name in ext) { for (name in ext) {
if (existing.contains(name)) continue if (existing.contains(name)) continue
@ -494,10 +494,10 @@ class LyngCompletionContributor : CompletionContributor() {
} }
} }
// Supplement with stdlib extension-like methods defined in root.lyng (e.g., fun String.trim(...)) // Supplement with stdlib extension members defined in root.lyng (e.g., fun String.trim(...))
run { run {
val already = (directMap.keys + inheritedMap.keys).toMutableSet() val already = (directMap.keys + inheritedMap.keys).toMutableSet()
val ext = BuiltinDocRegistry.extensionMethodNamesFor(className) val ext = BuiltinDocRegistry.extensionMemberNamesFor(className)
if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Extensions for $className: count=${ext.size} -> ${ext}") if (DEBUG_COMPLETION) log.info("[LYNG_DEBUG] Extensions for $className: count=${ext.size} -> ${ext}")
for (name in ext) { for (name in ext) {
if (already.contains(name)) continue if (already.contains(name)) continue

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.0-beta1" version = "1.1.0-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

@ -49,12 +49,14 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
arguments: Arguments = scope.args, arguments: Arguments = scope.args,
defaultAccessType: AccessType = AccessType.Var, defaultAccessType: AccessType = AccessType.Var,
defaultVisibility: Visibility = Visibility.Public, defaultVisibility: Visibility = Visibility.Public,
declaringClass: net.sergeych.lyng.obj.ObjClass? = scope.currentClassCtx
) { ) {
fun assign(a: Item, value: Obj) { fun assign(a: Item, value: Obj) {
scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable, scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable,
value.byValueCopy(), value.byValueCopy(),
a.visibility ?: defaultVisibility, a.visibility ?: defaultVisibility,
recordType = ObjRecord.Type.Argument) recordType = ObjRecord.Type.Argument,
declaringClass = declaringClass)
} }
// Prepare positional args and parameter count, handle tail-block binding // Prepare positional args and parameter count, handle tail-block binding

View File

@ -58,17 +58,25 @@ class ClosureScope(val callScope: Scope, val closureScope: Scope) :
?.instanceScope ?.instanceScope
?.objects ?.objects
?.get(name) ?.get(name)
?.let { return it } ?.let { rec ->
closureScope.thisObj.objClass.getInstanceMemberOrNull(name)?.let { return it } 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 // 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 // 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) // 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 // 6) Module pseudo-symbols (e.g., __PACKAGE__) — walk caller ancestry and query ModuleScope directly
if (name.startsWith("__")) { if (name.startsWith("__")) {

View File

@ -2366,6 +2366,7 @@ class Compiler(
throw ScriptError(t.pos, "Expected identifier after 'fun'") throw ScriptError(t.pos, "Expected identifier after 'fun'")
else t.value else t.value
var nameStartPos: Pos = t.pos var nameStartPos: Pos = t.pos
var receiverMini: MiniTypeRef? = null
val annotation = lastAnnotation val annotation = lastAnnotation
val parentContext = codeContexts.last() val parentContext = codeContexts.last()
@ -2374,6 +2375,12 @@ class Compiler(
// Is extension? // Is extension?
if (t.type == Token.Type.DOT) { if (t.type == Token.Type.DOT) {
extTypeName = name 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() t = cc.next()
if (t.type != Token.Type.ID) if (t.type != Token.Type.ID)
throw ScriptError(t.pos, "illegal extension format: expected function name") throw ScriptError(t.pos, "illegal extension format: expected function name")
@ -2417,7 +2424,8 @@ class Compiler(
returnType = returnTypeMini, returnType = returnTypeMini,
body = null, body = null,
doc = declDocLocal, doc = declDocLocal,
nameStart = nameStartPos nameStart = nameStartPos,
receiver = receiverMini
) )
miniSink?.onFunDecl(node) miniSink?.onFunDecl(node)
pendingDeclDoc = null pendingDeclDoc = null
@ -2486,7 +2494,7 @@ class Compiler(
// class extension method // class extension method
val type = context[typeName]?.value ?: context.raiseSymbolNotFound("class $typeName not found") val type = context[typeName]?.value ?: context.raiseSymbolNotFound("class $typeName not found")
if (type !is ObjClass) context.raiseClassCastError("$typeName is not the class instance") if (type !is ObjClass) context.raiseClassCastError("$typeName is not the class instance")
type.addFn(name, isOpen = true, visibility = visibility) { val stmt = statement {
// ObjInstance has a fixed instance scope, so we need to build a closure // ObjInstance has a fixed instance scope, so we need to build a closure
(thisObj as? ObjInstance)?.let { i -> (thisObj as? ObjInstance)?.let { i ->
annotatedFnBody.execute(ClosureScope(this, i.instanceScope)) annotatedFnBody.execute(ClosureScope(this, i.instanceScope))
@ -2494,6 +2502,7 @@ class Compiler(
// other classes can create one-time scope for this rare case: // other classes can create one-time scope for this rare case:
?: annotatedFnBody.execute(thisObj.autoInstanceScope(this)) ?: annotatedFnBody.execute(thisObj.autoInstanceScope(this))
} }
context.addExtension(type, name, ObjRecord(stmt, isMutable = false, visibility = visibility, declaringClass = null))
} }
// regular function/method // regular function/method
?: run { ?: run {
@ -2640,7 +2649,27 @@ class Compiler(
if (nextToken.type != Token.Type.ID) if (nextToken.type != Token.Type.ID)
throw ScriptError(nextToken.pos, "Expected identifier or [ here") 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 // Optional explicit type annotation
val varTypeMini: MiniTypeRef? = if (cc.current().type == Token.Type.COLON) { val varTypeMini: MiniTypeRef? = if (cc.current().type == Token.Type.COLON) {
@ -2648,19 +2677,22 @@ class Compiler(
} else null } else null
val markBeforeEq = cc.savePos() val markBeforeEq = cc.savePos()
cc.skipWsTokens()
val eqToken = cc.next() val eqToken = cc.next()
var setNull = false var setNull = false
var isProperty = false var isProperty = false
val declaringClassNameCaptured = (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.name val declaringClassNameCaptured = (codeContexts.lastOrNull() as? CodeContext.ClassBody)?.name
if (declaringClassNameCaptured != null) { if (declaringClassNameCaptured != null || extTypeName != null) {
val mark = cc.savePos() val mark = cc.savePos()
cc.restorePos(markBeforeEq) cc.restorePos(markBeforeEq)
cc.skipWsTokens()
val next = cc.peekNextNonWhitespace() val next = cc.peekNextNonWhitespace()
if (next.isId("get") || next.isId("set")) { if (next.isId("get") || next.isId("set")) {
isProperty = true isProperty = true
cc.restorePos(markBeforeEq) cc.restorePos(markBeforeEq)
cc.skipWsTokens()
} else { } else {
cc.restorePos(mark) cc.restorePos(mark)
} }
@ -2673,10 +2705,11 @@ class Compiler(
true true
} else { } else {
if (!isProperty && eqToken.type != Token.Type.ASSIGN) { if (!isProperty && eqToken.type != Token.Type.ASSIGN) {
if (!isMutable && (declaringClassNameCaptured == null)) if (!isMutable && (declaringClassNameCaptured == null) && (extTypeName == null))
throw ScriptError(start, "val must be initialized") throw ScriptError(start, "val must be initialized")
else { else {
cc.restorePos(markBeforeEq) cc.restorePos(markBeforeEq)
cc.skipWsTokens()
setNull = true setNull = true
} }
} }
@ -2698,7 +2731,8 @@ class Compiler(
type = varTypeMini, type = varTypeMini,
initRange = initR, initRange = initR,
doc = pendingDeclDoc, doc = pendingDeclDoc,
nameStart = start nameStart = nameStartPos,
receiver = receiverMini
) )
miniSink?.onValDecl(node) miniSink?.onValDecl(node)
pendingDeclDoc = null pendingDeclDoc = null
@ -2722,7 +2756,7 @@ class Compiler(
// Check for accessors if it is a class member // Check for accessors if it is a class member
var getter: Statement? = null var getter: Statement? = null
var setter: Statement? = null var setter: Statement? = null
if (declaringClassNameCaptured != null) { if (declaringClassNameCaptured != null || extTypeName != null) {
while (true) { while (true) {
val t = cc.peekNextNonWhitespace() val t = cc.peekNextNonWhitespace()
if (t.isId("get")) { if (t.isId("get")) {
@ -2785,6 +2819,22 @@ class Compiler(
} }
return statement(start) { context -> 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 // 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 // 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. // ClassBody qualifies for class-field storage. Otherwise, this is a plain local.

View File

@ -45,19 +45,36 @@ class ModuleScope(
if (record.visibility.isPublic) { if (record.visibility.isPublic) {
val newName = symbols?.let { ss: Map<String, String> -> val newName = symbols?.let { ss: Map<String, String> ->
ss[symbol] ss[symbol]
?.also { symbolsToImport!!.remove(it) } ?.also { symbolsToImport!!.remove(symbol) }
?: scope.raiseError("internal error: symbol $symbol not found though the module is cached") ?: return@let null
} ?: symbol } ?: if (symbols == null) symbol else null
val existing = scope.objects[newName]
if (existing != null ) { if (newName != null) {
if (existing.importedFrom != record.importedFrom) val existing = scope.objects[newName]
scope.raiseError("symbol ${existing.importedFrom?.packageName}.$newName already exists, redefinition on import is not allowed") if (existing != null) {
// already imported 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 for ((cls, map) in this.extensions) {
scope.objects[newName] = record 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)
}
} }
} }
} }

View File

@ -62,6 +62,30 @@ open class Scope(
*/ */
internal val localBindings: MutableMap<String, ObjRecord> = mutableMapOf() 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. */ /** Debug helper: ensure assigning [candidateParent] does not create a structural cycle. */
private fun ensureNoCycle(candidateParent: Scope?) { private fun ensureNoCycle(candidateParent: Scope?) {
if (candidateParent == null) return if (candidateParent == null) return
@ -82,10 +106,17 @@ 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): ObjRecord? { private fun tryGetLocalRecord(s: Scope, name: String, caller: net.sergeych.lyng.obj.ObjClass?): ObjRecord? {
s.objects[name]?.let { return it } s.objects[name]?.let { rec ->
s.localBindings[name]?.let { return it } if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec
s.getSlotIndexOf(name)?.let { return s.getSlotRecord(it) } }
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 return null
} }
@ -95,7 +126,7 @@ open class Scope(
val visited = HashSet<Long>(4) val visited = HashSet<Long>(4)
while (s != null) { while (s != null) {
if (!visited.add(s.frameId)) return null if (!visited.add(s.frameId)) return null
tryGetLocalRecord(s, name)?.let { return it } tryGetLocalRecord(s, name, currentClassCtx)?.let { return it }
s = s.parent s = s.parent
} }
return null return null
@ -110,17 +141,22 @@ open class Scope(
*/ */
internal fun baseGetIgnoreClosure(name: String): ObjRecord? { internal fun baseGetIgnoreClosure(name: String): ObjRecord? {
// 1) locals/bindings in this frame // 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 // 2) walk parents for plain locals/bindings only
var s = parent var s = parent
val visited = HashSet<Long>(4) val visited = HashSet<Long>(4)
while (s != null) { while (s != null) {
if (!visited.add(s.frameId)) return null if (!visited.add(s.frameId)) return null
tryGetLocalRecord(s, name)?.let { return it } tryGetLocalRecord(s, name, currentClassCtx)?.let { return it }
s = s.parent s = s.parent
} }
// 3) fallback to instance/class members of this frame's thisObj // 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 * This completely avoids invoking overridden `get` implementations, preventing
* ping-pong recursion between `ClosureScope` frames. * 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 var s: Scope? = this
val visited = HashSet<Long>(4) val visited = HashSet<Long>(4)
while (s != null) { while (s != null) {
if (!visited.add(s.frameId)) return null if (!visited.add(s.frameId)) return null
tryGetLocalRecord(s, name)?.let { return it } tryGetLocalRecord(s, name, caller)?.let { return it }
s.thisObj.objClass.getInstanceMemberOrNull(name)?.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 s = s.parent
} }
return null return null
@ -152,6 +193,10 @@ open class Scope(
// copy locals and bindings // copy locals and bindings
snap.objects.putAll(this.objects) snap.objects.putAll(this.objects)
snap.localBindings.putAll(this.localBindings) snap.localBindings.putAll(this.localBindings)
// copy extensions
for ((cls, map) in extensions) {
snap.extensions[cls] = map.toMutableMap()
}
// copy slots map preserving indices // copy slots map preserving indices
if (this.slotCount() > 0) { if (this.slotCount() > 0) {
var i = 0 var i = 0
@ -273,13 +318,19 @@ open class Scope(
if (name == "this") thisObj.asReadonly if (name == "this") thisObj.asReadonly
else { else {
// Prefer direct locals/bindings declared in this frame // 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) // 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 // Walk up ancestry
?: parent?.get(name) ?: parent?.get(name)
// Finally, fallback to class members on thisObj // 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() slots.clear()
nameToSlot.clear() nameToSlot.clear()
localBindings.clear() localBindings.clear()
extensions.clear()
// Now safe to validate and re-parent // Now safe to validate and re-parent
ensureNoCycle(parent) ensureNoCycle(parent)
this.parent = parent this.parent = parent
@ -400,9 +452,10 @@ open class Scope(
isMutable: Boolean, isMutable: Boolean,
value: Obj, value: Obj,
visibility: Visibility = Visibility.Public, visibility: Visibility = Visibility.Public,
recordType: ObjRecord.Type = ObjRecord.Type.Other recordType: ObjRecord.Type = ObjRecord.Type.Other,
declaringClass: net.sergeych.lyng.obj.ObjClass? = currentClassCtx
): ObjRecord { ): ObjRecord {
val rec = ObjRecord(value, isMutable, visibility, declaringClass = currentClassCtx, type = recordType) val rec = ObjRecord(value, isMutable, visibility, declaringClass = declaringClass, type = recordType)
objects[name] = rec objects[name] = rec
// Index this binding within the current frame to help resolve locals across suspension // Index this binding within the current frame to help resolve locals across suspension
localBindings[name] = rec localBindings[name] = rec

View File

@ -26,7 +26,7 @@ enum class Visibility {
/** MI-aware visibility check: whether [caller] can access a member declared in [decl] with [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 { 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.Public -> true
Visibility.Private -> (decl != null && caller === decl) Visibility.Private -> (decl != null && caller === decl)
Visibility.Protected -> when { Visibility.Protected -> when {
@ -36,4 +36,5 @@ fun canAccessMember(visibility: Visibility, decl: net.sergeych.lyng.obj.ObjClass
else -> (caller.allParentsSet.contains(decl)) else -> (caller.allParentsSet.contains(decl))
} }
} }
return res
} }

View File

@ -105,20 +105,24 @@ object BuiltinDocRegistry : BuiltinDocSource {
} }
/** /**
* List names of extension-like methods defined for [className] in the stdlib text (`root.lyng`). * List names of extension members defined for [className] in the stdlib text (`root.lyng`).
* We do a lightweight regex scan like: `fun ClassName.methodName(` and collect distinct names. * 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 src = try { rootLyng } catch (_: Throwable) { null } ?: return emptyList()
val out = LinkedHashSet<String>() val out = LinkedHashSet<String>()
// Match lines like: fun String.trim(...) // Match lines like: fun String.trim(...) or val Int.isEven = ...
val re = Regex("(?m)^\\s*fun\\s+${className}\\.([A-Za-z_][A-Za-z0-9_]*)\\s*\\(") val re = Regex("(?m)^\\s*(?:fun|val|var)\\s+${className}\\.([A-Za-z_][A-Za-z0-9_]*)\\b")
re.findAll(src).forEach { m -> re.findAll(src).forEach { m ->
val name = m.groupValues.getOrNull(1)?.trim() val name = m.groupValues.getOrNull(1)?.trim()
if (!name.isNullOrEmpty()) out.add(name) if (!name.isNullOrEmpty()) out.add(name)
} }
return out.toList() return out.toList()
} }
@Deprecated("Use extensionMemberNamesFor", ReplaceWith("extensionMemberNamesFor(className)"))
fun extensionMethodNamesFor(className: String): List<String> = extensionMemberNamesFor(className)
} }
// ---------------- Builders ---------------- // ---------------- Builders ----------------
@ -367,8 +371,8 @@ private object StdlibInlineDocIndex {
else -> { else -> {
// Non-comment, non-blank: try to match a declaration just after comments // Non-comment, non-blank: try to match a declaration just after comments
if (buf.isNotEmpty()) { if (buf.isNotEmpty()) {
// fun Class.name( ... ) // fun/val/var Class.name( ... )
val mExt = Regex("^fun\\s+([A-Za-z_][A-Za-z0-9_]*)\\.([A-Za-z_][A-Za-z0-9_]*)\\s*\\(").find(line) 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) { if (mExt != null) {
val (cls, name) = mExt.destructured val (cls, name) = mExt.destructured
flushTo(Key.Method(cls, name)) flushTo(Key.Method(cls, name))

View File

@ -208,10 +208,10 @@ object CompletionEngineLight {
emitGroup(directMap) emitGroup(directMap)
emitGroup(inheritedMap) 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 { run {
val already = (directMap.keys + inheritedMap.keys).toMutableSet() val already = (directMap.keys + inheritedMap.keys).toMutableSet()
val ext = BuiltinDocRegistry.extensionMethodNamesFor(className) val ext = BuiltinDocRegistry.extensionMemberNamesFor(className)
for (name in ext) { for (name in ext) {
if (already.contains(name)) continue if (already.contains(name)) continue
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, className, name) val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, className, name)

View File

@ -100,7 +100,7 @@ fun ObjClass.addFnDoc(
code: suspend Scope.() -> Obj code: suspend Scope.() -> Obj
) { ) {
// Register runtime method // Register runtime method
addFn(name, isOpen, visibility, code) addFn(name, isOpen, visibility, code = code)
// Register docs for the member under this class // Register docs for the member under this class
BuiltinDocRegistry.module(moduleName ?: ownerModuleNameFromClassOrUnknown()) { BuiltinDocRegistry.module(moduleName ?: ownerModuleNameFromClassOrUnknown()) {
classDoc(this@addFnDoc.className, doc = "") { classDoc(this@addFnDoc.className, doc = "") {

View File

@ -109,7 +109,8 @@ data class MiniFunDecl(
val returnType: MiniTypeRef?, val returnType: MiniTypeRef?,
val body: MiniBlock?, val body: MiniBlock?,
override val doc: MiniDoc?, override val doc: MiniDoc?,
override val nameStart: Pos override val nameStart: Pos,
val receiver: MiniTypeRef? = null
) : MiniDecl ) : MiniDecl
data class MiniValDecl( data class MiniValDecl(
@ -119,7 +120,8 @@ data class MiniValDecl(
val type: MiniTypeRef?, val type: MiniTypeRef?,
val initRange: MiniRange?, val initRange: MiniRange?,
override val doc: MiniDoc?, override val doc: MiniDoc?,
override val nameStart: Pos override val nameStart: Pos,
val receiver: MiniTypeRef? = null
) : MiniDecl ) : MiniDecl
data class MiniClassDecl( data class MiniClassDecl(

View File

@ -85,21 +85,50 @@ open class Obj {
args: Arguments = Arguments.EMPTY, args: Arguments = Arguments.EMPTY,
onNotFoundResult: (suspend () -> Obj?)? = null onNotFoundResult: (suspend () -> Obj?)? = null
): Obj { ): Obj {
val rec = objClass.getInstanceMemberOrNull(name) // 1. Hierarchy members (excluding root fallback)
if (rec != null) { for (cls in objClass.mro) {
val decl = rec.declaringClass ?: objClass.findDeclaringClassOf(name) if (cls.className == "Obj") break
val caller = scope.currentClassCtx val rec = cls.members[name] ?: cls.classScope?.objects?.get(name)
if (!canAccessMember(rec.visibility, decl, caller)) if (rec != null) {
scope.raiseError(ObjAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})")) val decl = rec.declaringClass ?: cls
// Propagate declaring class as current class context during method execution val caller = scope.currentClassCtx
val saved = scope.currentClassCtx if (!canAccessMember(rec.visibility, decl, caller))
scope.currentClassCtx = decl scope.raiseError(ObjAccessException(scope, "can't invoke ${name}: not visible (declared in ${decl.className}, caller ${caller?.className ?: "?"})"))
try { val saved = scope.currentClassCtx
return rec.value.invoke(scope, this, args) scope.currentClassCtx = decl
} finally { try {
scope.currentClassCtx = saved 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() return onNotFoundResult?.invoke()
?: scope.raiseError( ?: scope.raiseError(
"no such member: $name on ${objClass.className}. Considered order: ${objClass.renderLinearization(true)}. " + "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() } // 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 {
// could be property or class field: // 1. Hierarchy members (excluding root fallback)
val obj = objClass.getInstanceMemberOrNull(name) ?: scope.raiseError( 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)}" "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 val caller = scope.currentClassCtx
// Check visibility for non-property members here if they weren't checked before
if (!canAccessMember(obj.visibility, decl, caller)) if (!canAccessMember(obj.visibility, decl, caller))
scope.raiseError(ObjAccessException(scope, "can't access field ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})")) scope.raiseError(ObjAccessException(scope, "can't access field ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})"))
return when (val value = obj.value) { return obj
is Statement -> {
ObjRecord(value.execute(scope.createChildScope(scope.pos, newThisObj = this)), obj.isMutable)
}
// could be writable property naturally
// null -> ObjNull.asReadonly
else -> obj
}
} }
open suspend fun writeField(scope: Scope, name: String, newValue: Obj) { open suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
willMutate(scope) willMutate(scope)
val field = objClass.getInstanceMemberOrNull(name) ?: scope.raiseError( 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)}" "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 val caller = scope.currentClassCtx
if (!canAccessMember(field.visibility, decl, caller)) if (!canAccessMember(field.visibility, decl, caller))
scope.raiseError(ObjAccessException(scope, "can't assign field ${name}: not visible (declared in ${decl?.className ?: "?"}, caller ${caller?.className ?: "?"})")) 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 { open suspend fun getAt(scope: Scope, index: Obj): Obj {

View File

@ -53,6 +53,12 @@ class ObjChar(val value: Char): Obj() {
returns = type("lyng.Int"), returns = type("lyng.Int"),
moduleName = "lyng.stdlib" moduleName = "lyng.stdlib"
) { ObjInt(thisAs<ObjChar>().value.code.toLong()) } ) { ObjInt(thisAs<ObjChar>().value.code.toLong()) }
addFn("isDigit") {
thisAs<ObjChar>().value.isDigit().toObj()
}
addFn("isSpace") {
thisAs<ObjChar>().value.isWhitespace().toObj()
}
} }
} }
} }

View File

@ -175,7 +175,11 @@ open class ObjClass(
} }
/** Full C3 MRO including this class at index 0. */ /** 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). */ /** Parents in C3 order (no self). */
val mroParents: List<ObjClass> by lazy { mro.drop(1) } 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. // 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)
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 // 1) members-defined methods
@ -268,7 +273,7 @@ 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) meta.assignToContext(instance.instanceScope, argsHere, declaringClass = c)
// 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) {
@ -297,7 +302,7 @@ 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) meta.assignToContext(instance.instanceScope, argsHere, declaringClass = c)
// 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]
@ -340,14 +345,15 @@ open class ObjClass(
initialValue: Obj, initialValue: Obj,
isMutable: Boolean = false, isMutable: Boolean = false,
visibility: Visibility = Visibility.Public, 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 // Allow overriding ancestors: only prevent redefinition if THIS class already defines an immutable member
val existingInSelf = members[name] val existingInSelf = members[name]
if (existingInSelf != null && existingInSelf.isMutable == false) if (existingInSelf != null && existingInSelf.isMutable == false)
throw ScriptError(pos, "$name is already defined in $objClass") throw ScriptError(pos, "$name is already defined in $objClass")
// Install/override in this class // 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 // Structural change: bump layout version for PIC invalidation
layoutVersion += 1 layoutVersion += 1
} }
@ -377,10 +383,11 @@ open class ObjClass(
name: String, name: String,
isOpen: Boolean = false, isOpen: Boolean = false,
visibility: Visibility = Visibility.Public, visibility: Visibility = Visibility.Public,
declaringClass: ObjClass? = this,
code: suspend Scope.() -> Obj code: suspend Scope.() -> Obj
) { ) {
val stmt = statement { code() } 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) fun addConst(name: String, value: Obj) = createField(name, value, isMutable = false)

View File

@ -33,9 +33,10 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
override suspend fun readField(scope: Scope, name: String): ObjRecord { override suspend fun readField(scope: Scope, name: String): ObjRecord {
// Direct (unmangled) lookup first // Direct (unmangled) lookup first
instanceScope[name]?.let { rec -> 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 // 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 val caller = scope.currentClassCtx
if (!canAccessMember(rec.visibility, decl, caller)) if (!canAccessMember(rec.visibility, decl, caller))
scope.raiseError( scope.raiseError(
@ -47,7 +48,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
} }
if (rec.type == ObjRecord.Type.Property) { if (rec.type == ObjRecord.Type.Property) {
val prop = rec.value as ObjProperty 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 return rec
} }
@ -70,7 +71,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
instanceScope.objects.containsKey("${cls.className}::$name") -> cls instanceScope.objects.containsKey("${cls.className}::$name") -> cls
else -> cls.mroParents.firstOrNull { instanceScope.objects.containsKey("${it.className}::$name") } 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 val caller = scope.currentClassCtx
if (!canAccessMember(rec.visibility, declaring, caller)) if (!canAccessMember(rec.visibility, declaring, caller))
scope.raiseError( scope.raiseError(
@ -82,7 +83,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
} }
if (rec.type == ObjRecord.Type.Property) { if (rec.type == ObjRecord.Type.Property) {
val prop = rec.value as ObjProperty 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 return rec
} }
@ -93,8 +94,8 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) { override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
// Direct (unmangled) first // Direct (unmangled) first
instanceScope[name]?.let { f -> instanceScope[name]?.let { f ->
val decl = f.declaringClass ?: objClass.findDeclaringClassOf(name) val decl = f.declaringClass
if (scope.thisObj !== this) { if (scope.thisObj !== this || scope.currentClassCtx == null) {
val caller = scope.currentClassCtx val caller = scope.currentClassCtx
if (!canAccessMember(f.visibility, decl, caller)) if (!canAccessMember(f.visibility, decl, caller))
ObjIllegalAssignmentException( ObjIllegalAssignmentException(
@ -104,7 +105,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
} }
if (f.type == ObjRecord.Type.Property) { if (f.type == ObjRecord.Type.Property) {
val prop = f.value as ObjProperty val prop = f.value as ObjProperty
prop.callSetter(scope, this, newValue) prop.callSetter(scope, this, newValue, decl)
return return
} }
if (!f.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise() 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 instanceScope.objects.containsKey("${cls.className}::$name") -> cls
else -> cls.mroParents.firstOrNull { instanceScope.objects.containsKey("${it.className}::$name") } 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 val caller = scope.currentClassCtx
if (!canAccessMember(rec.visibility, declaring, caller)) if (!canAccessMember(rec.visibility, declaring, caller))
ObjIllegalAssignmentException( ObjIllegalAssignmentException(
@ -138,7 +139,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
} }
if (rec.type == ObjRecord.Type.Property) { if (rec.type == ObjRecord.Type.Property) {
val prop = rec.value as ObjProperty val prop = rec.value as ObjProperty
prop.callSetter(scope, this, newValue) prop.callSetter(scope, this, newValue, declaring)
return return
} }
if (!rec.isMutable) ObjIllegalAssignmentException(scope, "can't reassign val $name").raise() 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?)? onNotFoundResult: (suspend () -> Obj?)?
): Obj = ): Obj =
instanceScope[name]?.let { rec -> 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 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(
@ -163,23 +164,16 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
"can't invoke method $name (declared in ${decl?.className ?: "?"})" "can't invoke method $name (declared in ${decl?.className ?: "?"})"
) )
) )
// execute with lexical class context propagated to declaring class rec.value.invoke(
val saved = instanceScope.currentClassCtx instanceScope,
instanceScope.currentClassCtx = decl this,
try { args
rec.value.invoke( )
instanceScope,
this,
args
)
} finally {
instanceScope.currentClassCtx = saved
}
} }
?: run { ?: run {
// fallback: class-scope function (registered during class body execution) // fallback: class-scope function (registered during class body execution)
objClass.classScope?.objects?.get(name)?.let { rec -> 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 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(
@ -188,13 +182,7 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
"can't invoke method $name (declared in ${decl?.className ?: "?"})" "can't invoke method $name (declared in ${decl?.className ?: "?"})"
) )
) )
val saved = instanceScope.currentClassCtx rec.value.invoke(instanceScope, this, args)
instanceScope.currentClassCtx = decl
try {
rec.value.invoke(instanceScope, this, args)
} finally {
instanceScope.currentClassCtx = saved
}
} }
} }
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult) ?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)

View File

@ -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 package net.sergeych.lyng.obj
import net.sergeych.lyng.Arguments import net.sergeych.lyng.Arguments
import net.sergeych.lyng.ClosureScope
import net.sergeych.lyng.Scope import net.sergeych.lyng.Scope
import net.sergeych.lyng.Statement import net.sergeych.lyng.Statement
@ -14,16 +32,24 @@ class ObjProperty(
val setter: Statement? val setter: Statement?
) : Obj() { ) : 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") val g = getter ?: scope.raiseError("property $name has no getter")
// Execute getter in a child scope of the instance with 'this' properly set // 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") 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 // 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)" override fun toString(): String = "Property($name)"

View File

@ -1178,18 +1178,25 @@ class MethodCallRef(
when (base) { when (base) {
is ObjInstance -> { is ObjInstance -> {
// Prefer resolved class member to avoid per-call lookup on hit // Prefer resolved class member to avoid per-call lookup on hit
val member = base.objClass.getInstanceMemberOrNull(name) // BUT only if it's NOT a root object member (which can be shadowed by extensions)
if (member != null) { var hierarchyMember: ObjRecord? = null
val visibility = member.visibility for (cls in base.objClass.mro) {
val callable = member.value 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 -> mKey1 = key; mVer1 = ver; mInvoker1 = { obj, sc, a ->
val inst = obj as ObjInstance 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")) sc.raiseError(ObjAccessException(sc, "can't invoke non-public method $name"))
callable.invoke(inst.instanceScope, inst, a) callable.invoke(inst.instanceScope, inst, a)
} }
} else { } 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) } 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 hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
val slot = if (hit) cachedSlot else resolveSlot(scope) val slot = if (hit) cachedSlot else resolveSlot(scope)
if (slot >= 0) { if (slot >= 0) {
if (PerfFlags.PIC_DEBUG_COUNTERS) { val rec = scope.getSlotRecord(slot)
if (hit) PerfStats.localVarPicHit++ else PerfStats.localVarPicMiss++ 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++ 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`
@ -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 hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
val slot = if (hit) cachedSlot else resolveSlot(scope) val slot = if (hit) cachedSlot else resolveSlot(scope)
if (slot >= 0) return scope.getSlotRecord(slot).value if (slot >= 0) {
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` // Fallback name in scope or field on `this`
scope[name]?.let { return it.value } scope[name]?.let { return it.value }
run { 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) val slot = if (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount()) cachedSlot else resolveSlot(scope)
if (slot >= 0) { if (slot >= 0) {
val rec = scope.getSlotRecord(slot) val rec = scope.getSlotRecord(slot)
if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value") if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) {
rec.value = newValue if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value")
return rec.value = newValue
return
}
} }
scope[name]?.let { stored -> scope[name]?.let { stored ->
if (stored.isMutable) stored.value = newValue if (stored.isMutable) stored.value = newValue
@ -1444,17 +1462,25 @@ class BoundLocalVarRef(
) : ObjRef { ) : ObjRef {
override suspend fun get(scope: Scope): ObjRecord { override suspend fun get(scope: Scope): ObjRecord {
scope.pos = atPos 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 { override suspend fun evalValue(scope: Scope): Obj {
scope.pos = atPos 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) { override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
scope.pos = atPos scope.pos = atPos
val rec = 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"))
if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value") if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value")
rec.value = newValue rec.value = newValue
} }
@ -1518,10 +1544,13 @@ class FastLocalVarRef(
val slot = if (ownerValid && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope) val slot = if (ownerValid && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope)
val actualOwner = cachedOwnerScope val actualOwner = cachedOwnerScope
if (slot >= 0 && actualOwner != null) { if (slot >= 0 && actualOwner != null) {
if (PerfFlags.PIC_DEBUG_COUNTERS) { val rec = actualOwner.getSlotRecord(slot)
if (ownerValid) PerfStats.fastLocalHit++ else PerfStats.fastLocalMiss++ 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) // Try per-frame local binding maps in the ancestry first (locals declared in frames)
run { run {
@ -1558,7 +1587,11 @@ class FastLocalVarRef(
val ownerValid = isOwnerValidFor(scope) val ownerValid = isOwnerValidFor(scope)
val slot = if (ownerValid && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope) val slot = if (ownerValid && cachedSlot >= 0) cachedSlot else resolveSlotInAncestry(scope)
val actualOwner = cachedOwnerScope val actualOwner = cachedOwnerScope
if (slot >= 0 && actualOwner != null) 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 // Try per-frame local binding maps in the ancestry first
run { run {
var s: Scope? = scope var s: Scope? = scope
@ -1595,9 +1628,11 @@ class FastLocalVarRef(
val actualOwner = cachedOwnerScope val actualOwner = cachedOwnerScope
if (slot >= 0 && actualOwner != null) { if (slot >= 0 && actualOwner != null) {
val rec = actualOwner.getSlotRecord(slot) val rec = actualOwner.getSlotRecord(slot)
if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value") if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, scope.currentClassCtx)) {
rec.value = newValue if (!rec.isMutable) scope.raiseError("Cannot assign to immutable value")
return rec.value = newValue
return
}
} }
// Try per-frame local binding maps in the ancestry first // Try per-frame local binding maps in the ancestry first
run { run {

View File

@ -122,6 +122,12 @@ 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(
name = "first",
doc = "Return the oldest element in the buffer.",
returns = type("lyng.Any"),
moduleName = "lyng.stdlib"
) { thisAs<ObjRingBuffer>().buffer.first() }
} }
} }
} }

View File

@ -22,6 +22,7 @@ import net.sergeych.lyng.obj.ObjInstance
import net.sergeych.lyng.obj.ObjList import net.sergeych.lyng.obj.ObjList
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFails
class OOTest { class OOTest {
@Test @Test
@ -343,4 +344,56 @@ class OOTest {
val b2 = list.list[1] as ObjInstance val b2 = list.list[1] as ObjInstance
assertEquals(0, b1.compareTo(scope, b2)) 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())
}
} }

View File

@ -16,7 +16,7 @@ fun cached(builder) {
} }
} }
/* Filter elements of this iterable using the provided predicate. */ /* Filter elements of this iterable using the provided predicate. */
fun Iterable.filter(predicate) { fun Iterable.filterFlow(predicate) {
val list = this val list = this
flow { flow {
for( item in list ) { 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 filter out all null elements from this collection (Iterable); collection of
non-null elements is returned non-null elements is returned
*/ */
fun Iterable.filterFlowNotNull() {
filterFlow { it != null }
}
fun Iterable.filterNotNull() { fun Iterable.filterNotNull() {
filter { it != null } filter { it != null }
} }
@ -42,10 +52,10 @@ fun Iterable.drop(n) {
} }
/* Return the first element or throw if the iterable is empty. */ /* Return the first element or throw if the iterable is empty. */
fun Iterable.first() { val Iterable.first get() {
val i = iterator() val i = iterator()
if( !i.hasNext() ) throw NoSuchElementException() 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. */ /* Return the last element or throw if the iterable is empty. */
fun Iterable.last() { val Iterable.last get() {
var found = false var found = false
var element = null var element = null
for( i in this ) { for( i in this ) {
@ -81,7 +91,7 @@ fun Iterable.last() {
found = true found = true
} }
if( !found ) throw NoSuchElementException() if( !found ) throw NoSuchElementException()
element element
} }
/* Emit all but the last N elements of this iterable. */ /* Emit all but the last N elements of this iterable. */
@ -247,5 +257,5 @@ fun Exception.printStackTrace() {
} }
/* Compile this string into a regular expression. */ /* Compile this string into a regular expression. */
fun String.re(): Regex { Regex(this) } val String.re get() = Regex(this)