The return type stops being a guess: the slot is mandatory, unit is ()
The slot after a defn's parameters is unconditionally a type. Parse.decl no
longer takes a set of type names, and is_type_form, qualified_type, types_in,
declared_types and prelude_types are gone with the pre-pass that fed them.
What they were for: (Option f64) and (Some 1) are the same s-expression, so the
parser decided which it had by looking the head up in a set of the file's own
type names. Sound -- 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.
Two things fall out. A type the parser could not have known -- a struct
declared further down the file, rl/Vector2 behind an unresolved alias, a
prelude type -- never needed recognising, only placing. And a mistyped type is
a mistyped type: (defn f [] f65 0.0) reaches the resolver's near-miss check and
says did you mean f64, where it used to be read as the first form of the body
and reported as an unknown name.
Unit is written (). The old spelling is refused with a message naming the new
one, the rule the colon-to-dot change followed. Internally it is still
Tname "Unit" and Types.Unit, so the resolver, the shim and the emitter did not
change; Cimport still builds Tname "Unit" for C's void without going through
the parser. Types.to_string prints () though -- that printer prints what a
person would write for every other type it knows, [i32], {K V}, (Ptr T), and
Unit was the odd one out once the source spelling moved.
Dropping prelude_types removes one of the two reasons Macro.reduce may only
drop defns: the memoised set a bootstrap build could have poisoned is gone, so
the remaining reason is the plain one.
This commit is contained in:
parent
26c53e0a19
commit
df73f87b2f
117
BUILT.md
117
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
|
||||
|
||||
48
NEXT.md
48
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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
10
lib/macro.ml
10
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. *)
|
||||
|
||||
190
lib/parse.ml
190
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
|
||||
|
||||
@ -1501,9 +1501,9 @@ let file = "<prelude>"
|
||||
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)
|
||||
|
||||
@ -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 ^ "]"
|
||||
|
||||
13
plan.org
13
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
|
||||
|
||||
@ -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".
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 [] ()
|
||||
|
||||
@ -340,9 +340,9 @@ produced. <code>run</code> builds to a temporary file and execs it.</p>
|
||||
<pre><code>(defn main [] ()
|
||||
(println "hello from flan"))</code></pre>
|
||||
|
||||
<p>The entry point is <code>(defn main [args [string]] i32)</code>. Both the parameter
|
||||
and the return type are optional: omitting <code>args</code> means the program ignores
|
||||
argv, and omitting the return type means <code>Unit</code> and an exit status of 0.</p>
|
||||
<p>The entry point is <code>(defn main [args [string]] i32)</code>. The parameter is
|
||||
optional — omitting <code>args</code> means the program ignores argv — and the return
|
||||
type is not: <code>()</code> is unit, and a <code>main</code> that returns it exits 0.</p>
|
||||
|
||||
<h2 id="values">Values and memory</h2>
|
||||
|
||||
@ -448,7 +448,7 @@ notation reads as exactly one data item.</p>
|
||||
<tr><td><code>(Option T)</code></td><td><code>Some</code> / <code>None</code></td><td>tag byte + T</td></tr>
|
||||
<tr><td>a struct</td><td>value type</td><td>fields in declaration order</td></tr>
|
||||
<tr><td>an enum</td><td>its own type in the checker</td><td><code>i32</code></td></tr>
|
||||
<tr><td><code>Unit</code></td><td>one value, zero size</td><td>empty</td></tr>
|
||||
<tr><td><code>()</code></td><td>one value, zero size</td><td>empty</td></tr>
|
||||
<tr><td><code>Never</code></td><td>fits anywhere; nothing has it</td><td>empty</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
@ -552,10 +552,18 @@ aliases. A second declaration of a name is rejected whatever kind either one is.
|
||||
|
||||
<h2 id="functions">Functions</h2>
|
||||
|
||||
<p><code>(defn name [param Type ...] ReturnType? body ...)</code>. The parameters are
|
||||
inline name/type pairs, as in <code>let</code> and <code>defstruct</code>. An omitted
|
||||
return type means <code>Unit</code>. There is no separate <code>declare</code> form for
|
||||
a function with a body — <code>declare</code> is kept only where there is none.</p>
|
||||
<p><code>(defn name [param Type ...] ReturnType body ...)</code>. The parameters are
|
||||
inline name/type pairs, as in <code>let</code> and <code>defstruct</code>. The return
|
||||
type is always written, and a function that returns nothing writes <code>()</code>,
|
||||
which is unit. There is no separate <code>declare</code> form for a function with a
|
||||
body — <code>declare</code> is kept only where there is none.</p>
|
||||
|
||||
<p>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 <code>(Rune {.code 65})</code> at the head of a
|
||||
body as the function's return type. Writing the type removes the guess, and a
|
||||
mistyped one now says <em>did you mean f64</em> rather than <em>unknown name</em>.</p>
|
||||
|
||||
<p>Top-level names are order-independent within a package, so mutually recursive
|
||||
functions need no forward declaration. Globals come in two kinds:</p>
|
||||
@ -765,7 +773,7 @@ user-supplied printer to choose between.</p>
|
||||
none
|
||||
no newline: true</code></pre>
|
||||
|
||||
<p>The walk covers every integer and float type, <code>bool</code>, <code>Unit</code>,
|
||||
<p>The walk covers every integer and float type, <code>bool</code>, <code>()</code>,
|
||||
<code>string</code>, <code>[u8]</code>, enums, <code>Ptr</code>, <code>Option</code>,
|
||||
structs, fixed arrays and slices. An enum member comes back as its name: the value is
|
||||
an <code>i32</code> by the time the backend sees it, so the name is recovered here from
|
||||
@ -916,7 +924,7 @@ signalling end says <em>here is something notable, here is the data</em>, and an
|
||||
caller decides what to do about it — or decides nothing, in which case the signaller
|
||||
carries on.</p>
|
||||
|
||||
<pre><code>(signal c) ; Unit. Handler returns -> carry on. No handler -> no-op.
|
||||
<pre><code>(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.</p>
|
||||
|
||||
(invoke-restart 'name) ; Never. Innermost frame offering the name wins.</code></pre>
|
||||
|
||||
<p><code>signal</code> has type <code>Unit</code>, always. A handler that returns
|
||||
<p><code>signal</code> has type <code>()</code>, always. A handler that returns
|
||||
normally leaves the signaller to carry on — the accumulation case:</p>
|
||||
|
||||
<pre><code>(defstruct AssetMissing [id i32])
|
||||
@ -934,7 +942,7 @@ normally leaves the signaller to carry on — the accumulation case:</p>
|
||||
(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 [] ()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user