fix endless recursion in scope resolution in some specific cases
This commit is contained in:
1 parent
c0fab3d60e
commit
bcabfc8962
7 files changed
+426
-46
No files matched your search
@@ -38,35 +38,80 @@ class ClosureScope(val callScope: Scope, val closureScope: Scope) :
|
||||
}
|
||||
|
||||
override fun get(name: String): ObjRecord? {
|
||||
// Fast-path built-ins
|
||||
if (name == "this") return thisObj.asReadonly
|
||||
|
||||
// Priority:
|
||||
// 1) Locals and arguments declared in this lambda frame (including values defined before suspension)
|
||||
// 2) Instance/class members of the captured receiver (`closureScope.thisObj`), e.g., fields like `coll`, `factor`
|
||||
// 3) Symbols from the captured closure scope (its locals and parents)
|
||||
// 2) Instance/class members of the captured receiver (`closureScope.thisObj`)
|
||||
// 3) Symbols from the captured closure scope chain (locals/parents), ignoring nested ClosureScope overrides
|
||||
// 4) Instance members of the caller's `this` (e.g., FlowBuilder.emit)
|
||||
// 5) Fallback to the standard chain (this frame -> parent (callScope) -> class members)
|
||||
// 5) Symbols from the caller chain (locals/parents), ignoring nested ClosureScope overrides
|
||||
// 6) Special fallback for module pseudo-symbols (e.g., __PACKAGE__)
|
||||
|
||||
// First, prefer locals/arguments bound in this frame
|
||||
// 1) Locals/arguments in this closure frame
|
||||
super.objects[name]?.let { return it }
|
||||
super.localBindings[name]?.let { return it }
|
||||
|
||||
// Prefer instance fields/methods declared on the captured receiver:
|
||||
// First, resolve real instance fields stored in the instance scope (constructor vars like `coll`, `factor`)
|
||||
// 2) Members on the captured receiver instance
|
||||
(closureScope.thisObj as? net.sergeych.lyng.obj.ObjInstance)
|
||||
?.instanceScope
|
||||
?.objects
|
||||
?.get(name)
|
||||
?.let { return it }
|
||||
|
||||
// Then, try class-declared members (methods/properties declared in the class body)
|
||||
closureScope.thisObj.objClass.getInstanceMemberOrNull(name)?.let { return it }
|
||||
|
||||
// Then delegate to the full closure scope chain (locals, parents, etc.)
|
||||
closureScope.get(name)?.let { return it }
|
||||
// 3) Closure scope chain (locals/parents + members), ignore ClosureScope overrides to prevent recursion
|
||||
closureScope.chainLookupWithMembers(name)?.let { return it }
|
||||
|
||||
// Allow resolving instance members of the caller's `this` (e.g., FlowBuilder.emit)
|
||||
// 4) Caller `this` members
|
||||
callScope.thisObj.objClass.getInstanceMemberOrNull(name)?.let { return it }
|
||||
|
||||
// Fallback to the standard lookup chain: this frame -> parent (callScope) -> class members
|
||||
return super.get(name)
|
||||
// 5) Caller chain (locals/parents + members)
|
||||
callScope.chainLookupWithMembers(name)?.let { return it }
|
||||
|
||||
// 6) Module pseudo-symbols (e.g., __PACKAGE__) — walk caller ancestry and query ModuleScope directly
|
||||
if (name.startsWith("__")) {
|
||||
var s: Scope? = callScope
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) break
|
||||
if (s is ModuleScope) return s.get(name)
|
||||
s = s.parent
|
||||
}
|
||||
}
|
||||
|
||||
// 7) Direct module/global fallback: try to locate nearest ModuleScope and check its own locals
|
||||
fun lookupInModuleAncestry(from: Scope): ObjRecord? {
|
||||
var s: Scope? = from
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) break
|
||||
if (s is ModuleScope) {
|
||||
s.objects[name]?.let { return it }
|
||||
s.localBindings[name]?.let { return it }
|
||||
// check immediate parent (root scope) locals/constants for globals like `delay`
|
||||
val p = s.parent
|
||||
if (p != null) {
|
||||
p.objects[name]?.let { return it }
|
||||
p.localBindings[name]?.let { return it }
|
||||
p.thisObj.objClass.getInstanceMemberOrNull(name)?.let { return it }
|
||||
}
|
||||
return null
|
||||
}
|
||||
s = s.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
lookupInModuleAncestry(closureScope)?.let { return it }
|
||||
lookupInModuleAncestry(callScope)?.let { return it }
|
||||
|
||||
// 8) Global root scope constants/functions (e.g., delay, yield) via current import provider
|
||||
runCatching { this.currentImportProvider.rootScope.objects[name] }.getOrNull()?.let { return it }
|
||||
|
||||
// Final safe fallback: base scope lookup from this frame walking raw parents
|
||||
return baseGetIgnoreClosure(name)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,108 @@ open class Scope(
|
||||
*/
|
||||
internal val localBindings: MutableMap<String, ObjRecord> = mutableMapOf()
|
||||
|
||||
/** Debug helper: ensure assigning [candidateParent] does not create a structural cycle. */
|
||||
private fun ensureNoCycle(candidateParent: Scope?) {
|
||||
if (candidateParent == null) return
|
||||
var s: Scope? = candidateParent
|
||||
var hops = 0
|
||||
while (s != null && hops++ < 1024) {
|
||||
if (s === this) {
|
||||
// In production we silently ignore; for debugging throw an error to signal misuse
|
||||
throw IllegalStateException("cycle detected in scope parent chain assignment")
|
||||
}
|
||||
s = s.parent
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal lookup helpers that deliberately avoid invoking overridden `get` implementations
|
||||
* (notably in ClosureScope) to prevent accidental ping-pong and infinite recursion across
|
||||
* 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) }
|
||||
return null
|
||||
}
|
||||
|
||||
internal fun chainLookupIgnoreClosure(name: String): ObjRecord? {
|
||||
var s: Scope? = this
|
||||
// use frameId to detect unexpected structural cycles in the parent chain
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) return null
|
||||
tryGetLocalRecord(s, name)?.let { return it }
|
||||
s = s.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform base Scope.get semantics for this frame without delegating into parent.get
|
||||
* virtual dispatch. This checks:
|
||||
* - locals/bindings in this frame
|
||||
* - walks raw parent chain for locals/bindings (ignoring ClosureScope-specific overrides)
|
||||
* - finally falls back to this frame's `thisObj` instance/class members
|
||||
*/
|
||||
internal fun baseGetIgnoreClosure(name: String): ObjRecord? {
|
||||
// 1) locals/bindings in this frame
|
||||
tryGetLocalRecord(this, name)?.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 }
|
||||
s = s.parent
|
||||
}
|
||||
// 3) fallback to instance/class members of this frame's thisObj
|
||||
return thisObj.objClass.getInstanceMemberOrNull(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk the ancestry starting from this scope and try to resolve [name] against:
|
||||
* - locals/bindings of each frame
|
||||
* - then instance/class members of each frame's `thisObj`.
|
||||
* This completely avoids invoking overridden `get` implementations, preventing
|
||||
* ping-pong recursion between `ClosureScope` frames.
|
||||
*/
|
||||
internal fun chainLookupWithMembers(name: String): 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 }
|
||||
s = s.parent
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a non-pooled snapshot of this scope suitable for capturing as a closure environment.
|
||||
* Copies locals, slots, and localBindings; preserves parent chain and class context.
|
||||
*/
|
||||
fun snapshotForClosure(): Scope {
|
||||
val snap = Scope(parent, args, pos, thisObj)
|
||||
snap.currentClassCtx = this.currentClassCtx
|
||||
// copy locals and bindings
|
||||
snap.objects.putAll(this.objects)
|
||||
snap.localBindings.putAll(this.localBindings)
|
||||
// copy slots map preserving indices
|
||||
if (this.slotCount() > 0) {
|
||||
var i = 0
|
||||
while (i < this.slotCount()) {
|
||||
val rec = this.getSlotRecord(i)
|
||||
snap.allocateSlotFor(this.nameToSlot.entries.firstOrNull { it.value == i }?.key ?: "slot${'$'}i", rec)
|
||||
i++
|
||||
}
|
||||
}
|
||||
return snap
|
||||
}
|
||||
|
||||
/**
|
||||
* Hint internal collections to reduce reallocations for upcoming parameter/local assignments.
|
||||
* Only effective for ArrayList-backed slots; maps are left as-is (rehashed lazily by JVM).
|
||||
@@ -200,6 +302,7 @@ open class Scope(
|
||||
* Clears locals and slots, assigns new frameId, and sets parent/args/pos/thisObj.
|
||||
*/
|
||||
fun resetForReuse(parent: Scope?, args: Arguments, pos: Pos, thisObj: Obj) {
|
||||
ensureNoCycle(parent)
|
||||
this.parent = parent
|
||||
this.args = args
|
||||
this.pos = pos
|
||||
@@ -220,7 +323,10 @@ open class Scope(
|
||||
* Creates a new child scope using the provided arguments and optional `thisObj`.
|
||||
*/
|
||||
fun createChildScope(pos: Pos, args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope =
|
||||
Scope(this, args, pos, newThisObj ?: thisObj).also { it.reserveLocalCapacity(args.list.size + 4) }
|
||||
Scope(this, args, pos, newThisObj ?: thisObj).also {
|
||||
it.ensureNoCycle(it.parent)
|
||||
it.reserveLocalCapacity(args.list.size + 4)
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a block inside a child frame. Guarded for future pooling via [PerfFlags.SCOPE_POOL].
|
||||
@@ -249,7 +355,7 @@ open class Scope(
|
||||
* @return A new instance of [Scope] initialized with the specified arguments and `thisObj`.
|
||||
*/
|
||||
fun createChildScope(args: Arguments = Arguments.EMPTY, newThisObj: Obj? = null): Scope =
|
||||
Scope(this, args, pos, newThisObj ?: thisObj)
|
||||
Scope(this, args, pos, newThisObj ?: thisObj).also { it.ensureNoCycle(it.parent) }
|
||||
|
||||
/**
|
||||
* @return A child scope with the same arguments, position and [thisObj]
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
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
|
||||
|
||||
@@ -54,13 +55,16 @@ class ObjDynamicContext(val delegate: ObjDynamic) : Obj() {
|
||||
open class ObjDynamic(var readCallback: Statement? = null, var writeCallback: Statement? = null) : Obj() {
|
||||
|
||||
override val objClass: ObjClass = type
|
||||
// Capture the lexical scope used to build this dynamic so callbacks can see outer locals
|
||||
internal var builderScope: Scope? = null
|
||||
|
||||
/**
|
||||
* Use read callback to dynamically resolve the field name. Note that it does not work
|
||||
* with method invocation which is implemented separately in [invokeInstanceMethod] below.
|
||||
*/
|
||||
override suspend fun readField(scope: Scope, name: String): ObjRecord {
|
||||
return readCallback?.execute(scope.createChildScope(Arguments(ObjString(name))))?.let {
|
||||
val execBase = builderScope?.let { ClosureScope(scope, it) } ?: scope
|
||||
return readCallback?.execute(execBase.createChildScope(Arguments(ObjString(name))))?.let {
|
||||
if (writeCallback != null)
|
||||
it.asMutable
|
||||
else
|
||||
@@ -79,28 +83,27 @@ open class ObjDynamic(var readCallback: Statement? = null, var writeCallback: St
|
||||
args: Arguments,
|
||||
onNotFoundResult: (() -> Obj?)?
|
||||
): Obj {
|
||||
val over = readCallback?.execute(
|
||||
scope.createChildScope(
|
||||
Arguments(ObjString(name)
|
||||
)
|
||||
)
|
||||
)
|
||||
val execBase = builderScope?.let { ClosureScope(scope, it) } ?: scope
|
||||
val over = readCallback?.execute(execBase.createChildScope(Arguments(ObjString(name))))
|
||||
return over?.invoke(scope, scope.thisObj, args)
|
||||
?: super.invokeInstanceMethod(scope, name, args, onNotFoundResult)
|
||||
}
|
||||
|
||||
override suspend fun writeField(scope: Scope, name: String, newValue: Obj) {
|
||||
writeCallback?.execute(scope.createChildScope(Arguments(ObjString(name), newValue)))
|
||||
val execBase = builderScope?.let { ClosureScope(scope, it) } ?: scope
|
||||
writeCallback?.execute(execBase.createChildScope(Arguments(ObjString(name), newValue)))
|
||||
?: super.writeField(scope, name, newValue)
|
||||
}
|
||||
|
||||
override suspend fun getAt(scope: Scope, index: Obj): Obj {
|
||||
return readCallback?.execute(scope.createChildScope(Arguments(index)))
|
||||
val execBase = builderScope?.let { ClosureScope(scope, it) } ?: scope
|
||||
return readCallback?.execute(execBase.createChildScope(Arguments(index)))
|
||||
?: super.getAt(scope, index)
|
||||
}
|
||||
|
||||
override suspend fun putAt(scope: Scope, index: Obj, newValue: Obj) {
|
||||
writeCallback?.execute(scope.createChildScope(Arguments(index, newValue)))
|
||||
val execBase = builderScope?.let { ClosureScope(scope, it) } ?: scope
|
||||
writeCallback?.execute(execBase.createChildScope(Arguments(index, newValue)))
|
||||
?: super.putAt(scope, index, newValue)
|
||||
}
|
||||
|
||||
@@ -109,7 +112,12 @@ open class ObjDynamic(var readCallback: Statement? = null, var writeCallback: St
|
||||
suspend fun create(scope: Scope, builder: Statement): ObjDynamic {
|
||||
val delegate = ObjDynamic()
|
||||
val context = ObjDynamicContext(delegate)
|
||||
builder.execute(scope.createChildScope(newThisObj = context))
|
||||
// Capture the function's lexical scope (scope) so callbacks can see outer locals like parameters.
|
||||
// Build the dynamic in a child scope purely to set `this` to context, but keep captured closure at parent.
|
||||
val buildScope = scope.createChildScope(newThisObj = context)
|
||||
// Snapshot the caller scope to capture locals/args even if the runtime pools/reuses frames
|
||||
delegate.builderScope = scope.snapshotForClosure()
|
||||
builder.execute(buildScope)
|
||||
return delegate
|
||||
}
|
||||
|
||||
|
||||
@@ -198,6 +198,10 @@ open class ObjException(
|
||||
)) {
|
||||
scope.addConst(name, getOrCreateExceptionClass(name))
|
||||
}
|
||||
// Backward compatibility alias used in older tests/docs
|
||||
val snd = getOrCreateExceptionClass("SymbolNotDefinedException")
|
||||
scope.addConst("SymbolNotFound", snd)
|
||||
existingErrorClasses["SymbolNotFound"] = snd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1240,7 +1240,28 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
||||
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.localVarPicMiss++
|
||||
// 2) Fallback to current-scope object or field on `this`
|
||||
scope[name]?.let { return it }
|
||||
return scope.thisObj.readField(scope, name)
|
||||
// 2a) Try nearest ClosureScope's closure ancestry explicitly
|
||||
run {
|
||||
var s: Scope? = scope
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) break
|
||||
if (s is ClosureScope) {
|
||||
s.closureScope.chainLookupWithMembers(name)?.let { return it }
|
||||
}
|
||||
s = s.parent
|
||||
}
|
||||
}
|
||||
// 2b) Try raw ancestry local/binding lookup (cycle-safe), including slots in parents
|
||||
scope.chainLookupIgnoreClosure(name)?.let { return it }
|
||||
try {
|
||||
return scope.thisObj.readField(scope, name)
|
||||
} catch (e: ExecutionError) {
|
||||
// Map missing symbol during unqualified lookup to SymbolNotFound (SymbolNotDefinedException)
|
||||
// to preserve legacy behavior expected by tests.
|
||||
if ((e.message ?: "").contains("no such field: $name")) scope.raiseSymbolNotFound(name)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
|
||||
val slot = if (hit) cachedSlot else resolveSlot(scope)
|
||||
@@ -1253,7 +1274,24 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
||||
if (PerfFlags.PIC_DEBUG_COUNTERS) PerfStats.localVarPicMiss++
|
||||
// 2) Fallback name in scope or field on `this`
|
||||
scope[name]?.let { return it }
|
||||
return scope.thisObj.readField(scope, name)
|
||||
run {
|
||||
var s: Scope? = scope
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) break
|
||||
if (s is ClosureScope) {
|
||||
s.closureScope.chainLookupWithMembers(name)?.let { return it }
|
||||
}
|
||||
s = s.parent
|
||||
}
|
||||
}
|
||||
scope.chainLookupIgnoreClosure(name)?.let { return it }
|
||||
try {
|
||||
return scope.thisObj.readField(scope, name)
|
||||
} catch (e: ExecutionError) {
|
||||
if ((e.message ?: "").contains("no such field: $name")) scope.raiseSymbolNotFound(name)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun evalValue(scope: Scope): Obj {
|
||||
@@ -1262,14 +1300,48 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
||||
scope.getSlotIndexOf(name)?.let { return scope.getSlotRecord(it).value }
|
||||
// fallback to current-scope object or field on `this`
|
||||
scope[name]?.let { return it.value }
|
||||
return scope.thisObj.readField(scope, name).value
|
||||
run {
|
||||
var s: Scope? = scope
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) break
|
||||
if (s is ClosureScope) {
|
||||
s.closureScope.chainLookupWithMembers(name)?.let { return it.value }
|
||||
}
|
||||
s = s.parent
|
||||
}
|
||||
}
|
||||
scope.chainLookupIgnoreClosure(name)?.let { return it.value }
|
||||
return try {
|
||||
scope.thisObj.readField(scope, name).value
|
||||
} catch (e: ExecutionError) {
|
||||
if ((e.message ?: "").contains("no such field: $name")) scope.raiseSymbolNotFound(name)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
val hit = (cachedFrameId == scope.frameId && cachedSlot >= 0 && cachedSlot < scope.slotCount())
|
||||
val slot = if (hit) cachedSlot else resolveSlot(scope)
|
||||
if (slot >= 0) return scope.getSlotRecord(slot).value
|
||||
// Fallback name in scope or field on `this`
|
||||
scope[name]?.let { return it.value }
|
||||
return scope.thisObj.readField(scope, name).value
|
||||
run {
|
||||
var s: Scope? = scope
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) break
|
||||
if (s is ClosureScope) {
|
||||
s.closureScope.chainLookupWithMembers(name)?.let { return it.value }
|
||||
}
|
||||
s = s.parent
|
||||
}
|
||||
}
|
||||
scope.chainLookupIgnoreClosure(name)?.let { return it.value }
|
||||
return try {
|
||||
scope.thisObj.readField(scope, name).value
|
||||
} catch (e: ExecutionError) {
|
||||
if ((e.message ?: "").contains("no such field: $name")) scope.raiseSymbolNotFound(name)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setAt(pos: Pos, scope: Scope, newValue: Obj) {
|
||||
@@ -1286,6 +1358,26 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
||||
else scope.raiseError("Cannot assign to immutable value")
|
||||
return
|
||||
}
|
||||
run {
|
||||
var s: Scope? = scope
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) break
|
||||
if (s is ClosureScope) {
|
||||
s.closureScope.chainLookupWithMembers(name)?.let { stored ->
|
||||
if (stored.isMutable) stored.value = newValue
|
||||
else scope.raiseError("Cannot assign to immutable value")
|
||||
return
|
||||
}
|
||||
}
|
||||
s = s.parent
|
||||
}
|
||||
}
|
||||
scope.chainLookupIgnoreClosure(name)?.let { stored ->
|
||||
if (stored.isMutable) stored.value = newValue
|
||||
else scope.raiseError("Cannot assign to immutable value")
|
||||
return
|
||||
}
|
||||
// Fallback: write to field on `this`
|
||||
scope.thisObj.writeField(scope, name, newValue)
|
||||
return
|
||||
@@ -1302,6 +1394,26 @@ class LocalVarRef(private val name: String, private val atPos: Pos) : ObjRef {
|
||||
else scope.raiseError("Cannot assign to immutable value")
|
||||
return
|
||||
}
|
||||
run {
|
||||
var s: Scope? = scope
|
||||
val visited = HashSet<Long>(4)
|
||||
while (s != null) {
|
||||
if (!visited.add(s.frameId)) break
|
||||
if (s is ClosureScope) {
|
||||
s.closureScope.chainLookupWithMembers(name)?.let { stored ->
|
||||
if (stored.isMutable) stored.value = newValue
|
||||
else scope.raiseError("Cannot assign to immutable value")
|
||||
return
|
||||
}
|
||||
}
|
||||
s = s.parent
|
||||
}
|
||||
}
|
||||
scope.chainLookupIgnoreClosure(name)?.let { stored ->
|
||||
if (stored.isMutable) stored.value = newValue
|
||||
else scope.raiseError("Cannot assign to immutable value")
|
||||
return
|
||||
}
|
||||
scope.thisObj.writeField(scope, name, newValue)
|
||||
return
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user