A function that captures and a function that cannot are two types

# Conflicts:
#	FIX.org
This commit is contained in:
Joseph Ferano 2026-09-21 16:17:10 +07:00
commit ca237adb22
44 changed files with 2476 additions and 252 deletions

95
FIX.org
View File

@ -6212,5 +6212,96 @@ already call. No new path, no polling.
- A data constructor is [Type.Case] and is one symbol. The type half is drawn - A data constructor is [Type.Case] and is one symbol. The type half is drawn
and the case half is not: the daemon answers with the type's name and knows and the case half is not: the daemon answers with the type's name and knows
nothing of its cases. nothing of its cases.
- [CFn] does not exist anywhere in the tree. [Fn] does — [(Fn [T ...] R)], - [CFn] did not exist anywhere in the tree when this pass ran. [Fn] did —
parse.ml:99 — and is in the type rule. Nothing was added for the other. [(Fn [T ...] R)], parse.ml:99 — and is in the type rule. Nothing was added
for the other. The closure lane landed [CFn] the same day, so the type rule
wants it adding.
* Closures, 2026-09-21 — two rulings, and what this lane built
Two rulings, both in the author's words.
The first, on what to build:
"do both, capture by value and handle escaping closures, allocated on the
GC side"
This lane is the first half: capture by value into a stack environment,
spec-memory.md's case 2, non-escaping only. The second half — an environment
the collector allocates, and with it the escaping closure — is a separate lane,
and every refusal this one prints names it.
The second, on the calling convention, after the first design put an
environment parameter on every Flan signature:
"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"
So there are two function types, Rust's and Swift's shape:
(Fn [T ...] R) captures; {code, env}; the common case, short name
(CFn [T ...] R) the bare address; one word; cannot capture
** The name, ruled 2026-09-21
CFn, because the C carries information rather than being decoration: a value
with no environment is the only kind that could ever cross to C, and under the
--no-conditions direction below it becomes literally a C function pointer. The
name points at what the type is and at where it is going.
Rejected: Closure, too long. Proc, because "procedure" is a word we disagree
with Odin about. Fun and Func, because beside Fn they differ only in length,
so nothing tells a reader which one captures. Fnptr, as ugly.
The thing to be careful about, and it is written into the crossable refusal so
a reader meets it where they would otherwise be misled: CFn is not the type
for C interop *today*. A declare cannot take a function type at all, because a
Flan signature ends with the transfer channel.
** Nobody needs CFn, and one of the four reasons is the common one
An Fn accepts everything a CFn does, so the narrow one is always reached for
on purpose:
1. handing a function to C — later, per the item below;
2. a table of bare addresses;
3. forbidding capture at a boundary, where the type is the statement;
4. performance, which is likeliest in practice. A *named* function passed to
an Fn parameter goes through the widening thunk and pays an indirect hop
per call; a CFn parameter is a direct call. (map-in-place s double) is the
example. The prelude's four stay Fn on purpose — a capturing predicate is
what people want — so this is the cost someone would opt out of by writing
their own signature, not one the prelude should have avoided.
** What the escaping lane inherits, and what it changes
Unchanged by it: a (Fn ...) is two words; the environment is the last
parameter; it 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. An ordinary defn declares none and is byte-for-byte what it was.
Changed by it: where the environment points. It is a Make into a frame slot
today and becomes a collector allocation; Check.escape_check goes away with it,
and with it the refusals on returning, storing, pointing at and pushing a
capturing value.
One thing for the collector to know now rather than discover: a widening
thunk's environment holds a *code pointer*, not a frame address and not a GC
object. When env becomes collector-allocated the marker needs a way not to
follow a thunk's.
Also still refused and belonging to the second half: capturing a dyn. The
struct dyn field is no longer refused — per-type descriptors landed — but a
synthesised environment has no descriptor, so the refusal stands until it does.
** Recorded, not built: CFn and C's convention
A CFn is one word and is the right shape for a C callback, and it is still
not one: a Flan function's signature ends with the transfer channel and a C
caller knows nothing about one. Under a future --no-conditions flag a CFn
signature could drop the channel and reach C's exact convention, which is the
direction the author is interested in. The refusal in [crossable] names it.
** Also worth an item: CFn in a struct or a fixed array
no_zeroed_fn refuses a function value in any position ZII would conjure one,
and it refuses a CFn for the same reason it refuses an Fn: a zeroed function
value is a null pointer, which is the one zero that is not a value the type can
have. But "a table of function pointers" is exactly what CFn is for, and that
objection is about ZII rather than about capture — an (Option (CFn ...)) field
is already legal and is the shape that works. Its own item.

View File

