flan/lib/ast.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

152 lines
6.9 KiB
OCaml

(** The AST: syntax with special forms recognised, before typing.
Sugar is gone by this point. [when], [unless], [cond] and [and]/[or] are
desugared into [If] and [Do]; they are compiler special forms until macros
arrive at milestone 5, so there is nothing to preserve for a macroexpander
to see yet.
Types here are *surface* type expressions, not resolved types. [Ptr] and
[Option] are still just names; the checker resolves them. *)
(* ── Type expressions ──────────────────────────────────────────────── *)
type texpr = { t : texpr_kind; tloc : Loc.t }
and texpr_kind =
| Tname of string (* i32 bool Cursor string *)
| Tslice of texpr (* [u8] ptr+len *)
| Tarray of len * texpr (* [4 f32] [rows [cols u32]] *)
| Tmap of texpr * texpr (* {string i32} *)
| Tapp of string * texpr list (* (Ptr Cursor) (Option f64) *)
| Tfn of texpr list * texpr (* (Fn [a a] bool) *)
(* An array length is an integer or a compile-time constant's name. *)
and len =
| Lint of int64
| Lname of string
(* ── Expressions ───────────────────────────────────────────────────── *)
type expr = { e : expr_kind; loc : Loc.t }
and expr_kind =
| Int of int64
| Float of float
| Byte of int
| Str of string
| Kw of string (* :space — coerced at typed call sites *)
| Quote of string (* 'skip-form — restart names *)
| Var of string
| Do of expr list
| Let of binding list * expr list
| If of expr * expr * expr option
| While of expr * expr list
| Return of expr option
| Set of place * expr
| Field of expr * string (* (.pos c) — auto-derefs one level *)
| Call of expr * expr list
| Match of expr * arm list
| Struct of string * (string * expr) list (* (Cursor {:src s}) *)
| Arr of expr list (* [0xE6B800FF ...] — a fixed array value *)
(* These bind names or alter control flow, so none of them can be a call. *)
| Fn of string list * expr list (* (fn [x y] ...) — non-escaping *)
| Dotimes of string * expr * expr list (* (dotimes [i n] ...) *)
| Defer of expr list (* runs on scope exit *)
| Unwrap of unwrap * expr (* (some x) / (try x) *)
(* (handler-bind [(Type [c] body ...) ...] body ...) — spec-conditions.md.
A clause binds a name for the condition, so this cannot be a call. *)
| HandlerBind of hclause list * expr list
| Signal of sigkind * expr (* (signal c) / (error c) *)
(* (restart-case body (name [p T] body ...) ...) and
(invoke-restart 'name arg ...). Both alter control flow, so neither can be
a call, and a clause binds its parameters — §3. *)
| RestartCase of expr * rclause list
| InvokeRestart of string * expr list
(* Two ways to signal, because they are two different things — §1 and §2.
[signal] returns Unit whatever it finds; [error] has type Never and, with
nothing transferring, the program stops. *)
and sigkind = Ssignal | Serror
and hclause = { hty : texpr; hname : string; hbody : expr list; hloc : Loc.t }
(* [rparams] are §3's inline annotations, the same name/type pairs a [defn]
takes. They are bound in the clause body and filled in by whatever invoked
the restart, which is why their count and types are checked at run time
(§3): a restart is found by name on a dynamic stack. *)
and rclause =
{ rname : string; rparams : field list; rbody : expr list; rloc : Loc.t }
(* Inline name/type pairs, as in [defn], [let] and [defstruct]. Here because a
restart clause's parameters are one, and a clause is part of an expression. *)
and field = { fname : string; fty : texpr; floc : Loc.t }
(* Two unwrap operators, because they are two different things — plan.org. *)
and unwrap = Usome | Utry
and binding = { bname : string; bty : texpr option; bval : expr; bloc : Loc.t }
(* The fixed list of assignable forms — spec-memory.md. Not setf. *)
and place =
| Pvar of string
| Pfield of expr * string (* (set (.hp e) v) *)
| Pindex of expr * expr list (* (set (at grid r c) v) *)
| Pderef of expr (* (set (deref p) v) *)
and arm = { pat : pattern; body : expr list; aloc : Loc.t }
and pattern =
| Pctor of string * string list (* (Some e) (Rect w h) None *)
| Pwild (* _ :else *)
(* ── Declarations ──────────────────────────────────────────────────── *)
type fn = {
name : string;
params : field list;
ret : texpr option; (* None means Unit *)
fbody : expr list;
nloc : Loc.t;
}
type decl = { d : decl_kind; dloc : Loc.t }
and decl_kind =
| Package of string
| Import of string * string (* alias, path *)
| Defalias of string * texpr
| Defstruct of string * field list
| Defunion of string * variant list
| Defn of fn
(* No body, so no [defn]: a foreign function, and the string is the C symbol
it is actually called by (plan.org, Types — [declare] is kept only where
there is no body). *)
| Declare of fn * string
(* The same, but written in the C library's own terms — structs by value,
strings as strings. [Shim] generates the C that flattens it and rewrites
this into a [Declare] plus an ordinary [Defn], so nothing downstream sees
one. Two forms and not one because [(declare f [p string] ...)] already
means "the symbol takes ptr+len", which is the opposite of what this
means. *)
| DeclareC of fn * string
(* Inline name/value pairs, as everywhere else. The members are what a
keyword at a call site resolves against. *)
| Defenum of string * (string * int64) list
(* value is optional: ZII. `uninit` opts out and is recorded as Uninit. *)
| Defvar of string * texpr option * init
| Defconst of string * texpr option * expr
and variant = { vname : string; vfields : field list; vloc : Loc.t }
and init = Zeroed | Uninit | Init of expr
(* Every top-level name a declaration introduces, whatever kind it is. There is
one top-level namespace, so this is both the set [Load] renames on an import
and the set [Check] refuses to see twice — one definition, so the two cannot
drift apart. *)
let declared_name (d : decl) =
match d.d with
| Defenum (n, _) | Defalias (n, _) | Defstruct (n, _) | Defunion (n, _)
| Defvar (n, _, _) | Defconst (n, _, _) -> Some n
| Declare (fn, _) | DeclareC (fn, _) | Defn fn -> Some fn.name
| Package _ | Import _ -> None