The return type stops being a guess, and recur beats silent TCO
This commit is contained in:
parent
ef9aebd807
commit
1393d817b0
42
NEXT.md
42
NEXT.md
@ -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.
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user