spec-conditions.md §1 and §2 and nothing else, because those two are worth having alone: signal returns Unit whatever it finds, a handler that returns normally leaves the signalling function to carry on, and with nothing matching it is a no-op. So none of §6's transfer machinery exists yet and no signature changed - which is the whole reason to do this step first. The runtime is a linked list. Establishing a handler is two stores and a push onto a frame on the establishing function's own stack, and signal with an empty stack is a null check, which is what §2 asks for. Popping is by frame rather than by count, so restoring what this one displaced is right even if something below it left the stack out of step. A condition's type is a hash of its name and not an index: an index would shift the moment a struct were added, and every handler a running program had already pushed would match the wrong type. The condition crosses as a pointer, since a handler runs while the signalling frame is alive and there is nothing to copy - but what the clause binds is the condition itself, the pointer being a hidden parameter and the name a slot loaded from it, so a handler passing c to something expecting the struct is not handed an address. A clause is lifted into a function of its own, because a handler runs from wherever the signal was and cannot be a branch in the function that wrote it. That gives two refusals, both by the house rule. 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. And return inside a handler-bind body is refused, since the frames are popped on the way out and an early exit would leave them pointing into a function that has gone. Settled in advance for the next step: in a dev build every function is transfer-transparent, because a cell can hold anything and the honest answer to what it can call is anything. Same bargain as the indirect call, and it means redefinition acquires no new refusal class. Still open is whether the discriminated result is returned by value or through an out-parameter.
143 lines
5.6 KiB
OCaml
143 lines
5.6 KiB
OCaml
(** 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
|
|
| 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 int * expr (* type id, the condition value *)
|
|
| Handled of hframe list * expr list
|
|
|
|
and place =
|
|
| Plocal of int
|
|
| Pglobal of string
|
|
| Pfield of expr * int
|
|
| Pindex of expr * expr list
|
|
| Pkey of expr * expr
|
|
| 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 }
|
|
|
|
(* [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 *)
|
|
ret : Types.t;
|
|
body : expr list;
|
|
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;
|
|
}
|
|
|
|
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
|