@ -1861,15 +1861,17 @@ stack does *not* observe a redefinition of its own clause; the next entry to the
Which gives the two refusals, both by the house rule rather than by accident: Which gives the two refusals, both by the house rule rather than by accident:
- **A handler cannot see the establishing function's locals.** That is a closure with an explicit environment, so a - **A handler cannot see the establishing function's locals.** *Superseded: it can, by value — see "Capture by value"
reference to one is refused *for that reason* rather than reported as an unknown name. Globals and the condition are in below. What is still refused is a `set` into one, because a captured name is a copy.* The original note read: that is a
scope, which is what the accumulation case needs. closure with an explicit environment, so a reference to one is refused *for that reason* rather than reported as an
unknown name; globals and the condition are in scope, which is what the accumulation case needs.
What it needs is narrower than it looks, and worth getting right before anyone schedules it: a handler frame does not The reading that turned out to be right: a handler frame does not outlive the function that established it, so this is
outlive the function that established it, so this is spec-memory.md's **case 2** — a non-escaping `fn` capturing by spec-memory.md's **case 2** — a non-escaping `fn` capturing by value into a stack environment — and *not* the escaping
value into a stack environment — and *not* the escaping closure that plan.org's open decision #5 defers until a concrete closure that plan.org's open decision #5 defers until a concrete use case. Case 2 was settled, was built, and brought
use case. Case 2 is settled, and #5 says in as many words that without it "conditions are not worth building". So the the handler along with it for one struct field and one argument. #5 says in as many words that without it "conditions
biggest usability limit in conditions is not behind the thing that was just deferred. are not worth building", and the biggest usability limit in conditions was indeed never behind the thing that was
deferred.
- **`return` inside a `handler-bind` body is refused.** The frames are popped on the way out and an early exit would - **`return` inside a `handler-bind` body is refused.** The frames are popped on the way out and an early exit would
leave them on the stack pointing into a function that has gone. Same shape as `defer` inside a block. leave them on the stack pointing into a function that has gone. Same shape as `defer` inside a block.
@ -2702,9 +2704,11 @@ spec together; neither is a cleanup, and until one is taken the word is carried
`(Map K V)` is built — see below. What is left: `drop` and with it the transitive move-only rule, recursive teardown, `(Map K V)` is built — see below. What is left: `drop` and with it the transitive move-only rule, recursive teardown,
and the refusal to construct a drop-carrying container against an allocator without `can-free`; `(Result T E)` and and the refusal to construct a drop-carrying container against an allocator without `can-free`; `(Result T E)` and
`try`; generics; the macro expander. `try`; generics; the macro expander.
And the **accumulation pattern** — `(fn [c] (push errors c) ...)` over an enclosing Vec — which `Vec` does not buy: And the **accumulation pattern** — `(fn [c] (push errors c) ...)` over an enclosing Vec — which `Vec` does not buy.
capture does not exist at all, and the spec's captured-`Vec`-by-pointer rule has never had to exist because every Capture exists now (see "Capture by value" below) and this is still not it: capture is by *value*, so the `fn` would
capturable type today is a value type. It is its own item and should be planned as one. push into its own copy of the header and leave the enclosing one at the length it had. The spec's
captured-`Vec`-by-pointer rule is exactly the thing that has still never had to exist. It is its own item and should
be planned as one.
## `(Map K V)`, which is Odin's map ## `(Map K V)`, which is Odin's map
@ -3817,9 +3821,10 @@ cannot share a name** — which is exactly what makes the bare name safe to read
`double` it could have meant instead, so the sharp quote would be punctuation answering a question the language does `double` it could have meant instead, so the sharp quote would be punctuation answering a question the language does
not ask. not ask.
A `Types.Fn` is one pointer. There is no environment beside it, so the type resolves to `ptr` and lays out as eight A `Types.Fn` was one pointer when this lane landed. It is two words since capture, and the one-word version has a
bytes, and a call through one is byte-for-byte the call a name would have produced — a Flan function's emitted name of its own now — `(CFn [T ...] R)`; see "Capture by value" below. A call through either is the call a name
signature is its parameters followed by the transfer channel whether it was reached by name or by pointer. That is would have produced, with the environment appended for a `Fn`: a Flan function's emitted signature is its
parameters, then the transfer channel, and then the environment on the bodies that can be reached that way. That is
why a handler established across a `fold` still catches a signal raised by the function the fold was handed: why a handler established across a `fold` still catches a signal raised by the function the fold was handed:
`programs/fn-values.flan` does exactly that, and it is the case that would fail if an indirect call skipped the `programs/fn-values.flan` does exactly that, and it is the case that would fail if an indirect call skipped the
guard. guard.
@ -3840,11 +3845,8 @@ implemented.
**Refused, each with its own reason and its own program:** **Refused, each with its own reason and its own program:**
- **Capture does not exist** (`fn-capture.flan`). An `fn` is lifted into a function of its own and handed nothing but - **Capture does not exist.** *Superseded — see "Capture by value" below. It exists, the program that was this
its parameters; a reference to a local of the enclosing function is refused by name. This is the same refusal a refusal's witness now runs, and what is refused in its place is the **escape**.*
handler clause has always carried, and the two now share one message with the construct's name in it.
`spec-memory.md`'s capture cases, and **escaping closures with them, stay deferred** — deliberately, and this is
what keeps a function value a bare code address that cannot outlive anything.
- **An `fn` with nothing to say what it takes** (`fn-no-type.flan`), above. - **An `fn` with nothing to say what it takes** (`fn-no-type.flan`), above.
- **A position that would zero one** (`fn-in-struct.flan`): a struct field, a global, a fixed array's element, - **A position that would zero one** (`fn-in-struct.flan`): a struct field, a global, a fixed array's element,
`(zeroed)`. ZII fills an omitted field with all-bytes-zero, and **a zeroed function value is a null pointer, which `(zeroed)`. ZII fills an omitted field with all-bytes-zero, and **a zeroed function value is a null pointer, which
@ -3855,9 +3857,314 @@ implemented.
past its length and `flan_map_alloc` zeroes only the hash run, so neither conjures an element nobody pushed or past its length and `flan_map_alloc` zeroes only the hash run, so neither conjures an element nobody pushed or
put. A function value as a map *key* is refused already, by `Types.keyable` — hashing an address is a different put. A function value as a map *key* is refused already, by `Types.keyable` — hashing an address is a different
operation from hashing what it points at. operation from hashing what it points at.
- **A foreign function's address** (`fn-extern.flan`). A Flan function's signature ends with the transfer channel and - **A foreign function's address** (`fn-extern.flan`). A Flan function's signature ends with the environment and the
a C one does not, and an aggregate crossing the boundary is flattened by a generated shim the raw symbol knows transfer channel and a C one does not, and an aggregate crossing the boundary is flattened by a generated shim the
nothing about. Wrap it in a `defn` and pass that. raw symbol knows nothing about. Wrap it in a `defn` and pass that. Capture widened this gap rather than closing
it: a Flan function value is now two words and a C symbol is one.
## Capture by value, and what "non-escaping" had to mean
`spec-memory.md`'s **case 2**, which the section above listed as the headline refusal and which is now the headline
feature. This compiles:
```
(let [bonus 10]
(apply2 (fn [x] (+ x bonus)) 5))
```
`bonus` is **copied** into an environment on the enclosing function's frame at the instant the `fn` value is made,
and the lifted body reads the copy. Not a reference: `fn-capture.flan` changes the local through a pointer *after*
the value exists and *before* it is called, and the `fn` still answers with the old one. That test is the whole
claim, and it is the one no evaluation order can fake.
### Two function types, because the static side does not pay for the dynamic side
```
(Fn [i32] i32) ; captures; {code, env}; the common case
(CFn [i32] i32) ; the bare code address; one word; cannot capture
```
The forcing constraint first. A callee that takes a `(Fn [i32] i32)` and calls it knows nothing about where the
value came from — `fold` is handed a value and calls it — so **the environment has to travel with the value** or
there is nowhere to put it. A `Fn` is therefore `{code, env}`: sixteen bytes, classified as an aggregate in both
backends exactly as a slice is.
The first design put an environment parameter on **every** Flan signature, uniform for the reason the transfer
channel is uniform. The author ruled against it, and the ruling is the principle rather than the case: *"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`, then we should be operating under Odin/C semantics and never paying any runtime
costs."* A uniform environment taxes every function in every program for a feature most of them never use.
So there are two types, Rust's and Swift's shape. `Fn` keeps the short name because it is what almost every
higher-order signature wants; `CFn` is the narrow one.
**The `C` is information and not decoration**, which is what settled the name. 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 transfer channel too — one becomes literally a C function pointer. The name points at
what the type *is* and at where it is going. `Closure` was rejected as too long; `Proc` because "procedure" is a
word this language disagrees with Odin about; `Fun` and `Func` because beside `Fn` they differ only in length, so
nothing tells a reader which one captures; `Fnptr` as ugly.
**It is not a capability today, and the diagnostic says so.** A `declare` cannot take a function type at all — a
Flan signature ends with the transfer channel and a C caller knows nothing about one — so anyone reaching for `CFn`
straight after writing a `declare-c` is reaching too early. `crossable`'s refusal names that in as many words: *"the
C in CFn is about having no environment, which is what a C function pointer would need, and not about crossing
today."*
**And nobody ever needs it.** `Fn` accepts everything a `CFn` does, so the narrow one is reached for on purpose, for
one of four reasons:
1. handing a function to C — later, as above;
2. a table of bare addresses;
3. forbidding capture at a boundary, where the type is the statement;
4. **performance, which is likeliest in practice.** A *named* function passed to an `Fn` parameter goes through the
widening thunk and pays an indirect hop per call; a `CFn` parameter is a direct call. `(map-in-place s double)`
is the example — and the prelude's four stay `Fn`, because a capturing predicate is exactly what people want.
**An ordinary `defn` keeps its exact signature.** Verified rather than asserted: the LLVM for `calc-me.flan` and
fourteen corpus programs was diffed against the same compiler without this lane. Exactly three kinds of difference
appear, and no fourth:
- two type declarations in the preamble — `%fnv`, and `%handler`'s new `env` field;
- the prelude's `sort-by-slice-u8`, whose *parameter* is now `%fnv` because it is declared `(Fn [$t $t] bool)` and
pays two words for the value it asked for; and the lifted literal it is handed, which gains a trailing `ptr %env`
because an `Fn` value can reach it;
- in a program with a `handler-bind`, its clauses gain the same trailing `ptr %env` — every clause declares one,
see below.
**No ordinary `defn` gained a parameter, in any program.** `flan_rt.c`'s hash and equality typedefs are untouched;
`main`, the macro thunk, the startup call and the reload thunk emit the calls they always emitted.
**`handler-bind` is not free, and rounding it to zero would be wrong.** A program that establishes a handler and
captures nothing still pays: `%handler` went from 24 bytes to 32, every push writes an unconditional null into the
new field, every clause gains `ptr %env` plus an alloca and a store at entry, and `flan_signal` passes one more
argument per dispatch. Measured on `loops.flan`: +20 changed lines of x86. It is small and it is real, and every
program with conditions in it pays it — the alternative was a second clause convention beside the capturing one,
which `flan_signal` could not choose between because it calls through one C function-pointer type and cannot know
which clause matched.
### The environment is the last argument, on exactly the bodies an `Fn` can reach
The environment is the last parameter, after the transfer channel, and it is declared by **exactly the bodies a
`(Fn ...)` value can reach**: 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 below, which exist to read it. Nothing else declares it, which is where an ordinary `defn` keeps
costing nothing.
So **every indirect call is exactly typed** and nowhere does a caller pass an argument the callee did not declare.
That was not the first attempt. The first put the environment last and let a body that never asked for one simply
ignore the register it arrived in — legal under SysV, where argument N is classified from arguments 1..N alone, and
exactly **Swift's thin-vs-thick convention**, where a thin function converts to a thick one by pairing with a null
context the thin body ignores. It works on x86-64 and it is dead on **wasm32**, where `call_indirect` compares the
signature at the call site: a spare argument is a trap, not a register nobody reads. `test_web`'s two cases and the
headless `sand` build failed with *"null function or function signature mismatch"*, and the honest reading is that
being exactly typed is checkable by a verifier rather than argued from a calling convention, which is the better
property to have wanted.
**So there is one adapter, and it is per *signature* rather than per function.** `CFn` → `Fn` is `Tast.Thicken`,
and the pair it builds is `{thunk, the address}`: the thunk's code, with the bare address stored where an
environment would be. The thunk — `thick/<mangled signature>`, minted and memoised by the checker the way
`struct_key_pair` mints a map's hash and equality pair — declares the environment, reads the address back out of it,
and calls through it. One small function per distinct shape a program widens, not per function it widens, and it
handles the dynamic case (a `CFn`-typed local or parameter widened at a call) with the same mechanism as the
static one.
**What it costs, plainly.** A *name* handed to an `Fn`-typed parameter now pays one indirect hop per call:
`(map-in-place s double)` goes through the thunk per element where it used to reach `double` directly. A literal
pays nothing — capturing or not, it is compiled to take an environment and needs no thunk. The escape hatch is
writing `CFn` in the signature, which is what the type is for; the prelude's `map`, `filter`, `reduce` and
`sort-by` correctly stay `Fn`, because a capturing comparator is exactly what people want, so the common
named-function case does pay. That is the one real price of two types, and it buys every function in every program
not paying for an environment it never has.
A redefinition module carries its own copy of every thunk, hidden. A module that widens a name refers to one, and
the host has no cell for it to be reached through — the same shape of bug as the `Fnval` cell below, found the same
way and closed before it shipped. `flan reload` with a body that widens a name builds on both backends.
**The memo is keyed on the types and the symbol is a counter**, which review found the hard way. Keyed on a *name*
derived from `mangle_ty` it was not a memo but a collision: that function flattens a whole signature into one
hyphen-joined string, so `(CFn [(Ptr i32)] i32)` and `(CFn [ptr i32] i32)` — the second over a struct someone
called `ptr` — flatten alike, and the second widening silently reused the first's thunk at the wrong arity. Both
backends compiled it without a word and neither ran it. `fn-thunk-share.flan` is that exact pair, and it prints 5
and 17. (`mangle_ty`'s ambiguity is older than this lane and is still there for the generic instantiation names it
was written for; at one type's granularity it is hard to reach, at a whole signature's it is a line of Flan away.)
**A generic that binds its variable *through* a function type needed both halves.** `(defn apply2 [f (Fn [$t] $t)
x $t] …)` called as `(apply2 bump 1)` is the shape a bare name has to reach now that a `defn`'s address carries
`CFn`, and it broke in two places that fail apart: `bind_ty` had no arm admitting a `CFn` argument at an `Fn`
pattern (so the instantiation was refused outright — a regression against a program that compiled before this
lane), and `generic_call`'s catch-up pass did not widen it (so the call site handed one word to an instance
declaring two). A parameter that still mentions a variable is checked with *no expectation*, by design — there is
nothing to expect until the argument has spoken — so `expect`, where the widening otherwise lives, never sees the
pair. The same gap hid the new half: a `CFn` argument at a `(CFn [$t] $t)` parameter had no arm either and fell
through to plain equality. `fn-generic.flan` covers both.
The corpus missed all of it, and the reason is worth keeping: the prelude's higher-order functions bind `$t` from
an *earlier* argument, so `subst_ty` has already made the parameter concrete by the time `bind_ty` sees it.
`(map-in-place s double)` and `higher-order.flan` really were still working.
The reverse coercion does not exist — there is nowhere for an environment to go — and is refused by the ordinary
type message, which names both spellings (`fn-cfn-narrow.flan`). A capturing literal written into a `CFn` position
is refused by name, with what it captured and the fix in the sentence (`fn-cfn-captures.flan`).
One trap worth naming, because it is where the map would have broken: `FnAddr` is asked for by unrelated readers. A
`Fn`-typed one is two words; a `CFn`-typed or `Alloc`-typed one is a bare address — the second is what the map's
hash and equality pair and a handler frame's clause are, fields of structs the runtime declares, and they must stay
one word. **The node's type is what discriminates**, in both backends.
A handler clause is the one place the runtime does the passing, so `flan_handler` grew an `env` field and
`flan_signal` calls `h->fn(condition, xfer, h->env)` — the same trailing position, and a clause that captured
nothing declares nothing and is unaffected.
### The environment is a struct the checker synthesised
One field per captured name, in first-reference order, registered in the same table a `defstruct` goes in — so both
backends lay it out with the calculator they already have and neither learns a new shape. Its name is the lifted
function's (`env/fn/OWNER/N`), which is unique and stable for the reason that name is.
Two ends, and the copy is at the near one. In the *enclosing* frame, a slot holding the struct, filled with a `Make`
of the outer locals: that store is the copy, and it happens where the value is made. In the *lifted* frame, a slot
holding the pointer and a `Let` around the whole body reading each field back into the named slot the body was
checked against — once, at entry, so nothing downstream has to know an environment exists.
Both of the compiler's new slots are **nameless**, which is how the break loop is told to hide them. That is a
deliberate call, not an omission: what a reader wants at a stop is the captured *copies*, and those are named slots
holding the values under the names the source gave them. An `env` pointer and a struct of bytes would be two rows of
noise above them.
A redefinition that changes which locals an `fn` names changes an environment's layout, and `Session`'s layout guard
**exempts** these. The guard is about values the running program is holding; an environment can be in exactly one
place, a slot of the frame the literal was written in, written by the same module that reads it on every entry. A
restart for editing a capture list would take the dev loop away from the feature it was built for.
### Escape, which is what makes "case 2" a bounded claim
A value carrying an environment may be **called, passed down, and held in a `let`**. It may not be **returned,
stored, pointed at, or pushed into a container**. The check runs over the typed IR of every function the program
ends up with — including the lifted ones, so an `fn` inside an `fn` needs no special case — and classifies
function-typed values as *suspect* or clean:
- suspect: a capturing literal (`Tast.Closure`, the only node that makes one); a **parameter** of type `Fn`, in
every function; an `Fn` read back out of a struct, a case or a pointer; a local bound to any of those,
transitively; a branch or a valued form whose value is one.
- clean: the address of a name, the result of any call, and **everything of type `CFn`** — the last for free,
because a `CFn` has no environment to dangle and the type says so. The second follows from the first refusal,
which is what stops a function from returning a suspect at all.
The two types made this pass narrower rather than wider, which is the point of having them: a signature that says
`CFn` has already promised what the analysis would otherwise have to prove, and nothing written against one is
ever examined.
**The clean set is the enumeration, not the suspect set**, and that is a correction. It read the other way round —
`Field`, `CaseField` and `Deref` named as suspect, everything else clean — and had a hole exactly where a list like
this cannot: `(at s 0)` over a slice of `Fn` is a `Prim`, so it came out clean while the `Vec`, struct and pointer
spellings of the same act were refused. Nothing can write an `Fn` into a slice today, so it was unreachable; but
the pass claims its enumeration is closed, and a default of "clean" is how that claim stops being true without
anyone noticing. `fn-escape-at.flan` pins it. The same inversion fixed which of the two refusal messages an index
read gets.
Two of those arms are there because leaving them out is unsound rather than merely conservative, and each has a
program. **A function value read out of an environment** (`fn-escape-copy.flan`): a lifted body holds *copies* of
what it captured, read back with `Field(Deref env, i)`, so a copy of a captured function value carries whatever
environment the original did. Treat it as clean and the lifted body can return it, the return arrives at the outer
caller as an ordinary call result, and the whole "a call result is clean" rule has been walked around from inside.
**A valued form's tail** (`fn-escape-handled.flan`): `handler-bind`, `with-allocator` and `restart-case` are
expressions whose value is their body's — and a `restart-case`'s is a clause's too — so each is a way for a suspect
to be a function's answer that a check looking only at `return` and at the last form of a block would step over.
**The parameter rule is the whole answer to the hard case.** A capturing `fn` passed to a function that stores it is
caught *inside that function*: its parameter is suspect there and the store is refused where it is written. So no
call can leak what its caller passed, and no caller has to be analysed. What it costs is real:
`(defn keep [f (Fn [] i32)] (Fn [] i32) f)` is refused although it is harmless, and so is holding a parameter of
function type in a `Vec` that never leaves the frame. `fn-escape-param.flan` is that refusal, written down as a
refusal of something that would sometimes have been fine.
Refusing `Addr` of a suspect matters more than it looks: without it, `deref` of a `(Ptr (Fn ...))` launders a
suspect into a clean value and the return refusal has been walked around. Treating the `deref` itself as suspect is
the other half of that door, and it is free: nothing a `(Ptr (Fn ...))` can point at is anywhere but a frame, since
a global and a struct field of function type are both refused already.
Name resolution inside a lifted body now asks the enclosing function's locals **before** the globals, which is a
deliberate tightening: inside the enclosing function a local shadows a global of the same name, so a body lifted out
of it must mean the same thing. The old order was an accident of where the refusal sat.
Every one of these messages names **case 3** — the escaping closure, with an environment the collector owns —
because "this cannot be done" and "this cannot be done yet" are different sentences and the second is the true one.
Five programs: `fn-escape-return.flan`, `fn-escape-param.flan`, `fn-escape-store.flan`, `fn-escape-vec.flan`, and
`fn-capture-set.flan`.
### What may be captured
Anything but a **dyn**. A scalar, a struct and a fixed array copy whole. A string, a slice and a `Vec` or `Map`
header copy as their words, aliasing whatever they pointed at — which is exactly right while the value cannot
outlive the frame that owns the storage, and is exactly what would break under escape. A function value copies as a
function value, environment included; capturing one into another `fn`'s environment is the one place a suspect may
be written into an aggregate, and it is sound because the outer literal is itself suspect, so the pair of
environments lives and dies with one frame.
A **dyn is refused**, for the reason a struct field of dyn already is (`A struct cannot hold a dyn field the
collector would never find`): the collector's roots are frames, and nothing pushes the fields of a synthesised
environment. 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 — and case 3's
collector-allocated environment is where it belongs anyway. `fn-capture-dyn.flan`.
### Handlers, which get this for free and have no case 3 to wait for
`docs/BUILT.md` said a handler clause cannot see the establishing function's locals, refused for the same reason,
and that this is also case 2. **It is, and it now can.** A handler frame is popped by the body that pushed it and
nothing in the language can name one, so the establishing frame is alive whenever the clause runs — there is no
escaping case here to leave over. The only compiler change beyond the shared machinery is one field on
`flan_handler` and one argument in `flan_signal`, which the uniform signature required anyway.
What is still refused is a **store** into a captured name, in a clause as in an `fn`: the clause holds a copy, and
writing to it would change the copy and leave the local as it was. Scope is asked first, so a body's own `let`
shadowing a name the enclosing function also has is an ordinary local and an ordinary store — the refusal is about a
captured copy and not about a spelling.
A **field** of a captured struct is a different matter and is deliberately left alone: `(set (.x p) 9)` inside an
`fn` writes the copy and leaves the enclosing `p` as it was — which is exactly what `(set (.x p) 9)` inside a
function whose `p` is a *parameter* already does, and has always done. Capture takes a copy the way a call takes
one, so the two agree; refusing here would make the `fn` stricter than the `defn` it was written in for no reason
anyone could state. So §1's accumulation case still accumulates into
a global — and now with whatever the establishing function knew readable beside it, which is the half that was
missing. `fn-capture.flan`'s `handles` reads a captured budget in the clause.
Capturing the establishing frame **by reference** would make the accumulation case work directly and would be sound
here, uniquely — but it is a different feature from case 2, it would fork what "capture" means between the two
constructs, and it is not what was asked for. Named, not done.
### Recursion, nesting and loops
Capture is **transitive**: an `fn` inside an `fn` naming a local of the function both were written in makes the
middle one capture it and the inner one copy the middle one's copy. That is the same value, because every copy on
the way was taken at the moment its own value was made and those moments are nested. `check`'s context carries a
`parent` for exactly this, and it is safe to reach into precisely because a lifted body is checked at the point it
is written, with its parent paused there.
An `fn` written **inside a loop** stores into the same environment slot each time round, so what it sees is the
value on its own iteration and not the last one. `fn-capture.flan` sums `0 + 1 + 2 + 3` through a fresh `fn` per
iteration to say so. The trap this could have been — a closure `set` into a local bound outside the loop and called
after it, seeing the last iteration's copies — cannot be written: a captured value cannot be `set` anywhere, and a
`let` binding scopes to the iteration.
### What each backend cost
Very little, which was the point of putting the environment in a frame slot and passing it as an ordinary argument
at the one call that needs it.
`emit.ml`: a `%fnv` type and a 16-byte layout for `Fn`, `ptr` and eight for `CFn`; an `insertvalue` pair where a
symbol used to stand alone; two `extractvalue`s at a call through an `Fn`; one appended operand on that call and on
no other; one store in the prologue of a body that declared an environment. The four hand-written glue sites — the
startup call, `main`, the macro thunk and the reload thunk — are **unchanged**.
`x86.ml`: `Fn` becomes an aggregate and `CFn` stays a scalar; one appended argument after the channel on an `Fn`
call; an optional `incoming` slot; the 16-byte value split at a `CallPtr`. The three hand-built entries are
unchanged.
`flan_rt.c`: one field on `flan_handler` and one argument on the clause typedef, both trailing. The hash and
equality typedefs and their five call sites are **unchanged** — a hasher is reached from inside that file and never
through a function value, so it declares no environment and is handed none.
### `Fnval`, and the one thing a dev build cannot do ### `Fnval`, and the one thing a dev build cannot do
@ -3871,6 +4178,14 @@ What that does *not* give: a value taken *before* a redefinition and called afte
address is in a slot there is nothing left to re-resolve, and the honest fix is a trampoline per function, which is a address is in a slot there is nothing left to re-resolve, and the honest fix is a trampoline per function, which is a
cost every program would pay for a case no one has hit. Named here rather than papered over. cost every program would pay for a case no one has hit. Named here rather than papered over.
**An `fn` literal was asking for `Fnval` and should never have been**, which the capture lane found as a bug rather
than as a design question: `flan reload` on any function containing an `fn` literal failed at `llc` with
`use of undefined value '@flan.cell.fn/OWNER/N'`. A lifted body has no name anyone can type and no way to be
redefined on its own — it is reached by address from the body it was written in, and a redefinition of that body
carries its own copy — so the cell could never hold anything but the symbol, and a redefinition module had no reason
to declare one. It takes `Flanfn` now, which is the choice a handler clause has always made and for the same reason.
`Fnval` remains what it was for: a `defn`'s *name* in value position.
The two lifted-function name sequences are counted **per kind** — `fn/OWNER/N` and `handler/OWNER/N/TYPE` — The two lifted-function name sequences are counted **per kind** — `fn/OWNER/N` and `handler/OWNER/N/TYPE` —
rather than off one list. Sharing a counter would rename every `fn` in a function the moment a `handler-bind` was rather than off one list. Sharing a counter would rename every `fn` in a function the moment a `handler-bind` was
added above one, which is a rename for a body that did not change, in exactly the names a redefinition module emits. added above one, which is a rename for a body that did not change, in exactly the names a redefinition module emits.

