diff --git a/BUILT.md b/BUILT.md
index bec245a..f482fa5 100644
--- a/BUILT.md
+++ b/BUILT.md
@@ -656,7 +656,7 @@ an enum i32
(Ptr T) ptr opaque pointers
(Option T) { i8, T } tag 0 None, 1 Some
a struct a literal struct, declaration order
-Unit and Never {}
+() and Never {}
```
No object headers anywhere, so a Flan struct is exactly its C struct and nothing marshals. Two consequences carry the
@@ -701,7 +701,7 @@ private copy of it.
Its string constants still come along; omitting them is an undefined `@.str.N` at link time, and it is easy to miss
because a one-function module usually has none. `Emit.signature` is now the single place a function's LLVM signature is
-spelled, because a `define` here and a `declare` there drift the moment one of them grows a case for `Unit` or for a
+spelled, because a `define` here and a `declare` there drift the moment one of them grows a case for `()` or for a
slice parameter.
**`flan_dev.c` is compiled into every build, not only a dev one.** Nothing in a release build calls into it — the
@@ -1356,14 +1356,14 @@ the request and appears at the prompt; anything the program printed while evalua
and goes to `*flan-output*`. Showing them in one place would be convenient and wrong, so there is a test for the
separation.
-That test is what caught a real bug: the renderer's `Unit` case emitted `()` without evaluating the expression, so
-`(println "x")` — the most ordinary thing anyone types at a prompt — answered `()` while nothing happened. A Unit
+That test is what caught a real bug: the renderer's unit case emitted `()` without evaluating the expression, so
+`(println "x")` — the most ordinary thing anyone types at a prompt — answered `()` while nothing happened. A `()`
expression is almost always a call made for its effect, and is now evaluated and *then* reported.
### Conditions — step 1: `handler-bind` and `signal`
`spec-conditions.md` §1 and §2, and nothing else yet. They are worth having on their own because **neither alters
-control flow**: `signal` returns `Unit` whatever it finds, a handler that returns normally leaves the signalling
+control flow**: `signal` returns `()` whatever it finds, a handler that returns normally leaves the signalling
function to carry on, and with nothing matching it is a no-op. So none of the transfer machinery §6 describes exists
yet, and no signature changed.
@@ -1870,7 +1870,7 @@ it.
everything. There is nowhere to resume, so there is nothing else to do.
**`(error c)`, §2.** The same walk as `signal`, and the difference is entirely what happens when the walk ends: `signal`
-returns `Unit` and the signalling function carries on, `error` has type `Never` and stops. So only a transfer gets past
+returns `()` and the signalling function carries on, `error` has type `Never` and stops. So only a transfer gets past
it, which is why `emit` puts a guard after the call and then `unreachable` — and why `flan_error` cannot be marked
`noreturn`, since it does return, on exactly one path. Being `Never` is also what lets it stand as a `restart-case`
body's fall-through, which is the shape §1's `load-texture` example needs. `test/programs/error.flan` is the unhandled
@@ -2026,7 +2026,7 @@ fires.
| `context/allocator` / `context/temp` | the current implicit allocator, and the per-frame arena |
| `(with-allocator a body...)` | rebinds for a dynamic extent and releases nothing |
| `(vec-new T)` / `(vec-new T a)` / `(vec-new)` | a Vec, against the context or a named allocator |
-| `(push v x)` / `(reserve v n)` | `Unit`, both |
+| `(push v x)` / `(reserve v n)` | `()`, both |
| `(at v i)` / `(len v)` | the array names, extended — not a parallel pair |
| `(as-slice v)` / `(as-slice v lo hi)` | a non-owning `[T]` view |
| `(clone v)` / `(clone v a)` | the only copy; assignment moves |
@@ -2143,7 +2143,7 @@ And a `Vec` does not cross to C: handing a header that owns storage to C hands o
No allocating operation returns an error and none can fail silently. When the allocator cannot satisfy a request the
operation signals `(StorageExhausted {.bytes n .align a .allocator id})` with `error` — whose type is `Never` — inside a
`restart-case` offering `retry`. One rule over every allocating operation, which is what keeps `push` and `reserve` at
-`Unit`, `clone` at the container, and no signature anywhere growing a `Result`.
+`()`, `clone` at the container, and no signature anywhere growing a `Result`.
It had to land with step 2 rather than after it: retrofitting adds a transfer check to every call site of every
allocating operation, which is the point of having decided it first. Odin's `append` returns an ignorable
@@ -2264,7 +2264,7 @@ calls per field, which has no channel to hand on.
| Name | What |
|---|---|
| `(map-new)` `(map-new K V)` `(map-new a)` `(map-new K V a)` | a new map; the pair may be omitted where the context says |
-| `(put m k v)` | upsert, `Unit` |
+| `(put m k v)` | upsert, `()` |
| `(get m k)` | `(Option V)` — absence is `None` |
| `(has-key? m k)` | `bool`, copying no value — **an addition; the spec does not name it** |
| `(len m)` `(reserve m n)` `(clone m)` `(clone m a)` `(free m)` | extended, not duplicated |
@@ -2300,7 +2300,7 @@ with `map-new` and filled with `put`.
- A **move-only value** — the refusal `(Vec (Vec T))` already carries, for the identical reason: the runtime copies
entries bytewise, so `clone` would duplicate headers and `free` would leak what they own. Owned entries arrive with
`drop`.
-- **`Unit` as a value** — there is nothing to store, and the cell geometry divides a cache line by the element size.
+- **`()` as a value** — there is nothing to store, and the cell geometry divides a cache line by the element size.
Named rather than left to divide by zero, because it is the natural spelling of a set.
`StorageExhausted` under `retry` holds over `map-new`, `put`, `reserve` and `clone`, reusing the machinery that landed
@@ -3076,13 +3076,15 @@ in `prelude`, which is where the module gets them from.
**`Macro.reduce`: for that one build, the prelude is smaller.** Every `defn` that names a macro is dropped, and then
every `defn` that names a dropped one, to a fixpoint — a function calling something unbuildable is as unbuildable as
-the thing it calls. `Prelude.bootstrap` is the hook, a ref rather than a parameter because the readers are
-`Check.program` and `Parse.prelude_types` and neither can be told.
+the thing it calls. `Prelude.bootstrap` is the hook, a ref rather than a parameter because the reader is
+`Check.program` and it cannot be told.
-Only `defn`s are dropped, and that restriction is load-bearing rather than tidy. `Parse.prelude_types` **memoises**,
-and it can be forced for the first time inside a bootstrap build; a reduced set of types cached there would be wrong
-for every compile afterwards. A `defstruct`, `defunion`, `defalias`, `defenum` or `defvar` therefore stays whatever it
-names.
+Only `defn`s are dropped: the functions that survive still mention the prelude's types, and a reduced prelude missing
+them would not check. A `defstruct`, `defunion`, `defalias`, `defenum` or `defvar` therefore stays whatever it names.
+There used to be a sharper reason — `Parse.prelude_types` memoised the prelude's type names for the parser's
+return-type guess, and it can be forced for the first time inside a bootstrap build, so a reduced set cached there
+would have been wrong for every compile afterwards. That set is gone with the guess; see *The return type is the slot*
+below.
**The one restriction that stays, and now names itself.** A prelude macro may not call a macro — the module that
expands it is compiled from the prelude, so there is no earlier module for its own call to have been expanded by.
@@ -3195,7 +3197,7 @@ NEXT.md decisions 2 and 5. `(slurp path)` and `(slurp path allocator)` read a wh
**`slurp` waited for `Vec` because its result has no length until the file is read**, and it obeys spec-memory.md's
rule without an exception: *no allocating operation returns an error*. There is no `Result` here, no out-parameter and
-no error code — `slurp`'s type is `(Vec u8)` and `barf`'s is `Unit`.
+no error code — `slurp`'s type is `(Vec u8)` and `barf`'s is `()`.
**Two failures, two conditions, and the guards nest rather than merge.** Allocation failure is `StorageExhausted` under
`retry`, unchanged and reused. File failure is `FileError {.path .op .reason}` under `retry` and `use-value`. They stay
@@ -3553,6 +3555,87 @@ on `window is not defined` — which says the module is live and says nothing ab
- **Canvas size against `screen-width`/`screen-height`.** The shell is a string in `Build` and its canvas is not sized
from the program, so 900x600 may be letterboxed or cropped.
+## The return type is the slot, and unit is `()`
+
+`(defn name [param Type ...] ReturnType body ...)`. The slot after the parameters is unconditionally a type, and a
+function that returns nothing writes `()`. It used to be optional.
+
+**What optional cost.** `(Option f64)` and `(Some 1)` are the same s-expression — a type application and a
+constructor call are indistinguishable by shape — so the parser decided which it had by looking the head up in a set
+of names that were actually types, collected by a pre-pass over the file's own declarations plus the prelude's. That
+is *sound*, because one top-level namespace means a name cannot be both a type and a value. It is brittle because the
+set has to be complete, and **it was wrong twice in one day**:
+
+- the prelude's type names were not in the set at all, so a macro's `Form` in return position read as an unknown
+ value;
+- the fix for 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.
+
+A silent misparse is the worst failure class available. Macros generate definitions now, which widens it, and a table
+that has to be complete will be incomplete again.
+
+**Mandatory removes the guess.** `Parse.decl` stops taking a set of names, `is_type_form`, `qualified_type`,
+`types_in`, `declared_types` and `prelude_types` are gone, and the pre-pass over a file's declarations that fed them is
+gone with them. Whatever is in the slot is a type; whatever follows is the body. Two things fall out:
+
+- **A type the parser could not have known is fine.** A struct declared further down the file, `rl/Vector2` behind an
+ alias that is not resolved until after parsing, a prelude type — none of them needed to be *recognised*, they just
+ needed to be in the slot.
+- **A typo is a typo.** `(defn f [] f65 0.0)` reaches `Check.resolve_name`, whose near-miss check answers *unknown
+ type f65 — did you mean f64?*. It used to be parsed as the first form of the body and reported as an unknown
+ **name**, which points at the wrong mistake.
+
+The cost is `()` on every void function, against `plan.org`'s deliberate short form. Taken.
+
+**Unit is `()`**, ML's spelling. It is honest, and it cannot collide: an empty call is not a valid expression, so
+there is no reading of `()` in value position for a body form to be confused with. The old `Unit` spelling is
+**refused**, with a message naming the new one — the same rule the colon-to-dot change followed, and for the same
+reason: two accepted spellings is how two spellings become permanent.
+
+Internally it is still `Types.Unit`, and `()` parses to `Ast.Tname "Unit"`, so the resolver, the shim and the emitter
+did not change. `Cimport` still builds `Tname "Unit"` for C's `void` and never goes through the parser, which is why
+`resolve_name` still answers to the word. **Diagnostics print `()`**: `Types.to_string` prints what a person would
+write for every other type it knows — `[i32]`, `{K V}`, `(Ptr T)` — and `Unit` was the odd one out the moment the
+source spelling changed.
+
+**One shape is new.** `(defn f [] ())` is a function with a return type and no body. The old optional slot could not
+produce it: `(defn f [])` had nowhere to put the type, and a lone form after the parameters was always the body.
+
+### The sweep
+
+`tools/unit-return.py`, kept rather than thrown away, because the lanes that branched before this wrote Flan in the
+old spelling and their files want the same pass at merge:
+
+```
+python3 tools/unit-return.py .
+python3 tools/unit-return.py --in-strings test/test_flan.ml test/test_acceptance.ml \
+ test/test_session.ml emacs/test-flan-dev.el emacs/test-flan-mode.el
+python3 tools/unit-return.py --raw-ml lib/prelude.ml
+python3 tools/unit-return.py --in-html web/index.html
+```
+
+It fills the empty slot with `()` and rewrites `Unit` as `()` wherever a type is spelled. Deciding whether a `defn`
+*already* had a return type is the whole difficulty, and the script does it by transcribing `parse.ml`'s
+`is_type_form` rather than improving on it — being identical to the parser it replaces is what makes the sweep
+meaning-preserving. 440 sites in `.flan`, 260 more embedded in OCaml, elisp and HTML; `-v` logs every `defn` it saw
+and what it decided, which is the only practical way to review a sweep that size.
+
+Three hazards it knows about, and one it cannot:
+
+- **A snippet split across concatenation.** `cursor ^ "(defn f [s [u8]] Cursor ...)"` is one OCaml literal that does
+ not contain the `defstruct` in the other. The embedded modes pool every fragment's type declarations across the
+ whole file and count a pooled name only in bare-symbol position — as a list head it would eat `(Some 1)` as a
+ return type, which is the misparse this change exists to remove. Sound because no *user* type takes arguments.
+- **A fragment that cuts off mid-form**, `"(defn step [] i64\n"`, is skipped rather than guessed at. `flan-mode.el`'s
+ `"(defn step"` search strings are the same case.
+- **A bare `Unit` outside a form is left alone**, so the checker's own `Tname "Unit"` pattern in a test literal is not
+ rewritten into nonsense.
+- **What it cannot know**: `test_flan.ml` deliberately spells the *refused* forms, to test that they are refused —
+ `(defn f [] (g))`, `(defn f [] Unit (g))`, `(defn f [] Nope (bar))`, `(defn f [] f65 0.0)`. The script encodes the
+ old rule, so it wants to convert all six of those sites and must not. Re-running it on this tree reports exactly
+ those six; anything else is a real conversion. Read the diff.
+
## The colon belongs to keys, so a field label is a dot
`{.x 1.0 .y 2.0}` is how a struct is constructed, and `{inner .field}` is how a pattern names one. The colon is gone
diff --git a/NEXT.md b/NEXT.md
index 5d6d03a..349adcb 100644
--- a/NEXT.md
+++ b/NEXT.md
@@ -684,34 +684,36 @@ 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`
+## Queued: `loop`/`recur` (the return type is done)
-**1. A `defn` must always state its return type, and unit is written `()`.**
+~~**1. A `defn` must always state its return type, and unit is written `()`.**~~ **Done.** See *The return type is
+the slot, and unit is `()`* in [`BUILT.md`](BUILT.md).
-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*.
+The slot after the parameters is unconditionally a type, the pre-pass that collected a file's type names is gone
+along with `is_type_form`, `qualified_type`, `types_in`, `declared_types` and `prelude_types`, and `Parse.decl` no
+longer takes a set of names at all. `(defn f [] f65 0.0)` now says *unknown type f65 — did you mean f64?* instead of
+*unknown name*. `()` is the only spelling of unit: `Unit` is refused with a message naming it, and `Types.to_string`
+prints `()` too, because that printer prints what a person would write for every other type it knows.
-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.
+Two things the plan did not anticipate. `(defn f [] ())` — a return type and an empty body — is a shape the optional
+slot could not produce, and it needed its own arm. And dropping `prelude_types` removes one of the two reasons
+`Macro.reduce` may only drop `defn`s: the memoised set that a bootstrap build could have poisoned no longer exists,
+so what is left is the plain one, that the surviving functions still mention those types.
-**Unit is `()`**, ML's spelling: it is the honest name, and it cannot collide because an empty call is not a valid
-expression anyway.
+**The sweep is `tools/unit-return.py`**, kept rather than thrown away, because the lanes that branched before this
+wrote Flan in the old spelling and their files want the same pass at merge:
-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.
+```
+python3 tools/unit-return.py .
+python3 tools/unit-return.py --in-strings test/test_flan.ml test/test_acceptance.ml \
+ test/test_session.ml emacs/test-flan-dev.el emacs/test-flan-mode.el
+python3 tools/unit-return.py --raw-ml lib/prelude.ml
+python3 tools/unit-return.py --in-html web/index.html
+```
-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.
+`-v` logs every `defn` it saw and what it decided; `--check` changes nothing. It is re-runnable, and on this tree it
+reports exactly six sites, all in `test_flan.ml`, which spell the refused forms *on purpose* so the refusals can be
+tested. Read the diff of every non-`.flan` file — BUILT.md lists what the script can and cannot see.
**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
@@ -1192,7 +1194,7 @@ plan.org's single line on it (831) names a `for` the language does not have and
frozen along with the rest of that file: when storage is released, the `drop` hook, alignment, and allocation
failure. Read them there rather than in a second copy here. The one consequence the build order below turns on is
that no allocating operation returns an error — a failure signals `StorageExhausted` under a `retry` restart — so
- `push` and `put` are `Unit`, `clone` returns the container, and no signature grows a `Result`. One question is left
+ `push` and `put` are `()`, `clone` returns the container, and no signature grows a `Result`. One question is left
open in that section on purpose; it does not block the build.
2. **The editor half of a typed restart.** The language half is in (see "Landed"): `(use-value [v i32] ...)` and
diff --git a/conditions.org b/conditions.org
index 99d7b6f..23ce07e 100644
--- a/conditions.org
+++ b/conditions.org
@@ -7,7 +7,7 @@ Why it is shaped this way: [[file:spec-conditions.md][spec-conditions.md]]. Some
* Works
#+begin_src lisp
-(signal c) ; Unit. Handler returns -> carry on. No handler -> no-op.
+(signal c) ; (). Handler returns -> carry on. No handler -> no-op.
(error c) ; Never. Only a transfer gets past; else the program stops.
(handler-bind [(Type [c] body ...) ...] body ...) ; match by type, no hierarchy
diff --git a/examples/core-input-gamepad.flan b/examples/core-input-gamepad.flan
index fdde691..90a53df 100644
--- a/examples/core-input-gamepad.flan
+++ b/examples/core-input-gamepad.flan
@@ -256,8 +256,8 @@
;; -1 when nothing is pressed, which is why the binding answers an
;; i32 and not a GamepadButton.
;; draw-int answers the width it drew, so a branch that ends in one
- ;; has type i32 while its sibling has type Unit and the `if` will not
- ;; typecheck — "expected i32, found Unit". Both arms end in a
+ ;; has type i32 while its sibling has type () and the `if` will not
+ ;; typecheck — "expected i32, found ()". Both arms end in a
;; draw-text here, which is the tidy way out; where that is awkward a
;; trailing `(do)` is the other.
(let [b (rl/get-gamepad-button-pressed)]
diff --git a/lib/ast.ml b/lib/ast.ml
index 2d2c93f..baf9c3f 100644
--- a/lib/ast.ml
+++ b/lib/ast.ml
@@ -119,7 +119,7 @@ and pattern =
type fn = {
name : string;
params : field list;
- ret : texpr option; (* None means Unit *)
+ ret : texpr option; (* None means (); only declare omits it *)
fbody : expr list;
nloc : Loc.t;
}
diff --git a/lib/macro.ml b/lib/macro.ml
index 916999b..8ff2627 100644
--- a/lib/macro.ml
+++ b/lib/macro.ml
@@ -78,9 +78,13 @@ let building = ref false
calling a dropped one is as unbuildable as the dropped one itself.
Only [defn]s are dropped. A [defstruct], [defunion], [defalias], [defenum]
- or [defvar] stays whatever it names, so [Parse.prelude_types] sees the same
- set of types during a bootstrap build as outside one — it memoises, and a
- reduced answer cached there would be wrong for every later compile.
+ or [defvar] stays whatever it names: the functions that survive still
+ mention those types, and a reduced prelude missing them would not check.
+ There used to be a sharper reason — [Parse.prelude_types] memoised the
+ prelude's type names for the parser's return-type guess, and a reduced
+ answer cached during a bootstrap build would have been wrong for every
+ compile after it. That set is gone with the guess: a defn states its return
+ type, so nothing in the parser asks what the prelude declares.
A [defmacro] that lands in the dropped set is the violation of the rule, and
it is refused here by name rather than reaching clang as an unknown symbol. *)
diff --git a/lib/parse.ml b/lib/parse.ml
index 2963017..7990985 100644
--- a/lib/parse.ml
+++ b/lib/parse.ml
@@ -43,22 +43,6 @@ let no_pattern (f : Form.t) =
(Form.to_string f)
| _ -> ()
-(* Primitive type names are lowercase but concrete; every other lowercase name
- in type position is a type variable (plan.org, Types). *)
-let primitives =
- [ "i8"; "i16"; "i32"; "i64"; "u8"; "u16"; "u32"; "u64";
- "f32"; "f64"; "bool"; "string"; "Unit"; "Never" ]
-
-let is_primitive s = List.mem s primitives
-
-module Names = Set.Make (String)
-
-(* Type constructors the language provides. User types are collected by a
- pre-pass over the file's declarations — see [program]. *)
-let builtin_types =
- Names.of_list (primitives @ [ "Ptr"; "Option"; "Result"; "Vec"; "Map";
- "Handle"; "Fn" ])
-
(* ── Type expressions ──────────────────────────────────────────────── *)
let rec texpr (f : Form.t) : Ast.texpr =
@@ -70,6 +54,12 @@ let rec texpr (f : Form.t) : Ast.texpr =
[Tname "Unit"] -- the resolver, the shim and the emitter all speak that
name, and diagnostics still print it. *)
| List [] -> mk (Ast.Tname "Unit")
+ (* One spelling. Two accepted spellings is how two spellings become
+ permanent, and the refusal names the new one -- the same rule the
+ colon-to-dot change followed. [Tname "Unit"] still exists below this
+ point: it is what [()] parses to, and what the resolver, the shim and the
+ emitter go on speaking. *)
+ | Sym "Unit" -> fail f "unit is written (), not Unit"
| Sym s -> mk (Ast.Tname s)
| Vec [ elem ] -> mk (Ast.Tslice (texpr elem))
| Vec [ n; elem ] -> mk (Ast.Tarray (len n, texpr elem))
@@ -695,7 +685,7 @@ and pattern (f : Form.t) : Ast.pattern =
(* ── Declarations ──────────────────────────────────────────────────── *)
-let rec decl types (f : Form.t) : Ast.decl =
+let rec decl (f : Form.t) : Ast.decl =
let mk d = { Ast.d; dloc = f.loc } in
match f.v with
| List ({ v = Sym "package"; _ } :: args) ->
@@ -723,25 +713,41 @@ let rec decl types (f : Form.t) : Ast.decl =
| [ n; { v = Vec vs; _ } ] -> mk (Ast.Defunion (sym n, List.map variant vs))
| _ -> fail f "defunion is (defunion Name [(Case [field Type ...]) ...])")
+ (* The slot after the parameters is unconditionally the return type. It used
+ to be optional, and the parser decided return-type-versus-body by looking
+ the symbol up in a set of the file's type names -- sound only because one
+ top-level namespace means a name cannot be both a type and a value, and
+ brittle because the set had to be complete. It was wrong twice in one day,
+ the second time 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. A silent misparse is the worst failure class available,
+ and macros now generate definitions, which widens it.
+
+ Mandatory removes the guess: nothing is consulted, [()] is what a function
+ that returns nothing writes, and a mistyped type is a mistyped type --
+ [(defn f [] f65 0.0)] reaches the resolver's near-miss check and comes back
+ as *did you mean f64*, where it used to come back as an unknown name. *)
| List ({ v = Sym "defn"; _ } :: args) ->
(match args with
- | n :: { v = Vec ps; _ } :: rest ->
- let ret, body =
- match rest with
- (* An omitted return type means Unit. A leading form that is a type
- and is not the whole body is the return type. *)
- | [] -> None, []
- (* A lone [()] is the return type and an empty body, never a body of
- one form: [()] is not an expression, so there is nothing for the
- [more <> []] guard below to protect here. *)
- | [ ({ v = List []; _ } as only) ] -> Some (texpr only), []
- | first :: more when more <> [] && is_type_form types first ->
- Some (texpr first), body_of more
- | _ -> None, body_of rest
+ | n :: { v = Vec ps; _ } :: ret :: body ->
+ (* The slot's own failure, because the thing found there is almost
+ always the old spelling: a body whose first form was a call, written
+ when the slot could be left out. [texpr]'s "expected a type" alone
+ would be true and unhelpful. *)
+ let rty =
+ try texpr ret with
+ | Loc.Error (loc, msg) ->
+ Loc.fail loc
+ "%s. This is the return type, which every defn states -- a \
+ function that returns nothing writes ()" msg
in
- mk (Ast.Defn { Ast.name = sym n; params = fields f ps; ret;
- fbody = body; nloc = n.loc })
- | _ -> fail f "defn is (defn name [param Type ...] ReturnType? body ...)")
+ mk (Ast.Defn { Ast.name = sym n; params = fields f ps;
+ ret = Some rty; fbody = body_of body;
+ nloc = n.loc })
+ | _ ->
+ fail f
+ "defn is (defn name [param Type ...] ReturnType body ...). The return \
+ type is not optional; a function that returns nothing writes ()")
| List ({ v = Sym ("declare" | "declare-c" as which); _ } :: args) ->
(* (declare name [param Type ...] ReturnType? "c_symbol"). The C symbol is
@@ -840,51 +846,6 @@ let rec decl types (f : Form.t) : Ast.decl =
| List ({ v = Sym s; _ } :: _) -> fail f "unknown top-level form (%s ...)" s
| _ -> fail f "expected a top-level declaration, found %s" (Form.to_string f)
-(* Is this form the function's return type, or the first form of its body?
-
- Shape alone cannot tell: [(Option f64)] and [(Some 1)] are identical
- s-expressions, one a type application and one a constructor call. Capitalised
- heads are not a good enough signal — [(defn f [] (Some 1) (bar))] would eat
- the body's first form as a return type, silently.
-
- So the decision uses the set of names that are actually types, which the
- pre-pass in [program] collects from the file's own declarations. That makes
- it exact rather than heuristic, because types are only ever introduced by
- defstruct, defunion and defalias — all syntactically obvious. *)
-(* A name a package brought in — [rl/Vector2]. It cannot be in [types]: the set
- comes from this file's own declarations, and an import is not resolved until
- after parsing, so a package's structs are unknown here by construction.
-
- The signal is the alias plus the capital. An alias is syntactically obvious,
- collected by the same pre-pass; and a bare capitalised symbol is never a
- *value* in this language — a struct or union constructor is [(Name {...})],
- a List, and an enum member is a keyword. So [alias/Name] in a type position
- is a type, and the case the comment above warns about — a body form eaten as
- a return type — cannot arise, because there is no body form of that shape.
-
- A lowercase qualified name stays an expression, which is what [rl/get-color]
- in [(defn f [] rl/get-color)] has to be. *)
-and qualified_type types s =
- match String.index_opt s '/' with
- | None -> false
- | Some i ->
- let alias = String.sub s 0 i and name = String.sub s (i + 1) (String.length s - i - 1) in
- Names.mem ("import " ^ alias) types
- && name <> ""
- && name.[0] >= 'A' && name.[0] <= 'Z'
-
-and is_type_form types (f : Form.t) =
- match f.v with
- | List [] -> true (* () is unit, never a body form *)
- | Sym s ->
- Names.mem s types || Names.mem ("enum " ^ s) types
- || Names.mem ("prelude " ^ s) types || qualified_type types s
- | Vec _ -> true (* [T] and [n T] are only types *)
- | Map _ -> true (* {K V} in this position *)
- | List ({ v = Sym n; _ } :: _) ->
- Names.mem n types || qualified_type types n
- | _ -> false
-
and variant (f : Form.t) : Ast.variant =
match f.v with
| Sym n -> { Ast.vname = n; vfields = []; vloc = f.loc }
@@ -893,71 +854,6 @@ and variant (f : Form.t) : Ast.variant =
| List [ { v = Sym n; _ } ] -> { Ast.vname = n; vfields = []; vloc = f.loc }
| _ -> fail f "a union case is Name or (Name [field Type ...])"
-(* Names introduced as types by a list of forms. Collected before anything is
- parsed, so a type declared at the bottom of a file is still known to a
- function at the top — top-level names are order-independent. *)
-let types_in (base : Names.t) (forms : Form.t list) : Names.t =
- List.fold_left
- (fun acc (f : Form.t) ->
- match f.v with
- | List [ { v = Sym ("defstruct" | "defunion" | "defalias"); _ };
- { v = Sym n; _ }; _ ] -> Names.add n acc
- (* The aliases too, under a key no symbol can collide with, so that
- [qualified_type] can tell [rl/Vector2] from a name with a slash in
- it that nothing imported. *)
- | List [ { v = Sym "import"; _ }; { v = Sym a; _ }; { v = Str _; _ } ] ->
- Names.add ("import " ^ a) acc
- (* An enum is a type too, but under its own key rather than beside the
- structs, because [(Key n)] is now a *value* — the integer-to-enum
- conversion — and putting Key in [types] would make [is_type_form]
- read that as a type application and eat it as a return type. So an
- enum name counts only as a bare symbol, which is the one position it
- can appear in as a type, and never as a list head. *)
- | List [ { v = Sym "defenum"; _ }; { v = Sym n; _ }; _ ] ->
- Names.add ("enum " ^ n) acc
- | _ -> acc)
- base forms
-
-(* The prelude's types are every file's types. [Check.program] prepends the
- prelude to every program, so StorageExhausted, FileError and Form are as
- available as i32 is — but [types_in] reads one file's own declarations, and
- the prelude is a different list of forms, so nothing here knew that.
-
- It read as a gap that could not matter, because a bare capitalised name is
- the only type position this set is consulted for and the prelude's structs
- were only ever *taken* as parameters, never *returned*. Macros are where it
- bites: a macro is (defn m [args [Form]] Form ...), and [Form] as the return
- type was parsed as the first form of the body and reported as an unknown
- name — the parser deciding a declared type was a value. [[Form]] worked,
- because a Vec in that position is a type whatever is in it, which is exactly
- the kind of half-working that hides this.
-
- They go in under their own key, for the reason the enums do one comment up.
- Added plainly, [is_type_form]'s list-head arm would read [(Rune {.code 65})]
- as a type application and eat it as a return type — silently, in every file
- in the language, which is the exact failure the comment above
- [is_type_form] warns about. No prelude type takes arguments, so a bare
- symbol is the only type position any of them can occupy, and the bare-symbol
- arm is the only one that asks.
-
- Read once: the prelude is a constant string and this set is a constant of
- it. *)
-let prelude_types =
- lazy
- (List.fold_left
- (fun acc (f : Form.t) ->
- match f.v with
- | Form.List [ { v = Form.Sym ("defstruct" | "defunion" | "defalias"); _ };
- { v = Form.Sym n; _ }; _ ] ->
- Names.add ("prelude " ^ n) acc
- | Form.List [ { v = Form.Sym "defenum"; _ }; { v = Form.Sym n; _ }; _ ] ->
- Names.add ("enum " ^ n) acc
- | _ -> acc)
- builtin_types (Prelude.forms ()))
-
-let declared_types (forms : Form.t list) : Names.t =
- types_in (Lazy.force prelude_types) forms
-
(* Macro expansion, which runs over [Form] and therefore before anything in
this file. It cannot be called directly: expanding a macro means compiling
it and dlopening it, so the expander sits above [Check] and [Build] and this
@@ -976,16 +872,14 @@ let program (forms : Form.t list) : Ast.decl list =
macros have to parse in a process that has not built a macro module yet.
Then expansion, which may need one. *)
let forms = !expander (List.map Expand.quasiquote forms) in
- let types = declared_types forms in
temps := 0;
- List.map (decl types) forms
+ List.map decl forms
-(* Single-declaration entry point, for tests and the REPL. Sees the builtin and
- prelude types plus whatever this one form declares. *)
+(* Single-declaration entry point, for tests and the REPL. *)
let decl (f : Form.t) : Ast.decl =
temps := 0;
match !expander [ Expand.quasiquote f ] with
- | [ f ] -> decl (declared_types [ f ]) f
+ | [ f ] -> decl f
| fs ->
(* One declaration in, one out. A macro at the top level would break that,
and there is no top-level macro call: [decl] dispatches on the head and
diff --git a/lib/prelude.ml b/lib/prelude.ml
index 1e0b8b9..11eb872 100644
--- a/lib/prelude.ml
+++ b/lib/prelude.ml
@@ -1501,9 +1501,9 @@ let file = "run builds to a temporary file and execs it.
(defn main [] ()
(println "hello from flan"))
-The entry point is (defn main [args [string]] i32). Both the parameter
-and the return type are optional: omitting args means the program ignores
-argv, and omitting the return type means Unit and an exit status of 0.
The entry point is (defn main [args [string]] i32). The parameter is
+optional — omitting args means the program ignores argv — and the return
+type is not: () is unit, and a main that returns it exits 0.
(Option T)Some / Nonei32Unit()Never(defn name [param Type ...] ReturnType? body ...). The parameters are
-inline name/type pairs, as in let and defstruct. An omitted
-return type means Unit. There is no separate declare form for
-a function with a body — declare is kept only where there is none.
(defn name [param Type ...] ReturnType body ...). The parameters are
+inline name/type pairs, as in let and defstruct. The return
+type is always written, and a function that returns nothing writes (),
+which is unit. There is no separate declare form for a function with a
+body — declare is kept only where there is none.
The slot used to be optional, and the parser decided return-type-versus-body by
+looking the name up in a table of the file's types. It was sound only because one
+top-level namespace means a name cannot be both a type and a value, and it was
+silently wrong twice — once reading (Rune {.code 65}) at the head of a
+body as the function's return type. Writing the type removes the guess, and a
+mistyped one now says did you mean f64 rather than unknown name.
Top-level names are order-independent within a package, so mutually recursive functions need no forward declaration. Globals come in two kinds:
@@ -765,7 +773,7 @@ user-supplied printer to choose between. none no newline: true -The walk covers every integer and float type, bool, Unit,
+
The walk covers every integer and float type, bool, (),
string, [u8], enums, Ptr, Option,
structs, fixed arrays and slices. An enum member comes back as its name: the value is
an i32 by the time the backend sees it, so the name is recovered here from
@@ -916,7 +924,7 @@ signalling end says here is something notable, here is the data, and an
caller decides what to do about it — or decides nothing, in which case the signaller
carries on.
(signal c) ; Unit. Handler returns -> carry on. No handler -> no-op.
+(signal c) ; (). Handler returns -> carry on. No handler -> no-op.
(error c) ; Never. Only a transfer gets past; else the program stops.
(handler-bind [(Type [c] body ...) ...] body ...) ; match by type, no hierarchy
@@ -926,7 +934,7 @@ carries on.
(invoke-restart 'name) ; Never. Innermost frame offering the name wins.
-signal has type Unit, always. A handler that returns
+
signal has type (), always. A handler that returns
normally leaves the signaller to carry on — the accumulation case:
(defstruct AssetMissing [id i32])
@@ -934,7 +942,7 @@ normally leaves the signaller to carry on — the accumulation case:
(defvar seen i64)
(defn load-all [] ()
- (signal (AssetMissing {.id 1})) ; Unit — the caller carries on
+ (signal (AssetMissing {.id 1})) ; () — the caller carries on
(signal (AssetMissing {.id 2})))
(defn main [] ()