Skip to content

Commit dc8f32d

Browse files
committed
[RFC] Early return
1 parent 57c696b commit dc8f32d

File tree

1 file changed

+76
-0
lines changed

1 file changed

+76
-0
lines changed

docs/early-return-design.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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

Comments
 (0)