An fn sees the locals it was written among, and Fn says so in its type
spec-memory.md's case 2, capture by value into a stack environment, and
the calling convention the author's rulings asked for.
(Fn [i32] i32) captures; {code, env}; the common case
(CFn [i32] i32) the bare address; one word; cannot capture
A local of the enclosing function that an fn names is copied into a
struct the checker synthesises, held in a slot of that function's frame,
and the value carries its address; the lifted body reads the copies back
into named slots of its own, once, at entry. So the name in the body
means what the local held at the instant the value was made --
fn-capture.flan changes the local through a pointer after the value
exists and the fn still answers with the old one.
Two types rather than a uniform environment parameter: "while it's dyn
first, static side should never have to pay the price for the existence
of the dyn side... if you fully opt out, for instance, using --no-gc
flag, then we should be operating under Odin/C semantics and never paying
any runtime costs." The environment is declared by exactly the bodies an
(Fn ...) value can reach -- a lifted literal in an Fn position, every
handler clause, and the widening thunks -- and by nothing else. An
ordinary defn emits the signature it always did; calc-me and fourteen
corpus programs were diffed to say so.
CFn, because the C carries information: a value with no environment is
the only kind that could ever cross to C, and under the --no-conditions
direction FIX.org records it becomes literally a C function pointer. It
is not that today -- a declare cannot take a function type at all -- and
crossable's refusal says so where a reader would otherwise be misled.
Nobody needs CFn: Fn accepts everything, and the commonest reason to
reach for the narrow one is that a *named* function handed to an Fn pays
a hop through the widening thunk where a CFn is a direct call.
That thunk is one small function per distinct signature widened, which
reads the bare address back out of the environment and calls it. The
cheaper trick -- the environment last, ignored by a body that never
declared it -- is legal under SysV and is a trap under wasm32's
call_indirect, which compares the signature at the call. Every indirect
call is exactly typed now.
A handler clause captures the same way and is sound with nothing left
over: its frame is popped by the body that pushed it. What is refused
there is a *store* into a captured name -- it is a copy, and writing to
it would leave the local as it was.
And the other half, which is what "non-escaping" means: a value carrying
an environment may be called, passed down and let-bound, and may not be
returned, stored, pointed at or pushed into a container. A parameter of
type Fn is treated as one, which answers "passed to something that stores
it" with no interprocedural analysis -- the store is refused inside the
callee. Everything of type CFn is clean for free, which is the second
thing having two types buys. Every refusal names case 3, the environment
the collector owns.
Two pre-existing bugs fell out on the way. A lifted fn asked for Fnval,
so `flan reload' on any function containing an fn literal died at llc
with an undefined cell; it takes Flanfn now, which is the choice a
handler clause always made. And a redefinition module now carries its
own hidden copy of every thunk it names, which is the same bug shape
caught before it shipped.
This commit is contained in:
parent
a885c1b0f1
commit
2572f0a537
@ -18,7 +18,12 @@ and texpr_kind =
|
||||
| Tarray of len * texpr (* [4 f32] [rows [cols u32]] *)
|
||||
| Tmap of texpr * texpr (* (Map string i32) *)
|
||||
| Tapp of string * texpr list (* (Ptr Cursor) (Option f64) *)
|
||||
| Tfn of texpr list * texpr (* (Fn [a a] bool) *)
|
||||
(* (Fn [a a] bool) and (CFn [a a] bool). The flag is whether the value
|
||||
carries an environment: true for [Fn], false for [CFn]. One case
|
||||
rather than two because everything that walks a type expression treats
|
||||
them identically — the difference is a fact about the value, and it is
|
||||
[Check.resolve] that turns it into one. *)
|
||||
| Tfn of bool * texpr list * texpr
|
||||
|
||||
(* An array length is an integer or a compile-time constant's name. *)
|
||||
and len =
|
||||
|
||||
801
lib/check.ml
801
lib/check.ml
File diff suppressed because it is too large
Load Diff
@ -413,8 +413,8 @@ let rec ty_source (t : Ast.texpr) =
|
||||
| Ast.Tarray (Ast.Lname n, e) -> Printf.sprintf "[%s %s]" n (ty_source e)
|
||||
| Ast.Tmap (k, v) ->
|
||||
Printf.sprintf "(Map %s %s)" (ty_source k) (ty_source v)
|
||||
| Ast.Tfn (ps, r) ->
|
||||
Printf.sprintf "(Fn [%s] %s)"
|
||||
| Ast.Tfn (env, ps, r) ->
|
||||
Printf.sprintf "(%s [%s] %s)" (if env then "Fn" else "CFn")
|
||||
(String.concat " " (List.map ty_source ps)) (ty_source r)
|
||||
|
||||
let tname n = ty (Ast.Tname n)
|
||||
|
||||
@ -2484,7 +2484,7 @@ let render_addr (s : Session.t) ~addr ~(ty : Types.t)
|
||||
let thunk : Tast.fn =
|
||||
{ Tast.name; params = []; ret = Types.Unit;
|
||||
body = (nullary "flan/dev-begin" :: parts) @ [ nullary "flan/dev-end" ];
|
||||
fdefers = []; fparent = None; floc = loc;
|
||||
fdefers = []; fenv = None; fparent = None; floc = loc;
|
||||
slots = Array.of_list (List.rev !extra);
|
||||
(* Every slot in here is the walk's own scratch: what is being shown
|
||||
is storage this thunk reaches by address. *)
|
||||
|
||||
229
lib/emit.ml
229
lib/emit.ml
@ -85,6 +85,38 @@ let cellname n = "@" ^ quoted (Mangle.cell n)
|
||||
written only by the guards this file emits. *)
|
||||
let xfer_param = "%xfer"
|
||||
|
||||
(* The environment parameter, the other name that is not a Flan name: the
|
||||
address of the captured copies a function value was made with.
|
||||
|
||||
**It is declared by exactly the bodies that can be reached through a
|
||||
[(Fn ...)] value**, and it is the *last* parameter, after the transfer
|
||||
channel. That set is: a lifted [fn] literal written into an [Fn] position,
|
||||
capturing or not; every handler clause, because [flan_signal] passes one
|
||||
to whichever clause matched and cannot know which of them captured; and
|
||||
the widening thunks ([Tast.Thicken]), which exist to read it.
|
||||
|
||||
Nothing else declares it. An ordinary [defn] therefore emits exactly the
|
||||
signature it always did — its parameters and then the channel, and not a
|
||||
byte more — and a call to it by name is unchanged. That is the whole of
|
||||
what keeps capture free for everyone who does not use it, and it is the
|
||||
author's ruling: the static side does not pay for the dynamic side.
|
||||
|
||||
So **every indirect call is exactly typed**. The two conventions meet in
|
||||
one place, the thunk, and nowhere does a caller pass an argument the callee
|
||||
did not declare. An earlier design did rely on that — the environment last,
|
||||
ignored by a body that never asked for it, which SysV allows and Swift's
|
||||
thin-vs-thick convention is built on — and wasm32 killed it: [call_indirect]
|
||||
compares the signature at the call site, so a spare argument is a trap and
|
||||
not a register nobody reads. Being exactly typed is checkable by a verifier
|
||||
rather than argued from a calling convention, which is the better property
|
||||
to have had all along. *)
|
||||
let env_param = "%env"
|
||||
|
||||
(* What a call through a [(Fn ...)] value passes when it has no environment —
|
||||
a value made out of a name, or one widened from a [CFn]. Spelled once so
|
||||
the sites cannot drift. *)
|
||||
let no_env = "ptr null"
|
||||
|
||||
(* The condition's own name, for the message an unhandled [error] prints. The
|
||||
checker has already refused anything that is not a struct. *)
|
||||
let struct_name_of (t : Types.t) =
|
||||
@ -130,10 +162,13 @@ module Rt = struct
|
||||
let ll_of = function Ptr -> "ptr" | I32 -> "i32" | I64 -> "i64"
|
||||
let size_of = function Ptr | I64 -> 8 | I32 -> 4
|
||||
|
||||
(* A handler frame: the one it displaced, the condition type it matches, and
|
||||
the lifted function that runs. *)
|
||||
(* A handler frame: the one it displaced, the condition type it matches, the
|
||||
lifted function that runs, and the environment that function is handed —
|
||||
the establishing function's captured copies, or null when the clause
|
||||
captured nothing. *)
|
||||
let handler =
|
||||
{ sname = "handler"; fields = [ "prev", Ptr; "type", I32; "fn", Ptr ] }
|
||||
{ sname = "handler";
|
||||
fields = [ "prev", Ptr; "type", I32; "fn", Ptr; "env", Ptr ] }
|
||||
|
||||
(* A restart frame. The first four fields are what the runtime's own
|
||||
[flan_restart] declares and their offsets do not move; the rest are §3's
|
||||
@ -246,11 +281,13 @@ 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"
|
||||
(* A code address and the environment it is called with: two words, always,
|
||||
whether or not this particular value captured anything. See [%fnv]. *)
|
||||
| Types.Fn _ -> "%fnv"
|
||||
(* The bare address, and nothing beside it: one pointer, the width of any
|
||||
other. A [CFn] cannot capture, so there is nothing an environment
|
||||
would hold. *)
|
||||
| Types.CFn _ -> "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
|
||||
@ -454,7 +491,8 @@ 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.Fn _ -> 16, 8
|
||||
| Types.CFn _ -> 8, 8
|
||||
| Types.Vec _ | Types.Map _ -> 40, 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
|
||||
@ -813,13 +851,26 @@ let rec dty m d (t : Types.t) : int =
|
||||
("len", Types.Int Types.I64); ("log2cap", Types.Int Types.I64);
|
||||
("allocator", Types.Alloc); ("epoch", Types.Int Types.I64) ]
|
||||
|> fun n -> ignore k; ignore v; n
|
||||
(* 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. *)
|
||||
(* Two words, and shown as two, the same rule the Vec and the Map above
|
||||
follow: a debugger told a function value were one pointer would put
|
||||
every offset after it out by eight. [code] is the address that
|
||||
resolves to a symbol, which is what [p f] was ever worth; [env] is
|
||||
the captured copies, and there is nothing here that could say what is
|
||||
in them — the environment is a struct the checker synthesised for one
|
||||
literal, and DWARF for it would describe a type the program cannot
|
||||
name. A reader who wants the copies asks the break loop for the
|
||||
locals, where they are under the names the source gave them. *)
|
||||
| Types.Fn _ ->
|
||||
composite (Types.to_string t)
|
||||
[ ("code", Types.Ptr Types.Unit); ("env", Types.Ptr Types.Unit) ]
|
||||
(* And the bare one is what it always was: 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 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.CFn _ ->
|
||||
dnode d
|
||||
(Printf.sprintf
|
||||
"!DIDerivedType(tag: DW_TAG_pointer_type, name: \"%s\", \
|
||||
@ -1763,19 +1814,41 @@ and value_at f (e : Tast.expr) : string =
|
||||
| Tast.Local _ | Tast.Global _ | Tast.Field _ | Tast.Deref _ ->
|
||||
(* Everything that denotes a location is a load from its address. *)
|
||||
load f (addr f e) e.Tast.ty
|
||||
(* The symbol itself, not a load from it: a function's address is a link-time
|
||||
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 docs/BUILT.md rather
|
||||
than papered over with a trampoline. *)
|
||||
| Tast.FnAddr (Tast.Fnval n) -> body_of f n
|
||||
(* A [(Fn ...)] value, which is two words: a code address and the
|
||||
environment it is called with. A value made out of a name captures
|
||||
nothing, so the second word is null and [zeroinitializer] has already put
|
||||
it there. See [%fnv].
|
||||
|
||||
Only a [Fn]-typed one. The same three [fnref] constructors are also asked
|
||||
for as bare addresses — carrying [CFn], and carrying [Alloc] for the
|
||||
map's hash and equality pair and a handler frame's clause, which are
|
||||
fields of structs the runtime declares — and those stay one word. The
|
||||
node's type is what says which is being asked for. *)
|
||||
| (Tast.FnAddr _ | Tast.Closure _ | Tast.Thicken _)
|
||||
when (match e.Tast.ty with Types.Fn _ -> true | _ -> false) ->
|
||||
let code, env =
|
||||
match e.Tast.e with
|
||||
| Tast.FnAddr r -> fnaddr f r, "null"
|
||||
| Tast.Closure (r, env) -> fnaddr f r, value f env
|
||||
(* The widening: the thunk's code, with the bare address stored where
|
||||
an environment would be. The thunk reads it back out and calls it,
|
||||
which is what keeps every indirect call exactly typed. *)
|
||||
| Tast.Thicken (n, p) -> fname n, value f p
|
||||
| _ -> assert false
|
||||
in
|
||||
let a = fresh f in
|
||||
ins f "%s = insertvalue %%fnv zeroinitializer, ptr %s, 0" a code;
|
||||
if String.equal env "null" then a
|
||||
else begin
|
||||
let b = fresh f in
|
||||
ins f "%s = insertvalue %%fnv %s, ptr %s, 1" b a env;
|
||||
b
|
||||
end
|
||||
| Tast.FnAddr r -> fnaddr f r
|
||||
| Tast.Closure _ | Tast.Thicken _ ->
|
||||
(* Unreachable: both are [Fn] values and the arm above has already taken
|
||||
every [Fn]-typed node. Here because nothing else could be meant. *)
|
||||
failwith "a closure is a function value"
|
||||
| Tast.Addr p -> fst (place f p)
|
||||
| Tast.Prim (p, args) -> prim f e p args
|
||||
| Tast.Call (name, args) ->
|
||||
@ -2192,14 +2265,57 @@ and call f ret flan args =
|
||||
the middle of an argument list). *)
|
||||
and call_ptr f ret callee args =
|
||||
let c = value f callee in
|
||||
(* A [(Fn ...)] is two words and both are taken before the arguments are
|
||||
evaluated: an argument may itself make a function value, and the two
|
||||
halves of *this* one have to come out of the same value. A
|
||||
[(CFn ...)] is the address alone, and the call that follows is the
|
||||
call a name would have produced. *)
|
||||
let code, env =
|
||||
match callee.Tast.ty with
|
||||
| Types.Fn _ ->
|
||||
let code = fresh f in
|
||||
ins f "%s = extractvalue %%fnv %s, 0" code c;
|
||||
let env = fresh f in
|
||||
ins f "%s = extractvalue %%fnv %s, 1" env c;
|
||||
code, Some ("ptr " ^ env)
|
||||
| _ -> c, None
|
||||
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
|
||||
call_through f ?env ret code vs
|
||||
|
||||
and call_through f ret callee vs =
|
||||
(* The code address behind one of the three [fnref]s, which is the same string
|
||||
whether it is wanted as a bare [Alloc] pointer or as the first word of a
|
||||
function value.
|
||||
|
||||
[Flanfn] and [Rtfn] are the symbol itself, not a load from it: a function's
|
||||
address is a link-time constant. [Fnval] is the one that is not — 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 docs/BUILT.md rather than papered over
|
||||
with a trampoline. *)
|
||||
and fnaddr f (r : Tast.fnref) =
|
||||
match r with
|
||||
| Tast.Flanfn n -> fname n
|
||||
| Tast.Rtfn n -> "@" ^ n
|
||||
| Tast.Fnval n -> body_of f n
|
||||
|
||||
(* [env] is present on exactly one kind of call: one through a [(Fn ...)]
|
||||
value, which cannot know whether the body it reaches declared one. Every
|
||||
other call — by name, through a [(CFn ...)] — passes what it always
|
||||
passed. See [env_param] for why appending it is safe when the callee did
|
||||
not ask for it. *)
|
||||
and call_through f ?env 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 ]));
|
||||
let tail =
|
||||
match env with
|
||||
| None -> [ "ptr " ^ xfer_param ]
|
||||
| Some e -> [ "ptr " ^ xfer_param; e ]
|
||||
in
|
||||
ins f "%s = call %s %s(%s)" t (ll ret) callee (String.concat ", " (vs @ tail));
|
||||
guard f;
|
||||
(* An aggregate with a dyn in it is spilled into a rooted slot the instant it
|
||||
arrives, the same move a dyn word gets in [prim] and for a sharper reason:
|
||||
@ -2293,6 +2409,16 @@ and emit_handled f frames body =
|
||||
stack finds what it pushed still valid, which is what "old code is
|
||||
never unloaded" means. See NEXT.md, conditions step 1. *)
|
||||
ins f "store ptr %s, ptr %s" (fname h.Tast.hfn) fp;
|
||||
(* And the environment the clause is called with, which is a pointer
|
||||
into this very frame. Written unconditionally — null when the
|
||||
clause captured nothing — because a frame the runtime reads a
|
||||
field of must have every field written, not only the ones this
|
||||
clause happens to use. *)
|
||||
let ep = fresh f in
|
||||
ins f "%s = getelementptr inbounds %%handler, ptr %s, i32 0, i32 3"
|
||||
ep slot;
|
||||
ins f "store ptr %s, ptr %s"
|
||||
(match h.Tast.henv with Some e -> value f e | None -> "null") ep;
|
||||
ins f "call void @flan_handler_push(ptr %s)" slot;
|
||||
slot)
|
||||
frames
|
||||
@ -3136,6 +3262,14 @@ let signature ~named (fn : Tast.fn) =
|
||||
analysis is an optimisation, and in a dev build a cell can hold anything,
|
||||
so the honest answer to "what can this call?" is "anything". *)
|
||||
let params = params @ [ (if named then "ptr " ^ xfer_param else "ptr") ] in
|
||||
(* And the environment, last, and only on a body that can be reached
|
||||
through an [Fn] value: see [env_param]. Everything else emits the
|
||||
signature it always did. *)
|
||||
let params =
|
||||
match fn.Tast.fenv with
|
||||
| None -> params
|
||||
| Some _ -> params @ [ (if named then "ptr " ^ env_param else "ptr") ]
|
||||
in
|
||||
Printf.sprintf "%s %s(%s)" (ll fn.Tast.ret) (fname fn.Tast.name)
|
||||
(String.concat ", " params)
|
||||
|
||||
@ -3205,6 +3339,14 @@ let emit_fn m ?(hidden = false) ?(pnames = []) (fn : Tast.fn) =
|
||||
Buffer.add_string f.allocas
|
||||
(Printf.sprintf " store %s %%p%d, ptr %s\n" (ll ty) i f.slots.(i)))
|
||||
fn.Tast.params;
|
||||
(* And the environment, on the one kind of function that has one. Every
|
||||
other function is handed it too and never reads it; there is no slot for
|
||||
it there and nothing to store. *)
|
||||
(match fn.Tast.fenv with
|
||||
| Some slot ->
|
||||
Buffer.add_string f.allocas
|
||||
(Printf.sprintf " store ptr %s, ptr %s\n" env_param f.slots.(slot))
|
||||
| None -> ());
|
||||
(* The dyn roots, and this is not gated on [m.dev]: the shadow stack below is
|
||||
a debugging convenience and a release build does without it, while a
|
||||
collector that cannot find its roots is a collector that frees live
|
||||
@ -3709,7 +3851,7 @@ let emit_startup m ?(hidden = false) (globals : Tast.global list) =
|
||||
List.iter (emit_global m ~hidden) flags;
|
||||
emit_fn m ~hidden
|
||||
{ Tast.name = ".init-globals"; params = []; slots = [||]; snames = [||];
|
||||
ret = Types.Unit; body; fdefers = []; fparent = None;
|
||||
ret = Types.Unit; body; fdefers = []; fenv = None; fparent = None;
|
||||
floc = (List.hd computed).Tast.ginit.Tast.loc };
|
||||
true
|
||||
|
||||
@ -3758,6 +3900,13 @@ let header = {|; Generated by flan. The layout is C's: no object headers anywher
|
||||
; so a Flan struct is exactly its C struct and nothing marshals.
|
||||
|
||||
%slice = type { ptr, i64 }
|
||||
; A function value: the code address, and the environment the captured copies
|
||||
; live in. Two words rather than one because the environment has to travel
|
||||
; *with* the value — a callee that takes a (Fn [T] R) and calls it knows
|
||||
; nothing about where the value came from, so there is nowhere else to put it.
|
||||
; A value that captures nothing carries a null there and every call passes it
|
||||
; on regardless; see [env_param].
|
||||
%fnv = type { ptr, ptr }
|
||||
; (Vec T), spec-memory.md. The element type is nowhere in it: the runtime is
|
||||
; type-erased and every operation is handed size and align at its call site.
|
||||
%vec = type { ptr, i64, i64, ptr, i64 }
|
||||
@ -3765,8 +3914,9 @@ let header = {|; Generated by flan. The layout is C's: no object headers anywher
|
||||
; nor value type appears in it, for the same reason: one type-erased runtime,
|
||||
; handed the two sizes and a hash/equality pair at each call site.
|
||||
%map = type { ptr, i64, i64, ptr, i64 }
|
||||
; A handler frame: the one it displaced, the condition type it matches, and
|
||||
; the lifted function that runs. Allocated on the establishing frame's stack.
|
||||
; A handler frame: the one it displaced, the condition type it matches, the
|
||||
; lifted function that runs, and the environment that function is handed.
|
||||
; Allocated on the establishing frame's stack.
|
||||
|} ^ Rt.ll_type Rt.handler ^ {|
|
||||
; A restart frame: the one it displaced and the name it offers. There is no
|
||||
; target field, because the frame's own address *is* the target — which makes
|
||||
@ -4521,11 +4671,18 @@ let redefinition ?(checks = true) ?(dev = false) ?(debug = false)
|
||||
(* A clause lifted out of one of these comes with it: its body may have
|
||||
changed too, and it is reached by address from inside the module rather
|
||||
than through a cell. Every other lifted clause is invisible here — it
|
||||
needs no declaration, since nothing in this module names it. *)
|
||||
needs no declaration, since nothing in this module names it.
|
||||
|
||||
And the widening thunks, every one of them, whichever body they belong
|
||||
to: a module that hands a name to an [Fn]-typed parameter names one, and
|
||||
the host has no cell for it to be reached through. They are hidden and
|
||||
tiny, so a copy per module is the whole cost — and the alternative is an
|
||||
undefined symbol at dlopen, which is the shape of bug [Fnval] was. *)
|
||||
let lifted =
|
||||
List.filter
|
||||
(fun (f : Tast.fn) ->
|
||||
match f.Tast.fparent with
|
||||
| Some "<thick>" -> true
|
||||
| Some p -> List.mem p fns
|
||||
| None -> false)
|
||||
p.Tast.fns
|
||||
|
||||
14
lib/js.ml
14
lib/js.ml
@ -220,7 +220,8 @@ let rec refuse_ty loc (t : Types.t) =
|
||||
| Types.Never | Types.Named _ | Types.Enum _ -> ()
|
||||
| Types.Slice t | Types.Array (_, t) | Types.Option t -> refuse_ty loc t
|
||||
| Types.Vec t -> refuse_ty loc t
|
||||
| Types.Fn (ps, r) -> List.iter (refuse_ty loc) ps; refuse_ty loc r
|
||||
| Types.Fn (ps, r) | Types.CFn (ps, r) ->
|
||||
List.iter (refuse_ty loc) ps; refuse_ty loc r
|
||||
| Types.Ptr _ ->
|
||||
at loc
|
||||
"(Ptr T) is not in the JS dialect — JavaScript has no addresses, so a \
|
||||
@ -698,6 +699,17 @@ let rec value f (e : Tast.expr) : string =
|
||||
"the runtime entry point %s has no JS counterpart — it is C in \
|
||||
flan_rt.c, and this dialect has no C"
|
||||
n
|
||||
(* A capturing fn literal. A JS function closes over its enclosing scope for
|
||||
free, so this dialect would not need the environment at all — but the
|
||||
environment is a struct the checker synthesised and the captured copies
|
||||
are read out of it by index, which is machinery this backend has nothing
|
||||
to lower. Refused by name rather than emitted as a plain function that
|
||||
would read the *current* value of a local instead of the copy. *)
|
||||
| Tast.Closure _ | Tast.Thicken _ ->
|
||||
at e.Tast.loc
|
||||
"an fn that captures has no JS lowering yet — the environment is a \
|
||||
struct laid out for the two native backends, and this dialect has no \
|
||||
layout"
|
||||
| Tast.Prim (p, args) -> prim f e p args
|
||||
| Tast.Call (n, args) ->
|
||||
Printf.sprintf "%s(%s)" (fname n) (String.concat ", " (call_args f args))
|
||||
|
||||
@ -206,8 +206,9 @@ let rec rename_texpr owned alias (t : Ast.texpr) : Ast.texpr =
|
||||
Ast.Tmap (rename_texpr owned alias k, rename_texpr owned alias v)
|
||||
| Ast.Tapp (n, args) ->
|
||||
Ast.Tapp (n, List.map (rename_texpr owned alias) args)
|
||||
| Ast.Tfn (ps, r) ->
|
||||
Ast.Tfn (List.map (rename_texpr owned alias) ps, rename_texpr owned alias r)
|
||||
| Ast.Tfn (env, ps, r) ->
|
||||
Ast.Tfn (env, List.map (rename_texpr owned alias) ps,
|
||||
rename_texpr owned alias r)
|
||||
in
|
||||
{ t with Ast.t = k }
|
||||
|
||||
@ -746,7 +747,7 @@ let rec texpr_uses acc (t : Ast.texpr) =
|
||||
texpr_uses acc e
|
||||
| Ast.Tmap (k, v) -> texpr_uses acc k; texpr_uses acc v
|
||||
| Ast.Tapp (_, args) -> List.iter (texpr_uses acc) args
|
||||
| Ast.Tfn (ps, r) -> List.iter (texpr_uses acc) ps; texpr_uses acc r
|
||||
| Ast.Tfn (_, ps, r) -> List.iter (texpr_uses acc) ps; texpr_uses acc r
|
||||
|
||||
let rec expr_uses acc (e : Ast.expr) =
|
||||
let go = expr_uses acc in
|
||||
|
||||
11
lib/parse.ml
11
lib/parse.ml
@ -96,11 +96,16 @@ let rec texpr (f : Form.t) : Ast.texpr =
|
||||
confused with. *)
|
||||
| Map _ ->
|
||||
fail f "a map type is written (Map K V), not in braces"
|
||||
| List ({ v = Sym "Fn"; _ } :: rest) ->
|
||||
(* The two function types. [Fn] is the one almost every signature wants — a
|
||||
value that may carry an environment — and [CFn] is the bare address,
|
||||
for a C callback or a table of them. Parsed together because they differ
|
||||
in one word and the refusal should name both. *)
|
||||
| List ({ v = Sym (("Fn" | "CFn") as which); _ } :: rest) ->
|
||||
let env = String.equal which "Fn" in
|
||||
(match rest with
|
||||
| [ { v = Vec params; _ }; ret ] ->
|
||||
mk (Ast.Tfn (List.map texpr params, texpr ret))
|
||||
| _ -> fail f "a function type is (Fn [T ...] R)")
|
||||
mk (Ast.Tfn (env, List.map texpr params, texpr ret))
|
||||
| _ -> fail f "a function type is (%s [T ...] R)" which)
|
||||
| List ({ v = Sym name; _ } :: args) when args <> [] ->
|
||||
mk (Ast.Tapp (name, List.map texpr args))
|
||||
| _ -> fail f "expected a type, found %s" (Form.to_string f)
|
||||
|
||||
@ -50,7 +50,12 @@ let expr_refs f (e : Tast.expr) =
|
||||
name used as a value is never a [Call], so without the second one the
|
||||
one function a program passes to [map] is the one function the link
|
||||
drops. [Rtfn] is C in flan_rt.c and is linked whatever happens. *)
|
||||
| Tast.FnAddr (Tast.Flanfn n) | Tast.FnAddr (Tast.Fnval n) -> f n
|
||||
| Tast.FnAddr (Tast.Flanfn n) | Tast.FnAddr (Tast.Fnval n)
|
||||
| Tast.Closure (Tast.Flanfn n, _) | Tast.Closure (Tast.Fnval n, _)
|
||||
(* And the widening thunk, which is reached by address from the value
|
||||
it builds and from nowhere else. Without this edge the one function
|
||||
a program widens is the one function the link drops. *)
|
||||
| Tast.Thicken (n, _) -> f n
|
||||
| Tast.Set (Tast.Pglobal n, _) | Tast.Addr (Tast.Pglobal n) -> f n
|
||||
| Tast.Handled (frames, _) ->
|
||||
List.iter (fun (h : Tast.hframe) -> f h.Tast.hfn) frames
|
||||
|
||||
@ -373,6 +373,16 @@ let compatible ?(origin = fun _ -> None) ?(relaxed = []) ~loc
|
||||
new_.Tast.globals;
|
||||
List.iter
|
||||
(fun (s : Tast.structure) ->
|
||||
(* An environment the checker synthesised for a capturing fn is not
|
||||
subject to this rule, and that is not a loophole. The layout rule is
|
||||
about values the running program is *holding*: every other struct can
|
||||
be in a global, in a container, in a frame that is on the stack right
|
||||
now. An environment can be in exactly one place — a slot of the frame
|
||||
the literal was written in — and it is written there by the same
|
||||
module that reads it, on every entry. So editing which locals an fn
|
||||
names is an ordinary body change, and demanding a restart for it
|
||||
would take the dev loop away from the feature it was built for. *)
|
||||
if Check.is_env_struct s.Tast.sname then () else
|
||||
match
|
||||
List.find_opt
|
||||
(fun (r : Tast.structure) -> String.equal r.Tast.sname s.Tast.sname)
|
||||
@ -992,7 +1002,7 @@ let eval ?(origin = "<eval>") ?pause t src : change =
|
||||
Some
|
||||
{ Tast.name = Printf.sprintf "install/%d" t.thunks;
|
||||
params = []; ret = Types.Unit; body;
|
||||
fdefers = []; fparent = None; floc = loc;
|
||||
fdefers = []; fenv = None; fparent = None; floc = loc;
|
||||
slots = [||]; snames = [||] }
|
||||
in
|
||||
let ir =
|
||||
@ -1319,7 +1329,7 @@ let render_locals ?(origin = "<locals>") t ~frame ~(fn : Tast.fn) ~bound
|
||||
let thunk : Tast.fn =
|
||||
{ Tast.name; params = []; ret = Types.Unit;
|
||||
body = (nullary "flan/dev-begin" :: body) @ [ nullary "flan/dev-end" ];
|
||||
fdefers = []; fparent = None; floc = loc;
|
||||
fdefers = []; fenv = None; fparent = None; floc = loc;
|
||||
slots = Array.of_list (List.rev !extra);
|
||||
(* Every slot in here is the walk's own scratch: the locals being shown
|
||||
are the *other* frame's, and this thunk reaches them by address. *)
|
||||
@ -1408,7 +1418,7 @@ let render_condition t ~(st : Tast.structure) : change * (string * string) list
|
||||
let thunk : Tast.fn =
|
||||
{ Tast.name; params = []; ret = Types.Unit;
|
||||
body = (nullary "flan/dev-begin" :: body) @ [ nullary "flan/dev-end" ];
|
||||
fdefers = []; fparent = None; floc = loc;
|
||||
fdefers = []; fenv = None; fparent = None; floc = loc;
|
||||
slots = Array.of_list (List.rev !extra);
|
||||
snames = Array.make (List.length !extra) None }
|
||||
in
|
||||
@ -1662,7 +1672,7 @@ let render_slot ?(origin = "<inspect>") t ~frame ~(fn : Tast.fn) ~slot ~path
|
||||
{ Tast.name = tname; params = []; ret = Types.Unit;
|
||||
body =
|
||||
(nullary "flan/dev-begin" :: parts) @ [ nullary "flan/dev-end" ];
|
||||
fdefers = []; fparent = None; floc = loc;
|
||||
fdefers = []; fenv = None; fparent = None; floc = loc;
|
||||
slots = Array.of_list (List.rev !extra);
|
||||
snames = Array.make (List.length !extra) None }
|
||||
in
|
||||
@ -1932,7 +1942,7 @@ let write_slot ?(origin = "<set>") t ~frame ~(fn : Tast.fn) ~slot ~path
|
||||
stores
|
||||
@ (nullary "flan/dev-begin" :: parts)
|
||||
@ [ nullary "flan/dev-end" ];
|
||||
fdefers = []; fparent = None; floc = loc;
|
||||
fdefers = []; fenv = None; fparent = None; floc = loc;
|
||||
slots = Array.append base (Array.of_list (List.rev !extra));
|
||||
(* The stored expressions' own [let]s keep their names; the
|
||||
slots [render] added behind them are the walk's own
|
||||
@ -2030,7 +2040,7 @@ let render_globals ?(origin = "<globals>") t ~(globals : Tast.global list)
|
||||
let thunk : Tast.fn =
|
||||
{ Tast.name; params = []; ret = Types.Unit;
|
||||
body = (nullary "flan/dev-begin" :: body) @ [ nullary "flan/dev-end" ];
|
||||
fdefers = []; fparent = None; floc = loc;
|
||||
fdefers = []; fenv = None; fparent = None; floc = loc;
|
||||
slots = Array.of_list (List.rev !extra);
|
||||
(* Every slot in here is the walk's own scratch: what is being shown is
|
||||
the program's storage, which this thunk reaches by name. *)
|
||||
@ -2118,7 +2128,7 @@ let eval_expr ?(origin = "<eval>") ?(pause = false) t src : change =
|
||||
t.thunks <- t.thunks + 1;
|
||||
let name = Printf.sprintf "eval/%d" t.thunks in
|
||||
let thunk : Tast.fn =
|
||||
{ Tast.name; params = []; ret = Types.Unit; body; fdefers = []; fparent = None; floc = loc;
|
||||
{ Tast.name; params = []; ret = Types.Unit; body; fdefers = []; fenv = None; fparent = None; floc = loc;
|
||||
slots = Array.append base (Array.of_list (List.rev !extra));
|
||||
(* The expression's own [let]s keep their names; the slots [render] added
|
||||
behind them are the walk's own scratch and have none to keep. *)
|
||||
|
||||
67
lib/tast.ml
67
lib/tast.ml
@ -112,6 +112,42 @@ and expr_kind =
|
||||
dev build is not the symbol but whatever the indirection cell holds, and
|
||||
carries the Flan type [Fn]. *)
|
||||
| FnAddr of fnref
|
||||
(* A function value with an environment: the lifted body, and the address of
|
||||
the copies the enclosing frame is holding for it. The environment is a
|
||||
[Make] of a struct the checker synthesised, stored into a slot of the
|
||||
frame the literal was written in, so this node's second half is an
|
||||
[Addr (Plocal _)] and the copies were taken where the value was made.
|
||||
|
||||
Its own node rather than a field on [FnAddr] because the two answer
|
||||
different questions: [FnAddr] is an address, and is asked for by three
|
||||
unrelated readers that want a bare symbol ([Alloc]-typed, see [fnref]),
|
||||
while this is a *value* of type [Fn] and can never be anything else.
|
||||
|
||||
What stops it dangling is the checker, not this node: a value carrying an
|
||||
environment may not leave the frame that owns it, so every position that
|
||||
would outlive the frame is refused. spec-memory.md's case 2, and the
|
||||
escaping half — an environment the collector allocates — is the case the
|
||||
refusals name. *)
|
||||
| Closure of fnref * expr
|
||||
(* A (CFn ...) value where a (Fn ...) is wanted. The one coercion between
|
||||
the two function types, and it goes this way only: there is nowhere for
|
||||
an environment to go in the other direction.
|
||||
|
||||
The pair it builds is {thunk, the address}: the *thunk's* code, one per
|
||||
signature, with the original bare address stored where an environment
|
||||
would be. The thunk reads it back out and calls it. So a value reached
|
||||
through this is reached by a body that really does take an environment,
|
||||
which is what keeps every indirect call exactly typed — including on
|
||||
wasm32, where [call_indirect] checks the signature and an argument the
|
||||
callee did not declare is a trap rather than a register nobody reads.
|
||||
|
||||
The string is the thunk's name, minted and memoised by the checker: the
|
||||
backends emit the pair and derive nothing. What it costs is one hop per
|
||||
call, paid by a *name* handed to an [Fn]-typed parameter and by nothing
|
||||
else — a literal, capturing or not, is compiled to take an environment
|
||||
and needs no thunk. A signature that wants the address alone writes
|
||||
[CFn] and pays nothing at all, which is what the type is for. *)
|
||||
| Thicken of string * expr
|
||||
(* 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
|
||||
@ -246,9 +282,16 @@ and place =
|
||||
| Pindex of expr * expr list
|
||||
| Pderef of expr
|
||||
|
||||
(* A pushed handler: which condition type it matches, and the lifted function
|
||||
that runs when one is signalled. *)
|
||||
and hframe = { htype : int; hfn : string }
|
||||
(* A pushed handler: which condition type it matches, the lifted function that
|
||||
runs when one is signalled, and the environment that function is handed.
|
||||
|
||||
[henv] is the address of the establishing frame's copies of whatever the
|
||||
clause captured, or [None] when it captured nothing. It is sound for the
|
||||
same reason the frame itself is: a handler frame is popped by the body that
|
||||
pushed it, so it can never be reached from outside the extent of the
|
||||
function whose stack both it and the environment live on. There is no
|
||||
escaping case here to defer. *)
|
||||
and hframe = { htype : int; hfn : string; henv : expr option }
|
||||
|
||||
(* A restart clause. [rname_id] is what [invoke-restart] matches by name; the
|
||||
body is a branch in the function that wrote it, because unlike a handler a
|
||||
@ -307,6 +350,19 @@ type fn = {
|
||||
cell and no registry slot, and a redefinition of the parent carries its
|
||||
own copy. *)
|
||||
fparent : string option;
|
||||
(* The slot the environment parameter is stored into, on a function that
|
||||
was lifted out of something and captures one of its locals. Every
|
||||
emitted signature takes the environment (see Emit's [env_param]) and
|
||||
almost every function ignores it; this is the one that does not, and it
|
||||
says where the pointer goes rather than fixing an index by convention,
|
||||
because the slot is minted by [fresh_slot] like any other and a rule of
|
||||
the form "the slot after the parameters" would be a second thing to keep
|
||||
in step with the allocation order.
|
||||
|
||||
[None] on everything anyone wrote. A capturing body reads its copies out
|
||||
of this pointer once, at entry, into named slots of its own — so the
|
||||
copy the value was made with is the copy the body sees. *)
|
||||
fenv : int option;
|
||||
floc : Loc.t;
|
||||
}
|
||||
|
||||
@ -403,9 +459,10 @@ let rec walk (f : expr -> unit) (e : expr) =
|
||||
| Set (p, v) -> walk_place f p; go v
|
||||
| Addr p -> walk_place f p
|
||||
| Field (t, _) | Deref t | CaseField (t, _, _) | Some_ t | UnwrapSome t
|
||||
| Signal (_, _, t) -> go t
|
||||
| Signal (_, _, t) | Closure (_, t) | Thicken (_, t) -> go t
|
||||
| Match (sc, arms) -> go sc; List.iter (fun a -> gos a.abody) arms
|
||||
| Handled (_, body) -> gos body
|
||||
| Handled (hs, body) ->
|
||||
List.iter (fun h -> Option.iter go h.henv) hs; gos body
|
||||
| RestartCase (cs, body) -> List.iter (fun c -> gos c.rbody) cs; go body
|
||||
| WithAlloc (a, body) -> go a; gos body
|
||||
|
||||
|
||||
51
lib/types.ml
51
lib/types.ml
@ -48,7 +48,50 @@ type t =
|
||||
at the call site, which is exactly where the two numbers are produced. *)
|
||||
| Vec of t
|
||||
| Option of t (* (Option T) *)
|
||||
(* The two function types, and the difference between them is what a value
|
||||
of each one *is* rather than what it may do.
|
||||
|
||||
[(Fn [T ...] R)] is a code address and the environment it is called
|
||||
with: two words. It is the common case and keeps the short name, because
|
||||
it is what almost every higher-order signature wants — a caller may pass
|
||||
it a name, a non-capturing literal, or one that captured half the frame,
|
||||
and the callee neither knows nor cares.
|
||||
|
||||
[(CFn [T ...] R)] is the bare address: one word, no environment, and
|
||||
therefore nothing that can capture.
|
||||
|
||||
**The [C] is information, not decoration.** A value with no environment
|
||||
is the only kind that could ever cross to C, and under the
|
||||
[--no-conditions] direction FIX.org records — where a signature that
|
||||
cannot transfer drops the channel too — one becomes literally a C
|
||||
function pointer. The name points at what the type *is* and at where it
|
||||
is going.
|
||||
|
||||
What it does **not** point at is a capability that exists now: a
|
||||
[declare] cannot take a function type at all today, because a Flan
|
||||
signature ends with the transfer channel and a C caller knows nothing
|
||||
about one. Anyone reaching for [CFn] straight after writing a
|
||||
[declare-c] is reaching too early, and [crossable] says so where they
|
||||
will meet it.
|
||||
|
||||
The whole of the reason there are two: a uniform environment would tax
|
||||
every function in every program for a feature most of them never use,
|
||||
and the static side is not to pay for the dynamic side's existence. With
|
||||
two types an ordinary [defn] keeps exactly the signature it always had.
|
||||
|
||||
**Nobody ever needs [CFn].** [Fn] accepts everything a [CFn] does, so
|
||||
the narrow one is reached for on purpose, for one of four reasons:
|
||||
handing a function to C (later, as above); a table of bare addresses;
|
||||
forbidding capture at a boundary; and the one that is likeliest in
|
||||
practice — a *named* function passed to an [Fn] parameter goes through
|
||||
the widening thunk and pays an indirect hop per call, where a [CFn]
|
||||
parameter is a direct call. [(map-in-place s double)] is the example.
|
||||
|
||||
One-way: a [CFn] value satisfies an [Fn] (paired with a null
|
||||
environment), and an [Fn] does not satisfy a [CFn] — there is nowhere
|
||||
for the environment to go. *)
|
||||
| Fn of t list * t (* (Fn [T ...] R) *)
|
||||
| CFn of t list * t (* (CFn [T ...] R) *)
|
||||
| Var of string (* a type variable — milestone 5 *)
|
||||
(* [dyn]: one machine word whose contents the runtime knows and this module
|
||||
does not. It is a written type — [(defonce x dyn 5)] boxes the 5 — and it
|
||||
@ -142,7 +185,10 @@ let rec equal a b =
|
||||
| Alloc, Alloc -> true
|
||||
| Vec x, Vec y -> equal x y
|
||||
| Option x, Option y -> equal x y
|
||||
| Fn (ps, r), Fn (ps', r') ->
|
||||
(* The two are *not* equal to each other, in either direction. One-way
|
||||
coercion lives in [Check.expect], where it can build the value the
|
||||
wider type needs; here there is only identity. *)
|
||||
| Fn (ps, r), Fn (ps', r') | CFn (ps, r), CFn (ps', r') ->
|
||||
List.length ps = List.length ps'
|
||||
&& List.for_all2 equal ps ps'
|
||||
&& equal r r'
|
||||
@ -167,6 +213,9 @@ let rec to_string = function
|
||||
| Fn (ps, r) ->
|
||||
Printf.sprintf "(Fn [%s] %s)"
|
||||
(String.concat " " (List.map to_string ps)) (to_string r)
|
||||
| CFn (ps, r) ->
|
||||
Printf.sprintf "(CFn [%s] %s)"
|
||||
(String.concat " " (List.map to_string ps)) (to_string r)
|
||||
| Var n -> n
|
||||
| Dyn -> "dyn"
|
||||
|
||||
|
||||
179
lib/x86.ml
179
lib/x86.ml
@ -498,9 +498,15 @@ let alignof md t = snd (Emit.lay md t)
|
||||
than SysV's eight. *)
|
||||
let is_agg (t : Types.t) =
|
||||
match t with
|
||||
(* A [(CFn ...)] is one word and crosses exactly as a pointer does, which
|
||||
is the whole of its reason for existing. *)
|
||||
| Types.Int _ | Types.Float _ | Types.Bool | Types.Ptr _ | Types.Enum _
|
||||
| Types.Alloc | Types.Fn _ -> false
|
||||
| Types.Alloc | Types.CFn _ -> false
|
||||
| Types.Unit | Types.Never -> false
|
||||
(* A [(Fn ...)] is two words — the code address and the environment beside
|
||||
it — so it crosses the way a slice does. [Emit.lay] is the one place that
|
||||
says how wide it is and this agrees with it by asking. *)
|
||||
| Types.Fn _ -> true
|
||||
| Types.String | Types.Slice _ | Types.Array _ | Types.Map _ | Types.Vec _
|
||||
| Types.Option _ | Types.Named _ -> true
|
||||
(* A scalar, and trivially one: runtime/flan_dyn.h says [typedef uint64_t
|
||||
@ -1189,6 +1195,27 @@ let load_sym f ~dst s =
|
||||
end
|
||||
else load_int f.b ~dst ~mm:(Sym (s, 0)) ~size:8 ~signed:false
|
||||
|
||||
(* The code address behind one of the three [fnref]s, which is the same
|
||||
sequence whether it is wanted as a bare [Alloc] pointer or as the first
|
||||
word of a function value.
|
||||
|
||||
[Flanfn] and [Rtfn] are the symbol itself, not a load from it: a function's
|
||||
address is a link-time constant, and [Flanfn] is the spelling a lifted
|
||||
handler clause is reached by. [Fnval] is the one that is not — in a release
|
||||
build there is nothing to redefine and it is the symbol after all; in a dev
|
||||
build it is the cell's contents, so that a value taken after a redefinition
|
||||
is the new body. What that does not give — and [emit.ml] names it rather
|
||||
than papering over it with a trampoline — is a value taken *before* a
|
||||
redefinition and called after it. Once the address is in a slot there is
|
||||
nothing left to re-resolve. *)
|
||||
let fnaddr f ~reg (r : Tast.fnref) =
|
||||
match r with
|
||||
| Tast.Flanfn n -> addr_sym f ~dst:reg (fsym n)
|
||||
| Tast.Rtfn n -> addr_sym f ~dst:reg n
|
||||
| Tast.Fnval n ->
|
||||
if f.md.Emit.dev then load_sym f ~dst:reg (csym n)
|
||||
else addr_sym f ~dst:reg (fsym n)
|
||||
|
||||
let scalar_size f (t : Types.t) =
|
||||
match t with Types.Bool -> 1 | _ -> max 1 (sizeof f.md t)
|
||||
|
||||
@ -1454,6 +1481,7 @@ let agg_tmp f (ty : Types.t) =
|
||||
let h_size = Emit.Rt.size Emit.Rt.handler
|
||||
let h_type = Emit.Rt.field Emit.Rt.handler "type"
|
||||
let h_fn = Emit.Rt.field Emit.Rt.handler "fn"
|
||||
let h_env = Emit.Rt.field Emit.Rt.handler "env"
|
||||
|
||||
let r_size = Emit.Rt.size Emit.Rt.restart
|
||||
let r_field = Emit.Rt.field Emit.Rt.restart
|
||||
@ -1477,6 +1505,11 @@ type arg =
|
||||
| Aflt of loc * Types.t
|
||||
| Aptr of loc
|
||||
| Alen of loc
|
||||
(* A null pointer, which is what a call site with no environment to pass
|
||||
hands over — every Flan signature takes one. It is its own case rather
|
||||
than a frame temporary holding zero because there is nothing to spill:
|
||||
the register is zeroed where it is placed. *)
|
||||
| Anull
|
||||
|
||||
(* The C boundary, and the one place this backend must match SysV rather than
|
||||
pick. [check.ml] rejects an aggregate in a [declare] signature and the shim
|
||||
@ -1525,6 +1558,7 @@ let emit_args f (args : arg list) =
|
||||
| Alen l ->
|
||||
load_int f.b ~dst:reg ~mm:(lmem f (shift l 8) ~scratch:r11) ~size:8
|
||||
~signed:true
|
||||
| Anull -> xor_rr f.b ~dst:reg ~src:reg
|
||||
in
|
||||
(* The stack half first, because it uses rax as its courier and a register
|
||||
argument must not already be sitting in rax while that happens. *)
|
||||
@ -1720,27 +1754,34 @@ and lower_at f (e : Tast.expr) (dst : loc) : unit =
|
||||
let l = place f p in
|
||||
addr_into f ~reg:rax l;
|
||||
store_int f.b ~src:rax ~mm:(lmem f dst ~scratch:r11) ~size:8
|
||||
(* The symbol itself, not a load from it: a function's address is a
|
||||
link-time constant, and this is the spelling a lifted handler clause is
|
||||
reached by. [emit.ml] says the same of [Flanfn]. *)
|
||||
| Tast.FnAddr (Tast.Flanfn n) ->
|
||||
addr_sym f ~dst:rax (fsym n);
|
||||
store_int f.b ~src:rax ~mm:(lmem f dst ~scratch:r11) ~size:8
|
||||
(* A function value someone wrote, which is the one [FnAddr] that is not the
|
||||
symbol. In a release build there is nothing to redefine and it is the
|
||||
symbol after all; in a dev build it is the cell's contents, so that a
|
||||
value taken after a redefinition is the new body. What that does not give
|
||||
— and [emit.ml] names it rather than papering over it with a trampoline —
|
||||
is a value taken *before* a redefinition and called after it. Once the
|
||||
address is in a slot there is nothing left to re-resolve. *)
|
||||
| Tast.FnAddr (Tast.Fnval n) ->
|
||||
if f.md.Emit.dev then
|
||||
load_sym f ~dst:rax (csym n)
|
||||
else addr_sym f ~dst:rax (fsym n);
|
||||
store_int f.b ~src:rax ~mm:(lmem f dst ~scratch:r11) ~size:8
|
||||
| Tast.FnAddr (Tast.Rtfn n) ->
|
||||
addr_sym f ~dst:rax n;
|
||||
(* A [(Fn ...)] value: the code address, then the environment beside it.
|
||||
Two words — see [Emit]'s %fnv. Only a [Fn]-typed node; the same three
|
||||
constructors are also asked for as bare addresses, carrying [CFn] or
|
||||
[Alloc], and those stay one word. The node's type says which. *)
|
||||
| (Tast.FnAddr _ | Tast.Closure _ | Tast.Thicken _)
|
||||
when (match t with Types.Fn _ -> true | _ -> false) ->
|
||||
let env =
|
||||
match e.Tast.e with
|
||||
| Tast.FnAddr r -> fnaddr f ~reg:rax r; None
|
||||
| Tast.Closure (r, env) -> fnaddr f ~reg:rax r; Some env
|
||||
(* The widening: the thunk's code, with the bare address stored where
|
||||
an environment would be. The thunk reads it back out and calls it,
|
||||
which is what keeps every indirect call exactly typed. *)
|
||||
| Tast.Thicken (n, p) -> addr_sym f ~dst:rax (fsym n); Some p
|
||||
| _ -> assert false
|
||||
in
|
||||
store_int f.b ~src:rax ~mm:(lmem f dst ~scratch:r11) ~size:8;
|
||||
(match env with
|
||||
| None -> xor_rr f.b ~dst:rax ~src:rax
|
||||
| Some ev -> let l = eval f ev in load_loc f ~reg:rax l (Types.Ptr Types.Unit));
|
||||
store_int f.b ~src:rax ~mm:(lmem f (shift dst 8) ~scratch:r11) ~size:8
|
||||
| Tast.FnAddr r ->
|
||||
fnaddr f ~reg:rax r;
|
||||
store_int f.b ~src:rax ~mm:(lmem f dst ~scratch:r11) ~size:8
|
||||
| Tast.Closure _ | Tast.Thicken _ ->
|
||||
(* Unreachable: the arm above has taken every [Fn]-typed node, and both of
|
||||
these are function values and can be nothing else. *)
|
||||
unsupported "a closure that is not a function value"
|
||||
| Tast.Prim (p, args) -> prim f e p args dst
|
||||
| Tast.Call (name, args) ->
|
||||
(match Hashtbl.find_opt f.externs name with
|
||||
@ -1752,8 +1793,17 @@ and lower_at f (e : Tast.expr) (dst : loc) : unit =
|
||||
~target:(if f.md.Emit.dev then `Cell (csym name) else `Sym (fsym name))
|
||||
~args ~rty:t dst)
|
||||
| Tast.CallPtr (callee, args) ->
|
||||
(* Through a [(Fn ...)]: both words out of one value, the code address as
|
||||
the call target and the environment beside it as the extra argument.
|
||||
Through a [(CFn ...)]: the address alone, and the call that follows
|
||||
is the call a name would have produced. *)
|
||||
let c = eval f callee in
|
||||
call_flan f ~target:(`Loc c) ~args ~rty:t dst
|
||||
let env =
|
||||
match callee.Tast.ty with
|
||||
| Types.Fn _ -> Some (Aint (shift c 8, Types.Ptr Types.Unit))
|
||||
| _ -> None
|
||||
in
|
||||
call_flan f ?env ~target:(`Loc c) ~args ~rty:t dst
|
||||
| Tast.Do body -> block f body dst t
|
||||
| Tast.Let (bs, body) ->
|
||||
List.iter
|
||||
@ -1953,6 +2003,16 @@ and emit_handled f frames body dst t =
|
||||
name it and it lives only for this body. *)
|
||||
addr_sym f ~dst:rax (fsym h.Tast.hfn);
|
||||
store_int f.b ~src:rax ~mm:(Frame (slot + h_fn)) ~size:8;
|
||||
(* And the environment the clause is called with, a pointer into this
|
||||
very frame. Written unconditionally — null when the clause
|
||||
captured nothing — because the runtime reads the field either
|
||||
way. *)
|
||||
(match h.Tast.henv with
|
||||
| Some ev ->
|
||||
let l = scoped f (fun () -> eval f ev) in
|
||||
load_loc f ~reg:rax l (Types.Ptr Types.Unit)
|
||||
| None -> xor_rr f.b ~dst:rax ~src:rax);
|
||||
store_int f.b ~src:rax ~mm:(Frame (slot + h_env)) ~size:8;
|
||||
lea f.b ~dst:rdi ~mm:(Frame slot);
|
||||
xor_rr f.b ~dst:rax ~src:rax;
|
||||
call_sym f.b "flan_handler_push";
|
||||
@ -2812,7 +2872,7 @@ and ret_loc f = if is_agg f.fret then Lp (f.sret_off, 0) else Lf f.retval
|
||||
integer or SSE sequence, every aggregate by pointer, a hidden [sret] in the
|
||||
first integer register when the result is an aggregate, and the transfer
|
||||
channel last of all. *)
|
||||
and call_flan f ~target ~args ~rty dst =
|
||||
and call_flan f ?env ~target ~args ~rty dst =
|
||||
let vals = List.map (fun (a : Tast.expr) -> eval f a, a.Tast.ty) args in
|
||||
let callee =
|
||||
match target with
|
||||
@ -2832,7 +2892,13 @@ and call_flan f ~target ~args ~rty dst =
|
||||
(* The channel is this frame's own: a callee that transfers writes through
|
||||
the pointer we were handed, so one cell serves the whole chain. *)
|
||||
let chan = [ Aint (Lf f.xfer_off, Types.Ptr Types.Unit) ] in
|
||||
ignore (emit_args f (head @ body @ chan));
|
||||
(* And the environment last of all, on exactly one kind of call: one through
|
||||
a [(Fn ...)] value, which cannot know whether the body it reaches
|
||||
declared one. Every other call passes what it always passed — this is
|
||||
where an ordinary [defn] keeps costing nothing. [Emit.env_param] is where
|
||||
the position is argued. *)
|
||||
let tail = match env with None -> [] | Some a -> [ a ] in
|
||||
ignore (emit_args f (head @ body @ chan @ tail));
|
||||
(* The cell is loaded *after* the arguments, and [emit.ml] has the same as a
|
||||
load-bearing comment: a redefinition that lands between two calls still
|
||||
must not land in the middle of one. [r11] is scratch and no argument
|
||||
@ -3371,11 +3437,12 @@ let frame_bytes f = ((f.maxframe + f.outgoing + 15) / 16) * 16
|
||||
|
||||
(* Where each argument arrives, in the order the header lays down: a hidden
|
||||
[sret] first when the result is an aggregate, then the parameters, then the
|
||||
transfer channel. Answers one entry per incoming value — a register number,
|
||||
or a positive [rbp] displacement for the ones that came on the stack. *)
|
||||
environment, then the transfer channel. Answers one entry per incoming
|
||||
value — a register number, or a positive [rbp] displacement for the ones
|
||||
that came on the stack. *)
|
||||
type incoming = Ireg of int | Isse of int | Istk of int
|
||||
|
||||
let incoming_of ~sret (params : Types.t list) =
|
||||
let incoming_of ~sret ~env (params : Types.t list) =
|
||||
let ints = ref 0 and sses = ref 0 and stk = ref 0 in
|
||||
let next_int () =
|
||||
if !ints < n_int_args then (incr ints; Ireg int_args.(!ints - 1))
|
||||
@ -3395,7 +3462,17 @@ let incoming_of ~sret (params : Types.t list) =
|
||||
else next_int ())
|
||||
params
|
||||
in
|
||||
sret_at, ps, next_int ()
|
||||
(* Left to right, and the two [next_int ()] calls must be sequenced: OCaml's
|
||||
argument evaluation order is unspecified, so a tuple built in one
|
||||
expression could hand the channel's register to the environment.
|
||||
|
||||
The channel, then the environment, and the environment only on a body
|
||||
that declared one — which is exactly the set of bodies an [Fn] value can
|
||||
reach. Every other function is never the target of an env-passing call,
|
||||
so the two never meet out of step. See [Emit.env_param]. *)
|
||||
let xfer_at = next_int () in
|
||||
let env_at = if env then Some (next_int ()) else None in
|
||||
sret_at, ps, env_at, xfer_at
|
||||
|
||||
(* ── The frame map ───────────────────────────────────────────────────── *)
|
||||
|
||||
@ -3422,7 +3499,7 @@ let where_from = function
|
||||
|
||||
let frame_map (md : Emit.m) (fn : Tast.fn) ~slots ~fixed ~total ~outgoing
|
||||
~xfer_off ~sret_off ~retval ~dframe ~dslotv ~sret ~sret_at ~param_at
|
||||
~xfer_at =
|
||||
~env_at ~xfer_at =
|
||||
let b = Buffer.create 1024 in
|
||||
let line s = Buffer.add_string b (if s = "" then "#\n" else "# " ^ s ^ "\n") in
|
||||
(* The prose paragraphs wrap; the table below does not, because its columns
|
||||
@ -3493,10 +3570,22 @@ let frame_map (md : Emit.m) (fn : Tast.fn) ~slots ~fixed ~total ~outgoing
|
||||
(Types.to_string fn.Tast.ret)
|
||||
(if is_float fn.Tast.ret then "xmm0" else "rax")));
|
||||
para (Printf.sprintf
|
||||
"The transfer channel arrives last of all, %s. It is a pointer to the cell a \
|
||||
callee writes its target into, and reading it is what every guard below \
|
||||
does."
|
||||
(where_from xfer_at));
|
||||
"The transfer channel arrives after the parameters, %s. It is a pointer to the \
|
||||
cell a callee writes its target into, and reading it is what every guard \
|
||||
below does.%s"
|
||||
(where_from xfer_at)
|
||||
(match env_at with
|
||||
| None ->
|
||||
" Nothing follows it: no (Fn ...) value can reach this function, so it \
|
||||
declares no environment — which is what lets an ordinary defn cost \
|
||||
exactly what it did before capture existed."
|
||||
| Some at ->
|
||||
Printf.sprintf
|
||||
" And then the environment, %s, because a (Fn ...) value can reach this \
|
||||
function and every such call passes one. It holds the captured copies \
|
||||
when there are any and is ignored when there are not; either way the \
|
||||
signature declares it, so the call is exactly typed."
|
||||
(where_from at)));
|
||||
line "";
|
||||
para (Printf.sprintf
|
||||
"The frame is 0x%x bytes below rbp. %s" total
|
||||
@ -3716,7 +3805,9 @@ let emit_fn (md : Emit.m) ~externs ~fns ?(ext = fun _ -> false)
|
||||
high-water mark of the temporaries and this is the boundary below which
|
||||
they start. It is the frame map's last line. *)
|
||||
let fixed = f.frame in
|
||||
let sret_at, param_at, xfer_at = incoming_of ~sret fn.Tast.params in
|
||||
let sret_at, param_at, env_at, xfer_at =
|
||||
incoming_of ~sret ~env:(fn.Tast.fenv <> None) fn.Tast.params
|
||||
in
|
||||
(* An aggregate parameter arrives as a pointer to the caller's copy and has
|
||||
to be copied into its slot before anything else runs — and [rep movsb]
|
||||
eats rdi, rsi and rcx, which is where three of the other parameters still
|
||||
@ -3975,6 +4066,18 @@ let emit_fn (md : Emit.m) ~externs ~fns ?(ext = fun _ -> false)
|
||||
| _ -> max 1 (fst (Emit.lay md ty)))
|
||||
end)
|
||||
fn.Tast.params;
|
||||
(* The environment, on the one kind of function that declared one. Every
|
||||
other function never asks where it is, which is exactly why a call site
|
||||
may append it whether or not the callee wanted it. *)
|
||||
(match fn.Tast.fenv, env_at with
|
||||
| Some slot, Some at ->
|
||||
(match at with
|
||||
| Ireg r -> store_int pb ~src:r ~mm:(Frame f.slots.(slot)) ~size:8
|
||||
| Istk d ->
|
||||
load_int pb ~dst:rax ~mm:(Frame d) ~size:8 ~signed:false;
|
||||
store_int pb ~src:rax ~mm:(Frame f.slots.(slot)) ~size:8
|
||||
| Isse _ -> unsupported "the environment in an SSE register")
|
||||
| _ -> ());
|
||||
(match xfer_at with
|
||||
| Ireg r -> store_int pb ~src:r ~mm:(Frame f.xfer_off) ~size:8
|
||||
| Istk d ->
|
||||
@ -4052,7 +4155,7 @@ let emit_fn (md : Emit.m) ~externs ~fns ?(ext = fun _ -> false)
|
||||
(frame_map md fn ~slots:f.slots ~fixed ~total:(frame_bytes f)
|
||||
~outgoing:f.outgoing ~xfer_off:f.xfer_off ~sret_off:f.sret_off
|
||||
~retval:f.retval ~dframe:f.dframe ~dslotv:f.dslotv ~sret ~sret_at
|
||||
~param_at ~xfer_at);
|
||||
~param_at ~env_at ~xfer_at);
|
||||
Buffer.add_string out (Printf.sprintf "\t.globl\t%s\n" sym);
|
||||
(* [emit.ml:2072] says this is load-bearing and it is: default visibility in
|
||||
a shared object is interposable, and that applies to taking the address
|
||||
@ -4191,6 +4294,8 @@ let emit_main ?(cfi = false) ?(ann = false) ?(startup = false) ?(gc = false)
|
||||
let xfer = -8 and argv = -32 in
|
||||
xor_rr b ~dst:rax ~src:rax;
|
||||
store_int b ~src:rax ~mm:(Frame xfer) ~size:8;
|
||||
(* [main] declares no environment — it is not reached through a function
|
||||
value — so this is the call it always was. *)
|
||||
(match fn.Tast.params with
|
||||
| [] -> lea b ~dst:rdi ~mm:(Frame xfer)
|
||||
| [ _ ] ->
|
||||
@ -4857,10 +4962,14 @@ let redefinition ~checks ?(dev = true) ?(known = fun _ -> true)
|
||||
(* A clause lifted out of a target comes with it: its body may have changed
|
||||
too, and it is reached by address from inside this module rather than
|
||||
through a cell. Every other lifted clause is invisible here. *)
|
||||
(* The widening thunks come whole, for the reason [Emit.redefinition]
|
||||
gives: a module that hands a name to an [Fn]-typed parameter names one
|
||||
and the host has no cell for it. *)
|
||||
let lifted =
|
||||
List.filter
|
||||
(fun (f : Tast.fn) ->
|
||||
match f.Tast.fparent with
|
||||
| Some "<thick>" -> true
|
||||
| Some q -> List.mem q fns
|
||||
| None -> false)
|
||||
p.Tast.fns
|
||||
|
||||
@ -33,10 +33,21 @@
|
||||
* A type is a number rather than a pointer to anything, so that a module
|
||||
* compiled later against a running program agrees with it: see Check.type_id. */
|
||||
|
||||
/* [env] is the establishing function's copies of whatever the clause
|
||||
* captured, or NULL. It is passed after the channel, and *every* clause
|
||||
* declares it whether or not it captured — this walk cannot know which one
|
||||
* it is about to reach, and a call whose signature is one argument longer
|
||||
* than the callee's is a trap on wasm32, where call_indirect compares them.
|
||||
* Emit's env_param is where the rule is written.
|
||||
*
|
||||
* It points into the establishing frame, which is alive for exactly as long
|
||||
* as the handler frame below it is on this stack — a handler frame is popped
|
||||
* by the body that pushed it, so there is no dangling case here to defer. */
|
||||
typedef struct flan_handler {
|
||||
struct flan_handler *prev;
|
||||
uint32_t type_id;
|
||||
void (*fn)(void *condition, void *xfer);
|
||||
void (*fn)(void *condition, void *xfer, void *env);
|
||||
void *env;
|
||||
} flan_handler;
|
||||
|
||||
static flan_handler *handlers;
|
||||
@ -64,7 +75,7 @@ void flan_handler_pop(flan_handler *h) {
|
||||
void flan_signal(uint32_t type_id, void *condition, void *xfer) {
|
||||
for (flan_handler *h = handlers; h != NULL; h = h->prev)
|
||||
if (h->type_id == type_id) {
|
||||
h->fn(condition, xfer);
|
||||
h->fn(condition, xfer, h->env);
|
||||
if (*(void **)xfer != NULL) return;
|
||||
}
|
||||
}
|
||||
@ -1992,7 +2003,9 @@ static uint64_t flan_hash_mem(const uint8_t *p, int64_t n, uint64_t seed) {
|
||||
*
|
||||
* The pointer form has to match flan_hash_fn, whose last parameter exists
|
||||
* because a hash function emitted for a struct key is an ordinary Flan
|
||||
* function and every Flan function's signature ends with the transfer channel.
|
||||
* function and every Flan function's signature ends with the transfer
|
||||
* channel. No environment: a hasher is reached from this file and never
|
||||
* through a function value, so it declares none and is handed none.
|
||||
* The direct form has to match what such an emitted function *calls*, and an
|
||||
* emitted function has no channel to hand on — it would be passing its own,
|
||||
* which is not the same thing and not something a leaf hasher should see. So
|
||||
|
||||
13
test/programs/fn-capture-dyn.flan
Normal file
13
test/programs/fn-capture-dyn.flan
Normal file
@ -0,0 +1,13 @@
|
||||
;; A dyn is the one thing a capture refuses outright, and for the reason a
|
||||
;; struct field of dyn already refuses: the collector's roots are frames, and
|
||||
;; nothing pushes the fields of the environment struct a capture synthesises.
|
||||
;; A copy in there would be a live value reachable only through memory the
|
||||
;; marker never walks. Milestone 2's per-type descriptors lift it, alongside
|
||||
;; the condition payload's and the struct field's.
|
||||
(defn run [f (Fn [] i64)] i64 (f))
|
||||
|
||||
;; [d] is unannotated, which is what makes it a dyn.
|
||||
(defn use [d] i64
|
||||
(run (fn [] (i64 d))))
|
||||
|
||||
(defn main [] i32 (println (use 7)) 0)
|
||||
10
test/programs/fn-capture-set.flan
Normal file
10
test/programs/fn-capture-set.flan
Normal file
@ -0,0 +1,10 @@
|
||||
;; A captured name is a copy, taken where the value was made. A store into it
|
||||
;; would change the copy and leave the local it came from as it was, which is
|
||||
;; a silent disagreement — so it is refused, and the message says which of the
|
||||
;; two would have moved.
|
||||
(defn run [f (Fn [] i32)] i32 (f))
|
||||
|
||||
(defn main [] i32
|
||||
(let [n 1]
|
||||
(println (run (fn [] (set n 2) n))))
|
||||
0)
|
||||
@ -1,11 +1,116 @@
|
||||
;; 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))
|
||||
;; Capture by value into a stack environment — spec-memory.md's case 2.
|
||||
;;
|
||||
;; An fn is still lifted into a function of its own, but it is no longer handed
|
||||
;; nothing but its parameters: a local of the enclosing function that it names
|
||||
;; is *copied* into an environment on that function's frame when the value is
|
||||
;; made, and the lifted body reads the copy. The value is the code address and
|
||||
;; that environment beside it, which is why a callee that knows only
|
||||
;; (Fn [i32] i32) can still call it.
|
||||
;;
|
||||
;; What is not here is the escaping half — a value carrying an environment may
|
||||
;; not outlive the frame the copies are on, and fn-escape*.flan is where each
|
||||
;; of those refusals is written down.
|
||||
|
||||
(defn double [x i32] i32 (* x 2))
|
||||
|
||||
(defn apply2 [f (Fn [i32] i32) x i32] i32 (f x))
|
||||
(defn call0 [f (Fn [] i32)] i32 (f))
|
||||
(defn twice [f (Fn [i32] i32) x i32] i32 (f (f x)))
|
||||
|
||||
;; The copy is taken where the value is made and not where it is read, and
|
||||
;; this is what proves it: the local is changed *after* the fn value exists
|
||||
;; and before it is called, through a pointer, so nothing about the order can
|
||||
;; be an accident of evaluation.
|
||||
(defn bump-then-call [f (Fn [] i32) p (Ptr i32)] i32
|
||||
(set (deref p) 99)
|
||||
(f))
|
||||
|
||||
(defstruct Pt [x i32 y i32])
|
||||
|
||||
(defstruct TooBig [n i32])
|
||||
(defonce seen i32)
|
||||
|
||||
(defn checked [x i32] i32
|
||||
(when (> x 100) (signal (TooBig {.n x})))
|
||||
x)
|
||||
|
||||
;; A handler clause is lifted the same way and captures the same way, and is
|
||||
;; sound with nothing left over: a handler frame is popped by the body that
|
||||
;; pushed it, so the establishing frame is alive whenever the clause runs.
|
||||
;; [budget] is read out of the environment; the accumulator is a global,
|
||||
;; because a captured copy is a copy and a store into one would leave the
|
||||
;; local it came from as it was.
|
||||
(defn handles [] i32
|
||||
(let [budget 1000
|
||||
xs [5 200 7 300]
|
||||
s (slice xs 0 4)
|
||||
t 0]
|
||||
(handler-bind [(TooBig [c] (set seen (+ seen (+ budget (.n c)))))]
|
||||
(dotimes [i 4]
|
||||
(set t (+ t (checked (at s i))))))
|
||||
(print t) (print " ") (println seen)
|
||||
seen))
|
||||
|
||||
(defn main [] i32
|
||||
(let [n 7]
|
||||
(println (use (fn [] n))))
|
||||
;; The motivating program.
|
||||
(let [bonus 10]
|
||||
(println (apply2 (fn [x] (+ x bonus)) 5)))
|
||||
|
||||
;; Copy at creation: the fn answers 1 and the local is 99.
|
||||
(let [n 1]
|
||||
(print (bump-then-call (fn [] n) (addr n)))
|
||||
(print " ")
|
||||
(println n))
|
||||
|
||||
;; What may be captured. A string and a slice are two words copied as two
|
||||
;; words — the bytes stay whoever's they were, which is fine exactly while
|
||||
;; the value cannot outlive the frame that owns them. A struct and a fixed
|
||||
;; array are copied whole. A function value is copied as a function value.
|
||||
(let [s "hi"
|
||||
arr [1 2 3 4]
|
||||
sl (slice arr 0 4)
|
||||
p (Pt {.x 3 .y 4})
|
||||
g double]
|
||||
(println (call0 (fn [] (i32 (length s)))))
|
||||
(println (call0 (fn [] (at sl 2))))
|
||||
(println (call0 (fn [] (+ (.x p) (.y p)))))
|
||||
(println (call0 (fn [] (at arr 3))))
|
||||
(println (apply2 (fn [x] (g (+ x 1))) 4)))
|
||||
|
||||
;; An fn inside an fn, each capturing. The inner one names a local neither
|
||||
;; of them declared, so the outer one captures it too and the inner one
|
||||
;; copies the outer one's copy.
|
||||
(let [a 100
|
||||
b 20]
|
||||
(println (apply2 (fn [x] (+ x (call0 (fn [] (+ a b))))) 3)))
|
||||
|
||||
;; A loop variable: what the fn sees is the value at the iteration it was
|
||||
;; made on, not the last one. 0 + 1 + 2 + 3.
|
||||
(let [total 0]
|
||||
(dotimes [i 4]
|
||||
(set total (+ total (call0 (fn [] i)))))
|
||||
(println total))
|
||||
|
||||
;; And the same again where the loop variable is rebound by a recur rather
|
||||
;; than stepped by a dotimes, which is a store into the slot the copy is
|
||||
;; taken from: 100 + 101 + 102.
|
||||
(println
|
||||
(let [base 100]
|
||||
(loop [i 0 acc 0]
|
||||
(if (< i 3)
|
||||
(recur (+ i 1) (+ acc (call0 (fn [] (+ base i)))))
|
||||
acc))))
|
||||
|
||||
;; Called twice, so the environment is read more than once and a body that
|
||||
;; consumed it would show.
|
||||
(let [k 5]
|
||||
(println (twice (fn [x] (+ x k)) 1)))
|
||||
|
||||
;; An fn's own let may shadow a name the enclosing function also has, and a
|
||||
;; store into *that* one is an ordinary store: the refusal is about a
|
||||
;; captured copy and not about the spelling. 5 + 1.
|
||||
(let [n 5]
|
||||
(println (+ n (call0 (fn [] (let [n 0] (set n 1) n))))))
|
||||
|
||||
(println (handles))
|
||||
0)
|
||||
|
||||
10
test/programs/fn-cfn-captures.flan
Normal file
10
test/programs/fn-cfn-captures.flan
Normal file
@ -0,0 +1,10 @@
|
||||
;; A CFn is the bare address, so a literal written into one has nowhere to
|
||||
;; keep the copies. Refused with the name of what it captured, because that is
|
||||
;; the fact to act on, and with the fix named: widen the position to Fn, which
|
||||
;; is what the type is for.
|
||||
(defn apply-bare [f (CFn [i32] i32) x i32] i32 (f x))
|
||||
|
||||
(defn main [] i32
|
||||
(let [bonus 10]
|
||||
(println (apply-bare (fn [x] (+ x bonus)) 5)))
|
||||
0)
|
||||
13
test/programs/fn-cfn-narrow.flan
Normal file
13
test/programs/fn-cfn-narrow.flan
Normal file
@ -0,0 +1,13 @@
|
||||
;; Coercion between the two function types goes one way only. A (CFn ...)
|
||||
;; widens into a (Fn ...) through a per-signature thunk, and a (Fn ...) does
|
||||
;; not narrow: there is nowhere for the environment to go, and nothing at this
|
||||
;; definition can know whether there is one.
|
||||
;;
|
||||
;; Refused by the ordinary type message, which names both spellings and is the
|
||||
;; right sentence for it: the fix is to widen the position, not to convert the
|
||||
;; value.
|
||||
(defn apply-bare [f (CFn [i32] i32) x i32] i32 (f x))
|
||||
|
||||
(defn hand-on [f (Fn [i32] i32) x i32] i32 (apply-bare f x))
|
||||
|
||||
(defn main [] i32 0)
|
||||
67
test/programs/fn-cfn.flan
Normal file
67
test/programs/fn-cfn.flan
Normal file
@ -0,0 +1,67 @@
|
||||
;; The narrow function type. A (CFn [T ...] R) is the bare code address —
|
||||
;; one word, no environment, and therefore nothing that can capture. A
|
||||
;; (Fn [T ...] R) is that address and the environment beside it, two words.
|
||||
;;
|
||||
;; The reason there are two rather than one: an environment on every signature
|
||||
;; would tax every function in every program for a feature most of them never
|
||||
;; use. With CFn written where it is wanted, an ordinary defn emits exactly
|
||||
;; the signature it emitted before capture existed, and a call to it by name
|
||||
;; is byte-for-byte what it was.
|
||||
;;
|
||||
;; The C is information and not decoration. A value with no environment is the
|
||||
;; only kind that could ever cross to C, and under the --no-conditions
|
||||
;; direction FIX.org records — where a signature that cannot transfer drops
|
||||
;; the channel too — one becomes literally a C function pointer. It is not
|
||||
;; that today: a declare cannot take a function type at all, and the refusal
|
||||
;; it meets says so. The name points at what the type is, and at where it is
|
||||
;; going.
|
||||
;;
|
||||
;; **Nobody needs CFn.** An Fn accepts everything a CFn does, so the narrow
|
||||
;; one is reached for on purpose, for one of four reasons: handing a function
|
||||
;; to C, later; a table of bare addresses; forbidding capture at a boundary;
|
||||
;; and the one that is likeliest in practice — a *named* function handed to an
|
||||
;; Fn parameter goes through the widening thunk and pays an indirect hop per
|
||||
;; call, where a CFn parameter is a direct call. (map-in-place s double) is
|
||||
;; the example, and [apply-bare] below is it in miniature.
|
||||
;;
|
||||
;; Coercion is one-way. A defn's address and a non-capturing literal satisfy
|
||||
;; both. An Fn does not narrow to a CFn — there is nowhere for the
|
||||
;; environment to go — and fn-cfn-narrow.flan is that refusal.
|
||||
|
||||
(defn double [x i32] i32 (* x 2))
|
||||
(defn negate [x i32] i32 (- 0 x))
|
||||
|
||||
;; Taking the narrow one. Nothing that reaches here can carry an environment,
|
||||
;; which is what the signature is saying.
|
||||
(defn apply-bare [f (CFn [i32] i32) x i32] i32 (f x))
|
||||
|
||||
;; And the wide one, which is what almost every higher-order signature wants.
|
||||
(defn apply-any [f (Fn [i32] i32) x i32] i32 (f x))
|
||||
|
||||
;; A CFn returned. It is a link-time constant with nothing behind it, so
|
||||
;; handing one back is no different from handing it down — which is exactly
|
||||
;; what a capturing value cannot do.
|
||||
(defn pick [up bool] (CFn [i32] i32) (if up double negate))
|
||||
|
||||
;; A CFn parameter widened to an Fn at a call: the address goes where an
|
||||
;; environment would be and the thunk reads it back out. This is the hop the
|
||||
;; narrow type exists to avoid.
|
||||
(defn through [f (CFn [i32] i32) x i32] i32 (apply-any f x))
|
||||
|
||||
(defn main [] i32
|
||||
;; A name into a CFn, and into an Fn.
|
||||
(println (apply-bare double 4))
|
||||
(println (apply-any negate 4))
|
||||
;; A literal that captures nothing into a CFn.
|
||||
(println (apply-bare (fn [x] (+ x 1)) 4))
|
||||
;; And one that does capture, into an Fn.
|
||||
(let [k 10]
|
||||
(println (apply-any (fn [x] (+ x k)) 4)))
|
||||
;; A returned CFn, called through a computed head.
|
||||
(println ((pick true) 21))
|
||||
(println ((pick false) 21))
|
||||
;; The widening, twice over: a CFn local through a CFn parameter into
|
||||
;; an Fn parameter.
|
||||
(let [g double]
|
||||
(println (through g 5)))
|
||||
0)
|
||||
19
test/programs/fn-escape-copy.flan
Normal file
19
test/programs/fn-escape-copy.flan
Normal file
@ -0,0 +1,19 @@
|
||||
;; The hole a capture could otherwise be laundered through, and the reason
|
||||
;; "the result of a call is clean" is a rule and not a hope.
|
||||
;;
|
||||
;; [sneak]'s literal captures [g], so its body holds a *copy* of a function
|
||||
;; value that may itself carry an environment — and the copy is read out of an
|
||||
;; environment, which is the one aggregate a function value is ever stored in.
|
||||
;; If a copy read back out were treated as clean, the literal could return it,
|
||||
;; the return would arrive at [sneak]'s caller as an ordinary call result, and
|
||||
;; a capturing value would be out of the frame that owns it with nothing
|
||||
;; having refused anything.
|
||||
;;
|
||||
;; So a function value read out of a struct, a case or a pointer is suspect,
|
||||
;; and the refusal lands inside the lifted body where the return is written.
|
||||
(defn getf [f (Fn [] (Fn [] i32))] (Fn [] i32) (f))
|
||||
|
||||
(defn sneak [g (Fn [] i32)] (Fn [] i32)
|
||||
(getf (fn [] g)))
|
||||
|
||||
(defn main [] i32 0)
|
||||
12
test/programs/fn-escape-handled.flan
Normal file
12
test/programs/fn-escape-handled.flan
Normal file
@ -0,0 +1,12 @@
|
||||
;; A handler-bind is an expression and its value is its body's, so it is a way
|
||||
;; for a function value to be a function's answer — and it would have walked
|
||||
;; straight past a check that only looked at [return] and at the last form of
|
||||
;; a block. with-allocator and restart-case are the same shape and are checked
|
||||
;; the same way.
|
||||
(defstruct C [id i32])
|
||||
(defonce seen i32)
|
||||
|
||||
(defn keep [f (Fn [] i32)] (Fn [] i32)
|
||||
(handler-bind [(C [c] (set seen (.id c)))] f))
|
||||
|
||||
(defn main [] i32 0)
|
||||
11
test/programs/fn-escape-param.flan
Normal file
11
test/programs/fn-escape-param.flan
Normal file
@ -0,0 +1,11 @@
|
||||
;; The hard case, answered without looking at a single call site: a function
|
||||
;; value that arrives as a parameter may carry an environment on its caller's
|
||||
;; frame, so a function that *stores* one is refused where it is written.
|
||||
;;
|
||||
;; That is what makes passing a capturing fn down safe everywhere — no callee
|
||||
;; can keep it — and it is also the conservative half: this particular [keep]
|
||||
;; would be harmless for a caller that passed a name, and there is no way for
|
||||
;; the definition to know that it did.
|
||||
(defn keep [f (Fn [] i32)] (Fn [] i32) f)
|
||||
|
||||
(defn main [] i32 (println ((keep (fn [] 1)))) 0)
|
||||
10
test/programs/fn-escape-return.flan
Normal file
10
test/programs/fn-escape-return.flan
Normal file
@ -0,0 +1,10 @@
|
||||
;; The refusal that defines "non-escaping". The copies live in a slot of
|
||||
;; [make]'s frame, and the value would still be pointing at them after that
|
||||
;; frame has gone.
|
||||
;;
|
||||
;; A returned function value is still fine when it captures nothing —
|
||||
;; fn-values.flan returns one — so this is about the environment and not about
|
||||
;; the shape of the value.
|
||||
(defn make [n i32] (Fn [] i32) (fn [] n))
|
||||
|
||||
(defn main [] i32 (println ((make 3))) 0)
|
||||
7
test/programs/fn-escape-store.flan
Normal file
7
test/programs/fn-escape-store.flan
Normal file
@ -0,0 +1,7 @@
|
||||
;; A store through a pointer is the same escape wearing a different hat: the
|
||||
;; pointer names storage this frame does not own, so the value would outlive
|
||||
;; the environment it carries.
|
||||
(defn stash [p (Ptr (Fn [] i32)) f (Fn [] i32)] ()
|
||||
(set (deref p) f))
|
||||
|
||||
(defn main [] i32 0)
|
||||
9
test/programs/fn-escape-vec.flan
Normal file
9
test/programs/fn-escape-vec.flan
Normal file
@ -0,0 +1,9 @@
|
||||
;; A Vec's elements are in a block the allocator owns and the frame does not,
|
||||
;; so a function value pushed into one outlives whatever environment it
|
||||
;; carries. Refused for that, and not for the shape of the element type: a Vec
|
||||
;; of function values is a perfectly good thing to want, and is what case 3
|
||||
;; is for.
|
||||
(defn stash [v (Vec (Fn [] i32)) f (Fn [] i32)] ()
|
||||
(push v f))
|
||||
|
||||
(defn main [] i32 0)
|
||||
@ -1,6 +1,8 @@
|
||||
;; 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
|
||||
;; function's emitted signature ends with the environment and the transfer
|
||||
;; channel and a C one does not, so nothing could call the resulting pointer
|
||||
;; correctly — and the gap is wider since capture arrived, because a Flan
|
||||
;; function value is two words and a C symbol is one — 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.
|
||||
|
||||
@ -4,6 +4,17 @@
|
||||
;; 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).
|
||||
;;
|
||||
;; Capture sharpened the reason behind this one without changing it. A struct
|
||||
;; outlives the frame it was built on, so a field could not hold a value
|
||||
;; carrying an environment either — see fn-escape-*.flan. The zero is still
|
||||
;; what the message names, because it is the objection that applies to every
|
||||
;; function value and not only to a capturing one.
|
||||
;;
|
||||
;; Which means a (CFn ...) field is refused too, and for the zero alone —
|
||||
;; a table of function pointers is exactly what that type is for, and nothing
|
||||
;; about capture stands in its way. An (Option (CFn ...)) field is already
|
||||
;; legal and is the shape that works; FIX.org carries the rest as its own item.
|
||||
(defstruct Ops [run (Fn [i32] i32)])
|
||||
|
||||
(defn main [] i32 0)
|
||||
|
||||
@ -2,6 +2,10 @@
|
||||
;; 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.
|
||||
;;
|
||||
;; The one thing capture did not change. It is about where the *types* come
|
||||
;; from and not about what the body may see, so an fn is still written where
|
||||
;; something says what it takes.
|
||||
(defn main [] i32
|
||||
(let [f (fn [x] (* x 2))]
|
||||
(println (f 3)))
|
||||
|
||||
@ -1,5 +1,13 @@
|
||||
;; Function values, the non-escaping kind: a code address and no environment
|
||||
;; beside it. Capture does not exist, so nothing here can outlive anything.
|
||||
;; Function values, and specifically the ones with no environment. Nothing
|
||||
;; here captures, which is what makes every one of these safe to return and to
|
||||
;; hand around — fn-capture.flan is the other half, and fn-escape-*.flan is
|
||||
;; the line between them.
|
||||
;;
|
||||
;; Every signature below says (Fn ...), which is the wide one: it admits a
|
||||
;; capturing value and so pays for a two-word value and a widening thunk where
|
||||
;; a name is handed to it. Written as (CFn ...) these would pay neither, and
|
||||
;; fn-cfn.flan is where that is spelled out — the spellings are kept apart
|
||||
;; here so that the two programs cover the two conventions between them.
|
||||
;;
|
||||
;; This is a Lisp-1 — one top-level namespace, enforced — so a bare function
|
||||
;; name *is* the function and there is no #' to write.
|
||||
|
||||
@ -43,7 +43,10 @@
|
||||
/* The trailing ptr is the transfer channel spec-conditions.md §6 puts in every
|
||||
* Flan signature. This host never transfers, so it passes a slot of its own
|
||||
* that stays null — but the parameter is not optional: getting it wrong reads
|
||||
* garbage as the channel and fails nowhere near here. */
|
||||
* garbage as the channel and fails nowhere near here.
|
||||
*
|
||||
* No environment: [outer] is called by name and not through a function value,
|
||||
* so it declares none. That is the point of there being two function types. */
|
||||
extern int64_t flan_outer(void *xfer) __asm__("flan.outer");
|
||||
extern int64_t flan_counter __asm__("flan.counter");
|
||||
|
||||
|
||||
@ -3928,12 +3928,78 @@ level "1"
|
||||
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";
|
||||
(* Capture by value into a stack environment — spec-memory.md's case 2.
|
||||
Three opt levels for the reason the case above has them, and for one
|
||||
more: the environment is a struct in the frame and the value carries
|
||||
its address, which is exactly the shape -O2 is entitled to make
|
||||
disappear. -O0 is what proves there is a real store and a real load
|
||||
behind it. A dev build is here because the value's code half still
|
||||
comes out of the indirection cell and the environment half must not
|
||||
have disturbed that.
|
||||
|
||||
The two lines worth naming. "1 99" is copy-at-creation: the local is
|
||||
changed through a pointer after the value exists and before it is
|
||||
called, so no evaluation order can account for the fn still answering
|
||||
1. And "512 2500" is the handler clause reading a captured budget,
|
||||
which is the same machinery in the one place where there is no
|
||||
escaping case left over. *)
|
||||
let fn_capture_out =
|
||||
"15\n1 99\n2\n3\n7\n4\n10\n123\n6\n303\n11\n6\n512 2500\n2500\n"
|
||||
in
|
||||
outputs "an fn capturing by value" "programs/fn-capture.flan"
|
||||
fn_capture_out;
|
||||
outputs ~opt:"-O0" "an fn capturing by value, -O0" "programs/fn-capture.flan"
|
||||
fn_capture_out;
|
||||
(* The narrow function type, and the one-way coercion. What this asserts
|
||||
that no checker test can: a CFn widened into an Fn and called through
|
||||
the wider signature reaches the same body and answers the same thing,
|
||||
on both opt levels — so the null environment a widening pairs with the
|
||||
address really is ignored by a body that declared none. *)
|
||||
let fn_ptr_out = "8\n-4\n5\n14\n42\n-21\n10\n" in
|
||||
outputs "the two function types" "programs/fn-cfn.flan" fn_ptr_out;
|
||||
outputs ~opt:"-O0" "the two function types, -O0" "programs/fn-cfn.flan"
|
||||
fn_ptr_out;
|
||||
(* And a dev build, which is the one that exercises the widening thunk
|
||||
over an indirection cell: a name widened into an Fn is a cell load for
|
||||
the address and the thunk for the call, and the two have to compose. *)
|
||||
outputs ~dev:true "the two function types, dev" "programs/fn-cfn.flan"
|
||||
fn_ptr_out;
|
||||
outputs ~dev:true "an fn capturing by value, dev" "programs/fn-capture.flan"
|
||||
fn_capture_out;
|
||||
|
||||
(* What function values do *not* include, each refused by name. Escape is
|
||||
the headline now that capture is not: the copies live in the frame the
|
||||
literal was written in, so a value carrying their address may be
|
||||
called, passed down and copied about, and may not outlive that frame.
|
||||
Each of these names case 3 — the collector-allocated environment —
|
||||
because "not yet" is the true sentence. *)
|
||||
refuses "a captured fn cannot be returned" "programs/fn-escape-return.flan"
|
||||
"a return would outlive the frame";
|
||||
refuses "a function value parameter cannot be kept"
|
||||
"programs/fn-escape-param.flan" "may carry an environment";
|
||||
refuses "a function value cannot be stored through a pointer"
|
||||
"programs/fn-escape-store.flan" "a store would outlive the frame";
|
||||
refuses "a function value cannot be pushed into a Vec"
|
||||
"programs/fn-escape-vec.flan" "a container would outlive the frame";
|
||||
(* The two an escape check written by eye would have missed. A function
|
||||
value read back out of an environment is a copy of something that may
|
||||
carry one, and a handler-bind is an expression whose value is its
|
||||
body's — so both are ways for a suspect to be a function's answer. *)
|
||||
refuses "a captured function value cannot be handed back"
|
||||
"programs/fn-escape-copy.flan" "a return would outlive the frame";
|
||||
refuses "a handler-bind's value is a return too"
|
||||
"programs/fn-escape-handled.flan" "a return would outlive the frame";
|
||||
refuses "a captured local is a copy and cannot be assigned"
|
||||
"programs/fn-capture-set.flan" "cannot assign to n";
|
||||
(* The two function types, and the line between them. A CFn is the bare
|
||||
address, so nothing that captures can be one and nothing that may
|
||||
capture can narrow into one. *)
|
||||
refuses "an fn that captures is not a CFn"
|
||||
"programs/fn-cfn-captures.flan" "and not a (CFn [i32] i32)";
|
||||
refuses "an Fn does not narrow to a CFn"
|
||||
"programs/fn-cfn-narrow.flan" "expected (CFn [i32] i32)";
|
||||
refuses "an fn cannot capture a dyn" "programs/fn-capture-dyn.flan"
|
||||
"the collector finds its roots by frame";
|
||||
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"
|
||||
|
||||
@ -473,7 +473,7 @@ let () =
|
||||
(match ty "(Map string i32)" with
|
||||
| Tapp ("Map", [ _; _ ]) -> () | _ -> check "(Map K V) is a map type" false);
|
||||
(match ty "(Fn [a a] bool)" with
|
||||
| Tfn ([ _; _ ], _) -> () | _ -> check "(Fn [T] R)" false);
|
||||
| Tfn (_, [ _; _ ], _) -> () | _ -> check "(Fn [T] R)" false);
|
||||
|
||||
(* ── Declarations ──────────────────────────────────────────────── *)
|
||||
(match (parse_decl "(defn f [x i32] bool x)").d with
|
||||
@ -3668,14 +3668,21 @@ let () =
|
||||
rejects_check "signal in value position"
|
||||
"(defstruct C [id i32])\n\
|
||||
(defn f [] i32 (signal (C {.id 1})))" ~needle:"expected i32";
|
||||
(* A handler is lifted into a function of its own, so the establishing
|
||||
function's locals are not there. Capturing them is a closure, which is
|
||||
milestone 5 — until then it is refused for the reason it is refused for
|
||||
rather than as an unknown name. *)
|
||||
rejects_check "a handler capturing a local"
|
||||
(* A handler clause captures the establishing function's locals by value —
|
||||
spec-memory.md's case 2 — so it can read one. A *store* is the thing that
|
||||
is not there: the clause holds a copy, and writing to it would leave the
|
||||
local it came from as it was, which is a silent disagreement and not a
|
||||
feature. Refused for that reason, with the accumulation case pointed at a
|
||||
global. *)
|
||||
accepts "a handler reading a local"
|
||||
"(defstruct C [id i32])\n\
|
||||
(defonce seen i32)\n\
|
||||
(defn f [] () (let [n 7] (handler-bind [(C [c] (set seen (+ n (.id c))))] \
|
||||
(signal (C {.id 2})))))";
|
||||
rejects_check "a handler assigning to a captured local"
|
||||
"(defstruct C [id i32])\n\
|
||||
(defn f [] () (let [n 0] (handler-bind [(C [c] (set n 1))] (signal (C {.id 2})))))"
|
||||
~needle:"a handler cannot see n";
|
||||
~needle:"a handler cannot assign to n";
|
||||
(* The frames are popped on the way out of the body, so an early exit would
|
||||
leave them on the stack pointing into a function that has gone. *)
|
||||
rejects_check "return inside handler-bind"
|
||||
@ -3761,14 +3768,18 @@ let () =
|
||||
accepts "handler-case with several clauses"
|
||||
(boom ^ "(defn f [] i32 (handler-case 1 [(Boom [c] (.id c)) \
|
||||
(Dud [c] (+ 1 (.id c)))]))");
|
||||
(* The whole difference from handler-bind: a clause runs at the form, in the
|
||||
function that wrote it, so it sees that function's locals. The same body
|
||||
under a handler-bind is refused by name. *)
|
||||
(* The difference from handler-bind is narrower than it was. Both see the
|
||||
establishing function's locals now — a handler-case clause *is* that
|
||||
function, and a handler-bind clause captures them by value. What only a
|
||||
handler-case clause can do is *assign* to one, because it is not holding
|
||||
a copy. *)
|
||||
accepts "a handler-case clause sees the establishing function's locals"
|
||||
(boom ^ "(defn f [] i32 (let [n 1] (handler-case 0 [(Boom [c] n)])))");
|
||||
accepts "and may assign to one, which a handler-bind clause may not"
|
||||
(boom ^ "(defn f [] i32 (let [n 1] (handler-case 0 [(Boom [c] (set n 2) n)])))");
|
||||
rejects_check "a handler-bind clause still cannot"
|
||||
(boom ^ "(defn f [] i32 (let [n 1] (handler-bind [(Boom [c] (set n 2))] 0)))")
|
||||
~needle:"a handler cannot see n — it is a local of the enclosing function";
|
||||
~needle:"a handler cannot assign to n";
|
||||
(* Nothing static refuses a condition no clause lists: it installs no frame
|
||||
that matches, so it goes past untouched and the body carries on. *)
|
||||
accepts "a condition no clause lists"
|
||||
|
||||
@ -1305,7 +1305,7 @@ let () =
|
||||
if Session.strip_rebind "~2" <> "~2" then fail "a name that is only a suffix was stripped";
|
||||
(let fn snames : Tast.fn =
|
||||
{ Tast.name = "f"; params = []; ret = Types.Unit; body = [];
|
||||
fdefers = []; fparent = None; floc = Loc.unknown;
|
||||
fdefers = []; fenv = None; fparent = None; floc = Loc.unknown;
|
||||
slots = Array.make (Array.length snames) (Types.Int Types.I32);
|
||||
snames }
|
||||
in
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user