added return statement support

This commit is contained in:
Sergey Chernov 2026-01-06 02:18:31 +01:00
parent 5fc0969491
commit d91acd593a
7 changed files with 330 additions and 4 deletions

View File

@ -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

View File

@ -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
View 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`.

View File

@ -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() {

View File

@ -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) {

View File

@ -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()

View 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())
}
}