added return statement support
This commit is contained in:
parent
5fc0969491
commit
d91acd593a
@ -2,6 +2,15 @@
|
|||||||
|
|
||||||
### Unreleased
|
### Unreleased
|
||||||
|
|
||||||
|
- Language: Added `return` statement
|
||||||
|
- `return [expression]` exits the innermost enclosing callable (function or lambda).
|
||||||
|
- Supports non-local returns using `@label` syntax (e.g., `return@outer 42`).
|
||||||
|
- Named functions automatically provide their name as a label for non-local returns.
|
||||||
|
- Labeled lambdas: lambdas can be explicitly labeled using `@label { ... }`.
|
||||||
|
- Restriction: `return` is forbidden in shorthand function definitions (e.g., `fun f(x) = return x` is a syntax error).
|
||||||
|
- Control Flow: `return` and `break` are now protected from being caught by user-defined `try-catch` blocks in Lyng.
|
||||||
|
- Documentation: New `docs/return_statement.md` and updated `tutorial.md`.
|
||||||
|
|
||||||
- Language: stdlib improvements
|
- Language: stdlib improvements
|
||||||
- Added `with(self, block)` function to `root.lyng` which executes a block with `this` set to the provided object.
|
- Added `with(self, block)` function to `root.lyng` which executes a block with `this` set to the provided object.
|
||||||
- Language: Abstract Classes and Interfaces
|
- Language: Abstract Classes and Interfaces
|
||||||
|
|||||||
@ -44,6 +44,7 @@ and it is multithreaded on platforms supporting it (automatically, no code chang
|
|||||||
- [Language home](https://lynglang.com)
|
- [Language home](https://lynglang.com)
|
||||||
- [introduction and tutorial](docs/tutorial.md) - start here please
|
- [introduction and tutorial](docs/tutorial.md) - start here please
|
||||||
- [Testing and Assertions](docs/Testing.md)
|
- [Testing and Assertions](docs/Testing.md)
|
||||||
|
- [Return Statement](docs/return_statement.md)
|
||||||
- [Efficient Iterables in Kotlin Interop](docs/EfficientIterables.md)
|
- [Efficient Iterables in Kotlin Interop](docs/EfficientIterables.md)
|
||||||
- [Samples directory](docs/samples)
|
- [Samples directory](docs/samples)
|
||||||
- [Formatter (core + CLI + IDE)](docs/formatter.md)
|
- [Formatter (core + CLI + IDE)](docs/formatter.md)
|
||||||
|
|||||||
86
docs/return_statement.md
Normal file
86
docs/return_statement.md
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
# The `return` statement
|
||||||
|
|
||||||
|
The `return` statement is used to terminate the execution of the innermost enclosing callable (a function or a lambda) and optionally return a value to the caller.
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
By default, Lyng functions and blocks return the value of their last expression. However, `return` allows you to exit early, which is particularly useful for guard clauses.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
fun divide(a, b) {
|
||||||
|
if (b == 0) return null // Guard clause: early exit
|
||||||
|
a / b
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If no expression is provided, `return` returns `void`:
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
fun logIfDebug(msg) {
|
||||||
|
if (!DEBUG) return
|
||||||
|
println("[DEBUG] " + msg)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scoping Rules
|
||||||
|
|
||||||
|
In Lyng, `return` always exits the **innermost enclosing callable**. Callables include:
|
||||||
|
* Named functions (`fun` or `fn`)
|
||||||
|
* Anonymous functions/lambdas (`{ ... }`)
|
||||||
|
|
||||||
|
Standard control flow blocks like `if`, `while`, `do`, and `for` are **not** callables; `return` inside these blocks will return from the function or lambda that contains them.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
fun findFirstPositive(list) {
|
||||||
|
list.forEach {
|
||||||
|
if (it > 0) return it // ERROR: This returns from the lambda, not findFirstPositive!
|
||||||
|
}
|
||||||
|
null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
*Note: To return from an outer scope, use [Non-local Returns](#non-local-returns).*
|
||||||
|
|
||||||
|
## Non-local Returns
|
||||||
|
|
||||||
|
Lyng supports returning from outer scopes using labels. This is a powerful feature for a closure-intensive language.
|
||||||
|
|
||||||
|
### Named Functions as Labels
|
||||||
|
Every named function automatically provides its name as a label.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
fun findFirstPositive(list) {
|
||||||
|
list.forEach {
|
||||||
|
if (it > 0) return@findFirstPositive it // Returns from findFirstPositive
|
||||||
|
}
|
||||||
|
null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Labeled Lambdas
|
||||||
|
You can explicitly label a lambda using the `@label` syntax to return from it specifically when nested.
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
val process = @outer { x ->
|
||||||
|
val result = {
|
||||||
|
if (x < 0) return@outer "negative" // Returns from the outer lambda
|
||||||
|
x * 2
|
||||||
|
}()
|
||||||
|
"Result: " + result
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Restriction on Shorthand Functions
|
||||||
|
|
||||||
|
To maintain Lyng's clean, expression-oriented style, the `return` keyword is **forbidden** in shorthand function definitions (those using `=`).
|
||||||
|
|
||||||
|
```lyng
|
||||||
|
fun square(x) = x * x // Correct
|
||||||
|
fun square(x) = return x * x // Syntax Error: 'return' not allowed here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
* `return [expression]` exits the innermost `fun` or `{}`.
|
||||||
|
* Use `return@label` for non-local returns.
|
||||||
|
* Named functions provide automatic labels.
|
||||||
|
* Cannot be used in `=` shorthand functions.
|
||||||
|
* Consistency: Mirrors the syntax and behavior of `break@label expression`.
|
||||||
@ -8,7 +8,7 @@ __Other documents to read__ maybe after this one:
|
|||||||
|
|
||||||
- [Advanced topics](advanced_topics.md), [declaring arguments](declaring_arguments.md), [Scopes and Closures](scopes_and_closures.md)
|
- [Advanced topics](advanced_topics.md), [declaring arguments](declaring_arguments.md), [Scopes and Closures](scopes_and_closures.md)
|
||||||
- [OOP notes](OOP.md), [exception handling](exceptions_handling.md)
|
- [OOP notes](OOP.md), [exception handling](exceptions_handling.md)
|
||||||
- [math in Lyng](math.md), [the `when` statement](when.md)
|
- [math in Lyng](math.md), [the `when` statement](when.md), [return statement](return_statement.md)
|
||||||
- [Testing and Assertions](Testing.md)
|
- [Testing and Assertions](Testing.md)
|
||||||
- [time](time.md) and [parallelism](parallelism.md)
|
- [time](time.md) and [parallelism](parallelism.md)
|
||||||
- [parallelism] - multithreaded code, coroutines, etc.
|
- [parallelism] - multithreaded code, coroutines, etc.
|
||||||
@ -32,6 +32,15 @@ any block also returns it's last expression:
|
|||||||
}
|
}
|
||||||
>>> 6
|
>>> 6
|
||||||
|
|
||||||
|
If you want to exit a function or lambda earlier, use the `return` statement:
|
||||||
|
|
||||||
|
fn divide(a, b) {
|
||||||
|
if( b == 0 ) return null
|
||||||
|
a / b
|
||||||
|
}
|
||||||
|
|
||||||
|
See [return statement](return_statement.md) for more details on scoping and non-local returns.
|
||||||
|
|
||||||
If you don't want block to return anything, use `void`:
|
If you don't want block to return anything, use `void`:
|
||||||
|
|
||||||
fn voidFunction() {
|
fn voidFunction() {
|
||||||
|
|||||||
@ -285,9 +285,11 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var lastAnnotation: (suspend (Scope, ObjString, Statement) -> Statement)? = null
|
private var lastAnnotation: (suspend (Scope, ObjString, Statement) -> Statement)? = null
|
||||||
|
private var lastLabel: String? = null
|
||||||
|
|
||||||
private suspend fun parseStatement(braceMeansLambda: Boolean = false): Statement? {
|
private suspend fun parseStatement(braceMeansLambda: Boolean = false): Statement? {
|
||||||
lastAnnotation = null
|
lastAnnotation = null
|
||||||
|
lastLabel = null
|
||||||
while (true) {
|
while (true) {
|
||||||
val t = cc.next()
|
val t = cc.next()
|
||||||
return when (t.type) {
|
return when (t.type) {
|
||||||
@ -305,6 +307,10 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Token.Type.ATLABEL -> {
|
Token.Type.ATLABEL -> {
|
||||||
|
val label = t.value
|
||||||
|
if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
|
||||||
|
lastLabel = label
|
||||||
|
}
|
||||||
lastAnnotation = parseAnnotation(t)
|
lastAnnotation = parseAnnotation(t)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -653,6 +659,7 @@ class Compiler(
|
|||||||
private suspend fun parseLambdaExpression(): ObjRef {
|
private suspend fun parseLambdaExpression(): ObjRef {
|
||||||
// lambda args are different:
|
// lambda args are different:
|
||||||
val startPos = cc.currentPos()
|
val startPos = cc.currentPos()
|
||||||
|
val label = lastLabel
|
||||||
val argsDeclaration = parseArgsDeclaration()
|
val argsDeclaration = parseArgsDeclaration()
|
||||||
if (argsDeclaration != null && argsDeclaration.endTokenType != Token.Type.ARROW)
|
if (argsDeclaration != null && argsDeclaration.endTokenType != Token.Type.ARROW)
|
||||||
throw ScriptError(
|
throw ScriptError(
|
||||||
@ -660,7 +667,9 @@ class Compiler(
|
|||||||
"lambda must have either valid arguments declaration with '->' or no arguments"
|
"lambda must have either valid arguments declaration with '->' or no arguments"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
label?.let { cc.labels.add(it) }
|
||||||
val body = parseBlock(skipLeadingBrace = true)
|
val body = parseBlock(skipLeadingBrace = true)
|
||||||
|
label?.let { cc.labels.remove(it) }
|
||||||
|
|
||||||
return ValueFnRef { closureScope ->
|
return ValueFnRef { closureScope ->
|
||||||
statement {
|
statement {
|
||||||
@ -684,7 +693,12 @@ class Compiler(
|
|||||||
// assign vars as declared the standard way
|
// assign vars as declared the standard way
|
||||||
argsDeclaration.assignToContext(context, defaultAccessType = AccessType.Val)
|
argsDeclaration.assignToContext(context, defaultAccessType = AccessType.Val)
|
||||||
}
|
}
|
||||||
body.execute(context)
|
try {
|
||||||
|
body.execute(context)
|
||||||
|
} catch (e: ReturnException) {
|
||||||
|
if (e.label == null || e.label == label) e.result
|
||||||
|
else throw e
|
||||||
|
}
|
||||||
}.asReadonly
|
}.asReadonly
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1425,6 +1439,7 @@ class Compiler(
|
|||||||
"while" -> parseWhileStatement()
|
"while" -> parseWhileStatement()
|
||||||
"do" -> parseDoWhileStatement()
|
"do" -> parseDoWhileStatement()
|
||||||
"for" -> parseForStatement()
|
"for" -> parseForStatement()
|
||||||
|
"return" -> parseReturnStatement(id.pos)
|
||||||
"break" -> parseBreakStatement(id.pos)
|
"break" -> parseBreakStatement(id.pos)
|
||||||
"continue" -> parseContinueStatement(id.pos)
|
"continue" -> parseContinueStatement(id.pos)
|
||||||
"if" -> parseIfStatement()
|
"if" -> parseIfStatement()
|
||||||
@ -1780,6 +1795,10 @@ class Compiler(
|
|||||||
try {
|
try {
|
||||||
// body is a parsed block, it already has separate context
|
// body is a parsed block, it already has separate context
|
||||||
result = body.execute(this)
|
result = body.execute(this)
|
||||||
|
} catch (e: ReturnException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: LoopBreakContinueException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// convert to appropriate exception
|
// convert to appropriate exception
|
||||||
val objException = when (e) {
|
val objException = when (e) {
|
||||||
@ -2488,6 +2507,38 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun parseReturnStatement(start: Pos): Statement {
|
||||||
|
var t = cc.next()
|
||||||
|
|
||||||
|
val label = if (t.pos.line != start.line || t.type != Token.Type.ATLABEL) {
|
||||||
|
cc.previous()
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
t.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// expression?
|
||||||
|
t = cc.next()
|
||||||
|
cc.previous()
|
||||||
|
val resultExpr = if (t.pos.line == start.line && (!t.isComment &&
|
||||||
|
t.type != Token.Type.SEMICOLON &&
|
||||||
|
t.type != Token.Type.NEWLINE &&
|
||||||
|
t.type != Token.Type.RBRACE &&
|
||||||
|
t.type != Token.Type.RPAREN &&
|
||||||
|
t.type != Token.Type.RBRACKET &&
|
||||||
|
t.type != Token.Type.COMMA &&
|
||||||
|
t.type != Token.Type.EOF)
|
||||||
|
) {
|
||||||
|
// we have something on this line, could be expression
|
||||||
|
parseExpression()
|
||||||
|
} else null
|
||||||
|
|
||||||
|
return statement(start) {
|
||||||
|
val returnValue = resultExpr?.execute(it) ?: ObjVoid
|
||||||
|
throw ReturnException(returnValue, label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun ensureRparen(): Pos {
|
private fun ensureRparen(): Pos {
|
||||||
val t = cc.next()
|
val t = cc.next()
|
||||||
if (t.type != Token.Type.RPAREN)
|
if (t.type != Token.Type.RPAREN)
|
||||||
@ -2601,6 +2652,7 @@ class Compiler(
|
|||||||
|
|
||||||
// Capture doc locally to reuse even if we need to emit later
|
// Capture doc locally to reuse even if we need to emit later
|
||||||
val declDocLocal = pendingDeclDoc
|
val declDocLocal = pendingDeclDoc
|
||||||
|
val outerLabel = lastLabel
|
||||||
|
|
||||||
// Emit MiniFunDecl before body parsing (body range unknown yet)
|
// Emit MiniFunDecl before body parsing (body range unknown yet)
|
||||||
run {
|
run {
|
||||||
@ -2627,6 +2679,8 @@ class Compiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return inCodeContext(CodeContext.Function(name)) {
|
return inCodeContext(CodeContext.Function(name)) {
|
||||||
|
cc.labels.add(name)
|
||||||
|
outerLabel?.let { cc.labels.add(it) }
|
||||||
|
|
||||||
val paramNames: Set<String> = argsDeclaration.params.map { it.name }.toSet()
|
val paramNames: Set<String> = argsDeclaration.params.map { it.name }.toSet()
|
||||||
|
|
||||||
@ -2642,7 +2696,9 @@ class Compiler(
|
|||||||
val next = cc.peekNextNonWhitespace()
|
val next = cc.peekNextNonWhitespace()
|
||||||
if (next.type == Token.Type.ASSIGN) {
|
if (next.type == Token.Type.ASSIGN) {
|
||||||
cc.nextNonWhitespace() // consume '='
|
cc.nextNonWhitespace() // consume '='
|
||||||
val expr = parseExpression() ?: throw ScriptError(cc.current().pos, "Expected function body expression")
|
if (cc.peekNextNonWhitespace().value == "return")
|
||||||
|
throw ScriptError(cc.currentPos(), "return is not allowed in shorthand function")
|
||||||
|
val expr = parseExpression() ?: throw ScriptError(cc.currentPos(), "Expected function body expression")
|
||||||
// Shorthand function returns the expression value
|
// Shorthand function returns the expression value
|
||||||
statement(expr.pos) { scope ->
|
statement(expr.pos) { scope ->
|
||||||
expr.execute(scope)
|
expr.execute(scope)
|
||||||
@ -2674,8 +2730,15 @@ class Compiler(
|
|||||||
if (extTypeName != null) {
|
if (extTypeName != null) {
|
||||||
context.thisObj = callerContext.thisObj
|
context.thisObj = callerContext.thisObj
|
||||||
}
|
}
|
||||||
fnStatements?.execute(context) ?: ObjVoid
|
try {
|
||||||
|
fnStatements?.execute(context) ?: ObjVoid
|
||||||
|
} catch (e: ReturnException) {
|
||||||
|
if (e.label == null || e.label == name || e.label == outerLabel) e.result
|
||||||
|
else throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
cc.labels.remove(name)
|
||||||
|
outerLabel?.let { cc.labels.remove(it) }
|
||||||
// parentContext
|
// parentContext
|
||||||
val fnCreateStatement = statement(start) { context ->
|
val fnCreateStatement = statement(start) { context ->
|
||||||
if (isDelegated) {
|
if (isDelegated) {
|
||||||
|
|||||||
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 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
|
||||||
|
|
||||||
|
import net.sergeych.lyng.obj.Obj
|
||||||
|
import net.sergeych.lyng.obj.ObjVoid
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception used to implement `return` statement.
|
||||||
|
* It carries the return value and an optional label for non-local returns.
|
||||||
|
*/
|
||||||
|
class ReturnException(
|
||||||
|
val result: Obj = ObjVoid,
|
||||||
|
val label: String? = null
|
||||||
|
) : RuntimeException()
|
||||||
128
lynglib/src/commonTest/kotlin/ReturnStatementTest.kt
Normal file
128
lynglib/src/commonTest/kotlin/ReturnStatementTest.kt
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import net.sergeych.lyng.ScriptError
|
||||||
|
import net.sergeych.lyng.eval
|
||||||
|
import net.sergeych.lyng.obj.toInt
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFailsWith
|
||||||
|
|
||||||
|
class ReturnStatementTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testBasicReturn() = runTest {
|
||||||
|
assertEquals(10, eval("""
|
||||||
|
fun foo() {
|
||||||
|
return 10
|
||||||
|
20
|
||||||
|
}
|
||||||
|
foo()
|
||||||
|
""").toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testReturnFromIf() = runTest {
|
||||||
|
assertEquals(5, eval("""
|
||||||
|
fun foo(x) {
|
||||||
|
if (x > 0) return 5
|
||||||
|
10
|
||||||
|
}
|
||||||
|
foo(1)
|
||||||
|
""").toInt())
|
||||||
|
|
||||||
|
assertEquals(10, eval("""
|
||||||
|
fun foo(x) {
|
||||||
|
if (x > 0) return 5
|
||||||
|
10
|
||||||
|
}
|
||||||
|
foo(-1)
|
||||||
|
""").toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testReturnFromLambda() = runTest {
|
||||||
|
assertEquals(2, eval("""
|
||||||
|
val f = { x ->
|
||||||
|
if (x < 0) return 0
|
||||||
|
x * 2
|
||||||
|
}
|
||||||
|
f(1)
|
||||||
|
""").toInt())
|
||||||
|
|
||||||
|
assertEquals(0, eval("""
|
||||||
|
val f = { x ->
|
||||||
|
if (x < 0) return 0
|
||||||
|
x * 2
|
||||||
|
}
|
||||||
|
f(-1)
|
||||||
|
""").toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testNonLocalReturn() = runTest {
|
||||||
|
assertEquals(100, eval("""
|
||||||
|
fun outer() {
|
||||||
|
[1, 2, 3].forEach {
|
||||||
|
if (it == 2) return@outer 100
|
||||||
|
}
|
||||||
|
0
|
||||||
|
}
|
||||||
|
outer()
|
||||||
|
""").toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testLabeledLambdaReturn() = runTest {
|
||||||
|
assertEquals(42, eval("""
|
||||||
|
val f = @inner { x ->
|
||||||
|
if (x == 0) return@inner 42
|
||||||
|
x
|
||||||
|
}
|
||||||
|
f(0)
|
||||||
|
""").toInt())
|
||||||
|
|
||||||
|
assertEquals(5, eval("""
|
||||||
|
val f = @inner { x ->
|
||||||
|
if (x == 0) return@inner 42
|
||||||
|
x
|
||||||
|
}
|
||||||
|
f(5)
|
||||||
|
""").toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testForbidEqualReturn() = runTest {
|
||||||
|
assertFailsWith<ScriptError> {
|
||||||
|
eval("fun foo(x) = return x")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDeepNestedReturn() = runTest {
|
||||||
|
assertEquals(42, eval("""
|
||||||
|
fun find() {
|
||||||
|
val data = [[1, 2], [3, 42], [5, 6]]
|
||||||
|
data.forEach { row ->
|
||||||
|
row.forEach { item ->
|
||||||
|
if (item == 42) return@find item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0
|
||||||
|
}
|
||||||
|
find()
|
||||||
|
""").toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testReturnFromOuterLambda() = runTest {
|
||||||
|
assertEquals("found", eval("""
|
||||||
|
val f_outer = @outer {
|
||||||
|
val f_inner = {
|
||||||
|
return@outer "found"
|
||||||
|
}
|
||||||
|
f_inner()
|
||||||
|
"not found"
|
||||||
|
}
|
||||||
|
f_outer()
|
||||||
|
""").toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user