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