View File

@ -18,7 +18,12 @@ and texpr_kind =
| Tarray of len * texpr (* [4 f32] [rows [cols u32]] *) | Tarray of len * texpr (* [4 f32] [rows [cols u32]] *)
| Tmap of texpr * texpr (* (Map string i32) *) | Tmap of texpr * texpr (* (Map string i32) *)
| Tapp of string * texpr list (* (Ptr Cursor) (Option f64) *) | 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. *) (* An array length is an integer or a compile-time constant's name. *)
and len = and len =

File diff suppressed because it is too large Load Diff

View File

@ -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.Tarray (Ast.Lname n, e) -> Printf.sprintf "[%s %s]" n (ty_source e)
| Ast.Tmap (k, v) -> | Ast.Tmap (k, v) ->
Printf.sprintf "(Map %s %s)" (ty_source k) (ty_source v) Printf.sprintf "(Map %s %s)" (ty_source k) (ty_source v)
| Ast.Tfn (ps, r) -> | Ast.Tfn (env, ps, r) ->
Printf.sprintf "(Fn [%s] %s)" Printf.sprintf "(%s [%s] %s)" (if env then "Fn" else "CFn")
(String.concat " " (List.map ty_source ps)) (ty_source r) (String.concat " " (List.map ty_source ps)) (ty_source r)
let tname n = ty (Ast.Tname n) let tname n = ty (Ast.Tname n)

View File

@ -2605,7 +2605,7 @@ let render_addr (s : Session.t) ~addr ~(ty : Types.t)
let thunk : Tast.fn = let thunk : Tast.fn =
{ Tast.name; params = []; ret = Types.Unit; { Tast.name; params = []; ret = Types.Unit;
body = (nullary "flan/dev-begin" :: parts) @ [ nullary "flan/dev-end" ]; 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); slots = Array.of_list (List.rev !extra);
(* Every slot in here is the walk's own scratch: what is being shown (* Every slot in here is the walk's own scratch: what is being shown
is storage this thunk reaches by address. *) is storage this thunk reaches by address. *)

View File

@ -85,6 +85,38 @@ let cellname n = "@" ^ quoted (Mangle.cell n)
written only by the guards this file emits. *) written only by the guards this file emits. *)
let xfer_param = "%xfer" 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 (* The condition's own name, for the message an unhandled [error] prints. The
checker has already refused anything that is not a struct. *) checker has already refused anything that is not a struct. *)
let struct_name_of (t : Types.t) = 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 ll_of = function Ptr -> "ptr" | I32 -> "i32" | I64 -> "i64"
let size_of = function Ptr | I64 -> 8 | I32 -> 4 let size_of = function Ptr | I64 -> 8 | I32 -> 4
(* A handler frame: the one it displaced, the condition type it matches, and (* A handler frame: the one it displaced, the condition type it matches, the
the lifted function that runs. *) 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 = 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 (* 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 [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 (* 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. *) copy of one: see Types. Opaque here in the same sense [ptr] is. *)
| Types.Alloc -> "ptr" | Types.Alloc -> "ptr"
(* A function value is a code address and nothing else. There is no (* A code address and the environment it is called with: two words, always,
environment beside it — capture does not exist (check.ml refuses it by whether or not this particular value captured anything. See [%fnv]. *)
name) — so it is one pointer, the same width as any other, and a backend | Types.Fn _ -> "%fnv"
needs to know no more about it than that. *) (* The bare address, and nothing beside it: one pointer, the width of any
| Types.Fn _ -> "ptr" 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 (* 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 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 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.Enum _ -> 4, 4
| Types.Ptr _ -> 8, 8 | Types.Ptr _ -> 8, 8
| Types.Alloc -> 8, 8 | Types.Alloc -> 8, 8
| Types.Fn _ -> 8, 8 | Types.Fn _ -> 16, 8
| Types.CFn _ -> 8, 8
| Types.Vec _ | Types.Map _ -> 40, 8 | Types.Vec _ | Types.Map _ -> 40, 8
(* [n x T] adds no padding of its own: T's size already carries its tail. *) (* [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 | 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); ("len", Types.Int Types.I64); ("log2cap", Types.Int Types.I64);
("allocator", Types.Alloc); ("epoch", Types.Int Types.I64) ] ("allocator", Types.Alloc); ("epoch", Types.Int Types.I64) ]
|> fun n -> ignore k; ignore v; n |> fun n -> ignore k; ignore v; n
(* A pointer to code, and lldb is told exactly that and no more. DWARF (* Two words, and shown as two, the same rule the Vec and the Map above
has DW_TAG_subroutine_type for the signature behind it, and spelling follow: a debugger told a function value were one pointer would put
one out here would buy a reader nothing they cannot get from the every offset after it out by eight. [code] is the address that
function it points at — [p f] answers with an address either way, and resolves to a symbol, which is what [p f] was ever worth; [env] is
the address is what resolves to a symbol. The name carries the the captured copies, and there is nothing here that could say what is
signature, which is where it is actually legible. *) 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 _ -> | 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 dnode d
(Printf.sprintf (Printf.sprintf
"!DIDerivedType(tag: DW_TAG_pointer_type, name: \"%s\", \ "!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 _ -> | Tast.Local _ | Tast.Global _ | Tast.Field _ | Tast.Deref _ ->
(* Everything that denotes a location is a load from its address. *) (* Everything that denotes a location is a load from its address. *)
load f (addr f e) e.Tast.ty load f (addr f e) e.Tast.ty
(* The symbol itself, not a load from it: a function's address is a link-time (* A [(Fn ...)] value, which is two words: a code address and the
constant. The same spelling the handler frames use for a lifted clause. *) environment it is called with. A value made out of a name captures
| Tast.FnAddr (Tast.Flanfn n) -> fname n nothing, so the second word is null and [zeroinitializer] has already put
| Tast.FnAddr (Tast.Rtfn n) -> "@" ^ n it there. See [%fnv].
(* 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 Only a [Fn]-typed one. The same three [fnref] constructors are also asked
after a redefinition is the new body — the same load a direct call to the for as bare addresses — carrying [CFn], and carrying [Alloc] for the
same name would do, at the point the *address* is taken rather than at the map's hash and equality pair and a handler frame's clause, which are
call. What that does not give is a value taken before a redefinition and fields of structs the runtime declares — and those stay one word. The
called after it: that one is still the old body, because there is nothing node's type is what says which is being asked for. *)
left to re-resolve once the address is in a slot. Named in docs/BUILT.md rather | (Tast.FnAddr _ | Tast.Closure _ | Tast.Thicken _)
than papered over with a trampoline. *) when (match e.Tast.ty with Types.Fn _ -> true | _ -> false) ->
| Tast.FnAddr (Tast.Fnval n) -> body_of f n 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.Addr p -> fst (place f p)
| Tast.Prim (p, args) -> prim f e p args | Tast.Prim (p, args) -> prim f e p args
| Tast.Call (name, args) -> | Tast.Call (name, args) ->
@ -2192,14 +2265,57 @@ and call f ret flan args =
the middle of an argument list). *) the middle of an argument list). *)
and call_ptr f ret callee args = and call_ptr f ret callee args =
let c = value f callee in 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 vs = map_lr (fun (a : Tast.expr) ->
let v = value f a in Printf.sprintf "%s %s" (ll a.Tast.ty) v) args in 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 let t = fresh f in
ins f "%s = call %s %s(%s)" t (ll ret) callee let tail =
(String.concat ", " (vs @ [ "ptr " ^ xfer_param ])); 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; guard f;
(* An aggregate with a dyn in it is spilled into a rooted slot the instant it (* 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: 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 stack finds what it pushed still valid, which is what "old code is
never unloaded" means. See NEXT.md, conditions step 1. *) never unloaded" means. See NEXT.md, conditions step 1. *)
ins f "store ptr %s, ptr %s" (fname h.Tast.hfn) fp; 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; ins f "call void @flan_handler_push(ptr %s)" slot;
slot) slot)
frames 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, 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". *) so the honest answer to "what can this call?" is "anything". *)
let params = params @ [ (if named then "ptr " ^ xfer_param else "ptr") ] in 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) Printf.sprintf "%s %s(%s)" (ll fn.Tast.ret) (fname fn.Tast.name)
(String.concat ", " params) (String.concat ", " params)
@ -3205,6 +3339,14 @@ let emit_fn m ?(hidden = false) ?(pnames = []) (fn : Tast.fn) =
Buffer.add_string f.allocas Buffer.add_string f.allocas
(Printf.sprintf " store %s %%p%d, ptr %s\n" (ll ty) i f.slots.(i))) (Printf.sprintf " store %s %%p%d, ptr %s\n" (ll ty) i f.slots.(i)))
fn.Tast.params; 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 (* 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 a debugging convenience and a release build does without it, while a
collector that cannot find its roots is a collector that frees live 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; List.iter (emit_global m ~hidden) flags;
emit_fn m ~hidden emit_fn m ~hidden
{ Tast.name = ".init-globals"; params = []; slots = [||]; snames = [||]; { 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 }; floc = (List.hd computed).Tast.ginit.Tast.loc };
true 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. ; so a Flan struct is exactly its C struct and nothing marshals.
%slice = type { ptr, i64 } %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 ; (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. ; type-erased and every operation is handed size and align at its call site.
%vec = type { ptr, i64, i64, ptr, i64 } %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, ; 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. ; handed the two sizes and a hash/equality pair at each call site.
%map = type { ptr, i64, i64, ptr, i64 } %map = type { ptr, i64, i64, ptr, i64 }
; A handler frame: the one it displaced, the condition type it matches, and ; A handler frame: the one it displaced, the condition type it matches, the
; the lifted function that runs. Allocated on the establishing frame's stack. ; lifted function that runs, and the environment that function is handed.
; Allocated on the establishing frame's stack.
|} ^ Rt.ll_type Rt.handler ^ {| |} ^ Rt.ll_type Rt.handler ^ {|
; A restart frame: the one it displaced and the name it offers. There is no ; 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 ; 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 (* 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 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 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 = let lifted =
List.filter List.filter
(fun (f : Tast.fn) -> (fun (f : Tast.fn) ->
match f.Tast.fparent with match f.Tast.fparent with
| Some "<thick>" -> true
| Some p -> List.mem p fns | Some p -> List.mem p fns
| None -> false) | None -> false)
p.Tast.fns p.Tast.fns

View File

@ -220,7 +220,8 @@ let rec refuse_ty loc (t : Types.t) =
| Types.Never | Types.Named _ | Types.Enum _ -> () | Types.Never | Types.Named _ | Types.Enum _ -> ()
| Types.Slice t | Types.Array (_, t) | Types.Option t -> refuse_ty loc t | Types.Slice t | Types.Array (_, t) | Types.Option t -> refuse_ty loc t
| Types.Vec 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 _ -> | Types.Ptr _ ->
at loc at loc
"(Ptr T) is not in the JS dialect — JavaScript has no addresses, so a \ "(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 \ "the runtime entry point %s has no JS counterpart — it is C in \
flan_rt.c, and this dialect has no C" flan_rt.c, and this dialect has no C"
n 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.Prim (p, args) -> prim f e p args
| Tast.Call (n, args) -> | Tast.Call (n, args) ->
Printf.sprintf "%s(%s)" (fname n) (String.concat ", " (call_args f args)) Printf.sprintf "%s(%s)" (fname n) (String.concat ", " (call_args f args))

View File

@ -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.Tmap (rename_texpr owned alias k, rename_texpr owned alias v)
| Ast.Tapp (n, args) -> | Ast.Tapp (n, args) ->
Ast.Tapp (n, List.map (rename_texpr owned alias) args) Ast.Tapp (n, List.map (rename_texpr owned alias) args)
| Ast.Tfn (ps, r) -> | Ast.Tfn (env, ps, r) ->
Ast.Tfn (List.map (rename_texpr owned alias) ps, rename_texpr owned alias r) Ast.Tfn (env, List.map (rename_texpr owned alias) ps,
rename_texpr owned alias r)
in in
{ t with Ast.t = k } { t with Ast.t = k }
@ -746,7 +747,7 @@ let rec texpr_uses acc (t : Ast.texpr) =
texpr_uses acc e texpr_uses acc e
| Ast.Tmap (k, v) -> texpr_uses acc k; texpr_uses acc v | Ast.Tmap (k, v) -> texpr_uses acc k; texpr_uses acc v
| Ast.Tapp (_, args) -> List.iter (texpr_uses acc) args | 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 rec expr_uses acc (e : Ast.expr) =
let go = expr_uses acc in let go = expr_uses acc in

View File

@ -96,11 +96,16 @@ let rec texpr (f : Form.t) : Ast.texpr =
confused with. *) confused with. *)
| Map _ -> | Map _ ->
fail f "a map type is written (Map K V), not in braces" 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 (match rest with
| [ { v = Vec params; _ }; ret ] -> | [ { v = Vec params; _ }; ret ] ->
mk (Ast.Tfn (List.map texpr params, texpr ret)) mk (Ast.Tfn (env, List.map texpr params, texpr ret))
| _ -> fail f "a function type is (Fn [T ...] R)") | _ -> fail f "a function type is (%s [T ...] R)" which)
| List ({ v = Sym name; _ } :: args) when args <> [] -> | List ({ v = Sym name; _ } :: args) when args <> [] ->
mk (Ast.Tapp (name, List.map texpr args)) mk (Ast.Tapp (name, List.map texpr args))
| _ -> fail f "expected a type, found %s" (Form.to_string f) | _ -> fail f "expected a type, found %s" (Form.to_string f)

View File

@ -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 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 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. *) 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.Set (Tast.Pglobal n, _) | Tast.Addr (Tast.Pglobal n) -> f n
| Tast.Handled (frames, _) -> | Tast.Handled (frames, _) ->
List.iter (fun (h : Tast.hframe) -> f h.Tast.hfn) frames List.iter (fun (h : Tast.hframe) -> f h.Tast.hfn) frames

