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

This commit is contained in:
sergeych committed 2025-12-24 00:29:10 +01:00
1 parent 3b6504d3b1
commit cd2b1a9cb7
26 files changed
+606 -163

No files matched your search

+1 -1
View File
@@ -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() }
}
}
}
+53
View File
@@ -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 -6
View File
@@ -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)