diff --git a/BUILT.md b/BUILT.md index eae583e..34efb28 100644 --- a/BUILT.md +++ b/BUILT.md @@ -2700,6 +2700,11 @@ constant-folds a `powf` of two literals and leaves nothing to link. ### What could not be built, and why it is not "no generics" Four things on NEXT.md's list did not land, and the interesting part is that the reason differs in each case. +**Three of the four have since landed** — see "`map-next!`, the one thing a Map could not do", "Function values, with +no capture" and "A prelude function may call a prelude macro" below — and each was fixed by the thing named here +rather than by generics, which is the argument this list was making. The fourth, the path-insensitive dead set, is +still open. Kept as written because the diagnoses are what the later lanes worked from, and one of them turned out to +be wrong in a way worth being able to see: the prelude *was* reaching the expander. - **`Map` keys and values** need a **map iterator**, and there is none. `flan_map_len`, `_get`, `_put`, `_has`, `_clone`, `_reserve`, `_free` is the runtime's entire map surface; nothing walks the open-addressed block. One @@ -2733,6 +2738,123 @@ Tests: `programs/strings.flan`, `programs/format.flan`, `programs/algorithms.fla `-O2` and `-O0`, and `strings.flan` also in a dev build — the one that checks a container's recorded allocator epoch, so it is what would catch one of these `Vec`s being used after the arena under it was released. +## Function values, with no capture, and why that was the whole blocker + +`map`, `filter`, `reduce` and `sort-by` could not be written, and the previous lane's sharpening of the reason was +right: **function values, not generics**. Generics alone would not have fixed it — without something to pass there is +nothing to be generic over — and function values alone did fix it, which is the evidence. The prelude gained all four +the same day, without generics, and is still one copy per element type, which is the half generics would remove. + +### The shape, and why it was not invented here + +The compiler has built and called function values internally since the Map landed. A `handler-bind` clause is lowered +to a function of its own, its address goes into a `flan_handler`, and the runtime calls it back through +`h->fn(condition, xfer)`; a Map's hash and equality pair is the same arrangement, reached as Odin reaches +`Map_Info`'s two contextless `proc` fields. **The surface feature is that machinery given a name**, not a second one +beside it. `check_fn` is `check_handler_bind`'s clause lifting with the parameters coming from the type instead of +from the condition, and `emit`'s indirect call is the callee expression handed to the same `call_through` a direct +call already went through. + +### A bare name is the function + +``` +(map double xs) +``` + +and not Common Lisp's `#'double`. **This is a Lisp-1 — one top-level namespace, enforced, so a `defn` and a `defvar` +cannot share a name** — which is exactly what makes the bare name safe to read: there is no second binding of +`double` it could have meant instead, so the sharp quote would be punctuation answering a question the language does +not ask. + +A `Types.Fn` is one pointer. There is no environment beside it, so the type resolves to `ptr` and lays out as eight +bytes, and a call through one is byte-for-byte the call a name would have produced — a Flan function's emitted +signature is its parameters followed by the transfer channel whether it was reached by name or by pointer. That is +why a handler established across a `fold` still catches a signal raised by the function the fold was handed: +`programs/fn-values.flan` does exactly that, and it is the case that would fail if an indirect call skipped the +guard. + +### `fn` literals take their types from the position + +`Ast.Fn` carries parameter *names* and no types — that is the surface syntax, not an omission — so an `fn` is +checkable exactly where something says what is wanted. An argument position does, because `named_call` already +threads the callee's parameter type into each argument; a bare `(let [f (fn [x] x)])` does not, and is refused saying +so (`programs/fn-no-type.flan`). A name already written as a `defn` goes anywhere, because it carries its own +signature. + +### What was built, and what was refused by name + +**Built:** a written `(Fn [T ...] R)` annotation; a `defn`'s name in value position; an `fn` literal; a call through +a value, both by the name it is bound to and through a computed head; returning one. Four refusal sites, all four +implemented. + +**Refused, each with its own reason and its own program:** + +- **Capture does not exist** (`fn-capture.flan`). An `fn` is lifted into a function of its own and handed nothing but + its parameters; a reference to a local of the enclosing function is refused by name. This is the same refusal a + handler clause has always carried, and the two now share one message with the construct's name in it. + `spec-memory.md`'s capture cases, and **escaping closures with them, stay deferred** — deliberately, and this is + what keeps a function value a bare code address that cannot outlive anything. +- **An `fn` with nothing to say what it takes** (`fn-no-type.flan`), above. +- **A position that would zero one** (`fn-in-struct.flan`): a struct field, a global, a fixed array's element, + `(zeroed)`. ZII fills an omitted field with all-bytes-zero, and **a zeroed function value is a null pointer, which + is the one kind of zero that is not a value the type can have** — every other type's zero is one: `0`, `false`, an + empty slice, `None`, a union's first case. A parameter, a return type and a `let` binding are not on the list + because none of them is ever conjured, and an `(Option (Fn ...))` is not either, because a `None`'s tag is what + nobody may look past. Nor are a `(Vec (Fn ...))` or a `Map` with function values: the Vec runtime never zeroes + past its length and `flan_map_alloc` zeroes only the hash run, so neither conjures an element nobody pushed or + put. A function value as a map *key* is refused already, by `Types.keyable` — hashing an address is a different + operation from hashing what it points at. +- **A foreign function's address** (`fn-extern.flan`). A Flan function's signature ends with the transfer channel and + a C one does not, and an aggregate crossing the boundary is flattened by a generated shim the raw symbol knows + nothing about. Wrap it in a `defn` and pass that. + +### `Fnval`, and the one thing a dev build cannot do + +`Tast.FnAddr` had two `fnref` cases and now has three. `Flanfn` and `Rtfn` are the compiler's own uses and want the +*symbol*, always — a lifted handler clause and a hash pair have no indirection cell to load from. **`Fnval` is a +function value someone wrote, and in a dev build it is the cell's contents rather than the symbol**, so a value taken +after a redefinition is the new body. Splitting the case rather than overloading `Flanfn` is what keeps that true +without breaking the two paths that must not take it. + +What that does *not* give: a value taken *before* a redefinition and called after it is still the old body. Once the +address is in a slot there is nothing left to re-resolve, and the honest fix is a trampoline per function, which is a +cost every program would pay for a case no one has hit. Named here rather than papered over. + +The two lifted-function name sequences are counted **per kind** — `fn/OWNER/N` and `handler/OWNER/N/TYPE` — +rather than off one list. Sharing a counter would rename every `fn` in a function the moment a `handler-bind` was +added above one, which is a rename for a body that did not change, in exactly the names a redefinition module emits. + +`Tast.CallPtr` is its own node for the same kind of reason. Everything that walks this IR treats `Call`'s string as a +*link-time* edge — `Reach` roots the callee, `Dev` finds the cell, `Emit` may load it — and none of those are +questions an indirect call can answer. `Reach` gains the `Fnval` edge, and that edge is load-bearing: a name used as +a value is never a `Call`, so without it the one function a program passes to `map` is the one function the link +drops. + +### A user-written allocator: still refused, and now for two different reasons + +NEXT.md said it needed "a defn's name in value position". **It has that now, and it is still two things short**, +neither of them a function-value question: + +1. The runtime calls `a->proc(a, mode, p, old_size, size, align)` — six C arguments and no transfer channel — and + every Flan function value's signature ends with one. It is the same mismatch a foreign function's address is + refused for, pointing the other way. +2. `Allocator` is opaque and pointer-width, so there is nowhere for a program to put the `flan_allocator` that + pointer would have to point at. + +The refusal message says both, and `programs/user-allocator.flan` is the row that holds it. `(arena-new ...)` over a +backing buffer remains the parameterised allocator that does exist. + +### The prelude's four + +`map-i32!`/`map-f32!`, `filter-i32`/`filter-f32`, `reduce-i32`/`reduce-f32` and `sort-i32-by!`/`sort-f32-by!`. Two +rules, both inherited rather than invented: the in-place ones write back into the slice they were handed, because a +slice is non-owning and transforming a thing you already own should not allocate; and `filter` allocates and the +caller frees, like everything in the building tier. + +**A `map` that changes the element type is the one shape that did not come with them** — it is one copy per *ordered +pair* of types rather than per type, which is where a per-type family stops being honest. That entry is what is left +in `prelude.ml`'s refusal block where `map, filter, reduce, sort-by` used to be, and its reason is generics. + ## `map-next!`, the one thing a Map could not do `flan_map_len`, `_get`, `_put`, `_has`, `_clone`, `_reserve` and `_free` was the runtime's entire map surface, and diff --git a/NEXT.md b/NEXT.md index 86a6c31..c0d6657 100644 --- a/NEXT.md +++ b/NEXT.md @@ -558,12 +558,16 @@ them wants a language decision. iterator: a `defn` has to name its types and `(defn map-keys [m {K V}] (Vec K))` has no `K`. The loop is three lines at the call site, where `K` is known. -- **`map`, `filter`, `reduce`, and a sort taking a comparator.** Blocked on **function values**, not on generics, - which is the sharper statement than the one this list made. `Types.Fn` exists; `check.ml` refuses it with "a - function type is not implemented yet — milestone 5"; and there is nothing else in the language to pass. Generics on - top of that is what would make them one copy rather than one per element type, but without function values there is - nothing to be generic *over*. `sort-f32!` and `sort-bytes!` are the concrete answer in the meantime, and `sum-i32` - and `sum-f32` already are `reduce` with the `+` written in. +- ~~**`map`, `filter`, `reduce`, and a sort taking a comparator.**~~ **All four are in the prelude.** The diagnosis + was right and is now evidenced: **function values, not generics** — they arrived with no generics at all. See + [`BUILT.md`](BUILT.md), "Function values, with no capture". They are one copy per element type (i32 and f32), which + is the half generics would remove, and a `map` that *changes* the element type is the one shape that did not come + with them — one copy per ordered pair of types rather than per type. + + **Capture is not built and escaping closures stay deferred.** An `fn` is lifted into a function of its own and + handed nothing but its parameters; a reference to an enclosing local is refused by name. That is what keeps a + function value a bare code address with no environment, and it is the next thing to want if a callback needs + state — `spec-memory.md`'s cases 1 and 2 are still the design to build from. - **`(vec-new [u8])` is refused**, so a `(Vec [u8])` can only be made where the *context* names the type. `check.ml`'s `vec_new_elem` accepts a single bare symbol naming a type and nothing else, and a `let` has no type @@ -654,11 +658,13 @@ run one lane at a time; item 4 is disjoint and runs alongside any of them. **`Result`/`try`** follows, being another union. - **Generics are deliberately NOT here.** They feel adjacent and are not urgent, and today is the evidence: `Vec` and - `Map` were the obvious customer and needed none — they are type-erased, with the compiler emitting sizes and the - hash/equality pair per call site, which is Odin's design. The remaining customers are user-written allocators and - escaping closures, and both actually want **function values**, which is a separate milestone-5 feature. Leave - generics until something concrete needs them. + **Generics are deliberately NOT here** — and function values landing has *sharpened* the case rather than made it, + which is the useful update. `Vec` and `Map` needed none, being type-erased. Function values needed none. What + needs them is now concrete and small: the prelude's `map!`/`filter`/`reduce`/`sort-by!` are **two copies each**, + i32 and f32, differing in nothing but the element type; `map-keys`/`map-values` cannot be written at all because + a `defn` must name its types and `(defn map-keys [m {K V}] (Vec K))` has no `K`; and a `map` from `[i32]` to + `[f32]` would be one copy per ordered pair. A user-written allocator is *not* on this list any more — it wants + a C-shaped callback and somewhere to put a `flan_allocator`, neither of which is a type parameter. 6. **`Handle` and the pool.** A reference to something that can die, that reports that it died rather than silently resolving to whatever reused the slot. Wanted on its own terms for entities referred to across frames, and it is the diff --git a/lib/check.ml b/lib/check.ml index 772ad6b..b07e489 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -163,7 +163,11 @@ type ctx = { locals can be refused for the reason it is really refused for rather than as an unknown name. *) outer : (string * binding) list; - mutable in_handler : bool; + (* Set on the context of a body the checker lifted into a function of its + own — a handler clause, or an [fn] literal — and naming which, so the + refusal below says why the enclosing function's locals are not there. Both + are the same gap: capture does not exist. *) + mutable outer_what : string option; (* True wherever handler or restart frames established by this function are on the stack. A [return] from there would leave them pointing into a frame that has gone, so it is refused — the same rule as [defer] inside a @@ -258,14 +262,23 @@ let lookup ctx name = List.assoc_opt name ctx.scope for the reason it is really refused for, rather than as a name nobody has heard of. *) let captured ctx loc name = - if ctx.in_handler && List.mem_assoc name ctx.outer then + match ctx.outer_what with + | Some what when List.mem_assoc name ctx.outer -> + let why = + if String.equal what "a handler" then + "a handler runs from wherever the signal was. Use a global, or pass \ + it on the condition" + else + "an fn is lifted into a function of its own and is handed nothing but \ + its parameters. Pass it in, or use a global" + in raise (Loc.Error (loc, Printf.sprintf - "a handler cannot see %s: it is a local of the function that \ - established the handler, and a handler runs from wherever the \ - signal was. Use a global, or pass it on the condition." name)) + "%s cannot see %s: it is a local of the enclosing function, and \ + %s." what name why)) + | _ -> () let scoped ctx f = let saved = ctx.scope in @@ -339,23 +352,56 @@ let map_type loc (k : Types.t) (v : Types.t) = (Types.to_string k); Types.Map (k, v) +(* The positions a function value may not be written in, and the one reason + they are all the same position: something zeroes it. + + ZII is the language's rule — an omitted struct field, a fixed array's + elements, a [defvar] with no initialiser are all all-bytes-zero — and a + zeroed function value is a null pointer with a signature on it, which is the + one kind of zero that cannot be used for anything. Every other type's zero + is a value: 0, false, an empty slice, [None], a union's first case. So these + are refused where they are written rather than left to crash at the call. + + A parameter, a return type, a [let] binding and an [(Option (Fn ...))] are + not on the list: none of them is ever conjured, and an [Option]'s zero is a + [None] whose tag nobody may look past. *) +let rec no_zeroed_fn loc what (t : Types.t) = + match t with + | Types.Fn _ -> + fail loc + "%s cannot be %s: it would be zeroed, and a zeroed function value is \ + a null pointer — every other type's zero is a value it can have, and \ + this one is not. Pass it as a parameter, or hold it in a let" + what (Types.to_string t) + | Types.Array (_, e) -> no_zeroed_fn loc what e + | _ -> () + let rec resolve env ?(seen = []) (t : Ast.texpr) : Types.t = let loc = t.Ast.tloc in match t.Ast.t with | Ast.Tname n -> resolve_name env ~seen loc n | Ast.Tslice e -> Types.Slice (resolve env ~seen e) - | Ast.Tarray (l, e) -> Types.Array (array_len env loc l, resolve env ~seen e) + | Ast.Tarray (l, e) -> + let e = resolve env ~seen e in + no_zeroed_fn loc "a fixed array's element" e; + Types.Array (array_len env loc l, e) (* {K V} is the type spelling. There is no map *literal*: a bare map form in expression position is a struct literal's field list, and giving the same braces two meanings is what the colon-to-dot change was for. A map is built with (map-new) and filled with (put). *) | Ast.Tmap (k, v) -> map_type loc (resolve env ~seen k) (resolve env ~seen v) - (* The function *value* is refused where it is written; the annotation was - not refused anywhere, so [(defn f [g (Fn [] i32)])] type checked and then - died in emit with "no layout for". Refused here, beside the Map line - above, which is the same shape of not-yet. *) - | Ast.Tfn _ -> unimplemented loc "a function type" 5 + (* (Fn [T ...] R): a function value, which is one code address and no + environment beside it. There is no capture — [check_fn] refuses a + reference to an enclosing local by name — so this is a pointer with a + signature and nothing about it can dangle. + + Where one may be *written* is narrower than where the type resolves, and + the two rules live apart on purpose: this is what the spelling means, and + [no_zeroed_fn] is where a position that would zero one is refused. A + parameter, a return type and a let binding are the positions that work. *) + | Ast.Tfn (ps, r) -> + Types.Fn (List.map (resolve env ~seen) ps, resolve env ~seen r) | Ast.Tapp (name, args) -> (match name, args with | "Ptr", [ a ] -> Types.Ptr (resolve env ~seen a) @@ -677,7 +723,7 @@ let hash_ty = Types.Int Types.U64 none of these is a body anyone wrote. *) let invented_ctx env ret = { env; ret; slots = 0; slot_tys = []; slot_names = []; scope = []; - defers = []; outer = []; in_handler = false; in_frames = None; loops = []; + defers = []; outer = []; outer_what = None; in_frames = None; loops = []; in_defer = false; defer_ok = false; defer_block = "a nested form"; dead = []; borrow = false; owner = "" } @@ -796,7 +842,8 @@ and struct_key_pair env loc n = let one = match h with | Tast.Rtfn s -> rt loc hash_ty (direct s) args - | Tast.Flanfn s -> mk loc hash_ty (Tast.Call (s, args)) + | Tast.Flanfn s | Tast.Fnval s -> + mk loc hash_ty (Tast.Call (s, args)) in mk loc Types.Unit (Tast.Set (Tast.Plocal acc, @@ -832,7 +879,8 @@ and struct_key_pair env loc n = let call = match eq with | Tast.Rtfn s -> rt loc (Types.Int Types.I8) (direct s) args - | Tast.Flanfn s -> mk loc (Types.Int Types.I8) (Tast.Call (s, args)) + | Tast.Flanfn s | Tast.Fnval s -> + mk loc (Types.Int Types.I8) (Tast.Call (s, args)) in let differs = mk loc Types.Bool (Tast.Prim (Tast.Eq, [ call; i8 0L ])) @@ -998,7 +1046,7 @@ let rec check ctx ?want (e : Ast.expr) : Tast.expr = "some early-returns None, so the enclosing function must return an \ Option; this one returns %s" (Types.to_string other)) | Ast.Unwrap (Ast.Utry, _) -> unimplemented loc "try (Result)" 6 - | Ast.Fn _ -> unimplemented loc "fn values" 5 + | Ast.Fn (params, body) -> check_fn ctx ~want loc params body | Ast.Dotimes (label, name, count, body) -> check_dotimes ctx ~want loc label name count body (* (signal c) : Unit, always — spec-conditions.md §1. A handler that returns @@ -1185,10 +1233,27 @@ and var ctx loc ~want name = "%s is a case of the union %s, and a union value names both — \ write %s.%s" name uname uname c.Tast.vname | None -> - if Hashtbl.mem ctx.env.fns name then - unimplemented loc - (Printf.sprintf "the function value %s (a name used as a value)" name) 5 - else begin captured ctx loc name; fail loc "unknown name %s" name end + (* A bare function name *is* the function. This is a Lisp-1 — one + top-level namespace, enforced, so a defn and a defvar cannot share + a name — and that is exactly what makes (map double xs) safe to + read: there is no second binding of [double] for it to have meant + instead, so Common Lisp's #'double would be punctuation answering + a question this language does not ask. *) + (match Hashtbl.find_opt ctx.env.fns name with + | Some (params, ret) -> + (* A foreign function is in [fns] too, and its emitted signature + is C's: no transfer channel, and an aggregate flattened by the + shim. Nothing could call the resulting pointer correctly, so it + is refused for what it is rather than handed out. *) + if Hashtbl.mem ctx.env.externs name then + fail loc + "%s is a foreign function, and its address is not a Flan \ + function value: a Flan function's signature ends with the \ + transfer channel and a C one does not. Wrap it in a defn \ + and pass that" name; + expect loc ~want + (mk loc (Types.Fn (params, ret)) (Tast.FnAddr (Tast.Fnval name))) + | None -> captured ctx loc name; fail loc "unknown name %s" name) (* Reading a move-only local. Every read is a move unless the site said it was a borrow, which is the conservative direction: passing one to a function, @@ -1248,6 +1313,98 @@ and block ctx ?want ?(defer_ok = false) loc body = let body, ty = go body in mk loc ty (Tast.Do body) +(* (fn [x y] BODY...) — a function value, lifted into a function of its own. + + The same arrangement a handler clause already uses, and deliberately so: + this compiler has built and called function values internally since the Map + landed, and the surface feature is that machinery given a name rather than a + second one invented beside it. + + **No capture, and that is the scope of this milestone.** The body sees its + parameters and the program's globals and nothing else; a reference to a + local of the enclosing function is refused by name (see [captured]) rather + than resolved to something it did not mean. That is what makes the value a + bare code address with no environment behind it, which in turn is what makes + it safe to pass down, return, and store: there is nothing that can outlive + anything. spec-memory.md's capture cases, and escaping closures with them, + stay deferred. + + **The parameter types come from the position.** [Ast.Fn] carries names and + no types — that is the surface syntax, not an omission here — so an fn is + checkable exactly where something says what is wanted. An argument position + does, because [named_call] threads the callee's parameter type into each + argument; a bare [(let [f (fn [x] x)])] does not, and is refused saying so. *) +and check_fn ctx ~want loc (params : string list) body = + let pts, ret = + match want with + | Some (Types.Fn (ps, r)) when List.length ps = List.length params -> ps, r + | Some (Types.Fn (ps, r)) -> + fail loc + "this fn has %d parameter%s and %s was wanted here" + (List.length params) + (if List.length params = 1 then "" else "s") + (Types.to_string (Types.Fn (ps, r))) + | Some other when other <> Types.Never -> + fail loc "expected %s, found an fn" (Types.to_string other) + | _ -> + fail loc + "nothing here says what this fn's parameters are — an fn takes its \ + types from the position it is written in, so it goes in an argument \ + whose parameter is a (Fn [T ...] R), and a name already written as a \ + defn goes anywhere" + in + (* Its own frame and its own empty scope, with [outer] kept only so that a + reference to the enclosing function's locals is refused for the reason it + is really refused for. *) + let fctx = + { env = ctx.env; ret; slots = 0; slot_tys = []; slot_names = []; + scope = []; defers = []; outer = ctx.scope; + outer_what = Some "an fn"; in_frames = None; loops = []; + in_defer = false; defer_ok = false; defer_block = "a nested form"; + dead = []; borrow = false; owner = ctx.owner } + in + List.iter2 + (fun n t -> ignore (bind fctx n t ~assignable:false)) params pts; + let fbody = map_lr (fun e -> check fctx e) body in + (* The same rule an ordinary defn's body follows: the last form is the + answer, and it has to be the declared return type. *) + let fbody = + match List.rev fbody with + | [] -> fbody + | last :: rest -> + List.rev (expect last.Tast.loc ~want:(Some ret) last :: rest) + in + (* Named after the function it was written in and numbered within it, which + is the handler clause's rule and is stable for the same reason: a + redefinition module emits the lifted functions belonging to the bodies it + replaces, and an index into the whole program's list could not say which + those were. *) + let fname = + (* Counted per *kind*, not over everything this function has lifted. A + handler clause and an fn share one list, and a shared counter would + renumber every fn in a function the moment a handler-bind was added + above one — a rename for a body that did not change, in the names a + redefinition module emits. Two counters, two stable sequences. *) + let mine = + List.filter + (fun (l : Tast.fn) -> + l.Tast.fparent = Some ctx.owner + && String.length l.Tast.name >= 3 + && String.sub l.Tast.name 0 3 = "fn/") + ctx.env.lifted + in + Printf.sprintf "fn/%s/%d" ctx.owner (List.length mine) + in + ctx.env.lifted <- + { Tast.name = fname; params = pts; + slots = Array.of_list (List.rev fctx.slot_tys); + snames = Array.of_list (List.rev fctx.slot_names); + ret; body = fbody; fdefers = []; + fparent = Some ctx.owner; floc = loc } + :: ctx.env.lifted; + expect loc ~want + (mk loc (Types.Fn (pts, ret)) (Tast.FnAddr (Tast.Fnval fname))) + (* A handler runs where the *signal* was, not where it was established, so it cannot be a branch in the function that wrote it: it is lifted into a function of its own and reached through a pointer. @@ -1278,7 +1435,7 @@ and check_handler_bind ctx ?want loc clauses body = the enclosing one. *) let hctx = { env = ctx.env; ret = Types.Unit; slots = 0; slot_tys = []; slot_names = []; - scope = []; defers = []; outer = ctx.scope; in_handler = true; in_frames = None; loops = []; in_defer = false; defer_ok = false; defer_block = "a nested form"; dead = []; borrow = false; owner = "" } + scope = []; defers = []; outer = ctx.scope; outer_what = Some "a handler"; in_frames = None; loops = []; in_defer = false; defer_ok = false; defer_block = "a nested form"; dead = []; borrow = false; owner = "" } in (* The condition crosses as a pointer, because the handler runs while the signalling frame is still alive and there is nothing to copy. @@ -1302,12 +1459,17 @@ and check_handler_bind ctx ?want loc clauses body = stable against an unrelated handler-bind being added elsewhere, which an index into the whole program's lifted list would not be. *) let fname = - Printf.sprintf "handler/%s/%d/%s" ctx.owner - (List.length - (List.filter - (fun (l : Tast.fn) -> l.Tast.fparent = Some ctx.owner) - ctx.env.lifted)) - name + (* Per kind, for the reason [check_fn] gives: an fn lifted out of + the same function must not shift this sequence. *) + let mine = + List.filter + (fun (l : Tast.fn) -> + l.Tast.fparent = Some ctx.owner + && String.length l.Tast.name >= 8 + && String.sub l.Tast.name 0 8 = "handler/") + ctx.env.lifted + in + Printf.sprintf "handler/%s/%d/%s" ctx.owner (List.length mine) name in ctx.env.lifted <- { Tast.name = fname; params = [ Types.Ptr ty ]; @@ -1989,8 +2151,26 @@ and indexed ctx (target : Tast.expr) (idx : Ast.expr list) = and check_call ctx ~want loc (head : Ast.expr) (args : Ast.expr list) = match head.Ast.e with | Ast.Var name -> named_call ctx ~want loc name args - | _ -> - unimplemented loc "calling something other than a named function" 5 + (* A computed head: ((choose k) 3). The head is an ordinary expression and + the only thing asked of it is that it be a function. *) + | _ -> call_value ctx ~want loc (check ctx head) args + +(* The indirect call, once the callee is checked. Shared by the computed head + above and by a name that resolved to a local or a parameter of function + type, which is the shape every caller of [map] has. *) +and call_value ctx ~want loc (callee : Tast.expr) args = + match callee.Tast.ty with + | Types.Fn (params, ret) -> + if List.length args <> List.length params then + fail loc "this function value takes %d argument%s, given %d" + (List.length params) + (if List.length params = 1 then "" else "s") + (List.length args); + let args = map2_lr (fun p a -> check ctx ~want:p a) params args in + expect loc ~want (mk loc ret (Tast.CallPtr (callee, args))) + | other -> + fail loc "this is a %s and not a function, so it cannot be called" + (Types.to_string other) and arity loc name n args = if List.length args <> n then @@ -2404,7 +2584,9 @@ and named_call ctx ~want loc name args = | "zeroed" -> arity loc name 0 args; (match want with - | Some ty when ty <> Types.Never -> mk loc ty (Tast.Zero ty) + | Some ty when ty <> Types.Never -> + no_zeroed_fn loc "this" ty; + mk loc ty (Tast.Zero ty) | _ -> fail loc "zeroed needs to know the type it is zeroing — use it where one is \ @@ -2467,19 +2649,26 @@ and named_call ctx ~want loc name args = (* Every one of these is an ordinary named call, which is the whole of the escape NEXT.md describes: [check_call] already routes a named call through here, so none of the four function-value refusals is anywhere near it. *) - (* A *user-written* allocator is the one thing in this tier that does need - milestone 5, and it is refused by name rather than left as an unknown - one. "Here is my proc, make an Allocator from it" needs a defn's name in - value position, which is the refusal a few hundred lines below this. The - built-in set needs nothing from milestone 5 because its procedures are C - symbols the emitter names and no Flan type mentions them. *) + (* A *user-written* allocator, and the reason it is still refused now that + function values exist. NEXT.md said it needed "a defn's name in value + position"; it has that, and it is still two things short, both of them + nameable and neither of them a function-value question any more. + + The built-in set needs none of it: heap-allocator and arena-new are C + symbols the emitter names, and no Flan type mentions them. *) | "make-allocator" | "allocator-from" | "allocator" -> fail loc - "a user-written allocator is not implemented yet — milestone 5. It needs \ - a defn's name in value position, which is a function value; the \ - built-in allocators (heap-allocator, arena-new) need none of that \ - because their procedures are runtime symbols and no Flan type names \ - them" + "a user-written allocator is not implemented yet, and a defn's name in \ + value position — which is what this used to wait for — is no longer \ + what is missing. Two things are. The runtime calls an allocator as \ + proc(a, mode, p, old, size, align): six C arguments and no transfer \ + channel, and every Flan function value's signature ends with one, so \ + the pointer would be called with the wrong shape (the same mismatch a \ + foreign function's address is refused for). And Allocator is opaque \ + and pointer-width, so there is nowhere for a program to put the \ + flan_allocator the pointer would have to point at. Use \ + (arena-new ...) with a backing buffer, which is the parameterised \ + allocator that does exist" | "heap-allocator" -> arity loc name 0 args; expect loc ~want @@ -3392,6 +3581,22 @@ and named_call ctx ~want loc name args = prim (Tast.Cast target) target [ a ] (* ── ordinary calls ────────────────────────────────────────────── *) + (* A local or a parameter holding a function value, called by the name it is + bound to — which is what the body of [map] looks like. It is checked + before the global function table and after every builtin: a binding + shadows a defn of the same name (one namespace, ordinary lexical + scoping), and nothing shadows [+]. A local of any *other* type falls + through to the table, so a program that shadows a function name with an + i32 and then calls the function still means the function. *) + | _ when (match lookup ctx name with + | Some b -> (match b.bty with Types.Fn _ -> true | _ -> false) + | None -> false) -> + (* The binding the guard already found, read directly. Going back through + [check] would repeat the lookup and walk the move and capture paths for + a type that is neither move-only nor capturable. *) + (match lookup ctx name with + | Some b -> call_value ctx ~want loc (mk loc b.bty (Tast.Local b.slot)) args + | None -> assert false) | _ -> match Hashtbl.find_opt ctx.env.fns name with | Some (params, ret) -> @@ -3536,7 +3741,10 @@ let collect env (decls : Ast.decl list) = in while fold_consts () do () done; let field (f : Ast.field) : Tast.field = - { Tast.fname = f.Ast.fname; fty = resolve env f.Ast.fty } + let fty = resolve env f.Ast.fty in + no_zeroed_fn f.Ast.fty.Ast.tloc + (Printf.sprintf "the field %s" f.Ast.fname) fty; + { Tast.fname = f.Ast.fname; fty } in (* Constants with no declared type are inferred from their value, which needs every other signature in hand — so they are deferred to a pass of their @@ -3704,7 +3912,7 @@ let collect env (decls : Ast.decl list) = run without swallowing it. *) let infer (_, v) = (check { env; ret = Types.Unit; slots = 0; slot_tys = []; slot_names = []; scope = []; defers = []; - outer = []; in_handler = false; in_frames = None; loops = []; in_defer = false; defer_ok = false; defer_block = "a nested form"; dead = []; borrow = false; owner = "" } v).Tast.ty + outer = []; outer_what = None; in_frames = None; loops = []; in_defer = false; defer_ok = false; defer_block = "a nested form"; dead = []; borrow = false; owner = "" } v).Tast.ty in let pending = ref (List.rev !untyped) in let rec settle () = @@ -3757,7 +3965,7 @@ let check_finite env = let check_fn env (fn : Ast.fn) : Tast.fn = let params, ret = Hashtbl.find env.fns fn.Ast.name in let ctx = { env; ret; slots = 0; slot_tys = []; slot_names = []; scope = []; defers = []; - outer = []; in_handler = false; in_frames = None; loops = []; in_defer = false; defer_ok = false; defer_block = "a nested form"; dead = []; borrow = false; + outer = []; outer_what = None; in_frames = None; loops = []; in_defer = false; defer_ok = false; defer_block = "a nested form"; dead = []; borrow = false; owner = fn.Ast.name } in List.iter2 (fun (p : Ast.field) ty -> @@ -3836,6 +4044,7 @@ let check_fn env (fn : Ast.fn) : Tast.fn = this — an allocator is a copyable opaque handle — which is what makes the handler-owns-the-arena shape in exhausted.flan expressible. *) let no_move_only_global loc n (ty : Types.t) = + no_zeroed_fn loc (Printf.sprintf "the global %s" n) ty; if Types.is_move_only ty then fail loc "the global %s is %s, which is move-only, and ownership of a global \ @@ -3846,7 +4055,7 @@ let no_move_only_global loc n (ty : Types.t) = let check_global env (d : Ast.decl) : Tast.global option = let ctx () = { env; ret = Types.Unit; slots = 0; slot_tys = []; slot_names = []; scope = []; defers = []; - outer = []; in_handler = false; in_frames = None; loops = []; in_defer = false; defer_ok = false; defer_block = "a nested form"; dead = []; borrow = false; owner = "" } in + outer = []; outer_what = None; in_frames = None; loops = []; in_defer = false; defer_ok = false; defer_block = "a nested form"; dead = []; borrow = false; owner = "" } in match d.Ast.d with | Ast.Defvar (n, _, init) -> let ty, _ = Hashtbl.find env.globals n in @@ -3978,7 +4187,7 @@ let expression env (e : Ast.expr) : Tast.expr * Types.t array * string option array = let ctx = { env; ret = Types.Unit; slots = 0; slot_tys = []; slot_names = []; scope = []; defers = []; - outer = []; in_handler = false; in_frames = None; loops = []; in_defer = false; defer_ok = false; defer_block = "a nested form"; dead = []; borrow = false; owner = "" } + outer = []; outer_what = None; in_frames = None; loops = []; in_defer = false; defer_ok = false; defer_block = "a nested form"; dead = []; borrow = false; owner = "" } in let t = check ctx e in (t, Array.of_list (List.rev ctx.slot_tys), diff --git a/lib/emit.ml b/lib/emit.ml index a979463..f668f2d 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -96,6 +96,11 @@ let rec ll (t : Types.t) = (* An [Allocator] is a pointer to the runtime's [flan_allocator] and never a copy of one: see Types. Opaque here in the same sense [ptr] is. *) | Types.Alloc -> "ptr" + (* A function value is a code address and nothing else. There is no + environment beside it — capture does not exist (check.ml refuses it by + name) — so it is one pointer, the same width as any other, and a backend + needs to know no more about it than that. *) + | Types.Fn _ -> "ptr" (* ptr + len + cap + allocator, and two more words the runtime owns: see flan_rt.c's (Vec T) header for why they are in every build. Nothing in this file reads a field of one — every operation is a runtime call taking @@ -109,8 +114,8 @@ let rec ll (t : Types.t) = and a copy in the IR are the right number of bytes. *) | Types.Map _ -> "%map" | Types.Option e -> Printf.sprintf "{ i8, %s }" (ll e) - | Types.Fn _ | Types.Var _ -> - (* The checker rejects each of these by name — nothing reaches here. *) + | Types.Var _ -> + (* The checker rejects it by name — nothing reaches here. *) failwith ("no layout for " ^ Types.to_string t) let is_void (t : Types.t) = match t with Types.Unit | Types.Never -> true | _ -> false @@ -260,6 +265,7 @@ let rec lay m (t : Types.t) : int * int = | Types.Enum _ -> 4, 4 | Types.Ptr _ -> 8, 8 | Types.Alloc -> 8, 8 + | Types.Fn _ -> 8, 8 | Types.Vec _ | Types.Map _ -> 48, 8 (* [n x T] adds no padding of its own: T's size already carries its tail. *) | Types.Array (n, e) -> let s, a = lay m e in Int64.to_int n * s, a @@ -288,8 +294,7 @@ let rec lay m (t : Types.t) : int * int = in s, a | None -> failwith ("no layout for struct " ^ n)) - | Types.Fn _ | Types.Var _ -> - failwith ("no layout for " ^ Types.to_string t) + | Types.Var _ -> failwith ("no layout for " ^ Types.to_string t) (* Size, alignment, and the offset of every member. *) and lay_fields m tys = @@ -455,7 +460,19 @@ let rec dty m d (t : Types.t) : int = ("allocator", Types.Alloc); ("gen", Types.Int Types.I64); ("epoch", Types.Int Types.I64) ] |> fun n -> ignore k; ignore v; n - | Types.Fn _ | Types.Var _ -> + (* A pointer to code, and lldb is told exactly that and no more. DWARF + has DW_TAG_subroutine_type for the signature behind it, and spelling + one out here would buy a reader nothing they cannot get from the + function it points at — [p f] answers with an address either way, and + the address is what resolves to a symbol. The name carries the + signature, which is where it is actually legible. *) + | Types.Fn _ -> + dnode d + (Printf.sprintf + "!DIDerivedType(tag: DW_TAG_pointer_type, name: \"%s\", \ + baseType: null, size: 64)" + (Types.to_string t)) + | Types.Var _ -> failwith ("no debug type for " ^ Types.to_string t) in Hashtbl.replace d.dtys key n; @@ -797,12 +814,22 @@ and value_at f (e : Tast.expr) : string = constant. The same spelling the handler frames use for a lifted clause. *) | Tast.FnAddr (Tast.Flanfn n) -> fname n | Tast.FnAddr (Tast.Rtfn n) -> "@" ^ n + (* A function value someone wrote, which is the one [FnAddr] that is not the + symbol. In a dev build it is the cell's contents, so that a value taken + after a redefinition is the new body — the same load a direct call to the + same name would do, at the point the *address* is taken rather than at the + call. What that does not give is a value taken before a redefinition and + called after it: that one is still the old body, because there is nothing + left to re-resolve once the address is in a slot. Named in BUILT.md rather + than papered over with a trampoline. *) + | Tast.FnAddr (Tast.Fnval n) -> body_of f n | Tast.Addr p -> fst (place f p) | Tast.Prim (p, args) -> prim f e p args | Tast.Call (name, args) -> (match Hashtbl.find_opt f.md.externs name with | Some sym -> extern_call f e.Tast.ty ("@" ^ sym) args | None -> call f e.Tast.ty name args) + | Tast.CallPtr (callee, args) -> call_ptr f e.Tast.ty callee args | Tast.Do body -> block f body | Tast.Let (bs, body) -> List.iter @@ -1110,27 +1137,51 @@ and block f body = List.iter (fun e -> last := value f e) body; !last +(* The current body of a named Flan function, as something callable. A release + build is the symbol; a dev build is whatever the indirection cell holds, and + there are two spellings of that because a function this module emitted has + its cell as a symbol and one it does not has only a cached address. *) +and body_of f flan = + if not f.md.dev then fname flan + else if f.md.known flan then begin + let p = fresh f in + ins f "%s = load ptr, ptr %s" p (cellname flan); + p + end else begin + (* The cell itself is not a symbol here; its address was looked up by + name at install time and cached. *) + let c = fresh f in + ins f "%s = load ptr, ptr %s" c (cellptr flan); + let p = fresh f in + ins f "%s = load ptr, ptr %s" p c; + p + end + and call f ret flan args = let vs = map_lr (fun (a : Tast.expr) -> let v = value f a in Printf.sprintf "%s %s" (ll a.Tast.ty) v) args in (* The cell is loaded *after* the arguments, so a redefinition that lands between two calls still cannot land in the middle of one. *) - let callee = - if not f.md.dev then fname flan - else if f.md.known flan then begin - let p = fresh f in - ins f "%s = load ptr, ptr %s" p (cellname flan); - p - end else begin - (* The cell itself is not a symbol here; its address was looked up by - name at install time and cached. *) - let c = fresh f in - ins f "%s = load ptr, ptr %s" c (cellptr flan); - let p = fresh f in - ins f "%s = load ptr, ptr %s" p c; - p - end - in + let callee = body_of f flan in + call_through f ret callee vs + +(* A call through a function value. Identical to the direct case once the + callee is in hand — a Flan function's signature is its parameters followed + by the transfer channel whether it was reached by name or by pointer — so + the guard after it is the same guard, and a [return] out of a callee taken + as a value transfers exactly as one out of a callee named does. + + The callee is evaluated *before* the arguments, which is the order it is + written in and the order a reader expects; the direct case is the other way + round for a reason that does not apply here (there is no cell to keep out of + the middle of an argument list). *) +and call_ptr f ret callee args = + let c = value f callee in + let vs = map_lr (fun (a : Tast.expr) -> + let v = value f a in Printf.sprintf "%s %s" (ll a.Tast.ty) v) args in + call_through f ret c vs + +and call_through f ret callee vs = let t = fresh f in ins f "%s = call %s %s(%s)" t (ll ret) callee (String.concat ", " (vs @ [ "ptr " ^ xfer_param ])); diff --git a/lib/prelude.ml b/lib/prelude.ml index 022f5eb..03b6c48 100644 --- a/lib/prelude.ml +++ b/lib/prelude.ml @@ -103,14 +103,13 @@ let source = {flan| ;; allocating tier is further down, and a caller sorts a Vec by sorting ;; (as-slice v). ;; -;; **map, filter, reduce and a sort taking a comparator are not here, and they -;; are not blocked on generics.** They are blocked on *function values*: each -;; of them takes a callable as an argument, Types.Fn exists but check.ml -;; refuses it with "a function type is not implemented yet — milestone 5", and -;; there is nothing else in the language to pass. Generics on top of that is -;; what would make them one copy instead of one per element type; without -;; either, the honest form is the concrete fold, which is what sum-i32 and -;; sum-f32 below already are — (reduce + 0) with the + written in. +;; **map, filter, reduce and a sort taking a comparator are here now**, in a +;; section of their own after the f32 family. They were blocked on *function +;; values* and not on generics, which is why they arrived without generics: +;; a (Fn [T ...] R) is an ordinary parameter type. What they are still one +;; copy per element type for *is* generics — sum-i32 and sum-f32 are the same +;; shape and the same argument — so the set is the same i32 and f32 the rest of +;; this family covers. (defn swap-i32! [s [i32] i i32 j i32] (let [t (at s i)] @@ -244,6 +243,99 @@ let source = {flan| (set t (+ t (f64 (at s i))))) t)) +;; ── The ones that take a function ───────────────────────────────────── +;; +;; map, filter, reduce and a comparator sort, which were the four the previous +;; tier could not write. The blocker was function values and not generics, and +;; the difference shows in what arrived and what did not: these take a +;; (Fn [T ...] R) as an ordinary parameter and needed nothing else, and they +;; are still one copy per element type because *that* is the generics half. +;; +;; Two rules, both inherited rather than invented here: +;; +;; 1. **The in-place ones stay in place.** map! writes back into the slice it +;; was handed, for the same reason sort-i32! does — a slice is non-owning, +;; and transforming a thing you already own should not allocate. A map that +;; produces a *different* element type is not here: it would be one copy per +;; ordered pair of types, which is the point at which a per-type family +;; stops being honest. +;; 2. **filter allocates and the caller frees**, like everything in the +;; building tier: (free v), or let a (free-all a) take the region. +;; +;; The function is passed by name — this is a Lisp-1, so a bare defn name is +;; the function — or written inline as an (fn [x] ...), whose parameter types +;; come from the parameter it is being passed to. It may not capture: an fn is +;; lifted into a function of its own and sees its parameters and the globals +;; and nothing else. + +(defn map-i32! [s [i32] f (Fn [i32] i32)] + (dotimes [i (len s)] + (set (at s i) (f (at s i))))) + +(defn map-f32! [s [f32] f (Fn [f32] f32)] + (dotimes [i (len s)] + (set (at s i) (f (at s i))))) + +;; The general fold, of which sum-i32 is the special case with the + written +;; in. The accumulator comes first in the step, which is the order that reads +;; as (f acc x) and the order Odin's slice.reduce uses. +(defn reduce-i32 [s [i32] init i32 f (Fn [i32 i32] i32)] i32 + (let [acc init] + (dotimes [i (len s)] + (set acc (f acc (at s i)))) + acc)) + +(defn reduce-f32 [s [f32] init f32 f (Fn [f32 f32] f32)] f32 + (let [acc init] + (dotimes [i (len s)] + (set acc (f acc (at s i)))) + acc)) + +;; A new Vec holding the elements the predicate kept, in the order they were +;; in. Owned by the caller. +(defn filter-i32 [s [i32] keep? (Fn [i32] bool)] (Vec i32) + (let [v (vec-new i32)] + (dotimes [i (len s)] + (when (keep? (at s i)) + (push v (at s i)))) + v)) + +(defn filter-f32 [s [f32] keep? (Fn [f32] bool)] (Vec f32) + (let [v (vec-new f32)] + (dotimes [i (len s)] + (when (keep? (at s i)) + (push v (at s i)))) + v)) + +;; The same insertion sort sort-i32! is, with the one comparison it had written +;; in replaced by the one it is told. before? answers "does a come before b", +;; so passing (fn [a b] (< a b)) is ascending and reversing it is descending — +;; and a caller wanting a key rather than an order writes the comparison. +;; +;; It is stable exactly as sort-i32! is: the loop stops the moment before? says +;; no, so equal elements never swap past each other. A before? that is not a +;; strict weak ordering — one answering true for both (a b) and (b a) — is the +;; caller's mistake and shows up as an order, not as a loop: the inner while is +;; bounded by j reaching 0 whatever the comparison says. +(defn sort-i32-by! [s [i32] before? (Fn [i32 i32] bool)] + (let [i 1] + (while (< i (len s)) + (let [j i] + ;; `and` short-circuits, so (at s -1) is never evaluated at j = 0. + (while (and (> j 0) (before? (at s j) (at s (- j 1)))) + (swap-i32! s (- j 1) j) + (set j (- j 1)))) + (set i (+ i 1))))) + +(defn sort-f32-by! [s [f32] before? (Fn [f32 f32] bool)] + (let [i 1] + (while (< i (len s)) + (let [j i] + (while (and (> j 0) (before? (at s j) (at s (- j 1)))) + (swap-f32! s (- j 1) j) + (set j (- j 1)))) + (set i (+ i 1))))) + ;; ── Bytes ───────────────────────────────────────────────────────────── ;; ;; Over [u8] and not over string, so (bytes s) is what a caller writes and one @@ -1200,9 +1292,14 @@ let source = {flan| ;; allocation one. format-f64 above is the piece of it ;; that was actually wanted, and `print`/`println` are ;; already the structural walk over any one value. -;; map, filter, reduce Function values. See the head of the slice-algorithm -;; sort-by section: check.ml refuses a function type outright, -;; and there is nothing in the language to pass. +;; map that changes the Generics, and only that. map!, filter, reduce and +;; element type sort-by! landed the day function values did — see +;; "The ones that take a function" above — at i32 and +;; f32, the two element types the rest of that family +;; covers. A map from [i32] to [f32] is the one shape +;; that did not come with them, because it is one copy +;; per *ordered pair* of types rather than per type, +;; which is where a per-type family stops being honest. ;; map-keys, map-values Generics — and the reason changed, which is the ;; point of naming them separately. It used to be the ;; missing Map iterator; `map-next!` is that iterator diff --git a/lib/reach.ml b/lib/reach.ml index 1a59826..331f4cf 100644 --- a/lib/reach.ml +++ b/lib/reach.ml @@ -45,8 +45,15 @@ let rec expr_refs f (e : Tast.expr) = a map loses the two functions its every lookup calls through. *) | Tast.FnAddr (Tast.Flanfn n) -> f n | Tast.FnAddr (Tast.Rtfn _) -> () + (* A function value, and the *only* thing that keeps it linked. A name used + as a value is never a [Call], so without this edge the one function a + program passes to [map] is the one function the link drops. *) + | Tast.FnAddr (Tast.Fnval n) -> f n | Tast.Prim (_, es) -> gos es | Tast.Call (n, es) -> f n; gos es + (* No name to root: whatever this calls was reached as a value, and the + [FnAddr] that produced it is somewhere in the callee expression. *) + | Tast.CallPtr (callee, es) -> go callee; gos es | Tast.Do es -> gos es | Tast.Let (bs, body) -> List.iter (fun (_, v) -> go v) bs; gos body | Tast.If (c, t, e') -> go c; go t; go e' diff --git a/lib/render.ml b/lib/render.ml index 43344bf..09a4486 100644 --- a/lib/render.ml +++ b/lib/render.ml @@ -122,6 +122,12 @@ let rec render c depth (e : Tast.expr) : Tast.expr list = does not own, and the walk is what [as-slice] is for: (print (as-slice v)) prints the elements and says at the call site that it borrowed. *) | Types.Vec _ -> [ lit "" ] + (* A function value is a code address, and printing the address would make + an inspection depend on where the image loaded. The signature is what a + reader can act on, so that is what is shown — and the inspector reaches + every local of a stopped frame, so a frame holding one has to render + rather than refuse. *) + | Types.Fn _ as ft -> [ lit ("<" ^ Types.to_string ft ^ ">") ] | Types.Option t -> let tag = { Tast.e = Tast.Field (e, 0); ty = Types.Int Types.I8; loc } in let some = { Tast.e = Tast.Field (e, 1); ty = t; loc } in diff --git a/lib/tast.ml b/lib/tast.ml index a350965..40181fe 100644 --- a/lib/tast.ml +++ b/lib/tast.ml @@ -72,17 +72,28 @@ and expr_kind = | Local of int (* slot index into the frame *) | Global of string | Prim of prim * expr list - | Call of string * expr list (* direct call; no first-class fns yet *) - (* The address of a function the compiler emitted, by symbol. Not a function - *value*: nothing in the surface language can produce one, name its type or - call through it, and its only consumers are runtime entry points that take - a procedure the way spec-memory.md's type-erased allocator does. The Map's - hash and equality pair is what wanted it — Odin's [Map_Info] is two - contextless [proc] fields reached exactly this way — and a handler-bind - clause is the same arrangement with the symbol carried on [hframe] - instead. Its Flan type is [Alloc]: an opaque pointer-width value with no - user-writable constructor, which is all any backend needs to know. *) + | Call of string * expr list (* a call naming its callee *) + (* The address of a function the compiler emitted, by symbol. Which symbol + table, and whether the surface language can see it, is [fnref]'s job. + + Two unrelated consumers, and the difference between them is the whole + reason [fnref] has three cases rather than two. The compiler's own uses — + the Map's hash and equality pair (Odin's [Map_Info] is two contextless + [proc] fields reached exactly this way) and a handler-bind clause's + symbol — want the *symbol*, always, and carry the Flan type [Alloc]. A + function *value* someone wrote wants the body that is current, which in a + dev build is not the symbol but whatever the indirection cell holds, and + carries the Flan type [Fn]. *) | FnAddr of fnref + (* A call through a function value: the callee is an expression of type + [Fn], not a name. Its own node rather than a [Call] with an expression in + the name slot, because everything that walks this IR treats [Call]'s + string as a *link-time* edge — [Reach] roots the callee, [Dev] finds the + cell to redefine, [Emit] may load that cell — and none of those are + questions an indirect call can answer. Keeping them apart means each of + those readers keeps working on the direct case unchanged and says + explicitly what it does with the indirect one. *) + | CallPtr of expr * expr list | Do of expr list | Let of (int * expr) list * expr list | If of expr * expr * expr @@ -173,7 +184,17 @@ and expr_kind = interchangeable at the call site because a Flan function's emitted signature is its parameters followed by the transfer channel, and the runtime's matching typedef spells that last pointer out. *) -and fnref = Flanfn of string | Rtfn of string +(* [Flanfn] is a function this compiler emitted, named by its mangled symbol, + and always the symbol itself. [Rtfn] is a C entry point in flan_rt.c, spelled + as written. [Fnval] is also a Flan function this compiler emitted, but as a + *value* someone asked for by writing its name — and it is a separate case + because a dev build must answer it with the current body rather than with the + original symbol, which means a load from the indirection cell. The first two + must never take that path: a lifted handler clause and a hash pair have no + cell to load from. The three are interchangeable at a call site, because a + Flan function's emitted signature is its parameters followed by the transfer + channel and the runtime's matching typedef spells that last pointer out. *) +and fnref = Flanfn of string | Rtfn of string | Fnval of string and sigkind = Ssignal | Serror diff --git a/test/programs/fn-capture.flan b/test/programs/fn-capture.flan new file mode 100644 index 0000000..7b9975f --- /dev/null +++ b/test/programs/fn-capture.flan @@ -0,0 +1,11 @@ +;; Capture does not exist. An fn is lifted into a function of its own and is +;; handed nothing but its parameters, so a reference to a local of the +;; enclosing function is refused by name rather than resolved to something it +;; did not mean. spec-memory.md's capture cases, and escaping closures with +;; them, are deferred; this is the refusal that says so where it happens. +(defn use [f (Fn [] i32)] i32 (f)) + +(defn main [] i32 + (let [n 7] + (println (use (fn [] n)))) + 0) diff --git a/test/programs/fn-extern.flan b/test/programs/fn-extern.flan new file mode 100644 index 0000000..1cf2882 --- /dev/null +++ b/test/programs/fn-extern.flan @@ -0,0 +1,13 @@ +;; A foreign function's address is not a Flan function value. A Flan +;; function's emitted signature ends with the transfer channel and a C one +;; does not, so nothing could call the resulting pointer correctly — and an +;; aggregate crossing the boundary is flattened by a generated shim, which the +;; raw symbol knows nothing about. Refused for what it is, with the wrapper +;; named as the way to get one. +(declare c-abs [n i32] i32 "abs") + +(defn use [f (Fn [i32] i32)] i32 (f 3)) + +(defn main [] i32 + (println (use c-abs)) + 0) diff --git a/test/programs/fn-in-struct.flan b/test/programs/fn-in-struct.flan new file mode 100644 index 0000000..510b9b6 --- /dev/null +++ b/test/programs/fn-in-struct.flan @@ -0,0 +1,9 @@ +;; ZII fills an omitted field with all-bytes-zero, and a zeroed function value +;; is a null pointer — the one kind of zero that is not a value the type can +;; have. Every other type's zero is one: 0, false, an empty slice, None, a +;; union's first case. So it is refused where the field is written rather than +;; left to crash at the call, and the same rule covers a global, a fixed +;; array's element and (zeroed). +(defstruct Ops [run (Fn [i32] i32)]) + +(defn main [] i32 0) diff --git a/test/programs/fn-no-type.flan b/test/programs/fn-no-type.flan new file mode 100644 index 0000000..2189b49 --- /dev/null +++ b/test/programs/fn-no-type.flan @@ -0,0 +1,8 @@ +;; An fn carries parameter names and no types — that is the surface syntax — +;; so it takes them from the position it is written in. An argument position +;; says what is wanted, because the callee's signature is threaded into every +;; argument; a let binding does not, and is refused saying so. +(defn main [] i32 + (let [f (fn [x] (* x 2))] + (println (f 3))) + 0) diff --git a/test/programs/fn-values.flan b/test/programs/fn-values.flan new file mode 100644 index 0000000..212e595 --- /dev/null +++ b/test/programs/fn-values.flan @@ -0,0 +1,88 @@ +;; Function values, the non-escaping kind: a code address and no environment +;; beside it. Capture does not exist, so nothing here can outlive anything. +;; +;; This is a Lisp-1 — one top-level namespace, enforced — so a bare function +;; name *is* the function and there is no #' to write. + +(defn double [x i32] i32 (* x 2)) +(defn negate [x i32] i32 (- 0 x)) +(defn square [x i32] i32 (* x x)) + +;; The shape map/filter/reduce want: the function arrives as a parameter, is +;; called, and is never stored. +(defn each! [xs [i32] f (Fn [i32] i32)] Unit + (dotimes [i (len xs)] + (set (at xs i) (f (at xs i))))) + +(defn fold [xs [i32] f (Fn [i32] i32)] i32 + (let [t 0] + (dotimes [i (len xs)] + (set t (+ t (f (at xs i))))) + t)) + +;; A comparator, which is the other half of what was blocked: a sort that is +;; told the order rather than having it written in. Insertion sort, because the +;; point here is the parameter and not the algorithm. +(defn sort-by! [xs [i32] before? (Fn [i32 i32] bool)] Unit + (dotimes [i (len xs)] + (let [j i] + (while (and (> j 0) (before? (at xs j) (at xs (- j 1)))) + (swap-i32! xs j (- j 1)) + (set j (- j 1)))))) + +(defn ascending [a i32 b i32] bool (< a b)) +(defn descending [a i32 b i32] bool (> a b)) + +;; Returning one. A function value is a link-time constant with no environment, +;; so handing it back up is no different from handing it down. +(defn pick [up bool] (Fn [i32 i32] bool) + (if up ascending descending)) + +;; A function value calling another, and the transfer channel crossing an +;; indirect call: a callee reached by pointer signals exactly as one reached by +;; name, and the handler is established across the call. +(defstruct TooBig [n i32]) + +(defn checked [x i32] i32 + (when (> x 100) (signal (TooBig {.n x}))) + x) + +(defvar seen i32) + +;; A handler-bind and an fn literal in *one* function, which is the case that +;; would catch the two lifted-function name sequences sharing a counter: both +;; are lifted out of [handles] and both are numbered within it. +(defn handles [] Unit + (handler-bind [(TooBig [c] (set seen (+ seen (.n c))))] + (let [xs [5 200 7 300]] + (println (fold (slice xs 0 4) checked)) + (println (fold (slice xs 0 4) (fn [x] (min x 10)))))) + (print "seen ") (print seen) (println "")) + +(defn main [] i32 + (let [xs [1 2 3 4]] + ;; A name in value position, passed down. + (each! (slice xs 0 4) double) + (print (at xs 0)) (print " ") (print (at xs 3)) (println "") + ;; 2 + 4 + 6 + 8 negated + (println (fold (slice xs 0 4) negate)) + ;; An fn literal, whose parameter types come from the position it is in. + (println (fold (slice xs 0 4) (fn [x] (+ x 1)))) + ;; A let binding of function type, called by the name it is bound to. + (let [f square] + (println (f 9)))) + + ;; A comparator, and the same slice sorted both ways. + (let [ys [3 1 4 1 5 9 2 6] + s (slice ys 0 8)] + (sort-by! s ascending) + (print (at s 0)) (print " ") (print (at s 7)) (println "") + (sort-by! s descending) + (print (at s 0)) (print " ") (print (at s 7)) (println "") + ;; A returned function value, and a computed head calling it. + (sort-by! s (pick true)) + (print (at s 0)) (println "") + (println ((pick false) 1 2))) + + (handles) + 0) diff --git a/test/programs/higher-order.flan b/test/programs/higher-order.flan new file mode 100644 index 0000000..08db084 --- /dev/null +++ b/test/programs/higher-order.flan @@ -0,0 +1,50 @@ +;; The prelude's function-taking family: map!, filter, reduce and a comparator +;; sort. These were the four the second tier could not write, and they arrived +;; the day function values did — so what this checks is that they are ordinary +;; prelude functions, called the ordinary way, with the function passed by +;; name or written inline. + +(defn triple [x i32] i32 (* x 3)) +(defn odd? [x i32] bool (= (% x 2) 1)) +(defn adds [a i32 b i32] i32 (+ a b)) +(defn longer-first [a i32 b i32] bool (> a b)) +(defn halve [x f32] f32 (/ x 2.0)) +(defn big? [x f32] bool (> x 1.0)) + +(defn main [] i32 + ;; map! writes back into the slice it was handed. + (let [xs [1 2 3 4] + s (slice xs 0 4)] + (map-i32! s triple) + (print (at s 0)) (print " ") (print (at s 3)) (println "") + + ;; reduce, with the accumulator first in the step. The prelude's own + ;; sum-i32 is this with the + written in. + (print (reduce-i32 s 0 adds)) (println "") + ;; ... and an fn literal, whose parameter types come from the parameter. + (print (reduce-i32 s 1 (fn [a b] (* a b)))) (println "") + + ;; filter allocates and the caller frees. + (let [v (filter-i32 s odd?)] + (print (len v)) (print " ") (print (at v 0)) (println "") + (free v)) + + ;; A comparator sort, both directions off the same slice. + (sort-i32-by! s longer-first) + (print (at s 0)) (print " ") (print (at s 3)) (println "") + (sort-i32-by! s (fn [a b] (< a b))) + (print (at s 0)) (print " ") (print (at s 3)) (println "")) + + ;; The f32 half of the family, which is the same code at the other element + ;; type — the copy that generics would remove. + (let [ys [(f32 4.0) (f32 1.0) (f32 8.0) (f32 2.0)] + t (slice ys 0 4)] + (map-f32! t halve) + (print (at t 0)) (print " ") (print (at t 2)) (println "") + (print (reduce-f32 t 0.0 (fn [a b] (+ a b)))) (println "") + (let [w (filter-f32 t big?)] + (print (len w)) (println "") + (free w)) + (sort-f32-by! t (fn [a b] (> a b))) + (print (at t 0)) (print " ") (print (at t 3)) (println "")) + 0) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 5691f24..95a4fbe 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -1254,12 +1254,15 @@ let () = and this row is what says so. *) refuses "nth is not a name" "programs/nth-gone.flan" "unknown function nth"; - (* The one thing in the allocator tier that really does need milestone 5, - refused by name and with the reason rather than as an unknown function. - NEXT.md's escape is that the *built-in* set needs nothing from milestone - 5; this row is the other half of that claim. *) + (* Still the one thing in the allocator tier that does not work, and the + reason changed when function values landed: it *has* a defn's name in + value position now. What it does not have is a way to be called — the + runtime calls proc(a, mode, p, old, size, align), six C arguments with + no transfer channel, and every Flan function value's signature ends with + one — or anywhere to put the flan_allocator, Allocator being opaque and + pointer-width. Two reasons, both named, neither a function value. *) refuses "a user-written allocator" "programs/user-allocator.flan" - "a defn's name in value position"; + "is no longer what is missing"; (* Move-only, spec-memory.md. Each of these would otherwise be a double free or a use-after-free at run time, and each is refused at the second use with the first one's location in the message. *) @@ -1847,6 +1850,55 @@ ERR@7 unexpected token: not the kind the caller was reading outputs ~opt:"-O0" "macros, -O0" "programs/macros.flan" macros_out; outputs ~dev:true "macros, dev" "programs/macros.flan" macros_out; + (* Function values, the non-escaping kind. Three opt levels because the + indirect call is the one shape LLVM is most likely to devirtualise: at + -O2 a name passed straight down becomes a direct call and the pointer + vanishes, so -O0 is what proves there is a real load and a real + [call ptr] behind it, and a dev build is what proves the value is read + out of the indirection cell rather than frozen as a symbol. + + The two lines worth naming. A *returned* function value, called through + a computed head, is the case that would fail if the value were anything + other than a link-time constant. And the handler-bind around a fold + whose element function signals is the case that would fail if an + indirect call skipped the transfer guard — a callee reached by pointer + has to answer a signal exactly as one reached by name. *) + let fn_values_out = + "2 8\n-20\n24\n81\n1 9\n9 1\n1\nfalse\n512\n32\nseen 500\n" + in + outputs "function values" "programs/fn-values.flan" fn_values_out; + outputs ~opt:"-O0" "function values, -O0" "programs/fn-values.flan" + fn_values_out; + outputs ~dev:true "function values, dev" "programs/fn-values.flan" + fn_values_out; + + (* The prelude's four, which is the point of the whole lane: map!, filter, + reduce and a comparator sort were blocked on function values and not on + generics, so they arrived without generics — and are still one copy per + element type, which is the generics half. The f32 rows are that copy. + -O0 as well, because filter allocates and the -O2 run can fold a + predicate over four literals into nothing. *) + let higher_order_out = + "3 12\n30\n1944\n2 3\n12 3\n3 12\n2 4\n7.5\n2\n4 0.5\n" + in + outputs "the prelude's map, filter, reduce and sort-by" + "programs/higher-order.flan" higher_order_out; + outputs ~opt:"-O0" "the prelude's map, filter, reduce and sort-by, -O0" + "programs/higher-order.flan" higher_order_out; + + (* What function values do *not* include, each refused by name. Capture is + the headline: an fn is lifted into a function of its own and handed + nothing but its parameters, so spec-memory.md's capture cases and + escaping closures with them stay deferred. *) + refuses "an fn cannot capture" "programs/fn-capture.flan" + "cannot see n"; + refuses "an fn with no type to take" "programs/fn-no-type.flan" + "nothing here says what this fn"; + refuses "a function value would be zeroed" "programs/fn-in-struct.flan" + "it would be zeroed"; + refuses "a foreign function's address" "programs/fn-extern.flan" + "is not a Flan function value"; + (* The exit criterion plan.org set for milestone 5: a special form moved out of the compiler and into the prelude, with the corpus that was written against the special form unchanged. *) diff --git a/test/test_flan.ml b/test/test_flan.ml index 9307230..761458e 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -948,12 +948,19 @@ let () = "(defstruct V [x f32]) (declare f [v V] \"c_f\")" ~needle:"cannot cross to C"; rejects_check "an extern may not return a struct" "(defstruct V [x f32]) (declare f [] V \"c_f\")" ~needle:"cannot cross to C"; - rejects_check "fn values are milestone 5" "(defn f [] (fn [x] x))" - ~needle:"milestone 5"; + (* Function values landed; what stayed refused is what they do not include. + An fn takes its parameter types from the position it is written in, and a + defn's body that just answers one says nothing about them. *) + rejects_check "an fn with nothing to say what it takes" + "(defn f [] (fn [x] x))" ~needle:"nothing here says what this fn"; rejects_check "type variables are milestone 5" "(defn f [x a])" ~needle:"milestone 5"; - rejects_check "a function name as a value is milestone 5" - "(defn g []) (defn f [] i32 g)" ~needle:"milestone 5"; + (* The other half: a name in value position now *works*, and the arity is + checked against the function it names. *) + rejects_check "a function value at the wrong arity" + "(defn g [x i32] i32 x) (defn u [f (Fn [i32] i32)] i32 (f 1 2)) \ + (defn f [] i32 (u g))" + ~needle:"takes 1 argument, given 2"; rejects_check "a struct cannot contain itself by value" "(defstruct Node [next Node])" ~needle:"contains itself by value"; @@ -1476,6 +1483,37 @@ let () = | _ -> false | exception Cjson.Bad _ -> true); + (* ── Lifted function names, and why they are counted per kind ──────── + A handler clause and an fn literal are both lifted into functions of their + own, and both are numbered within the function they came out of. One + shared counter would mean that adding a handler-bind above an existing fn + renamed the fn — a rename for a body that did not change, in exactly the + names a dev redefinition module emits and matches on. These check that + each sequence is stable against the other. *) + let lifted_names src = + List.filter_map + (fun (f : Tast.fn) -> + match f.Tast.fparent with Some _ -> Some f.Tast.name | None -> None) + (Check.program (Parse.program (read src))).Tast.fns + in + let with_handler = + "(defstruct Boom [n i32]) (defvar hit i32) \ + (defn u [f (Fn [i32] i32)] i32 (f 1)) \ + (defn m [] i32 \ + (handler-bind [(Boom [c] (set hit (.n c)))] (u (fn [x] x))) 0)" + in + let without_handler = + "(defstruct Boom [n i32]) (defvar hit i32) \ + (defn u [f (Fn [i32] i32)] i32 (f 1)) \ + (defn m [] i32 (u (fn [x] x)) 0)" + in + check "an fn keeps its number when a handler-bind is added beside it" + (List.mem "fn/m/0" (lifted_names with_handler) + && List.mem "fn/m/0" (lifted_names without_handler)); + check "and the handler clause has a sequence of its own" + (List.exists + (fun n -> contains n "handler/m/0/Boom") (lifted_names with_handler)); + (* ── The prelude's own macro calls, and the bootstrap that allows them ── A macro module is compiled *from* the prelude, so a prelude function that calls a prelude macro cannot be in the module that would expand it. The