Extensions methods and properties now are correctly isolated, respect visibility rules and allow adding class properties and class vals.
This commit is contained in:
parent
3b6504d3b1
commit
cd2b1a9cb7
@ -98,7 +98,17 @@ esac
|
||||
|
||||
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
|
||||
refreshTextmateZip
|
||||
updateIdeaPluginDownloadLink || echo "WARN: proceeding without updating IDEA plugin download link"
|
||||
|
||||
./gradlew site:jsBrowserDistribution
|
||||
|
||||
71
docs/OOP.md
71
docs/OOP.md
@ -491,9 +491,11 @@ As usual, private statics are not accessible from the outside:
|
||||
|
||||
# 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
|
||||
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:
|
||||
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.
|
||||
|
||||
## 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() {
|
||||
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() )
|
||||
>>> 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,
|
||||
as they are modifying the type, not the context.
|
||||
## Extension properties
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -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.
|
||||
- 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).
|
||||
@ -76,7 +76,7 @@
|
||||
},
|
||||
"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]*" } ] },
|
||||
"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" } ] },
|
||||
"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*\\()" } ] },
|
||||
|
||||
@ -157,8 +157,12 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
is MiniFunDecl -> {
|
||||
addTypeSegments(d.returnType)
|
||||
d.params.forEach { addTypeSegments(it.type) }
|
||||
addTypeSegments(d.receiver)
|
||||
}
|
||||
is MiniValDecl -> {
|
||||
addTypeSegments(d.type)
|
||||
addTypeSegments(d.receiver)
|
||||
}
|
||||
is MiniValDecl -> addTypeSegments(d.type)
|
||||
is MiniClassDecl -> {
|
||||
d.ctorFields.forEach { addTypeSegments(it.type) }
|
||||
d.classFields.forEach { addTypeSegments(it.type) }
|
||||
|
||||
@ -184,7 +184,7 @@ class LyngCompletionContributor : CompletionContributor() {
|
||||
?: guessReturnClassAcrossKnownCallees(text, memberDotPos, imported)
|
||||
?: guessReceiverClass(text, memberDotPos, imported)
|
||||
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}")
|
||||
for (name in ext) {
|
||||
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 {
|
||||
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}")
|
||||
for (name in ext) {
|
||||
if (already.contains(name)) continue
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user