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.
68 lines
3.2 KiB
Plaintext
68 lines
3.2 KiB
Plaintext
;; 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)
|