flan/lib/tast.ml
Joseph Ferano 468dab6e4c Restarts take parameters, and the check for them is where it has to be
spec-conditions.md §3's remaining half: a clause binds parameters, an
invoke-restart supplies them, and what a restart takes is compared at run
time because a restart is found by name on a dynamic stack — neither end
of the transfer can see the other.

The parameters live in a buffer the restart-case owns, not the invoker's
frame. A clause runs after every frame between the two has returned (§5),
so anything on the invoking side is gone by then; the invoker stores into
the target frame while both are still alive, which is the one moment they
are.

The frame carries the parameter count and a hash of how the types are
spelled, and every frame carries them whether it takes parameters or not:
a clause taking none has to refuse arguments as loudly as one taking two
of the wrong type. The count is not redundant with the hash — it is what
makes a 32-bit collision between two different signatures harmless — and
the spelling itself rides along so that a mismatch can say what was
wanted and what was given, which neither end alone knows.

The arguments are evaluated into slots before the invoke node rather than
hanging off it. An argument that transfers on its own is then guarded
before anything aims the channel, and a call written in an argument is on
the ordinary walk Reach and Load already do — a node they treat as a leaf
would have dropped the function and failed to link.

The other way a transfer starts is the break loop, which chooses by
position and has nothing to fill parameters in with. It reaches a clause
through the same channel, so nothing downstream could tell the two apart:
the frame is pushed with the buffer marked unfilled and a clause with
parameters checks that mark before reading it. Refused with the reason
rather than run on values no one supplied.

runtime/flan_rt.c gains two message functions and nothing else; the
restart frame's first four fields, which are the ones C declares, do not
move.
2026-09-12 10:46:24 +07:00