View File

@ -282,8 +282,16 @@ let compatible ?(origin = fun _ -> None) ?(relaxed = []) ~loc
refusal is about a function the source does not name." refusal is about a function the source does not name."
gname ) gname )
in in
(* The parameters and the return as a [defn] writes them, and not
as [(Fn [...] ...)]. That spelling was harmless while [Fn] was
the only function type and is not now: it is a real type, it is
not the same as [(CFn [...] ...)], and a *declaration* is
neither of them — rendering one as a type invites a reader to
go looking for which of the two this function's name carries,
which is a question about taking its address and not about the
edit that was refused. *)
fail loc fail loc
"%s changes signature, from (Fn [%s] %s) to (Fn [%s] %s).%s \ "%s changes signature, from [%s] %s to [%s] %s.%s \
Restart to change it." Restart to change it."
what what
(String.concat " " (List.map Types.to_string g.Tast.params)) (String.concat " " (List.map Types.to_string g.Tast.params))
@ -373,6 +381,16 @@ let compatible ?(origin = fun _ -> None) ?(relaxed = []) ~loc
new_.Tast.globals; new_.Tast.globals;
List.iter List.iter
(fun (s : Tast.structure) -> (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 match
List.find_opt List.find_opt
(fun (r : Tast.structure) -> String.equal r.Tast.sname s.Tast.sname) (fun (r : Tast.structure) -> String.equal r.Tast.sname s.Tast.sname)
@ -992,7 +1010,7 @@ let eval ?(origin = "<eval>") ?pause t src : change =
Some Some
{ Tast.name = Printf.sprintf "install/%d" t.thunks; { Tast.name = Printf.sprintf "install/%d" t.thunks;
params = []; ret = Types.Unit; body; params = []; ret = Types.Unit; body;
fdefers = []; fparent = None; floc = loc; fdefers = []; fenv = None; fparent = None; floc = loc;
slots = [||]; snames = [||] } slots = [||]; snames = [||] }
in in
let ir = let ir =
@ -1319,7 +1337,7 @@ let render_locals ?(origin = "<locals>") t ~frame ~(fn : Tast.fn) ~bound
let thunk : Tast.fn = let thunk : Tast.fn =
{ Tast.name; params = []; ret = Types.Unit; { Tast.name; params = []; ret = Types.Unit;
body = (nullary "flan/dev-begin" :: body) @ [ nullary "flan/dev-end" ]; 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); slots = Array.of_list (List.rev !extra);
(* Every slot in here is the walk's own scratch: the locals being shown (* 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. *) are the *other* frame's, and this thunk reaches them by address. *)
@ -1408,7 +1426,7 @@ let render_condition t ~(st : Tast.structure) : change * (string * string) list
let thunk : Tast.fn = let thunk : Tast.fn =
{ Tast.name; params = []; ret = Types.Unit; { Tast.name; params = []; ret = Types.Unit;
body = (nullary "flan/dev-begin" :: body) @ [ nullary "flan/dev-end" ]; 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); slots = Array.of_list (List.rev !extra);
snames = Array.make (List.length !extra) None } snames = Array.make (List.length !extra) None }
in in
@ -1662,7 +1680,7 @@ let render_slot ?(origin = "<inspect>") t ~frame ~(fn : Tast.fn) ~slot ~path
{ Tast.name = tname; params = []; ret = Types.Unit; { Tast.name = tname; params = []; ret = Types.Unit;
body = body =
(nullary "flan/dev-begin" :: parts) @ [ nullary "flan/dev-end" ]; (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); slots = Array.of_list (List.rev !extra);
snames = Array.make (List.length !extra) None } snames = Array.make (List.length !extra) None }
in in
@ -1932,7 +1950,7 @@ let write_slot ?(origin = "<set>") t ~frame ~(fn : Tast.fn) ~slot ~path
stores stores
@ (nullary "flan/dev-begin" :: parts) @ (nullary "flan/dev-begin" :: parts)
@ [ nullary "flan/dev-end" ]; @ [ 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)); slots = Array.append base (Array.of_list (List.rev !extra));
(* The stored expressions' own [let]s keep their names; the (* The stored expressions' own [let]s keep their names; the
slots [render] added behind them are the walk's own slots [render] added behind them are the walk's own
@ -2030,7 +2048,7 @@ let render_globals ?(origin = "<globals>") t ~(globals : Tast.global list)
let thunk : Tast.fn = let thunk : Tast.fn =
{ Tast.name; params = []; ret = Types.Unit; { Tast.name; params = []; ret = Types.Unit;
body = (nullary "flan/dev-begin" :: body) @ [ nullary "flan/dev-end" ]; 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); slots = Array.of_list (List.rev !extra);
(* Every slot in here is the walk's own scratch: what is being shown is (* 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. *) the program's storage, which this thunk reaches by name. *)
@ -2118,7 +2136,7 @@ let eval_expr ?(origin = "<eval>") ?(pause = false) t src : change =
t.thunks <- t.thunks + 1; t.thunks <- t.thunks + 1;
let name = Printf.sprintf "eval/%d" t.thunks in let name = Printf.sprintf "eval/%d" t.thunks in
let thunk : Tast.fn = 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)); slots = Array.append base (Array.of_list (List.rev !extra));
(* The expression's own [let]s keep their names; the slots [render] added (* 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. *) behind them are the walk's own scratch and have none to keep. *)

View File

@ -112,6 +112,42 @@ and expr_kind =
dev build is not the symbol but whatever the indirection cell holds, and dev build is not the symbol but whatever the indirection cell holds, and
carries the Flan type [Fn]. *) carries the Flan type [Fn]. *)
| FnAddr of fnref | 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 (* 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 [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 the name slot, because everything that walks this IR treats [Call]'s
@ -246,9 +282,16 @@ and place =
| Pindex of expr * expr list | Pindex of expr * expr list
| Pderef of expr | Pderef of expr
(* A pushed handler: which condition type it matches, and the lifted function (* A pushed handler: which condition type it matches, the lifted function that
that runs when one is signalled. *) runs when one is signalled, and the environment that function is handed.
and hframe = { htype : int; hfn : string }
[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 (* 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 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 cell and no registry slot, and a redefinition of the parent carries its
own copy. *) own copy. *)
fparent : string option; 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; floc : Loc.t;
} }
@ -403,9 +459,10 @@ let rec walk (f : expr -> unit) (e : expr) =
| Set (p, v) -> walk_place f p; go v | Set (p, v) -> walk_place f p; go v
| Addr p -> walk_place f p | Addr p -> walk_place f p
| Field (t, _) | Deref t | CaseField (t, _, _) | Some_ t | UnwrapSome t | 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 | 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 | RestartCase (cs, body) -> List.iter (fun c -> gos c.rbody) cs; go body
| WithAlloc (a, body) -> go a; gos body | WithAlloc (a, body) -> go a; gos body

View File

@ -48,7 +48,50 @@ type t =
at the call site, which is exactly where the two numbers are produced. *) at the call site, which is exactly where the two numbers are produced. *)
| Vec of t | Vec of t
| Option of t (* (Option 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) *) | Fn of t list * t (* (Fn [T ...] R) *)
| CFn of t list * t (* (CFn [T ...] R) *)
| Var of string (* a type variable — milestone 5 *) | Var of string (* a type variable — milestone 5 *)
(* [dyn]: one machine word whose contents the runtime knows and this module (* [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 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 | Alloc, Alloc -> true
| Vec x, Vec y -> equal x y | Vec x, Vec y -> equal x y
| Option x, Option 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.length ps = List.length ps'
&& List.for_all2 equal ps ps' && List.for_all2 equal ps ps'
&& equal r r' && equal r r'
@ -167,6 +213,9 @@ let rec to_string = function
| Fn (ps, r) -> | Fn (ps, r) ->
Printf.sprintf "(Fn [%s] %s)" Printf.sprintf "(Fn [%s] %s)"
(String.concat " " (List.map to_string ps)) (to_string r) (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 | Var n -> n
| Dyn -> "dyn" | Dyn -> "dyn"

View File

@ -498,9 +498,15 @@ let alignof md t = snd (Emit.lay md t)
than SysV's eight. *) than SysV's eight. *)
let is_agg (t : Types.t) = let is_agg (t : Types.t) =
match t with 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.Int _ | Types.Float _ | Types.Bool | Types.Ptr _ | Types.Enum _
| Types.Alloc | Types.Fn _ -> false | Types.Alloc | Types.CFn _ -> false
| Types.Unit | Types.Never -> 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.String | Types.Slice _ | Types.Array _ | Types.Map _ | Types.Vec _
| Types.Option _ | Types.Named _ -> true | Types.Option _ | Types.Named _ -> true
(* A scalar, and trivially one: runtime/flan_dyn.h says [typedef uint64_t (* A scalar, and trivially one: runtime/flan_dyn.h says [typedef uint64_t
@ -1189,6 +1195,27 @@ let load_sym f ~dst s =
end end
else load_int f.b ~dst ~mm:(Sym (s, 0)) ~size:8 ~signed:false 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) = let scalar_size f (t : Types.t) =
match t with Types.Bool -> 1 | _ -> max 1 (sizeof f.md 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_size = Emit.Rt.size Emit.Rt.handler
let h_type = Emit.Rt.field Emit.Rt.handler "type" let h_type = Emit.Rt.field Emit.Rt.handler "type"
let h_fn = Emit.Rt.field Emit.Rt.handler "fn" 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_size = Emit.Rt.size Emit.Rt.restart
let r_field = Emit.Rt.field Emit.Rt.restart let r_field = Emit.Rt.field Emit.Rt.restart
@ -1720,27 +1748,34 @@ and lower_at f (e : Tast.expr) (dst : loc) : unit =
let l = place f p in let l = place f p in
addr_into f ~reg:rax l; addr_into f ~reg:rax l;
store_int f.b ~src:rax ~mm:(lmem f dst ~scratch:r11) ~size:8 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 (* A [(Fn ...)] value: the code address, then the environment beside it.
link-time constant, and this is the spelling a lifted handler clause is Two words — see [Emit]'s %fnv. Only a [Fn]-typed node; the same three
reached by. [emit.ml] says the same of [Flanfn]. *) constructors are also asked for as bare addresses, carrying [CFn] or
| Tast.FnAddr (Tast.Flanfn n) -> [Alloc], and those stay one word. The node's type says which. *)
addr_sym f ~dst:rax (fsym n); | (Tast.FnAddr _ | Tast.Closure _ | Tast.Thicken _)
store_int f.b ~src:rax ~mm:(lmem f dst ~scratch:r11) ~size:8 when (match t with Types.Fn _ -> true | _ -> false) ->
(* A function value someone wrote, which is the one [FnAddr] that is not the let env =
symbol. In a release build there is nothing to redefine and it is the match e.Tast.e with
symbol after all; in a dev build it is the cell's contents, so that a | Tast.FnAddr r -> fnaddr f ~reg:rax r; None
value taken after a redefinition is the new body. What that does not give | Tast.Closure (r, env) -> fnaddr f ~reg:rax r; Some env
— and [emit.ml] names it rather than papering over it with a trampoline — (* The widening: the thunk's code, with the bare address stored where
is a value taken *before* a redefinition and called after it. Once the an environment would be. The thunk reads it back out and calls it,
address is in a slot there is nothing left to re-resolve. *) which is what keeps every indirect call exactly typed. *)
| Tast.FnAddr (Tast.Fnval n) -> | Tast.Thicken (n, p) -> addr_sym f ~dst:rax (fsym n); Some p
if f.md.Emit.dev then | _ -> assert false
load_sym f ~dst:rax (csym n) in
else addr_sym f ~dst:rax (fsym n); store_int f.b ~src:rax ~mm:(lmem f dst ~scratch:r11) ~size:8;
store_int f.b ~src:rax ~mm:(lmem f dst ~scratch:r11) ~size:8 (match env with
| Tast.FnAddr (Tast.Rtfn n) -> | None -> xor_rr f.b ~dst:rax ~src:rax
addr_sym f ~dst:rax n; | 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 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.Prim (p, args) -> prim f e p args dst
| Tast.Call (name, args) -> | Tast.Call (name, args) ->
(match Hashtbl.find_opt f.externs name with (match Hashtbl.find_opt f.externs name with
@ -1752,8 +1787,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)) ~target:(if f.md.Emit.dev then `Cell (csym name) else `Sym (fsym name))
~args ~rty:t dst) ~args ~rty:t dst)
| Tast.CallPtr (callee, args) -> | 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 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.Do body -> block f body dst t
| Tast.Let (bs, body) -> | Tast.Let (bs, body) ->
List.iter List.iter
@ -1953,6 +1997,16 @@ and emit_handled f frames body dst t =
name it and it lives only for this body. *) name it and it lives only for this body. *)
addr_sym f ~dst:rax (fsym h.Tast.hfn); addr_sym f ~dst:rax (fsym h.Tast.hfn);
store_int f.b ~src:rax ~mm:(Frame (slot + h_fn)) ~size:8; 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); lea f.b ~dst:rdi ~mm:(Frame slot);
xor_rr f.b ~dst:rax ~src:rax; xor_rr f.b ~dst:rax ~src:rax;
call_sym f.b "flan_handler_push"; call_sym f.b "flan_handler_push";
@ -2812,7 +2866,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 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 first integer register when the result is an aggregate, and the transfer
channel last of all. *) 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 vals = List.map (fun (a : Tast.expr) -> eval f a, a.Tast.ty) args in
let callee = let callee =
match target with match target with
@ -2832,7 +2886,13 @@ and call_flan f ~target ~args ~rty dst =
(* The channel is this frame's own: a callee that transfers writes through (* 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. *) the pointer we were handed, so one cell serves the whole chain. *)
let chan = [ Aint (Lf f.xfer_off, Types.Ptr Types.Unit) ] in 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 (* 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 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 must not land in the middle of one. [r11] is scratch and no argument
@ -3371,11 +3431,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 (* 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 [sret] first when the result is an aggregate, then the parameters, then the
transfer channel. Answers one entry per incoming value — a register number, environment, then the transfer channel. Answers one entry per incoming
or a positive [rbp] displacement for the ones that came on the stack. *) 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 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 ints = ref 0 and sses = ref 0 and stk = ref 0 in
let next_int () = let next_int () =
if !ints < n_int_args then (incr ints; Ireg int_args.(!ints - 1)) if !ints < n_int_args then (incr ints; Ireg int_args.(!ints - 1))
@ -3395,7 +3456,17 @@ let incoming_of ~sret (params : Types.t list) =
else next_int ()) else next_int ())
params params
in 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 ───────────────────────────────────────────────────── *) (* ── The frame map ───────────────────────────────────────────────────── *)
@ -3422,7 +3493,7 @@ let where_from = function
let frame_map (md : Emit.m) (fn : Tast.fn) ~slots ~fixed ~total ~outgoing 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_off ~sret_off ~retval ~dframe ~dslotv ~sret ~sret_at ~param_at
~xfer_at = ~env_at ~xfer_at =
let b = Buffer.create 1024 in let b = Buffer.create 1024 in
let line s = Buffer.add_string b (if s = "" then "#\n" else "# " ^ s ^ "\n") 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 (* The prose paragraphs wrap; the table below does not, because its columns
@ -3493,10 +3564,22 @@ let frame_map (md : Emit.m) (fn : Tast.fn) ~slots ~fixed ~total ~outgoing
(Types.to_string fn.Tast.ret) (Types.to_string fn.Tast.ret)
(if is_float fn.Tast.ret then "xmm0" else "rax"))); (if is_float fn.Tast.ret then "xmm0" else "rax")));
para (Printf.sprintf para (Printf.sprintf
"The transfer channel arrives last of all, %s. It is a pointer to the cell a \ "The transfer channel arrives after the parameters, %s. It is a pointer to the \
callee writes its target into, and reading it is what every guard below \ cell a callee writes its target into, and reading it is what every guard \
does." below does.%s"
(where_from xfer_at)); (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 ""; line "";
para (Printf.sprintf para (Printf.sprintf
"The frame is 0x%x bytes below rbp. %s" total "The frame is 0x%x bytes below rbp. %s" total
@ -3716,7 +3799,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 high-water mark of the temporaries and this is the boundary below which
they start. It is the frame map's last line. *) they start. It is the frame map's last line. *)
let fixed = f.frame in 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 (* 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] 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 eats rdi, rsi and rcx, which is where three of the other parameters still
@ -3975,6 +4060,18 @@ let emit_fn (md : Emit.m) ~externs ~fns ?(ext = fun _ -> false)
| _ -> max 1 (fst (Emit.lay md ty))) | _ -> max 1 (fst (Emit.lay md ty)))
end) end)
fn.Tast.params; 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 (match xfer_at with
| Ireg r -> store_int pb ~src:r ~mm:(Frame f.xfer_off) ~size:8 | Ireg r -> store_int pb ~src:r ~mm:(Frame f.xfer_off) ~size:8
| Istk d -> | Istk d ->
@ -4052,7 +4149,7 @@ let emit_fn (md : Emit.m) ~externs ~fns ?(ext = fun _ -> false)
(frame_map md fn ~slots:f.slots ~fixed ~total:(frame_bytes f) (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 ~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 ~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); 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 (* [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 a shared object is interposable, and that applies to taking the address
@ -4191,6 +4288,8 @@ let emit_main ?(cfi = false) ?(ann = false) ?(startup = false) ?(gc = false)
let xfer = -8 and argv = -32 in let xfer = -8 and argv = -32 in
xor_rr b ~dst:rax ~src:rax; xor_rr b ~dst:rax ~src:rax;
store_int b ~src:rax ~mm:(Frame xfer) ~size:8; 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 (match fn.Tast.params with
| [] -> lea b ~dst:rdi ~mm:(Frame xfer) | [] -> lea b ~dst:rdi ~mm:(Frame xfer)
| [ _ ] -> | [ _ ] ->
@ -4857,10 +4956,14 @@ let redefinition ~checks ?(dev = true) ?(known = fun _ -> true)
(* A clause lifted out of a target comes with it: its body may have changed (* 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 too, and it is reached by address from inside this module rather than
through a cell. Every other lifted clause is invisible here. *) 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 = let lifted =
List.filter List.filter
(fun (f : Tast.fn) -> (fun (f : Tast.fn) ->
match f.Tast.fparent with match f.Tast.fparent with
| Some "<thick>" -> true
| Some q -> List.mem q fns | Some q -> List.mem q fns
| None -> false) | None -> false)
p.Tast.fns p.Tast.fns

View File

@ -33,10 +33,21 @@
* A type is a number rather than a pointer to anything, so that a module * 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. */ * 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 { typedef struct flan_handler {
struct flan_handler *prev; struct flan_handler *prev;
uint32_t type_id; uint32_t type_id;
void (*fn)(void *condition, void *xfer); void (*fn)(void *condition, void *xfer, void *env);
void *env;
} flan_handler; } flan_handler;
static flan_handler *handlers; 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) { void flan_signal(uint32_t type_id, void *condition, void *xfer) {
for (flan_handler *h = handlers; h != NULL; h = h->prev) for (flan_handler *h = handlers; h != NULL; h = h->prev)
if (h->type_id == type_id) { if (h->type_id == type_id) {
h->fn(condition, xfer); h->fn(condition, xfer, h->env);
if (*(void **)xfer != NULL) return; 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 * 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 * 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 * 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, * 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 * which is not the same thing and not something a leaf hasher should see. So

View 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)

View 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)

View File

@ -1,11 +1,116 @@
;; Capture does not exist. An fn is lifted into a function of its own and is ;; Capture by value into a stack environment — spec-memory.md's case 2.
;; 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 ;; An fn is still lifted into a function of its own, but it is no longer handed
;; did not mean. spec-memory.md's capture cases, and escaping closures with ;; nothing but its parameters: a local of the enclosing function that it names
;; them, are deferred; this is the refusal that says so where it happens. ;; is *copied* into an environment on that function's frame when the value is
(defn use [f (Fn [] i32)] i32 (f)) ;; 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 (defn main [] i32
(let [n 7] ;; The motivating program.
(println (use (fn [] n)))) (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) 0)

View 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)

View 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
View 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)

View File

@ -0,0 +1,13 @@
;; An index read is a read, and the escape check's clean list has to be a
;; list. A function value out of a slice is refused exactly as one out of a
;; Vec, a struct or a pointer is — the four are the same act and there is no
;; reason for a reader to have to remember which spellings were enumerated.
;;
;; Not reachable today: nothing can write an Fn into a slice, because every
;; position that would have to hold one is refused. It is here so that the
;; day one can, this is already true — the alternative was a default of
;; "clean" for anything the enumeration had not thought of, which is how a
;; closed list quietly stops being closed.
(defn leak [s [(Fn [] i32)]] (Fn [] i32) (at s 0))
(defn main [] i32 0)

View 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)

View 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)

View File

@ -0,0 +1,16 @@
;; A match arm's binding is a binding, and the escape check has to see it.
;;
;; Reading the payload by hand is a case-field read, which is suspect: a copy
;; of a function value carries whatever environment the original did. Binding
;; it to a name in an arm is the same read, and the store that fills the arm's
;; slot is inside the branch rather than in any form the walk reads as a
;; binding — so without the arm's slots being taken as suspect too, the Vec,
;; slice, struct and pointer spellings of this were all refused while the one
;; that goes through Option and a name was not.
(defn leak [o (Option (Fn [] i32))] (Fn [] i32)
(match o
(Some f) f
None (fn [] 0)))
(defn main [] i32 0)

View 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)

View 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)

View 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)

