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 = "" drops every [defn] depending, directly or transitively, on a macro. A *macro* that lands in that set is refused by name — see [Macro.reduce]. - A ref rather than a parameter because the readers are [Check.program] and - [Parse.prelude_types], neither of which can be told, and because [Macro] - sits above both and cannot be depended on from here. *) + A ref rather than a parameter because the reader is [Check.program], which + cannot be told, and because [Macro] sits above it and cannot be depended on + from here. *) let bootstrap : (Form.t list -> Form.t list) ref = ref (fun fs -> fs) let forms () = !bootstrap (Reader.read_all ~file source) diff --git a/lib/types.ml b/lib/types.ml index ff3918c..463343d 100644 --- a/lib/types.ml +++ b/lib/types.ml @@ -66,7 +66,10 @@ let fkind_of_name = function | "f32" -> Some F32 | "f64" -> Some F64 | _ -> None (* Every name the resolver accepts as a primitive type. The list exists so a - near-miss can be reported as the typo it is. *) + near-miss can be reported as the typo it is. [Unit] is on it because the + resolver still answers to that name -- [Cimport] builds [Tname "Unit"] for + C's void, and never goes through the parser -- but nobody writes it: unit + is spelled [()] in source, and [Parse.texpr] refuses the word. *) let primitive_names = [ "i8"; "i16"; "i32"; "i64"; "u8"; "u16"; "u32"; "u64"; "f32"; "f64"; "bool"; "string"; "Unit"; "Never"; "Allocator" ] @@ -104,7 +107,7 @@ let rec to_string = function | Float k -> fkind_name k | Bool -> "bool" | String -> "string" - | Unit -> "Unit" + | Unit -> "()" | Never -> "Never" | Named n | Enum n -> n | Slice t -> "[" ^ to_string t ^ "]" diff --git a/plan.org b/plan.org index 1a6bf92..85b579e 100644 --- a/plan.org +++ b/plan.org @@ -102,7 +102,7 @@ world. integers, enums, strings, fixed arrays and value structs; pointers, slices and owning containers are excluded. A map is homogeneous, and empty construction is type-directed: ~(defvar enemies (Map string Enemy) (map-new))~. ~get~ - returns ~(Option V)~; ~put~ is the `Unit`-returning upsert. See + returns ~(Option V)~; ~put~ is the ~()~-returning upsert. See spec-memory.md for the deferred move-aware operations. - Operations: ~get~, ~put~, ~remove~, ~push~, ~pop~, ~at~, ~len~, ~update~. Copying is explicit: ~(clone m)~, and owning containers move rather than copy on @@ -199,8 +199,11 @@ and on a managed ~class~ instance. An ordinary ~struct~ never carries one. ~declare~ is kept only where there is no body (forward declarations, FFI). - Annotations at function boundaries are unavoidable, because compile-time overloading is incompatible with full inference. Locals are inferred. -- An omitted return type means ~Unit~ — a real zero-sized type with one value, - not C's ~void~. Generic code over it works, so there is no ~Action~/~Func~ split. +- The return type is always written, and a function that returns nothing writes + ~()~ — a real zero-sized type with one value, not C's ~void~. Generic code over + it works, so there is no ~Action~/~Func~ split. The slot was optional once and + the parser guessed between a return type and a body form from a table of type + names; the guess was silently wrong twice, so the slot is mandatory. - Every type notation reads as exactly one data item: ~[f32]~, ~[4 f32]~, ~(Vec f32)~, ~{string i32}~, ~(Ptr World)~, ~(Fn [f32] bool)~, ~(Option a)~, ~(Handle a)~. @@ -278,7 +281,7 @@ an outliving array copies it. A pointer-to-condition would dangle. *Under static typing.* Restarts are dynamically scoped and named, so ~(invoke-restart 'skip-form)~ cannot be fully checked at compile time. Accept a runtime error initially; a statically tracked restart set (as Zig tracks error sets) -is a nice-to-have, not a blocker. ~signal~ has type ~Unit~, ~invoke-restart~ and +is a nice-to-have, not a blocker. ~signal~ has type ~()~, ~invoke-restart~ and ~error~ have type ~Never~, and every restart clause shares one type with the ~restart-case~ body — so a ~restart-case~ in value position needs a fall-through that produces the type or diverges. @@ -393,7 +396,7 @@ dynamic printers need. *Entry point.* ~(defn main [args [string]] i32)~. Both the parameter and the return type are optional: omitting ~args~ means the program ignores argv, -omitting the return type means ~Unit~ and an exit status of 0. sand.flan uses +a return type of ~()~ means an exit status of 0. sand.flan uses the short form, calc-me the long one. *RNG is ours, not libc's.* ~rand-f32~ is a seeded PRNG implemented in Flan diff --git a/sand.flan b/sand.flan index 3338764..b5a215f 100644 --- a/sand.flan +++ b/sand.flan @@ -24,7 +24,7 @@ ;;;; [T] slice, ptr+len, non-owning (Vec T) owning, move-only ;;;; (Ptr T) pointer (Handle T) generational handle ;;;; types are inline name/type pairs, as in `let` and `defstruct` -;;;; an omitted return type means Unit +;;;; a return type of () means the function returns nothing ;;;; lowercase in a TYPE position is a type variable; in a LENGTH position ;;;; it is an ordinary compile-time value, so [rows [cols u32]] is unambiguous @@ -426,7 +426,7 @@ ;; Release builds compile the same source to direct calls. ;; ;; A cell holds an (Fn ...) — a plain function pointer, no captured environment; -;; this one is (Fn [] Unit), `settle`'s is (Fn [i32 i32] Unit). Redefining +;; this one is (Fn [] ()), `settle`'s is (Fn [i32 i32] ()). Redefining ;; `settle` while `game-update` is mid-frame is safe ;; because old code is never unloaded; changing its SIGNATURE is not, and the ;; reload rejects it. See plan.org "What redefinition cannot do". diff --git a/spec-conditions.md b/spec-conditions.md index 1391c8e..250729d 100644 --- a/spec-conditions.md +++ b/spec-conditions.md @@ -7,10 +7,10 @@ Four operators: `handler-bind`, `handler-case`, `restart-case`, `invoke-restart` No condition class hierarchy — condition types are structs, matching is by type plus an optional predicate. -## 1. `signal` returns `Unit` +## 1. `signal` returns `()` -`(signal c)` has type `Unit`, always. When every applicable handler returns -normally without transferring, `signal` returns `Unit` and the signalling +`(signal c)` has type `()`, always. When every applicable handler returns +normally without transferring, `signal` returns `()` and the signalling function simply carries on. This is the accumulation case. The alternative — `signal` producing a value supplied by the handler — was @@ -37,7 +37,7 @@ must produce its type on the *fall-through* path too: ## 2. No handler `signal` with no matching handler on the handler stack is a **no-op** that -returns `Unit`. It does not abort, does not print, does not enter a break loop. +returns `()`. It does not abort, does not print, does not enter a break loop. `(error c)` is the diverging variant: same lookup, but with type `Never` and, if nothing handles it, it enters the dev-build break loop or aborts in release. diff --git a/spec-memory.md b/spec-memory.md index 72f3f66..dec8722 100644 --- a/spec-memory.md +++ b/spec-memory.md @@ -47,7 +47,7 @@ An empty map takes its type from its context: ``` `(get m k)` returns `(Option V)`: absence is `None`, not an untyped `nil`. -`(put m k v)` is the upsert operation and returns `Unit`; it either inserts or +`(put m k v)` is the upsert operation and returns `()`; it either inserts or replaces. `(set (get m k) v)` is not map syntax. The first Map implementation admits copyable keys and values only, so `get` @@ -391,7 +391,7 @@ the allocator cannot satisfy a request, the operation signals with `error`, whose type is `Never` (spec-conditions.md §2), inside a `restart-case` offering `retry`. This is one rule over *every* allocating operation — `vec-new`, `map-new`, `push`, `put`, `reserve`, `clone` — so their -result types stay `(Vec T)`, `Unit`, `Unit` and so on, with no `Result` and no +result types stay `(Vec T)`, `()`, `()` and so on, with no `Result` and no out-parameter anywhere. What that buys, against the alternative: Odin's `append` returns an ignorable diff --git a/syntax-sketch.flan b/syntax-sketch.flan index c783a5a..de6241e 100644 --- a/syntax-sketch.flan +++ b/syntax-sketch.flan @@ -3,7 +3,8 @@ ;; Rules held here: ;; - every type notation reads as exactly ONE data item ;; - types are inline name/type pairs, as in `let` and `defstruct` -;; - an omitted return type means Unit (a real zero-sized type, not C's void) +;; - the return type is always written; () is unit, a real zero-sized type +;; rather than C's void ;; - lowercase type names are variables, Capitalized are concrete ;; - no `!` convention (nothing is immutable), no `->`, no sigils ;; @@ -119,7 +120,7 @@ ;; file is fixed on disk. So it offers a menu and the caller chooses. (defcondition AssetMissing [path string]) -;; `signal` has type Unit and RETURNS if every handler returns normally, so the +;; `signal` has type () and RETURNS if every handler returns normally, so the ;; fall-through path of a restart-case in value position must still produce the ;; type. `abort` has type Never, which unifies with (Handle Texture). ;; Every clause body and the restart-case body share one type. diff --git a/test/programs/conditions.flan b/test/programs/conditions.flan index c439cfd..a42a29e 100644 --- a/test/programs/conditions.flan +++ b/test/programs/conditions.flan @@ -1,7 +1,7 @@ ;;;; handler-bind and signal — spec-conditions.md §1 and §2. ;;;; ;;;; The accumulation case, which is what makes these two worth having on their -;;;; own: signal returns Unit, a handler that returns normally leaves the +;;;; own: signal returns (), a handler that returns normally leaves the ;;;; signalling function to carry on, and with nothing matching signal is a ;;;; no-op. No control flow is altered, so none of the transfer machinery ;;;; restart-case needs exists yet. diff --git a/test/programs/exhausted.flan b/test/programs/exhausted.flan index 070c196..6a4eb97 100644 --- a/test/programs/exhausted.flan +++ b/test/programs/exhausted.flan @@ -2,7 +2,7 @@ ;;;; ;;;; No allocating operation returns an error and none can fail silently. The ;;;; operation signals StorageExhausted with `error`, whose type is Never, -;;;; inside a restart-case offering `retry` — so push stays Unit, clone stays +;;;; inside a restart-case offering `retry` — so push stays (), clone stays ;;;; the container, and no signature anywhere grows a Result. Odin's append ;;;; returns an ignorable Allocator_Error; an append that appends nothing and ;;;; says nothing is the outcome this rule exists to make impossible. diff --git a/test/programs/macro-unless.flan b/test/programs/macro-unless.flan index 4e2b95c..ac3ca0e 100644 --- a/test/programs/macro-unless.flan +++ b/test/programs/macro-unless.flan @@ -32,7 +32,7 @@ (unless (< n 3) (println "7 is not less than 3"))) ;; Inside a function that returns a value, and inside a loop: the expansion - ;; is an if with no else, so it is Unit and it does not decide the body's + ;; is an if with no else, so it is () and it does not decide the body's ;; value. (println (classify 4)) (println (classify 5)) diff --git a/test/programs/map-exhausted.flan b/test/programs/map-exhausted.flan index 4ca6e2e..92ed688 100644 --- a/test/programs/map-exhausted.flan +++ b/test/programs/map-exhausted.flan @@ -1,7 +1,7 @@ ;;;; StorageExhausted and retry, over a Map — spec-memory.md, "Allocation ;;;; failure". The rule is one rule over *every* allocating operation, so it has ;;;; to hold for map-new, put, reserve and clone exactly as exhausted.flan shows -;;;; it holding for vec-new, push, reserve and clone. put stays Unit, clone +;;;; it holding for vec-new, push, reserve and clone. put stays (), clone ;;;; stays the container, and no signature anywhere grows a Result. ;;;; ;;;; A map is the harder case of the two, and that is why it gets its own diff --git a/test/programs/println.flan b/test/programs/println.flan index 1e105f7..d77ad92 100644 --- a/test/programs/println.flan +++ b/test/programs/println.flan @@ -63,7 +63,7 @@ (println true) (println false) - ;; Unit is evaluated and *then* reported: a Unit expression is a call made + ;; () is evaluated and *then* reported: a () expression is a call made ;; for its effect, so emitting () without running it would be a lie. (println (nothing)) diff --git a/test/programs/unit-main.flan b/test/programs/unit-main.flan index 9f86544..08e6238 100644 --- a/test/programs/unit-main.flan +++ b/test/programs/unit-main.flan @@ -1,4 +1,4 @@ ;;;; The short entry point: both the parameter and the i32 status are optional, -;;;; and an omitted return type means Unit, so the process exits 0. +;;;; and a return type of () means the process exits 0. (defn main [] () (println "ok")) diff --git a/test/test_flan.ml b/test/test_flan.ml index ff4a484..bca099d 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -436,6 +436,19 @@ let () = parse_rejects "defmacro in expression position" "(defn f [] () (defmacro m [] 1))" ~needle:"top-level declaration"; + (* The return type is not optional. A void function writes (), and the + refusal says so rather than leaving someone to find it in a grammar. *) + parse_rejects "defn with no return type" "(defn f [] (g))" + ~needle:"a function that returns nothing writes ()"; + parse_rejects "defn with nothing after the parameters" "(defn f [])" + ~needle:"The return type is not optional"; + (* One spelling for unit, and the old one names the new one. *) + parse_rejects "the old Unit spelling in return position" "(defn f [] Unit (g))" + ~needle:"unit is written (), not Unit"; + parse_rejects "the old Unit spelling anywhere else" + "(defn f [g (Fn [i32] Unit)] () (g 1))" + ~needle:"unit is written (), not Unit"; + (* Quasiquote is a desugaring over Form, and it has already run by the time the parser sees anything, so what is written here is what a macro body actually compiles to: the prelude's three form-building functions and @@ -513,11 +526,13 @@ let () = exit 1 end -(* ═══ The return type / body ambiguity ═════════════════════════════ *) -(* (Option f64) and (Some 1) are the same s-expression shape. Which one is a - return type is decided by the set of names that are actually types, not by - capitalisation — otherwise a body starting with a constructor call gets - silently eaten as a return type. *) +(* ═══ The return type is the slot, not a guess ══════════════ *) +(* (Option f64) and (Some 1) are the same s-expression shape, and the parser + used to tell them apart by looking the head up in a set of the file's type + names. The set had to be complete, it twice was not, and the failure was a + body form silently eaten as a return type. The slot is mandatory now, so + there is nothing to look up: whatever is written there is a type, and + whatever follows is the body. These pin that down from both sides. *) (* Through [read], so the parser and checker tables are under the reader's alarm too: their sources go through the same reader. *) @@ -537,38 +552,36 @@ let () = | None -> check (name ^ ": has a defn") false; (false, 0) in - check "known type ctor is a return type" + check "the slot is the return type" (ret_and_body "option" "(defn f [] (Option f64) (g))" = (true, 1)); - check "value ctor is NOT a return type" - (ret_and_body "some" "(defn f [] (Some 1) (bar))" = (false, 2)); + (* The two that used to be decided by the table, and are decided by position + now: a constructor call and a prelude struct literal are body forms + because they are not in the slot, not because anything knows what they + are. [(Rune {.code 65})] is the one that was misparsed in every file in + the language. *) + check "a ctor after the slot is a body form" + (ret_and_body "some" "(defn f [] () (Some 1) (bar))" = (true, 2)); + check "a prelude struct literal after the slot is a body form" + (ret_and_body "preludelit" "(defn f [] () (Rune {.code 65}) (bar))" + = (true, 2)); - check "user struct is a return type" - (ret_and_body "user" - "(defstruct Cursor [pos i32]) (defn f [] Cursor (g))" = (true, 1)); - - (* Order-independent: the type is declared after the function that returns it *) - check "type declared later is still known" + (* And what is *in* the slot is a type whether or not the parser could know + it: a struct declared further down the file, a package's type behind an + alias the parser has not resolved, a prelude type. None of these needed a + pre-pass any more. *) + check "a type declared later is still the return type" (ret_and_body "later" "(defn f [] Cursor (g)) (defstruct Cursor [pos i32])" = (true, 1)); - - check "unknown capitalised head is a body form" - (ret_and_body "unknown" "(defn f [] (Nope 1) (bar))" = (false, 2)); - - (* The prelude's types are every file's types -- Check.program prepends the - prelude to every program -- and until macros needed it, nothing told the - parser so. A macro is (defn m [args [Form]] Form ...) and bare Form in - return position was read as the first form of the body. *) - check "a prelude type is a return type" + check "a prelude type is the return type" (ret_and_body "prelude" "(defn f [] Form (g))" = (true, 1)); + check "an unknown name in the slot is still the return type" + (ret_and_body "unknown" "(defn f [] Nope (bar))" = (true, 1)); - (* And the other half, which is the whole reason those names go in under - their own key: a prelude type is a bare symbol in type position and never - a list head, so a struct literal of one opening a body stays a body form. - Added plainly this reads as a type application and eats the body, in every - file in the language, and nothing would have said so. *) - check "a prelude struct literal is NOT a return type" - (ret_and_body "preludelit" "(defn f [] (Rune {.code 65}) (bar))" = (false, 2)); + (* The body may be empty, which is the shape the old optional slot could not + produce: [(defn f [])] had nowhere to put the type. *) + check "a function with a return type and no body" + (ret_and_body "nobody" "(defn f [] ())" = (true, 0)); () @@ -675,6 +688,11 @@ let () = "(defn g [x u8] ()) (defn f [] () (g 1 2))" ~needle:"takes 1 argument"; rejects_check "wrong return type" "(defn f [] bool 1)" ~needle:"expected bool"; + (* What the mandatory slot bought: a mistyped type in return position is a + mistyped type. It used to be parsed as the first form of the body and + reported as an unknown *name*, which points at the wrong mistake. *) + rejects_check "a mistyped return type says which type was meant" + "(defn f [] f65 0.0)" ~needle:"did you mean f64"; rejects_check "if branches disagree" "(defn f [] i32 (if true 1 true))" ~needle:"expected i32"; diff --git a/tools/unit-return.py b/tools/unit-return.py index 84f2376..2865c39 100755 --- a/tools/unit-return.py +++ b/tools/unit-return.py @@ -45,6 +45,13 @@ symbol, exactly as the prelude's types count: as a list head it would eat change exists to remove. That is sound because no user type takes arguments -- only the builtin constructors do, and they are known already -- but it is a pool and not the real scope, so read the diff. + +And one thing it cannot know at all: a file that spells the *refused* forms on +purpose, to test that they are refused. `test/test_flan.ml` holds six such +sites -- `(defn f [] (g))`, `(defn f [] Unit (g))`, `(defn f [] Nope (bar))`, +`(defn f [] f65 0.0)` -- and this script encodes the old rule, so it wants to +convert every one of them and must not. A clean run over this repo reports +exactly those six; anything else is a real conversion. """ import sys, os, re diff --git a/web/examples/conds.flan b/web/examples/conds.flan index b3f9728..e50c255 100644 --- a/web/examples/conds.flan +++ b/web/examples/conds.flan @@ -3,7 +3,7 @@ (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 [] () diff --git a/web/index.html b/web/index.html index a2283cd..22601ef 100644 --- a/web/index.html +++ b/web/index.html @@ -340,9 +340,9 @@ produced. 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.

Values and memory

@@ -448,7 +448,7 @@ notation reads as exactly one data item.

(Option T)Some / Nonetag byte + T a structvalue typefields in declaration order an enumits own type in the checkeri32 -Unitone value, zero sizeempty +()one value, zero sizeempty Neverfits anywhere; nothing has itempty @@ -552,10 +552,18 @@ aliases. A second declaration of a name is rejected whatever kind either one is.

Functions

-

(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 [] ()