flan/lib/ast.ml
Joseph Ferano 008165335d break crosses only sometimes, so the refusal is relative now
return is refused inside handler-bind and restart-case blanketly, and rightly:
a return always crosses the frames they pushed. A break does not. A loop
written wholly inside a restart-case body has a perfectly good local break, so
the rule is a barrier on the loop stack rather than a flag — a jump is refused
exactly when a barrier stands between it and the loop it names, and the message
says which construct. handler-bind and restart-case bodies are barriers, so is
a restart clause, so are a defer's forms; a handler clause is lifted into its
own function and needs no rule at all. in_frames is untouched: a return is the
special case where the target is always outside every barrier.

continue wanted the other blocker. check_dotimes folded its step onto the end
of the body, which a continue would jump past, so the counter would never
advance and the loop would hang. Tast.While carries a latch now — condition,
body, latch — the step goes there, and emit_while emits four blocks. A while's
latch is empty and folds away.

Labels are Odin's, in the head position: (while :outer c ...) and (break
:outer). A keyword there is unambiguous because a loop condition is never one,
so one label function serves while, until, dotimes, break and continue. It is
not a goto — the checker resolves a label against the loops the form is
lexically inside, so control can only leave a loop it is already in.

Break and Continue carry a relative depth rather than a name, because that is
what a backend already has: emit keeps one entry per While the way it keeps
one pad per frame, and indexes it.

Nothing in the prelude wants either. Every early exit there is a return from
the function, which break cannot replace; the sentinel-flag loop break exists
to remove does not appear in it. The two the compiler emits are that shape and
are the one place it cannot help — their sentinel is set inside a restart-case.

reach.ml and render.ml take the While arity change and nothing else.
2026-09-12 21:58:53 +07:00

168 lines
8.0 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
(* The [string option] is a loop label: [(while :outer c ...)]. A keyword in
that position is unambiguous because a loop condition is never one. *)
| While of string option * expr * expr list
| Return of expr option
(* Leaving a loop, and starting its next iteration. The [string option] is
the label of the loop meant, and [None] means the innermost. Neither is a
goto: the checker resolves the name against the loops this form is
lexically inside, so control can only leave a loop it is already in —
Odin's restriction, and what keeps it safe. *)
| Break of string option
| Continue of string 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 *)
(* (array 4 rl/Vector2) — a zeroed fixed array, given its count and its
element type. [n T] is the ordinary *type* syntax and already works
everywhere a type is expected; a [let] binding is the one position with no
type slot, so there [4 rl/Vector2] reads as a two-element [Arr] literal and
fails on an unknown name. This is that position's answer, and it says what
it does rather than looking like a vector of two things. *)
| ArrayOf of texpr (* the whole array type, built by Parse *)
(* 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 option * string * expr * expr list (* (dotimes :o [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