View 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)

View File

@ -1,6 +1,8 @@
;; A foreign function's address is not a Flan function value. A Flan ;; 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 ;; function's emitted signature ends with the environment and the transfer
;; does not, so nothing could call the resulting pointer correctly — and an ;; 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 ;; 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 ;; raw symbol knows nothing about. Refused for what it is, with the wrapper
;; named as the way to get one. ;; named as the way to get one.

View File

@ -0,0 +1,15 @@
;; The same line drawn in return position, which is the half that is easy to
;; miss: a (Fn [] (Fn [] $t)) parameter handed a (CFn [] (CFn [] i32)) needs
;; the inner widening built by whatever calls the *argument*, and that is the
;; generic's own body, which was compiled against the parameter's type and not
;; against this caller's.
(defn inner [] i32 3)
(defn outer [] (CFn [] i32) inner)
(defn call-twice [g (Fn [] (Fn [] $t))] $t ((g)))
(defn main [] i32
(println (call-twice outer))
0)

View File

@ -0,0 +1,19 @@
;; The widening between the two function types is a value the caller builds —
;; a thunk, minted at the call — so there is exactly one place to build it:
;; around the whole argument. Nested inside one, there is no caller standing
;; where the thunk would have to go.
;;
;; The parameter here is (Fn [(Fn [$t] $t)] i32) and taker's address carries
;; (CFn [(CFn [i32] i32)] i32). Admitting that structurally binds $t and then
;; hands one word where the instance declares two, in a position no later pass
;; can widen: the catch-up that builds the outer thunk compares the whole
;; substituted parameter list and this mismatch is inside it. A call with no
;; type variables in it is refused, so this one is too.
(defn taker [h (CFn [i32] i32)] i32 (h 1))
(defn hof [g (Fn [(Fn [$t] $t)] i32) k $t] i32 (g (fn [x] x)))
(defn main [] i32
(println (hof taker 0))
0)

