From d91acd593a4236755dfbe00af75613634fc1eb17 Mon Sep 17 00:00:00 2001 From: sergeych Date: Tue, 6 Jan 2026 02:18:31 +0100 Subject: [PATCH] added return statement support --- CHANGELOG.md | 9 ++ README.md | 1 + docs/return_statement.md | 86 ++++++++++++ docs/tutorial.md | 11 +- .../kotlin/net/sergeych/lyng/Compiler.kt | 69 +++++++++- .../net/sergeych/lyng/ReturnException.kt | 30 ++++ .../commonTest/kotlin/ReturnStatementTest.kt | 128 ++++++++++++++++++ 7 files changed, 330 insertions(+), 4 deletions(-) create mode 100644 docs/return_statement.md create mode 100644 lynglib/src/commonMain/kotlin/net/sergeych/lyng/ReturnException.kt create mode 100644 lynglib/src/commonTest/kotlin/ReturnStatementTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 2da2c2a..3104e0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 4a80c02..78355f6 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/docs/return_statement.md b/docs/return_statement.md new file mode 100644 index 0000000..e32915f --- /dev/null +++ b/docs/return_statement.md @@ -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`. diff --git a/docs/tutorial.md b/docs/tutorial.md index fafff51..4738a2d 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -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() { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index ccd201e..51c3023 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -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 = 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) { diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ReturnException.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ReturnException.kt new file mode 100644 index 0000000..7dcc569 --- /dev/null +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ReturnException.kt @@ -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() diff --git a/lynglib/src/commonTest/kotlin/ReturnStatementTest.kt b/lynglib/src/commonTest/kotlin/ReturnStatementTest.kt new file mode 100644 index 0000000..4de7e79 --- /dev/null +++ b/lynglib/src/commonTest/kotlin/ReturnStatementTest.kt @@ -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 { + 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()) + } +}