213 lines
9.9 KiB
OCaml
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(** The typed IR: what the checker produces and what every backend consumes.
Three backends share this — the tree-walking interpreter, dev redefinition
and the release AOT build (plan.org, Compilation) — so everything a backend
would otherwise have to re-derive is resolved here and nowhere else:
- names are gone. A local is a slot index into the frame, a global is a
name, and a call names its callee directly. No environment lookup.
- field access is an index, not a string, and any auto-deref the source
relied on is an explicit [Deref] node.
- literals have a machine type. There is no untyped 1 past this point.
- a struct literal lists every field in declaration order, with the omitted
ones filled in as [Zero] — ZII is settled here rather than at runtime.
- sugar is already gone from the AST; what is left is the small set below. *)
type prim =
(* arithmetic and comparison, per machine type — the operands carry their own
kind at runtime, so one constructor covers every width *)
| Add | Sub | Mul | Div | Rem
| Eq | Ne | Lt | Le | Gt | Ge
| Not
(* bitwise, integers only. [Shr] is arithmetic on a signed type and logical
on an unsigned one, which is what the operand's own kind already says. *)
| BitAnd | BitOr | BitXor | Shl | Shr
(* containers: fixed arrays and slices only at milestone 2 *)
| Len | At | Slice
(* the milestone-2 host primitives, plan.org. The four conversions are
*text*: bytes->f64 parses "12.5", f64->bytes renders it — that is what
calc-me's tokenizer and the prelude's printers each need. *)
| Bytes | BytesToF64 | BytesToI64 | F64ToBytes | I64ToBytes
(* (string b): the other direction of [Bytes], and the same non-instruction.
See check.ml's "string" case for why it is unchecked. *)
| StrOfBytes
(* No surface name: the structural printer is the only thing that builds
these. U64ToBytes because u64 is not i64 with a flag, EscapeBytes for a
string nested inside a printed structure. *)
| U64ToBytes | EscapeBytes
| WriteStdout | Exit | Argv
| Cast of Types.t
type expr = { e : expr_kind; ty : Types.t; loc : Loc.t }
and expr_kind =
| Int of int64 * Types.ikind
| Float of float * Types.fkind
| Bool of bool
| Str of string
| Unit
| Zero of Types.t (* ZII: all-bytes-zero of this type *)
| Uninit of Types.t (* the explicit opt-out *)
| Local of int (* slot index into the frame *)
| Global of string
| Prim of prim * expr list
| Call of string * expr list (* direct call; no first-class fns yet *)
| Do of expr list
| Let of (int * expr) list * expr list
| If of expr * expr * expr
| While of expr * expr list
| Return of expr option
| Set of place * expr
| Field of expr * int (* target is already a struct value *)
| Addr of place
| Deref of expr
| Make of string * expr list (* struct literal, every field, in order *)
| Arr of expr list (* fixed-array literal *)
| Some_ of expr
| None_
| Match of expr * arm list
(* (some x): unwrap Some, else early-return None from the enclosing function.
An early return, not an expression that can fail — hence its own node. *)
| UnwrapSome of expr
(* Conditions, spec-conditions.md. [Signal] walks the handler stack and
returns Unit whatever it finds — with nothing matching it is a no-op, so
nothing here alters control flow. [HandlerBind] pushes one frame per
clause, runs its body, and pops them; each clause was lifted into its own
function by the checker, so what is left is the frame and the call. *)
| Signal of sigkind * int * expr (* how, the type id, the condition *)
| Handled of hframe list * expr list
(* The transfer, spec-conditions.md §3§6. [RestartCase] pushes one frame per
clause, runs its body, and pops them; if a transfer arrives naming one of
*its* frames it runs that clause instead, and the whole form yields either
way. [InvokeRestart] looks the name up on the restart stack, writes the
frame it found into the transfer channel and leaves — it has type Never,
so nothing follows it.
[InvokeRestart]'s arguments are already evaluated: the checker binds each
to a slot and wraps the node in a [Let], so what is left here is a list of
locals to copy into the frame. Two reasons, and both matter. An argument
that transfers on its own must be guarded before this one aims the
channel; and a call written in an argument has to be on the walk [Reach]
and [Load] already do, which a list hanging off a node they treat as a
leaf would not be. [rsig] is the argument types as written, and [rsig_id]
their hash — §3's run-time check, since the name is resolved on a stack
nothing static can see. *)
| RestartCase of rclause list * expr
(* name id, name, arguments, their spelling, its hash, where *)
| InvokeRestart of int * string * expr list * string * int * Loc.t
(* [Serror] is §2's diverging variant: the same lookup, type Never, and with
nothing transferring the program stops rather than carrying on. *)
and sigkind = Ssignal | Serror
and place =
| Plocal of int
| Pglobal of string
| Pfield of expr * int
| 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 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
clause runs at the restart-case, which is where it was written.
[rparams] are the slots §3's parameters are bound to, in order, with their
types; the invoker stores into a buffer this frame owns and the clause loads
them from it. [rsig] is how those types are spelled and [rsig_id] its hash:
what the two ends compare, since neither can see the other. *)
and rclause =
{ rname_id : int; rname : string; rparams : (int * Types.t) list;
rsig : string; rsig_id : int; rbody : expr list }
(* [binds] are the slots the pattern's fields are bound to, in field order. *)
and arm = { acase : string option; binds : int list; abody : expr list }
type field = { fname : string; fty : Types.t }
type structure = { sname : string; fields : field list }
type variant = { vname : string; vfields : field list }
type union = { uname : string; cases : variant list }
type fn = {
name : string;
params : Types.t list; (* bound to slots 0 .. n-1, in order *)
slots : Types.t array; (* the frame: one entry per slot *)
(* What the source called each slot, parallel to [slots]. [None] is a slot
the compiler made up and no one wrote a name for -- [dotimes]'s hidden
bound, the pair (min) and (max) evaluate their operands into, the slot a
tail expression goes through. Names are otherwise gone from this IR (see
the header); this is the one exception, and it exists so a debug build can
emit a [!DILocalVariable] that says [lo] where the source said [lo]. A
backend is free to ignore it entirely -- nothing is *resolved* through it,
and a slot is still only ever referred to by index. *)
snames : string option array;
ret : Types.t;
body : expr list;
(* The defers again, innermost first. [body] already has them spliced onto
the normal exit path; this is the same list for the *transfer* exit path,
which leaves through a landing block the backend builds and no form in
[body] can reach. spec-conditions.md §5: they run, and errdefer does not. *)
fdefers : expr list;
(* Set on a function the checker made up rather than one anyone wrote: a
handler-bind clause, lifted out of the function named here. It is reached
by address from that function's body and from nowhere else, so it needs no
cell and no registry slot, and a redefinition of the parent carries its
own copy. *)
fparent : string option;
floc : Loc.t;
}
(* [gfolded] is the difference between a constant whose value the *checker*
consumed — an array length, decided before any type resolves — and one that
is only ever read at run time. The first is in the program's shape and can
never be reloaded; the second is just bytes in memory and can. Nothing else
can tell them apart afterwards, so it is recorded here. *)
type global = {
gname : string;
gty : Types.t;
ginit : expr;
gconst : bool;
gfolded : bool;
}
(* A foreign function: no body, and [esym] is the symbol the linker sees. The
aggregate calling convention is not modelled here — a C shim flattens every
struct that crosses the boundary, so clang classifies it per target and
nothing in the backend has to know x86-64 from arm64 from wasm32. *)
type extern = {
ename : string; (* the Flan name, e.g. rl/init-window *)
esym : string; (* the C symbol *)
eparams : Types.t list;
eret : Types.t;
}
type program = {
structs : structure list;
unions : union list;
globals : global list; (* in declaration order *)
externs : extern list;
fns : fn list;
(* The C the program's own (declare-c ...) forms generated, if any: one
translation unit, compiled into the build like a package's hand-written
.c file. It is on the program rather than beside it so that every driver
— the CLI, the REPL, the acceptance table — carries it without knowing
it exists. See [Shim]. *)
(* The generated FFI shim, in parts keyed by the declaration each serves,
with "" for the shared preamble. Parts rather than one string so that
[Reach.link] can drop a wrapper whose binding nothing reachable calls. *)
cshim : (string * string) list;
}
let field_index (s : structure) name =
let rec go i = function
| [] -> None
| f :: rest -> if String.equal f.fname name then Some i else go (i + 1) rest
in
go 0 s.fields