View File

@ -0,0 +1,33 @@
;; A generic whose function parameter binds the type variable, which is the
;; shape a name now has to reach: a defn's address carries (CFn [i32] i32),
;; and (apply2 bump 1) has to bind $t from it and then widen the argument.
;;
;; Both halves are here because they fail apart. The binding is Check's
;; bind_ty, which runs inside generic_call and decides the instantiation; the
;; widening is the catch-up pass at the end of the same function, because a
;; parameter that still mentioned a variable was checked with no expectation
;; at all and expect never saw the pair. Get the first without the second and
;; the call site hands one word to an instance that declares two.
;;
;; The prelude does not cover this and that is worth saying: its higher-order
;; functions bind $t from an *earlier* argument, so the parameter is already
;; concrete by the time the function value is reached, and (map-in-place s
;; double) never walks this path at all.
;;
;; The CFn half is new rather than restored: a matching bare address against
;; a (CFn [$t] $t) parameter had no arm either, and fell through to plain
;; equality.
(defn apply2 [f (Fn [$t] $t) x $t] $t (f (f x)))
(defn applyc [f (CFn [$t] $t) x $t] $t (f (f x)))
(defn bump [n i32] i32 (+ n 1))
(defn twice [x f64] f64 (* x 2.0))
(defn main [] i32
(println (apply2 bump 1))
(println (applyc bump 1))
;; A second instantiation, so the two copies are really two and the thunk
;; the first one minted is not reused at the wrong signature.
(println (apply2 twice 1.5))
0)

