Developers regularly ask for a lightweight way to exit a function before its final expression. Today they must emulate early exits using nested conditionals, exceptions, or helper functions, which obscures intent and bloats JavaScript output. Supporting a first-class return keyword improves readability, enables more idiomatic interop with JavaScript, and narrows the ergonomics gap with other languages while preserving ReScript's expression-oriented style.
- Introduce a
returnexpression that exits the innermost function, optionally carrying a value (return expror barereturn). - Type-check
returnso that subsequent code is treated as unreachable, avoiding spurious exhaustiveness warnings. - Emit direct JavaScript
returnstatements to make async andtryinteractions behave exactly like plain JS. - Preserve backward compatibility for existing code that does not use
return.
- Adding multi-value returns or early exit for non-function constructs (loops, switches without functions, etc.).
- Introducing new runtime constructs beyond the emitted JavaScript
return. - Changing module-level or top-level behaviour;
returnremains illegal outside function bodies.
returnis an expression with the bottom-like typenever. The payload, when present, must unify with the enclosing function's declared result.returntargets only the innermost function scope, including anonymous functions and closures.- A bare
returnis sugar for returningunit, still typed asnever. - Once a
returnis evaluated, control flow stops at that point; subsequent expressions in the same block are unreachable.
- Extend the grammar handled in
compiler/syntax/src/res_parser.ml(and related helpers such asres_grammar.ml) to parsereturnas an expression with an optional trailing payload (returnorreturn expr). - Add
Pexp_return of expression optiontoparsetree.ml, and update related helpers (ast_iterator, printers, etc.). - Mirror the changes in
ast_mapper_from0.mlandast_mapper_to0.mlto maintain compatibility withparsetree0.ml(which must stay frozen). - Update syntax error recovery to produce messages such as “
returnis only allowed inside function bodies" when seen in invalid positions. - Add new parser fixtures under
tests/syntax_tests/(positive and negative cases).
- Introduce
Texp_returninTypedtree.expression_desc(updatecompiler/ml/typedtree.mliandtypedtree.ml) and thread it through the existing iterators/printers. - Extend
typecore.mlto:- Reject uses outside functions by reusing the existing optional
in_functionplumbing thattype_functionalready threads through. - Type-check the optional payload against the enclosing function's result type.
- Reject uses outside functions by reusing the existing optional
- Populate the new node with a freshly created type variable (mirroring how
%raiseis typed today) so downstream phases treat it as non-returning without introducing a bespoke primitive type.- Emit appropriate errors on context or payload mismatches.
- Keep
type_statementwarning behaviour intact soreturninherits the existingWarnings.Nonreturning_statementflow (compiler/ml/typecore.ml:3884-3894).
- If the type-variable approach proves insufficient, adding an explicit bottom constructor would require touching
Types.type_descplusbtype.ml,ctype.ml,predef.ml, and the printers inprinttyp.ml, but the current pipeline already models non-returning code viaTvar. - Ensure exhaustiveness and dead code analysis (e.g.
compiler/ml/parmatch.ml,compiler/ext/warnings.ml) treatreturnas non-fallthrough so we avoid double warnings. - Update typed tree iterators and printers (
TypedtreeIter,Printtyped) to handleTexp_return.
- Extend
lam.mlwith anLreturn of lambda optionconstructor (or reuse existing exit nodes if we can adapt them). - Modify
translcore.ml(and related helpers) to translateTexp_returninto the new lambda form, marking the generated continuation as finished. - Adjust passes that manipulate control flow:
- Ensure
lam_pass_exits,lam_dce, and similar optimizations treatLreturnas terminating. - Update
lam_print.mland analysis utilities to print and traverse the new node.
- Ensure
- Update JS lowering (
lam_compile.ml,js_output.ml) so lambda outputs marked as “finished” get converted toreturn_stmt payloadand no additional implicit return is appended. - Ensure
switch/iflowering avoids emitting duplicatereturnstatements when a branch already ends withreturn. This likely relies onoutput_finished = Trueplumbing already used bythrowand existing returns. - Adjust
js_stmt_make/js_exp_maketo expose helper constructors where needed, and audit passes likejs_pass_flatten_and_mark_dead.mlto respect terminating statements. - Validate async helpers and promise sugar to confirm the generated functions contain direct
returnstatements, ensuring semantics match JavaScript.
- Update AST printers (
pprintast.ml,js_dump_*) to displayreturnexpressions. - Extend the language server (
analysis/) to surface the new node in hover/type info and to provide quick-fix diagnostics. - Document the feature in
docs/Syntax.md, including examples and restrictions.
- Existing code continues to compile; no change to default behaviour.
- PPX compatibility: because
parsetree0.mlremains frozen, PPXs continue to receive the v0 AST withoutreturn. We maintain compatibility by mappingPexp_returnto/from the v0 representation throughast_mapper_from0/ast_mapper_to0. - JavaScript output remains stable aside from functions that now contain explicit
returnstatements when developers opt in to the new feature.
- Syntax tests: new fixtures for valid/invalid
returnusages, nested functions, and top-level errors. - Typechecker tests (
tests/ounit_tests/or similar): ensure payload type mismatches raise errors, unreachable code warnings are produced, and nested function scoping works. - Lambda / JS IR tests: add golden-print tests verifying
Lreturninlam_printand generated JS blocks for representative cases (if,switch,try/finally, async wrappers). - Integration tests (
tests/build_tests/): demonstrate runtime behaviour, including interaction with promise helpers and exceptions.
- Typechecker warnings:
type_statementwarns withWarnings.Nonreturning_statementwhenever an expression typed as a bareTvaris discarded (compiler/ml/typecore.ml:3884-3894), which is how%raisecommunicates non-returning behaviour today. - Pattern reachability:
Parmatch.check_unusedemitsWarnings.Unreachable_casefor dead match arms and already runs for everyTexp_match/Texp_function(compiler/ml/parmatch.ml:2158-2201). - Backend pruning:
%raiselowers toLprim (Praise, …)intranslcore(compiler/ml/translcore.ml:738-745). The JS backend recognises that primitive and marks the output as finished (compiler/core/lam_compile.ml:1540-1560), andJs_output.append_outputdrops any subsequent statements whenoutput_finished = True(compiler/core/js_output.ml:82-138). A futurereturnnode should reuse this plumbing so dead statements are automatically discarded without a new bottom type.
- Rust
- Syntax: supports
return expr;and barereturn;alongside the idiomatic “last expression wins” rule. - Semantics: the
returnexpression has the bottom type!, so type inference and control-flow analysis mark all following code as unreachable. The same machinery covers other diverging constructs (loop {},panic!), letting borrow checking and MIR optimisations short-circuit safely. - Interoperability: because Rust targets native back-ends, early return is compiled to direct jumps, proving the pattern fits expression-oriented languages that still value low-level control.
- Syntax: supports
- Kotlin
- Syntax:
return exprexits the current function;return@label exprexits a labeled lambda or loop, preserving expression-based APIs such asrun { … }and collection pipelines. - Semantics:
returnyields the bottom typeNothing. Smart casts and exhaustiveness checking treatNothingas terminating, so unreachable code is rejected and type inference remains precise. - Diagnostics: Kotlin’s flow analysis creates “dead code” warnings immediately after a
return, demonstrating the value of threading bottom types through the checker.
- Syntax:
- Scala
- Syntax:
return exprreturns from the nearest named method; it is legal inside expression bodies but not idiomatic. - Semantics: the return expression has type
Nothing, Scala’s bottom type. Inside anonymous functions the compiler lowersreturnto throwingNonLocalReturnControl, which highlights surprising control flow when the target is not obvious. - Lesson: ReScript should explicitly specify whether
returnis allowed in closures and, if so, how it interacts with captured continuations to avoid Scala’s non-local return pitfalls.
- Syntax:
- Swift
- Syntax:
return expris required unless the function consists of a single expression;guard … else { return }is a first-class use-case. - Semantics: Swift’s
Neverbottom type represents non-returning code. Diagnose unreachable statements immediately afterreturn, and type inference propagatesNeverthroughguard/switchconstructs. - Interop: Because Swift targets multiple back-ends (including JS through SwiftWasm), it shows early return maps cleanly to JavaScript code generation.
- Syntax:
- TypeScript / JavaScript
- Syntax:
return expr;is a statement. TypeScript adds inference of the bottom typeneverfor functions that always return or throw, feeding its exhaustiveness checking and control-flow narrowing. - Semantics: even without expression syntax, TypeScript’s
neverdemonstrates the benefit of a bottom type for tooling and editor diagnostics—something ReScript can leverage while preserving JS parity.
- Syntax:
- Swift / Rust Hybrids vs ML lineage
- OCaml, Standard ML, Elm, Haskell, and F# avoid early return altogether, relying on expression composition or exceptions. This contrast underlines that adopting
returnaligns ReScript with Rust/Kotlin ergonomics rather than traditional ML style, but also that we can reuse ML-derived analyses if we thread a bottom type carefully.
- OCaml, Standard ML, Elm, Haskell, and F# avoid early return altogether, relying on expression composition or exceptions. This contrast underlines that adopting
- The compiler already models non-returning expressions via fresh type variables plus warning logic (
compiler/ml/typecore.ml:3884-3894) and by marking backend outputs as finished (compiler/core/lam_compile.ml:1540-1560,compiler/core/js_output.ml:117-138). Reuse that machinery forreturnbefore introducing a dedicatedneverconstructor—but note that every language we surveyed leans on an explicit bottom type (!,Nothing,Never,never) to make control-flow reasoning robust. - Validate how
returnreads inside pipeline-heavy expressions; current proposal allows it everywhere, but we should document guidance if certain patterns feel awkward. - Consider introducing linting guidance to discourage overuse in expression-heavy code while still allowing pragmatic escapes.