|
| 1 | +# Design: Early Return in Functions |
| 2 | + |
| 3 | +## Context & Motivation |
| 4 | +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. |
| 5 | + |
| 6 | +## Goals |
| 7 | +- Introduce a `return` expression that exits the innermost function, optionally carrying a value (`return expr` or `return;`). |
| 8 | +- Type-check `return` so that subsequent code is treated as unreachable, avoiding spurious exhaustiveness warnings. |
| 9 | +- Emit direct JavaScript `return` statements to make async and `try` interactions behave exactly like plain JS. |
| 10 | +- Preserve backward compatibility for existing code that does not use `return`. |
| 11 | + |
| 12 | +## Non-goals |
| 13 | +- Adding multi-value returns or early exit for non-function constructs (loops, switches without functions, etc.). |
| 14 | +- Introducing new runtime constructs beyond the emitted JavaScript `return`. |
| 15 | +- Changing module-level or top-level behaviour; `return` remains illegal outside function bodies. |
| 16 | + |
| 17 | +## Semantics Overview |
| 18 | +- `return` is an expression with the bottom-like type `never`. The payload, when present, must unify with the enclosing function's declared result. |
| 19 | +- `return` targets only the innermost function scope, including anonymous functions and closures. |
| 20 | +- `return;` is syntactic sugar for `return ();` but keeps type `never` so the function's result type must be `unit`. |
| 21 | +- Once a `return` is evaluated, control flow stops at that point; subsequent expressions in the same block are unreachable. |
| 22 | + |
| 23 | +## Syntax Layer Changes (`compiler/syntax/`) |
| 24 | +- Extend the grammar in `parser.mly` to parse `return` as a `simple_expr` with an optional trailing expression (`return`, `return expr`). |
| 25 | +- Add `Pexp_return of expression option` to `parsetree.ml`, and update related helpers (`ast_iterator`, printers, etc.). |
| 26 | +- Mirror the changes in `ast_mapper_from0.ml` and `ast_mapper_to0.ml` to maintain compatibility with `parsetree0.ml` (which must stay frozen). |
| 27 | +- Update syntax error recovery to produce messages such as “`return` is only allowed inside function bodies" when seen in invalid positions. |
| 28 | +- Add new parser fixtures under `tests/syntax_tests/` (positive and negative cases). |
| 29 | + |
| 30 | +## Typed Tree & Type Checking (`compiler/ml/`) |
| 31 | +- Introduce `Texp_return` in `Typedtree.expression_desc` and a corresponding record in `Typedtree_helper` utilities. |
| 32 | +- Extend `typecore.ml` to: |
| 33 | + - Ensure we are inside a function context (reusing or extending `env.in_function`). |
| 34 | + - Type-check the optional payload against the enclosing function's result type. |
| 35 | + - Assign the new `never` type to the expression so downstream phases treat it as non-returning. |
| 36 | + - Register an error when used outside functions or when the payload type mismatches. |
| 37 | +- Introduce a dedicated bottom type constructor (`never`) if one does not already exist: |
| 38 | + - Extend `Types.type_desc` and helpers in `btype.ml` / `ctype.ml` with `Tnever` (or similar) plus `Predef` registration, including printer support in `printtyp.ml`. |
| 39 | + - Update utility predicates (`Types.maybe_bottom`, dead-code checks) to understand the new type. |
| 40 | +- Ensure exhaustiveness and dead code analysis (e.g. `parmatch.ml`, `clflags.warn_error`) treat `never` as non-fallthrough so we avoid double warnings. |
| 41 | +- Update typed tree iterators and printers (`TypedtreeIter`, `Printtyped`) to handle `Texp_return`. |
| 42 | + |
| 43 | +## Lambda IR Translation (`compiler/core/`) |
| 44 | +- Extend `lam.ml` with an `Lreturn of lambda option` constructor (or reuse existing exit nodes if we can adapt them). |
| 45 | +- Modify `translcore.ml` (and related helpers) to translate `Texp_return` into the new lambda form, marking the generated continuation as finished. |
| 46 | +- Adjust passes that manipulate control flow: |
| 47 | + - Ensure `lam_pass_exits`, `lam_dce`, and similar optimizations treat `Lreturn` as terminating. |
| 48 | + - Update `lam_print.ml` and analysis utilities to print and traverse the new node. |
| 49 | + |
| 50 | +## JavaScript Backend (`compiler/core/js_*`) |
| 51 | +- Update JS lowering (`lam_compile.ml`, `js_output.ml`) so lambda outputs marked as “finished” get converted to `return_stmt payload` and no additional implicit return is appended. |
| 52 | +- Ensure `switch`/`if` lowering avoids emitting duplicate `return` statements when a branch already ends with `return`. This likely relies on `output_finished = True` plumbing already used by `throw` and existing returns. |
| 53 | +- Adjust `js_stmt_make` / `js_exp_make` to expose helper constructors where needed, and audit passes like `js_pass_flatten.ml` to respect terminating statements. |
| 54 | +- Validate async helpers and promise sugar to confirm the generated functions contain direct `return` statements, ensuring semantics match JavaScript. |
| 55 | + |
| 56 | +## Tooling & Diagnostics |
| 57 | +- Update AST printers (`pprintast.ml`, `js_dump_*`) to display `return` expressions. |
| 58 | +- Extend the language server (`analysis/`) to surface the new node in hover/type info and to provide quick-fix diagnostics. |
| 59 | +- Document the feature in `docs/Syntax.md`, including examples and restrictions. |
| 60 | + |
| 61 | +## Migration & Compatibility |
| 62 | +- Existing code continues to compile; no change to default behaviour. |
| 63 | +- PPX compatibility: because `parsetree0.ml` remains frozen, PPXs continue to receive the v0 AST without `return`. We maintain compatibility by mapping `Pexp_return` to/from the v0 representation through `ast_mapper_from0` / `ast_mapper_to0`. |
| 64 | +- JavaScript output remains stable aside from functions that now contain explicit `return` statements when developers opt in to the new feature. |
| 65 | + |
| 66 | +## Testing Strategy |
| 67 | +- **Syntax tests**: new fixtures for valid/invalid `return` usages, nested functions, and top-level errors. |
| 68 | +- **Typechecker tests** (`tests/ounit_tests/` or similar): ensure payload type mismatches raise errors, unreachable code warnings are produced, and nested function scoping works. |
| 69 | +- **Lambda / JS IR tests**: add golden-print tests verifying `Lreturn` in `lam_print` and generated JS blocks for representative cases (`if`, `switch`, `try/finally`, async wrappers). |
| 70 | +- **Integration tests** (`tests/build_tests/`): demonstrate runtime behaviour, including interaction with promise helpers and exceptions. |
| 71 | + |
| 72 | +## Open Questions & Follow-ups |
| 73 | +- Does the compiler already expose a notion of bottom in other phases? If so, integrate rather than re-invent; otherwise, ensure `never` is threaded consistently (e.g. into `Predef` and external tooling). |
| 74 | +- Should `return` be allowed inside `fun { } =>` pipelines or only as a standalone statement expression? Current proposal allows it anywhere expressions are permitted, but we should validate editor ergonomics and readability. |
| 75 | +- Consider introducing linting guidance to discourage overuse in expression-heavy code while still allowing pragmatic escapes. |
| 76 | + |
0 commit comments