View File

@ -4,6 +4,17 @@
;; union's first case. So it is refused where the field is written rather than ;; 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 ;; left to crash at the call, and the same rule covers a global, a fixed
;; array's element and (zeroed). ;; 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)]) (defstruct Ops [run (Fn [i32] i32)])
(defn main [] i32 0) (defn main [] i32 0)

View File

@ -2,6 +2,10 @@
;; so it takes them from the position it is written in. An argument position ;; 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 ;; says what is wanted, because the callee's signature is threaded into every
;; argument; a let binding does not, and is refused saying so. ;; 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 (defn main [] i32
(let [f (fn [x] (* x 2))] (let [f (fn [x] (* x 2))]
(println (f 3))) (println (f 3)))

View File

@ -0,0 +1,24 @@
;; Two widenings of different signatures in one program, so the dev loop has
;; two thunks to tell apart across a reload.
;;
;; A thunk is not a function anybody wrote, so nothing in the source names it
;; and nothing can be edited to rename it. But Session.compatible compares a
;; reload's functions against the running program's *by name*, and a name that
;; means whichever thunk was minted first means a different signature the
;; moment the forms are reordered — which reads to the session as a function
;; whose signature was edited, and answers an ordinary edit with "Restart to
;; change it". So the name is the signature, spelled so that it can be read
;; back, and reordering these two calls is an ordinary body change.
(defn a1 [x i32] i32 (+ x 1))
(defn b1 [x i64] i64 (+ x 1))
(defn use32 [f (Fn [i32] i32)] i32 (f 1))
(defn use64 [f (Fn [i64] i64)] i64 (f 1))
(defn both [] i32
(println (use32 a1))
(println (use64 b1))
0)
(defn main [] i32 (both))

View File

@ -0,0 +1,25 @@
;; The widening thunk is memoised per signature, and the key is the types.
;;
;; It used to be a *name*, derived from mangle_ty, which flattens a whole
;; signature into one hyphen-joined string and loses arity and every type
;; boundary with it: (CFn [(Ptr i32)] i32) and (CFn [ptr i32] i32) — the
;; second over a struct someone called ptr — both flatten to the same thing.
;; Keyed on that, the second widening reuses the first's thunk and calls it
;; with the wrong arity, which both backends compile without a word and
;; neither runs. This program is that pair, and it prints 5 and 17.
;;
;; A struct named ptr is legal and ordinary; nothing about the collision
;; needed a program written to provoke it, only two signatures that happened
;; to flatten alike.
(defstruct ptr [a i32 b i32])
(defn f1 [p (Ptr i32)] i32 (deref p))
(defn f2 [a ptr b i32] i32 (+ (.a a) b))
(defn use1 [f (Fn [(Ptr i32)] i32)] i32 (let [x 5] (f (addr x))))
(defn use2 [f (Fn [ptr i32] i32)] i32 (f (ptr {.a 10 .b 0}) 7))
(defn main [] i32
(println (use1 f1))
(println (use2 f2))
0)

View File

@ -1,5 +1,13 @@
;; Function values, the non-escaping kind: a code address and no environment ;; Function values, and specifically the ones with no environment. Nothing
;; beside it. Capture does not exist, so nothing here can outlive anything. ;; 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 ;; This is a Lisp-1 — one top-level namespace, enforced — so a bare function
;; name *is* the function and there is no #' to write. ;; name *is* the function and there is no #' to write.

View File

@ -43,7 +43,10 @@
/* The trailing ptr is the transfer channel spec-conditions.md §6 puts in every /* 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 * 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 * 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_outer(void *xfer) __asm__("flan.outer");
extern int64_t flan_counter __asm__("flan.counter"); extern int64_t flan_counter __asm__("flan.counter");

View File

@ -3928,12 +3928,121 @@ level "1"
outputs ~opt:"-O0" "the prelude's map, filter, reduce and sort-by, -O0" outputs ~opt:"-O0" "the prelude's map, filter, reduce and sort-by, -O0"
"programs/higher-order.flan" higher_order_out; "programs/higher-order.flan" higher_order_out;
(* What function values do *not* include, each refused by name. Capture is (* Capture by value into a stack environment — spec-memory.md's case 2.
the headline: an fn is lifted into a function of its own and handed Three opt levels for the reason the case above has them, and for one
nothing but its parameters, so spec-memory.md's capture cases and more: the environment is a struct in the frame and the value carries
escaping closures with them stay deferred. *) its address, which is exactly the shape -O2 is entitled to make
refuses "an fn cannot capture" "programs/fn-capture.flan" disappear. -O0 is what proves there is a real store and a real load
"cannot see n"; 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;
(* Two signatures that flatten to one string under [mangle_ty], which is
how the thunk memo used to be keyed. Keyed on the name, the second
widening reuses the first's thunk at the wrong arity — a miscompile
both backends emit without a word. Keyed on the types, this prints
5 and 17. *)
outputs "two signatures that mangle alike" "programs/fn-thunk-share.flan"
"5\n17\n";
outputs ~opt:"-O0" "two signatures that mangle alike, -O0"
"programs/fn-thunk-share.flan" "5\n17\n";
(* A generic whose *function* parameter binds the type variable, which is
the shape a bare name has to reach now that a defn's address carries
CFn. It fails in two places and they fail apart: the binding, in
bind_ty, and the widening, in generic_call's catch-up pass — a
parameter that still mentioned a variable was checked with no
expectation, so expect never saw the pair. The prelude misses it
entirely, because its higher-order functions bind $t from an earlier
argument and the parameter is concrete by the time the function value
is reached. *)
let fn_generic_out = "3\n3\n6\n" in
outputs "a generic that binds its variable through a function type"
"programs/fn-generic.flan" fn_generic_out;
outputs ~opt:"-O0"
"a generic that binds its variable through a function type, -O0"
"programs/fn-generic.flan" fn_generic_out;
(* And where the same binding stops. The widening is a value the caller
builds around the whole argument, so a function type nested inside an
argument's own type has no caller standing where its thunk would go —
and the catch-up pass that builds the outer one compares the whole
substituted parameter list, so a mismatch inside it is not something
any later pass can repair. Both positions, because the return one is
the easier of the two to leave open. *)
refuses "a nested function type does not widen"
"programs/fn-generic-nested.flan"
"hof expects (Fn [(Fn [t] t)] i32) here";
refuses "and neither does one in return position"
"programs/fn-generic-nested-return.flan"
"call-twice expects (Fn [] (Fn [] t)) here";
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 "an index read is a read like any other"
"programs/fn-escape-at.flan" "may carry an environment";
refuses "a match arm's binding is a binding"
"programs/fn-escape-match.flan" "may carry an environment";
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" refuses "an fn with no type to take" "programs/fn-no-type.flan"
"nothing here says what this fn"; "nothing here says what this fn";
refuses "a function value would be zeroed" "programs/fn-in-struct.flan" refuses "a function value would be zeroed" "programs/fn-in-struct.flan"

View File

@ -473,7 +473,7 @@ let () =
(match ty "(Map string i32)" with (match ty "(Map string i32)" with
| Tapp ("Map", [ _; _ ]) -> () | _ -> check "(Map K V) is a map type" false); | Tapp ("Map", [ _; _ ]) -> () | _ -> check "(Map K V) is a map type" false);
(match ty "(Fn [a a] bool)" with (match ty "(Fn [a a] bool)" with
| Tfn ([ _; _ ], _) -> () | _ -> check "(Fn [T] R)" false); | Tfn (_, [ _; _ ], _) -> () | _ -> check "(Fn [T] R)" false);
(* ── Declarations ──────────────────────────────────────────────── *) (* ── Declarations ──────────────────────────────────────────────── *)
(match (parse_decl "(defn f [x i32] bool x)").d with (match (parse_decl "(defn f [x i32] bool x)").d with
@ -3668,14 +3668,21 @@ let () =
rejects_check "signal in value position" rejects_check "signal in value position"
"(defstruct C [id i32])\n\ "(defstruct C [id i32])\n\
(defn f [] i32 (signal (C {.id 1})))" ~needle:"expected i32"; (defn f [] i32 (signal (C {.id 1})))" ~needle:"expected i32";
(* A handler is lifted into a function of its own, so the establishing (* A handler clause captures the establishing function's locals by value —
function's locals are not there. Capturing them is a closure, which is spec-memory.md's case 2 — so it can read one. A *store* is the thing that
milestone 5 — until then it is refused for the reason it is refused for is not there: the clause holds a copy, and writing to it would leave the
rather than as an unknown name. *) local it came from as it was, which is a silent disagreement and not a
rejects_check "a handler capturing a local" 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\ "(defstruct C [id i32])\n\
(defn f [] () (let [n 0] (handler-bind [(C [c] (set n 1))] (signal (C {.id 2})))))" (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 (* 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. *) leave them on the stack pointing into a function that has gone. *)
rejects_check "return inside handler-bind" rejects_check "return inside handler-bind"
@ -3761,14 +3768,18 @@ let () =
accepts "handler-case with several clauses" accepts "handler-case with several clauses"
(boom ^ "(defn f [] i32 (handler-case 1 [(Boom [c] (.id c)) \ (boom ^ "(defn f [] i32 (handler-case 1 [(Boom [c] (.id c)) \
(Dud [c] (+ 1 (.id c)))]))"); (Dud [c] (+ 1 (.id c)))]))");
(* The whole difference from handler-bind: a clause runs at the form, in the (* The difference from handler-bind is narrower than it was. Both see the
function that wrote it, so it sees that function's locals. The same body establishing function's locals now — a handler-case clause *is* that
under a handler-bind is refused by name. *) 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" accepts "a handler-case clause sees the establishing function's locals"
(boom ^ "(defn f [] i32 (let [n 1] (handler-case 0 [(Boom [c] n)])))"); (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" rejects_check "a handler-bind clause still cannot"
(boom ^ "(defn f [] i32 (let [n 1] (handler-bind [(Boom [c] (set n 2))] 0)))") (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 (* Nothing static refuses a condition no clause lists: it installs no frame
that matches, so it goes past untouched and the body carries on. *) that matches, so it goes past untouched and the body carries on. *)
accepts "a condition no clause lists" accepts "a condition no clause lists"

View File

@ -1177,6 +1177,30 @@ let () =
| exception Loc.Error { Loc.dmsg = m; _ } -> | exception Loc.Error { Loc.dmsg = m; _ } ->
fail "an expression that instantiates a generic: %s" m); fail "an expression that instantiates a generic: %s" m);
(* ── The widening thunks, across a reorder ──────────────────────
A thunk is a function nobody wrote and nothing in the source names, so
the only way one can change is the program growing or losing a widening
— and reordering two calls is neither. But [compatible] compares by
name, so a thunk named for the order it was minted in means one
signature before the edit and another after, and the session answers a
body change with "Restart to change it" about a name the programmer
cannot find. The name spells the signature, so this reload is ordinary.
Both directions of the pair are here — the same two calls, swapped —
because a name that is a counter is wrong for exactly one of them and
the test has to be the one that is wrong. *)
(let t, _ = Session.create ~file:"programs/fn-thunk-reload.flan" () in
match
Session.eval t
"(defn both [] i32 (println (use64 b1)) (println (use32 a1)) 0)"
with
| c ->
if not (List.mem "both" c.Session.fns) then
fail "reordering two widenings installed %s"
(String.concat " " c.Session.fns)
| exception Loc.Error { Loc.dmsg = m; _ } ->
fail "reordering two widenings was refused: %s" m);
(* ── A class whose slots changed ──────────────────────────────── (* ── A class whose slots changed ────────────────────────────────
The dev loop's half of CLHS 4.3.6. Three things have to be true of the The dev loop's half of CLHS 4.3.6. Three things have to be true of the
session for the runtime's migration to ever be reached: a changed slot session for the runtime's migration to ever be reached: a changed slot
@ -1305,7 +1329,7 @@ let () =
if Session.strip_rebind "~2" <> "~2" then fail "a name that is only a suffix was stripped"; if Session.strip_rebind "~2" <> "~2" then fail "a name that is only a suffix was stripped";
(let fn snames : Tast.fn = (let fn snames : Tast.fn =
{ Tast.name = "f"; params = []; ret = Types.Unit; body = []; { 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); slots = Array.make (Array.length snames) (Types.Int Types.I32);
snames } snames }
in in

View File

@ -519,7 +519,8 @@ notation reads as exactly one data item.</p>
<tr><td><code>(Handle T)</code></td><td>a reference into a pool that reports a dead referent</td><td>index and generation packed into an <code>i64</code></td></tr> <tr><td><code>(Handle T)</code></td><td>a reference into a pool that reports a dead referent</td><td>index and generation packed into an <code>i64</code></td></tr>
<tr><td><code>(Ptr T)</code></td><td>raw pointer</td><td>a pointer</td></tr> <tr><td><code>(Ptr T)</code></td><td>raw pointer</td><td>a pointer</td></tr>
<tr><td><code>(Option T)</code></td><td><code>Some</code> / <code>None</code></td><td>tag byte + T</td></tr> <tr><td><code>(Option T)</code></td><td><code>Some</code> / <code>None</code></td><td>tag byte + T</td></tr>
<tr><td><code>(Fn [T ...] R)</code></td><td>a function value</td><td>a pointer</td></tr> <tr><td><code>(Fn [T ...] R)</code></td><td>a function value, which may have captured</td><td>a code address and an environment pointer</td></tr>
<tr><td><code>(CFn [T ...] R)</code></td><td>a function value that cannot capture — the <code>C</code> is what a C function pointer would need, not a way to reach C today</td><td>a pointer</td></tr>
<tr><td><code>Allocator</code></td><td>an opaque builtin: a proc, its data and a capability set</td><td>a pointer to that</td></tr> <tr><td><code>Allocator</code></td><td>an opaque builtin: a proc, its data and a capability set</td><td>a pointer to that</td></tr>
<tr><td><code>$t</code></td><td>a type variable — see <a href="#generics">generics</a></td><td>whatever it is instantiated at</td></tr> <tr><td><code>$t</code></td><td>a type variable — see <a href="#generics">generics</a></td><td>whatever it is instantiated at</td></tr>
<tr><td>a struct</td><td>value type</td><td>fields in declaration order</td></tr> <tr><td>a struct</td><td>value type</td><td>fields in declaration order</td></tr>