added return statement support
This commit is contained in:
parent
5fc0969491
commit
d91acd593a
@ -2,6 +2,15 @@
|
||||
|
||||
### 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
|
||||
- Added `with(self, block)` function to `root.lyng` which executes a block with `this` set to the provided object.
|
||||
- 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)
|
||||
- [introduction and tutorial](docs/tutorial.md) - start here please
|
||||
- [Testing and Assertions](docs/Testing.md)
|
||||
- [Return Statement](docs/return_statement.md)
|
||||
- [Efficient Iterables in Kotlin Interop](docs/EfficientIterables.md)
|
||||
- [Samples directory](docs/samples)
|
||||
- [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)
|
||||
- [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)
|
||||
- [time](time.md) and [parallelism](parallelism.md)
|
||||
- [parallelism] - multithreaded code, coroutines, etc.
|
||||
@ -32,6 +32,15 @@ any block also returns it's last expression:
|
||||
}
|
||||
>>> 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`:
|
||||
|
||||
fn voidFunction() {
|
||||
|
||||
@ -285,9 +285,11 @@ class Compiler(
|
||||
}
|
||||
|
||||
private var lastAnnotation: (suspend (Scope, ObjString, Statement) -> Statement)? = null
|
||||
private var lastLabel: String? = null
|
||||
|
||||
private suspend fun parseStatement(braceMeansLambda: Boolean = false): Statement? {
|
||||
lastAnnotation = null
|
||||
lastLabel = null
|
||||
while (true) {
|
||||
val t = cc.next()
|
||||
return when (t.type) {
|
||||
@ -305,6 +307,10 @@ class Compiler(
|
||||
}
|
||||
|
||||
Token.Type.ATLABEL -> {
|
||||
val label = t.value
|
||||
if (cc.peekNextNonWhitespace().type == Token.Type.LBRACE) {
|
||||
lastLabel = label
|
||||
}
|
||||
lastAnnotation = parseAnnotation(t)
|
||||
continue
|
||||
}
|
||||
@ -653,6 +659,7 @@ class Compiler(
|
||||
private suspend fun parseLambdaExpression(): ObjRef {
|
||||
// lambda args are different:
|
||||
val startPos = cc.currentPos()
|
||||
val label = lastLabel
|
||||
val argsDeclaration = parseArgsDeclaration()
|
||||
if (argsDeclaration != null && argsDeclaration.endTokenType != Token.Type.ARROW)
|
||||
throw ScriptError(
|
||||
@ -660,7 +667,9 @@ class Compiler(
|
||||
"lambda must have either valid arguments declaration with '->' or no arguments"
|
||||
)
|
||||
|
||||
label?.let { cc.labels.add(it) }
|
||||
val body = parseBlock(skipLeadingBrace = true)
|
||||
label?.let { cc.labels.remove(it) }
|
||||
|
||||
return ValueFnRef { closureScope ->
|
||||
statement {
|
||||
@ -684,7 +693,12 @@ class Compiler(
|
||||
// assign vars as declared the standard way
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -1425,6 +1439,7 @@ class Compiler(
|
||||
"while" -> parseWhileStatement()
|
||||
"do" -> parseDoWhileStatement()
|
||||
"for" -> parseForStatement()
|
||||
"return" -> parseReturnStatement(id.pos)
|
||||
"break" -> parseBreakStatement(id.pos)
|
||||
"continue" -> parseContinueStatement(id.pos)
|
||||
"if" -> parseIfStatement()
|
||||
@ -1780,6 +1795,10 @@ class Compiler(
|
||||
try {
|
||||
// body is a parsed block, it already has separate context
|
||||
result = body.execute(this)
|
||||
} catch (e: ReturnException) {
|
||||
throw e
|
||||
} catch (e: LoopBreakContinueException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
// convert to appropriate exception
|
||||
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 {
|
||||
val t = cc.next()
|
||||
if (t.type != Token.Type.RPAREN)
|
||||
@ -2601,6 +2652,7 @@ class Compiler(
|
||||
|
||||
// Capture doc locally to reuse even if we need to emit later
|
||||
val declDocLocal = pendingDeclDoc
|
||||
val outerLabel = lastLabel
|
||||
|
||||
// Emit MiniFunDecl before body parsing (body range unknown yet)
|
||||
run {
|
||||
@ -2627,6 +2679,8 @@ class Compiler(
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@ -2642,7 +2696,9 @@ class Compiler(
|
||||
val next = cc.peekNextNonWhitespace()
|
||||
if (next.type == Token.Type.ASSIGN) {
|
||||
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
|
||||
statement(expr.pos) { scope ->
|
||||
expr.execute(scope)
|
||||
@ -2674,8 +2730,15 @@ class Compiler(
|
||||
if (extTypeName != null) {
|
||||
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
|
||||
val fnCreateStatement = statement(start) { context ->
|
||||
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