flan/lib/tast.ml
Joseph Ferano 93231e8c9e println, the structural printer, shared with the REPL
session.ml already had this: a compile-time walk over a Tast type that
emits the calls to print a value of it, handling every concrete type the
language has. It was dev-build-only and went to flan_dev_emit, and
prelude.ml justified the per-type print-* functions by saying a real
println had to wait for milestone 5 and generics. It did not. plan.org
specifies println as compiler-provided and per concrete type, which is
not overloading: there is nothing to dispatch on at run time and no
user-supplied printer to choose between, so no type variables appear.

The walk moves to render.ml, parameterised on an emitter and a slot
allocator. The emitter is five functions rather than five extern names
because the two sides are not both extern calls -- the REPL's are, and
stdout's compose a conversion with a write. The slot allocator differs
too: the REPL builds a thunk's frame, println takes slots from the
enclosing function being checked, once per call site.

Two runtime shims, both only reachable from the walk. flan_u64_to_bytes,
because routing u64 through the signed printer makes 0xFFFF...F read as
-1, which is the one way println could disagree with the REPL about a
value both can hold. flan_escape_bytes, so a string nested in a printed
structure is quoted and escaped -- same table as flan_dev_emit_str, noted
in both, because the REPL and println must not disagree about what a
struct looks like.

A string at top level prints raw and nested prints quoted. Not a conflict:
(println "hello") has to print hello, and a struct's string field has to
be distinguishable from the punctuation around it. The split is top-level
vs nested, so it lives in check.ml and not in the walk.

Found on the way: a field of an Option had no gep in emit.ml, so the
walk's Option arm had never run -- the REPL would have failed on one too.
Option is { i8, T } with no declared name, so its layout is now spelled
out. Nothing in the surface language reaches a field of an Option; the
printer does, to read the tag without unwrapping a None.

The print-* functions stay. They print without a newline, which println
cannot express -- slices.flan's show prints elements separated by spaces
-- and they are raw where print is structural.

println.flan covers every arm at -O0 and -O2: the u64, the raw/quoted
split, both Option arms, the depth and span caps, and the slice arm's
loop twice over plus once inside a dotimes, which is where per-call-site
slot allocation would show if it were per-iteration.
2026-09-12 04:55:42 +07:00

183 lines
8.1 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
(* 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. *)
| 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 *)
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