flan/lib/tast.ml
Joseph Ferano 335e817676 Two kinds of defconst, and only one of them is unreloadable
Refusing every defconst was right about the class and wrong about most of the
instances. A constant the checker consumed - (defconst rows (/ h c)), which
decides grid's type before anything else resolves - is in the shape of the
program and no store can reach it. A constant that is only ever read at run
time is just bytes in memory. sand's colors is the second kind, and tuning a
colour table live is exactly the thing you would want a dev loop for.

So a dev build emits every defconst as a mutable global rather than a constant.
LLVM can then no longer fold a read of it and a module can store into it, and a
changed one is published at the frame boundary the same way a new function body
is. Release builds emit constant and get all the folding back.

Tast.global.gfolded records which kind it is, because nothing downstream of the
checker can tell: env.consts holds exactly the constants the folding pass
consumed, and membership is the question "is this value in the program's
shape?". The session keys its refusal on that, with a message that says what
the constant is used for rather than just that it changed.

Verified against a running sand: sim/colors is accepted, sim/rows is refused
and says why.
2026-09-11 07:03:08 +07:00

132 lines
5.0 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
and place =
| Plocal of int
| Pglobal of string
| Pfield of expr * int
| Pindex of expr * expr list
| Pkey of expr * expr
| Pderef of expr
(* [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