A let-bound local printed as s0 under lldb. Parameters were fine, because the driver recovered their names from the AST and handed them down in pnames; everything else was a slot index, since Check knew the name in its scope list and dropped it at allocation. Tast.fn now carries snames beside slots, Check fills it in at bind, and Emit prefers it over pnames. A slot the compiler invented keeps s<index>: fresh_slot takes the name as an optional argument, so dotimes' hidden bound and the pair min and max evaluate into say nothing and get None without any of their call sites changing. Naming those something plausible would put a variable in the debugger that is not in the file. Shadowing needed deciding rather than assuming. Every DILocalVariable is scoped to the subprogram — the typed IR has no block structure to build a DILexicalBlock from — so two slots called v landed in one flat scope, and lldb answered p v with the outer one while the body computed with the inner, which it did not list at all. A debugger confident and wrong is the one outcome worse than s0, so a repeat of a name already bound in this function gets a ~2 suffix: ~ is the reader's delimiter and cannot occur in a source symbol, so v~2 is unambiguous and visibly the compiler's. It is a way of not lying, not a way of being right; scoping properly means a lexical block per Let and the declares moved out of the entry block. (lldb) breakpoint set --file debug.flan --line 20 (lldb) frame variable (Cell *) c = 0x00007fffffffd970 (int) n = 41 (int) bump = 42 The test breaks after the binding on purpose. A name breakpoint stops on the function's first line, before the let has stored anything, and a variable is nominally in scope from entry — so the name is checked there and the value only where it means something.
188 lines
8.5 KiB
OCaml
188 lines
8.5 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 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. *)
|
||
| RestartCase of rclause list * expr
|
||
| InvokeRestart of int * string * Loc.t (* name id, name, where *)
|
||
|
||
(* [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. *)
|
||
and rclause = { rname_id : int; rname : string; 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
|