The return type stops being a guess, and recur beats silent TCO

This commit is contained in:
Joseph Ferano 2026-09-12 22:44:12 +07:00
parent ef9aebd807
commit 1393d817b0

42
NEXT.md
View File

@ -678,6 +678,48 @@ destination is always honest about what it is.
Drop Clojure's `:eduction` branch — that is the pass-around case, and the one part that would need runtime machinery. Drop Clojure's `:eduction` branch — that is the pass-around case, and the one part that would need runtime machinery.
## Queued: the return type is mandatory, and `loop`/`recur`
**1. A `defn` must always state its return type, and unit is written `()`.**
Today the slot is optional and the parser decides return-type-from-body by looking the symbol up in a set of known type
names. It is *sound* only because there is one top-level namespace, so a name cannot be both a type and a value — and
it is brittle because the set has to be complete. **It has been wrong twice in one day**, both from the same root: the
prelude's type names were not in the set, so `Form` in return position read as an unknown value; and fixing that
exposed a second arm reading the same set, which had been parsing `(defn f [] (Rune {.code 65}) (bar))` as a function
*returning* a `Rune` with a one-form body — **silently, in every file in the language**. The diagnostic is poor too:
`(defn f [] f65 0.0)` says *unknown name* rather than *did you mean f64*.
A silent misparse is the worst failure class available, and macros now generate definitions, which widens it.
**Mandatory removes the guess entirely** — the slot after the parameters is unconditionally a type, the parser stops
consulting a table, and the typo case gets a real message. The cost is `(defn main [] () ...)` on void functions,
against `plan.org`'s deliberate short form. Take the cost.
**Unit is `()`**, ML's spelling: it is the honest name, and it cannot collide because an empty call is not a valid
expression anyway.
Alternatives considered and rejected: a separate signature form, Typed Racket's `(: foo (-> i32 i32))` — removes the
ambiguity equally but splits the signature from the body and gives two things to keep in sync. Return type *before*
the parameters — does not work, because a fixed-array type is spelled with brackets, so `(defn foo [4 i32] ...)` could
be either. Inferring the return type — it is local and therefore cheap, but it only helps if annotating is
*forbidden*, and recursion forces an annotation back anyway, which makes it optional, which is where we started.
Note the function *type* spelling is already decided and needs no arrows: `(Fn [i32 i32] i32)`, mirroring `defn`.
Arrows would imply currying, which this language does not do.
**2. `loop` and `recur`.** Both are already refused by name in `parse.ml`. There is **no TCO** — nothing emits tail
calls, and `plan.org` mentions them only as something the current backend choice *could* control (LLVM's `musttail` is
there if wanted).
`recur` is the better answer than silent TCO, and not only because it is cheaper. **It is checked**: the compiler
verifies the call is in tail position and turns it into a jump, so breaking tail position is a compile error rather
than a stack overflow at run time. Clojure adopted it because the JVM lacks TCO and it turned out to be the better
design.
Cheap here — a jump to the top of a `loop`, which is the machinery `while` and the new labelled `break`/`continue`
already have. What it does **not** give is mutual recursion between two functions; that needs real tail calls, and is
a separate question if it is ever wanted.
## The next batch, in order ## The next batch, in order
Agreed at the end of 2026-09-12. Ordered by priority, not by size. Items 1-3 and 5-6 want the compiler core and should Agreed at the end of 2026-09-12. Ordered by priority, not by size. Items 1-3 and 5-6 want the compiler core and should