flan/lib/check.ml

21685 lines
1.0 MiB
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 checker: AST → typed IR.
Two passes, because top-level names in a package are order-independent
(plan.org, Modules): the first collects every type, signature and global,
the second checks bodies against them. Mutually recursive functions need no
forward declaration, and a struct may be used above where it is declared.
Checking is *bidirectional*. An expression is checked against an expected
type when there is one and inferred when there is not, which is what makes
[None], a bare [0] and a struct literal work without any inference engine:
the expected type flows in from the function's return type, the parameter
it is being passed to, or the field it is being stored in.
The rule from the two misparse bugs applies here too: *anything not yet
implemented is rejected by name*, never approximated. Milestone 2 is
calc-me.flan and nothing more (plan.org, Build sequence), so [Vec], [Map],
[Result]/[try], user data types, closures, [dotimes], [defer], generics and
cross-package imports are all errors with a message that says which
milestone they belong to. *)
let fail = Loc.fail
(* A type in a message about the code at [loc], in that code's syntax. *)
let tyname (loc : Loc.t) t = Types.spell ~indented:(Source.indented_at loc) t
(* "A literal could not be built at the type this site asked for": 300 at a u8,
1.5 at an i32, 3000000000 at the i32 an unconstrained integer defaults to.
It is kinded rather than left generic because one caller has to tell this
refusal apart from every other one. [binary] retries a refused operand
against the other operand's type (TODO.org, "Implicit numeric widening is
legal; narrowing stays a hard error"),
and it must not retry *this* one: a literal takes its width from the other
side and always could, so a literal that does not fit is the program's
mistake and not a pair of types that failed to meet. Without the kind the
retry turns (+ u8-thing 300) into i32 arithmetic, which is a different
language from the one the author decided on. *)
let literal_at_want = "check/literal-at-want"
(* A refusal that is two types failing to meet, a literal's included: what
an arm checked at another arm's type says when the two simply differ. *)
let is_mismatch (d : Loc.diag) =
String.equal d.Loc.kind "check/type-mismatch"
|| String.equal d.Loc.kind literal_at_want
(* [List.map]'s evaluation order is unspecified, and checking allocates frame
slots as a side effect. Left-to-right is required, not a preference: a later
let binding sees an earlier one, and slot numbering must be reproducible. *)
let rec map_lr f = function
| [] -> []
| x :: rest -> let y = f x in y :: map_lr f rest
let rec map2_lr f xs ys =
match xs, ys with
| [], [] -> []
| x :: xs, y :: ys -> let z = f x y in z :: map2_lr f xs ys
| _ -> invalid_arg "map2_lr"
(* ── Environments ──────────────────────────────────────────────────── *)
type binding = {
slot : int;
bty : Types.t;
assignable : bool; (* locals are places; parameters are not — spec-memory *)
(* Where the name came from, in words, when that is worth saying in a
refusal about it. Set by the match-arm path and nowhere else: a case
pattern binds the case's fields positionally, so [(Circle c)] over a
one-field case binds [c] to an [f64] and the reach for [(.r c)] gets a
type fact about [f64] instead of the one sentence that helps, which is
that the field is already in hand. [None] everywhere else, and a refusal
with [None] says exactly what it said before. *)
bwhat : string option;
(* The literal a [let] or [loop] bound this name to, when its type is read
off the uses ([lit_session]). The initialiser's node, by identity, is the
key: two expansions of one macro are two nodes. *)
blit : Ast.expr option;
}
(* A class slot's type: what a value stored into it is checked against. A
dyn value's tag is all a store can ask of it, so the scalar types a tag
answers for, an instance of a class, and (Option T) of either, which
admits nil as well. *)
type slot_ty =
| Sany (* no type written: any dyn value *)
| Sval of Types.t (* bool, an integer type, f32, f64, string *)
| Sclass of string (* an instance of this class *)
| Sopt of slot_ty (* nil, or a value of the inner type *)
let rec slot_text = function
| Sany -> "dyn"
| Sval t -> Types.to_string t
| Sclass c -> c
| Sopt s -> "(Option " ^ slot_text s ^ ")"
(* Where [sep] first occurs in [m]. *)
let find_sub m sep =
let n = String.length m and k = String.length sep in
let rec go i =
if i + k > n then None
else if String.sub m i k = sep then Some i
else go (i + 1)
in
go 0
(* ── Generic structs ─────────────────────────────────────────────────
[(defstruct Small [items [$n $t] count i32])] is a template, not a type.
Its parameters are the sigil names its fields introduce, in the order
first written — [$n] then [$t] here, so the type is spelled
[(Small 8 i32)] — and each is a length or a type by where it stands: in an
array's length slot, or in a generic struct's length argument, it is a
length; anywhere else a type.
Each application at concrete arguments is a copy: an ordinary struct under
a symbol-safe key, [Small-8-i32], so layout, both backends, the renderer
and DWARF see a struct and nothing else — the same arrangement a generic
function's copy has. [struct_apps] is how the checker still knows what a
copy was applied to, which is what binding [(defn push [s (Ptr (Small $n
$t))] ...)] against an argument needs. An application at variables is a
copy too, under a key with the variables in it, whose array lengths are
[abstract_len]; it exists for the abstract pass over a generic body and is
left out of the program. *)
type gstruct = {
gparams : (string * bool) list; (* name, and whether it is a length *)
gfields : Ast.field list;
gloc : Loc.t;
}
(* Key -> the generic struct and the arguments it was applied to; a length
argument is [Types.Len], a variable one [Types.Var]. Global for the reason
[Types.display] is: [bind_ty] and [subst_ty] are called from places with no
env in hand. The key is made from exactly these, so an entry can only
mislead where a later program in the same process declares a struct under
a copy's key by hand, and [struct_copy] refuses that name the moment the
program asks for the copy itself. *)
let struct_apps : (string, string * Types.t list) Hashtbl.t = Hashtbl.create 16
(* The undo journal a check that may be abandoned writes into: every table
write a body's check makes goes through [jreplace]/[jremove]/[jset], which
note how to take it back while a [snapshot_env] is open. So abandoning a
check costs what it wrote, not the size of the tables it could have. *)
let journal : (unit -> unit) list ref = ref []
let journal_open = ref 0
let jot undo = if !journal_open > 0 then journal := undo :: !journal
let jreplace tbl k v =
(if !journal_open > 0 then
let old = Hashtbl.find_opt tbl k in
jot (fun () ->
match old with
| Some o -> Hashtbl.replace tbl k o
| None -> Hashtbl.remove tbl k));
Hashtbl.replace tbl k v
let jremove tbl k =
(if !journal_open > 0 then
match Hashtbl.find_opt tbl k with
| Some o -> jot (fun () -> Hashtbl.replace tbl k o)
| None -> ());
Hashtbl.remove tbl k
let jset r v =
(if !journal_open > 0 then let old = !r in jot (fun () -> r := old));
r := v
(* The length every length variable has inside a generic body's abstract
pass. Large so that no constant index into such an array is refused as out
of bounds there, and within i32 so that [(length a)] is an ordinary index.
Every length is answered again, exactly, per copy. *)
let abstract_len = 2147483647L
type env = {
structs : (string, Tast.structure) Hashtbl.t;
datas : (string, Tast.data) Hashtbl.t;
(* The untagged unions, by name, and they are [Tast.structure] values on
purpose: a union's members *are* a field list, and every one of them is at
offset zero. Giving them a record of their own would have meant a second
shape for [field_index] and for every walk over a member list, to say
nothing new — which table the name is in is already what says whether the
offsets are cumulative or all zero, exactly as it is for a data type. *)
unions : (string, Tast.structure) Hashtbl.t;
(* Every data type case, twice over: once under its full spelling ["U.C"], which
is how a value of it is written, and once under the bare ["C"], which is
how a [match] arm names it and how a mistake spells a constructor. The
full spelling is a key rather than something split out of a dotted name at
the use site, because a data type's own name can contain a slash (an imported
[rl/U]) and may one day contain a dot; string surgery would own an edge
this does not have to.
The bare entry is deliberately last-writer-wins and is *only* used to say
"C is a case of U, write (U.C ...)". Two data types may share a case name —
construction is qualified and a pattern resolves against the scrutinee, so
both are unambiguous — and refusing that would be a restriction with no
mechanism behind it. *)
cases : (string, string * Tast.variant) Hashtbl.t;
aliases : (string, Ast.texpr) Hashtbl.t;
consts : (string, int64) Hashtbl.t; (* compile-time array lengths *)
locs : (string, Loc.t) Hashtbl.t; (* where each type was declared *)
(* Enum name -> its members, in declaration order. A keyword at a call site
resolves against this and nothing else. *)
enums : (string, (string * int64) list) Hashtbl.t;
(* A condition type -> the parent it names, [(defstruct T :parent P ...)].
Handler matching walks this chain; see [condition_chain]. *)
parents : (string, string) Hashtbl.t;
(* Flan name -> the C symbol it is really called by. A foreign function is an
ordinary entry in [fns] as well; this only records how to name it. *)
externs : (string, string) Hashtbl.t;
(* Where each [declare] was written, by Flan name. A second table rather
than a pair in [externs], because every other reader of that one wants
the symbol and nothing else. *)
extern_locs : (string, Loc.t) Hashtbl.t;
fns : (string, Types.t list * Types.t) Hashtbl.t;
(* The parameter vector as it was *written*, by function name: the names and
the locations [fns] threw away when it resolved the types. Nothing needs
it to compile; it exists so that a refusal at a call argument can point
at the parameter that wanted the other type, which is the second half of
every message in Elm and was the one thing the reader could not see from
the caret. Missing for a foreign [declare] and for a generic copy, and a
missing entry degrades to the message alone rather than to a wrong
pointer — [declared_note]'s rule. *)
fparams : (string, Ast.field list) Hashtbl.t;
(* And where the defn was written, for the same reason: a refusal about a
function can show it. Kept apart from [fparams] because a foreign
[declare] has a location and no parameter vector worth showing. *)
fn_locs : (string, Loc.t) Hashtbl.t;
(* A fn with several arities (decision 139): the name as written, to each
arity and the name it was renamed to ([version_name]). A fn with one
arity is not in here and keeps its own name, so its symbol does not
change. *)
versions : (string, (int * string) list) Hashtbl.t;
(* Every [defn-], by name, with where it was written and how far it is
visible. See [private_ref]. *)
privates : (string, Loc.t * Ast.privacy) Hashtbl.t;
globals : (string, Types.t * bool) Hashtbl.t; (* type, is a constant *)
(* Where each global was declared, so a refusal about one can show it. A
second table rather than a third field, because every other reader of
[globals] wants the type and the constness and nothing else. *)
global_locs : (string, Loc.t) Hashtbl.t;
(* Functions the checker made up: a handler-bind clause is lifted into one,
because a handler is called from wherever the signal was and cannot be a
branch in the function that established it. *)
mutable lifted : Tast.fn list;
(* ── Generics by monomorphisation (spike, milestone 5) ────────────────
A generic [defn] is *not* in [fns]: its signature mentions type variables
and nothing can be called at it. It lives here, as the AST it was written
as, and every call site turns it into an ordinary function with concrete
types. Odin's model exactly — [find_or_generate_polymorphic_procedure]
keeps the source [Entity] and hangs generated ones off it. *)
generics : (string, Ast.fn) Hashtbl.t;
(* Its signature as *written*: parameter and return types with [Types.Var]
in them. This is the pattern a call site matches its argument types
against to bind the variables. *)
gsigs : (string, string list * Types.t list * Types.t) Hashtbl.t;
(* The instantiation cache. Odin's [gen_procs] list, keyed the way Odin keys
it: a linear scan comparing whole concrete signatures with
[are_types_identical] — here [Types.equal] pairwise. Same types twice
means one copy. *)
insts : (string, (Types.t list * Types.t * string) list ref) Hashtbl.t;
(* The copies themselves, in the order they were generated. They are
ordinary [Tast.fn]s from here down; nothing in a backend knows they were
ever generic. *)
mutable instances : Tast.fn list;
(* The type variables in scope while a generic signature is being resolved.
Empty everywhere else, which is what keeps the lowercase rejection at
[resolve_name] the default. *)
mutable tyvars : string list;
(* What each of them is bound to while one instantiation's body is checked.
[resolve_name] consults it before anything else, so the body resolves
[t] to [i32] and every node under it is concrete. *)
mutable subst : (string * Types.t) list;
(* The [where] predicates in scope: what the abstract pass may assume about
the variables, and what each instantiation checks its concrete types
answer yes to. Empty everywhere a generic signature or body is not being
resolved, which is what keeps every refusal below the default. *)
mutable tvpreds : Ast.pred list;
(* The chain of instantiations currently being generated, innermost last:
the generic's name and the concrete parameter types each copy was asked
for. It is the refusal for a generic that instantiates itself without
end — [(defn grow [x $t] () (grow [x x]))] asks for a copy at [[2 t]],
which asks for one at [[2 [2 t]]], forever — and without it the checker
does not fail, it *hangs*, which through [Session.eval] is [C-c C-c]
hanging with the dev daemon wedged behind it.
The test is structural rather than a depth count. A depth count names a
number the programmer did not write and cannot act on; this names the
chain. Odin has no cap of its own to copy, so there was nothing to
borrow. *)
mutable chain : (string * Types.t list * Loc.t) list;
(* Generics whose abstract pass was refused and recorded, in a whole-file
check that goes on after a refusal. A call site still gets a copy's
signature, but its body is not checked again: every refusal the abstract
pass made would come back from the copy, at the same line, once per type
it was called at. *)
refused_generics : (string, unit) Hashtbl.t;
(* The generic structs, by name; see [gstruct]. *)
gstructs : (string, gstruct) Hashtbl.t;
(* The struct copies this env made, by key, and whether each is one at
variables — those are left out of the program. *)
copies : (string, bool) Hashtbl.t;
(* Templates whose own check was refused while [deferred] was collecting:
a use of one is a copy with no fields, so the refusal is said once, at
the defstruct, and nothing downstream repeats it. *)
broken : (string, unit) Hashtbl.t;
(* While a whole-file check collects every error, the refusals [collect]
can go on past — a generic struct's template, a where clause over a
length — are kept here instead of ending the pass. [None] everywhere
else, where they raise as before. *)
mutable deferred : Loc.diag list option;
(* A generic defn's length variables, by name: the ones of its [gsigs]
variables that are lengths. *)
glens : (string, string list) Hashtbl.t;
(* Which of [tyvars] are lengths. A length variable is also a value inside
the body — [n] reads as the integer it was bound to. *)
mutable lenvars : string list;
(* Set while a generic body is checked abstractly, and while a struct copy
at variables is laid out: a length variable's array is then
[abstract_len] long rather than the [Types.LArray] a signature pattern
needs. *)
mutable len_placeholder : bool;
(* The struct copies being laid out, innermost last, so a template that
asks for a copy of itself at a bigger type is refused rather than
followed forever. *)
mutable schain : (string * Types.t list) list;
(* Set while a struct, data-case or union field's type is being resolved,
and only then. It exists for one message: an unknown lowercase name in a
type slot is told to introduce a type variable with [$name] in the
parameter vector, and a field has no parameter vector — a defstruct's
field introduces one where it stands. The flag is what lets
[resolve_name] say the honest thing in each place. *)
mutable in_field : bool;
(* Every [defclass], by name: its slots in constructor order, each with the
type a value stored in it must have — [Types.Dyn] for a slot written
with no type. Filled by [pair_decls], which is where a slot vector is
first readable. The type is a declaration about the values and not a
layout: an instance is a dyn map whatever this says, and what reads it
is [class_spec], which is what the runtime checks a store against. *)
classes : (string, (string * slot_ty) list) Hashtbl.t;
(* The bindings a dev build counts at every call: [Shim.resources], read off
the declare-c forms before [Shim.expand] rewrites them. Keyed by the Flan
name a program calls. *)
tracks : (string, Shim.track) Hashtbl.t;
(* Every [defn] whose return type was read off its body ([_]), with the
form that decided it — what a stale-caller warning points at. *)
inferred : (string, Loc.t) Hashtbl.t;
(* The [_] bodies whose type could not be read because of an error of
their own, while every error is being collected. Each stands in [fns]
as Never, a call to one stands as a poison, and pass two reports the
body's errors once. *)
infer_failed : (string, Loc.diag option) Hashtbl.t;
(* Recovery: checking goes on past a refused subexpression. See [check].
[recovering] is on only while a whole-file or session check is collecting
every error; [recovered] is what it found, newest first; [poison] counts
failed subexpressions and reads of what they were bound to, which is how
an error caused by an earlier one is told apart and left unsaid.
[speculating] turns recovery off inside a trial, whose refusal is an
answer the caller acts on; [guard_next] turns it off for the one next
[check], whose own refusal a caller re-words. *)
mutable recovering : bool;
mutable recovered : Loc.diag list;
mutable poison : int;
mutable speculating : int;
mutable guard_next : bool;
}
(* The struct table [box] describes a struct from when it was handed no
[ctx]: the newest program's, set by [new_env]. Every caller that has a
[ctx] passes it, so its own program's table is the one read. *)
let view_structs : (string, Tast.structure) Hashtbl.t ref = ref (Hashtbl.create 1)
(* The global whose initialiser is being checked, and its form. A view taken
there of storage the initialiser itself built is gone the moment the
initialiser returns, so it is refused rather than left to the dev check:
nothing could ever read it. *)
let view_global_init : (string * Ast.reinit) option ref = ref None
let rec new_env () =
let env = new_env_record () in
view_structs := env.structs;
env
and new_env_record () = {
structs = Hashtbl.create 16;
datas = Hashtbl.create 16;
unions = Hashtbl.create 16;
cases = Hashtbl.create 32;
aliases = Hashtbl.create 16;
consts = Hashtbl.create 16;
locs = Hashtbl.create 16;
enums = Hashtbl.create 8;
parents = Hashtbl.create 8;
externs = Hashtbl.create 32;
extern_locs = Hashtbl.create 32;
fns = Hashtbl.create 32;
fparams = Hashtbl.create 32;
fn_locs = Hashtbl.create 32;
versions = Hashtbl.create 4;
privates = Hashtbl.create 8;
globals = Hashtbl.create 16;
global_locs = Hashtbl.create 16;
lifted = [];
generics = Hashtbl.create 8;
gsigs = Hashtbl.create 8;
insts = Hashtbl.create 8;
instances = [];
tyvars = [];
subst = [];
tvpreds = [];
chain = [];
refused_generics = Hashtbl.create 4;
gstructs = Hashtbl.create 4;
copies = Hashtbl.create 8;
broken = Hashtbl.create 2;
deferred = None;
glens = Hashtbl.create 8;
lenvars = [];
len_placeholder = false;
schain = [];
in_field = false;
classes = Hashtbl.create 8;
tracks = Hashtbl.create 16;
inferred = Hashtbl.create 8;
infer_failed = Hashtbl.create 4;
recovering = false;
recovered = [];
poison = 0;
speculating = 0;
guard_next = false;
}
(* A check that may be abandoned — a trial, a probe, a return type read and
thrown away, a tolerated body — opens one of these: [undo] puts back
everything it wrote into [env], [keep] closes it and leaves the writes,
which an enclosing one can still undo. Partial undo is the bug this
exists for: rewinding [lifted] and not the generic cache left a copy made
during a trial with the lambda it lifted gone, which is a link error. So
the whole record is named, closed with warning 9: a new field stops this
compiling until it is decided here.
The tables a body's check writes go through the journal ([jreplace]);
the lists and flags are held here, which costs nothing; the rest is
written by the declaration passes alone, before any body is checked. *)
let snapshot_env env : (unit -> unit) * (unit -> unit) =
let[@warning "+9"] { lifted; instances; tyvars; subst; tvpreds; chain;
deferred; lenvars; len_placeholder; schain; in_field;
recovering; recovered; poison; speculating;
guard_next;
(* Journaled at their writes. *)
structs = _; locs = _; copies = _; insts = _; fns = _;
(* Declaration passes only. *)
datas = _; unions = _; cases = _; aliases = _;
consts = _; enums = _; parents = _; externs = _;
extern_locs = _; fparams = _; fn_locs = _;
versions = _; privates = _; globals = _; global_locs = _;
generics = _; gsigs = _; refused_generics = _;
gstructs = _; broken = _; glens = _; classes = _;
tracks = _; inferred = _; infer_failed = _ } = env in
incr journal_open;
let mark = !journal in
let close () =
decr journal_open;
if !journal_open = 0 then journal := []
in
let undo () =
let rec back l =
if l != mark then
match l with
| u :: rest -> u (); back rest
| [] -> ()
in
back !journal;
journal := mark;
close ();
env.lifted <- lifted; env.instances <- instances; env.tyvars <- tyvars;
env.subst <- subst; env.tvpreds <- tvpreds; env.chain <- chain;
env.deferred <- deferred; env.lenvars <- lenvars;
env.len_placeholder <- len_placeholder; env.schain <- schain;
env.in_field <- in_field; env.recovering <- recovering;
env.recovered <- recovered; env.poison <- poison;
env.speculating <- speculating; env.guard_next <- guard_next
in
(undo, close)
(* A refusal [collect] can go on past: kept while a whole-file check is
collecting, in the order found, and raised otherwise. *)
let defer_or_raise env (d : Loc.diag) =
match env.deferred with
| Some l -> env.deferred <- Some (d :: l)
| None -> Loc.raise_diag d
(* Where a named type was declared, and what it has, as a note.
This is the second half of the two-place messages: a refusal that says
[Cursor has no field pos] is true, and the reader's next move is always to
go and look at Cursor. Attaching the declaration's location and its actual
field names means the answer arrives with the question, and [next-error]
will take you there because a note prints as an entry of its own. Empty when
the name is not one this environment placed, so it degrades to the message
alone rather than to a wrong pointer. *)
let declared_note env name =
(* A generic struct's copy is declared where its template is, and is
spoken of by the template's name there. *)
let shown =
match Hashtbl.find_opt struct_apps name with
| Some (g, _) when Hashtbl.mem env.copies name -> g
| _ -> name
in
match Hashtbl.find_opt env.locs name with
| None -> []
| Some at ->
let names =
match Hashtbl.find_opt env.structs name with
| Some s -> List.map (fun (f : Tast.field) -> f.Tast.fname) s.Tast.fields
| None ->
(match Hashtbl.find_opt env.datas name with
| Some u -> List.map (fun (c : Tast.variant) -> c.Tast.vname) u.Tast.cases
| None ->
match Hashtbl.find_opt env.unions name with
| Some u -> List.map (fun (f : Tast.field) -> f.Tast.fname) u.Tast.fields
| None -> [])
in
let what =
if names = [] then shown ^ " is declared here"
else shown ^ " is declared here, with " ^ String.concat ", " names
in
[ Loc.note at what ]
(* One edit apart: a substitution, an insertion, a deletion, or a transposition
of neighbours. Bounded at one, because two edits is no longer a typo, it is
a guess. Hoisted out of [near_miss] so that the did-you-mean over *values* —
function names, globals, locals — matches on exactly the same rule the one
over types has always matched on, rather than on a second one that would
drift. *)
let one_edit a b =
let la = String.length a and lb = String.length b in
if abs (la - lb) > 1 then false
else begin
(* Walk both until they diverge, then require the tails to match with the
single edit applied. *)
let i = ref 0 in
while !i < la && !i < lb && a.[!i] = b.[!i] do incr i done;
let ta s k = String.sub s k (String.length s - k) in
if la = lb then
!i < la
&& (ta a (!i + 1) = ta b (!i + 1)
(* stirng/string: two neighbours swapped. *)
|| (!i + 1 < la && a.[!i] = b.[!i + 1] && a.[!i + 1] = b.[!i]
&& ta a (!i + 2) = ta b (!i + 2)))
else if la < lb then ta a !i = ta b (!i + 1)
else ta a (!i + 1) = ta b !i
end
(* The same question asked of a candidate list the caller assembles, which for
a value position is the function table, the globals and whatever is in
scope — and nothing from the type tables, because a name written where a
value goes was not a mistyped struct. *)
let nearest cands n = List.find_opt (fun c -> c <> n && one_edit n c) cands
(* What the last language called it. [long] is two edits from [i64] and so is
outside [one_edit]'s net, which is right — two edits is a guess — but the
name is not a guess at all: it is what C, Java, Go and Python spell an
integer, and somebody writing it here has not mistyped anything, they have
not yet learned that this language sizes its integers in the name. Without
this list [long] falls through to the lowercase arm of [resolve_name] and
is reported as generic code over a type variable, which is a sentence about
a feature the reader was not reaching for.
Two names that belong on this list by that reasoning are *not* on it:
[int] and [float]. They are builtin aliases as of 2026-09-20 — [int] is
[i32] and [float] is [f32], spelled in [Types.ikind_of_name] and
[Types.fkind_of_name] — so they resolve rather than teach, and a row for
either here would be dead code the next reader would trust. The exception
stops at those two, on the author's decision: the default integer and the
default float are the two spellings a program reaches for constantly, and
[integer], [long], [double], [str] and the rest keep teaching. So [integer]
still answers [i32] here even though [int] no longer does — the sibling
spelling is still a name this language does not have.
Short on purpose, and only names with one honest answer. [char] is not
here: it is this language's own code point type. Nor [void]: it is a return type and the
answer there is the shape [()], which is [parse]'s message to give and not
this one's. Nor [usize] and [size_t]: the honest answer is "as wide as a
pointer on this target", which is [u64] on x86-64 and [u32] on wasm32, and
a message that named one of them would be wrong half the time on a tree
that builds both. A name goes on this list when the answer does not depend
on anything. *)
let foreign_spelling = function
| "integer" -> Some "i32"
| "uint" | "unsigned" -> Some "u32"
| "long" -> Some "i64"
| "ulong" -> Some "u64"
| "short" -> Some "i16"
| "ushort" -> Some "u16"
| "byte" -> Some "u8"
| "double" -> Some "f64"
| "boolean" -> Some "bool"
| "string" -> Some "str"
| _ -> None
(* The builtin names, for the did-you-mean at a call — [prinltn] is a typo for
[println], and [println] is not in any table the checker keeps, it is an arm
of the call dispatch. The full [builtins] table is a long way below this
point and carries a signature and a sentence per entry for eldoc; a forward
reference to its names is cheaper than moving it or writing the list twice
and letting the two drift. Filled once, immediately after that table. *)
let builtin_names : string list ref = ref []
(* The same names as a set, and the two are not one because they are asked
two different questions. The list above is read once, at a refusal, and
its order is the order the did-you-mean walks. This is asked at *every*
named call — [shadows_builtin] is the first arm of the dispatch — and a
linear walk of eighty-odd strings per call is a cost a whole-program check
pays in full: measured at about a third of check time on a program of
twenty thousand calls. Filled beside the list. *)
let builtin_set : (string, unit) Hashtbl.t = Hashtbl.create 128
(* Set while [bool_operands] asks an operand its type; see there. *)
let probing = ref false
(* ── builtin/, the reserved qualifier ──────────────────────────────────
[builtin/length] is the builtin [length], whatever else the program has
decided [length] means. It is the way out of the dead end shadowing used to leave: a
[(defn length ...)] takes the bare name over for its whole file, and before
this there was no remaining spelling for the thing it was wrapping, so the
wrapper was unbounded recursion instead.
The spelling is the package qualifier's, deliberately. A reader who knows
that [rl/draw-fps] is [draw-fps] from the package imported as [rl] already
knows what [builtin/length] is, and needs no second syntax to learn. What
makes it work is that [builtin] is reserved rather than resolved: [Load]'s
qualifier comes from the alias in an [import] form and from nowhere else,
so refusing that one alias ([Load.reserved_alias]) is the whole of keeping
this prefix unambiguous.
It is legal when nothing is shadowed, too. A spelling that only compiles
while some other declaration exists is a spelling nobody can write down in
advance, and the point of an escape hatch is that it is always there. *)
let builtin_prefix = "builtin/"
let qualified_builtin n =
let p = String.length builtin_prefix in
if String.length n >= p && String.sub n 0 p = builtin_prefix then
Some (String.sub n p (String.length n - p))
else None
(* [builtin/] reached with something that is not a builtin's name. The
did-you-mean is over the builtins alone and not over the program's own
names: the reader wrote the qualifier, so they were reaching for a
compiler name, and offering them a defn called [lem] would be answering a
question they did not ask. Every other did-you-mean in this file keeps the
candidates it already had. *)
let not_a_builtin loc bare =
if bare = "" then
Loc.failk "check/unknown-builtin" loc
"%s needs a name after it — the qualifier reaches a builtin, as \
(%slength v)" builtin_prefix builtin_prefix
else
match nearest !builtin_names bare with
| Some m ->
Loc.failk "check/unknown-builtin" loc
"%s is not a builtin, so %s%s reaches nothing — did you mean %s%s?"
bare builtin_prefix bare builtin_prefix m
| None ->
Loc.failk "check/unknown-builtin" loc
"%s is not a builtin, so %s%s reaches nothing. The %s qualifier \
reaches the compiler's own names and nothing else; an ordinary \
function is called by the name it was defined under"
bare builtin_prefix bare builtin_prefix
(* One argument of a call, written back out as source. A name is its name and
an integer is its digits; anything with structure inside it — a call, a
field, an index — becomes the stand-in the caller supplies, because a
suggestion with a hole in it is worse than one that names its blank. Shared
by the refusals that answer a name nothing defines by writing the call the
reader should have written.
This spells one argument and says nothing about how many there are. Whether
the result compiles is the caller's to arrange: a call site that writes out
every argument it was given is only honest where the name it is suggesting
takes that many, so the caller either knows the arity matches or falls back
to a shape of its own. *)
let spell_arg stand_for (a : Ast.expr) =
match a.Ast.e with
| Ast.Var v -> v
| Ast.Int n -> Int64.to_string n
| Ast.UInt (_, s) -> s
| _ -> stand_for
(* Operators other languages spell differently, each mapped to the Flan
builtin that computes the same thing. Only exact equivalents: [mod] is left
out because Clojure's is floored and [%] is not. [&&] and [||] are not
here: they are the bit operators, and a bool reaching one is told which
logical operator it wanted there. *)
let operator_aliases =
[ ("not=", ("!=", "Not-equal")); ("=/=", ("!=", "Not-equal"));
("/=", ("!=", "Not-equal")); ("<>", ("!=", "Not-equal"));
("==", ("=", "Equality")); ("===", ("=", "Equality"));
("!", ("not", "Logical not")) ]
(* The fix, as the sentence that ends the refusal. The reader's call is
written back out under the Flan name only when every argument can be
spelled and the count is one the builtin takes, so a suggestion printed as
code compiles once pasted. An argument that cannot be spelled leaves the
call as it was, with only the name to change; a count the builtin does not
take gets the builtin's shape. *)
let alias_fix flan (args : Ast.expr list) =
let spelled = List.map (spell_arg "") args in
let n = List.length args in
let arity_ok =
match flan with
| "not" -> n = 1
| "and" | "or" -> true
| _ -> n >= 2
in
if not arity_ok then
Printf.sprintf "It is called as %s"
(if flan = "not" then "(not x)" else "(" ^ flan ^ " x y)")
else if List.mem "" spelled then Printf.sprintf "Write %s in its place" flan
else Printf.sprintf "Write (%s)" (String.concat " " (flan :: spelled))
(* What a [break] or a [continue] may be talking about, innermost first.
[Lloop] is a loop it is lexically inside, carrying its label if it was given
one. [Lbarrier] is something a jump may not cross, named so the refusal can
say which — and the barriers are the whole of the answer to the question
[return]'s [in_frames] rule could not answer.
[return] is refused inside a [handler-bind] or a [restart-case] blanketly,
because a return *always* crosses the frames established there and leaves
them on the stack pointing into a frame that has gone. A break crosses only
sometimes: a loop written wholly inside a [restart-case] body has a
perfectly good local break, and refusing it would be refusing the common
case for the uncommon one. So the rule here is relative rather than blanket
— a jump is refused exactly when a barrier stands between it and the loop it
names — and the two rules agree on the case they share, because a [return]
is a jump whose target is always outside every barrier.
A [defer]'s forms are a barrier for a different reason with the same shape:
they are copied into the function's exit paths, where the loop they were
written next to no longer exists. A loop *inside* the defer is fine, which
is again the relative rule and not a blanket one.
A handler clause is not on this list at all: it is lifted into a function of
its own and gets a fresh [ctx], so its loops start empty and nothing inside
it can name a loop outside it. *)
type lentry =
| Lloop of string option
(* A [(loop ...)], carrying the slot and type of each of its names so that a
[recur] can rebind them. It is *also* a barrier for [break] and
[continue]: a loop answers with the value of its body, so a jump that left
one would have no value to give. A [while] written inside a loop is
unaffected, which is the relative rule doing its job again. *)
| Lrecur of (int * Types.t) list
| Lbarrier of string
(* Local inference for a number or character literal bound by a [let] or a
[loop]: [(let [t 0.0] ... (set t (+ t x)))] makes [t] x's type. The form
is checked with every literal local at its current guess while each use
records what it says about the local. When the uses agree with the
guesses that check is the answer; otherwise it is undone, the guesses are
solved and it is checked again. Locals that feed one another are merged
into one group (union-find), so a chain of any length settles in one more
round. One session per function context, opened by its outermost such
[let], so a lambda or a generic's copy is inferred on its own and a
literal's type never depends on another function.
What a use says about the local:
- [Up t]: the local flows into a [t] — a parameter, a return, a field, an
index. The local has to widen into [t].
- [Down t]: a [t] is [set] into it, or passed to [recur] for it. [t] has to
widen into the local.
- [Hint t]: an operator's other operand, which meets it at either; or a
dyn it meets, as the dyn width ([Types.Dyn] here, i64 or f64 in [solve]).
A [set] of arithmetic over such locals and literals into another merges
them, as does an operator between two of them. *)
type lit_con = Up | Down | Hint
(* Initialiser nodes by identity: two expansions of one macro are two nodes
and may print alike. *)
module Phys = Hashtbl.Make (struct
type t = Ast.expr
let equal = ( == )
let hash = Hashtbl.hash
end)
type lit_session = {
(* The type each literal local is checked at. Kept across rounds. *)
decided : Types.t Phys.t;
(* On while a round checks; off for a final check after one that failed. *)
mutable recording : bool;
(* This round's locals, numbered as they are bound, and their names. *)
ids : int Phys.t;
mutable keys : (Ast.expr * string) list;
mutable count : int;
(* Union-find over the numbers, and each one's uses. *)
parent : (int, int) Hashtbl.t;
cons : (int, lit_con * Types.t * Loc.t) Hashtbl.t;
(* A use its guess could not serve was read at the type it asked for, so
this round's check is not a program and is thrown away. *)
mutable dirty : bool;
}
(* Nonzero while any recording check runs: the refusal memos ([arm_failed],
[if_failed], [truthy_failed]) are not written then, since a refusal made
at a guessed type must not be replayed at the decided one. *)
let lit_recording = ref 0
(* The operands of the operators being checked that are literal locals, by
location: a want reaching one is the other operand's type, a [Hint] and not
an [Up], and a refusal there is the operator's to handle. *)
let lit_operand_locs : Loc.t list ref = ref []
(* Set while arithmetic over literal locals is checked at the type of the
local it is stored into ([lit_down]): the locals in it are merged with that
one, so the guess it is checked at says nothing about them. *)
let lit_quiet = ref false
(* Text and bracket literals keep their typed reading while this is set (the
[dyn] switch only): a [defconst]'s value, and a let-bound one some typed
use wants. *)
let typed_literals = ref false
let with_typed_literals f =
let was = !typed_literals in
typed_literals := true;
Fun.protect ~finally:(fun () -> typed_literals := was) f
(* Per-function state. Slots are never reused, so [slots] is also the frame
size — the interpreter allocates one array of this length per call. *)
type ctx = {
env : env;
ret : Types.t;
(* The literal-inference session of this function, while one is open. *)
mutable lits : lit_session option;
mutable slots : int;
(* The type of each slot, newest first. A backend needs it to size the
frame — nothing else records it, since the IR refers to slots by index. *)
mutable slot_tys : Types.t list;
(* The source name of each slot, newest first, parallel to [slot_tys].
[None] for a slot the checker invented -- see [Tast.fn.snames]. Recorded
here rather than recovered later because this scope list is the only place
that ever knows it. *)
mutable slot_names : string option list;
(* The slots an [as] bound in this function: [Tast.fn.as_slots]. *)
mutable as_slots : int list;
mutable scope : (string * binding) list; (* innermost first *)
(* Deferred forms, most recently registered first — which is also the order
they run in. [defer] is function-scoped, so this list belongs to the
function and not to a block — see [defer_ok] for where one may be written
and [check_fn] for where the list is spliced onto the exit paths. *)
mutable defers : Tast.expr list;
(* The slot that counts how many of them have registered, minted on the
first [defer] this function writes and [None] until then.
The normal exit paths need no such thing: falling off the end is below
every defer in the text, and a [return] splices the ones registered
above it, both of which are decided while checking. The *transfer* exit
is the one path that is not, because a transfer can start anywhere,
including in the initialiser of the very [let] whose body the defer is
written in — and that defer has not registered yet. Running it there is
not a leak the other way round; it is cleanup over a binding nothing has
written, which is [(free v)] on whatever the stack held.
So the count is kept at run time and the transfer path's copy of each
defer is guarded on it. The cost is not all on the unwinding path, and
the half that is not is the half worth naming: every function with a
defer pays one i64 of frame, one store of zero at entry, and one store
of an ordinal where each defer is written — on the ordinary path,
whether anything ever transfers or not. What the unwind adds on top is
one compare per defer. A store of a constant into a frame slot nothing
else reads is about as little as a fact can cost, but it is not nothing
and it is not only on the cold path. *)
mutable defer_slot : int option;
(* Where a [defer] may be written, which is exactly: a form whose extent is
the whole function body. Two things have that extent and only two — a
top-level form of the body, and a form in the body of a [let] that itself
has it, to any depth. A [let] always registers and its bindings outlive
the block textually enclosing them, because a [let] is not a frame here:
its bindings are function slots like any other, and nothing is released at
scope exit (spec-memory.md, "When storage is released").
Everything else is refused, and the two that matter are refused for a
reason rather than by omission. [defer] is a *compile-time* construct —
the cleanup is copied into every exit path — so a branch would have to
express "maybe registered", which it cannot, and a loop body would fire
once at function exit rather than once per iteration.
The flag is set immediately before each form that may carry one, never
once around a body: [check] clears it on entry, so a body whose first form
set it would otherwise refuse the second. [defer_block] names the
innermost construct that cleared it, so the refusal says which. *)
mutable defer_ok : bool;
mutable defer_block : string;
(* Only for the two things a handler clause cannot do. [outer] is the
establishing function's scope, kept so that a reference to one of its
locals can be refused for the reason it is really refused for rather than
as an unknown name. *)
outer : (string * binding) list;
(* Set on the context of a body the checker lifted into a function of its
own — a handler clause, or an [fn] literal — and naming which, so the
refusal below says why the enclosing function's locals are not there. Both
are the same gap: capture does not exist. *)
mutable outer_what : string option;
(* What this body has captured out of [outer], in the order it first named
each one: the source name, the *outer* binding the copy is taken from,
and the slot in this body's own frame the copy is read into. Empty
everywhere [outer] is, which is everywhere but a lifted body.
The order is the environment's field order, so it is the order the copy
is made in and the order the body reads it back in. First-reference order
rather than declaration order because it is the only one this pass has:
the outer scope is a list and the names on it were not all written for
this body's sake. *)
mutable caught : (string * (binding * int)) list;
(* Whether the form being checked is the target of a place — indexed,
sliced, a field read, its address taken — rather than a value. Granted by
[check_target] to the one form it checks and withdrawn at the top of
[check]. See [refuse_owned_copy]. *)
mutable place_ok : bool;
(* The context this body was lifted out of, so that capture can be
transitive: an [fn] inside an [fn] naming a local of the function both
were written in is captured by the middle one and then by the inner one
out of the middle one's copy. Without it the inner body would see only
what the middle body happened to have named already, which is a rule
about the text and not about the scope.
It is safe to reach into while it is on the stack, and only then: a
lifted body is checked at the point it is written, so the parent is
paused exactly there and its scope is the snapshot [outer] holds. *)
parent : ctx option;
(* The slot the environment pointer arrives in, minted the first time
something is captured and [None] until then. Nameless, so the break loop
hides it the way it hides every other slot the compiler made: what a
reader wants to see is the copies, and those are under the names the
source gave them. *)
mutable envslot : int option;
(* True wherever handler or restart frames established by this function are
on the stack. A [return] from there would leave them pointing into a frame
that has gone, so it is refused — the same rule as [defer] inside a
block. *)
mutable in_frames : string option;
(* The loops and the barriers this form is inside, innermost first. See
[lentry]: it is what [break] and [continue] resolve against, and the whole
of why they are not a goto — a label that names no loop on this list is
refused, so control can only leave a loop it is already in. *)
mutable loops : lentry list;
(* True where this form's value is the value of the enclosing [loop]'s body,
which is the only place a [recur] may stand. Read and withdrawn at the top
of [check] exactly as [defer_ok] is, and granted again by the three forms
that pass a tail through: the last form of a block, both arms of an [if],
and a [match] arm. Everything else is therefore non-tail by construction,
and no walk has to enumerate the cases that are not. *)
mutable tail : bool;
(* True where this form's value is kept: a [let] binding's value, and what
a block's last form, an [if]'s arms and a [match]'s arms inherit from
the form they stand in. Read and withdrawn at the top of [check] as
[tail] is. Only a one-armed [if] ([when]) asks: used, it answers an
Option; not, it is a statement. [want] alone cannot say, since a
statement and an unannotated [let] value both arrive with none. *)
mutable used : bool;
(* The arguments of the call being checked: each is kept, whatever the
callee wants of it, so a [when] written as an operand answers its Option
there rather than a Unit the other operands are then blamed against. *)
mutable kept : Ast.expr list;
(* True inside a [defer]'s forms. A defer is the cleanup a transfer runs on
its way out (§5), so a transfer *starting* there has no answer: this
function's defers are already half run and the first transfer's target is
already in hand. Refused where it is written. *)
mutable in_defer : bool;
(* The function being checked, so a clause lifted out of it can be named
after it. The name has to be stable and has to say whose it is: a
redefinition module emits the clauses belonging to the bodies it is
replacing, and nothing else in the program can tell it which those are. *)
owner : string;
}
(* [?name] is the source name, when there is one. It is optional so that the
several places that allocate a hidden slot say nothing and get [None] --
a synthesized slot cannot accidentally acquire a name it was never given. *)
let fresh_slot ?name ctx ty =
let s = ctx.slots in
ctx.slots <- s + 1;
ctx.slot_tys <- ty :: ctx.slot_tys;
ctx.slot_names <- name :: ctx.slot_names;
s
(* The structural printer's context over this checker's tables, with the
pieces aimed wherever [emit] sends them. [print] and [watch] differ in the
emitter and in nothing else, and a second copy of this would be a second
answer to which types the walk knows. *)
let render_ctx ctx (emit : Render.emitter) : Render.ctx =
{ Render.structs = Hashtbl.fold (fun _ v acc -> v :: acc) ctx.env.structs [];
datas = Hashtbl.fold (fun _ v acc -> v :: acc) ctx.env.datas [];
unions = Hashtbl.fold (fun _ v acc -> v :: acc) ctx.env.unions [];
enums = Hashtbl.fold (fun k v acc -> (k, v) :: acc) ctx.env.enums [];
emit;
(* Neither [println] nor [watch] follows a pointer, and the allocation
registry does not change that. spec-memory.md fixes what it prints —
"Ptr and Handle print their address or identity rather than
recursively dereferencing" — and a printed line belongs to the program,
so it must read the same in a release build, where there is no registry
to ask. Following one is the *inspector's* move, and session.ml is where
that context is built. *)
ptrs = None;
alloc = (fun ty -> fresh_slot ctx ty) }
(* Shadowing is legal -- [(let [v 11] (let [v 22] ...))] is two slots, both
named [v] -- and the debug info has nowhere to put the distinction. Every
[!DILocalVariable] is scoped to the subprogram, because the typed IR has no
block structure for a [!DILexicalBlock] to be built from, so two variables
called [v] land in one flat scope and lldb answers [p v] with whichever it
finds first. Measured, not assumed: it answers with the *outer* one, so it
prints 11 while the body it is stopped in is computing with 22, and the
inner binding is not listed at all.
That is the one outcome worse than printing [s3]: a name the debugger is
confident about and wrong about. So a repeat of a name already bound in this
function gets a suffix, and both bindings are then visible and unambiguous.
[~] is the reader's delimiter and cannot occur in a source symbol (the same
reason [destructure~nth] is spelled that way), so [v~2] is visibly the
compiler's doing and can never collide with something the programmer wrote.
This is a way of not lying, not a way of being right: [v] is still the outer
binding everywhere, including inside the inner one's extent. Scoping the
variables properly means emitting a [!DILexicalBlock] per [Let] and moving
the [llvm.dbg.declare]s out of the entry block to the binding sites, which
needs block structure this IR does not carry. *)
let bind ctx ?what ?lit name bty ~assignable =
let taken n = List.exists (fun s -> s = Some n) ctx.slot_names in
let name' =
if not (taken name) then name
else
let rec go k =
let c = Printf.sprintf "%s~%d" name k in
if taken c then go (k + 1) else c
in
go 2
in
let slot = fresh_slot ~name:name' ctx bty in
(* [ctx.scope] keeps the *source* name: the suffix is a debug-info artifact
and resolving [v] must still find the innermost binding. *)
ctx.scope <- (name, { slot; bty; assignable; bwhat = what; blit = lit }) :: ctx.scope;
slot
(* The last answer for each name, with the scope it was read from. A body is
checked under one scope for long stretches and names the same local
several times per form, so compared by identity this answers most
lookups without walking a scope that, in a long [let], holds thousands of
names. *)
let lookup_cache : (string, (string * binding) list * binding option) Hashtbl.t =
Hashtbl.create 64
let lookup ctx name =
match Hashtbl.find_opt lookup_cache name with
| Some (sc, r) when sc == ctx.scope -> r
| _ ->
let r = List.assoc_opt name ctx.scope in
Hashtbl.replace lookup_cache name (ctx.scope, r);
r
(* Capture, spec-memory.md's case 2: a body lifted into a function of its own —
an [fn] literal or a handler clause — naming a local of the function it was
written in.
The copy is taken where the value is made and not where it is read, so the
name in the body means what the local held at that instant and nothing
later can change it. What makes that safe is the extent: the copies live in
a slot of the *enclosing* frame, and a value holding their address may not
outlive it. [escapes] is the whole of what enforces that, and it is where
the escaping half — an environment the collector allocates — is named.
Answers the binding the body should use, minting one on first reference:
a named slot of this body's own frame, which the prologue fills from the
environment. Not assignable, and that is not an omission — see [captured_set].
A dyn is refused, and for the reason a struct field of dyn already is (see
the [defstruct] arm): the collector's roots are frames, a captured copy
lives inside a struct the checker synthesised, and nothing pushes the
fields of one. A dyn in there would be a live value reachable only through
memory the marker never walks. Milestone 2's per-type descriptors lift it,
alongside the condition payload's and the struct field's. *)
let rec capture ctx loc name =
match List.assoc_opt name ctx.caught with
(* Already captured, and named again from a scope that no longer lists it —
a [let] inside the body restores what it displaced, and the copy's
binding goes with it. One field, not two: the environment is keyed by the
source name. *)
| Some (outer, slot) -> Some { slot; bty = outer.bty; assignable = false; bwhat = None; blit = None }
| None ->
let from_parent () =
(* Not a local of the body directly around this one, so ask whether that
body can capture it in turn. The middle one takes a copy and this one
takes a copy of that — which is the same value, because every copy on
the way was taken at the moment its own value was made, and those
moments are nested. *)
match ctx.parent with
| Some p -> capture p loc name
| None -> None
in
let outer =
match List.assoc_opt name ctx.outer with
| Some b -> Some b
| None -> if ctx.outer_what = None then None else from_parent ()
in
match ctx.outer_what, outer with
| Some _, Some (outer : binding) ->
let slot = bind ctx name outer.bty ~assignable:false in
ctx.caught <- ctx.caught @ [ (name, (outer, slot)) ];
Some { slot; bty = outer.bty; assignable = false; bwhat = None; blit = None }
| _ -> None
(* The one thing [capture] does not answer for. A captured name is a copy, so
a store into it would change this body's copy and leave the local it came
from as it was — which is a silent disagreement and not a feature. The
ordinary "not assignable" message would name the wrong reason, so this one
names the right one.
[and] rather than a second [let] only so the two read together; neither
calls the other. *)
(* What [capture] would find, without capturing it. A guard that has to know
the *type* of an enclosing local before deciding what a form means — a
name in head position is a call through a value only if the value is a
function — must not take a copy on the way to answering. The binding it
answers with is only good for its type unless it came out of [caught]. *)
and peek_outer ctx name =
if ctx.outer_what = None then None
else
match List.assoc_opt name ctx.caught with
| Some ((b : binding), slot) -> Some { slot; bty = b.bty; assignable = false; bwhat = None; blit = None }
| None ->
match List.assoc_opt name ctx.outer with
| Some b -> Some b
| None ->
match ctx.parent with Some p -> peek_outer p name | None -> None
and outer_local ctx name =
ctx.outer_what <> None
&& (List.mem_assoc name ctx.caught
|| List.mem_assoc name ctx.outer
|| (match ctx.parent with
| Some p -> outer_local p name
| None -> false))
and captured_set ctx loc name =
if outer_local ctx name then
match ctx.outer_what with
| Some what ->
Loc.failk "check/capture-set" loc
"%s cannot assign to %s: it is a copy of the enclosing function's \
local, taken where the value was made, so a store here would change \
the copy and leave %s as it was. %s"
what name name
(if String.equal what "a handler" then
"Accumulate into a global, or put the value on the condition — \
capture is by value, which is what lets the copy be read at all"
else
"Return the new value, or keep it in a local of this fn")
| None -> ()
let scoped ctx f =
let saved = ctx.scope in
let r = f () in
ctx.scope <- saved;
r
(* A scope that is also a named blocker for [defer]. An arm of an [if] or a
[match] runs only sometimes, and "maybe registered" is not something a
compile-time construct can express — the cleanup is copied into every exit
path or into none — so the refusal is about the branch and says so.
Outside the [check] recursion on purpose: inside it the inferred type would
be monomorphic, and the two callers pass functions returning different
things. *)
let branch ctx f =
let blocker = ctx.defer_block in
ctx.defer_block <- "a branch";
let r = scoped ctx f in
ctx.defer_block <- blocker;
r
(* ── Type resolution ───────────────────────────────────────────────── *)
let unimplemented loc what milestone =
(* No repo filename in a message, and no milestone number either: neither
means anything to somebody who has this compiler and nothing else. What
they need is that the thing is not there yet. *)
ignore milestone;
fail loc "%s is not implemented yet" what
(* Four names the randomness functions do not have, each with the name that
does and a call that compiles. A reader who has never seen this language
arrives at one of these by copying a line from somewhere, and what they need
is the spelling that works — so these say what the surface *is*, not what it
once was. The suggestions are checked by the suite, which compiles each one.
[rand-int] answers a u64 whose every bit is a fresh draw, so a narrower
draw is that value narrowed — which is why the [rand-u32] line suggests a
cast rather than another function, and takes the high half, the half a
reader should be taught to take. *)
let no_such_rand name =
match name with
| "rand-u32" ->
Some "there is no rand-u32 — a random integer is (rand-int), which answers \
a u64 with every bit drawn. For 32 bits of one, write \
(u32 (>> (rand-int) 32))"
| "rand-f32" ->
Some "there is no rand-f32 — a random float in [0, 1) is (rand), which \
answers an f64. For an f32, write (f32 (rand))"
| "rand-i32-range" ->
Some "there is no rand-i32-range — a random integer in [lo, hi) is \
(rand-int-range lo hi), which answers an i64: (rand-int-range 0 10) \
is one of 0 to 9, and (i32 (rand-int-range 0 10)) is that as an i32"
| "rand-f32-range" ->
Some "there is no rand-f32-range — a random float in [lo, hi) is \
(rand-float-range lo hi), which answers an f64, as in \
(rand-float-range 0.0 1.0)"
| _ -> None
(* ── where predicates ──────────────────────────────────────────────────
A predicate is a compile-time question about a type, and that is the whole
of it. It carries no implementation, selects no instance, and is not
extensible: it gates a builtin the compiler already has. So there are no
dictionaries, no coherence rules and no run-time cost — and the ceiling is
that nobody can supply a [<] of their own, which does not bind because
every operation the prelude and the containers need is a primitive.
Odin's [where] clause is the same shape ([core/slice/slice.odin:289] is
[where intrinsics.type_is_ordered(T)]) with forty-one predicates against
these six. There is no [copyable?] any more and no Odin counterpart
either: Odin has no move semantics, and since the repeal neither does this
language, so [$T] never has to answer the question.
[is-integer] is the narrowest numeric bound and exists because [is-numeric] was
one type too wide for a family of bodies: an integer body under [is-numeric]
is instantiated at f32 and f64 too, and (if (< x 0) (- 0 x) x) at -0.0 is
the wrong abs while %, the bitwise operators and the shifts have no float
meaning at all. A function that can be generalized should not need a
variant per numeric type, and [is-integer] is what lets the integer-only
ones say exactly what they need.
[is-enum] admits exactly the enums. It entails [is-ordered] and [is-equal] and
not [is-numeric]: an enum compares, and it converts to a number, but it is
not one — no arithmetic, no literal. It is what licenses the generic
enum-to-number conversion, beside [is-numeric]. *)
let predicate_names =
[ "is-ordered"; "is-equal"; "is-hashable"; "is-numeric"; "is-integer"; "is-enum" ]
(* ── What a type owns, transitively ────────────────────────────────────
The one structural ownership question that survived the repeal, because it
is not about copying at all.
What it decides, and the only thing it decides, is whether a container of
this element type has to be built against a region allocator — see
[region_only] below and [flan_alloc_region_only] in the runtime. That is a
question about *release*, so it is asked of every arm a release would have
to reach and would not: a Vec or Map owns a block outright; an Option,
a fixed array, a struct or a data type's case owns whatever its payload
does.
It does not live in [Types] for the reason [Types.keyable] gives: that
module has no field table. The [seen] list is the cycle guard, and the cycle
is real — the recursive dynamic value this exists for holds a [(Vec Value)],
so [Value]'s walk reaches [Value]. Answering [false] for a name already on
the path is right rather than merely terminating: whatever made the outer
name own something was found by the arm that got here, and a name cannot
contain itself by value anyway — [check_finite] refuses that — only through
a container, which is an arm that answers for itself.
[env.unions], the untagged ones, are deliberately not consulted: nothing
anywhere records which member of one is live, so there is no fact this
walk could read — an untagged union is treated as owning nothing, and what
its members point at is the program's, through whatever tag it keeps
beside the union. *)
(* One edit apart — a substitution, an insertion, a deletion or a
transposition of neighbours. Bounded at one, because two edits is no longer
a typo, it is a guess. Shared by the unknown-type near miss and the enum
member one. *)
let one_edit a b =
let la = String.length a and lb = String.length b in
if abs (la - lb) > 1 then false
else begin
(* Walk both until they diverge, then require the tails to match with the
single edit applied. *)
let i = ref 0 in
while !i < la && !i < lb && a.[!i] = b.[!i] do incr i done;
let ta s k = String.sub s k (String.length s - k) in
if la = lb then
!i < la
&& (ta a (!i + 1) = ta b (!i + 1)
(* stirng/string: two neighbours swapped. *)
|| (!i + 1 < la && a.[!i] = b.[!i + 1] && a.[!i + 1] = b.[!i]
&& ta a (!i + 2) = ta b (!i + 2)))
else if la < lb then ta a !i = ta b (!i + 1)
else ta a (!i + 1) = ta b !i
end
(* Whether [got] could be [want] once [want]'s type variables are bound:
the same shape, a variable matching anything. Binding them consistently is
the generic call's business; this only says a literal may take the shape. *)
let rec fits_shape (want : Types.t) (got : Types.t) =
match want, got with
| Types.Var _, _ -> true
| Types.Fn (ps, r), Types.Fn (qs, s) | Types.CFn (ps, r), Types.CFn (qs, s) ->
List.length ps = List.length qs && List.for_all2 fits_shape ps qs && fits_shape r s
| Types.Slice (a, x), Types.Slice (b, y) | Types.Ptr (a, x), Types.Ptr (b, y) ->
a = b && fits_shape x y
| Types.Vec x, Types.Vec y | Types.Option x, Types.Option y -> fits_shape x y
| Types.Array (n, x), Types.Array (m, y) -> n = m && fits_shape x y
| Types.Map (k, v), Types.Map (k', v') -> fits_shape k k' && fits_shape v v'
| _ -> Types.equal want got
(* [Dir.north] as the enum and the member's value, when [Dir] is an enum
with a member [north]. *)
let enum_member env name =
match String.rindex_opt name '.' with
| Some i when i > 0 && i < String.length name - 1 ->
let e = String.sub name 0 i and m = String.sub name (i + 1) (String.length name - i - 1) in
(match Hashtbl.find_opt env.enums e with
| Some members -> Option.map (fun v -> (e, v)) (List.assoc_opt m members)
| None -> None)
| _ -> None
(* The enum members' own near miss. One edit away is the usual typo; the
second rule is for a package whose members carry a disambiguating prefix —
raylib's Key spells them [key-r], [key-space] — where the natural mistake
is writing the bare name. [:r] against [key-r] is four edits and one
thought, so the rule is the thought: a member whose last segment is exactly
the name written. *)
let member_near_miss members k =
List.find_opt
(fun (m, _) ->
one_edit k m
|| (let lm = String.length m and lk = String.length k in
lm > lk + 1
&& m.[lm - lk - 1] = '-'
&& String.sub m (lm - lk) lk = k))
members
let owning_fields env n =
match Hashtbl.find_opt env.structs n with
| Some s -> [ s.Tast.fields ]
| None ->
match Hashtbl.find_opt env.datas n with
| Some d -> List.map (fun (c : Tast.variant) -> c.Tast.vfields) d.Tast.cases
| None -> []
let rec owning env ?(seen = []) (t : Types.t) =
match t with
| Types.Vec _ | Types.Map _ -> true
| Types.Option e | Types.Array (_, e) -> owning env ~seen e
| Types.Named n ->
not (List.mem n seen)
&& List.exists
(List.exists
(fun (f : Tast.field) -> owning env ~seen:(n :: seen) f.Tast.fty))
(owning_fields env n)
| _ -> false
(* Does a value of this type hold a dyn anywhere — through a field, a case, an
element or a view? Asked where the program is still being checked, so it
reads the environment's tables rather than a finished program. *)
let rec holds_dyn env ?(seen = []) (t : Types.t) =
match t with
| Types.Dyn -> true
| Types.Array (_, e) | Types.Vec e | Types.Option e | Types.Slice (_, e)
| Types.Ptr (_, e) -> holds_dyn env ~seen e
| Types.Map (k, v) -> holds_dyn env ~seen k || holds_dyn env ~seen v
| Types.Named n when not (List.mem n seen) ->
let seen = n :: seen in
let fields (fs : Tast.field list) =
List.exists (fun (f : Tast.field) -> holds_dyn env ~seen f.Tast.fty) fs
in
(match Hashtbl.find_opt env.structs n with
| Some s -> fields s.Tast.fields
| None ->
match Hashtbl.find_opt env.unions n with
| Some u -> fields u.Tast.fields
| None ->
match Hashtbl.find_opt env.datas n with
| Some d ->
List.exists (fun (c : Tast.variant) -> fields c.Tast.vfields)
d.Tast.cases
| None -> false)
| _ -> false
(* Does a container of this type have to be built against an allocator that
cannot free one block? Only the half a release would have to walk is asked:
a map's key cannot own anything — [map_type] refuses one, because a key that
owned storage would hash its header rather than what it points at — so the
value is the whole of the question there. *)
let region_only env (t : Types.t) =
match t with
| Types.Vec e -> owning env e
| Types.Map (_, v) -> owning env v
| _ -> false
(* Does a concrete type answer yes? Checked at every instantiation, against
the type the call site asked for. *)
let pred_holds p (t : Types.t) =
match p with
| "is-ordered" -> Types.is_comparable t
| "is-equal" -> Types.is_equatable t
(* [Types.keyable] says yes to a struct and leaves its fields to [key_pair],
which walks them at the operation. That split is the existing one and is
kept: a generic declared [is-hashable] and instantiated at a struct whose
fields are not keyable is refused where every other program is, by
[key_pair]. *)
| "is-hashable" -> Types.keyable t
| "is-numeric" -> Types.is_numeric t
| "is-integer" -> Types.is_integer t
| "is-enum" -> (match t with Types.Enum _ -> true | _ -> false)
| _ -> false
(* What one declared predicate *also* gives you. These are entailments over
the type system as it stands, not conveniences: every type [is_comparable]
admits is a number or an enum, so it is equatable. The table is only sound
while that is true — an ordered type with no [=] would make it wrong — so
it lives in one place and says so. The gain is real ergonomics:
[{:where (is-ordered $t)}] is enough for a [sort] that also compares,
rather than two predicates on one line. *)
let pred_entails ~declared ~wanted =
String.equal declared wanted
|| match wanted, declared with
| "is-ordered", ("is-numeric" | "is-integer" | "is-enum") -> true
| "is-equal", ("is-numeric" | "is-ordered" | "is-integer" | "is-enum") -> true
(* Every integer type is a number, so [is-integer] gives a body everything
[is-numeric] does — the arithmetic, the written 0, the untyped integer
literal — on top of the operations only it admits. The reverse is
never true: [is-numeric] admits floats, which is exactly what a body
under [is-integer] is promising it never meets. *)
| "is-numeric", "is-integer" -> true
| _ -> false
let declares preds v wanted =
List.exists
(fun (p : Ast.pred) ->
String.equal p.Ast.pvar v && pred_entails ~declared:p.Ast.pname ~wanted)
preds
(* Which variable, if any, a type bottoms out at. Only a bare variable can
carry a predicate: [(Vec t)] is a Vec whatever [t] is, and its own
properties are the Vec's. *)
let tyvar_of (t : Types.t) = match t with Types.Var v -> Some v | _ -> None
(* ── (Map K V), spec-memory.md ──────────────────────────────────────────
Both halves are checked where the type is written, not where an operation
is, so that a map nothing ever uses is still refused if it cannot work.
[Check.key_pair] emits the hash and equality pair later, at the operation,
and repeats these refusals rather than assuming: the two are reached by
different paths and a silent disagreement between them would be worse than
saying the same thing twice. *)
let map_type ?(preds = []) loc (k : Types.t) (v : Types.t) =
ignore preds;
(* The value used to be refused here when it owned anything, in the same
words [(Vec (Vec T))] used, and the refusal is gone for the reason set out
over [map-new]: it was about *teardown*, and which tier this map will be
built against is not knowable where its type is written. The question is
asked at the construction instead, of the allocator, once. *)
(* () has no bytes, so a slot for one is a slot of nothing: the cell
geometry divides the cache line by the element size and there is nothing
to divide by. It is also the natural spelling of a *set*, which is why
someone will write it, so it is refused by name rather than by a crash. *)
if Types.equal v Types.Unit then
fail loc
"a map value cannot be () — write (Map %s bool) and ignore the value"
(tyname loc k);
if Types.equal k Types.Unit then
fail loc "a map key cannot be () — every key would be the same key";
(* The key, as far as the type alone can say. A struct passes here and is
decided at the operation, by [key_pair], which walks its fields — the
struct table is not necessarily complete while a type is being resolved,
and every map that exists reaches an operation anyway, because a global
map starts zeroed and a local needs (map-new). *)
(* A type variable is a map key exactly when the [where] clause says it is
hashable. Nothing else about it is knowable here, and falling through to
[Types.keyable] would answer no for a variable that is about to be
instantiated at [string]. *)
if not (match k with
| Types.Var v -> declares preds v "is-hashable"
| k -> Types.keyable k) then
fail loc
"%s is not a map key. A key is an integer, an enum, a bool, a string, a \
fixed array of those, or a struct of those"
(tyname loc k);
Types.Map (k, v)
(* The positions a function value may not be written in, and the one reason
they are all the same position: something zeroes it.
The zero is the whole reason: a capturing value's environment belongs to
the collector and may be kept anywhere, and [(Option (Fn ...))] is how a
field or a global holds one.
ZII is the language's rule — an omitted struct field, a fixed array's
elements, a [defonce] with no initialiser are all all-bytes-zero — and a
zeroed function value is a null pointer with a signature on it, which is the
one kind of zero that cannot be used for anything. Every other type's zero
is a value: 0, false, an empty slice, [None], a data type's first case. So these
are refused where they are written rather than left to crash at the call.
A parameter, a return type, a [let] binding and an [(Option (Fn ...))] are
not on the list: none of them is ever conjured, and an [Option]'s zero is a
[None] whose tag nobody may look past. *)
(* The two function types as one question, for every place that wants the
signature and does not care which of them carries an environment: a call
site, a shadowing guard, a walk over the parameters. Where the difference
matters it is matched on directly, and there are few such places — the
representation is [Emit]'s business and the coercion is [expect]'s. *)
let fn_sig (t : Types.t) =
match t with
| Types.Fn (ps, r) | Types.CFn (ps, r) -> Some (ps, r)
| _ -> None
let callable_ty t = fn_sig t <> None
(* A (CFn ...) is not on the list: it is one code address, and every call
through one tests for null and signals NullCall (see [Emit.null_check]), so
a zeroed one is an empty slot rather than a crash. That is what lets a table
of function pointers be a struct or a fixed array. An (Fn ...) stays
refused: a call through one is not tested, and (Option (Fn ...)) is the
field that holds one. *)
let rec no_zeroed_fn loc what (t : Types.t) =
match t with
| Types.Fn (ps, r) ->
fail loc
"%s cannot be %s — it would be zeroed, and a zeroed function value is a \
null pointer. Pass it as a parameter, hold it in a let, or store a \
%s if it captures nothing"
what (tyname loc t) (tyname loc (Types.CFn (ps, r)))
| Types.Array (_, e) -> no_zeroed_fn loc what e
| _ -> ()
(* ── What may be overwritten with raw bytes ────────────────────────────
[(filled b)] and [(sentinel-filled)] are the only two things in the
language that write a byte pattern over storage the type system has an
opinion about, so the question they raise is which types survive having
arbitrary bytes put in them. The answer here is the narrow one: numbers,
and aggregates built out of numbers. Everything else is refused by name.
What is being kept out, and why each one is not a matter of taste:
- [dyn]. A struct that holds a dyn is rooted on the collector's root stack
with a descriptor naming the byte offsets of its dyn words (see the
per-type descriptor note at the bottom of this file). Filling one leaves
a word that is not a dyn at an offset the collector is told to walk, and
the next collection follows it. The refusal is what keeps that from
being reachable at all.
- [Vec], [(Map K V)], [Allocator]. An owning header: a pointer, a length, a
capacity and an allocator the runtime frees through. A filled one is a
free of a wild pointer the first time it is touched.
- [string] and a slice. Two words, the second of which is a length every
bounds check believes. A filled length is a bounds check that passes and
an access that does not.
- [bool]. The one refusal that is about the backends rather than the
runtime: a bool is a byte here and an [i1] to LLVM, which reads the low
bit, where x86 compares the whole byte against zero. 0xDE is false on
one and true on the other, and byte-identical behaviour across the two
backends is the property this feature is pinned on.
- an enum, a data type, an [(Option T)], a function value. Each
carries a tag or a case index that something later reads as a small
number with a meaning, and a filled one names a case that does not
exist.
Floats are in: every bit pattern is a float, NaNs included, and both
backends move one as bytes. So is a [Ptr], which the collector does not
walk and whose poisoned value is the useful case, and an untagged union
whose members are all admitted, filled over its whole size. *)
let rec unfillable env seen (t : Types.t) : Types.t option =
match t with
| Types.Int _ | Types.Float _ | Types.Ptr _ -> None
| Types.Array (_, e) -> unfillable env seen e
| Types.Named n when not (List.mem n seen) ->
(match
match Hashtbl.find_opt env.structs n with
| Some s -> Some s
| None -> Hashtbl.find_opt env.unions n
with
| Some s ->
List.fold_left
(fun acc (fl : Tast.field) ->
match acc with
| Some _ -> acc
| None -> unfillable env (n :: seen) fl.Tast.fty)
None s.Tast.fields
(* A data type, the one [Named] thing in neither table: its tag names a
case, so the type itself is what the refusal names. *)
| None -> Some t)
| _ -> Some t
(* How a concrete type is spelled inside an instantiation's name. The prelude
already writes this by hand — [filter-i32], [sum-f32], [append-i64] — so a
generated name reads like the handwritten one it replaces, which is what a
backtrace, a [Reach] edge and a dev-build cell all end up showing.
[Types.to_string] cannot serve: [[i32]] and [(Vec i32)] are not symbols. *)
let rec mangle_ty (t : Types.t) =
match t with
| Types.Unit -> "unit"
| Types.Slice (Types.Mut, e) -> "slice-" ^ mangle_ty e
| Types.Slice (Types.Const, e) -> "cslice-" ^ mangle_ty e
| Types.Array (n, e) -> Printf.sprintf "arr%Ld-%s" n (mangle_ty e)
| Types.Map (k, v) -> Printf.sprintf "map-%s-%s" (mangle_ty k) (mangle_ty v)
| Types.Ptr (Types.Mut, e) -> "ptr-" ^ mangle_ty e
| Types.Ptr (Types.Const, e) -> "cptr-" ^ mangle_ty e
| Types.Vec e -> "vec-" ^ mangle_ty e
| Types.Option e -> "opt-" ^ mangle_ty e
| Types.Fn (ps, r) ->
Printf.sprintf "fn-%s-to-%s"
(String.concat "-" (List.map mangle_ty ps)) (mangle_ty r)
| Types.CFn (ps, r) ->
Printf.sprintf "cfn-%s-to-%s"
(String.concat "-" (List.map mangle_ty ps)) (mangle_ty r)
(* Bare, because [Types.to_string] spells a variable with its [$] for the
reader and a symbol has no room for one. *)
| Types.Var n -> n
(* The key, not [Types.to_string]'s [(Small 8 i32)], which is a reader's
spelling and not a symbol. *)
| Types.Named n -> n
| t -> Types.to_string t
let rec occurs_in ~needle (t : Types.t) =
Types.equal needle t
||
match t with
| Types.Slice (_, e) | Types.Array (_, e) | Types.Ptr (_, e) | Types.Vec e
| Types.Option e -> occurs_in ~needle e
| Types.Map (k, v) -> occurs_in ~needle k || occurs_in ~needle v
| Types.Fn (ps, r) | Types.CFn (ps, r) ->
List.exists (occurs_in ~needle) ps || occurs_in ~needle r
| Types.LArray (_, e) -> occurs_in ~needle e
(* Through a struct copy's arguments, or [(Node (Node $t))] would not be
seen to contain [(Node $t)]. *)
| Types.Named k ->
(match Hashtbl.find_opt struct_apps k with
| Some (_, args) -> List.exists (occurs_in ~needle) args
| None -> false)
| _ -> false
(* [b] is [a] with something built around it: same shape, strictly bigger. *)
let grows ~from_:a ~to_:b =
List.length a = List.length b
&& List.for_all2 (fun x y -> occurs_in ~needle:x y) a b
&& not (List.for_all2 Types.equal a b)
(* A generic struct's copy at [args], by key: [Small-8-i32], or
[Small-$n-$t] at variables. Recorded in [struct_apps] and [Types.display]
as the key is made; the copy's fields are [struct_copy]'s business. *)
let struct_app g args =
let key =
g ^ "-"
^ String.concat "-"
(List.map
(function
| Types.Var v -> "$" ^ v
| Types.Len n -> Int64.to_string n
| t -> mangle_ty t)
args)
in
if not (Hashtbl.mem struct_apps key) then begin
jreplace struct_apps key (g, args);
Hashtbl.replace Types.display key
(Printf.sprintf "(%s %s)" g
(String.concat " " (List.map Types.to_string args)));
Hashtbl.replace Types.display_app key (g, args)
end;
key
(* Does [name] contain itself by value? [check_finite] asks it of every
declared type once they are all collected, and a generic struct's copy asks
it of itself when it is made, which is after that. *)
let finite_from env name0 =
let rec walk seen name =
if List.mem name seen then
(let l = Option.value (Hashtbl.find_opt env.locs name) ~default:Loc.unknown in
fail l "%s contains itself by value, so it has no size — go through %s"
(tyname l (Types.Named name))
(tyname l (Types.Ptr (Types.Mut, Types.Named name))));
let seen = name :: seen in
match Hashtbl.find_opt env.structs name with
| Some s -> List.iter (fun (f : Tast.field) -> ty seen f.Tast.fty) s.Tast.fields
| None ->
match Hashtbl.find_opt env.datas name with
| Some u ->
List.iter
(fun (c : Tast.variant) ->
List.iter (fun (f : Tast.field) -> ty seen f.Tast.fty) c.Tast.vfields)
u.Tast.cases
| None ->
(* A union whose member is itself is the same infinite type a struct's
is — the size is the largest member and the largest member is the
whole thing. Nothing about overlaying storage makes the recursion
finite, so it is on the same walk rather than left to hang the
layout calculator. *)
match Hashtbl.find_opt env.unions name with
| None -> ()
| Some u ->
List.iter (fun (f : Tast.field) -> ty seen f.Tast.fty) u.Tast.fields
and ty seen = function
| Types.Named n -> walk seen n
| Types.Array (_, e) | Types.Option e -> ty seen e
| _ -> ()
in
walk [] name0
(* The name under the sigil. [$t] is how a defn signature introduces a type
variable and [t] is how the body spells the same one, so the tables that
record which variables are in scope — [env.tyvars] and [env.subst] — are
keyed on the bare name and every membership test has to strip first. A name
with no sigil is its own bare name. *)
let tyvar_bare n =
if n <> "" && n.[0] = '$' then String.sub n 1 (String.length n - 1) else n
(* Is this name, as written, a type variable that is in scope here? Both
spellings answer yes, because both denote the same variable: the sigil is
the binding site's and is redundant rather than wrong in the body. Every
test against [tyvars] or [subst] goes through this, so a caller cannot ask
the question of the raw name and miss the spelling with the sigil — which is
what made [(vec-new $t)] report a missing element type for a body that had
written one. *)
let tyvar_in_scope env n =
let bare = tyvar_bare n in
List.mem bare env.tyvars || List.mem_assoc bare env.subst
let rec resolve env ?(seen = []) (t : Ast.texpr) : Types.t =
let loc = t.Ast.tloc in
match t.Ast.t with
(* The one slot that takes [_] never reaches here: [collect] reads it off
the body first. So every [_] that does is in a slot with no body behind
it. *)
| Ast.Tinfer ->
Loc.failk "check/infer-misplaced" loc
"_ here asks for a type to be read off a function body, and only a \
defn's return slot has a body to read. Write the type out"
| Ast.Tname "const" ->
fail loc
"const is not a type on its own — it marks one that can only be read, \
as in [const u8] or (Ptr const u8)"
| Ast.Tname n -> resolve_name env ~seen loc n
| Ast.Tslice (c, e) ->
Types.Slice ((if c then Types.Const else Types.Mut), resolve env ~seen e)
| Ast.Tarray (l, e) ->
let e = resolve env ~seen e in
no_zeroed_fn loc "a fixed array's element" e;
(match l with
| Ast.Lname n
when (not env.len_placeholder)
&& List.mem (tyvar_bare n) env.lenvars
&& not (List.mem_assoc (tyvar_bare n) env.subst) ->
Types.LArray (tyvar_bare n, e)
| _ -> Types.Array (array_len env loc l, e))
| Ast.Tlen n ->
fail loc
"%Ld is not a type. An integer stands only where a generic struct takes \
a length, as in (Small 8 i32)" n
(* {K V} is the type spelling. There is no map *literal*: a bare map form in
expression position is a struct literal's field list, and giving the same
braces two meanings is what the colon-to-dot change was for. A map is
built with (map-new) and filled with (put). *)
| Ast.Tmap (k, v) ->
map_type ~preds:env.tvpreds loc (resolve env ~seen k)
(resolve env ~seen v)
(* (Fn [T ...] R) is a code address and the environment it is called with:
two words. A value made out of a name carries a null there; one made out
of an [fn] that captures carries the address of its copies: a slot of
the frame it was written in, or an environment the collector allocated
when the value outlives that frame (see [place_closures]).
(CFn [T ...] R) is the address alone, one word, and nothing that can
capture — see [Types] for why the C is information rather than
decoration, and for why it is not yet a capability. Nobody needs it:
[Fn] accepts everything, and the commonest reason to reach for the
narrow one is that a *named* function handed to an [Fn] pays a hop
through the widening thunk where a [CFn] is a direct call.
Where one may be *written* is narrower than where the type resolves, and
the two rules live apart on purpose: this is what the spelling means, and
[no_zeroed_fn] is where a position that would zero one is refused. A
parameter, a return type and a let binding are the positions that work,
for both. *)
| Ast.Tfn (env', ps, r) ->
let ps = List.map (resolve env ~seen) ps and r = resolve env ~seen r in
if env' then Types.Fn (ps, r) else Types.CFn (ps, r)
| Ast.Tapp (name, args) ->
(match name, args with
| "Ptr", [ a ] -> Types.Ptr (Types.Mut, resolve env ~seen a)
(* The pointer beside [[const T]]: nothing is written through it, and a
(Ptr T) converts to one. [const] cannot name a type, so this reading
is the only one the two arguments have. *)
| "Ptr", [ { Ast.t = Ast.Tname "const"; _ }; a ] ->
Types.Ptr (Types.Const, resolve env ~seen a)
| "Option", [ a ] -> Types.Option (resolve env ~seen a)
| "Ptr", _ -> fail loc "a pointer type is (Ptr T), or (Ptr const T) for one \
nothing is written through"
| "Option", _ -> fail loc "(Option T) takes exactly one type"
| "Vec", [ a ] ->
let e = resolve env ~seen a in
(* A Vec of a Vec used to be refused here, and the refusal named two
different failures under one sentence: that [clone] would duplicate
inner headers instead of copying, and that [free] would drop their
buffers on the floor. Only the second one was about *teardown*, and
only the second one an arena answers — [free-all] takes the region
and the inner blocks with it, because they came out of the same
region. So the type is admitted, the tier is checked at the
construction where the allocator is a value that exists (see
[vec-new]), and [clone] stays refused at the operation, on its own
merits, in its own words.
It cannot be refused *here* because nothing here knows the tier:
[with-allocator] rebinds a dynamic variable, so which allocator a
[(vec-new)] meets is not a property of where the type is written. *)
Types.Vec e
| "Vec", _ -> fail loc "(Vec T) takes exactly one type"
| "Map", [ k; v ] ->
map_type ~preds:env.tvpreds loc (resolve env ~seen k)
(resolve env ~seen v)
| "Map", _ -> fail loc "(Map K V) takes exactly two types"
| "Result", _ -> unimplemented loc "(Result T E)" 6
| _ when Hashtbl.mem env.gstructs name -> apply_struct env ~seen loc name args
| _ ->
(* No type of this name takes arguments: a generic struct is caught
by the arm above, and [Ptr], [Option], [Vec] and [Map] further up. *)
(* A head that is not a type at all but one edit from one is the typo
[(Vect i32)], and the generics sentence would answer a question
nobody asked. *)
let constructors = [ "Ptr"; "Option"; "Vec"; "Map" ] in
(match
if Hashtbl.mem env.aliases name || Hashtbl.mem env.structs name
|| Hashtbl.mem env.datas name || Hashtbl.mem env.unions name
|| Hashtbl.mem env.enums name
then None
else near_miss env ~also:constructors name
with
| Some m when List.mem m constructors ->
Loc.failk "check/unknown-type" loc
"unknown type %s — did you mean %s?" name m
| _ -> ());
fail loc
"%s takes no type arguments. A generic struct is one whose fields \
introduce $t, as in (defstruct %s [x $t]), and a generic function \
one whose parameter vector does"
name name)
(* [(Small 8 i32)]: each argument read as the parameter it stands for — a
length or a type — and the copy made, or found. *)
and apply_struct env ~seen loc name args =
let g = Hashtbl.find env.gstructs name in
let spelled =
Printf.sprintf "(%s %s)" name
(String.concat " " (List.map (fun (p, _) -> "$" ^ p) g.gparams))
in
let n = List.length g.gparams in
if List.length args <> n then
Loc.failk "check/generic-struct-arity" loc
~notes:[ Loc.note g.gloc (name ^ " is declared here") ]
"%s takes %d argument%s, %s, and this gives %d"
name n (if n = 1 then "" else "s") spelled (List.length args);
let targs =
List.map2
(fun (p, is_len) (a : Ast.texpr) ->
if is_len then struct_len_arg env name p a
else
match a.Ast.t with
| Ast.Tlen k ->
fail a.Ast.tloc
"%s's $%s is a type, and %Ld is a length — %s" name p k spelled
| _ -> resolve env ~seen a)
g.gparams args
in
Types.Named (struct_copy env loc name targs)
and struct_len_arg env name p (a : Ast.texpr) =
let not_one what =
fail a.Ast.tloc
"%s's $%s is a length: an integer, a constant's name or a length \
variable, and %s is %s" name p (Cimport.ty_source a) what
in
match a.Ast.t with
| Ast.Tlen k when Int64.compare k 0L < 0 ->
fail a.Ast.tloc "%s's $%s is a length, and %Ld is negative" name p k
| Ast.Tlen k -> Types.Len k
| Ast.Tname n ->
let bare = tyvar_bare n in
(match List.assoc_opt bare env.subst with
| Some (Types.Len _ as l) -> l
| Some (Types.Var v) -> Types.Var v
| Some t -> not_one ("the type " ^ tyname a.Ast.tloc t)
| None ->
if List.mem bare env.lenvars then Types.Var bare
else if List.mem bare env.tyvars then not_one "a type variable"
else
match Hashtbl.find_opt env.consts n with
| Some k -> Types.Len k
| None -> not_one "none of them")
| _ -> not_one "a type"
(* The copy of generic struct [name] at [targs], made on first use and
registered as an ordinary struct under its key. *)
and struct_copy ?(at_definition = false) env loc name targs =
let key = struct_app name targs in
if Hashtbl.mem env.copies key then key
else if Hashtbl.mem env.broken name then begin
jreplace env.copies key (List.exists generic_arg targs);
jreplace env.structs key { Tast.sname = key; fields = [] };
key
end
else begin
if Hashtbl.mem env.structs key || Hashtbl.mem env.datas key
|| Hashtbl.mem env.unions key then
fail loc
"%s at these arguments is called %s, and %s is already defined — \
rename one" name key key;
let g = Hashtbl.find env.gstructs name in
(* A copy that asks for a copy of its own template at a type built around
its own arguments — [(defstruct Grow [next (Ptr (Grow [$t]))])] — asks
forever, and pointers do not stop it: each copy is made the moment it
is named. *)
let chain_text () =
String.concat "\n "
(List.map
(fun (h, a) ->
Printf.sprintf "(%s %s)" h
(String.concat " " (List.map (tyname loc) a)))
(env.schain @ [ (name, targs) ]))
in
if List.exists
(fun (h, a) -> String.equal h name && grows ~from_:a ~to_:targs)
env.schain
|| List.length env.schain >= 64 then
Loc.failk "check/runaway-instantiation" loc
~notes:[ Loc.note g.gloc (name ^ " is declared here") ]
"%s names a copy of itself at a type built around its own \
arguments, and that copy names another, without end:\n %s\n\
Name the same arguments, or smaller ones" name (chain_text ());
let generic = List.exists generic_arg targs in
(* In before its fields, so a field that names the same copy through a
pointer — [(defstruct Node [next (Ptr (Node $t))])] — finds it. *)
jreplace env.copies key generic;
jreplace env.structs key { Tast.sname = key; fields = [] };
jreplace env.locs key g.gloc;
let saved =
(env.subst, env.tyvars, env.lenvars, env.tvpreds, env.len_placeholder,
env.in_field, env.schain)
in
let restore () =
let s, t, l, p, lp, f, c = saved in
env.subst <- s; env.tyvars <- t; env.lenvars <- l; env.tvpreds <- p;
env.len_placeholder <- lp; env.in_field <- f; env.schain <- c
in
env.subst <- List.map2 (fun (p, _) a -> (p, a)) g.gparams targs;
env.tyvars <- [];
env.lenvars <- [];
env.tvpreds <- [];
env.len_placeholder <- generic;
env.in_field <- true;
env.schain <- env.schain @ [ (name, targs) ];
match
List.map
(fun (f : Ast.field) ->
let fty = resolve env f.Ast.fty in
no_zeroed_fn f.Ast.fty.Ast.tloc
(Printf.sprintf "the field %s" f.Ast.fname) fty;
{ Tast.fname = f.Ast.fname; fty })
g.gfields
with
| fields ->
restore ();
jreplace env.structs key { Tast.sname = key; fields };
finite_from env key;
key
| exception e ->
restore ();
jremove env.copies key;
jremove env.structs key;
(* A field refused inside the template says nothing about which use
asked for this copy; the note names it, one per level of copies. *)
(match e with
| Loc.Error d when d.Loc.dloc <> loc && not at_definition ->
Loc.raise_diag
{ d with
Loc.notes =
d.Loc.notes
@ [ Loc.note loc
(tyname loc (Types.Named key) ^ " is made here") ] }
| e -> raise e)
end
(* Does a struct argument still mention a variable? *)
and generic_arg (t : Types.t) =
match t with
| Types.Var _ | Types.LArray _ -> true
| Types.Slice (_, e) | Types.Array (_, e) | Types.Ptr (_, e) | Types.Vec e
| Types.Option e -> generic_arg e
| Types.Map (k, v) -> generic_arg k || generic_arg v
| Types.Fn (ps, r) | Types.CFn (ps, r) ->
List.exists generic_arg ps || generic_arg r
| Types.Named k ->
(match Hashtbl.find_opt struct_apps k with
| Some (_, a) -> List.exists generic_arg a
| None -> false)
| _ -> false
(* One edit away from a type that exists — a substitution, an insertion, a
deletion or a transposition of neighbours. Bounded at one, because two edits
is no longer a typo, it is a guess. *)
and near_miss env ?(also = []) n =
(* [also] widens the candidate list past the types, and exactly one caller
passes it: the defonce whose third element has to be a type *or* a value,
whose suggestion is worth nothing if it can only ever name a type. *)
let candidates =
also
@ Types.primitive_names
@ Hashtbl.fold (fun k _ acc -> k :: acc) env.aliases []
@ Hashtbl.fold (fun k _ acc -> k :: acc) env.structs []
@ Hashtbl.fold (fun k _ acc -> k :: acc) env.datas []
@ Hashtbl.fold (fun k _ acc -> k :: acc) env.unions []
@ Hashtbl.fold (fun k _ acc -> k :: acc) env.enums []
in
List.find_opt (fun c -> c <> n && one_edit n c) candidates
and resolve_name env ~seen loc n =
(* ── Type variables, with a sigil at the binding site ─────────────────
[$t] *introduces* a variable and bare [t] uses it, which is Odin's
spelling ([$T] in the signature, [T] in the body). The sigil is read as
an ordinary symbol character, so the whole decision lives here: nothing
in the reader, the parser or the AST knows the character means anything.
Which names are variables is decided before this is ever called —
[signature_tyvars] scans the signature for the sigil and puts the bare
names in [env.tyvars] — so an unknown lowercase name is still the
unknown-type error it always was. That is the point of the sigil: without
one, a mistyped type name silently became a type parameter and made the
function more permissive than it was written to be. *)
let bare = tyvar_bare n in
let a_length () =
fail loc
"%s is a length, not a type — it stands where an array's length does, \
as in [%s T], or as a generic struct's length argument" n n
in
match List.assoc_opt bare env.subst with
| Some (Types.Len _) -> a_length ()
(* Inside an instantiation: the variable is this concrete type, and every
node checked under it is as concrete as if it had been written out. *)
| Some t -> t
| None ->
if List.mem bare env.lenvars then a_length ()
else if List.mem bare env.tyvars then Types.Var bare
else if n <> bare then
(* A sigil on a name nothing binds. Two different mistakes wear the same
spelling, and which one it is turns on whether any variable is in scope
at all. Where none is — a struct field, a global, a [let] annotation —
there is nowhere for a variable to bind and the fix is a concrete type.
Where some are, the name is almost always a variable that was
introduced once and spelled differently the second time, and the fix is
one of the names that *is* bound. Naming them is the difference between
a rule and an answer.
Which names those are is read from [tyvars] during the abstract pass and
from [subst] inside an instantiation, because the instantiation clears
the first and fills the second — and a body is checked under both, so
reading only one of them would answer the same mistake two ways in a
single run. *)
(match (match env.tyvars with [] -> List.map fst env.subst | vs -> vs) with
| [] ->
Loc.failk "check/unbound-type-variable" loc
"%s introduces a type variable, and only a defn signature or a \
defstruct's fields can — write the concrete type here" n
| [ v ] ->
Loc.failk "check/unbound-type-variable" loc
"nothing binds the type variable %s — this signature introduces %s, \
so write %s here, or a concrete type" n ("$" ^ v) ("$" ^ v)
| vars ->
Loc.failk "check/unbound-type-variable" loc
"nothing binds the type variable %s — this signature introduces %s, \
so write one of those here, or a concrete type"
n (String.concat " and " (List.map (fun v -> "$" ^ v) vars)))
else
match Types.ikind_of_name n with
| Some k -> Types.Int k
| None ->
match Types.fkind_of_name n with
| Some k -> Types.Float k
| None ->
match n with
| "bool" -> Types.Bool
| "char" -> Types.Char
| "str" -> Types.String
(* Lowercase and concrete, which the rule three screens down says is a
type variable. It is spelled this way because it is a primitive and
every other primitive is lowercase — [dyn] beside [i64] and [bool]
reads as one of them, [Dyn] beside [Vec] and [Option] reads as a
container over something. The type-variable rule is reached by a
[when] guard below and this arm is before it, so the spelling costs
nothing but the note. *)
| "dyn" -> Types.Dyn
| "Unit" -> Types.Unit
| "Never" -> Types.Never
(* A builtin opaque type, the way [string] is a builtin ptr+len. There is
no user-writable constructor and no way to name its procedure: see
Types, and TODO.org, "The escape was real: a value the compiler builds
trips no function-value refusal". *)
| "Allocator" -> Types.Alloc
| _ when Hashtbl.mem env.aliases n ->
if List.mem n seen then
fail loc "the type alias %s is defined in terms of itself" n
else resolve env ~seen:(n :: seen) (Hashtbl.find env.aliases n)
| _ when Hashtbl.mem env.gstructs n ->
let g = Hashtbl.find env.gstructs n in
Loc.failk "check/generic-struct-arity" loc
~notes:[ Loc.note g.gloc (n ^ " is declared here") ]
"%s is generic, and a type only once it is given its arguments: \
write (%s %s)" n n
(* Variables are only an answer where a signature binds them; in
ordinary code the example is concrete. *)
(String.concat " "
(List.map
(fun (p, is_len) ->
if env.tyvars <> [] then "$" ^ p
else if is_len then "8"
else "i32")
g.gparams))
| _ when Hashtbl.mem env.structs n -> Types.Named n
(* A data type is [Named] exactly as a struct is: one case in [Types.t]
covers both, and which table the name is in is what tells them apart.
Keeping them one case is what lets a data type be a field, a parameter, a
return type and a slot without a single one of those paths learning
that data types exist. *)
| _ when Hashtbl.mem env.datas n -> Types.Named n
(* And so is an untagged union, for the same reason: it is a value of a
size and an alignment, and nothing that carries one has to know it is
a union rather than a struct. *)
| _ when Hashtbl.mem env.unions n -> Types.Named n
| _ when Hashtbl.mem env.enums n -> Types.Enum n
(* A typo in a primitive is lowercase too, and the type-variable rule
below would otherwise report [f65] as unimplemented generics and send
you to plan.org instead of to the character you mistyped. *)
| _ when foreign_spelling n <> None ->
Loc.failk "check/unknown-type" loc "unknown type %s — Flan spells it %s"
n (Option.get (foreign_spelling n))
| _ when near_miss env n <> None ->
Loc.failk "check/unknown-type" loc "unknown type %s — did you mean %s?" n
(Option.get (near_miss env n))
(* An unknown lowercase name, and the sentence it gets used to be that
generics were milestone 5 work. They are not: [$t] binds a type
variable and bare [t] uses one, and [resolve_name] has already
consulted [env.tyvars] and [env.subst] before anything reaches here.
So a lowercase name arriving at this arm is one of exactly two
things, and the message names both rather than sending somebody to a
schedule.
Either it is a typo too far from any type to be guessed at — the
near-miss arm above catches the one-edit ones — or it is a type
variable that was never introduced, which is the sigil's whole
purpose to notice: without the binding site a mistyped type name
silently became a type parameter and made the signature more
permissive than it was written to be. *)
| _ when n <> "" && n.[0] = Char.lowercase_ascii n.[0] ->
(* The parameter-vector suggestion is only followable where a
parameter vector exists. A field has none: a defstruct's field
introduces the variable where it stands, so at a field the message
says that instead. *)
if env.in_field then
Loc.failk "check/unknown-type" loc
"unknown type %s. A lowercase name is a type variable only where \
it is introduced with $%s, and in a defstruct's fields that makes \
the struct generic over it. Write $%s, a concrete type, or dyn to \
hold any value"
n n n
else
Loc.failk "check/unknown-type" loc
"unknown type %s. A lowercase name is a type variable only where a \
defn signature introduced it — write $%s in the parameter vector \
to introduce one, and %s reads it from there"
n n n
| _ -> Loc.failk "check/unknown-type" loc "unknown type %s" n
and array_len env loc = function
| Ast.Lint n -> n
| Ast.Lname n ->
let bare = tyvar_bare n in
(match List.assoc_opt bare env.subst with
| Some (Types.Len k) -> k
| Some (Types.Var _) -> abstract_len
| Some t ->
fail loc "%s is the type %s here, and an array length is an integer, a \
constant or a length variable" n (tyname loc t)
| None when List.mem bare env.lenvars -> abstract_len
| None when List.mem bare env.tyvars ->
fail loc "%s is a type variable, and an array length is an integer, a \
constant or a length variable" n
| None ->
match Hashtbl.find_opt env.consts n with
| Some v -> v
| None ->
fail loc "%s is not a compile-time integer constant, so it cannot be \
an array length" n)
(* ── Pairing a defn's parameter vector ──────────────────────────────────
[(defn f [x y] ...)] is one parameter [x] of type [y] if [y] names a type,
and two parameters of type [dyn] if it does not. Parse could not tell — the
long argument is beside its [defn] case — so it handed over the slots
undecided and this is where they are paired, with every type name in hand:
every file loaded, every macro expanded, every C header imported.
The walk is left to right and takes two slots or one. A name followed by
something that is a type takes two and is annotated; a name followed by
another name that is not a type, or by nothing, takes one and is [dyn]. That
is the whole rule, and it reads the way the vector reads.
A name that *is* a type name pairs only when a type follows it: [[str str]]
has one reading. [(defn f [i64 x] ...)] has two — a dyn parameter called
[i64], or a pair written backwards — so it is refused rather than handed
back as a signature nobody wrote. *)
let is_type_name env n =
Types.ikind_of_name n <> None
|| Types.fkind_of_name n <> None
|| List.mem n [ "bool"; "char"; "str"; "dyn"; "Unit"; "Never"; "Allocator" ]
|| Hashtbl.mem env.aliases n
|| Hashtbl.mem env.structs n
|| Hashtbl.mem env.gstructs n
|| Hashtbl.mem env.datas n
|| Hashtbl.mem env.unions n
|| Hashtbl.mem env.enums n
(* A type variable: [$t] in a signature is generics' binding site, and a
slot holding one is a type however few of them there are. *)
|| (n <> "" && n.[0] = '$')
(* A defn written without its return type puts the body's first form in the
slot, and [(dotimes [i n] ...)] parses as a type application. Every type
application's head is a constructor, and a constructor is capitalised, so a
lowercase head there — or the name of a function — is a body form and not a
malformed type. A capitalised head that is neither stays with [resolve],
whose unknown-type and no-type-arguments sentences are the right ones for
[(Vect i32)] and [(Pair i32)]. *)
let missing_return_type env (fn : Ast.fn) =
match fn.Ast.ret with
| Some { Ast.t = Ast.Tapp (head, args); tloc } ->
let lowercase =
head <> "" && not (head.[0] >= 'A' && head.[0] <= 'Z')
in
let constructor =
List.mem head [ "Ptr"; "Option"; "Vec"; "Map"; "Result" ]
in
(* [(vec i32)] is the constructor with the wrong case, not a body form —
but only while every argument is a type, since [(map inc xs)] is a
body form whose head is a function. *)
let args_are_types =
List.for_all
(fun (a : Ast.texpr) ->
match a.Ast.t with Ast.Tname n -> is_type_name env n | _ -> true)
args
in
(match
List.find_opt
(fun c -> String.lowercase_ascii c = String.lowercase_ascii head
&& c <> head)
[ "Ptr"; "Option"; "Vec"; "Map" ]
with
| Some c when args_are_types && not (is_type_name env head) ->
Loc.failk "check/unknown-type" tloc
"unknown type %s — did you mean %s?" head c
| _ -> ());
if (not constructor) && (not (is_type_name env head))
&& (lowercase || Hashtbl.mem env.fns head
|| List.mem head !builtin_names)
then
Loc.failk "check/return-type-missing" tloc
"%s has no return type: (%s ...) stands where the return type goes, \
and %s is not a type. The return type is written between the \
parameter vector and the body, and a function that returns nothing \
writes () there, or _ to read it off the body"
fn.Ast.name head head
| _ -> ()
(* Before a bare symbol is allowed to become an unannotated parameter, the two
ways it is more likely to be a type that went wrong.
This is the cost dynamic-by-default puts on the parameter vector, and it is
worth naming plainly: a slot with no type used to be a syntax error, and now
it is a [dyn] parameter. So [(defn f [x f65] ())] — a typo for [f64] — no
longer reads as a mistyped type. It reads as two parameters, one of them
called [f65], and the function silently takes an argument nobody meant to
give it. An arity that changes because of a typo, with no diagnostic, is the
failure class Parse's [defn] comment calls the worst available, and the
feature reintroduces it in a new place.
Two rules take most of it back. A name within one edit of a type's name is
the typo it looks like, and is refused with the same "did you mean" the
resolver gives — the near-miss table is already there and is exactly the
right question. And a capitalised name is a type by the convention the whole
corpus keeps: not one parameter in the language is capitalised, while [Form],
[Cursor], [Vector2] and the rest appear in these vectors constantly. So an
unknown capitalised name is an unknown *type*, reported as one, rather than
a parameter nobody would have spelled that way.
What is left uncovered is a lowercase name that resembles no type: [(defn f
[x widget] ())] is two dyn parameters and there is no evidence in the text
that it was meant to be one. That case is the feature working as specified,
and it is the residual the parent owns. *)
let dyn_param_or_typo env n loc =
(* [(defn idx [v i] dyn ...)] is two dyn parameters, and [i] is one edit
from [i8], so the did-you-mean used to accuse a perfectly ordinary
parameter name of being a mistyped type. What separates the two is the
digits: this language sizes its machine types in the name, so a typo in
one keeps them — [f65] for [f64], [i33] for [i32] — while [i], [v], [n]
and [x] carry none and are what parameters are actually called. A name
with no digit, one edit from a type that has one, is a parameter; the
suggestion is dropped and the dyn reading stands, which is the reading
the writer meant. *)
let has_digit s = String.exists (fun c -> c >= '0' && c <= '9') s in
let suggestion =
match near_miss env n with
| Some m when has_digit m && not (has_digit n) -> None
| m -> m
in
(* In a .fln file the type sits after [name:], so it cannot be read as a
second parameter and needs no word about parameter vectors. *)
let fln = Source.indented_at loc in
(* [string] is what most languages call the text type, so [[s string]] is
far likelier one parameter of a misspelled type than two dyn ones. The
other foreign spellings ([long], [byte], [double]) are ordinary parameter
names and keep the dyn reading. *)
if n = "string" then
Loc.failk "check/unknown-type" loc
"unknown type string — Flan spells it str%s"
(if fln then ""
else ". Otherwise string reads as a second parameter, because a \
parameter with no type is dyn, and wants another name");
match suggestion with
| Some m when fln ->
Loc.failk "check/unknown-type" loc "unknown type %s — did you mean %s?" n m
| Some m ->
Loc.failk "check/unknown-type" loc
"unknown type %s — did you mean %s? Otherwise %s reads as a second \
parameter, because a parameter with no type is dyn"
n m n
| None ->
if n <> "" && n.[0] = Char.uppercase_ascii n.[0]
&& n.[0] <> Char.lowercase_ascii n.[0]
then
if fln then Loc.failk "check/unknown-type" loc "unknown type %s" n
else
Loc.failk "check/unknown-type" loc
"unknown type %s. A capitalised name in a parameter vector is a type; \
parameters are lowercase"
n
(* The pairings a parameter vector owes to a type the program declares under
a name that is also a legal parameter name — a lowercase one, since a
capitalised name is refused as a parameter. [(defn f [p point] ...)] is one
parameter while [point] is a type and two dyn ones the moment it is not,
so adding or removing the type re-pairs the signature with no edit to it.
The type still wins; this is the warning at the parameter, filled by
[pair_decls] and printed by [build_program] with the other warnings. *)
let pairing_warnings : Loc.diag list ref = ref []
let pair_params ?(also = fun _ -> false) ?(declared = fun _ -> None)
?(hide = fun _ -> false) env (items : Ast.pitem list) : Ast.field list =
let is_type_name env n = (is_type_name env n && not (hide n)) || also n in
let warn_pairing n t tloc =
let bare =
match String.rindex_opt t '/' with
| Some i -> String.sub t (i + 1) (String.length t - i - 1)
| None -> t
in
match declared t with
| Some (what, (at : Loc.t))
when bare <> "" && bare.[0] >= 'a' && bare.[0] <= 'z' ->
pairing_warnings :=
Loc.diag ~kind:"check/parameter-reads-a-type" tloc
(Printf.sprintf
"[%s %s] is one parameter %s of type %s, the %s declared at %s, \
and not two dyn parameters. If two were meant, give the second \
a name no type has"
n t n t what (Loc.to_string at))
:: !pairing_warnings
| _ -> ()
in
let dyn loc = { Ast.t = Ast.Tname "dyn"; tloc = loc } in
let rec go = function
| [] -> []
| Ast.Ptype t :: _ ->
Loc.failk "check/parameter-name-expected" t.Ast.tloc
"a parameter's name was expected here, and this is a type. Parameters \
are [name Type ...]"
(* A parameter may share a type's name — [str] is a common name for text —
when a type follows it: [[str str]] and [str: str] can only be a name
then a type. Without a type after it, [[i64 x]] is a pair written
backwards as likely as a dyn parameter called [i64], so that one is
refused with both fixes. *)
| Ast.Pname (n, loc) :: Ast.Ptype t :: rest when is_type_name env n ->
{ Ast.fname = n; fty = t; floc = loc } :: go rest
| Ast.Pname (n, loc) :: Ast.Pname (t, tloc) :: rest
when is_type_name env n && is_type_name env t ->
{ Ast.fname = n; fty = { Ast.t = Ast.Tname t; tloc }; floc = loc } :: go rest
| Ast.Pname (n, loc) :: _ when is_type_name env n ->
Loc.failk "check/parameter-named-type" loc
"%s names a type, and no type follows this parameter called %s. Give \
it one, as [%s %s], or if the pair is backwards write [name %s]" n n n n n
(* [_] reads a type off a body, and a parameter has none to read. *)
| Ast.Pname (n, _) :: Ast.Pname ("_", tloc) :: _ ->
Loc.failk "check/infer-misplaced" tloc
"_ here asks for %s's type to be read off a body, and a parameter's \
type is never read off anything. Write its type, or leave it out \
and %s is dyn"
n n
| Ast.Pname (n, loc) :: Ast.Ptype t :: rest ->
{ Ast.fname = n; fty = t; floc = loc } :: go rest
| Ast.Pname (n, loc) :: Ast.Pname (t, tloc) :: rest when is_type_name env t ->
warn_pairing n t tloc;
{ Ast.fname = n; fty = { Ast.t = Ast.Tname t; tloc }; floc = loc } :: go rest
(* The slot after this one is not a type, so this one is a parameter with
no type written — unless the slot after it only *looks* unlike a type
because it was mistyped, which is what the check is for. The next slot
is the one interrogated, not this one: this one is a name either way. *)
| Ast.Pname (n, loc) :: (Ast.Pname (t, tloc) :: _ as rest) ->
dyn_param_or_typo env t tloc;
{ Ast.fname = n; fty = dyn loc; floc = loc } :: go rest
| Ast.Pname (n, loc) :: rest ->
{ Ast.fname = n; fty = dyn loc; floc = loc } :: go rest
in
go items
(* A class slot's type, held to the set a stored dyn value can be checked
against. A class's name is a type here, and only here: it is not a type
anywhere else in the language, since an instance is a dyn value. Every
other type a slot could name — a struct, a Vec, a pointer — does not cross
into dyn at all, so a slot of one could never be written. *)
(* A class named in [cls]'s slot vector: [n] as written, or [n] in [cls]'s own
package, since [Load] leaves a bare name in a slot vector unqualified. *)
let class_named ~classes cls n =
(* The class's own package first: an importer may declare a class of the
same bare name, and a slot the package wrote means the package's. *)
let own =
match String.rindex_opt cls '/' with
| Some i ->
let q = String.sub cls 0 (i + 1) ^ n in
if List.mem q classes then Some q else None
| None -> None
in
match own with
| Some _ -> own
| None -> if List.mem n classes then Some n else None
let rec slot_of env ~classes cls fname (t : Ast.texpr) : slot_ty =
let refuse what =
Loc.failk "check/slot-type" t.Ast.tloc
"the slot %s of %s is declared %s, and a class slot holds a dyn value, \
which can be checked as bool, an integer type, f32, f64, string, a \
class, or (Option T) of one of those. Write one of those, or leave the \
type out and the slot holds any dyn value: [%s]"
fname cls what fname
in
match t.Ast.t with
| Ast.Tname n when class_named ~classes cls n <> None ->
Sclass (Option.get (class_named ~classes cls n))
| Ast.Tapp ("Option", [ inner ]) ->
(match slot_of env ~classes cls fname inner with
| (Sval _ | Sclass _) as s -> Sopt s
| Sany -> refuse "(Option dyn)"
| Sopt _ as s -> refuse ("(Option " ^ slot_text s ^ ")"))
| _ ->
(match resolve env t with
| Types.Dyn -> Sany
| (Types.Bool | Types.Int _ | Types.Float _ | Types.String) as t -> Sval t
| other -> refuse (tyname t.Ast.tloc other))
(* The type's word in the string the runtime reads: the scalar type's name,
[#name] for a class, [?] in front for an Option. See [slot_type_of] in
runtime/flan_dyn.c, which is the reader. *)
let rec slot_word = function
| Sany -> ""
| Sval t -> Types.to_string t
| Sclass c -> "#" ^ c
| Sopt s -> "?" ^ slot_word s
(* What the runtime is told a class is: one line per slot, in constructor
order, the slot's name and then its type's word after a space — no type
for a dyn slot. The same string goes to [flan_dyn_map_new_class] from the
constructor and to [flan_dyn_class_def] from a reload, so the two cannot
describe one class differently. *)
let class_spec_of (slots : (string * slot_ty) list) =
String.concat "\n"
(List.map
(fun (n, t) -> match t with Sany -> n | t -> n ^ " " ^ slot_word t)
slots)
let class_slots env n = Hashtbl.find_opt env.classes n
(* A class's slot vector, paired by [pair_params]'s rule with the program's
class names counted as types — so [[owner point]] is one slot holding a
point.
A lowercase name after a name that is neither a type nor a class is a
second untyped slot, which is the rule for a [defn]'s parameters and is
not changed here. In a vector that types none of its slots that is the
plain reading — [[x y]] is two slots and says nothing more. In one that
types some of them, two untyped names side by side are as likely a type
nobody has declared, so that is said, at the second name, and the slot
stays what the rule makes it. *)
let pair_slots env ~classes cls (items : Ast.pitem list) : Ast.field list =
let fields =
pair_params ~also:(fun n -> class_named ~classes cls n <> None) env items
in
let untyped (f : Ast.field) =
match f.Ast.fty.Ast.t with Ast.Tname "dyn" -> true | _ -> false
in
let written_dyn =
List.exists (function Ast.Pname ("dyn", _) -> true | _ -> false) items
in
if List.exists (fun f -> not (untyped f)) fields && not written_dyn then begin
let rec scan = function
| (a : Ast.field) :: ((b : Ast.field) :: _ as rest) ->
if untyped a && untyped b then
prerr_endline
(Loc.entry ~mark:'~' ~label:"warning: " b.Ast.floc
(Printf.sprintf
"%s reads as a slot of %s with no type, because no type or \
class is named %s. If it was meant as the type of %s, \
declare it; if it is a slot, write its type or write \
[%s dyn] to say it holds any value"
b.Ast.fname cls b.Ast.fname a.Ast.fname b.Ast.fname));
scan rest
| _ -> ()
in
scan fields
end;
fields
(* Every [defn] in the program, with its parameter vector paired. Run as a pass
of its own, after the type names are registered and before any signature is
resolved, so that nothing downstream ever sees an unpaired one. *)
let pair_decls env (decls : Ast.decl list) : Ast.decl list =
let classes =
List.filter_map
(fun (d : Ast.decl) ->
match d.Ast.d with Ast.Defclass (n, _) -> Some n | _ -> None)
decls
in
(* The types the program declares, with what kind and where, for
[pair_params]'s warning. The prelude's are left out: its names are the
language's, not a declaration the reader made. *)
let types = Hashtbl.create 16 in
List.iter
(fun (d : Ast.decl) ->
let add n what =
if d.Ast.dloc.Loc.file <> Prelude.file
&& not (List.mem n Types.primitive_names) then
Hashtbl.replace types n (what, d.Ast.dloc)
in
match d.Ast.d with
| Ast.Defstruct (n, _, _) -> add n "struct"
| Ast.Defenum (n, _) -> add n "enum"
| Ast.Defalias (n, _) -> add n "alias"
| Ast.Defdata (n, _) -> add n "data type"
| Ast.Defunion (n, _) -> add n "union"
| _ -> ())
decls;
pairing_warnings := [];
(* A prelude signature is paired against the prelude's types alone: a
program's type named [t] must not turn the prelude's parameter [t] into
a type. *)
let fn ~prelude (f : Ast.fn) =
match f.Ast.praw with
| None -> f
| Some items ->
let hide n = prelude && Hashtbl.mem types n in
{ f with
Ast.params =
pair_params ~hide ~declared:(Hashtbl.find_opt types) env items;
praw = None }
in
List.map
(fun (d : Ast.decl) ->
let prelude = String.equal d.Ast.dloc.Loc.file Prelude.file in
match d.Ast.d with
(* A class's slot vector is paired here and nowhere earlier, for the
reason a [defn]'s is, and its constructor is written from the
pairs — [Classes.expand] left the declaration as it was for exactly
this. *)
| Ast.Defclass (n, items) ->
let slots = pair_slots env ~classes n items in
Hashtbl.replace env.classes n
(List.map
(fun (f : Ast.field) ->
(f.Ast.fname, slot_of env ~classes n f.Ast.fname f.Ast.fty))
slots);
Classes.constructor n slots d.Ast.dloc
| Ast.Defn f -> { d with Ast.d = Ast.Defn (fn ~prelude f) }
| Ast.Declare (f, c) -> { d with Ast.d = Ast.Declare (fn ~prelude f, c) }
| Ast.DeclareC (f, c) -> { d with Ast.d = Ast.DeclareC (fn ~prelude f, c) }
| _ -> d)
decls
(* ── The third element of a defonce, decided ────────────────────────────
The author's rule, 2026-09-20: "if it's 3 atoms then it's dyn", and
"dispatch the if it's a type do the right thing". [(defonce current-color
i32)] is the zeroed static it has always been, and [(defonce score 0)] is a
dyn global holding 0 — the same thing [(defonce score dyn 0)] spells out,
lowered by the same path and not by a second one.
[Parse] settled every shape a shape can settle and handed the rest over
carrying both readings ([Ast.Ambiguous], beside the type reading in the
same [Defvar]). What is left is the two forms only a name can settle, and
this is the first point where every type name is in hand: the same point
[pair_params] reads, for the same reason — a defonce may name a struct
declared fifty lines below it.
The type reading wins wherever there is one. That is what keeps today's
programs meaning today's thing: [(defonce p Point)] is a zeroed [Point],
[(defonce v (Vec i32))] is a zeroed [Vec], and a wrong type argument inside
one stays a type error rather than becoming an unknown function. It is also
why a built-in constructor is checked by name rather than by whether
[resolve] happens to accept it — [(Vec i32 i32)] is a malformed [Vec] and
not a call to something called [Vec].
A type and a value cannot share a name: [collect]'s [claimed] table is over
every declaration kind there is, so one name is one declaration and the two
readings can never both be live. *)
let defvar_reads_as_type env (t : Ast.texpr) =
match t.Ast.t with
| Ast.Tname n -> is_type_name env n
| Ast.Tapp (head, _) ->
List.mem head [ "Ptr"; "Option"; "Vec"; "Map"; "Result" ]
|| Hashtbl.mem env.gstructs head
(* A slice, a fixed array, a map type or an (Fn ...): [Parse] only carries
one of these over when it read as a type and had no value reading, so
there is nothing here to decide. *)
| _ -> true
(* The bare symbol that is neither. Before the rule there was one reading and
the message was "unknown type"; now the position takes either kind of name,
so a message naming only one of them would send a reader looking for the
wrong mistake. Both readings, both spellings, and the near miss over the
value names as well as the type names. *)
(* Both readings, and the paragraph that explains them — but only when both
readings really are open. Three things get in ahead of it, because each one
knows which of the two the writer meant and the paragraph would bury that
under a lecture about a fork they are not standing at:
a case name, which is a third thing entirely and has its own spelling; a
name another language spells for a type this one has under a different
name; and a plain type typo, where a confident one-edit suggestion turns a
one-line answer into four lines of unrelated reading. The paragraph is for
the name that genuinely could have been either and is neither. *)
let defvar_neither env loc ~form gname n ~values ~cases =
(match List.assoc_opt n cases with
| Some dname ->
Loc.failk "check/defvar-case-not-type" loc
"%s is a case of the data type %s, and a case is not a type of its \
own — the global's type is the data type: (%s %s %s). Assign the \
case you want, as (set %s (%s.%s {.field value ...}))"
n dname form gname dname gname dname n
| None -> ());
(match foreign_spelling n with
| Some m ->
Loc.failk "check/unknown-type" loc "unknown type %s — Flan spells it %s" n m
| None -> ());
(match near_miss env n with
| Some m ->
Loc.failk "check/unknown-type" loc "unknown type %s — did you mean %s?" n m
| None -> ());
let hint =
match near_miss env ~also:values n with
| Some m -> Printf.sprintf " — did you mean %s?" m
| None -> ""
in
Loc.failk "check/defvar-neither-type-nor-value" loc
"%s is neither a type nor a value, and the third element of a %s has \
to be one or the other: a type there declares a zeroed global of that \
type — (%s %s i64) — and a value there declares a dyn global holding \
it — (%s %s 0). Nothing named %s is declared as either%s"
n form form gname form gname n hint
(* Every name a value could be written under, which is every declaration that
is not a type plus whatever a session already has. The list is only ever
asked "is this name declared at all", so a global that is itself a defonce
still undecided belongs on it: what it resolves to is the next pass's
question, not this one's. *)
(* The infinities and NaNs, which the reader has no literal for and the
integer-only constant folder cannot compute, so the compiler supplies them
beside the prelude's f64-max and the rest. Negative infinity is
[(- f64-inf)]. *)
let special_float = function
| "f64-inf" -> Some (Float.infinity, Types.F64)
| "f64-nan" -> Some (Float.nan, Types.F64)
| "f32-inf" -> Some (Float.infinity, Types.F32)
| "f32-nan" -> Some (Float.nan, Types.F32)
| _ -> None
(* Case name -> the data type it belongs to, read off the declarations rather
than out of [env.cases]: this runs inside [collect], which has registered
the data type *names* by here but not resolved their cases, so the table
would be empty. Last writer wins, exactly as [env.cases] does, and for the
same reason — this is only ever asked "what is this a case of", and two
data types may share a case name. *)
let case_owners (decls : Ast.decl list) =
List.concat_map
(fun (d : Ast.decl) ->
match d.Ast.d with
| Ast.Defdata (dn, vs) ->
List.map (fun (v : Ast.variant) -> (v.Ast.vname, dn)) vs
| _ -> [])
decls
let value_names env (decls : Ast.decl list) =
let declared =
List.filter_map
(fun (d : Ast.decl) ->
match d.Ast.d with
| Ast.Defvar (n, _, _, _) | Ast.Defconst (n, _, _) -> Some n
| Ast.Defn fn | Ast.Declare (fn, _) | Ast.DeclareC (fn, _) ->
Some fn.Ast.name
| _ -> None)
decls
in
declared
@ Hashtbl.fold (fun k _ acc -> k :: acc) env.globals []
@ Hashtbl.fold (fun k _ acc -> k :: acc) env.fns []
(* The decision, applied: an undecided defonce leaves this pass as one of the
two forms that already existed, so no pass after it — the signature loop
below, [check_global], either backend — has a third case to know about. The
dyn reading is rewritten into exactly [(defonce x dyn <expr>)], which is the
whole of "it lowers to the same thing": the startup lifting, the re-run
guard and the collector root are the ones that form already had. *)
(* A bracket form whose element names a value. [(defonce g [a b])] parses as a
type and stays one — type wins wherever there is a type reading, which is
the rule — so the element had to name an element type, and [b] names a
defonce. Left alone this reaches [resolve_name], where a lowercase name that
is no type is a type variable, and the answer is a paragraph about generic
code the writer was not asking for.
Both readings, and both spellings, at the element that decided it. The dyn
spelling is the one that actually works: [(defonce g dyn [a b])] is a dyn
global holding a vector, which is what the brackets meant to whoever wrote
them. *)
let rec bracket_value_element env values (t : Ast.texpr) =
let elem (e : Ast.texpr) =
match e.Ast.t with
| Ast.Tname n when (not (is_type_name env n)) && List.mem n values ->
Some (n, e.Ast.tloc)
| _ -> bracket_value_element env values e
in
match t.Ast.t with
| Ast.Tslice (_, e) -> elem e
| Ast.Tarray (_, e) -> elem e
| _ -> None
let settle_defvars env (decls : Ast.decl list) : Ast.decl list =
let values = lazy (value_names env decls) in
let cases = lazy (case_owners decls) in
(* A bracket form never reaches the fork below: [Parse.defvar3] gives it
[Zeroed] outright, because a bracket that parses as a type has no second
reading to carry. So the element check runs on both, and it is the only
thing the [Zeroed] arm does. *)
let brackets ~form gname (t : Ast.texpr) =
match bracket_value_element env (Lazy.force values) t with
| Some (v, vloc) ->
Loc.failk "check/defvar-bracket-element-is-a-value" vloc
"%s names a value, not a type, and the brackets around it were read \
as a type — a %s's third element is a type wherever there is a \
type reading, so %s had to be the element type. Write a type there \
for a zeroed global, or put dyn in front of the same brackets — \
(%s %s dyn ...) — for a dyn global holding the vector you wrote"
v form v form gname
| None -> ()
in
let word = function Ast.Once -> "defonce" | Ast.Every -> "def" in
List.map
(fun (d : Ast.decl) ->
match d.Ast.d with
| Ast.Defvar (n, Some t, Ast.Zeroed, k) -> brackets ~form:(word k) n t; d
| Ast.Defvar (n, Some t, Ast.Ambiguous e, k) ->
let form = word k in
if defvar_reads_as_type env t then begin
brackets ~form n t;
{ d with Ast.d = Ast.Defvar (n, Some t, Ast.Zeroed, k) }
end
else begin
(match t.Ast.t with
| Ast.Tname s
when not (List.mem s (Lazy.force values))
&& special_float s = None ->
defvar_neither env t.Ast.tloc ~form n s
~values:(Lazy.force values) ~cases:(Lazy.force cases)
| _ -> ());
let dyn = { Ast.t = Ast.Tname "dyn"; tloc = t.Ast.tloc } in
{ d with Ast.d = Ast.Defvar (n, Some dyn, Ast.Init e, k) }
end
| _ -> d)
decls
(* ── Generics: the four operations monomorphisation needs ───────────────
Naming a variable, binding one from an argument, substituting the binding
back in, and spelling the result as a symbol. Everything else about the
feature is where these are called from. *)
(* The variables a signature introduces: every [$t] written in it, in the
order written, once each. Only a [defn] signature is scanned, which is what
makes the binding site a *place* and not merely a spelling. *)
let sigil_vars ~kinds_of (ts : Ast.texpr list) =
let acc = ref [] in
let add loc n is_len =
if n <> "" && n.[0] = '$' then begin
let bare = String.sub n 1 (String.length n - 1) in
if bare = "" then fail loc "$ on its own does not name a type variable";
(* [$i32] would shadow a machine type inside the body and read as one
everywhere else. There is no reason to want it. *)
if List.mem bare Types.primitive_names
|| Types.ikind_of_name bare <> None
|| Types.fkind_of_name bare <> None then
fail loc "%s is a type, so $%s cannot be a type variable" bare bare;
match List.assoc_opt bare !acc with
| None -> acc := (bare, is_len) :: !acc
| Some k when k = is_len -> ()
| Some _ ->
fail loc
"$%s stands for a length in one place here and a type in another — \
a length goes in an array's length slot, [$%s T], and a type \
everywhere else. Give the two different names" bare bare
end
in
let rec ty (t : Ast.texpr) =
match t.Ast.t with
| Ast.Tname n -> add t.Ast.tloc n false
| Ast.Tslice (_, e) -> ty e
| Ast.Tarray (Ast.Lname n, e) -> add t.Ast.tloc n true; ty e
| Ast.Tarray (_, e) -> ty e
| Ast.Tmap (k, v) -> ty k; ty v
(* The head of an application is a constructor — [Ptr], [Option], [Vec],
a generic struct — and a variable cannot stand there: this is generic
over types, not over type constructors. A [$t] inside the arguments is
ordinary, and a generic struct's length argument is a length. *)
| Ast.Tapp (h, args) ->
(match kinds_of h with
| Some ks when List.length ks = List.length args ->
List.iter2
(fun is_len (a : Ast.texpr) ->
match a.Ast.t with
| Ast.Tname n when is_len -> add a.Ast.tloc n true
| _ -> ty a)
ks args
| _ -> List.iter ty args)
| Ast.Tfn (_, ps, r) -> List.iter ty ps; ty r
| Ast.Tinfer -> ()
| Ast.Tlen _ -> ()
in
List.iter ty ts;
let vs = List.rev !acc in
(List.map fst vs, List.filter_map (fun (v, l) -> if l then Some v else None) vs,
vs)
let struct_kinds env h =
Option.map (fun g -> List.map snd g.gparams) (Hashtbl.find_opt env.gstructs h)
(* The variables a signature introduces: every [$t] written in it, in the
order written, once each, and which of them are lengths. Only a [defn]
signature and a [defstruct]'s fields are scanned, which is what makes the
binding site a *place* and not merely a spelling. *)
let signature_tyvars env (fn : Ast.fn) =
let vars, lens, _ =
sigil_vars ~kinds_of:(struct_kinds env)
(List.map (fun (p : Ast.field) -> p.Ast.fty) fn.Ast.params
@ Option.to_list fn.Ast.ret)
in
vars, lens
(* Bind the variables in a parameter's written type from the type an argument
turned out to have. Odin's [is_polymorphic_type_assignable], structurally
and with the same rule: a variable already bound must match what it is
bound to, so [(pair 1 2.0)] over [a $t b $t] is a refusal and not a
second instantiation. *)
(* [ro] is whether a [[T]] argument may meet a [[const $t]] pattern here: at
the top of an argument's type, and under a const slice, which is exactly
where [Types.const_widens] lets [expect] convert the value afterwards. *)
let rec bind_ty ?(widen = false) ?(ro = true) subst (pat : Types.t)
(arg : Types.t) =
let inner = bind_ty ~ro:false subst in
match pat, arg with
| Types.Var v, a ->
(match List.assoc_opt v !subst with
| None -> subst := (v, a) :: !subst; true
| Some b when Types.equal a b -> true
(* Two arguments that differ only in const bind the variable to the
read-only one, whichever came first — the same meeting an [if]'s two
branches have. [expect] converts the writable argument afterwards. *)
| Some b when ro ->
(match Types.const_join a b with
| Some j ->
subst := (v, j) :: List.remove_assoc v !subst; true
| None -> false)
| Some _ -> false)
| Types.Slice (m, p), Types.Slice (m', a)
when m = m' || (ro && m = Types.Const) ->
bind_ty ~ro:(m = Types.Const) subst p a
| Types.Ptr (m, p), Types.Ptr (m', a)
when m = m' || (ro && m = Types.Const) ->
bind_ty ~ro:(m = Types.Const) subst p a
| Types.Vec p, Types.Vec a
| Types.Option p, Types.Option a -> inner p a
(* A plain value at a [$t?] parameter binds [$t] to its own type, and
[expect] wraps it (decision 138). At the top of an argument only, as the
widening below: an element of a Vec is never wrapped, so [(Vec $t?)]
meets a [(Vec i32)] as a mismatch. *)
| Types.Option p, a
when widen && (match a with Types.Dyn | Types.Never -> false | _ -> true) ->
bind_ty ~widen subst p a
| Types.Array (n, p), Types.Array (m, a) -> Int64.equal n m && inner p a
| Types.Map (k, v), Types.Map (k', v') -> inner k k' && inner v v'
(* Each function type against its own. *)
| Types.Fn (ps, r), Types.Fn (ps', r')
| Types.CFn (ps, r), Types.CFn (ps', r') ->
List.length ps = List.length ps'
&& List.for_all2 inner ps ps' && inner r r'
(* And the widening between them, which is admitted at the top of an
argument's type and nowhere inside it.
[(Fn [$t] $t)] against a [(CFn [i32] i32)] is the shape every caller of
a generic higher-order function has, because a [defn]'s name carries
[CFn]: [(apply2 bump 1)]. It has to bind here, where the variables are
decided, and not only in [expect] — [generic_call] binds first and would
have reported the mismatch before [expect] was ever reached.
The prelude hides that: its higher-order functions bind [$t] from an
earlier argument, so [subst_ty] has already made the parameter concrete
by the time this sees it and [(map-in-place s double)] never took this
path.
[widen] is why it goes no deeper. The widening is a *value* the caller
builds — a thunk, minted at the call — and there is exactly one place to
build it, around the whole argument. A [(Fn [(Fn [$t] $t)] i32)]
parameter handed a [(CFn [(CFn [i32] i32)] i32)] would need one built
inside the argument's own parameter list, where no caller stands, so the
two types do not meet there and the pattern does not match. What reaches
the fallthrough below is an ordinary mismatch and is refused as one, the
same answer a call with no type variables in it gets.
One way, as everywhere else: a [CFn] pattern does not admit an [Fn]
argument. *)
| Types.Fn (ps, r), Types.CFn (ps', r') when widen ->
List.length ps = List.length ps'
&& List.for_all2 inner ps ps' && inner r r'
(* A length variable's array against a concrete one: the length is bound
the way a type variable is, to a [Types.Len]. *)
| Types.LArray (v, p), Types.Array (n, a) ->
bind_ty ~ro:false subst (Types.Var v) (Types.Len n) && inner p a
(* A struct copy at variables against a copy of the same template: each
argument against its own. *)
| Types.Named p, Types.Named a ->
(match Hashtbl.find_opt struct_apps p, Hashtbl.find_opt struct_apps a with
| Some (g, ps), Some (h, as_) when String.equal g h ->
List.length ps = List.length as_ && List.for_all2 inner ps as_
| _ -> Types.fits ~expected:pat ~actual:arg)
(* Nothing generic left on the pattern side: this is ordinary type
equality, and [Never] fits anywhere exactly as it does elsewhere. *)
| p, a -> Types.fits ~expected:p ~actual:a
let rec subst_ty subst (t : Types.t) =
match t with
| Types.Var v -> (match List.assoc_opt v subst with Some c -> c | None -> t)
| Types.Slice (m, e) -> Types.Slice (m, subst_ty subst e)
| Types.Array (n, e) -> Types.Array (n, subst_ty subst e)
| Types.Map (k, v) -> Types.Map (subst_ty subst k, subst_ty subst v)
| Types.Ptr (m, e) -> Types.Ptr (m, subst_ty subst e)
| Types.Vec e -> Types.Vec (subst_ty subst e)
| Types.Option e -> Types.Option (subst_ty subst e)
| Types.Fn (ps, r) -> Types.Fn (List.map (subst_ty subst) ps, subst_ty subst r)
| Types.CFn (ps, r) ->
Types.CFn (List.map (subst_ty subst) ps, subst_ty subst r)
| Types.LArray (v, e) ->
(match List.assoc_opt v subst with
| Some (Types.Len n) -> Types.Array (n, subst_ty subst e)
| Some (Types.Var w) -> Types.LArray (w, subst_ty subst e)
| _ -> Types.LArray (v, subst_ty subst e))
(* A struct copy at variables becomes the copy at what they are bound to.
Only its key is made here — there is no env to lay it out in — and
[realise] makes the copy itself before anything reads its fields. *)
| Types.Named k ->
(match Hashtbl.find_opt struct_apps k with
| Some (g, args) when List.exists open_ty args ->
let args = List.map (subst_ty subst) args in
Types.Named (struct_app g args)
| _ -> t)
| t -> t
(* Does this resolved type still mention a variable? Not through a struct
copy's arguments: an operator over a [(Pair $t)] is refused as one over a
struct, not as one over a type variable. [open_ty] is the question that
does look through, for binding and substituting. *)
and generic_ty (t : Types.t) =
match t with
| Types.Var _ | Types.LArray _ -> true
| Types.Slice (_, e) | Types.Array (_, e) | Types.Ptr (_, e) | Types.Vec e
| Types.Option e -> generic_ty e
| Types.Map (k, v) -> generic_ty k || generic_ty v
| Types.Fn (ps, r) | Types.CFn (ps, r) ->
List.exists generic_ty ps || generic_ty r
| _ -> false
and open_ty (t : Types.t) =
match t with
| Types.Var _ | Types.LArray _ -> true
| Types.Slice (_, e) | Types.Array (_, e) | Types.Ptr (_, e) | Types.Vec e
| Types.Option e -> open_ty e
| Types.Map (k, v) -> open_ty k || open_ty v
| Types.Fn (ps, r) | Types.CFn (ps, r) -> List.exists open_ty ps || open_ty r
| Types.Named k ->
(match Hashtbl.find_opt struct_apps k with
| Some (_, args) -> List.exists open_ty args
| None -> false)
| _ -> false
(* Make every struct copy [t] names that [subst_ty] only named. A copy has
to exist in [env.structs] before a field of it is read, and [subst_ty] has
no env to make one in. *)
let rec realise env loc (t : Types.t) =
match t with
| Types.Slice (_, e) | Types.Array (_, e) | Types.Ptr (_, e) | Types.Vec e
| Types.Option e | Types.LArray (_, e) -> realise env loc e
| Types.Map (k, v) -> realise env loc k; realise env loc v
| Types.Fn (ps, r) | Types.CFn (ps, r) ->
List.iter (realise env loc) ps; realise env loc r
| Types.Named k when not (Hashtbl.mem env.structs k) ->
(match Hashtbl.find_opt struct_apps k with
| Some (g, args) when Hashtbl.mem env.gstructs g ->
List.iter (realise env loc) args;
ignore (struct_copy env loc g args)
| _ -> ())
| _ -> ()
(* Does a type a call site bound a variable to reach a [dyn] anywhere? See the
refusal in [generic_call]: [dyn] is a concrete type and substitutes like any
other, so nothing stopped a copy being made at it, and the copies walked
straight into holes the rest of the language has no [dyn] answer for yet. *)
let rec reaches_dyn (t : Types.t) =
match t with
| Types.Dyn -> true
| Types.Slice (_, e) | Types.Array (_, e) | Types.Ptr (_, e) | Types.Vec e
| Types.Option e -> reaches_dyn e
| Types.Map (k, v) -> reaches_dyn k || reaches_dyn v
| Types.Fn (ps, r) | Types.CFn (ps, r) ->
List.exists reaches_dyn ps || reaches_dyn r
| _ -> false
(* The refusal plan.org's Types section asks for, in one place so that every
operator says the same thing: with no constraints a type variable supports
only what *every* type supports, so [=], [<], [+] and [hash] over one are
rejected rather than silently instantiated at whatever type the first call
site happened to use.
With [where] there are now two ways out and the message names both: declare
the predicate, or take the operation as a function value the way
[sort-by] does. Declaring it is the one that keeps the call site short,
which is the whole reason predicates exist — under the no-constraint rule
[(sort xs)] had to become [(sort-by xs (fn [a b] (< a b)))] at every call
site in the corpus. *)
(* A predicate as a word in a sentence, [ordered (is-ordered)], and a where
clause as the file at [loc] spells one. [v] is the variable with its [$]. *)
let pred_word p =
let w =
if String.length p > 3 && String.sub p 0 3 = "is-" then String.sub p 3 (String.length p - 3)
else p
in
if w = p then p else Printf.sprintf "%s (%s)" w p
let where_text loc p v =
if Source.indented_at loc then Printf.sprintf "where %s(%s)" p v
else Printf.sprintf "{:where (%s %s)}" p v
let unconstrained env loc op ~needs (t : Types.t) =
if generic_ty t then
match tyvar_of t with
| Some v when declares env.tvpreds v needs -> ()
| _ ->
Loc.failk "check/unconstrained-type-variable" loc
"%s over the type variable %s: nothing declares %s %s. Write \
%s at the head of the body, or take the operation as \
a parameter, a (Fn [%s %s] ...), and call it here"
op (tyname loc t) (tyname loc t) (pred_word needs)
(where_text loc needs (tyname loc t))
(tyname loc t) (tyname loc t)
(* ── The runaway instantiation, refused by name rather than by depth ────
[(defn grow [x $t] () (grow [x x]))] asks for a copy at [[t]], which asks
for one at [[[t]]], forever. Before this the checker did not fail, it
*hung*, and [Session.eval] runs the same code — so what hung was [C-c C-c],
with the dev daemon wedged behind it and nothing to show the editor. That
is the project's stated priority stopped by three lines of ordinary-looking
Flan, which is why this is a refusal and not a cap.
The spike stopped it with a depth counter refusing past 32. A number is the
wrong thing to say: 32 is not in the program, the programmer cannot act on
it, and a legitimate deep instantiation and a runaway one look identical in
the message. **The structural test is exact.** A generic that is already on
the chain and is being asked for again at a type that *contains* the type
it was asked for before is growing, and growing without a smaller case is
not going to stop. A generic that recurses at the *same* types never
reaches here — the cache entry goes in before the body is checked — and one
that recurses at a *smaller* or unrelated type is fine and stays fine.
The message prints the chain, which is what the programmer can act on: each
link is a call site and a type, and the place the type started growing is
visible in the list.
Odin has no cap of its own to copy, so there was nothing to borrow and this
is the whole design. The depth backstop below stays as a backstop only: it
catches a growth this test does not recognise, and it is never the thing
the message is about. *)
let runaway env loc gname cparams =
let chain_text () =
String.concat "\n "
(List.map
(fun (g, ps, l) ->
Printf.sprintf "%s at (%s), asked for at %s" g
(String.concat " " (List.map (tyname loc) ps))
(Loc.to_string l))
(env.chain @ [ (gname, cparams, loc) ]))
in
let earlier =
List.find_opt
(fun (g, ps, _) -> String.equal g gname && grows ~from_:ps ~to_:cparams)
env.chain
in
(match earlier with
| Some _ ->
Loc.failk "check/runaway-instantiation" loc
"%s instantiates itself without end — each copy asks for another at a \
type built around the one before:\n %s\nRecur at the same type, or \
at a smaller one"
gname (chain_text ())
| None -> ());
(* The backstop. Nothing known reaches it; it exists so that a growth the
test above does not recognise is still a refusal with the chain in it
rather than a hang. *)
if List.length env.chain >= 64 then
Loc.failk "check/runaway-instantiation" loc
"%s has been instantiated 64 deep and is still going:\n %s"
gname (chain_text ())
(* [check_fn] is defined after the expression checker and an instantiation is
made from inside it, so the knot is tied here and closed at the bottom of
the file. One forward reference rather than moving a 90-line function. *)
let check_fn_ref : (env -> Ast.fn -> Tast.fn) ref =
ref (fun _ _ -> assert false)
(* ── Small helpers over the AST ────────────────────────────────────── *)
(* The untyped defconsts whose value is a char literal, by name, with the
code point. A use where typed code wants a number is that literal at the
number's type, as the literal written there would be, so a byte constant
such as (defconst sep \,) still compares with a u8 (decision 127). Filled
by the declaration pass of the program being checked. *)
let char_consts : (string, int) Hashtbl.t = Hashtbl.create 8
(* A [+] or [-] pair found to be char arithmetic after the ordinary join
refused it, with both operands checked on their own terms. *)
exception Char_pair of Tast.expr * Tast.expr
(* Untyped literals: their machine type comes from context, so when one is an
operand of a binary operator we look at the *other* operand first. *)
let is_literal (e : Ast.expr) =
match e.Ast.e with
| Ast.Int _ | Ast.UInt _ | Ast.Float _ | Ast.Byte _ -> true
| _ -> false
(* [addr] takes the address of a place, but the parser only builds places for
[set]. Recover one from the expression it parsed instead. *)
let place_of_expr (e : Ast.expr) : Ast.place option =
match e.Ast.e with
| Ast.Var s -> Some (Ast.Pvar s)
| Ast.Field (t, f) -> Some (Ast.Pfield (t, f))
| Ast.Call ({ Ast.e = Ast.Var "at"; _ }, t :: idx) when idx <> [] ->
Some (Ast.Pindex (t, idx))
| Ast.Call ({ Ast.e = Ast.Var "deref"; _ }, [ p ]) -> Some (Ast.Pderef p)
| _ -> None
let mk loc ty e : Tast.expr = { Tast.e; ty; loc }
(* A local narrowed by [if x?]: the same slot, read as its payload. *)
let narrowed_tag = "~narrowed"
(* The locals of the function being checked that [if x?] must not narrow:
one whose address is taken, which a pointer could clear behind the
block's back, and one a fn assigns (decision 133, Kotlin's rule). *)
let unnarrowable : string list ref = ref []
let unnarrowable_in (body : Ast.expr list) =
let out = ref [] in
let rec walk ~in_fn (e : Ast.expr) =
(match e.Ast.e with
| Ast.Call ({ Ast.e = Ast.Var ("addr" | "addr-of"); _ }, [ { Ast.e = Ast.Var x; _ } ]) ->
out := x :: !out
| Ast.Set (Ast.Pvar x, _) when in_fn -> out := x :: !out
| _ -> ());
let in_fn = in_fn || (match e.Ast.e with Ast.Fn _ -> true | _ -> false) in
ignore (Ast.map_children (fun x -> walk ~in_fn x; x) e)
in
List.iter (walk ~in_fn:false) body;
!out
(* The [bwhat] of a name an [as] bound (decision 136), for the refusal to
assign it. *)
let as_tag = "~as"
let local_of loc (b : binding) =
if b.bwhat = Some narrowed_tag then
mk loc b.bty (Tast.Field (mk loc (Types.Option b.bty) (Tast.Local b.slot), 1))
else mk loc b.bty (Tast.Local b.slot)
let unit_at loc = mk loc Types.Unit Tast.Unit
(* The compiler temp an [and] leaves in its else arm; see [check_if]. *)
let and_sentinel (x : Ast.expr) =
match x.Ast.e with
| Ast.Var n -> String.length n > 4 && String.sub n 0 4 = "and~"
| _ -> false
(* Integer arithmetic over literals alone, folded. Unlike [const_int] no name
is read: a defconst has a type of its own, and only an untyped constant may
stand at a type variable. *)
let rec literal_arith (e : Ast.expr) : int64 option =
match e.Ast.e with
| Ast.Int n -> Some n
| Ast.Call ({ Ast.e = Ast.Var "-"; _ }, [ x ]) ->
Option.map Int64.neg (literal_arith x)
| Ast.Call ({ Ast.e = Ast.Var op; _ }, x :: y :: rest) ->
let step a b =
match op with
| "+" -> Some (Int64.add a b)
| "-" -> Some (Int64.sub a b)
| "*" -> Some (Int64.mul a b)
| "/" when b <> 0L -> Some (Int64.div a b)
| "%" when b <> 0L && rest = [] -> Some (Int64.rem a b)
| _ -> None
in
List.fold_left
(fun acc e ->
match acc, literal_arith e with
| Some a, Some b -> step a b
| _ -> None)
(literal_arith x) (y :: rest)
| _ -> None
(* A value with no type until one is asked of it: a literal, or arithmetic
over literals alone. *)
let lone_literal (e : Ast.expr) = is_literal e || literal_arith e <> None
(* Set for one [check_value] entry: the literal is checked at the Option
itself, not wrapped, so a refusal names the Option (decision 138). *)
let skip_wrap = ref false
(* [None] as written, which has no type until an Option is asked of it. *)
let is_none_lit (e : Ast.expr) =
match e.Ast.e with Ast.Var "None" -> true | _ -> false
(* A value whose type comes only from defaults — a literal, [nil], [(Some 3)],
arithmetic over literals, a [do] ending in one — so it takes the type of whatever
meets it. An arm of this kind is checked after the others, at their type,
and never decides a join. *)
let rec adapts (e : Ast.expr) =
lone_literal e
|| (match e.Ast.e with
| Ast.Var "nil" -> true
| Ast.Call ({ Ast.e = Ast.Var "Some"; _ }, [ x ]) -> adapts x
| Ast.Call ({ Ast.e = Ast.Var ("+" | "-" | "*" | "/" | "%"); _ },
(_ :: _ as xs)) ->
List.for_all adapts xs
| Ast.Do (_ :: _ as xs) | Ast.Let (_, (_ :: _ as xs)) ->
adapts (List.hd (List.rev xs))
| Ast.If (_, a, Some b) -> adapts a && adapts b
| _ -> false)
(* The environment for a lifted body, built once its own body has been checked
and [caught] is therefore final. spec-memory.md's case 2, and the whole of
its machinery.
It is a struct the checker synthesises — one field per captured name, in
first-reference order — and it is registered in the same table a
[defstruct] goes in, so both backends lay it out with the calculator they
already have and neither learns a new shape. Its name is the lifted
function's, which is unique and stable for the reason that name is: a
redefinition module emits the lifted functions belonging to the bodies it
replaces, and it emits their environments with them.
Two ends, and the copy is at the near one:
- in the *enclosing* frame, a slot holding the struct, filled with a [Make]
of the outer locals. That store is the copy, and it happens where the
value is made. A literal written inside a loop stores into the same slot
each time round, so each value is made from the locals as they were on
its own iteration.
- in the *lifted* frame, a slot holding the pointer, and a [Let] around the
whole body reading each field back into the named slot the body has been
checked against. Once, at entry, for the same reason a handler clause
binds its condition once: what the body names is the copy and not an
address, so nothing downstream has to know an environment exists.
Both new slots are nameless, which is how the break loop is told to hide
them: a reader wants the captured copies, and those are the named slots the
body reads them into. Answers the prefixed body, the slot the pointer
arrives in, the enclosing frame's binding, and the address to put in the
value. *)
let close_over ~fname (octx : ctx) (fctx : ctx) loc =
match fctx.caught with
| [] -> (fun body -> body), None, None, None
| caught ->
let ename = "env/" ^ fname in
let fields =
List.map
(fun (n, ((b : binding), _)) -> { Tast.fname = n; fty = b.bty })
caught
in
jreplace fctx.env.structs ename { Tast.sname = ename; fields };
let ety = Types.Named ename in
let eslot = fresh_slot fctx (Types.Ptr (Types.Mut, ety)) in
let binds =
List.mapi
(fun i (_, ((b : binding), slot)) ->
let p = mk loc (Types.Ptr (Types.Mut, ety)) (Tast.Local eslot) in
(slot, mk loc b.bty (Tast.Field (mk loc ety (Tast.Deref p), i))))
caught
in
let prefix body = [ mk loc fctx.ret (Tast.Let (binds, body)) ] in
let make =
mk loc ety
(Tast.Make (ename,
List.map
(fun (_, ((b : binding), _)) -> local_of loc b)
caught))
in
let mslot = fresh_slot octx ety in
prefix, Some eslot, Some (mslot, make),
Some (mk loc (Types.Ptr (Types.Mut, ety)) (Tast.Addr (Tast.Plocal mslot)))
(* A source location as a value, for a runtime trap that has to name the site
rather than the runtime. The bounds and slice traps get theirs from [Emit],
which renders the [Loc.t] it is already carrying; a trap reached through a
plain runtime call has no such carrier, so the string is built here and
crosses as ptr+len like any other. *)
let here loc = mk loc Types.String (Tast.Str (Loc.to_string loc))
(* Names for the value an [if let] over a plain name holds; [~] keeps them
out of any reader's reach. *)
let held_n = ref 0
(* The read-only slice a value's storage is reached through, if there is one:
an element of a [[const T]], a field of such an element, or an element of
an array that is. The last slice stepped through decides, because the
const is shallow — an element of a [[const [u8]]] is itself a writable
[[u8]], and what it views is not the outer slice's to protect. *)
let rec const_reached (e : Tast.expr) =
match e.Tast.e with
| Tast.Prim (Tast.At, target :: idx) ->
const_steps (const_reached target) target.Tast.ty (List.length idx)
| Tast.Field (target, _) -> const_reached target
| Tast.Deref p ->
(match p.Tast.ty with Types.Ptr (Types.Const, _) -> Some p.Tast.ty | _ -> None)
| _ -> None
(* [ro] after stepping [n] dimensions into [ty], the way [indexed] steps. *)
and const_steps ro (ty : Types.t) n =
if n = 0 then ro
else
match ty with
| Types.Slice (Types.Const, t) -> const_steps (Some ty) t (n - 1)
| Types.Slice (Types.Mut, t) -> const_steps None t (n - 1)
| Types.Array (_, t) -> const_steps ro t (n - 1)
| _ -> ro
(* A copy of a read-only slice's elements that can be written, spelled so it
compiles: [clone], for exactly the element types clone copies. It refuses
elements that own storage — a copy would share their blocks — and
elements that hold a dyn, which its allocator storage cannot root. *)
let const_copy env (e : Types.t) =
if owning env e || holds_dyn env e then None else Some "(clone v)"
(* Whether (clone x) accepts a value of this type — the same arms the clone
builtin takes: a Vec or a Map whose elements own nothing, or a slice whose
elements neither own storage nor hold a dyn. *)
let clone_accepts env (t : Types.t) =
match t with
| Types.Vec _ | Types.Map _ -> not (region_only env t)
| Types.Slice (_, e) -> not (owning env e || holds_dyn env e)
| _ -> false
(* The end of clone's refusal for a container of owning elements. Pushing
the elements themselves into a second container would copy their headers
and share their blocks, so the advice is a copy of each element where
clone takes one, and otherwise that there is no copy to make. *)
let insert_copies ?(loc = Loc.unknown) env (t : Types.t) =
let elem =
match t with
| Types.Vec e | Types.Slice (_, e) | Types.Map (_, e) -> Some e
| _ -> None
in
match elem with
| Some e when clone_accepts env e ->
"Build a second container and push a (clone x) of each element into it"
| Some e ->
Printf.sprintf
"Nothing copies what a %s owns either, so read the elements where they \
are"
(tyname loc e)
| None -> "Build a second container and insert into it"
(* A call written back out as source, for a fix that has to repeat what the
reader wrote: names, integers and calls of those. Anything else is [None]
and the caller says the fix in words. *)
let rec spell_form (a : Ast.expr) =
match a.Ast.e with
| Ast.Var v -> Some v
| Ast.Int n -> Some (Int64.to_string n)
| Ast.UInt (_, s) -> Some s
| Ast.Call (f, args) ->
let parts = List.map spell_form (f :: args) in
if List.mem None parts then None
else Some ("(" ^ String.concat " " (List.filter_map Fun.id parts) ^ ")")
| _ -> None
(* A store through a read-only view: a [[const T]] or a (Ptr const T). *)
let refuse_const_place env loc (view : Types.t) =
match view with
| Types.Ptr (_, ((Types.Vec _ | Types.Map _) as t)) ->
Loc.failk "check/store-through-const" loc
"this changes the %s behind a %s, which can only be read through. A \
container that has to change is handed over as a (Ptr %s)"
(tyname loc t) (tyname loc view) (tyname loc t)
| Types.Ptr (_, t) ->
Loc.failk "check/store-through-const" loc
"this writes through a %s, which can only be read, so what it points at \
is a value and not a place. (deref p) copies the %s out, and the copy \
can be written"
(tyname loc view) (tyname loc t)
| _ ->
let elem = match view with Types.Slice (_, t) -> t | t -> t in
Loc.failk "check/store-through-const" loc
"this writes through a %s, which can only be read, so the element is a \
value and not a place. %s"
(tyname loc view)
(match const_copy env elem with
| Some c ->
Printf.sprintf
"Write into a slice that can be written: %s copies v's elements \
into one" c
| None ->
Printf.sprintf "Where it has to be written, take it as a [%s] instead"
(tyname loc elem))
(* A runtime call, with the result type spelled at the site. *)
let rt loc ty sym args = mk loc ty (Tast.Prim (Tast.Rt sym, args))
(* ── The allocation registry's note ──────────────────────────────────
TODO.org, "The allocation registry".
One after every operation that may have allocated — which is *here*, and
nowhere else, because here is the only place the concrete type is known. A
Flan struct is exactly its C layout with no header and no tag word, so
nothing at run time can say what is at an address; the allocator's caller
knew, and this is the caller writing it down.
The type is spelled with [Types.to_string], the same spelling a slot
fingerprint and a DWARF node already key on, so a name that appears in a
registry answer is a name the programmer wrote.
It is built unconditionally and dropped by the backend in a release build
(see [Emit]'s [Rt] arm). The checker does not know which kind of build this
is and must not learn: a note that existed only in a dev build would make
the two builds different *trees*, and every pass between here and the
backend would have to agree about which one it was looking at.
[target] is the container, passed by address like every other container
operation; the extent comes off its header in the runtime, because the
header is the only thing that knows where the storage landed. *)
let reg_note loc sym (target : Tast.expr) sizes ty =
rt loc Types.Unit sym
((target :: sizes) @ [ mk loc Types.String (Tast.Str (Types.to_string ty)) ])
(* [(do attempt note)] — the note runs only once the guard's retry loop has
stopped, so it describes the storage the program ended up with rather than
one of the attempts that failed. *)
let with_note loc (guarded : Tast.expr) (note : Tast.expr) =
mk loc Types.Unit (Tast.Do [ guarded; note ])
(* ── Reading a file at compile time, decision 1 ────────────────────────
The path is a *literal*, because the bytes have to be in hand before any
value exists — this is Odin's rule too (check_load_directive rejects
anything that is not Addressing_Constant) and it is what makes the result
cost nothing at run time.
It resolves relative to the directory of the file the form is written in,
which is again Odin's rule (dir_from_path of the call's file). Relative to
the compiler's working directory would make a package's assets depend on
where flan was invoked from, which is the thing that cannot be right. An
absolute path is taken as written. *)
let embed_path loc (p : Ast.expr) =
match p.Ast.e with
| Ast.Str "" -> Loc.fail p.Ast.loc "an embedded path cannot be empty"
| Ast.Str s when Filename.is_relative s ->
let base = Filename.dirname loc.Loc.file in
if String.equal base "" then s else Filename.concat base s
| Ast.Str s -> s
| _ ->
Loc.fail p.Ast.loc
"an embedded path must be a literal string"
(* The whole read is guarded, not only the open. On Linux [open_in_bin] on a
*directory* succeeds and [in_channel_length] answers a number; the read is
where EISDIR arrives. Guarding only the open therefore turned (embed "dir")
— someone who meant embed-dir — into an uncaught OCaml exception out of the
checker, which is the one way a user can make the compiler crash rather than
refuse. *)
let read_embed_file path loc =
match
let ch = open_in_bin path in
Fun.protect ~finally:(fun () -> close_in_noerr ch)
(fun () -> really_input_string ch (in_channel_length ch))
with
| s -> s
| exception Sys_error msg ->
if Sys.file_exists path && (try Sys.is_directory path with Sys_error _ -> false)
then
Loc.fail loc
"cannot embed %s: it is a directory — use embed-dir for one of those"
path
else Loc.fail loc "cannot embed %s: %s" path msg
| exception End_of_file ->
Loc.fail loc "cannot embed %s: it changed size while being read" path
(* Non-recursive, files only, sorted by name — the three things Odin's
#load_directory settles, and the sort is the one that matters most here:
readdir order is filesystem-dependent, so an unsorted embed would make the
emitted .ll differ between two builds of identical sources. *)
let read_embed_dir path loc =
let names =
match Sys.readdir path with
| exception Sys_error msg -> Loc.fail loc "cannot embed %s: %s" path msg
| a -> Array.to_list a
in
(* [Sys.is_directory] *raises* on a path that does not resolve, so the
existence test has to come first: a dangling symlink in an embedded
directory would otherwise crash the compiler before it was ever asked
about. Non-recursive and files only, which is Odin's rule too. *)
let files =
List.filter
(fun n ->
let full = Filename.concat path n in
Sys.file_exists full
&& not (try Sys.is_directory full with Sys_error _ -> true))
names
in
List.map
(fun n -> (n, read_embed_file (Filename.concat path n) loc))
(List.sort String.compare files)
let i64_at loc n = mk loc (Types.Int Types.I64) (Tast.Int (n, Types.I64))
(* spec-memory.md, "Alignment": the number is produced where the concrete
element type is known, which without generics is simply the call site. *)
let size_of loc t = mk loc (Types.Int Types.I64) (Tast.Prim (Tast.SizeOf t, []))
let align_of loc t = mk loc (Types.Int Types.I64) (Tast.Prim (Tast.AlignOf t, []))
(* The address of an expression, place or not: the type-erased runtime takes
the element [push] copies by pointer. *)
let addr_of loc (e : Tast.expr) =
mk loc (Types.Ptr (Types.Mut, e.Tast.ty)) (Tast.Prim (Tast.AddrOf, [ e ]))
(* ── A frame slot for a rendered number ────────────────────────────────
The printer and the prelude's number appends render a number into a
buffer that is the caller's, one frame slot per call site, and write or
copy it out before the next; i64->bytes and f64->bytes elsewhere answer
text in the temp allocator instead. The slot is allocated here rather than in either backend on purpose: a slot is a
function-lifetime frame location in both of them — an entry-block alloca in
[Emit], a prologue-allocated offset in [X86] — where a backend temporary in
[X86] is bump-allocated and reclaimed at the end of the expression that made
it, which is exactly the lifetime a returned slice must outlive. Doing it
once here also keeps the two backends symmetric by construction: each gains
one pointer argument and no lifetime reasoning of its own.
64 bytes is agreed with flan_rt.c's FLAN_NUM_BYTES, which clamps the length
it publishes to it. The zeroing the [Let] does is one 64-byte clear beside
an snprintf. *)
let num_bytes = 64L
let to_bytes ctx loc pr (x : Tast.expr) =
let bty = Types.Array (num_bytes, Types.Int Types.U8) in
let bslice = Types.Slice (Types.Mut, (Types.Int Types.U8)) in
let s = fresh_slot ctx bty in
mk loc bslice
(Tast.Let
([ (s, mk loc bty (Tast.Zero bty)) ],
[ mk loc bslice
(Tast.Prim (pr, [ x; addr_of loc (mk loc bty (Tast.Local s)) ])) ]))
(* ── An Allocator value, made and used ─────────────────────────────────
A value is two words, flan_rt.c's [flan_alloc_value]: the runtime's
allocator record and the incarnation of it the value was made for, which
arena-destroy bumps. Every runtime operation takes the bare record, typed
[raw_alloc] here; [seal_alloc] makes a value from one and [use_alloc] opens
one, trapping if the incarnation has moved. So a value kept past its arena's
destroy traps at its next use, including after arena-new has taken the
record back for another arena — which a one-word value could not tell from
the new arena's own. Both cross through the value's address, since nothing
the runtime answers or takes is a struct by value. *)
let raw_alloc = Types.Ptr (Types.Mut, Types.Unit)
let seal_alloc ctx loc (record : Tast.expr) =
let s = fresh_slot ctx Types.Alloc in
mk loc Types.Alloc
(Tast.Let
([ (s, mk loc Types.Alloc (Tast.Zero Types.Alloc)) ],
[ rt loc Types.Unit "flan_alloc_seal"
[ record; addr_of loc (mk loc Types.Alloc (Tast.Local s)) ];
mk loc Types.Alloc (Tast.Local s) ]))
let use_alloc ctx loc (v : Tast.expr) =
let s = fresh_slot ctx Types.Alloc in
mk loc raw_alloc
(Tast.Let
([ (s, v) ],
[ rt loc raw_alloc "flan_alloc_use"
[ addr_of loc (mk loc Types.Alloc (Tast.Local s)); here loc ] ]))
(* A string literal handed to a [declare-c] function goes to C without the copy
the wrapper makes of any other string. Both backends write a NUL after a
literal's bytes, and here — the one place that knows the argument is a
literal — it is passed with its length encoded as -(n+1) by
[flan_c_literal]. No other Flan string has a negative length, so the
wrapper can tell ([Shim.cstr_helpers]); a string that merely ends in a NUL
is still copied and refused.
The callee is a declare-c when it binds the shim's symbol for its own name —
directly, or through the flattened [-c] declaration the shim puts under a
Flan wrapper. The encoded value passes through that wrapper untouched,
because its body only forwards it. *)
let c_literals ctx name (params : Types.t list) (args : Tast.expr list) =
let sym = Shim.shim_symbol name in
let bound n = Hashtbl.find_opt ctx.env.externs n = Some sym in
if not (bound name || bound (Shim.raw_name name)) then args
else
List.map2
(fun (p : Types.t) (a : Tast.expr) ->
match p, a.Tast.e with
| Types.String, Tast.Str _ ->
let loc = a.Tast.loc in
let s = fresh_slot ctx Types.String in
mk loc Types.String
(Tast.Let
([ (s, mk loc Types.String (Tast.Zero Types.String)) ],
[ rt loc Types.Unit "flan_c_literal"
[ a; addr_of loc (mk loc Types.String (Tast.Local s)) ];
mk loc Types.String (Tast.Local s) ]))
| _ -> a)
params args
(* ── The region requirement, emitted ───────────────────────────────────
spec-memory.md's arena rule, and the whole of what replaced the three
refusals a container of owning elements used to meet at its *type*. The
question those refusals asked was about teardown: the type-erased runtime
copies and releases slots bytewise, so a [free] would release the slots and
leave everything inside them stranded. A region never releases a slot —
[free-all] takes the whole thing, inner blocks included, because they came
out of the same region — so the premise does not hold there and the refusal
was over-broad.
What could not move with it is *where* the question is asked. [can-free] is
a capability on an allocator value, read at run time, and [with-allocator]
rebinds a dynamic variable, so the tier a [(vec-new)] will meet is not a
property of the place its type is written. The compile-time half is
therefore only the decision to ask — [region_only], a property of the
element type and settled here — and the run-time half is the answer.
One branch per container and never per element, which spec-memory.md fixes
and which is a performance decision before it is a safety one: the
alternative is a walk at release, and a walk at release is the registry of
destructors the frame tier's reset exists to not have.
Emitted at every site that can *allocate* for such a container, not only at
its construction, and the extra sites are not belt and braces. ZII means a
container can exist without ever passing through [vec-new]: a data type
case's field left out of a literal, a [(defonce xs (Vec Value))] a global
starts as. Those are zeroed, they have no allocator at all, and the first
[push] is what adopts the context — so a guard only at the construction
would have a hole exactly the width of ZII.
The *container* is what is asked in both places, and at a construction that
means the guard runs immediately after the init rather than before it. The
allocator is right there as an argument of the site, but naming it twice in
the emitted tree is not free: it is an arbitrary expression, and [(vec-new
Value (arena-new 4096))] would build two arenas and guard the one it threw
away. The container has recorded it by the time the init returns, so asking
the container asks that one expression exactly once. It is still the point
of construction and still before anything is put in; what a trap there costs
is the empty block just taken, on a process that is about to die.
At a growth the guard runs first, because there the container already exists
and the allocation is what has to be stopped. A zeroed one answers from the
context it is about to adopt, which is not a guess — [flan_vec_adopt] is the
code that will take it, on this same call. *)
let region_sym (t : Types.t) =
match t with
| Types.Vec _ -> "flan_vec_region_only"
| Types.Map _ -> "flan_map_region_only"
| _ -> assert false
let region_check env loc (target : Tast.expr) (after : Tast.expr) =
if not (region_only env target.Tast.ty) then after
else
mk loc after.Tast.ty
(Tast.Do
[ rt loc Types.Unit (region_sym target.Tast.ty) [ target; here loc ];
after ])
(* Every integer index into an array or slice is i32 at milestone 2. *)
let index_ty = Types.Int Types.I32
(* A condition's type at run time is a number, and it has to be the *same*
number in a module compiled later against a program already running. So it
is a hash of the name and not an index into anything: an index would shift
the moment a struct were added, and every handler pushed by the old code
would then match the wrong type. FNV-1a over the name, 32 bits. *)
let type_id name =
let h = ref 0x811c9dc5 in
String.iter
(fun c ->
h := (!h lxor Char.code c) land 0xffffffff;
h := (!h * 0x01000193) land 0xffffffff)
name;
!h
(* A condition type and every type it names as a parent, own first. The chain
is static: a signal site knows its condition's type, so the whole walk a
handler match makes is written into the site's descriptor, and the runtime
only compares numbers. [collect] has refused a cycle, but the walk stops at
one anyway rather than trusting that it ran. *)
let condition_chain env name =
let rec go seen n =
if List.mem n seen then List.rev seen
else
match Hashtbl.find_opt env.parents n with
| Some p -> go (n :: seen) p
| None -> List.rev (n :: seen)
in
go [] name
(* How a restart's parameter list is spelled, and with it what the two ends of
an [invoke-restart] compare — spec-conditions.md §3's run-time check. A
restart is found by name on a dynamic stack, so neither end can see the
other and nothing static can be checked: what is compared at run time is
this string's hash, alongside the count, and the string itself is carried so
that a mismatch can say what was wanted and what was given.
Comparing a 32-bit hash means two different parameter lists could in
principle collide. The count is checked separately, which rules out every
practical case (a collision would have to be between two lists of the same
length), and the types are parenthesised so that [(Option i32)] cannot read
as two parameters. *)
let restart_sig tys =
"(" ^ String.concat " " (List.map Types.to_string tys) ^ ")"
(* ── The dyn boundary ───────────────────────────────────────────────────
Typed to dyn is implicit and dyn to typed is not. That asymmetry is the
whole of the design and it is worth saying why it is not arbitrary.
Boxing loses nothing: the value goes in and the runtime records what it was.
It can happen anywhere a dyn is wanted without a reader being surprised,
because nothing about the program's meaning turns on it. Unboxing can fail,
at run time, on a value the compiler cannot inspect -- so it happens only
where somebody *wrote a type*: a typed parameter, a typed binding, a typed
field. Those are the places a reader already understands as a claim about
what a value is, and a claim that can be wrong is exactly what a trap is
for. Nowhere else does the compiler decide a dyn is an i64 on its own.
Both directions go through [expect], because [expect] is already the one
place a wanted type meets a produced one. Every site that annotates -- and
only those sites -- calls it with [~want].
Milestone 1 boxes the scalars and refuses everything else by name. A typed
container crossing into dyn is the interesting refusal: [(Vec i64)] has a
representation the dyn runtime does not know how to walk, and heterogeneity
at milestone 1 is served by the runtime's own vector behind
[flan_dyn_vec_new] instead. That is a "not yet" and says so. *)
let dyn_i64 = Types.Int Types.I64
let dyn_f64 = Types.Float Types.F64
(* The switch the unfinished half of TODO.org's "Dyn unless annotated" is
measured with: [dyn] makes an unwanted text or bracket literal dyn (a
let-bound one stays typed when a use wants it, [lit_session]), and [log]
prints each literal local inference moved off its default. Deleted when
that half lands. *)
let lit_mode = try Sys.getenv "FLAN_LIT" with Not_found -> ""
let lit_has m = List.mem m (String.split_on_char ',' lit_mode)
(* An unconstrained float literal is an f64 (decision 121); it is an f32 only
where typed code wants one. *)
let float_default () = Types.F64
(* ── Literal locals ([lit_session]) ────────────────────────────────── *)
(* The literal a [let] or [loop] initialiser is, when its type is to be read
off the uses: a number or a character, negated or not. A bool has one type
and a wide literal one (u64), so neither has anything to infer. *)
let lit_kind (e : Ast.expr) =
match e.Ast.e with
| Ast.Int _ -> Some `Int
| Ast.Byte _ -> Some `Char
| Ast.Float _ -> Some `Float
| Ast.Call ({ Ast.e = Ast.Var "-"; _ }, [ { Ast.e = Ast.Int _; _ } ]) -> Some `Int
| Ast.Call ({ Ast.e = Ast.Var "-"; _ }, [ { Ast.e = Ast.Float _; _ } ]) -> Some `Float
| Ast.Str _ | Ast.Arr (_ :: _) when lit_has "dyn" -> Some `Box
| _ -> None
(* What the literal is with no use to say otherwise. *)
(* A text or bracket literal ([`Box]) is dyn, or with a typed use its typed
reading; [Types.Unit] stands for "typed" in [decided], since the typed
reading is the literal's own and not one a use names. *)
let lit_default (_ : Ast.expr) = function
| `Int -> Types.Int Types.I32
| `Char -> Types.Char
| `Float -> Types.Float (float_default ())
| `Box -> Types.Dyn
(* The types a use can give it: any number for an integer or a character,
since an untyped integer constant is usable where a float is wanted, and
only a float for a float. A character may also be a char, its own type. A type variable is admitted and left to
[int_literal] to judge against its bound. Anything else — dyn, a struct —
says nothing about the literal's type; the local keeps its guess and the
use is checked as it always was. *)
let lit_admits kind (t : Types.t) =
match kind, t with
| (`Int | `Char), (Types.Int _ | Types.Float _ | Types.Var _) -> true
| `Char, Types.Char -> true
| `Float, (Types.Float _ | Types.Var _) -> true
| `Box, t -> not (Types.equal t Types.Dyn)
| _ -> false
(* What a use at [t] says about a literal local: an (Option T) wanted of it
says T, since the local is built at T and then wrapped (decision 138), so
[let w: i64? = x] makes [x] the i64 [let w: i64 = x] does. *)
let rec lit_payload (t : Types.t) =
match t with Types.Option p -> lit_payload p | t -> t
(* The rounds a session may take before its last guesses are checked as
they stand. Merging makes two the usual count; the bound only stops a
pathological program from looping. *)
let lit_rounds = 8
let lit_id (s : lit_session) (key : Ast.expr) = Phys.find_opt s.ids key
let rec lit_root (s : lit_session) i =
match Hashtbl.find_opt s.parent i with
| Some p when p <> i ->
let r = lit_root s p in
Hashtbl.replace s.parent i r;
r
| _ -> i
let lit_add (s : lit_session) key c =
match lit_id s key with Some i -> Hashtbl.add s.cons i c | None -> ()
let lit_union (s : lit_session) a b =
match lit_id s a, lit_id s b with
| Some i, Some j ->
let ri = lit_root s i and rj = lit_root s j in
if ri <> rj then Hashtbl.replace s.parent (max ri rj) (min ri rj)
| _ -> ()
(* What earlier sessions decided, while an outermost one is open. An outer
session that checks its form again checks every lambda inside it again,
and each of those opens a session of its own; starting that one from its
last answer makes it settle in one round, where starting from the default
made nested lambdas cost a factor per level. Keyed by the node and the
type variables' bindings, since a generic's body is one node checked at
several types. Only ever a first guess: a wrong one costs a round. *)
let lit_depth = ref 0
let lit_memo : ((string * Types.t) list * Types.t) list Phys.t = Phys.create 64
let lit_guess ~subst (s : lit_session) key kind =
match Phys.find_opt s.decided key with
| Some t -> t
| None ->
let same (sb, _) =
List.equal (fun (a, t) (b, u) -> String.equal a b && Types.equal t u) sb subst
in
match Option.bind (Phys.find_opt lit_memo key) (List.find_opt same) with
| Some (_, t) -> t
| None -> lit_default key kind
(* Each literal local this round bound, with the type its group's uses decide
or the first pair of uses no one type satisfies. Linear in locals and
uses. *)
let lit_solve (s : lit_session) =
let keys = Array.of_list (List.rev s.keys) in
let n = Array.length keys in
let root = Array.init n (lit_root s) in
let members = Array.make n [] in
for i = n - 1 downto 0 do members.(root.(i)) <- i :: members.(root.(i)) done;
let widens a b = Types.equal a b || Types.widens_to ~from:a ~into:b in
let result = Array.make n (Ok Types.Unit) in
Array.iteri
(fun r ms ->
if ms <> [] then begin
let kinds = List.map (fun m -> Option.value (lit_kind (fst keys.(m))) ~default:`Int) ms in
let kind =
if List.mem `Box kinds then `Box
else if List.mem `Float kinds then `Float
else if List.mem `Int kinds then `Int
else `Char
in
let dyn_width =
match kind with `Float -> Types.Float Types.F64 | _ -> Types.Int Types.I64
in
let cons =
List.concat_map (fun m -> List.rev (Hashtbl.find_all s.cons m)) ms
|> List.filter_map (fun (c, t, l) ->
(* A character meeting dyn stays a char, which crosses as
a dyn char; a number takes the dyn width. *)
if kind = `Char && Types.equal t Types.Dyn then None
else if kind <> `Box && Types.equal t Types.Dyn then Some (Hint, dyn_width, l)
else if lit_admits kind t then Some (c, t, l)
else None)
in
let pick c = List.filter_map (fun (c', t, l) -> if c' = c then Some (t, l) else None) cons in
let res =
if kind = `Box then Ok (if cons = [] then Types.Dyn else Types.Unit)
else
let ups = pick Up and downs = pick Down and hints = pick Hint in
match ups with
| (u0, l0) :: _ ->
(match List.find_opt (fun (u, _) -> List.for_all (fun (u', _) -> widens u u') ups) ups with
| None ->
let (u1, l1) = List.find (fun (u, _) -> not (widens u u0 || widens u0 u)) ups in
Error ((u0, l0), (u1, l1))
| Some (c, lc) ->
(match List.find_opt (fun (d, _) -> not (widens d c)) downs with
| None -> Ok c
| Some (d, ld) -> Error ((c, lc), (d, ld))))
| [] ->
(match downs @ hints with
| [] ->
(* No use names a type: the widest of the members' own. *)
Ok (List.fold_left
(fun acc m ->
let t = lit_default (fst keys.(m)) kind in
match Types.join acc t with Some j -> j | None -> acc)
(lit_default (fst keys.(r)) kind) ms)
| (t0, l0) :: rest ->
let rec fold (t, l) = function
| [] -> Ok t
| (t', l') :: rest ->
(match Types.join t t' with
| Some j -> fold ((j, if Types.equal j t then l else l')) rest
| None -> Error ((t, l), (t', l')))
in
fold (t0, l0) rest)
in
List.iter (fun m -> result.(m) <- res) ms
end)
members;
Array.to_list (Array.mapi (fun i (k, name) -> (k, name, result.(i))) keys)
(* Converting to whatever width the other side of the boundary wants, with a
[Cast] and not a silent reinterpretation. The name is for the direction it
was written for: runtime/flan_dyn.h boxes integers as [i64] and floats as
[f64] and offers no other width, so *into* a box this only ever widens, and
a widening cast loses nothing.
Coming back *out* it is used in both directions, and that is deliberate:
[cast_dyn] below unboxes to [i64]/[f64] and then hands the value to this to
reach the cast's target, which may be narrower ([(i32 d)]) or a different
kind ([(f32 d)]). Nothing is lost quietly there either — the [Cast] emitted
is the same node [(i32 x)] on a typed value emits, so the narrowing rule,
the fptosi range check and NaN are the emitter's, identical to the typed
spelling. This helper picks the node; it does not promise the conversion is
free. *)
let widen loc (want : Types.t) (e : Tast.expr) =
if Types.equal want e.Tast.ty then e
else mk loc want (Tast.Prim (Tast.Cast want, [ e ]))
(* The sentence a refusal at this boundary gives. It names the type and says
which direction failed, because "expected dyn, found (Vec i64)" would read
as a type error the programmer could fix by writing something else, and
there is nothing else to write -- the feature is not there yet. *)
let no_dyn_yet loc ~into t extra =
Loc.failk "check/dyn-not-yet" loc
"%s does not cross into %s yet%s"
(tyname loc t) (if into then "dyn" else "a written type") extra
(* A typed container crossing into dyn is a view, and the runtime needs to
know what one element is: its descriptor, a prefix code runtime/flan_dyn.c
documents beside [desc_lay] and reads offsets out of by C's layout rule.
Every number, bool, char, str, struct of those, and fixed array, slice or
Vec of those can be described. [Error t] names the first type inside that cannot:
a dyn, a pointer, a function, an Option, a map, an enum or a data type.
None of those is refused for want of a descriptor letter — each is one a
dyn value cannot be read out of or written into without a meaning
nobody has decided. A str is read as a copy and never written, since a
dyn text is a collector pointer and typed storage is never scanned; a
[const] slice is refused because a dyn view can be written through. One
is described only [~into] a written type ([into_typed]), as [c]. *)
let rec view_desc ?(into = false) structs (t : Types.t)
: (string, Types.t) result =
let view_desc = view_desc ~into in
let ( let* ) = Result.bind in
match t with
| Types.Int k ->
Ok (match k with
| Types.I8 -> "b" | Types.U8 -> "B" | Types.I16 -> "h" | Types.U16 -> "H"
| Types.I32 -> "i" | Types.U32 -> "I" | Types.I64 -> "l" | Types.U64 -> "L")
| Types.Float Types.F32 -> Ok "f"
| Types.Float Types.F64 -> Ok "d"
| Types.Bool -> Ok "?"
| Types.Char -> Ok "C"
| Types.String -> Ok "t"
| Types.Array (n, e) ->
let* d = view_desc structs e in
Ok (Printf.sprintf "a%Ld;%s" n d)
| Types.Slice (Types.Mut, e) -> let* d = view_desc structs e in Ok ("s" ^ d)
| Types.Slice (Types.Const, e) when into ->
let* d = view_desc structs e in Ok ("c" ^ d)
| Types.Vec e -> let* d = view_desc structs e in Ok ("v" ^ d)
| Types.Named n ->
(match Hashtbl.find_opt structs n with
| None -> Error t
| Some st ->
let* fs =
List.fold_left
(fun acc (fl : Tast.field) ->
let* acc = acc in
let* d = view_desc structs fl.Tast.fty in
Ok ((fl.Tast.fname ^ ";" ^ d) :: acc))
(Ok []) st.Tast.fields
in
Ok ("{" ^ n ^ ";" ^ String.concat "" (List.rev fs) ^ "}"))
| _ -> Error t
(* A fix is spelled in the syntax of the file the mistake is in: the checker
sees one AST for both, so the location's file is the only thing left that
says which one the reader is looking at. *)
let fln_source (loc : Loc.t) = Source.indented_at loc
(* A version's own name: see [split_versions]. *)
let version_name base arity = base ^ "~" ^ string_of_int arity
(* The name as written and the arity, for a name [version_name] made. *)
let version_of n =
match String.rindex_opt n '~' with
| Some i when i > 0 && i < String.length n - 1 ->
let tail = String.sub n (i + 1) (String.length n - i - 1) in
if String.for_all (fun c -> c >= '0' && c <= '9') tail then
Some (String.sub n 0 i, int_of_string tail)
else None
| _ -> None
(* A name as its reader wrote it: a version's is the name it is a version of,
which is what every message about it says. *)
let written_name n =
match version_of n with
| Some (b, _) -> b
| None ->
(* A generic version's copy, [pick~3-f64], is the copy [pick-f64]. *)
match String.rindex_opt n '~' with
| Some i when i > 0 ->
let j = ref (i + 1) in
while !j < String.length n && n.[!j] >= '0' && n.[!j] <= '9' do incr j done;
if !j > i + 1 && !j < String.length n && n.[!j] = '-' then
String.sub n 0 i ^ String.sub n !j (String.length n - !j)
else n
| _ -> n
(* Which form defined each mutable global, [defonce] or [def], so a fix that
rewrites the definition keeps the form the programmer chose. Filled where
globals are collected; a name missing from it (a defconst) is given
[defonce]. *)
let global_forms : (string, Ast.reinit) Hashtbl.t = Hashtbl.create 16
(* The function being checked and its parameters, by slot. A stack because
a generic's copy is checked from inside the body that called it. *)
let grow_params : (ctx * (int * Ast.field) list) list ref = ref []
(* The two refusals below share their subject and their fix. The subject is
the name as written when the refused value is a bare name, so the message
can say [a is a [4 i64]]; anything longer is "this". The fix is the one
spelling that works for every container either refusal reaches, whatever
its element type or wherever it lives: make it a dyn value where it is
built, and there is no view to refuse. *)
let view_subject (e : Tast.expr) =
match Loc.snippet e.Tast.loc with
| Some s
when s <> ""
&& String.for_all
(fun c -> not (List.mem c [ ' '; '('; ')'; '['; ']'; '{'; '}'; '"'; '.'; ',' ]))
s ->
Some s
| _ -> None
let view_refusal kind loc (e : Tast.expr) reason =
let ty = tyname loc e.Tast.ty in
let fln = fln_source loc in
(* A parameter is made by the caller, so its fix is its declaration. The
name is compared as well as the slot: a closure numbers its slots from
zero too, and is checked while its enclosing function is on the stack. *)
let param =
match e.Tast.e, view_subject e, !grow_params with
| Tast.Local s, Some n, (ctx, ps) :: _ ->
(match List.assoc_opt s ps with
| Some (p : Ast.field) when p.Ast.fname = n -> Some (ctx.owner, n)
| _ -> None)
| _ -> None
in
let subject, fix =
match param, view_subject e with
| Some (f, n), _ ->
( Printf.sprintf "%s is a %s parameter" n ty,
Printf.sprintf "Declare %s as dyn in %s's parameters: %s%s" n f n
(if fln then ": dyn" else " dyn") )
| None, Some n when (match e.Tast.e with Tast.Global _ -> true | _ -> false) ->
let every =
match e.Tast.e with
| Tast.Global g -> Hashtbl.find_opt global_forms g = Some Ast.Every
| _ -> false
in
( Printf.sprintf "%s is a %s" n ty,
Printf.sprintf "Define %s as a dyn value, as in %s" n
(if fln then
Printf.sprintf "%s %s: dyn = [...]" (if every then "def" else "once") n
else
Printf.sprintf "(%s %s dyn [...])" (if every then "def" else "defonce") n) )
| None, Some n ->
( Printf.sprintf "%s is a %s" n ty,
Printf.sprintf "Build %s as a dyn value where it is made, as in %s" n
(if fln then Printf.sprintf "let %s: dyn = [...]" n
else Printf.sprintf "(let [%s (the dyn [...])] ...)" n) )
| None, None ->
( Printf.sprintf "This is a %s" ty,
Printf.sprintf "Build it as a dyn value where it is made, as in %s"
(if fln then "the(dyn, [...])" else "(the dyn [...])") )
in
Loc.failk kind loc "%s, and a dyn value is wanted here. %s. %s" subject
reason fix
let view_not_yet loc (e : Tast.expr) (inner : Types.t) =
view_refusal "check/dyn-not-yet" loc e
(Printf.sprintf
"A dyn value sees into numbers, bools, str and structs, and arrays, \
slices and Vecs of those; a %s is none of these"
(tyname loc inner))
(* Whether a view's storage is the current function's own frame, which is
what the runtime's dev check needs to be told: it then records this
activation and traps if the view is used after the call returns. A local,
a parameter (copied into the frame, an array parameter too), a field or
an array element of one, a slice cut directly from a local array, and a
temporary [box] has bound to a slot of its own are all the frame's. For
anything else — a slice's data, a [Ptr]'s target, a global — the dev
runtime finds the frame that owns a stack address by the address itself,
or else the registry block that holds it. *)
let rec frame_root (e : Tast.expr) : bool =
let rec all_array ty = function
| [] -> true
| _ :: rest ->
(match ty with Types.Array (_, elem) -> all_array elem rest | _ -> false)
in
match e.Tast.e with
| Tast.Local _ -> (match e.Tast.ty with Types.Slice _ -> false | _ -> true)
| Tast.Field (target, _) ->
(match target.Tast.ty with Types.Named _ -> frame_root target | _ -> false)
| Tast.Prim (Tast.At, target :: idx) ->
all_array target.Tast.ty idx && frame_root target
| Tast.Prim (Tast.Slice, [ target; _; _ ]) ->
(match target.Tast.ty with Types.Array _ -> frame_root target | _ -> false)
| _ -> false
(* Whether [e] names storage that already has an address, so a view can
point at it; anything else is a temporary [box] binds to a slot first. *)
let rec view_place (e : Tast.expr) : bool =
match e.Tast.e with
| Tast.Local _ | Tast.Global _ | Tast.Deref _ -> true
| Tast.Field (target, _) ->
(match target.Tast.ty with Types.Named _ -> view_place target | _ -> true)
| Tast.Prim (Tast.At, _ :: _ :: _) -> true
| _ -> false
(* A value handed out of [f] that points into [f]'s own frame: returned (the
last form's tails, or a [return]), or stored into a global or a field or
array element of one. Both read a dead frame the moment [f] returns, so
both are refused.
Deliberately narrow — the exact spellings that can only be wrong, so
nothing that could be valid is ever refused:
- [(addr p)] where [p] is a local, a field of one, or an element of a
local *array* (an array's elements are the frame's bytes; a Vec's or a
slice's are not);
- [(slice a …)] where [a] is such a place of array type;
- a local bound by [let] to one of those and never assigned or addressed
afterwards, which is also how a [return] under a [defer] arrives here.
A parameter is a local: its value is copied into the frame (an array
parameter too), so its address dies with the frame as well. Anything
reached through a [Ptr] is not the frame's, and a struct literal holding
such an address is not looked into.
The store is let through in [main], whose frame outlives everything the
program runs.
A lifted [fn] literal or handler clause is checked as a function of its
own: its parameters, its locals and its copies of what it captured are its
frame. What this does not see is the enclosing function's frame escaping
*through* one — a closure over a pointer to a local, handed out — because
that is a value holding an address, not one of the spellings above. *)
let refuse_frame_escapes (f : Tast.fn) =
(* Keyed by slot alone: [fresh_slot] never reuses one, so a slot has at
most one binding [Let] in the function. *)
let binds = Hashtbl.create 16 in
let unstable = Hashtbl.create 16 in
(* A place a global owns, and so one that outlives every frame: the global,
a field or array element of it, or an element of a Vec it holds — the
Vec's block is the global's for as long as the global keeps it. A slice
is not stepped through: its storage may be anyone's. *)
let rec place_global = function
| Tast.Pglobal g -> Some g
| Tast.Pfield (t, _) -> expr_global t
| Tast.Pindex (t, idx) when owned_levels t.Tast.ty idx -> expr_global t
(* [(set (at v i) x)] on a Vec is a store through the checked element
address [vec_at] builds. Any other pointer's target is not known to
be the global's. *)
| Tast.Pderef { Tast.e = Tast.Prim (Tast.Rt "flan_vec_at", t :: _); _ } ->
expr_global t
| _ -> None
and expr_global (e : Tast.expr) =
match e.Tast.e with
| Tast.Global g -> Some g
| Tast.Field (t, _) -> expr_global t
| Tast.Prim (Tast.At, t :: idx) when owned_levels t.Tast.ty idx ->
expr_global t
| _ -> None
and owned_levels ty = function
| [] -> true
| _ :: rest ->
(match ty with
| Types.Array (_, el) | Types.Vec el -> owned_levels el rest
| _ -> false)
(* [(at a i j)] is one node carrying every index; each level stepped must
be an array for the element to be inside [a]'s own bytes. *)
and all_array ty = function
| [] -> true
| _ :: rest ->
(match ty with Types.Array (_, el) -> all_array el rest | _ -> false)
in
List.iter
(Tast.walk (fun (e : Tast.expr) ->
match e.Tast.e with
| Tast.Let (bs, _) -> List.iter (fun (s, v) -> Hashtbl.replace binds s v) bs
| Tast.Set (Tast.Plocal s, _) | Tast.Addr (Tast.Plocal s) ->
Hashtbl.replace unstable s ()
| _ -> ()))
f.Tast.body;
(* The local at the root of a place inside this frame, if it is one. *)
let rec root_expr (e : Tast.expr) =
match e.Tast.e with
| Tast.Local s -> Some s
| Tast.Field (t, _) -> root_expr t
| Tast.Prim (Tast.At, t :: idx) when all_array t.Tast.ty idx -> root_expr t
| _ -> None
in
let root_place = function
| Tast.Plocal s -> Some s
| Tast.Pfield (t, _) -> root_expr t
| Tast.Pindex (t, idx) when all_array t.Tast.ty idx -> root_expr t
| _ -> None
in
(* What escapes: the root local, whether it is a slice (else an address),
and whether the place is the bare local rather than a path into it —
only then can the fix be spelled with its name alone. *)
let rec escapes depth (e : Tast.expr) =
match e.Tast.e with
| Tast.Addr p ->
Option.map
(fun s ->
(e, (s, `Addr, (match p with Tast.Plocal _ -> true | _ -> false))))
(root_place p)
| Tast.Prim (Tast.Slice, [ t; _; _ ]) ->
(match t.Tast.ty with
| Types.Array _ ->
Option.map
(fun s ->
( e,
(s, `Slice,
(match t.Tast.e with Tast.Local _ -> true | _ -> false)) ))
(root_expr t)
| _ -> None)
| Tast.Local s when depth < 32 && not (Hashtbl.mem unstable s) ->
Option.bind (Hashtbl.find_opt binds s) (escapes (depth + 1))
| _ -> None
in
(* A slot with no name is a value the function made for itself, such as
the array a literal like [(slice [7 8 9])] is stored in. *)
(* What a lifted body is called in a message: its symbol is the compiler's. *)
let who =
let starts p =
String.length f.Tast.name >= String.length p
&& String.sub f.Tast.name 0 (String.length p) = p
in
if starts "fn/" then "this fn"
else if starts "handler/" then "this handler"
else f.Tast.name
in
let what (s, kind, exact) =
match f.Tast.snames.(s), kind, exact with
| Some n, `Slice, true -> "a slice of " ^ n
| Some n, `Slice, false -> "a slice of an array inside " ^ n
| Some n, `Addr, true -> "the address of " ^ n
| Some n, `Addr, false -> "an address inside " ^ n
| None, `Slice, _ -> "a slice of a temporary array"
| None, `Addr, _ -> "the address of a temporary"
in
(* Reported at the addr or slice itself: a [return] under a [defer], or a
local bound to one, reaches here as a read of a slot, and the caret
belongs on the form that took the address. *)
let fail ~verb ~target ~fix_slice ~fix_addr
((e : Tast.expr), ((s, kind, exact) as hit)) =
let fix =
(* The slice as written, bounds and all, when the source can be read
back; its root's name otherwise. *)
let rec bound (b : Tast.expr) =
match b.Tast.e with
| Tast.Int (v, _) -> Some (Int64.to_string v)
| Tast.Local i -> f.Tast.snames.(i)
| Tast.Prim (Tast.Cast _, [ x ]) -> bound x
| _ -> None
in
let rebuilt =
match e.Tast.e, f.Tast.snames.(s), exact with
| Tast.Prim (Tast.Slice, [ t; lo; hi ]), Some n, true ->
(match t.Tast.ty, bound lo, bound hi with
| Types.Array (len, _), Some "0", Some h
when h = Int64.to_string len ->
Some ("(slice " ^ n ^ ")")
| _, Some l, Some h -> Some ("(slice " ^ n ^ " " ^ l ^ " " ^ h ^ ")")
| _ -> None)
| _ -> None
in
let written =
match rebuilt, Loc.snippet ~lim:120 e.Tast.loc with
| Some t, _ -> Some t
| None, Some t
when String.length t > 7 && String.sub t 0 7 = "(slice "
&& not (String.contains t '\n')
&& not (String.ends_with ~suffix:"\xe2\x80\xa6" t) -> Some t
| _ -> None
in
match written, f.Tast.snames.(s), kind, exact with
| Some t, _, `Slice, _ ->
fix_slice ("wrap the slice in clone, as in (clone " ^ t ^ ")")
| None, Some n, `Slice, true ->
fix_slice ("wrap the slice in clone, as in (clone (slice " ^ n ^ "))")
| _, _, `Slice, _ -> fix_slice "wrap the slice in (clone ...)"
| _, _, `Addr, _ ->
let pointee =
match e.Tast.ty with
| Types.Ptr (_, t) -> tyname e.Tast.loc t
| t -> tyname e.Tast.loc t
in
fix_addr pointee
in
let whose =
match f.Tast.snames.(s) with
| Some n -> Printf.sprintf "%s is a local of %s" n who
| None -> "That value lives in the frame of " ^ who
in
Loc.failk "check/frame-escape" e.Tast.loc
"%s %s %s%s. %s, and its storage is gone once %s returns, so every \
later read through it reads whatever the next call leaves there. %s"
(if String.equal who f.Tast.name then who
else String.capitalize_ascii who)
verb (what hit) target whose who fix
in
(* Every value position of the body, walked by [Tast.iter_tails]: a
restart clause is a branch of this function whose value is the form's
value when that restart is taken, and it is on that list. *)
let tails =
Tast.iter_tails (fun (e : Tast.expr) ->
Option.iter
(fail ~verb:"returns" ~target:""
~fix_slice:(fun c ->
"Return a copy the caller owns: " ^ c
^ ", which puts the elements in the context allocator")
~fix_addr:(fun t ->
if String.equal who f.Tast.name then
"Return the value instead: declare " ^ who
^ " to return " ^ t ^ " and drop the addr"
else
"Return the value, of type " ^ t
^ ", instead of its address: drop the addr"))
(escapes 0 e))
in
(* Only a store into the global itself can be fixed by changing the
global's type; for a field, an element or a push, the value's type
belongs to something else. *)
let stored ~bare g v =
Option.iter
(fail ~verb:"stores" ~target:(" into the global " ^ g)
~fix_slice:(fun c ->
"Store a copy that outlives the frame: " ^ c
^ ", which puts the elements in the context allocator")
~fix_addr:(fun t ->
if bare then
"Store the value instead: declare " ^ g ^ " as " ^ t
^ " and drop the addr"
else "Store the value instead of its address"))
(escapes 0 v)
in
let returns = not (Types.equal f.Tast.ret Types.Unit) in
List.iter
(Tast.walk (fun (e : Tast.expr) ->
match e.Tast.e with
| Tast.Return (Some v) when returns -> tails v
| Tast.Set (p, v) when not (String.equal f.Tast.name "main") ->
Option.iter
(fun g ->
stored ~bare:(match p with Tast.Pglobal _ -> true | _ -> false)
g v)
(place_global p)
(* A push or put into a container a global owns: the element was
bound to a slot first, and its address is what the call takes. *)
| Tast.Prim (Tast.Rt ("flan_vec_push" | "flan_map_put"), target :: rest)
when not (String.equal f.Tast.name "main") ->
Option.iter
(fun g ->
List.iter
(fun (a : Tast.expr) ->
match a.Tast.e with
| Tast.Prim (Tast.AddrOf, [ v ]) -> stored ~bare:false g v
| _ -> ())
rest)
(expr_global target)
| _ -> ()))
f.Tast.body;
if returns then
match List.rev f.Tast.body with x :: _ -> tails x | [] -> ()
(* ── String, the prelude's owned text ────────────────────────────────
A (defstruct String [bytes (Vec u8)]) in the prelude, kept valid UTF-8 by
the arms in [named_call] that are the only way to change one — see
[string_call]. Every run-time operation reaches the Vec, which is the
struct's one field: the backends see a struct holding a Vec and nothing
else, which is why neither has a String of its own. *)
let string_ty = Types.Named "String"
let is_string_ty t = Types.equal t string_ty
let string_vec_ty = Types.Vec (Types.Int Types.U8)
let u8_ty = Types.Int Types.U8
let string_or_ptr t =
match t with
| Types.Ptr (_, t) -> is_string_ty t
| t -> is_string_ty t
(* The String's (Vec u8), through a pointer to one too. *)
let string_vec loc (s : Tast.expr) =
let s =
match s.Tast.ty with
| Types.Ptr (_, t) when is_string_ty t -> mk loc t (Tast.Deref s)
| _ -> s
in
mk loc string_vec_ty (Tast.Field (s, 0))
let box ?ctx loc (e : Tast.expr) : Tast.expr =
let dyn sym args = rt loc Types.Dyn sym args in
let structs =
match ctx with Some c -> c.env.structs | None -> !view_structs
in
match e.Tast.ty with
| Types.Dyn -> e
(* Dyn text is immutable and a String is not, so a String crosses as a copy
of its bytes rather than as the view any other struct would be. *)
| t when is_string_ty t -> dyn "flan_dyn_from_string" [ string_vec loc e ]
(* A dyn int is an i64, so a u64 past the largest i64 has none to become:
read as its bits it would be a different, negative number. It traps
here, at the crossing, as one read through a view does. *)
| Types.Int Types.U64 -> dyn "flan_dyn_from_u64" [ e; here loc ]
| Types.Int _ -> dyn "flan_dyn_from_i64" [ widen loc dyn_i64 e ]
| Types.Float _ -> dyn "flan_dyn_from_f64" [ widen loc dyn_f64 e ]
(* The ABI takes an [int32_t], because a C signature that says [_Bool] is a
width argument nobody wants to have. *)
| Types.Bool -> dyn "flan_dyn_from_bool" [ widen loc (Types.Int Types.I32) e ]
(* A typed char stays a char on the dyn side (decision 127). *)
| Types.Char -> dyn "flan_dyn_from_char" [ widen loc (Types.Int Types.I32) e ]
(* A string is ptr+len and arrives as two arguments, the way every other
(ptr, len) entry point in the runtime takes one. The runtime copies: the
bytes may be a literal or a slice of a buffer the program goes on to
write. *)
| Types.String -> dyn "flan_dyn_from_bytes" [ e ]
(* Unit does not box. A value of the zero-sized type carries nothing for a
dyn word to hold, and [nil] -- the absent dyn value, writable as the
literal [nil] -- is a different thing with a different constructor. The
two get confused if unit is allowed to become one. *)
| Types.Unit ->
Loc.failk "check/dyn-unit" loc
"() does not box into dyn. The absent dyn value is nil — write nil"
| Types.Never -> e
(* A [$t] is seen only in a generic's abstract pass; each copy is checked
again at its concrete type, where the value boxes as that type does, and
this node is thrown away with the rest of the pass. Admitting it is sound
only when every type the variable can be at crosses, or a refusal would
move from the definition to whichever call instantiates it at a pointer,
an enum or a function, with no written requirement to blame — the rule
beside [println]'s deferral. Every [is-numeric] type crosses (a u64 past
the largest i64 traps at run time, not at a copy's check), so that bound
admits it. [is-ordered] and [is-equal] do not: both admit an enum, which
has no dyn value. The node is a cast rather than [flan_dyn_nil] because
[is_nil_lit] would read that as a nil literal. *)
| Types.Var v
when (match ctx with
| Some c -> declares c.env.tvpreds v "is-numeric"
| None -> false) ->
mk loc Types.Dyn (Tast.Prim (Tast.Cast Types.Dyn, [ e ]))
| Types.Var _ ->
let t = tyname loc e.Tast.ty in
Loc.failk "check/dyn-type-variable" loc
"%s may be a type with no dyn value, such as a pointer or an enum, so \
it crosses into dyn only as a number. Write %s at the head of the body"
t (where_text loc "is-numeric" t)
(* A view, not a copy: the box holds a small record naming where the
storage is and what one element is (its descriptor, [view_desc]), and
every read or write goes straight through to the container's own
storage — see runtime/flan_dyn.h's view section. A Vec's view points AT
the Vec's header and reads its pointer and length live, so a push
through the view cannot go stale; a slice and a fixed array cannot grow,
so a snapshot taken at the crossing is sound for both. A struct's view
is a map-like value: (get p :x), (set (get p :x) v), (put p :x v).
A container, array or struct is handed over by address. One that is not
a place already — a call's result — is bound to a slot of its own
first, so the view points at storage that lives as long as the frame
rather than at a temporary the next statement reuses. [frame_root] then
says whether the storage is this frame's, for the runtime's dev check. *)
| Types.Vec _ | Types.Array _ | Types.Named _ | Types.Slice (Types.Mut, _)
when (match e.Tast.ty with
| Types.Named n -> Hashtbl.mem structs n
| _ -> true) ->
let desc_of t =
match view_desc structs t with
| Ok d -> mk loc Types.String (Tast.Str d)
| Error inner -> view_not_yet loc e inner
in
let i32 n = mk loc (Types.Int Types.I32) (Tast.Int (n, Types.I32)) in
let i64 n = mk loc dyn_i64 (Tast.Int (n, Types.I64)) in
let bind, e =
match ctx, e.Tast.ty with
| Some ctx, (Types.Vec _ | Types.Array _ | Types.Named _)
when not (view_place e) ->
let sl = fresh_slot ctx e.Tast.ty in
Some (sl, e), mk loc e.Tast.ty (Tast.Local sl)
| _ -> None, e
in
(match !view_global_init with
| Some (g, kind) when frame_root e ->
let fln = fln_source loc in
let ty = tyname loc e.Tast.ty in
Loc.failk "check/dyn-view-lifetime" loc
"%s is a dyn global, and its initialiser builds a %s that is gone \
once the initialiser returns, so a dyn view of it would outlive \
it. Give %s its type, as in %s"
g ty g
(let form =
match kind with Ast.Every -> "def" | Ast.Once -> "defonce" in
if fln then
Printf.sprintf "%s %s: %s = ..."
(match kind with Ast.Every -> "def" | Ast.Once -> "once") g ty
else Printf.sprintf "(%s %s %s ...)" form g ty)
| _ -> ());
let here = i32 (if frame_root e then 1L else 0L) in
let view =
match e.Tast.ty with
| Types.Slice (_, elem) ->
dyn "flan_dyn_view_slice" [ e; desc_of elem; here ]
| Types.Vec elem ->
dyn "flan_dyn_view_at" [ addr_of loc e; i64 0L; desc_of elem; i32 1L; here ]
| Types.Array (n, elem) ->
dyn "flan_dyn_view_at" [ addr_of loc e; i64 n; desc_of elem; i32 0L; here ]
| t ->
dyn "flan_dyn_view_at" [ addr_of loc e; i64 0L; desc_of t; i32 2L; here ]
in
(match bind with
| None -> view
| Some b -> mk loc Types.Dyn (Tast.Let ([ b ], [ view ])))
(* A dyn view is written through by (set (at d i) x), and nothing on the
dyn side can tell a read-only one apart, so a [[const T]] does not
cross. *)
| Types.Slice (Types.Const, elem) ->
Loc.failk "check/dyn-const-view" loc
"%s does not cross into dyn: a dyn view can be written through, and a \
[const %s] can only be read. A dyn view is taken of the writable \
storage it came from"
(tyname loc e.Tast.ty) (tyname loc elem)
(* No view for a map yet, and the suggestion is the *literal* rather than a
constructor call: there is no [(map-new dyn)] — [map_new_types] wants a
key and a value, and [map_type] refuses dyn as a key — so naming one
would be advice that does not compile. A dyn map is written {:k v}. *)
| Types.Map _ ->
no_dyn_yet loc ~into:true e.Tast.ty
". A dyn map is written as a literal, {:key value ...}"
(* [Option] is on this list in name only: [expect] intercepts it before
[box] ever sees one — [box_option] is the real answer, M2 item 4 — so
this arm only fires for a direct caller that hands [box] an Option
itself, and none does today. Left refused rather than removed, so a
caller that starts doing that gets a sentence instead of a silent
mis-lowering. *)
| Types.Named _ | Types.Enum _ | Types.Option _ | Types.Ptr _
| Types.Alloc | Types.Fn _ | Types.CFn _ | Types.Len _
| Types.LArray _ | Types.Vec _ | Types.Array _ | Types.Slice _ ->
no_dyn_yet loc ~into:true e.Tast.ty ""
(* Every dyn an expectation opened ([expect]'s dyn arm), by the node that
opened it, and the box: what lets a caller that asked for a typed value
see that the value was a dyn, whichever opening the want picked. Keyed by
identity and held weakly, so it is gone with the tree. *)
module Opened = Ephemeron.K1.Make (struct
type t = Tast.expr
let equal = ( == )
let hash = Hashtbl.hash
end)
let opened_by_want : Tast.expr Opened.t = Opened.create 16
(* What an [expect] mismatch found, by the refusal: the type a caller that
skipped a join can still name the way the join would have. *)
module Found = Ephemeron.K1.Make (struct
type t = Loc.diag
let equal = ( == )
let hash = Hashtbl.hash
end)
let mismatch_found : Types.t Found.t = Found.create 16
let unbox loc (want : Types.t) (e : Tast.expr) : Tast.expr =
let need sym ty = rt loc ty sym [ e; here loc ] in
match want with
| Types.Float Types.F64 -> need "flan_dyn_need_f64_at" dyn_f64
| Types.Bool ->
(* The ABI answers an [int32_t]; [bool] is an [i1]. The narrowing is the
language's own cast and cannot fail — the runtime already decided the
value was a bool, so what comes back is 0 or 1. *)
widen loc Types.Bool (need "flan_dyn_need_bool_at" (Types.Int Types.I32))
(* Only a dyn char: an int is a number until (char n) converts it. *)
| Types.Char -> rt loc Types.Char "flan_dyn_need_char" [ e; here loc ]
(* Any integer width, checked at run time at this site (TODO.org, "Dyn
unless annotated"): an int in the width's range, or a char's code point
where it fits — ASCII only into a byte, since a byte past ASCII is not
that char in UTF-8. The runtime answers an i64 it has already checked,
so the narrowing after it cannot lose a bit. *)
| Types.Int k ->
let code =
match k with
| Types.I8 -> 0L | Types.U8 -> 1L | Types.I16 -> 2L | Types.U16 -> 3L
| Types.I32 -> 4L | Types.U32 -> 5L | Types.I64 -> 6L | Types.U64 -> 7L
in
let n =
rt loc dyn_i64 "flan_dyn_need_int"
[ e; mk loc (Types.Int Types.I32) (Tast.Int (code, Types.I32)); here loc ]
in
if k = Types.I64 then n else mk loc want (Tast.Prim (Tast.Cast want, [ n ]))
(* An f32 is refused rather than served by a need_f64 and a rounding: the
box carries one float width, and narrowing is written or it does not
happen (TODO.org, "Implicit numeric widening is legal; narrowing stays a
hard error"). *)
| Types.Float _ ->
no_dyn_yet loc ~into:false want " — take it as f64 and convert"
| _ -> no_dyn_yet loc ~into:false want ""
(* A dyn where a str, a slice, a fixed array or a struct was written:
[flan_dyn_need_as] with the written type's descriptor, answering through
a slot of that type. The runtime decides, since only it can see what the
dyn holds: a text is a str's own bytes, a view of the wanted elements is
their own storage, a vec or a map is a checked copy, and a [T] that can be
written through is never a copy — a write through one would not reach the
dyn vec, so the answer would differ with the vec typed or dyn. Its comment
in runtime/flan_dyn.c says where the copies live and how long a str from a
text lasts.
A fixed array or a struct that holds a Vec is refused: from a view it
would be a second copy of an owning header. *)
let into_typed ctx loc (want : Types.t) (got : Tast.expr) : Tast.expr =
let structs = ctx.env.structs in
let rec owns (t : Types.t) =
match t with
| Types.Vec _ -> true
| Types.Array (_, e) -> owns e
| Types.Named n ->
(match Hashtbl.find_opt structs n with
| Some st -> List.exists (fun (f : Tast.field) -> owns f.Tast.fty) st.Tast.fields
| None -> false)
| _ -> false
in
match view_desc ~into:true structs want with
| Error inner ->
no_dyn_yet loc ~into:false want
(if Types.equal inner want then ""
else Printf.sprintf " — a dyn value has no %s to become" (tyname loc inner))
| Ok _ when owns want ->
no_dyn_yet loc ~into:false want " — it holds a Vec, which owns its storage"
| Ok d ->
let ds = fresh_slot ctx Types.Dyn and out = fresh_slot ctx want in
let local s ty = mk loc ty (Tast.Local s) in
mk loc want
(Tast.Let
([ (ds, got); (out, mk loc want (Tast.Zero want)) ],
[ rt loc Types.Unit "flan_dyn_need_as"
[ local ds Types.Dyn; mk loc Types.String (Tast.Str d);
addr_of loc (local out want); here loc ];
local out want ]))
(* ── A numeric cast written on a dyn ─────────────────────────────────
*
TODO.org, "A numeric cast opens a dyn box".
*
[(f64 d)] where [d] is dyn. Until this, the only place in the language
that opened a box was a typed parameter, which is why a program that
wanted a number out of a dyn had to define a one-line function whose
parameter slot did the unboxing and call *that*. A cast is already the
operator for "convert this to that", so it is the spelling that should
have worked, and now does.
What is built is a branch on the box's tag, not a call that converts:
(let ([s d])
(if (= (flan_dyn_cast_kind s "file:1:2" "u32" 0) 1)
(u32 (flan_dyn_need_f64 s))
(u32 (flan_dyn_need_i64 s))))
Each arm is an ordinary [Cast] over an ordinary [need], so the conversion
is *the same node* a typed operand of that type would have produced.
That is the point of this shape rather than a coercing runtime entry point
that answers the finished number: [(i64 2.5)] is not a bare [fptosi] in
this compiler — [Emit.check_cast] range-checks it first and signals
ArithError when the value will not fit, and the x86 backend does the same
— so a C function returning an [int64_t] would have had to grow its own
second opinion about range and NaN, in a second place, for two backends.
Here there is nothing to keep in step: [(i64 float-box)] *is* [(i64 x)]
with an unbox in front of it, which is exactly what the semantics say.
[need_f64] and [need_i64] are the trapping entry points, and neither can
trap here: each is reached only on the arm where the tag has already been
read as its own. The trap that can happen is the runtime's, for a box
holding a non-number, and [flan_dyn_cast_kind] owns that sentence — bool
included, which is what [flan_dyn_need_i64] already does with a bool at a
typed parameter. The cross-kind case does not trap: it converts and warns
once for the site, the author's call, recorded in TODO.org, "A numeric
cast opens a dyn box".
The slot exists because the value is read three times — once for the tag,
once on whichever arm runs — and the argument may be an arbitrary
expression. [unbox_option] above builds the same shape for the same
reason. *)
let cast_dyn ctx loc (target : Types.t) (got : Tast.expr) : Tast.expr =
let s = fresh_slot ctx Types.Dyn in
let sv = mk loc Types.Dyn (Tast.Local s) in
let want_float = match target with Types.Float _ -> 1 | _ -> 0 in
let kind =
rt loc (Types.Int Types.I32) "flan_dyn_cast_kind"
[ sv; here loc;
mk loc Types.String (Tast.Str (Types.to_string target));
mk loc (Types.Int Types.I32) (Tast.Int (Int64.of_int want_float, Types.I32)) ]
in
let is_float =
mk loc Types.Bool
(Tast.Prim (Tast.Eq,
[ kind;
mk loc (Types.Int Types.I32) (Tast.Int (1L, Types.I32)) ]))
in
let arm sym ty = widen loc target (rt loc ty sym [ sv ]) in
mk loc target
(Tast.Let ([ (s, got) ],
[ mk loc target
(Tast.If (is_float,
arm "flan_dyn_need_f64" dyn_f64,
arm "flan_dyn_int_of" dyn_i64)) ]))
(* nil is written [nil] and nothing else produces it, so this is the whole of
"the checker can see a nil reaching here" — a name, not a dataflow fact.
There is no propagation through a [let] or a call in this checker (see
"Ownership tracking repealed" — flow analysis was removed on purpose), so a
[nil] bound to a name and used later is exactly the case the runtime trap
below exists for. That is the intended split, not a gap: the syntax a
reader can see is refused where they are looking at it, and everything one
step removed from the syntax is caught when the program runs. *)
let is_nil_lit (e : Tast.expr) =
match e.Tast.e with
| Tast.Prim (Tast.Rt "flan_dyn_nil", []) -> true
| _ -> false
(* ── nil <-> None at (Option T) ────────────────────────────────────────────
The dyn absence and the typed one are the same absence at the one boundary
where both are meaningful, M2 item 4. Both directions build the same
[If]-over-a-tag shape [get] and [map-remove] already build (check.ml
4780-4900): the tag says which of [Some]/[None] it is, and the payload,
when there is one, crosses the scalar boundary [box]/[unbox] already own.
(Option (Option T)) does not cross either direction. Boxing [Some] of an
inner [None] would box that [None] as nil — the same nil an outer [None]
becomes — which is exactly the ambiguity [(Some nil)] is refused for one
level down; unboxing has the mirror problem, one dyn absence asked to tell
two levels of it apart. The type stays legal on the typed side (it is
already constructible: nothing here refuses it), only the crossing does
not exist for it.
(Option dyn) needs no case of its own. Its payload is already dyn, so
boxing it is the identity and unboxing it is the identity; the only thing
that has to hold is that the payload is never nil, which is [(Some nil)]'s
refusal below, not this boundary's. *)
let box_option ctx loc (t : Types.t) (got : Tast.expr) : Tast.expr =
match t with
| Types.Option inner ->
Loc.failk "check/option-nested-dyn" loc
"(Option (Option %s)) does not cross into dyn — Some None and None \
would both box as nil"
(tyname loc inner)
| _ ->
(* A literal [Some]/[None] built right here skips the runtime check: the
checker already knows which case it is, so there is nothing to test at
run time and the conversion is free on both backends. *)
match got.Tast.e with
| Tast.None_ -> rt loc Types.Dyn "flan_dyn_nil" []
| Tast.Some_ x -> if Types.equal t Types.Dyn then x else box ~ctx loc x
| _ ->
let s = fresh_slot ctx (Types.Option t) in
let sv = mk loc (Types.Option t) (Tast.Local s) in
let tag = mk loc (Types.Int Types.I8) (Tast.Field (sv, 0)) in
let is_some =
mk loc Types.Bool
(Tast.Prim (Tast.Ne,
[ tag; mk loc (Types.Int Types.I8) (Tast.Int (0L, Types.I8)) ]))
in
let payload = mk loc t (Tast.Field (sv, 1)) in
let some_dyn = if Types.equal t Types.Dyn then payload else box ~ctx loc payload in
let none_dyn = rt loc Types.Dyn "flan_dyn_nil" [] in
mk loc Types.Dyn
(Tast.Let ([ (s, got) ],
[ mk loc Types.Dyn (Tast.If (is_some, some_dyn, none_dyn)) ]))
(* [=] over a dyn pair, answering a bool. Shared by the [=] builtin and a
literal [match] over a dyn, which is (= t lit) by definition. *)
let dyn_eq loc u v =
unbox loc Types.Bool
(rt loc Types.Dyn "flan_dyn_eq_at" [ box loc u; box loc v; here loc ])
let unbox_option ctx loc (t : Types.t) (got : Tast.expr) : Tast.expr =
let oty = Types.Option t in
match t with
| Types.Option inner ->
Loc.failk "check/option-nested-dyn" loc
"(Option (Option %s)) does not cross from dyn — a dyn has one absence, \
nil, which cannot tell None from Some None apart"
(tyname loc inner)
| _ when is_nil_lit got -> mk loc oty Tast.None_
| _ ->
let s = fresh_slot ctx Types.Dyn in
let sv = mk loc Types.Dyn (Tast.Local s) in
let not_nil =
mk loc Types.Bool
(Tast.Prim (Tast.Eq,
[ rt loc (Types.Int Types.I32) "flan_dyn_is_nil" [ sv ];
mk loc (Types.Int Types.I32) (Tast.Int (0L, Types.I32)) ]))
in
let none = mk loc oty Tast.None_ in
let payload = if Types.equal t Types.Dyn then sv else unbox loc t sv in
let some = mk loc oty (Tast.Some_ payload) in
mk loc oty
(Tast.Let ([ (s, got) ], [ mk loc oty (Tast.If (not_nil, some, none)) ]))
(* What a numeric mismatch has left to say, now that widening is silent.
TODO.org, "Implicit numeric widening is legal; narrowing stays a hard
error": every conversion that cannot change
the number happens by itself, so a numeric pair that still reaches a refusal
is one of exactly two things, and this tells them apart.
Either the wanted type is *narrower* — the conversion can lose, which is
what the language has always refused to do without being told, and the
sentence names the cast and points out that the other direction needed
nothing. Or there is no direction at all: i32 and u32 are the same width and
each holds values the other cannot, so neither widens and the program has to
say which half it means to keep.
Written once and used by both refusals that can report one — [expect]'s, and
the binary operators' when their two operands have no join. *)
(* ── The widening thunk ────────────────────────────────────────────────
One per signature, and the whole of what a [CFn] costs on its way into
an [Fn]. Its parameters are the signature's, it declares the environment,
and its body calls through what it finds there — because that is where the
original bare address was put.
Which is the move that makes the two conventions meet in exactly one
place. Every body reachable through an [Fn] value declares the trailing
environment, so every indirect call is exactly typed and nothing anywhere
relies on a callee ignoring an argument it never declared. That was the
first design's hinge and it does not survive wasm32: [call_indirect]
compares the signature at the call and a spare argument is a trap.
Per *signature* and not per name, so a program pays one small function per
distinct shape it widens rather than one per function it widens. Two
widenings of the same shape share a thunk, which is what the memo below is
for — the same arrangement [struct_key_pair] uses for a map's hash and
equality pair, and for the same reason.
**The memo is keyed on the types and the symbol spells them back**, and
both halves matter. [mangle_ty] cannot serve as the spelling: it flattens
a whole signature into one hyphen-joined string, which loses arity and
every type boundary with it, so [(CFn [(Ptr i32)] i32)] and
[(CFn [ptr i32] i32)] — the second over a struct someone called [ptr] —
both come out [cfn-ptr-i32-to-i32]. Keyed on that string, the second
widening silently reuses the first's thunk and calls it with the wrong
arity, which is a miscompile on both backends and not a refusal anywhere.
So the key is the types, compared with [Types.equal], and the name is
[thick_enc]'s encoding, which no two signatures share.
([mangle_ty]'s ambiguity is older than this and is still there for the
generic instantiation names it was written for. At one type's granularity
it is hard to reach; at a whole signature's it is a line of Flan away.)
**Why the name has to be the signature and not a counter.** A counter over
the thunks minted so far is unique within one compilation and says nothing
across two: reorder the definitions in the file and [thick/0] is a
different signature than it was. [Session.compatible] compares a reload's
functions against the running program's *by name*, so a thunk that changed
shape under a fixed name reads to it as a function whose signature was
edited, and the dev loop answers a form reorder with "Restart to change
it". Spelled from the types, the name moves with the shape and that
comparison is right again for the same reason it is right everywhere else.
[fparent] is [<thick>]: not a name anyone wrote, so [defs] hides it, and a
marker the redefinition modules match on to carry a copy of their own. *)
(* A signature written so that it can be read back: every type is
self-delimiting, so no two distinct signatures encode alike.
An atom is its length and then its spelling, which is what closes the gap
[mangle_ty] leaves — a name's boundaries are in the string rather than
inferred from the separators. A constructor is one letter, and the two
that hold a count write it before their children, so [(Fn [i32] i32)] and
[(Fn [] (Fn [i32] i32))] cannot read alike. [Named] and [Enum] carry
different letters because a struct and a C enum may share a spelling. *)
let rec thick_enc (t : Types.t) =
let atom s = Printf.sprintf "%d-%s" (String.length s) s in
let arrow tag ps r =
Printf.sprintf "%s%d-%s" tag (List.length ps)
(String.concat "-" (List.map thick_enc (ps @ [ r ])))
in
match t with
| Types.Named n -> "n" ^ atom n
| Types.Enum n -> "e" ^ atom n
| Types.Var v -> "y" ^ atom v
| Types.Slice (Types.Mut, e) -> "s" ^ thick_enc e
| Types.Slice (Types.Const, e) -> "k" ^ thick_enc e
| Types.Ptr (Types.Mut, e) -> "p" ^ thick_enc e
| Types.Ptr (Types.Const, e) -> "q" ^ thick_enc e
| Types.Vec e -> "v" ^ thick_enc e
| Types.Option e -> "o" ^ thick_enc e
| Types.Array (n, e) -> Printf.sprintf "a%Ld-%s" n (thick_enc e)
| Types.Map (k, v) -> Printf.sprintf "m%s-%s" (thick_enc k) (thick_enc v)
| Types.Fn (ps, r) -> arrow "f" ps r
| Types.CFn (ps, r) -> arrow "c" ps r
| t -> atom (mangle_ty t)
let thick_thunk env loc ps r =
let same (f : Tast.fn) =
f.Tast.fparent = Some "<thick>"
&& List.length f.Tast.params = List.length ps
&& List.for_all2 Types.equal f.Tast.params ps
&& Types.equal f.Tast.ret r
in
match List.find_opt same env.lifted with
| Some f -> f.Tast.name
| None ->
let n = List.length ps in
let fty = Types.CFn (ps, r) in
let args = List.mapi (fun i t -> mk loc t (Tast.Local i)) ps in
let callee = mk loc fty (Tast.Local n) in
let name = "thick/" ^ thick_enc fty in
env.lifted <-
{ Tast.name; params = ps;
slots = Array.of_list (ps @ [ fty ]);
snames = Array.make (n + 1) None; as_slots = [];
ret = r; body = [ mk loc r (Tast.CallPtr (callee, args)) ];
fdefers = []; fenv = Some n; fparent = Some "<thick>"; floc = loc }
:: env.lifted;
name
(* The slot a lifted body's environment arrives in, minted when the body did
not capture anything and so has none of its own.
Every body that can be *reached* through an [Fn] value declares the
parameter, whether or not it reads it: a lifted [fn] literal in an [Fn]
position, and every handler clause, since [flan_signal] passes the frame's
environment to all of them. The alternative is a call whose signature is
one argument longer than the callee's, which SysV tolerates and wasm32
does not. The slot is nameless, so the break loop hides it, and a body
that never reads it costs one store the optimiser drops. *)
let declare_env ctx = function
| Some _ as s -> s
| None -> Some (fresh_slot ctx (Types.Ptr (Types.Mut, Types.Unit)))
let numeric_note ?(fln = false) ~(want : Types.t) ~(got : Types.t) () =
let cast =
if fln then Printf.sprintf "%s(x)" (Types.spell ~indented:fln want)
else Printf.sprintf "(%s x)" (Types.spell ~indented:fln want)
in
if not (Types.is_numeric want && Types.is_numeric got) then ""
else if Types.widens_to ~from:want ~into:got then
Printf.sprintf
" — %s into %s can lose, so it has to be written: %s. The other way \
round, %s widens into %s by itself"
(Types.spell ~indented:fln got) (Types.spell ~indented:fln want) cast
(Types.spell ~indented:fln want) (Types.spell ~indented:fln got)
else
Printf.sprintf
" — neither widens into the other, so the conversion has to be written: \
%s"
cast
(* The rest of the sentence when a read-only slice meets a writable one. *)
let const_note ?(fln = false) env ~(want : Types.t) ~(got : Types.t) =
match want, got with
| Types.Slice (Types.Mut, e), Types.Slice (Types.Const, e')
when Types.equal e e' ->
let copy =
Option.map (fun c -> if fln then "clone(v)" else c) (const_copy env e) in
Printf.sprintf
" — a %s can only be read, and never becomes a %s that can be written \
through. %sWhere nothing writes through it, the %s can be declared %s \
instead"
(Types.spell ~indented:fln got) (Types.spell ~indented:fln want)
(match copy with
| Some c ->
Printf.sprintf "%s copies v into a %s of its own. " c
(Types.spell ~indented:fln want)
| None -> "")
(Types.spell ~indented:fln want) (Types.spell ~indented:fln got)
| Types.Ptr (Types.Mut, e), Types.Ptr (Types.Const, e')
when Types.equal e e' ->
Printf.sprintf
" — a %s can only be read through, and never becomes a %s that can be \
written through. Copy the %s out with %s and point at the copy; \
where nothing writes through it, the %s can be declared %s instead"
(Types.spell ~indented:fln got) (Types.spell ~indented:fln want) (Types.spell ~indented:fln e)
(if fln then "deref(p)" else "(deref p)")
(Types.spell ~indented:fln want) (Types.spell ~indented:fln got)
| _ -> ""
let nil_has_no_none loc w =
fail loc
"nil has no None to become at %s — nil only converts to (Option T) \
or to dyn itself; wrap the type in Option, or keep the value dyn"
(tyname loc w)
(* A literal boxed where a dyn was wanted, and the literal. It brings no dyn
of its own to an operator: (+ nil 1) over a literal 1 checked at dyn is as
typed as (+ nil x) over an i32 x. *)
let boxed_literal (e : Tast.expr) =
match e.Tast.e with
| Tast.Prim (Tast.Rt ("flan_dyn_from_i64" | "flan_dyn_from_f64"
| "flan_dyn_from_char" | "flan_dyn_from_bool"), [ x ]) ->
let x = match x.Tast.e with Tast.Prim (Tast.Cast _, [ y ]) -> y | _ -> x in
(match x.Tast.e with
| Tast.Int (n, _) when x.Tast.ty <> Types.Char ->
(* The type the literal takes where nothing is wanted. *)
let k =
if Int64.compare n (Int64.of_int32 Int32.max_int) > 0
|| Int64.compare n (Int64.of_int32 Int32.min_int) < 0
then Types.I64 else Types.I32
in
Some (Types.Int k)
| Tast.Int _ | Tast.Float _ | Tast.Bool _ -> Some x.Tast.ty
| _ -> None)
| _ -> None
(* The operands of an arithmetic, bitwise or ordering operator gone dyn,
before they are boxed: a literal [nil] among them with nothing else dyn is
refused as [expect] refuses it at a typed want, whichever position it is
in and whether or not the form has a want. A dyn that holds nil is the
run-time trap's, so (+ 1 2 nil) is refused and (+ 1 2 (the dyn nil))
traps. [=] and [!=] ask no such question: nil is unequal to a number. *)
let no_bare_nil (ops : Tast.expr list) =
match List.find_opt is_nil_lit ops with
| None -> ()
| Some nil ->
let makes_dyn (e : Tast.expr) =
e.Tast.ty = Types.Dyn && not (is_nil_lit e) && boxed_literal e = None
in
if not (List.exists makes_dyn ops) then
match
List.find_map
(fun (e : Tast.expr) ->
if e.Tast.ty <> Types.Dyn then Some e.Tast.ty else boxed_literal e)
ops
with
| Some t -> nil_has_no_none nil.Tast.loc t
| None -> ()
let rec expect ctx loc ~want (got : Tast.expr) =
match want with
| None -> got
| Some w ->
(* The boundary: where a wanted type meets a produced one, and the one
place the language's implicit conversions live. It runs before [fits]
rather than instead of it: what comes back is an ordinary expression of
the wanted type, and if the coercion did not produce one the usual
message is still the one that reports it. *)
let got =
match w, got.Tast.ty with
| Types.Dyn, Types.Dyn -> got
| Types.Dyn, Types.Option t -> box_option ctx loc t got
| Types.Dyn, _ -> box ~ctx loc got
| Types.Option t, Types.Dyn when not (is_nil_lit got) ->
let opened = unbox_option ctx loc t got in
Opened.replace opened_by_want opened got;
opened
| Types.Option t, Types.Dyn -> unbox_option ctx loc t got
(* A bare T has no None to become, and this nil is one the checker can
actually see — the literal, written right where the mismatch is.
Refused here, at the offending line, instead of waiting for the
runtime trap [unbox] would otherwise reach for two arms down. *)
| w, Types.Dyn when is_nil_lit got -> nil_has_no_none loc w
| _, Types.Dyn when Types.fits ~expected:w ~actual:Types.Dyn -> got
| (Types.String | Types.Slice _ | Types.Array _), Types.Dyn ->
let opened = into_typed ctx loc w got in
Opened.replace opened_by_want opened got;
opened
| Types.Named n, Types.Dyn when Hashtbl.mem ctx.env.structs n ->
let opened = into_typed ctx loc w got in
Opened.replace opened_by_want opened got;
opened
| _, Types.Dyn ->
let opened = unbox loc w got in
Opened.replace opened_by_want opened got;
opened
(* Implicit widening, and this single arm is the whole of its surface.
[expect] is called by every site that annotates and by nothing else,
so an argument, a return, a let or defonce with a type, a struct field
initialiser, a push into a Vec and a C import's parameter all get it
here at once and none of them had to learn about it.
The conversion is performed, not waved through: [widen] emits the same
[Cast] node the written (i64 x) emits, so the backends sext or zext by
the *source* type's signedness and nothing downstream sees a node
whose type disagrees with its bits. [widens_to] is what keeps that
honest — it admits only conversions that cannot change the number, so
the cast this inserts is one no program can tell happened. *)
| _ when Types.widens_to ~from:got.Tast.ty ~into:w -> widen loc w got
(* The other widening, and the only coercion between the two function
types. A bare address satisfies a signature that asks for an
environment. It goes this way only — an [Fn] has an environment and a
[CFn] has nowhere to put one — so the reverse falls through to the
ordinary refusal, which names both types and is the right sentence. *)
| Types.Fn (ps, r), Types.CFn (ps', r')
when Types.fn_accepts ~from:(ps', r') ~into:(ps, r) ->
mk loc w (Tast.Thicken (thick_thunk ctx.env loc ps r, got))
(* The same signature up to const, which [Types.fn_accepts] defines. *)
| Types.Fn (ps, r), Types.Fn (ps', r')
| Types.CFn (ps, r), Types.CFn (ps', r')
when Types.fn_accepts ~from:(ps', r') ~into:(ps, r) ->
{ got with Tast.ty = w }
(* A writable view seen as a read-only one. The two are the same two
words, so the value is only retyped; the reverse is refused below,
with [const_note] naming the copy that would make it writable. *)
| (Types.Slice (Types.Const, _) | Types.Ptr (Types.Const, _)), _
when Types.const_widens ~from:got.Tast.ty ~into:w ->
{ got with Tast.ty = w }
(* Decision 138, Swift's rule: a T where a (Option T) is wanted is
[Some] of it. One level each time — a T? into a T?? is [Some] of the
Option, never the Option itself — and the payload goes through this
same boundary first, so a T into a T?? is [Some (Some t)] and an i32
into an (Option i64) is widened, then wrapped. Never the other way:
a T? where a T is wanted is still refused, and a dyn is left to the
nil <-> None arms above. When the payload is refused too, the
refusal below names the Option, as it did before. *)
| Types.Option t, g
when (match g with Types.Dyn | Types.Never -> false | _ -> true)
&& not (Types.fits ~expected:w ~actual:g) ->
(match expect ctx loc ~want:(Some t) got with
| v when Types.fits ~expected:t ~actual:v.Tast.ty -> mk loc w (Tast.Some_ v)
| _ -> got
| exception Loc.Error _ -> got)
| _ -> got
in
if Types.fits ~expected:w ~actual:got.Tast.ty then got
else
(* Kinded so that the one caller who knows more — a call argument, which
can name the function and the parameter — can recognise this exact
refusal at this exact span and say the rest. Every other reader of a
diagnostic ignores [kind].
[numeric_note] is the rest of the sentence when both sides are
numbers, and it is on this message rather than beside it because a
reader who has just been told i64 and i32 are different types needs
to be told, in the same breath, which direction needed nothing. *)
(try
Loc.failk "check/type-mismatch" loc "expected %s, found %s%s%s"
(tyname loc w) (tyname loc got.Tast.ty)
(numeric_note ~fln:(Source.indented_at loc) ~want:w ~got:got.Tast.ty ())
(const_note ~fln:(Source.indented_at loc) ctx.env ~want:w ~got:got.Tast.ty)
with Loc.Error d ->
Found.replace mismatch_found d got.Tast.ty;
raise (Loc.Error d))
(* Something a [break] may not jump out of, named so the refusal can say which.
See [lentry]: it is a barrier and not a blanket refusal, so a loop written
wholly inside one keeps its own perfectly good local break. Outside the
recursive group below because its callers hand it bodies of two shapes — one
expression and a list of them — and inside it would be monomorphic. *)
let barrier ctx what f =
let loops = ctx.loops in
ctx.loops <- Lbarrier what :: loops;
let r = f () in
ctx.loops <- loops;
r
(* ── Expressions ───────────────────────────────────────────────────── *)
(* ── (Map K V): the key's hash and equality pair ────────────────────────
spec-memory.md restricts the first implementation to built-in structural
key types — integers, enums, strings, fixed arrays, and value structs
composed recursively from those — and makes equality and hashing for them
compiler-provided structural operations rather than type classes. So there
is no dispatch to design: every key type resolves, here, to a pair of
symbols, and the pair is passed to the type-erased runtime the way Odin
hangs its two contextless procs off a Map_Info.
Most key types need no emitted function at all. A key whose equality is
bytewise and whose bytes are all present is served by one runtime pair over
(pointer, size), which is what [bytewise_key] identifies. Two kinds are not:
- a [string] is ptr+len and its bytes are elsewhere, so two equal strings at
different addresses must still hash the same;
- a struct may have padding, whose bytes are indeterminate, so two structs
that are equal field by field can differ bytewise — and it may hold a
string, which brings the first problem inside it.
A struct therefore gets a pair emitted for it, walking its fields, and that
is the only case that does. *)
let rec bytewise_key = function
| Types.Int _ | Types.Enum _ | Types.Bool | Types.Char -> true
| Types.Array (_, t) -> bytewise_key t
| _ -> false
let hash_ty = Types.Int Types.U64
(* The fresh context a frame starts from: no outer scope, no defers, no loops,
and [defer_ok] false. As written it is a function the checker invented —
nothing is reachable from it because none of it is a body anyone wrote — and
that is how the emitted hashers and the constant-inference pass take it.
A body someone *did* write starts here too and then reattaches the few
fields that make it theirs: [owner] is the function's name, and an fn or a
handler sets [outer] to the enclosing scope so that a reference to the
enclosing function's locals is refused for the reason it is really refused
for. Those are [with] clauses on this record rather than a literal of their
own, so that a field added here is added to all of them.
Each caller calls it again rather than sharing one value: [slots] and
[slot_tys] are counted up per frame, and two frames that shared a context
would share a slot counter. *)
(* The return type a body is checked against while it is being read for
one ([_] in a defn's return slot; see [infer_returns]). Compared by
address, so no type written anywhere is ever mistaken for it. What each
[return] in that body gives is pushed on [infer_seen]. *)
let infer_ret = Types.Named "_"
(* The type two arms meet at — an [if]'s two, or two exits of a [_] body —
the same whichever comes first: the wider where one widens into the other
without loss ([Types.join]), the read-only where they differ only in const,
and dyn where either is dyn, the other boxed. [None] is a refusal. *)
let arm_join (a : Types.t) (b : Types.t) =
if Types.equal a Types.Never then Some b
else if Types.equal b Types.Never then Some a
else
match Types.join a b with
| Some j -> Some j
| None ->
match Types.const_join a b with
| Some j -> Some j
| None ->
(* A T beside a T? meets at the T?, the T wrapped in [Some]
(decision 138). Only the plain side moves, and by one level. *)
let wraps p u =
(match u with Types.Option _ | Types.Unit -> false | _ -> true)
&& (Types.equal p u || Types.widens_to ~from:u ~into:p)
in
(match a, b with
| Types.Dyn, _ | _, Types.Dyn -> Some Types.Dyn
| Types.Option p, u when wraps p u -> Some a
| u, Types.Option p when wraps p u -> Some b
| _ -> None)
(* The type two untyped literals meet at: the wider of their own types, and
an integer beside a float at the float — [(if c 1 2.5)] is an f32, though
i32 does not widen into f32, because the 1 was never an i32 to lose. Only
for literals: a typed i32 beside a float literal still has to be converted. *)
let literal_meet (a : Types.t) (b : Types.t) =
match Types.join a b, a, b with
| Some j, _, _ -> Some j
| None, Types.Int _, Types.Float _ -> Some b
| None, Types.Float _, Types.Int _ -> Some a
| None, _, _ -> None
let infer_seen : (Types.t * Loc.t * bool) list ref = ref []
(* What a refused subexpression stands as while recovering. [Zero] of [Never]
is a value nothing else builds, so it is recognisable; see [check]. *)
let poison loc = { Tast.e = Tast.Zero Types.Never; ty = Types.Never; loc }
(* A poison, or a read of a local one was bound to. *)
let is_poison (r : Tast.expr) =
Types.equal r.Tast.ty Types.Never
&& (match r.Tast.e with Tast.Zero Types.Never | Tast.Local _ -> true | _ -> false)
let record_recovered env (d : Loc.diag) =
let same (x : Loc.diag) = x.Loc.dloc = d.Loc.dloc && String.equal x.Loc.dmsg d.Loc.dmsg in
if not (List.exists same env.recovered) then env.recovered <- d :: env.recovered
(* [f] with recovery off, for a check whose refusal is an answer: a trial, a
probe, a fallback that re-checks. *)
let speculate env f =
env.speculating <- env.speculating + 1;
Fun.protect ~finally:(fun () -> env.speculating <- env.speculating - 1) f
(* A refusal a caller has re-worded: recorded and stood in for while
recovering, raised otherwise. The [check] it re-words was [guarded], so its
own refusal came here rather than being recorded in its first wording. *)
let refuse_or_poison env loc (d : Loc.diag) =
if env.recovering && env.speculating = 0 then begin
record_recovered env d;
env.poison <- env.poison + 1;
poison loc
end
else raise (Loc.Error d)
(* [f], a declaration's body, with recovery on when [on]. Everything it
recorded is raised as [Loc.Errors] at the end, together with whatever
refusal ended it, so nothing checked with a poison in it is ever returned. *)
let with_recovery env ~on f =
if not on then f ()
else begin
let saved = (env.recovering, env.recovered, env.poison) in
let restore () =
let r, d, p = saved in
env.recovering <- r; env.recovered <- d; env.poison <- p
in
env.recovering <- true; env.recovered <- []; env.poison <- 0;
match f () with
| x ->
let found = List.rev env.recovered in
restore ();
if found = [] then x else raise (Loc.Errors found)
| exception Loc.Error d ->
let found = List.rev env.recovered in
restore ();
(* Raised past the end of the body after something in it already
failed: a return that does not fit, a value that is missing, both of
them what the failure left behind. *)
if found = [] then raise (Loc.Error d) else raise (Loc.Errors found)
| exception e -> restore (); raise e
end
let invented_ctx env ret =
{ env; ret; lits = None; slots = 0; slot_tys = []; slot_names = []; as_slots = []; scope = [];
defers = []; defer_slot = None; outer = []; outer_what = None; caught = []; place_ok = false; envslot = None; parent = None; in_frames = None; loops = []; tail = false; used = false; kept = [];
in_defer = false; defer_ok = false; defer_block = "a nested form";
owner = "<none>" }
(* Whether a struct has exactly Error's two fields, which is what a parent must
have: a handler for a parent is handed a view of that shape. *)
let error_shaped env n =
match Hashtbl.find_opt env.structs n with
| Some s ->
(match s.Tast.fields with
| [ { Tast.fname = "name"; fty = Types.String };
{ Tast.fname = "message"; fty = Types.String } ] -> true
| _ -> false)
| None -> false
(* What a signal site tells the runtime about its condition. A condition that
something could catch through a parent, and that is not Error-shaped itself,
gets a printer lifted out of the signalling function: the runtime calls it
only when a parent's handler is about to run, or nothing handled it, so a
signal nobody catches that way costs nothing. It prints what [println]
prints, fields and values, and that is the message such a handler reads. *)
let condition_desc ctx loc name =
let chain = condition_chain ctx.env name in
let self = error_shaped ctx.env name in
let render =
if self || List.length chain < 2 then None
else begin
let ty = Types.Named name in
let hctx = { (invented_ctx ctx.env Types.Unit) with owner = ctx.owner } in
let pslot = fresh_slot hctx (Types.Ptr (Types.Mut, ty)) in
let bslice = Types.Slice (Types.Mut, Types.Int Types.U8) in
let emit x = mk loc Types.Unit (Tast.Prim (Tast.Rt "flan_msg_emit", [ x ])) in
let emitter : Render.emitter =
{ Render.ebytes = emit;
estr = (fun x -> emit (mk loc bslice (Tast.Prim (Tast.EscapeBytes, [ x ]))));
ei64 = (fun x -> emit (to_bytes hctx loc Tast.I64ToBytes x));
eu64 = (fun x -> emit (to_bytes hctx loc Tast.U64ToBytes x));
ef64 = (fun x -> emit (to_bytes hctx loc Tast.F64ToBytes x));
edyn = (fun x -> mk loc Types.Unit (Tast.Prim (Tast.Rt "flan_dyn_emit_msg", [ x ])));
enested = (fun x -> mk loc Types.Unit (Tast.Prim (Tast.Rt "flan_dyn_emit_msg", [ x ]))) }
in
let value =
mk loc ty (Tast.Deref (mk loc (Types.Ptr (Types.Mut, ty)) (Tast.Local pslot)))
in
let body = Render.render (render_ctx hctx emitter) 0 value in
let mine =
List.filter
(fun (l : Tast.fn) ->
l.Tast.fparent = Some ctx.owner
&& String.length l.Tast.name >= 8
&& String.sub l.Tast.name 0 8 = "message/")
ctx.env.lifted
in
let fname =
Printf.sprintf "message/%s/%d/%s" ctx.owner (List.length mine) name
in
ctx.env.lifted <-
{ Tast.name = fname; params = [ Types.Ptr (Types.Mut, ty) ];
slots = Array.of_list (List.rev hctx.slot_tys);
snames = Array.of_list (List.rev hctx.slot_names); as_slots = hctx.as_slots;
ret = Types.Unit; body; fdefers = [];
fenv = None; fparent = Some ctx.owner; floc = loc }
:: ctx.env.lifted;
Some fname
end
in
{ Tast.cname = name; cchain = List.map type_id chain; cself = self;
crender = render }
(* The address of field [i] of the struct the pointer in slot [p] points at. *)
let field_addr_of loc sty fty p i =
let target = mk loc sty (Tast.Deref (mk loc (Types.Ptr (Types.Mut, sty)) (Tast.Local p))) in
mk loc (Types.Ptr (Types.Mut, fty)) (Tast.Addr (Tast.Pfield (target, i)))
(* The pointer form is what a Map_Info holds; the direct form is what an
emitted hasher calls. See flan_rt.c on why they are two symbols. *)
let direct = function
| "flan_hash_flat" -> "flan_key_hash_flat"
| "flan_eq_flat" -> "flan_key_eq_flat"
| "flan_hash_str" -> "flan_key_hash_str"
| "flan_eq_str" -> "flan_key_eq_str"
| s -> s
(* The pair for [k]: (hash, equality), each a symbol to be taken the address
of. Emits a function for a struct key the first time it sees one, and finds
it in [env.lifted] every time after — the name is derived from the type, so
two maps with the same key type share one pair. *)
let rec key_pair env loc (k : Types.t) : Tast.fnref * Tast.fnref =
match k with
(* A hash and an equality for a type variable would have to be *chosen*,
and nothing here can choose: the pair is emitted as concrete symbols and
the concrete type does not exist until the instantiation. Refused rather
than assumed — falling through to [bytewise_key] would hash whatever
bytes the variable turned out to have, which is the wrong answer for a
[string] and for any struct with padding.
**This arm is a backstop and nothing normal reaches it.** Two things get
there first. Inside an instantiation [env.subst] has already made [k]
concrete, so there is no variable left. Outside one — in the abstract
pass over a generic body — the map operations are *deferred*
([deferred_key] below): a key that is a variable declared [is-hashable]
never asks for a pair here, and a variable that is not declared it never
gets as far as a [(Map $t V)] to operate on, because [map_type] refuses
the type where it is written. What is left for this arm is a key that is
a variable by some route neither of those covers, and the honest answer
to that is still a refusal rather than a guessed pair. *)
| Types.Var v ->
Loc.failk "check/generic-map-key" loc
"a map keyed by the type variable $%s has no hash and no equality here. \
Write {:where (is-hashable $%s)} at the head of the body, or write the \
operation in a function over the concrete key type and call that" v v
| Types.String -> Tast.Rtfn "flan_hash_str", Tast.Rtfn "flan_eq_str"
| t when bytewise_key t ->
Tast.Rtfn "flan_hash_flat", Tast.Rtfn "flan_eq_flat"
| t when is_string_ty t ->
fail loc
"a String owns its bytes, and a map would share them with it rather \
than copy them, so a String is not a map key. Key the map by str and \
put %s, which is the String's text for as long as the String is not \
changed"
(if fln_source loc then "str(s)" else "(str s)")
| Types.Named n when Hashtbl.mem env.structs n -> struct_key_pair env loc n
(* A data type key would have to hash the tag and then only the bytes the
case in hand actually uses — the rest of the payload is indeterminate,
exactly as a struct's padding is, so hashing the blob would make two equal values
hash differently. That is a per-case walk driven by a switch, which is a
different shape from the field list [struct_key_pair] emits and which
nothing has yet wanted. Refused by name rather than written untested. *)
| Types.Named n when Hashtbl.mem env.datas n ->
fail loc
"%s is a data type, and a data type is not a map key — key on the tag, \
or on a struct holding what you meant" n
(* And an untagged union is refused for the half of that reason which has
nothing to do with a tag: a member smaller than the union leaves the rest
of the storage indeterminate, so two values that agree about every byte
anybody wrote hash differently. There is no per-member walk to write here
either — nothing records which member was written, which is the type. *)
| Types.Named n when Hashtbl.mem env.unions n ->
fail loc
"%s is a union, and a union is not a map key — key on the member you \
meant" n
(* A bytewise array took the arm above; this is one whose elements need
their own pair, a string's or a struct's. *)
| Types.Array (n, e) -> array_key_pair env loc n e
| Types.Float _ ->
(* Not a milestone question, which is why it is said separately: NaN is not
equal to itself, and 0.0 and -0.0 are equal while differing bytewise. A
float key therefore has no equality for a hash map to use, whatever the
implementation does. *)
fail loc
"a float is not a map key — NaN is not equal to itself. Key on an \
integer instead"
| other ->
fail loc
"%s is not a map key. A key is an integer, an enum, a bool, a string, a \
fixed array of those, or a struct of those"
(tyname loc other)
and struct_key_pair env loc n =
let hname = "map/hash/" ^ n and ename = "map/eq/" ^ n in
let known name =
List.exists (fun (f : Tast.fn) -> f.Tast.name = name) env.lifted
in
if known hname then Tast.Flanfn hname, Tast.Flanfn ename
else begin
let sty = Types.Named n in
let fields = (Hashtbl.find env.structs n).Tast.fields in
if fields = [] then
fail loc
"%s has no fields, so it is not a map key — every value of it would \
be the same key" n;
let hparams = [ Types.Ptr (Types.Mut, sty); hash_ty; Types.Int Types.I64 ] in
let eparams = [ Types.Ptr (Types.Mut, sty); Types.Ptr (Types.Mut, sty); Types.Int Types.I64 ] in
(* Registered before the fields are walked, so a struct reached twice
through two different fields emits one pair and not two. A struct cannot
contain itself by value, so there is no cycle to break — only sharing.
The body is filled in below; nothing can call these in between. *)
let placeholder name ret params =
{ Tast.name; params; slots = Array.of_list params;
snames = Array.make (List.length params) None; as_slots = [];
ret; body = []; fdefers = []; fenv = None; fparent = None; floc = loc }
in
env.lifted <-
placeholder hname hash_ty hparams
:: placeholder ename (Types.Int Types.I8) eparams
:: env.lifted;
(* The hash: seed, then one combine per field, in declaration order. Each
field is hashed by its own pair — the same recursion, so a string field
hashes its bytes and a nested struct hashes field by field. Padding is
never reached, because nothing here addresses anything but a field. *)
let hctx = invented_ctx env hash_ty in
let kp = fresh_slot ~name:"key" hctx (Types.Ptr (Types.Mut, sty)) in
let seed = fresh_slot ~name:"seed" hctx hash_ty in
ignore (fresh_slot ~name:"size" hctx (Types.Int Types.I64));
let acc = fresh_slot ~name:"h" hctx hash_ty in
let steps =
List.mapi
(fun i (fl : Tast.field) ->
let fty = fl.Tast.fty in
let h, _ = key_pair env loc fty in
let args =
[ field_addr_of loc sty fty kp i;
mk loc hash_ty (Tast.Local seed); size_of loc fty ]
in
let one =
match h with
| Tast.Rtfn s -> rt loc hash_ty (direct s) args
| Tast.Flanfn s | Tast.Fnval s ->
mk loc hash_ty (Tast.Call (s, args))
in
mk loc Types.Unit
(Tast.Set (Tast.Plocal acc,
rt loc hash_ty "flan_hash_combine"
[ mk loc hash_ty (Tast.Local acc); one ])))
fields
in
let hbody =
(mk loc Types.Unit
(Tast.Set (Tast.Plocal acc, mk loc hash_ty (Tast.Local seed))))
:: steps
@ [ mk loc hash_ty (Tast.Local acc) ]
in
(* The equality: one early return per field, then true. Written as returns
rather than as a conjunction so that the comparison stops at the first
field that differs, which for a struct with a string field is the
difference between one memcmp and two. *)
let ectx = invented_ctx env (Types.Int Types.I8) in
let ap = fresh_slot ~name:"a" ectx (Types.Ptr (Types.Mut, sty)) in
let bp = fresh_slot ~name:"b" ectx (Types.Ptr (Types.Mut, sty)) in
ignore (fresh_slot ~name:"size" ectx (Types.Int Types.I64));
let i8 v = mk loc (Types.Int Types.I8) (Tast.Int (v, Types.I8)) in
let checks =
List.mapi
(fun i (fl : Tast.field) ->
let fty = fl.Tast.fty in
let _, eq = key_pair env loc fty in
let args =
[ field_addr_of loc sty fty ap i;
field_addr_of loc sty fty bp i; size_of loc fty ]
in
let call =
match eq with
| Tast.Rtfn s -> rt loc (Types.Int Types.I8) (direct s) args
| Tast.Flanfn s | Tast.Fnval s ->
mk loc (Types.Int Types.I8) (Tast.Call (s, args))
in
let differs =
mk loc Types.Bool (Tast.Prim (Tast.Eq, [ call; i8 0L ]))
in
mk loc Types.Unit
(Tast.If (differs,
mk loc Types.Never (Tast.Return (Some (i8 0L))),
unit_at loc)))
fields
in
let ebody = checks @ [ i8 1L ] in
let finish name ret params ctx body =
{ Tast.name; params;
slots = Array.of_list (List.rev ctx.slot_tys);
snames = Array.of_list (List.rev ctx.slot_names); as_slots = ctx.as_slots;
ret; body; fdefers = []; fenv = None; fparent = None; floc = loc }
in
env.lifted <-
finish hname hash_ty hparams hctx hbody
:: finish ename (Types.Int Types.I8) eparams ectx ebody
:: List.filter
(fun (f : Tast.fn) ->
f.Tast.name <> hname && f.Tast.name <> ename)
env.lifted;
Tast.Flanfn hname, Tast.Flanfn ename
end
(* The pair for a fixed array whose elements are not bytewise: the struct
pair's shape, with the field list replaced by a loop over the elements, so
[[64 string]] is one call site in a loop and not sixty-four. Each element is
hashed and compared by its own pair, so an array of structs holding strings
is served by the same recursion. *)
and array_key_pair env loc n e =
if Int64.compare n 0L <= 0 then
fail loc
"%s has no elements, so it is not a map key — every value of it would be \
the same key" (tyname loc (Types.Array (n, e)));
let aty = Types.Array (n, e) in
(* The type's printed form, with what a symbol cannot hold replaced. *)
let tag =
String.map
(fun c -> match c with
| 'a' .. 'z' | 'A' .. 'Z' | '0' .. '9' | '_' | '-' -> c
| _ -> '_')
(tyname loc aty)
in
(* The mangle is many-to-one — [a+b] and [a_b] come out alike — so a digest
of the printed type, which is an identity, keeps two such keys apart. *)
let tag =
tag ^ "/" ^ String.sub (Digest.to_hex (Digest.string (Types.to_string aty))) 0 12
in
let hname = "map/hash/array/" ^ tag and ename = "map/eq/array/" ^ tag in
let known name =
List.exists (fun (f : Tast.fn) -> f.Tast.name = name) env.lifted
in
if known hname then Tast.Flanfn hname, Tast.Flanfn ename
else begin
let pty = Types.Ptr (Types.Mut, aty) in
let hparams = [ pty; hash_ty; Types.Int Types.I64 ] in
let eparams = [ pty; pty; Types.Int Types.I64 ] in
let placeholder name ret params =
{ Tast.name; params; slots = Array.of_list params;
snames = Array.make (List.length params) None; as_slots = [];
ret; body = []; fdefers = []; fenv = None; fparent = None; floc = loc }
in
env.lifted <-
placeholder hname hash_ty hparams
:: placeholder ename (Types.Int Types.I8) eparams
:: env.lifted;
let h, eq = key_pair env loc e in
let call ret f args =
match f with
| Tast.Rtfn s -> rt loc ret (direct s) args
| Tast.Flanfn s | Tast.Fnval s -> mk loc ret (Tast.Call (s, args))
in
let elem_addr p i =
let target = mk loc aty (Tast.Deref (mk loc pty (Tast.Local p))) in
mk loc (Types.Ptr (Types.Mut, e))
(Tast.Addr (Tast.Pindex (target, [ mk loc index_ty (Tast.Local i) ])))
in
(* The counter and its loop, which carries no break and no continue — the
condition tast.ml puts on a [While] the checker invents. *)
let loop ctx body =
let i = fresh_slot ~name:"i" ctx index_ty in
let iv = mk loc index_ty (Tast.Local i) in
let limit = mk loc index_ty (Tast.Int (n, Types.I32)) in
let one = mk loc index_ty (Tast.Int (1L, Types.I32)) in
let cond = mk loc Types.Bool (Tast.Prim (Tast.Lt, [ iv; limit ])) in
let step =
mk loc Types.Unit
(Tast.Set (Tast.Plocal i,
mk loc index_ty (Tast.Prim (Tast.Add, [ iv; one ]))))
in
mk loc Types.Unit
(Tast.Let ([ (i, mk loc index_ty (Tast.Int (0L, Types.I32))) ],
[ mk loc Types.Unit (Tast.While (cond, [ body i ], [ step ])) ]))
in
let hctx = invented_ctx env hash_ty in
let kp = fresh_slot ~name:"key" hctx pty in
let seed = fresh_slot ~name:"seed" hctx hash_ty in
ignore (fresh_slot ~name:"size" hctx (Types.Int Types.I64));
let acc = fresh_slot ~name:"h" hctx hash_ty in
let hbody =
[ mk loc Types.Unit
(Tast.Set (Tast.Plocal acc, mk loc hash_ty (Tast.Local seed)));
loop hctx (fun i ->
let one =
call hash_ty h
[ elem_addr kp i; mk loc hash_ty (Tast.Local seed);
size_of loc e ]
in
mk loc Types.Unit
(Tast.Set (Tast.Plocal acc,
rt loc hash_ty "flan_hash_combine"
[ mk loc hash_ty (Tast.Local acc); one ])));
mk loc hash_ty (Tast.Local acc) ]
in
let ectx = invented_ctx env (Types.Int Types.I8) in
let ap = fresh_slot ~name:"a" ectx pty in
let bp = fresh_slot ~name:"b" ectx pty in
ignore (fresh_slot ~name:"size" ectx (Types.Int Types.I64));
let i8 v = mk loc (Types.Int Types.I8) (Tast.Int (v, Types.I8)) in
let ebody =
[ loop ectx (fun i ->
let same =
call (Types.Int Types.I8) eq
[ elem_addr ap i; elem_addr bp i; size_of loc e ]
in
mk loc Types.Unit
(Tast.If (mk loc Types.Bool (Tast.Prim (Tast.Eq, [ same; i8 0L ])),
mk loc Types.Never (Tast.Return (Some (i8 0L))),
unit_at loc)));
i8 1L ]
in
let finish name ret params ctx body =
{ Tast.name; params;
slots = Array.of_list (List.rev ctx.slot_tys);
snames = Array.of_list (List.rev ctx.slot_names); as_slots = ctx.as_slots;
ret; body; fdefers = []; fenv = None; fparent = None; floc = loc }
in
env.lifted <-
finish hname hash_ty hparams hctx hbody
:: finish ename (Types.Int Types.I8) eparams ectx ebody
:: List.filter
(fun (f : Tast.fn) ->
f.Tast.name <> hname && f.Tast.name <> ename)
env.lifted;
Tast.Flanfn hname, Tast.Flanfn ename
end
(* The pair as two expressions, ready to be passed. Their Flan type is
[(Ptr ())]: one opaque word, which is all the backend needs. *)
let key_fns env loc k =
let h, e = key_pair env loc k in
mk loc raw_alloc (Tast.FnAddr h), mk loc raw_alloc (Tast.FnAddr e)
(* ── The map operations, deferred to the instantiation ─────────────────
True when the key is a type variable, which means the operation cannot be
built here and must be answered by the copy: [key_fns] emits concrete
symbols and there is no concrete key type yet. The caller checks its
arguments first and then returns a placeholder of the operation's own type,
exactly as [print] does — see the allow-list comment at the [print] arm for
what being on that list costs and why these are on it.
The predicate is *required* before deferring, and that is the whole safety
argument: with {:where (is-hashable $t)} in the signature, the instantiation
refuses at the call site against a requirement the author wrote down. A
variable with no such clause is refused here and now, at the definition,
which is where the abstract pass wants every refusal that has nothing to
point at. In practice [map_type] has already refused such a signature where
the type was written; this repeats it rather than relying on that, the same
way [key_pair] repeats [map_type]'s key check. *)
let deferred_key env loc what (k : Types.t) =
match k with
| Types.Var v ->
if not (declares env.tvpreds v "is-hashable") then
Loc.failk "check/generic-map-key" loc
"%s over a map keyed by the type variable %s needs %s to be hashable. \
Write {:where (is-hashable $%s)} at the head of the body"
what (tyname loc k) (tyname loc k) v;
true
| _ -> false
(* The body shared by [get] and [map-remove]: both answer an (Option V), both
ask the runtime one question over (key address, out address, sizes, hash,
equality), and the only thing that differs between them is which symbol is
called. [sym] is therefore the whole of the difference, and the entry points
above keep their own preambles — the dyn case and the deferred-key case are
not the same on the two sides.
The key is checked and the out slot zeroed by the caller's [k] and by
[Tast.Zero] here; the Option is built here rather than in the runtime,
because the runtime answers 1/0 and fills [out] only when it answers 1. It
has no idea what an Option's layout is, and keeping it that way is what lets
one entry point serve every value type. *)
let map_lookup ctx ~want loc sym target kt vt k =
let hash, eq = key_fns ctx.env loc kt in
let ks = fresh_slot ctx kt in
let out = fresh_slot ctx vt in
let found =
rt loc (Types.Int Types.I8) sym
[ target; addr_of loc (mk loc kt (Tast.Local ks));
addr_of loc (mk loc vt (Tast.Local out));
size_of loc kt; size_of loc vt; hash; eq; here loc ]
in
let oty = Types.Option vt in
let some = mk loc oty (Tast.Some_ (mk loc vt (Tast.Local out))) in
let none = mk loc oty Tast.None_ in
let cond =
mk loc Types.Bool
(Tast.Prim (Tast.Ne,
[ found;
mk loc (Types.Int Types.I8) (Tast.Int (0L, Types.I8)) ]))
in
expect ctx loc ~want
(mk loc oty
(Tast.Let ([ (ks, k);
(out, mk loc vt (Tast.Zero vt)) ],
[ mk loc oty (Tast.If (cond, some, none)) ])))
(* The braced-pairs scan a struct literal, a union value and a data case all
do: each key is refused if it has already been given, with a note at the
first mention, and the table of what was given comes back for the fill that
follows. [noun] is the word the message uses — a union's are members, the
other two have fields — and [~known] is the caller's own unknown-key
refusal, run after the duplicate check on the same pair so that a key given
twice is reported as the duplicate it is rather than as whatever the second
mention is. A caller that wants its unknown-key pass run over all the pairs
first, before any of this, passes no [~known] and keeps its own loop. *)
let given_once ~noun ?(known = fun _ _ -> ()) kvs =
let seen = Hashtbl.create 8 in
List.iter
(fun (k, (v : Ast.expr)) ->
(match Hashtbl.find_opt seen k with
| Some (first : Ast.expr) ->
Loc.failk "check/duplicate-field" v.Ast.loc
~notes:[ Loc.note first.Ast.loc (k ^ " is given here first") ]
"%s %s is given twice" noun k
| None -> ());
known k v;
Hashtbl.add seen k v)
kvs;
seen
(* ── Counting a library's resources ─────────────────────────────────
[Shim.resources] says which bindings acquire, release or change a resource
in place. The generated wrapper of each one carries [%res-acquire],
[%res-release] and [%res-done] forms over its own locals — names the reader
cannot produce, so no program can write them — and the call site opens the
note with where it is. The runtime pairs the two by the binding's name.
A resource is known by one field, not by its whole value: a program sets
[looping] on a Music or the [transform] of a Model and still unloads the
same resource. The field is the first pointer in the struct, depth first —
an Image's pixels, a Sound's audio buffer, a Mesh's vertices, a Model's
meshes — and, in a struct that holds no pointer, a field named [id], which
is how a Texture2D or a RenderTexture2D names its GPU object. A struct with
neither is keyed by every leaf field, hashed separately so padding never
reaches the key.
Every note is a [flan_dev_reg_note_] call. A release build drops that
family before building its arguments, and none of the notes binds a slot,
so what a release build emits is exactly what it emitted before the notes
existed. *)
let res_field env (t : Types.t) : int list option =
let fields n =
match Hashtbl.find_opt env.structs n with
| Some s -> s.Tast.fields
| None -> []
in
let rec first_ptr t =
match t with
| Types.Named n when Hashtbl.mem env.structs n ->
List.find_map
(fun (i, (f : Tast.field)) ->
match f.Tast.fty with
| Types.Ptr _ -> Some [ i ]
| ft -> Option.map (fun p -> i :: p) (first_ptr ft))
(List.mapi (fun i f -> (i, f)) (fields n))
| _ -> None
in
match first_ptr t with
| Some p -> Some p
| None ->
(match t with
| Types.Named n ->
List.find_map
(fun (i, (f : Tast.field)) ->
if String.equal f.Tast.fname "id" then Some [ i ] else None)
(List.mapi (fun i f -> (i, f)) (fields n))
| _ -> None)
let rec res_hash loc env (e : Tast.expr) : Tast.expr =
let seed = mk loc hash_ty (Tast.Int (0L, Types.U64)) in
match e.Tast.ty with
| Types.Named n when Hashtbl.mem env.structs n ->
let fields = (Hashtbl.find env.structs n).Tast.fields in
snd
(List.fold_left
(fun (i, acc) (fl : Tast.field) ->
let leaf = mk loc fl.Tast.fty (Tast.Field (e, i)) in
(i + 1,
rt loc hash_ty "flan_hash_combine" [ acc; res_hash loc env leaf ]))
(0, seed) fields)
| t -> rt loc hash_ty "flan_key_hash_flat" [ addr_of loc e; seed; size_of loc t ]
let res_key loc env (e : Tast.expr) : Tast.expr =
match res_field env e.Tast.ty with
| None -> res_hash loc env e
| Some path ->
let leaf =
List.fold_left
(fun (x : Tast.expr) i ->
match x.Tast.ty with
| Types.Named n ->
let f = List.nth (Hashtbl.find env.structs n).Tast.fields i in
mk loc f.Tast.fty (Tast.Field (x, i))
| _ -> x)
e path
in
res_hash loc env leaf
(* An argument that can be evaluated a second time and mean the same thing:
a name, or the address of one or of a field of one. The in-place re-key
reads its argument before and after the call, and anything with an effect
in it is not re-keyed. *)
let rec res_pure (e : Tast.expr) =
match e.Tast.e with
| Tast.Local _ | Tast.Global _ -> true
| Tast.Field (x, _) -> res_pure x
| Tast.Addr p | Tast.Prim (Tast.AddrOf, [ { Tast.e = Tast.Addr p; _ } ]) ->
res_place p
| Tast.Prim (Tast.AddrOf, [ x ]) -> res_pure x
| _ -> false
and res_place (p : Tast.place) =
match p with
| Tast.Plocal _ | Tast.Pglobal _ -> true
| Tast.Pfield (t, _) -> res_pure t
| _ -> false
let tracked_call loc env name (tr : Shim.track) ret (args : Tast.expr list) =
let str x = mk loc Types.String (Tast.Str x) in
let call = mk loc ret (Tast.Call (name, args)) in
let opened =
if tr.Shim.acquire || tr.Shim.releases <> [] then
[ rt loc Types.Unit "flan_dev_reg_note_res_site" [ str name; here loc ] ]
else []
in
let rekeyed =
if ret <> Types.Unit then []
else
List.filter_map
(fun i ->
match List.nth_opt args i with
| Some ({ Tast.ty = Types.Ptr (_, t); _ } as p) when res_pure p ->
Some (mk loc t (Tast.Deref p), t)
| _ -> None)
tr.Shim.rekey
in
(* The old keys go to the runtime before the call and the new ones after,
in the reverse order, so it can pair them on a stack. *)
let before =
List.map
(fun (v, _) ->
rt loc Types.Unit "flan_dev_reg_note_res_rekey_from" [ res_key loc env v ])
rekeyed
in
let after =
List.rev_map
(fun (v, t) ->
rt loc Types.Unit "flan_dev_reg_note_res_rekey_to"
[ res_key loc env v; str (Types.to_string t) ])
rekeyed
in
match opened @ before, after with
| [], [] -> call
| pre, post -> mk loc ret (Tast.Do (pre @ [ call ] @ post))
(* Every expression goes through here, and [check_value] is the one that
knows the forms. What this adds is [refuse_owned_copy], asked of whatever
came back unless the form was checked as the target of a place. *)
(* The conditions [check_truthy] has refused, each with the body it was
checked in and its diagnostic, for as long as the outermost call is on the
stack — see [check_truthy]. Physical identity on both, since a generic's
body is the same syntax checked again at another type. *)
let truthy_failed :
(Ast.expr * (string * binding) list * Types.t * Loc.diag) list ref = ref []
(* Whether two scopes bind the same names at the same types, which is what a
refusal under them can depend on — the slots are fresh on every pass. A
memo keyed on less would replay a refusal after a retry changed a type. *)
let same_scope (a : (string * binding) list) (b : (string * binding) list) =
a == b
|| List.equal
(fun (n, (x : binding)) (m, (y : binding)) ->
String.equal n m && Types.equal x.bty y.bty)
a b
let truthy_depth = ref 0
(* The same for an [if], keyed on its condition and the expectation:
an [if] whose else arm is tried on its own terms first (see [check_if])
would otherwise be re-checked, refused, by the trial of every [if] above
it — the square of a refused or/and chain's length. *)
let if_failed :
(Loc.t,
Ast.expr * ((string * binding) list * Types.t) * (Types.t option * bool)
* Loc.diag)
Hashtbl.t =
Hashtbl.create 16
let if_depth = ref 0
(* An arm refused at the other arm's type, keyed the same way: the arm is
then checked on its own terms, and an [if] above it that checks it again
— its own trial, then for real — finds the refusal here rather than
walking the arm to it once more, which in a chain nested in else arms
would be twice per level. Cleared for each program. *)
let arm_failed :
(Loc.t,
Ast.expr * ((string * binding) list * Types.t) * Types.t * Loc.diag)
Hashtbl.t =
Hashtbl.create 16
(* A dyn value opened at a typed want: the box, and the want it was opened
at. Two arms that meet this way meet at dyn — the typed one is boxed, not
the dyn one opened — whichever is written first. *)
let not_kept = "check/arm-not-kept"
let to_dyn ctx (x : Tast.expr) = expect ctx x.Tast.loc ~want:(Some Types.Dyn) x
let opened_dyn ~(box : Tast.expr -> Tast.expr) (v : Tast.expr) =
(* An opening is a value position of its own, whatever shape the
conversion built. *)
let stop x = Opened.mem opened_by_want x in
let any = ref false in
Tast.iter_tails ~stop (fun x -> if stop x then any := true) v;
if not !any then None
else
(* Every value position meets at dyn: an opened one is put back to the
box it opened, and any other is boxed. *)
Some
(Tast.map_tails ~stop ~ty:Types.Dyn
(fun x ->
match Opened.find_opt opened_by_want x with
| Some b -> b
| None -> box x)
v)
(* A Vec or a Map parameter is a copy of the caller's header — Odin's rule —
so growing it reallocates a block only this function's copy points at, and
the caller's container never sees the elements. The warnings found so far,
one per parameter, printed by [build_program]; the parameters themselves
are [grow_params], above [view_refusal], which reads them too. *)
let grow_warnings : Loc.diag list ref = ref []
let note_grown ctx op loc (target : Tast.expr) =
(* The parameter the container is reached from, through struct fields
taken by value — a field of a parameter is in the parameter's copy too —
and the path written back out. A [Deref] ends the walk: through a
pointer the caller's own storage is what grows. *)
let rec root (e : Tast.expr) =
match e.Tast.e with
| Tast.Local s -> Some (s, fun p -> p)
| Tast.Field (inner, i) ->
(match inner.Tast.ty with
| Types.Named n ->
(match Hashtbl.find_opt ctx.env.structs n with
| Some st when i < List.length st.Tast.fields ->
let f = (List.nth st.Tast.fields i).Tast.fname in
Option.map
(fun (s, path) -> (s, fun p -> Printf.sprintf "(.%s %s)" f (path p)))
(root inner)
| _ -> None)
| _ -> None)
| _ -> None
in
match target.Tast.ty, root target, !grow_params with
| ((Types.Vec _ | Types.Map _) as t), Some (s, path), (c, ps) :: _ when c == ctx ->
(match List.assoc_opt s ps with
| Some (p : Ast.field)
when not
(List.exists
(fun (d : Loc.diag) -> d.Loc.dloc = p.Ast.floc)
!grow_warnings) ->
let ts = tyname loc t in
let msg =
match target.Tast.e with
| Tast.Local _ ->
Printf.sprintf
"%s is a %s passed by value, a copy of the caller's header, so \
the %s at %s grows this function's copy and the caller's \
container never sees it. Take it as (Ptr %s) and write (%s \
(deref %s) ...), and each caller passes (addr c) for its \
container c"
p.Ast.fname ts op (Loc.to_string loc) ts op p.Ast.fname
| _ ->
let pt = tyname loc (List.nth ctx.slot_tys (ctx.slots - 1 - s)) in
Printf.sprintf
"%s is a %s passed by value, a copy of the caller's, so the %s \
at %s grows %s in this function's copy and the caller's never \
sees it. Take it as (Ptr %s), where %s reaches the caller's own, \
and each caller passes (addr c) for its %s c"
p.Ast.fname pt op (Loc.to_string loc) (path p.Ast.fname) pt
(path p.Ast.fname) pt
in
grow_warnings :=
Loc.diag ~kind:"check/grown-parameter" p.Ast.floc msg :: !grow_warnings
| _ -> ())
| _ -> ()
(* Recovery, when [env.recovering] is on: a subexpression that is refused is
recorded and stands as a [poison] of type [Never], which fits any want, so
checking carries on around it and every error in a body is reported. What
an earlier failure causes is not reported: an error raised by a node one of
whose subexpressions failed, or with [Never] wanted, is dropped, as long as
something has been recorded. That last condition keeps a poison from ever
reaching a backend unreported — a scope with a poison in it always ends in
a raise (see [with_recovery]). *)
let rec check ctx ?want (e : Ast.expr) : Tast.expr =
let env = ctx.env in
let guarded = env.guard_next in
env.guard_next <- false;
if (not env.recovering) || env.speculating > 0 || guarded then
check_plain ctx ?want e
else begin
let seen = env.poison in
let caused () =
(env.recovered <> [] || Hashtbl.length env.infer_failed > 0)
&& (env.poison > seen || want = Some Types.Never)
in
match check_plain ctx ?want e with
| r ->
if is_poison r then env.poison <- env.poison + 1;
r
| exception Loc.Error d ->
if not (caused ()) then begin
record_recovered env d;
(* A call refused as a whole — the wrong number of arguments, say —
never checked its arguments, and a mistake inside one is still a
mistake. They are checked on their own, with no expectation, so
only what no expectation could change is kept: a name that is not
there. *)
match e.Ast.e with
(* A type written in a value position — [map-new([const u8], i32)] —
is not a value to look names up in. *)
| Ast.Call ({ Ast.e = Ast.Var ("map-new" | "builtin/map-new"); _ }, args) ->
recheck_args ctx (List.filteri (fun i _ -> i >= 2) args)
| Ast.Call ({ Ast.e = Ast.Var ("vec-new" | "builtin/vec-new"); _ }, args) ->
recheck_args ctx (List.filteri (fun i _ -> i >= 1) args)
| Ast.Call (_, args) -> recheck_args ctx args
| _ -> ()
end;
env.poison <- env.poison + 1;
poison e.Ast.loc
(* A checker arm that was never written for a [Never] operand may fail
some other way over one. Only then, and only as a consequence. *)
| exception (Not_found | Invalid_argument _ | Failure _ | Assert_failure _
| Match_failure _) when caused () ->
env.poison <- env.poison + 1;
poison e.Ast.loc
end
and recheck_args ctx (args : Ast.expr list) =
let env = ctx.env in
let before = env.recovered in
List.iter (fun a -> ignore (check ctx a)) args;
let rec fresh l = if l == before then [] else match l with [] -> [] | d :: r -> d :: fresh r in
let kept =
List.filter
(fun (d : Loc.diag) ->
String.starts_with ~prefix:"check/unknown-" d.Loc.kind
|| String.equal d.Loc.kind "check/private")
(fresh env.recovered)
in
env.recovered <- kept @ before
and check_plain ctx ?want (e : Ast.expr) : Tast.expr =
let place = ctx.place_ok in
ctx.place_ok <- false;
let r = check_value ctx ?want e in
if not place then refuse_owned_copy ctx r;
r
(* A form checked as the target of a place: indexed, sliced, a field read,
measured, its address taken, or handed to a builtin that works on the
container where it stands. *)
and check_target ctx (e : Ast.expr) =
ctx.place_ok <- true;
check ctx e
(* Decision 81 (2026-09-25). A value that owns storage — a Vec, a Map, or an
array, Option or struct holding one — reached through a [[const T]] or a
(Ptr const T) is not copied out as a value. Its header shares its block
with the original, so a copy that could be grown, freed or handed on as
writable would be the original written through. It is used where it
stands instead: indexed, sliced (to a [[const T]]), its fields read when
they own nothing, or its address taken as a (Ptr const T). Refusing at the
source is the whole rule; there is no tracking of where a copy went. *)
and refuse_owned_copy ctx (r : Tast.expr) =
match const_reached r with
| Some view when owning ctx.env r.Tast.ty ->
let fln = Source.indented_at r.Tast.loc in
let t = tyname r.Tast.loc r.Tast.ty in
let fix =
match r.Tast.ty with
| (Types.Vec _ | Types.Map _) when not (region_only ctx.env r.Tast.ty) ->
Printf.sprintf "%s copies it into a %s of its own"
(if fln then "clone(v)" else "(clone v)") t
(* Nothing copies an array, an Option or a struct that owns storage,
nor a container whose elements do: its address is the way to it. *)
| _ ->
Printf.sprintf "%s gives a %s to read it through"
(if fln then "addr(v)" else "(addr v)")
(tyname r.Tast.loc (Types.Ptr (Types.Const, r.Tast.ty)))
in
Loc.failk "check/const-owned-copy" r.Tast.loc
"this copies a %s out of a %s, which can only be read, and the copy \
would share its storage with the original. Use it where it stands — \
index it, slice it or read its fields — or %s"
t (tyname r.Tast.loc view) fix
| _ -> ()
and check_value ctx ?want (e : Ast.expr) : Tast.expr =
let loc = e.Ast.loc in
(* Read the permission this form was given and withdraw it in the same
breath, so that nothing reached from here inherits it. The two callers
that may grant it — [check_fn]'s body walk and [check_let]'s, below —
grant it again before the *next* form rather than once around the body. *)
let defer_ok = ctx.defer_ok in
ctx.defer_ok <- false;
(* The same read-and-withdraw, for the same reason: a [recur] is in tail
position only if *this* form was, and nothing reached from here inherits
it unless the arm below hands it on deliberately. *)
let tail = ctx.tail in
ctx.tail <- false;
let used = ctx.used || List.memq e ctx.kept in
ctx.used <- false;
let skipping = !skip_wrap in
skip_wrap := false;
match e.Ast.e with
(* A literal where an (Option T) is wanted is built at T and then wrapped
(decision 138): [s = -1] over an [i64?] is [Some] of an i64 -1. It has
no type until one is asked of it, so it is asked the payload's, rather
than being built at a default and wrapped at the wrong width. *)
| Ast.Int _ | Ast.UInt _ | Ast.Float _ | Ast.Byte _ | Ast.Call _ | Ast.Arr (_ :: _)
when (match want with Some (Types.Option _) -> not skipping | _ -> false)
&& (lone_literal e || (match e.Ast.e with Ast.Arr _ -> true | _ -> false)) ->
let w = Option.get want in
let t = match w with Types.Option t -> t | _ -> assert false in
(match trial ctx (fun () -> check ctx ~want:t e) with
| Ok v when Types.fits ~expected:t ~actual:v.Tast.ty -> mk loc w (Tast.Some_ v)
| Ok v -> expect ctx loc ~want v
(* Refused at T: checked again at the Option as it was before 138, so
the refusal names what was wanted, [str?], and not only its payload. *)
| Error _ ->
skip_wrap := true;
check ctx ?want e)
(* A negative literal in a generic body, at an instantiation that made it
unsigned. The cast the ordinary refusal names would be wrong at every
other type the function is called at, so the fix is one that needs no
negative number at all, and the refusal says which call asked. *)
| Ast.Int n
when Int64.compare n 0L < 0 && ctx.env.chain <> []
&& (match want with
| Some (Types.Int k) -> not (Types.signed k)
| _ -> false) ->
let t = Option.get want in
(* [instantiate] adds the note naming the call that asked for this copy. *)
let gname, _, _ = List.nth ctx.env.chain (List.length ctx.env.chain - 1) in
let var =
match List.find_opt (fun (_, u) -> Types.equal u t) ctx.env.subst with
| Some (v, _) -> Printf.sprintf "$%s = %s" v (tyname loc t)
| None -> tyname loc t
in
Loc.failk literal_at_want loc
"%Ld does not fit in %s, which holds no negative number, and %s is called \
at %s — the body has to work at every type it is called at, so write \
it with no negative literal, as in (- x %Ld) in place of (+ x %Ld)"
n (tyname loc t) gname var (Int64.neg n) n
| Ast.Int n -> int_literal loc ~want ~preds:ctx.env.tvpreds n
| Ast.UInt (n, s) -> wide_literal loc ~want n s
(* A char literal that ends up dyn is a dyn char, never an int. *)
| Ast.Byte b when want = Some Types.Dyn ->
rt loc Types.Dyn "flan_dyn_from_char"
[ mk loc (Types.Int Types.I32) (Tast.Int (Int64.of_int b, Types.I32)) ]
(* A char literal is a code point, and a byte type holds one only when it
is ASCII: \é as a u8 would be 0xE9, which is not é in UTF-8 and never
equals a byte of it. Refused by the char's name, not its number. *)
| Ast.Byte b
when (match want with
| Some (Types.Int (Types.U8 | Types.I8)) -> b > 127
| Some (Types.Int Types.I16) -> b > 32767
| Some (Types.Int Types.U16) -> b > 65535
| _ -> false) ->
let t = tyname loc (Option.get want) in
let c = Form.byte_repr b in
if (match want with
| Some (Types.Int (Types.U8 | Types.I8)) -> true
| _ -> false)
then
Loc.failk literal_at_want loc
"%s is %d bytes in UTF-8, not one, so it is not a %s. Write the str \
\"%s\" for its bytes, or take its code point as an i32"
c (String.length (Form.utf8 b)) t (Form.utf8 b)
else
Loc.failk literal_at_want loc
"%s is code point %d, which does not fit in a %s. Take its code point \
as an i32" c b t
(* Where typed code wants a number the literal is that number, as an integer
literal would be; everywhere else it is a char (decision 127). *)
| Ast.Byte b
when (match want with
| Some (Types.Int _ | Types.Float _) -> true
| Some (Types.Var v) -> declares ctx.env.tvpreds v "is-numeric"
| _ -> false) ->
int_literal loc ~want ~preds:ctx.env.tvpreds (Int64.of_int b)
| Ast.Byte b ->
expect ctx loc ~want (mk loc Types.Char (Tast.Int (Int64.of_int b, Types.U32)))
(* The float literal's own dyn case, for the reason the integer's has one:
the ABI carries one width and the literal is built at it. f64 is already
what an unconstrained float literal defaults to, so this only has to stop
the "expected dyn, found the float literal" arm below from firing. *)
| Ast.Float x when want = Some Types.Dyn ->
box loc (mk loc dyn_f64 (Tast.Float (x, Types.F64)))
| Ast.Float x ->
let k =
match want with
| Some (Types.Float k) -> k
(* A float literal at a type variable, refused even under [is-numeric] —
the asymmetry with the integer literal above is deliberate and is the
same asymmetry the concrete arms already have. An untyped integer
constant is usable wherever a float is wanted; a float literal is
never usable where an integer is wanted (Odin's rule, stated at the
[Int] case). So [is-numeric] admits integers, and a body written with a
float literal has no meaning at the integer half of its own bound.
Refusing here keeps that a refusal at the definition rather than one
that surprises whichever call site first instantiates at [i32]. *)
| Some (Types.Var v) ->
(* Under {:where (is-integer $t)} the sentence is simpler and its own:
the bound has no float half at all, so the literal has no meaning
at *any* type the variable can become, not merely at some. *)
if declares ctx.env.tvpreds v "is-integer" then
Loc.failk literal_at_want loc
"the float literal %g cannot stand where $%s is wanted — \
{:where (is-integer $%s)} admits no float type. Write an integer \
literal, or take the value as a parameter"
x v v
else
Loc.failk literal_at_want loc
"the float literal %g cannot stand where $%s is wanted — %s may \
be instantiated at an integer type. Write an integer literal, \
which is admitted at every numeric type, or take the value as a \
parameter"
x v
(if declares ctx.env.tvpreds v "is-numeric" then
Printf.sprintf "{:where (is-numeric $%s)} admits integers too, so $%s" v v
else Printf.sprintf "$%s" v)
| Some other when other <> Types.Never ->
Loc.failk literal_at_want loc "expected %s, found the float literal %g"
(tyname loc other) x
| _ -> float_default ()
in
(* Where f32 is wanted, a literal past its range would be infinity or 0,
silently. *)
(if k = Types.F32 && Float.is_finite x && x <> 0.0 then
let f = Int32.float_of_bits (Int32.bits_of_float x) in
if Float.is_integer f && f = 0.0 then
Loc.failk literal_at_want loc
"%g is too small for f32, which rounds it to 0 — the smallest \
f32 above 0 is about 1.4e-45" x
else if not (Float.is_finite f) then
Loc.failk literal_at_want loc
"%g does not fit in f32, whose largest value is about 3.4e38" x);
mk loc (Types.Float k) (Tast.Float (x, k))
| Ast.Str s when want = None && lit_has "dyn" && not !typed_literals -> box loc (mk loc Types.String (Tast.Str s))
| Ast.Str s -> expect ctx loc ~want (mk loc Types.String (Tast.Str s))
| Ast.Kw k ->
(* Two keywords in one spelling, told apart by the expectation. Where an
enum type is expected, :space resolves at compile time against its
members and a typo is an error here rather than a wrong number at run
time (plan.org, settled: keywords at typed call sites) — no runtime
value exists at all. Everywhere else :foo is a first-class dyn value,
interned by the runtime so two spellings of one name are one word and
equality is an identity compare. The enum reading keeps priority
because it existed first and costs nothing; nothing is lost, since a
site that wants the dyn keyword against an enum expectation has none. *)
(match want with
| Some (Types.Enum name) ->
let members = Hashtbl.find ctx.env.enums name in
(match List.assoc_opt k members with
| Some v -> mk loc (Types.Enum name) (Tast.Int (v, Types.I32))
| None ->
(* The near miss first, because the raylib enums carry a
disambiguating member prefix — [:r] is four edits from [:key-r]
and one thought, and the list alone makes the reader do the
thought. The list still follows: the suggestion can be wrong. *)
(match member_near_miss members k with
| Some (m, _) ->
fail loc "%s has no member :%s — did you mean :%s? It has %s"
name k m
(String.concat " "
(List.map (fun (m, _) -> ":" ^ m) members))
| None ->
fail loc "%s has no member :%s — it has %s" name k
(String.concat " "
(List.map (fun (m, _) -> ":" ^ m) members))))
| Some Types.Dyn | None ->
expect ctx loc ~want
(rt loc Types.Dyn "flan_dyn_kw" [ mk loc Types.String (Tast.Str k) ])
| Some other ->
fail loc
":%s is an enum member where an enum is expected and a dyn keyword \
elsewhere, but %s is expected here" k
(tyname loc other))
(* {:a 1 :b s} — a dyn map, built where it stands. Always dyn: the
runtime owns the storage the way (vec-new dyn) does, keys and values are
both dyn words, and a typed want other than dyn refuses through [expect]
like any other dyn value would. The literal lowers to a fresh slot — a
rooted one, because a slot of type dyn is what [Emit.root_plan] counts — so
the map stays reachable across the allocations its own entries make. *)
| Ast.MapLit (tag, kvs) ->
let m = fresh_slot ctx Types.Dyn in
let mval = mk loc Types.Dyn (Tast.Local m) in
(* A class's constructor stores through [flan_dyn_slot_init], which is
the plain store plus the slot's type check, worded for the
constructor rather than for a [put] nobody wrote. *)
let sets =
List.map
(fun ((k : Ast.expr), v) ->
let args =
[ mval; check ctx ~want:Types.Dyn k; check ctx ~want:Types.Dyn v ]
in
(* The key's location is the slot's, in the defclass: the
constructor has no other place of its own to name. *)
if tag = None then rt loc Types.Unit "flan_dyn_map_set" args
else rt loc Types.Unit "flan_dyn_slot_init" (args @ [ here k.Ast.loc ]))
kvs
in
(* A shape tag, if this is the literal a class's constructor was written
from. It is the class's name interned as a keyword, and it goes into
the object's header rather than into the entries — so everything below
this line, the rooting included, is the untagged case unchanged. *)
let empty =
match tag with
| None -> rt loc Types.Dyn "flan_dyn_map_new" []
| Some cls ->
(* The class's slots and their types ride along, so the first
instance built registers the class with the runtime and every
store after it — this literal's own included — is checked. A
class registered already, by an earlier instance or by a reload,
keeps what it has: redefining one is a reload's business. *)
let spec =
match Hashtbl.find_opt ctx.env.classes cls with
| Some slots -> class_spec_of slots
| None -> ""
in
rt loc Types.Dyn "flan_dyn_map_new_class"
[ rt loc Types.Dyn "flan_dyn_kw" [ mk loc Types.String (Tast.Str cls) ];
mk loc Types.String (Tast.Str spec) ]
in
expect ctx loc ~want
(mk loc Types.Dyn (Tast.Let ([ (m, empty) ], sets @ [ mval ])))
| Ast.Quote _ ->
unimplemented loc "a quoted symbol (restart names)" 6
(* A length variable read as a value is the integer it was bound to, as a
literal — so it takes its width from where it stands, the way a written
8 would. In the abstract pass it is a 1: a literal that fits every
integer type, since the real one is answered again per copy. A local of
the same name shadows it. *)
| Ast.Var name
when (List.mem name ctx.env.lenvars
|| (match List.assoc_opt name ctx.env.subst with
| Some (Types.Len _) -> true
| _ -> false))
&& lookup ctx name = None ->
let n =
match List.assoc_opt name ctx.env.subst with
| Some (Types.Len n) -> n
| _ -> 1L
in
check ctx ?want { e with Ast.e = Ast.Int n }
| Ast.Var name -> var ctx loc ~want name
| Ast.Do body -> ctx.tail <- tail; ctx.used <- used; block ctx ?want loc body
(* [defer_ok] rides through: a [let] at the top level of a function body has
exactly the function's extent, and so does a [let] nested inside one.
[tail] rides through for the same shape of reason: a [recur] written as
the last form of a [let] inside a loop body is in the loop's tail. *)
| Ast.Let (bs, body) -> check_let ctx ~tail ~used ?want ~defer_ok loc bs body
| Ast.If (c, t, e') -> check_if ctx ~tail ~used ?want loc c t e'
| Ast.While (label, c, body) ->
(* The condition is part of the loop even though it is written outside the
braces — emit puts it in the header block, so it is re-evaluated at the
top of every trip — but it stays outside [in_loop], because a [break]
in a condition still means the enclosing loop and a [defer] there is
still the outer block's. *)
let names = narrows c in
let c = check_truthy ctx c in
let body = in_loop ctx ?label (fun () ->
scoped ctx (fun () ->
with_narrowed ctx names (fun () -> map_lr (fun b -> check ctx b) body)))
in
(* No latch: a [while] has nothing to run between the body and the test, so
a [continue] can branch straight at the condition. *)
expect ctx loc ~want (mk loc Types.Unit (Tast.While (c, body, [])))
(* [Never], as [exit] and [return] are: nothing after one of these runs, and
an [if] arm that ends in a break does not have to agree with the other. *)
(* (loop [x 0 acc 1] body ...) — a loop that answers with the value of its
body, and the only place a [recur] may stand. Not an IR node: it is a
[let] over the names, a [While] whose condition is [true], and a jump.
See [check_loop]. *)
| Ast.Loop (bs, body) -> check_loop ctx ?want loc bs body
| Ast.Recur args -> check_recur ctx ~tail loc args
| Ast.Break label ->
mk loc Types.Never (Tast.Break (loop_target ctx loc "break" label))
| Ast.Continue label ->
mk loc Types.Never (Tast.Continue (loop_target ctx loc "continue" label))
| Ast.Return v when ctx.in_frames <> None ->
ignore v;
(* The frames are pushed and popped around the body, so an early exit would
leave them on the handler or restart stack pointing into a frame that
has gone. Rejected rather than left to corrupt it, the same rule as
defer inside a block. *)
fail loc
"return is not allowed inside %s yet"
(match ctx.in_frames with Some n -> n | None -> assert false)
| Ast.Return v when ctx.ret == infer_ret ->
let lit = match v with Some x -> adapts x | None -> false in
let v = Option.map (check ctx) v in
infer_seen :=
(match v with
| Some (x : Tast.expr) -> (x.Tast.ty, loc, lit)
| None -> (Types.Unit, loc, false))
:: !infer_seen;
(match ctx.defers, v with
| [], _ -> mk loc Types.Never (Tast.Return v)
(* Thrown away after, so the order the defers run in is not built. *)
| ds, _ -> mk loc Types.Never (Tast.Do (ds @ [ mk loc Types.Never (Tast.Return v) ])))
| Ast.Return v ->
let v =
match v with
| None ->
if not (Types.equal ctx.ret Types.Unit) then
fail loc "this function returns %s, so return needs a value"
(tyname loc ctx.ret);
None
| Some v -> Some (check ctx ~want:ctx.ret v)
in
(* The value is computed first, then whatever has been deferred *so far*
runs, then the function returns — the order the fall-off-the-end path
in [check_fn] has, so [(return x)] and a last form [x] agree. A defer
written below this return has not executed yet and must not fire. *)
(match ctx.defers, v with
| [], _ -> mk loc Types.Never (Tast.Return v)
| ds, Some (value : Tast.expr)
when not (Types.equal value.Tast.ty Types.Never
|| Types.equal value.Tast.ty Types.Unit) ->
let s = fresh_slot ctx value.Tast.ty in
let r =
mk loc Types.Never
(Tast.Return (Some (mk loc value.Tast.ty (Tast.Local s))))
in
mk loc Types.Never (Tast.Let ([ (s, value) ], ds @ [ r ]))
(* A unit value has nothing to keep, and is still evaluated first. *)
| ds, Some value when Types.equal value.Tast.ty Types.Unit ->
mk loc Types.Never
(Tast.Do
((value :: ds)
@ [ mk loc Types.Never (Tast.Return (Some (unit_at loc))) ]))
(* A value that never arrives is computed first too, and the defers
after it are unreachable: a trap runs none, and a transfer out of it
runs the function's [fdefers]. *)
| _, Some _ -> mk loc Types.Never (Tast.Return v)
| ds, None ->
mk loc Types.Never
(Tast.Do (ds @ [ mk loc Types.Never (Tast.Return None) ])))
(* (set (at target i) x) against a dyn target — a dyn vec from (vec-new
dyn), or a typed container's own view (M2 item 3) — is a call and not a
place: [flan_dyn_set_at] tag-checks [x]'s dyn tag against what the vec
or the view holds and traps on a mismatch, which is not a memory write
[Tast.Set] could express through a pointer. [target] is checked once,
here, and handed to [vec_at]/[indexed] unchecked in the [else] branch
below rather than re-checked by [check_place] — checking it twice would
evaluate a target with a side effect twice. A dyn target indexed more
than once — nothing in this milestone builds one — still falls to the
ordinary [Ast.Set] arm below, and [indexed] refuses it by name. *)
| Ast.Set (Ast.Pindex (target, [ idx ]), v) ->
let target = check_target ctx target in
if target.Tast.ty = Types.Dyn then
let i = check ctx ~want:Types.Dyn idx in
let v = check ctx ~want:Types.Dyn v in
expect ctx loc ~want
(rt loc Types.Unit "flan_dyn_set_at" [ target; i; v; here loc ])
else begin
let p, pty =
match target.Tast.ty with
| Types.Vec _ ->
let pp, ty = vec_at ctx loc target [ idx ] in
Tast.Pderef pp, ty
| _ ->
(* [~place], for the reason [check_place] passes it: [indexed]
accepts a string, and this arm never reaches [check_place]. *)
let iidx, ty = indexed ~place:loc ctx target [ idx ] in
Tast.Pindex (target, iidx), ty
in
let v = check ctx ~want:pty v in
expect ctx loc ~want (mk loc Types.Unit (Tast.Set (p, v)))
end
(* (set (get inst :slot) x) — a class instance's declared slot. A call and
not a place for [flan_dyn_set_at]'s reason: the runtime has to look at
the value to know it is an instance, which of its slots the key names,
and whether [x] fits the type that slot was declared with, and it traps
on each with a sentence of its own. A typed map has no such place; its
entries are written with [put]. *)
| Ast.Set (Ast.Pslot (target, k), v) ->
let target = check ctx target in
if target.Tast.ty <> Types.Dyn then
fail loc
"(get m k) is a place only on a class instance, and this is %s. A \
map's entries are written with put"
(tyname loc target.Tast.ty);
let k = check ctx ~want:Types.Dyn k in
let v = check ctx ~want:Types.Dyn v in
expect ctx loc ~want
(rt loc Types.Unit "flan_dyn_slot_set" [ target; k; v; here loc ])
(* A name narrowed by [if x?] takes a value of its payload's type, which
keeps it present. An Option would end the narrowing partway through the
block, so it is refused (decision 133): the block reads x as present
throughout. *)
| Ast.Set (Ast.Pvar n, v)
when (match lookup ctx n with Some b -> b.bwhat = Some narrowed_tag | None -> false) ->
let b = Option.get (lookup ctx n) in
(* A parameter or a captured copy is refused as it is outside the block. *)
let place, _ = check_place ctx loc (Ast.Pvar n) in
(match trial ctx (fun () -> check ctx ~want:b.bty v) with
| Ok vv -> expect ctx loc ~want (mk loc Types.Unit (Tast.Set (place, vv)))
| Error d ->
(match trial ctx (fun () -> check ctx ~want:(Types.Option b.bty) v) with
| Ok { Tast.ty = Types.Option _; _ } ->
fail loc
"%s is tested with %s? above, so in this block it is %s, and it \
cannot be given an Option here: the block reads it as present \
throughout. Assign a %s, or test a new name, as in while %s as \
item, and assign %s from that"
n n (tyname loc b.bty) (tyname loc b.bty) n n
| _ -> raise (Loc.Error d)))
| Ast.Set ((Ast.Pvar n as p), v) when lit_recorded ctx n <> None ->
let key = Option.get (lit_recorded ctx n) in
let p, pty = check_place ctx loc p in
let v = lit_down ctx key pty v in
expect ctx loc ~want (mk loc Types.Unit (Tast.Set (p, v)))
(* (set (.name x) v) on a dyn is (put x :name v): a class slot's declared
type is checked by put, and a map takes a key it did not hold. Not
[flan_dyn_slot_set], which refuses a plain map — .name reads either, so
assigning it writes either. The target is checked once, here. *)
| Ast.Set (Ast.Pfield (target, name), v) ->
let t = check_target ctx target in
if t.Tast.ty = Types.Dyn then begin
refuse_const_change ctx loc t;
let k = dyn_kw ctx loc name in
let v = check ctx ~want:Types.Dyn v in
expect ctx loc ~want
(rt loc Types.Unit "flan_dyn_map_put" [ t; k; v; here loc ])
end else begin
let p, pty = field_place ~store:true ctx loc target t name in
let v = check ctx ~want:pty v in
expect ctx loc ~want (mk loc Types.Unit (Tast.Set (p, v)))
end
| Ast.Set (p, v) ->
let p, pty = check_place ctx loc p in
let v = check ctx ~want:pty v in
expect ctx loc ~want (mk loc Types.Unit (Tast.Set (p, v)))
(* On a dyn, (.name x) is (get x :name) — the same call, so a missing key is
nil and a value that is not a map traps with get's own sentence. *)
| Ast.Field (target, name) ->
let t = check_target ctx target in
if t.Tast.ty = Types.Dyn then
expect ctx loc ~want
(rt loc Types.Dyn "flan_dyn_get" [ t; dyn_kw ctx loc name; here loc ])
else
let target, sname = struct_of ctx target t in
let s = Option.get (fields_named ctx.env sname) in
(match Tast.field_index s name with
| None ->
Loc.failk "check/unknown-field" loc ~notes:(declared_note ctx.env sname)
"%s has no field %s" (tyname loc (Types.Named sname)) name
| Some i ->
let fty = (List.nth s.Tast.fields i).Tast.fty in
expect ctx loc ~want (mk loc fty (Tast.Field (target, i))))
| Ast.Struct (name, kvs) -> check_struct ctx ~want loc name kvs
| Ast.Bare kvs -> check_bare ctx ~want loc kvs
(* A bracket literal where a dyn is wanted is the runtime's own vec, built
where it stands — the same lowering the map literal gets, and what makes
{:xs [1 2]} mean what it reads as. Everywhere else brackets stay the
fixed-array literal they always were. *)
| Ast.Arr items when want = Some Types.Dyn ->
dyn_vec ctx loc (map_lr (fun x -> check ctx ~want:Types.Dyn x) items)
| Ast.Arr items when want = None && lit_has "dyn" && not !typed_literals ->
dyn_vec ctx loc (map_lr (fun x -> check ctx ~want:Types.Dyn x) items)
| Ast.Arr items -> check_arr ctx ~want loc items
(* (array 4 rl/Vector2). Parse already assembled the whole array type, so
there is nothing to infer: resolve it and hand back its all-bytes-zero
value, which is what a declared array with no initialiser gets. *)
| Ast.ArrayOf t ->
let ty = resolve ctx.env t in
expect ctx loc ~want (mk loc ty (Tast.Zero ty))
(* Parse writes one only into a type position of a call named vec-new or
map-new, and the builtins read it before it could get here. A program's
own function of that name does not. *)
| Ast.TypeArg _ ->
fail loc "this is a type, and a value is wanted here"
| Ast.ArrayFill (dims, v) -> check_array_fill ctx ~want loc dims v
| Ast.ArrayGen (dims, f) -> check_array_gen ctx ~want loc dims f
| Ast.The (t, v) -> check_the ctx ~want loc t v
| Ast.Match (scrutinee, arms) -> check_match ctx ~tail ~used ?want loc scrutinee arms
| Ast.IfLet (scrutinee, arm, els) ->
check_if_let ctx ~tail ~used ?want loc scrutinee arm els
| Ast.Chain (n, v, body) -> check_chain ctx ~want loc n v body
| Ast.Narrow (names, body) ->
with_narrowed ctx names (fun () ->
ctx.tail <- tail; ctx.used <- used; check ctx ?want body)
| Ast.Alias (pairs, body) ->
scoped ctx (fun () ->
List.iter
(fun (g, h) ->
match lookup ctx h with
| Some b -> ctx.scope <- (g, b) :: ctx.scope
| None -> ())
pairs;
ctx.tail <- tail; ctx.used <- used; check ctx ?want body)
(* Constant integer arithmetic where a type variable is wanted is folded to
the literal it computes first, so [(+ x (+ 1 2))] is admitted wherever
[(+ x 3)] is. The instantiation re-checks the form unfolded, at a concrete
type, where the ordinary arithmetic is fine. *)
| Ast.Call ({ Ast.e = Ast.Var ("+" | "-" | "*" | "/" | "%"); _ }, _)
when (match want with Some (Types.Var _) -> true | _ -> false)
&& literal_arith e <> None ->
int_literal loc ~want ~preds:ctx.env.tvpreds (Option.get (literal_arith e))
| Ast.Call (head, args) ->
let outer = ctx.kept in
(* A with-allocator's arguments after the first are a body, run in
order, and not values. *)
(match head.Ast.e with
| Ast.Var ("with-allocator" | "builtin/with-allocator") -> ctx.kept <- []
| _ -> ctx.kept <- args);
Fun.protect ~finally:(fun () -> ctx.kept <- outer)
(fun () -> check_call ctx ~want loc head args)
| Ast.Unwrap (Ast.Usome, v) ->
(* Unwrap Some, else early-return None from the enclosing function, so the
enclosing function must itself return an Option (plan.org). *)
(match ctx.ret with
| _ when ctx.ret == infer_ret ->
Loc.failk "check/infer-some" loc
"some returns None from the function when there is nothing, and \
this function's return type is read off its body, which cannot \
say what the Option holds. Write the return type: (Option T)"
| Types.Option _ ->
let v = check ctx v in
(match v.Tast.ty with
| Types.Option t ->
expect ctx loc ~want (mk loc t (Tast.UnwrapSome v))
| other ->
fail loc "some takes an (Option T), found %s" (tyname loc other))
| other ->
fail loc
"some early-returns None, so the enclosing function must return an \
Option; this one returns %s" (tyname loc other))
| Ast.Unwrap (Ast.Utry, _) -> unimplemented loc "try (Result)" 6
| Ast.Fn (params, body) -> check_fn ctx ~want loc params body
| Ast.Dotimes (label, name, bounds, body) ->
check_dotimes ctx ~want loc label name bounds body
(* (signal c) : Unit, always — spec-conditions.md §1. A handler that returns
normally leaves the signalling function to carry on, and with nothing
matching this is a no-op, so nothing about it alters control flow. That is
what makes it checkable here rather than needing the transfer machinery
restart-case will want. *)
| Ast.Signal (kind, c) ->
let c = check ctx c in
let name =
match c.Tast.ty with
| Types.Named n -> n
(* Its own arm ahead of the general one. "A condition is a struct, not
dyn" is true and useless here: a dyn holding a condition struct is
one edit away from working, and the edit is naming the struct type.
Since the descriptors landed, the fields inside it may be dyn — which
is the half of the answer the general sentence would have hidden. *)
| Types.Dyn ->
fail c.Tast.loc
"a condition is matched by its type and dyn is not one — write the \
condition's struct type, whose dyn fields are fine"
| t ->
fail c.Tast.loc
"a condition is a struct, not %s" (tyname loc t)
in
(* A condition that *holds* a dyn is not refused. The condition crosses as
a pointer to a value in the signalling frame, and that value is on the
collector's
root stack with its type's descriptor beside it — which is exactly the
shape the transfer needed and could not have. Where the condition is
not a place, the backends evaluate it into a rooted slot rather than a
scratch temporary, so a handler that allocates cannot collect the
payload it was handed. See [Emit.addr_rooted]. *)
(* §1 and §2. [signal] is Unit whatever it finds; [error] is Never,
because the only way past it is a handler that transfers — one that
returns normally has not answered it, and the program stops. *)
let ty, kind =
match kind with
| Ast.Ssignal -> (Types.Unit, Tast.Ssignal)
| Ast.Serror -> (Types.Never, Tast.Serror)
in
expect ctx loc ~want
(mk loc ty (Tast.Signal (kind, condition_desc ctx loc name, c)))
| Ast.HandlerBind (clauses, body) -> check_handler_bind ctx ?want loc clauses body
| Ast.HandlerCase (body, clauses) -> check_handler_case ctx ?want loc body clauses
(* spec-conditions.md §3–§6: the transfer. Neither of these is a call — one
establishes frames around a body, and the other leaves the function it is
written in — so both are their own nodes all the way down. *)
| Ast.RestartCase (body, clauses) -> check_restart_case ctx ?want loc body clauses
| Ast.InvokeRestart (name, args) ->
(* Never: control resumes at the restart-case, which yields the clause's
value to *its* continuation, so nothing here has a value and nothing
after it runs. The lookup is at run time because restarts are
dynamically scoped and named — §4 — and so, for the same reason, is the
check that these arguments are the ones the clause takes (§3). *)
if ctx.in_defer then
fail loc
"invoke-restart is not allowed inside a defer";
let written = args in
let args = map_lr (fun a -> check ctx a) args in
List.iter
(fun (a : Tast.expr) ->
match a.Tast.ty with
| Types.Unit | Types.Never ->
fail a.Tast.loc
"a restart argument must be a value, and this one is %s"
(tyname loc a.Tast.ty)
| _ -> ())
args;
let sg = restart_sig (List.map (fun (a : Tast.expr) -> a.Tast.ty) args) in
(* What the run-time refusal needs to write its fix, after the signature
and a 0x1f: the syntax (i indented, p parenthesised), then each
argument as written, or x. Only the message reads past the 0x1f; the
comparison is on [type_id sg]. *)
let said =
let spell (a : Ast.expr) =
match a.Ast.e with
| Ast.Float x ->
let t = Printf.sprintf "%g" x in
if String.exists (fun c -> c = '.' || c = 'e' || c = 'n' || c = 'i') t
then t else t ^ ".0"
| _ ->
(* The argument as written, when it is on one line of a file the
checker can read; otherwise an ellipsis. *)
let l = a.Ast.loc in
match Loc.source_line l with
| Some line
when l.Loc.macro = None && l.Loc.eline = l.Loc.line && l.Loc.col >= 1
&& l.Loc.ecol > l.Loc.col && l.Loc.ecol - 1 <= String.length line ->
String.sub line (l.Loc.col - 1) (l.Loc.ecol - l.Loc.col)
| _ -> spell_arg "\u{2026}" a
in
String.concat "\x1f"
(sg :: (if fln_source loc then "i" else "p") :: List.map spell written)
in
(* Evaluated into slots first, so that an argument which transfers on its
own is guarded before this form aims the channel, and so that a call
written in an argument is on the ordinary walk rather than hidden
inside a node that [Reach] and [Load] treat as a leaf. *)
let binds =
List.map (fun (a : Tast.expr) -> (fresh_slot ctx a.Tast.ty, a)) args
in
let locals =
List.map
(fun (s, (a : Tast.expr)) -> mk a.Tast.loc a.Tast.ty (Tast.Local s))
binds
in
let invoke =
mk loc Types.Never
(Tast.InvokeRestart (type_id name, name, locals, said, type_id sg, loc))
in
expect ctx loc ~want
(if binds = [] then invoke
else mk loc Types.Never (Tast.Let (binds, [ invoke ])))
| Ast.Defer forms ->
(* Registering is the whole of it: the forms are checked here, where they
can see the scope they are written in, and the node left behind is
[unit]. [check_fn] splices the registered list onto both exit paths.
[defer_ok] is true for a top-level form of the body and for a form in a
[let] whose extent is the body — see the field's comment. Anywhere else
the cleanup would run at function exit rather than at the exit of the
construct it was written in, so it is refused, and named. *)
if not defer_ok then
fail loc
"defer is not allowed inside %s — a defer always runs at function \
exit. Write it at the top level of the function body, or in a let \
that is itself at the top level"
ctx.defer_block;
register_defer ctx loc forms
and int_literal loc ~want ?(preds = []) ?(default = Types.I32) n =
match want with
| Some (Types.Int k) -> mk loc (Types.Int k) (Tast.Int (in_range loc k n, k))
(* An integer literal where a *type variable* is wanted: the abstract pass
over a generic body, checking [(> x 0)] or [(+ x 1)] with [x] at [$t].
It is admitted exactly when [$t] is declared [is-numeric], and that bound is
what makes it sound rather than optimistic: every type [is-numeric] admits
is an integer or a float, and an untyped integer constant is usable at all
of them — the same rule the [Float k] arm below encodes for a concrete
float. So there is no instantiation of a [is-numeric] variable at which this
literal has no meaning, which is the promise the abstract pass exists to
make.
The node built here is never emitted. A generic body produces no code; the
instantiation re-checks the same form with [$t] substituted, and then the
[Int k] or [Float k] arm above builds the literal at the concrete type and
runs the range check. [I64] is the placeholder width and is chosen only so
that a value too wide for [I32] survives the abstract pass to be ranged at
the instantiation that actually has a type — [(defn f [x $t] $t (+ x 300))]
is fine at [i32] and a refusal at [u8], and [u8] is where it is refused. *)
| Some (Types.Var v) when declares preds v "is-numeric" ->
mk loc (Types.Var v) (Tast.Int (n, Types.I64))
(* A literal in dyn position takes i64 and not the i32 an unconstrained one
defaults to. This is where "dyn integers are i64" stops being a statement
about the ABI and becomes one about the language: [(defonce x dyn 5)] holds
an i64 five, and the defaulting question a wider set of boxes would raise
never arises because there is only the one box. Handled here rather than
left to [expect] so the literal is *built* at the right width — the range
check below is the one that matters, and 3000000000 is a dyn integer even
though it is not an i32. *)
| Some Types.Dyn ->
box loc (mk loc dyn_i64 (Tast.Int (in_range loc Types.I64 n, Types.I64)))
(* An untyped integer constant is usable where a float is wanted, as in
Odin. A float literal is never usable where an integer is wanted. *)
| Some (Types.Float k) ->
mk loc (Types.Float k) (Tast.Float (Int64.to_float n, k))
(* The same position without the bound. An unconstrained variable supports
only what every type supports, and holding a number is not that, so the
refusal names the bound that would admit it rather than reporting a type
mismatch the programmer cannot act on. *)
| Some (Types.Var v) ->
Loc.failk literal_at_want loc
"the integer literal %Ld cannot stand where $%s is wanted — nothing \
declares $%s numeric. Write {:where (is-numeric $%s)} at the head of \
the body"
n v v v
| Some Types.Char ->
Loc.failk literal_at_want loc
"the integer literal %Ld is not a char, and a char compares only with \
a char. Take its code point with %s, or make a char with %s"
n (if Source.indented_at loc then "i32(c)" else "(i32 c)")
(if Source.indented_at loc then Printf.sprintf "char(%Ld)" n
else Printf.sprintf "(char %Ld)" n)
| Some other when other <> Types.Never ->
Loc.failk literal_at_want loc "expected %s, found the integer literal %Ld"
(tyname loc other) n
| _ -> mk loc (Types.Int default) (Tast.Int (in_range loc default n, default))
(* A wide literal where a dyn is wanted. A global's initialiser adds the fix
(its type); anywhere else there is nothing to retype. *)
and wide_at_dyn s =
Printf.sprintf
"expected dyn, found the integer literal %s, which is above the largest \
dyn int (9223372036854775807), so it has no dyn value" s
(* An integer written at or above 2^63, in decimal or in hex. Only a u64 holds
one, so it is accepted there and refused everywhere else, in the spelling it
was written in — its pattern read as an i64 is a different number. *)
and wide_literal loc ~want n s =
match want with
| Some (Types.Int Types.U64) -> mk loc (Types.Int Types.U64) (Tast.Int (n, Types.U64))
| Some (Types.Int k) ->
Loc.failk literal_at_want loc "%s does not fit in %s" s (Types.ikind_name k)
| Some (Types.Float _ as t) ->
Loc.failk literal_at_want loc
"%s is too large for any integer type but u64, and an integer literal \
where %s is wanted is read as one — write (%s (u64 %s))"
s (tyname loc t) (tyname loc t) s
| Some Types.Never | None ->
Loc.failk literal_at_want loc
"%s does not fit in i32, the type an integer literal takes when nothing \
says otherwise — write (u64 %s) for a u64"
s s
| Some Types.Dyn -> Loc.failk literal_at_want loc "%s" (wide_at_dyn s)
| Some other ->
Loc.failk literal_at_want loc
"expected %s, found the integer literal %s, which only a u64 holds"
(tyname loc other) s
(* Arithmetic wraps, but a literal that does not fit its type is a typo, not a
wrap — 300 is never what someone meant by a u8. *)
and in_range ?(pattern = false) loc k n =
let bits = Types.bits k in
let ok =
if Types.signed k then
bits = 64
|| (Int64.compare n (Int64.neg (Int64.shift_left 1L (bits - 1))) >= 0
&& Int64.compare n (Int64.shift_left 1L (bits - 1)) < 0)
(* A literal at or above 2^63 is a [UInt] and never reaches here as a
literal; see [wide_literal]. [pattern] is the folded-constant path,
which holds a u64 as its 64-bit pattern and cannot tell 2^64 - 1 from
-1, so there every pattern is a u64. *)
else if bits = 64 then pattern || Int64.compare n 0L >= 0
else
Int64.compare n 0L >= 0
&& Int64.compare n (Int64.shift_left 1L bits) < 0
in
if ok then n
else if Int64.compare n 0L < 0 && not (Types.signed k) then
(* A negative number at an unsigned type is never the value it reads as.
The cast is how to ask for the bit pattern, and names what it is. *)
let mask =
if bits = 64 then -1L else Int64.sub (Int64.shift_left 1L bits) 1L
in
let tn = Types.ikind_name k in
Loc.failk literal_at_want loc
"%Ld does not fit in %s, which holds no negative number — write (%s %Ld) \
for the %s with the same bits, %Lu"
n tn tn n tn (Int64.logand n mask)
else Loc.failk literal_at_want loc "%Ld does not fit in %s" n
(Types.ikind_name k)
(* The arms that are names rather than calls, and the same rule holds for them:
each is in [builtins] below, and test_flan reads this match to check it. *)
and var ctx ?(qualified = false) loc ~want name =
match name with
(* The qualifier in a value position: [builtin/nil], [builtin/true],
[builtin/context/allocator] — the last one falls out for free, since
stripping one prefix off it leaves a name this match has an arm for.
A qualified name that gets past these arms must be refused and not
handed on to the tables below, which is the whole difference between
this and the call path. Falling through would look up [length] in
[env.fns] and answer with the address of the very definition the reader
wrote [builtin/] to get away from — the feature inverted, silently. So
the catch-all arm below asks [qualified] before it looks anything up. *)
| _ when not qualified && qualified_builtin name <> None ->
let bare = Option.get (qualified_builtin name) in
if not (Hashtbl.mem builtin_set bare) then not_a_builtin loc bare;
var ctx ~qualified:true loc ~want bare
| "true" | "false" ->
expect ctx loc ~want (mk loc Types.Bool (Tast.Bool (name = "true")))
(* The dyn absence value, written down. It arrived with maps — (get m k) on
a key the map does not hold answers it — and this is its producer, so a
program can store one, compare against one, and put one in a map. It is
always dyn here: whatever it becomes at a typed want — None at an
(Option T), a refusal at a bare T — is [expect]'s boundary logic, M2
item 4. *)
| "nil" ->
expect ctx loc ~want (rt loc Types.Dyn "flan_dyn_nil" [])
(* [Dir.north]: an enum's member named through its type, as a data case is
[Shape.Rect]; the same value as [:north] where a Dir is expected. *)
| _ when lookup ctx name = None && enum_member ctx.env name <> None ->
let head = String.sub name 0 (String.rindex name '.') in
let field = String.sub name (String.length head + 1) (String.length name - String.length head - 1) in
(* A local named like the enum shadows it, as a local shadows any
global: [Dir.north] is then that local's field. *)
if lookup ctx head <> None then
check ctx ?want { Ast.e = Ast.Field ({ Ast.e = Ast.Var head; loc }, field); loc }
else
let e, v = Option.get (enum_member ctx.env name) in
expect ctx loc ~want (mk loc (Types.Enum e) (Tast.Int (v, Types.I32)))
| "None" ->
(match want with
| Some (Types.Option t) -> mk loc (Types.Option t) Tast.None_
(* The mirror of [nil] becoming [None]: at a dyn want, None *is* nil,
with nothing to build and nothing to check — there is only one dyn
absence and this is it, not an (Option T) that then gets boxed. *)
| Some Types.Dyn -> rt loc Types.Dyn "flan_dyn_nil" []
| Some other when other <> Types.Never ->
fail loc "expected %s, found None" (tyname loc other)
| _ ->
fail loc
"nothing here says what None is an Option of — use it where an \
Option is expected, or name one, as in %s"
(if fln_source loc then "the(Option(i32), None)" else "(the (Option i32) None)"))
(* spec-memory.md puts the allocator in the calling convention as
[context/allocator] and [context/temp]. They read as names rather than
calls because that is how the spec writes them, and they are dynamic
variables at run time rather than extra parameters — see docs/BUILT.md for why
the literal reading of "calling convention" is deferred. *)
| "context/allocator" ->
expect ctx loc ~want
(let s = fresh_slot ctx Types.Alloc in
mk loc Types.Alloc
(Tast.Let
([ (s, mk loc Types.Alloc (Tast.Zero Types.Alloc)) ],
[ rt loc Types.Unit "flan_context_value"
[ addr_of loc (mk loc Types.Alloc (Tast.Local s)) ];
mk loc Types.Alloc (Tast.Local s) ])))
| "context/temp" ->
expect ctx loc ~want
(seal_alloc ctx loc (rt loc raw_alloc "flan_context_temp" []))
| _ when qualified ->
(* In [builtin_set] — the arm above checked — but not one of the four
arms above this one, so it is a builtin that exists only as a call.
There is no value to hand back: a builtin is an arm in the compiler,
not a function in the program, so it has no address for a [Tast.FnAddr]
to carry. Bare [length] in this position says "unknown name"; this says
the true thing instead, which is that the name is real and the position
is wrong. *)
fail loc
"%s%s is the builtin %s, which is a call and not a value — a builtin \
has no address to pass. Write (%s%s ...) at the call, or wrap it in a \
defn to pass that" builtin_prefix name name builtin_prefix name
| _ ->
match lookup ctx name with
(* A literal local while its uses are being recorded: the use is noted.
One its guess cannot serve is read at the type it asks for, so the
check goes on to the uses after it, and the round is marked to be
thrown away. A dyn want says the dyn width ([lit_solve]). *)
| Some ({ blit = Some key; _ } as b)
when (match ctx.lits, want with
| Some s, Some t ->
let t = lit_payload t in
s.recording && not !lit_quiet
&& (Types.equal t Types.Dyn
|| lit_admits (Option.value (lit_kind key) ~default:`Int) t)
| _ -> false) ->
let s = Option.get ctx.lits and t = lit_payload (Option.get want) in
let operand = List.memq loc !lit_operand_locs in
let c = if operand || Types.equal t Types.Dyn then Hint else Up in
lit_add s key (c, t, loc);
(try expect ctx loc ~want (mk loc b.bty (Tast.Local b.slot))
with Loc.Error _ when lit_kind key <> Some `Box && not operand ->
s.dirty <- true;
expect ctx loc ~want (mk loc t (Tast.Local b.slot)))
| Some b ->
expect ctx loc ~want (local_of loc b)
(* A local of the enclosing function, in a body that was lifted out of it:
captured by value, here, where it is first named. Asked *before* the
globals, because that is what the name means at the place it is
written — inside the enclosing function a local shadows a global of the
same name, and a body lifted out of it must not silently mean something
else. *)
| None when capture ctx loc name <> None ->
let b = Option.get (capture ctx loc name) in
expect ctx loc ~want (mk loc b.bty (Tast.Local b.slot))
| None ->
match Hashtbl.find_opt ctx.env.globals name with
| Some _
when Hashtbl.mem char_consts name
&& (match want with
| Some (Types.Int _ | Types.Float _) -> true
| _ -> false) ->
check ctx ?want { Ast.e = Ast.Byte (Hashtbl.find char_consts name); loc }
| Some (ty, _) ->
expect ctx loc ~want (mk loc ty (Tast.Global name))
| None ->
match Hashtbl.find_opt ctx.env.cases name with
(* A case with no fields is a whole value on its own, so it is written
as a name and not as a call — the same shape [None] has, and for the
same reason: there is nothing to put in the braces. A case that does
have fields is refused here rather than silently zeroed, because ZII
on a constructor would quietly produce a value nobody wrote. *)
| Some (dname, c) when String.contains name '.' ->
if c.Tast.vfields <> [] then
fail loc
"%s has fields, so it needs them — write (%s {.%s ...})"
name name
(List.hd c.Tast.vfields).Tast.fname;
expect ctx loc ~want
(mk loc (Types.Named dname)
(Tast.MakeCase (dname, c.Tast.vname, [])))
| Some (dname, c) ->
fail loc
"%s is a case of the data type %s — write %s.%s"
name dname dname c.Tast.vname
| None ->
(* A bare function name *is* the function. This is a Lisp-1 — one
top-level namespace, enforced, so a defn and a defonce cannot share
a name — and that is exactly what makes (map double xs) safe to
read: there is no second binding of [double] for it to have meant
instead, so Common Lisp's #'double would be punctuation answering
a question this language does not ask. *)
(* A name with several versions is several functions, so the name
alone is not a value. A wanted function type with a count of
parameters says which one was meant. *)
let wanted_version () =
match Hashtbl.find_opt ctx.env.versions name with
| None -> None
| Some vs ->
match Option.bind want fn_sig with
| Some (ps, _) when List.mem_assoc (List.length ps) vs ->
Some (List.assoc (List.length ps) vs)
| wanted ->
let arities =
String.concat "\n"
(List.map (fun (_, v) -> " " ^ version_text ctx.env loc v) vs)
in
(match wanted with
(* The function type wanted here takes a count none of them
does, and no wrapping changes that. *)
| Some (ps, _) ->
let k = List.length ps in
Loc.failk "check/several-versions" loc
"%s has no arity that takes %d argument%s, which the \
function type wanted here does. Its arities are:\n%s"
name k (if k = 1 then "" else "s") arities
| None ->
let fln = fln_source loc in
let k, v0 = List.hd vs in
let xs =
match Hashtbl.find_opt ctx.env.fparams v0 with
| Some fs -> List.map (fun (f : Ast.field) -> f.Ast.fname) fs
| None -> List.init k (fun i -> Printf.sprintf "x%d" (i + 1))
in
Loc.failk "check/several-versions" loc
"%s has several arities, one per number of arguments, so \
the name alone does not say which function this is. \
Wrap it in a function that calls the one you mean, as \
in %s. Its arities are:\n%s"
name
(if fln then
Printf.sprintf "fn(%s) => %s(%s)" (String.concat ", " xs)
name (String.concat ", " xs)
else
Printf.sprintf "(fn [%s] (%s %s))" (String.concat " " xs)
name (String.concat " " xs))
arities)
in
let name =
match wanted_version () with Some v -> v | None -> name
in
(match Hashtbl.find_opt ctx.env.fns name with
| Some (params, ret) ->
private_ref ctx loc name;
(* A foreign function is in [fns] too, and its emitted signature
is C's: no transfer channel, and an aggregate flattened by the
shim. Nothing could call the resulting pointer correctly, so it
is refused for what it is rather than handed out. *)
if Hashtbl.mem ctx.env.externs name then
fail loc
"%s is a foreign function, and its address is not a Flan \
function value. Wrap it in a defn and pass that" name;
expect ctx loc ~want
(mk loc (Types.CFn (params, ret))
(Tast.FnAddr (Tast.Fnval name)))
| None ->
(* The float constants no literal can write, reached only once
every table above has missed, so a program's own binding of
one of these names is the one it gets. *)
match special_float name with
| Some (x, k) ->
expect ctx loc ~want (mk loc (Types.Float k) (Tast.Float (x, k)))
| None -> unknown_name ctx loc (written_name name))
(* What remains of spec-memory.md's ownership section after the repeals of
2026-09-18 is the allocator's side alone: the region rule decides where a
container of owning elements may be built, and the allocator's capability
decides what a free means at run time. Everything copies — a container as
its header, the copies aliasing one buffer — and which frees run, and in
what order, is the program's own business, the same contract Odin ships
with; the dev build's epoch words are the net under it. The flow analysis
that used to live here (a per-function dead set, a borrow flag, a
loop-iteration diff) went in the first repeal; the move-only concept
itself — copy refusals, [copyable?], the struct/union owning rules — went
in the second. See spec-memory.md, "The repeal". *)
(* [defer_ok] is granted again before *every* form, not once before the block:
[check] withdraws it as it starts, so granting it once would let the first
form carry a defer and refuse the second — and two resources acquired in one
[let] is the case the relaxation exists for. *)
and block ctx ?want ?(defer_ok = false) loc body =
match body with
(* Withdrawn here too. An empty body has no last form to be the tail, so
leaving the permission set would hand it to whatever is checked next. *)
| [] -> ctx.tail <- false; ctx.used <- false; expect ctx loc ~want (unit_at loc)
| _ ->
(* A block's tail is its last form and nothing else. Callers that must not
pass one on need do nothing: [check] withdrew it before they were
reached, so [tail] is already false here for all of them. *)
let tail = ctx.tail in
let used = ctx.used in
ctx.used <- false;
let rec go = function
| [ last ] ->
ctx.defer_ok <- defer_ok;
ctx.tail <- tail;
ctx.used <- used;
let l = check ctx ?want last in [ l ], l.Tast.ty
| x :: rest ->
ctx.defer_ok <- defer_ok;
ctx.tail <- false;
ctx.used <- false;
let x = check ctx x in
let rest, ty = go rest in x :: rest, ty
| [] -> assert false
in
let body, ty = go body in
mk loc ty (Tast.Do body)
(* (fn [x y] BODY...) — a function value, lifted into a function of its own.
The same arrangement a handler clause already uses, and deliberately so:
this compiler has built and called function values internally since the Map
landed, and the surface feature is that machinery given a name rather than a
second one invented beside it.
**Capture is by value.** The body sees its parameters, the program's
globals, and the locals of the function it was written in — those last
copied into an environment at the instant the value is made (see
[capture] and [close_over]). The copies go on that function's frame, and
[place_closures] moves them to an environment the collector allocates for
a value that may outlive the frame — so it may be returned, stored or
pushed like any other value: spec-memory.md's case 3.
**The parameter types come from the position.** [Ast.Fn] carries names and
no types — that is the surface syntax, not an omission here — so an fn is
checkable exactly where something says what is wanted. An argument position
does, because [named_call] threads the callee's parameter type into each
argument; a bare [(let [f (fn [x] x)])] does not, and is refused saying so. *)
and check_fn ctx ~want ?gen loc (params : string list) body =
(* [gen] is (array-gen ...)'s way in for an inline fn: the *form* knows the
parameter types — one i32 index per dimension — without there being a
[(Fn ...)] want to say so, and the return is the annotated element type,
or [None] to take the body's own. Everything else threads [want]. The
caller has already checked the arity, in its own words. *)
(* Which of the two function types was asked for. [CFn] is a bare
address, so a literal written into one has nowhere to put an
environment — it is checked exactly as an [Fn] is and then refused *if
it turned out to capture*, which is a decision only the finished body
can make. Nothing else differs. *)
let bare = match want with Some (Types.CFn _) -> true | _ -> false in
let pts, ret0 =
match gen with
| Some (pts, r) -> pts, r
| None ->
match Option.map fn_sig want with
| Some (Some (ps, r)) when List.length ps = List.length params ->
ps, Some r
| Some (Some (ps, r)) ->
fail loc
"this fn has %d parameter%s and %s was wanted here"
(List.length params)
(if List.length params = 1 then "" else "s")
(tyname loc
(if bare then Types.CFn (ps, r) else Types.Fn (ps, r)))
| _ ->
match want with
| Some other when other <> Types.Never ->
fail loc "expected %s, found an fn" (tyname loc other)
| _ ->
if fln_source loc then
fail loc
"nothing here says what this fn's parameters are — an fn takes \
its types from the position it is written in. Pass it where a \
Fn(T, ...) -> R is expected, or name the type where it is \
bound: let f: Fn(T, ...) -> R = fn(...) => ..."
else
fail loc
"nothing here says what this fn's parameters are — an fn takes \
its types from the position it is written in. Write it as an \
argument whose parameter is a (Fn [T ...] R), or a \
(CFn [T ...] R) when it captures nothing"
in
(* Its own frame and its own empty scope, with [outer] kept only so that a
reference to the enclosing function's locals is refused for the reason it
is really refused for. When the return is being inferred the context gets
Unit provisionally — a [return] inside such a body would check against
it, which is a rough edge left rough on purpose: the body of a generator
is an expression, and no machinery is built for the form nobody writes. *)
let fctx =
{ (invented_ctx ctx.env (Option.value ret0 ~default:Types.Unit)) with
outer = ctx.scope; outer_what = Some "an fn"; parent = Some ctx;
owner = ctx.owner }
in
List.iter2
(fun n t -> ignore (bind fctx n t ~assignable:false)) params pts;
(* The last form is checked at the return type the position wants, as a
defn's is at its declared one, so a value that takes its type from what
is asked of it — a kept [when], [None], a bare struct — gets it here. *)
let last_want =
match ret0 with
| Some r when not (Types.equal r Types.Unit) -> Some r
| _ -> None
in
let n = List.length body in
let fbody =
map_lr
(fun (i, e) -> if i = n - 1 then check fctx ?want:last_want e else check fctx e)
(List.mapi (fun i e -> (i, e)) body)
in
(* The same rule an ordinary defn's body follows: the last form is the
answer, and it has to be the declared return type — or, when nothing
declared one ([ret0] is [None]), the last form's own type *is* the
return, which is what lets a bare generator's element type be read off
its body. *)
let fbody, ret =
match List.rev fbody with
(* An fn with no body answers unit, the same as a defn whose declared
return type is () and whose body is empty. Unlike a defn it declares no
return type of its own, so there is nothing here to contradict — but
the *position* names one, and a position wanting a value is the case
[Check] has to refuse. Without this the empty body would simply fall
through and the call would read a return value nothing ever wrote. *)
| [] ->
(match ret0 with
| Some r when not (Types.equal r Types.Unit) ->
fail loc
"an fn with no body answers (), and this one is in a position \
that wants %s — write the value it should answer"
(tyname loc r)
| _ -> fbody, Types.Unit)
| last :: rest ->
(match ret0 with
| Some r ->
List.rev (expect fctx last.Tast.loc ~want:(Some r) last :: rest), r
| None -> fbody, last.Tast.ty)
in
(* Named after the function it was written in and numbered within it, which
is the handler clause's rule and is stable for the same reason: a
redefinition module emits the lifted functions belonging to the bodies it
replaces, and an index into the whole program's list could not say which
those were. *)
let fname =
(* Counted per *kind*, not over everything this function has lifted. A
handler clause and an fn share one list, and a shared counter would
renumber every fn in a function the moment a handler-bind was added
above one — a rename for a body that did not change, in the names a
redefinition module emits. Two counters, two stable sequences. *)
let mine =
List.filter
(fun (l : Tast.fn) ->
l.Tast.fparent = Some ctx.owner
&& String.length l.Tast.name >= 3
&& String.sub l.Tast.name 0 3 = "fn/")
ctx.env.lifted
in
Printf.sprintf "fn/%s/%d" ctx.owner (List.length mine)
in
(* And the environment, now that the body has named everything it is going
to. [close_over] allocates in both frames, so it runs after the body's
slots and before the lifted function is recorded. *)
let prefix, fenv, bind, addr = close_over ~fname ctx fctx loc in
(* A [CFn] is a bare address and has nowhere to keep an environment, so a
literal that captured cannot be one. Refused with the name of what it
captured, because that is the fact the writer has to act on — and with
the fix named, which is the wider type. *)
if bare && fctx.caught <> [] then begin
let names = List.map fst fctx.caught in
Loc.failk "check/cfn-captures" loc
"this fn captures %s, so it is a %s and not a %s: a CFn is the bare \
address, one word, with nowhere for the copies to live. Widen the \
position to Fn, or pass %s in as a parameter"
(String.concat ", " names)
(tyname loc (Types.Fn (pts, ret))) (tyname loc (Types.CFn (pts, ret)))
(match names with [ n ] -> n | _ -> "them")
end;
(* An [Fn]-position literal declares the environment whether or not it
captured: it is reached by a call that passes one. A [CFn]-position
one must not — it is reached by calls that pass none, and a parameter
nobody supplies is read off whatever the register held. *)
let fenv = if bare then fenv else declare_env fctx fenv in
let lifted =
{ Tast.name = fname; params = pts;
slots = Array.of_list (List.rev fctx.slot_tys);
snames = Array.of_list (List.rev fctx.slot_names); as_slots = fctx.as_slots;
ret; body = prefix fbody; fdefers = [];
fenv; fparent = Some ctx.owner; floc = loc }
in
refuse_frame_escapes lifted;
ctx.env.lifted <- lifted :: ctx.env.lifted;
let fty = if bare then Types.CFn (pts, ret) else Types.Fn (pts, ret) in
(* [Flanfn] and not [Fnval], which is the handler clause's choice and is the
same choice for the same reason. [Fnval] exists so that a *name* taken as
a value in a dev build answers with the body that is current, which means
a load from that name's indirection cell. A lifted body has no name
anyone can type and no way to be redefined on its own: it is reached by
address from the body it was written in, and a redefinition of that body
carries its own copy. So the cell would never hold anything but this
symbol, and asking for one is how a redefinition module came to reference
a cell nothing declares. *)
let v =
match addr with
| None -> mk loc fty (Tast.FnAddr (Tast.Flanfn fname))
(* The value, and the store that fills its environment on this frame
around it. Whether the environment stays there is decided once the
whole program is checked, by [place_closures]: a value that may
outlive this frame has its copies moved to an environment the
collector allocates instead. *)
| Some a ->
let c = mk loc fty (Tast.Closure (Tast.Flanfn fname, a)) in
mk loc fty (Tast.Let ([ Option.get bind ], [ c ]))
in
expect ctx loc ~want v
(* A handler runs where the *signal* was, not where it was established, so it
cannot be a branch in the function that wrote it: it is lifted into a
function of its own and reached through a pointer.
It *can* see the establishing function's locals, by value: the same capture
an [fn] literal gets, and the one place it has no escaping case left over.
A handler frame is popped by the body that pushed it and nothing in the
language can name one, so the establishing frame is alive whenever the
clause runs and the copies on it are good. What is still refused is a store
into a captured name — the clause holds a copy, and writing to it would
leave the local as it was — so §1's accumulation case still accumulates
into a global, and now with whatever the establishing function knew
readable beside it.
The body may not [return] either. The frames are pushed and popped around
it, and an early exit would leave them on the stack pointing into a function
that has gone. *)
and check_handler_bind ctx ?want ?(what = "handler-bind") loc clauses body =
let frames =
(* Left to right, and not [List.map], whose order is unspecified: each of
these calls lifts a function onto [ctx.env.lifted] and names it after
the count already there, so an order nobody chose would number the
clauses of one handler-bind differently between builds. The names go in
a redefinition module, which is where that would be noticed — see the
argument in [check_fn]. *)
map_lr
(fun (c : Ast.hclause) ->
let ty = resolve ctx.env c.Ast.hty in
let name =
match ty with
| Types.Named n -> n
| t ->
fail c.Ast.hloc
"a handler matches a struct type, not %s" (tyname loc t)
in
(* Its own context: a fresh frame, an empty scope, and no way to reach
the enclosing one. *)
let hctx =
{ (invented_ctx ctx.env Types.Unit) with
outer = ctx.scope; outer_what = Some "a handler";
parent = Some ctx }
in
(* The condition crosses as a pointer, because the handler runs while
the signalling frame is still alive and there is nothing to copy.
What the clause binds is the condition itself, though, so the
pointer is a hidden parameter and the name is a slot loaded from
it — a handler that passed [c] to something expecting the struct
would otherwise be handed an address. *)
let pslot = fresh_slot hctx (Types.Ptr (Types.Mut, ty)) in
let cslot = bind hctx c.Ast.hname ty ~assignable:false in
let hbody = map_lr (fun e -> check hctx e) c.Ast.hbody in
let hbody =
[ mk c.Ast.hloc Types.Unit
(Tast.Let
([ (cslot,
mk c.Ast.hloc ty
(Tast.Deref
(mk c.Ast.hloc (Types.Ptr (Types.Mut, ty)) (Tast.Local pslot)))) ],
hbody)) ]
in
(* Named after the function it came out of, and numbered within it:
stable against an unrelated handler-bind being added elsewhere,
which an index into the whole program's lifted list would not be. *)
let fname =
(* Per kind, for the reason [check_fn] gives: an fn lifted out of
the same function must not shift this sequence. *)
let mine =
List.filter
(fun (l : Tast.fn) ->
l.Tast.fparent = Some ctx.owner
&& String.length l.Tast.name >= 8
&& String.sub l.Tast.name 0 8 = "handler/")
ctx.env.lifted
in
Printf.sprintf "handler/%s/%d/%s" ctx.owner (List.length mine) name
in
(* And the environment, the same machinery an [fn] literal's capture
uses and settled by the same argument — only with no escaping
case to leave over. A handler frame is popped by the body that
pushed it and nothing in the language can name one, so the clause
cannot be reached from anywhere the establishing frame is not
alive. So its copies stay on the establishing frame, in
a slot rooted with the environment's descriptor. *)
let prefix, fenv, bind, addr =
close_over ~fname ctx hctx c.Ast.hloc
in
(* Every clause declares the environment, captured or not:
[flan_signal] reads it off the frame and passes it to whichever
clause matched, and it cannot know which of them captured. *)
let fenv = declare_env hctx fenv in
let lifted =
{ Tast.name = fname; params = [ Types.Ptr (Types.Mut, ty) ];
slots = Array.of_list (List.rev hctx.slot_tys);
snames = Array.of_list (List.rev hctx.slot_names); as_slots = hctx.as_slots;
ret = Types.Unit; body = prefix hbody; fdefers = [];
fenv; fparent = Some ctx.owner; floc = c.Ast.hloc }
in
refuse_frame_escapes lifted;
ctx.env.lifted <- lifted :: ctx.env.lifted;
{ Tast.htype = type_id name; hfn = fname; henv = addr }, bind)
clauses
in
(* The stores that fill the environments, one per clause that captured,
around the whole form: a handler frame carries the address and the frame
is pushed before the body runs, so the copies have to be made before
either. *)
let envbinds = List.filter_map snd frames in
let frames = List.map fst frames in
(* The flag is set on [ctx] itself and restored, not on a copy: [ctx.slots]
and [ctx.slot_tys] are mutable, so a copy would allocate the body's slots
into a record the function never sees again and the indices would
collide. *)
let saved = ctx.in_frames in
(* [what] is the form the reader wrote. A handler-case establishes its
frames through this function, so a [return] under one has to be refused
naming handler-case rather than naming the machinery underneath it. *)
ctx.in_frames <- Some what;
(* The body's last form is the form's value, which is [with-allocator]'s
shape and for the same reason: both wrap a body in something established
around it and taken off after, and neither is a reason for the body to
stop being an expression. §3 needs it — a [restart-case] whose body is a
[handler-bind] has to agree in type with its clauses, which is how all
four of this repository's crossing probes are written — and it is what
[handler-case] is *not*: that one's value is its clause's, which is the
whole difference between the two. [check_handler_case] below builds one
out of this form and a [restart-case], so both spellings run through
here and only the clause's landing place differs.
This used to be [ignore want] and a flat [Types.Unit], and nothing
complained, because a unit in value position is only caught where the
expectation is checked. So the two backends each answered a caller that
asked anyway, and answered differently: [emit.ml] a literal zero, this
machine whatever the body's last form had left in the slot. Neither was a
value; one of them merely looked like one. *)
let body, ty =
barrier ctx ("a " ^ what) (fun () ->
let rec go = function
| [] -> [ unit_at loc ], Types.Unit
| [ last ] -> let l = check ctx ?want last in [ l ], l.Tast.ty
| e :: rest ->
let e = check ctx e in
let rest, ty = go rest in
e :: rest, ty
in
go body)
in
ctx.in_frames <- saved;
let h = mk loc ty (Tast.Handled (frames, body)) in
let h =
if envbinds = [] then h else mk loc ty (Tast.Let (envbinds, [ h ]))
in
expect ctx loc ~want h
(* (restart-case BODY (name [] BODY-1) ...) — spec-conditions.md §3 and §6.
Unlike a handler, a clause runs *at* the restart-case, which is where it was
written, so it is a branch in this function and sees this function's scope.
What arrives from elsewhere is only the answer to "which clause": a transfer
names the frame it is aimed at, and this form compares that against the
frames it itself pushed.
Every clause body and the body have the same type, and that is the type of
the whole form — which is what makes the fall-through path visible in the
source (§1): a restart-case in value position has to produce its type when
no restart is invoked too. *)
and check_restart_case ctx ?want loc body clauses =
let saved = ctx.in_frames in
ctx.in_frames <- Some "restart-case";
let tbody = barrier ctx "a restart-case" (fun () -> check ctx ?want body) in
ctx.in_frames <- saved;
restart_clauses ctx ?want ~what:"restart-case" loc tbody clauses
(* The clauses of a [restart-case], checked against a body that has already
been checked. Separate from the form above because [handler-case] supplies
its own body — a [handler-bind] it built — and has to name itself in the
refusals rather than naming the machinery it is made of. *)
and restart_clauses ctx ?want ?(hidden = false) ~what loc (tbody : Tast.expr)
clauses =
(* With no expectation from outside, the body's own type is the expectation
the clauses are checked against — unless it produced no value at all, in
which case the first clause that does decides. *)
let want =
match want with
| Some _ -> want
| None -> if tbody.Tast.ty = Types.Never then None else Some tbody.Tast.ty
in
let ty = ref (match want with Some t -> Some t | None -> None) in
let seen = ref [] in
let clauses =
map_lr
(fun (c : Ast.rclause) ->
(* Two clauses of one name would make §4's "the first frame offering
the name" pick between them by an order nothing in the source
shows. *)
if List.mem c.Ast.rname !seen then
fail c.Ast.rloc "this %s offers %s twice" what c.Ast.rname;
seen := c.Ast.rname :: !seen;
(* §3's parameters. They are slots in *this* function — a clause runs
here, not where the invoke was — and the invoker stores into a
buffer this frame owns, because its own frame is gone by the time
the clause body starts (§5). Bound like a function's parameters:
visible only in the clause, and not assignable. *)
let params, b =
scoped ctx (fun () ->
let params =
List.map
(fun (p : Ast.field) ->
let ty = resolve ctx.env p.Ast.fty in
(match ty with
| Types.Unit | Types.Never ->
fail p.Ast.floc
"%s would be a restart parameter of type %s, which is \
not a value" p.Ast.fname (tyname loc ty)
| _ -> ());
(bind ctx p.Ast.fname ty ~assignable:false, ty))
c.Ast.rparams
in
(* Each is checked against what the form has settled on so far, so
a clause that disagrees fails where it is written. The first one
to produce a value is what settles it when nothing outside
did. *)
(params,
(* The same barrier the body gets, and for the same reason: a
clause runs after a transfer landed at this restart-case, with
its frames still to be popped. *)
barrier ctx ("a " ^ what)
(fun () -> block ctx ?want:!ty c.Ast.rloc c.Ast.rbody)))
in
if !ty = None && b.Tast.ty <> Types.Never then ty := Some b.Tast.ty;
let sg = restart_sig (List.map snd params) in
{ Tast.rname_id = type_id c.Ast.rname; rname = c.Ast.rname;
rparams = params; rsig = sg; rsig_id = type_id sg; rbody = [ b ];
rloc = c.Ast.rloc;
rreport = Option.value c.Ast.rreport ~default:"";
rhidden = hidden })
clauses
in
let ty = match !ty with Some t -> t | None -> Types.Never in
mk loc ty (Tast.RestartCase (clauses, tbody))
(* (handler-case BODY [(Type [c] BODY-1) ...]) — the unwinding handler, and
spec-conditions.md's one remaining open question about it, answered: it *is*
a handler-bind plus a transfer, built here rather than given nodes and
backends of its own.
(handler-case B [(T [c] A)])
== (restart-case (handler-bind [(T [c] (invoke-restart 'R c))] B)
(R [c T] A))
That is Common Lisp's own definition of the operator, and every property
this form is supposed to have falls out of the two it is made of rather
than being re-implemented beside them.
- The clause runs *here*, at the handler-case, because a restart clause
does; so it sees this function's locals, which a handler clause cannot,
and its value is the whole form's, because a restart-case's clause value
is the whole restart-case's.
- The stack below is gone by then. §5's defers, and the allocator a
[with-allocator] rebound, are honoured on the way out because that is
what a transfer already does for every frame it leaves.
- A condition matching no clause installs no frame, so nothing here sees
it and it keeps going outward exactly as it would have.
- The body and every clause agree on one type, because §3 already says a
restart-case's body and clauses do. A clause that disagrees is refused
where it is written, like an [if] whose arms disagree.
The condition crosses as a restart argument, which means by value into a
buffer this frame owns — which is the only thing that can work, since §5
kills the signalling frame the condition was living on the moment the
transfer starts.
The restart the two halves meet over is named after this function and
numbered within it, and the number is the count of handler clauses already
lifted out of this function. That count never goes down, and every
handler-case lifts at least one clause before the next one can read it, so
within a name's own bucket the numbers are strictly increasing and no two
forms can mint the same name. A clause body written inside another handler
clause counts against the [<none>] bucket rather than against a function's,
which is the same argument again and not a hole: that bucket is one list
for the whole program and it only grows.
Uniqueness is the requirement rather than a nicety. Two handler-cases
sharing a name, one inside the other's extent, would have the inner frame
shadow the outer one (§4), which lands a condition at the wrong form —
and, because the two would be expecting different condition types, lands it
as a run-time signature refusal rather than as a wrong answer. *)
and check_handler_case ctx ?want loc body clauses =
let what = "handler-case" in
(* Resolved once here, for the refusal below; [check_handler_bind] resolves
them again for the frames, which is cheap and keeps that function whole. *)
let names =
List.map
(fun (c : Ast.hclause) ->
match resolve ctx.env c.Ast.hty with
| Types.Named n -> n
| t ->
fail c.Ast.hloc
"a handler matches a struct type, not %s" (tyname loc t))
clauses
in
(* Two clauses for one condition type: the first would take every one of
them and the second could never run, and nothing in the source says which
the reader meant. The same refusal a duplicate restart name gets, and for
the same reason. *)
let seen = ref [] in
List.iter2
(fun (c : Ast.hclause) n ->
if List.mem n !seen then
fail c.Ast.hloc "this handler-case handles %s twice" n;
seen := n :: !seen)
clauses names;
let k =
List.length
(List.filter
(fun (l : Tast.fn) ->
l.Tast.fparent = Some ctx.owner
&& String.length l.Tast.name >= 8
&& String.sub l.Tast.name 0 8 = "handler/")
ctx.env.lifted)
in
let rnames =
List.map (Printf.sprintf "handler-case/%s/%d/%s" ctx.owner k) names
in
(* The handler half: one clause per arm, whose whole body is the transfer.
It is lifted into a function of its own like any handler clause, and the
condition it was handed is copied into the restart frame's buffer on its
way out. *)
let handlers =
List.map2
(fun (c : Ast.hclause) r ->
{ c with
Ast.hbody =
[ { Ast.e =
Ast.InvokeRestart
(r, [ { Ast.e = Ast.Var c.Ast.hname; loc = c.Ast.hloc } ]);
loc = c.Ast.hloc } ] })
clauses rnames
in
(* The landing half: one restart clause per arm, taking the condition as its
single parameter and running what the reader actually wrote. *)
let landings =
List.map2
(fun (c : Ast.hclause) r ->
{ Ast.rname = r;
rparams =
[ { Ast.fname = c.Ast.hname; fty = c.Ast.hty;
floc = c.Ast.hloc } ];
rreport = None; rbody = c.Ast.hbody; rloc = c.Ast.hloc })
clauses rnames
in
let tbody = check_handler_bind ctx ?want ~what loc handlers [ body ] in
(* Hidden: the landing is reached only through the handler above, and a break
loop under this form would otherwise list it as if someone could mean it. *)
restart_clauses ctx ?want ~hidden:true ~what loc tbody landings
(* The forms of a [defer], checked in place and hung on the function. What is
left where it stands is one store: this defer's number into the counter
[defer_slot] describes, which is how the transfer exit tells a defer that
has registered from one the text has not reached yet. The form's type is
still [unit], which is all a reader of the value can see. *)
and register_defer ctx loc forms =
(* Saved and put back rather than cleared, which is the same thing today and
will not be the day a defer may hold one. Nothing reaches here from
inside a defer now — [defer_ok] is false in there — so the saved value is
always false; written this way so that if it ever is not, the flag comes
back rather than being dropped. *)
let was_in_defer = ctx.in_defer in
ctx.in_defer <- true;
(* A barrier, for the reason [defer] itself exists: these forms are *copied*
into every exit path of the function, where the loop they were written
beside is not running. A loop written inside the defer is below the
barrier and breaks out of itself perfectly well. *)
let forms =
barrier ctx "a defer" (fun () -> map_lr (fun d -> check ctx d) forms)
in
ctx.in_defer <- was_in_defer;
ctx.defers <- mk loc Types.Unit (Tast.Do forms) :: ctx.defers;
let slot =
match ctx.defer_slot with
| Some s -> s
| None ->
let s = fresh_slot ctx (Types.Int Types.I64) in
ctx.defer_slot <- Some s;
s
in
mk loc Types.Unit
(Tast.Set
(Tast.Plocal slot,
mk loc (Types.Int Types.I64)
(Tast.Int (Int64.of_int (List.length ctx.defers), Types.I64))))
(* The transfer exit's copy of the defers, each under the count that says it
registered. [ds] is innermost first, so the last one registered is at the
head and the [j]th from the end is defer number [j].
The zero the counter starts at is written by [defer_counter_zero] below, at
the top of the body: a slot is an alloca like any other and holds whatever
the stack held until something stores to it, which at -O2 is not zero and
is exactly how this was found. *)
and guarded_defers slot (ds : Tast.expr list) =
let n = List.length ds in
List.mapi
(fun i (d : Tast.expr) ->
let loc = d.Tast.loc in
let i64 = Types.Int Types.I64 in
let test =
mk loc Types.Bool
(Tast.Prim
(Tast.Ge,
[ mk loc i64 (Tast.Local slot);
mk loc i64 (Tast.Int (Int64.of_int (n - i), Types.I64)) ]))
in
mk loc Types.Unit (Tast.If (test, d, unit_at loc)))
ds
and defer_counter_zero slot loc =
mk loc Types.Unit
(Tast.Set
(Tast.Plocal slot,
mk loc (Types.Int Types.I64) (Tast.Int (0L, Types.I64))))
(* [defer_ok] says whether *this* let has the function's extent. If it does, so
does every form in its body, including a nested let — which is why the flag
is handed to the body rather than consumed here. *)
(* A text or bracket literal local used where only its typed reading works —
sliced, its address taken, cloned, destructured: a typed use, recorded. *)
and lit_typed_use ctx (e : Ast.expr) =
match e.Ast.e, ctx.lits with
| Ast.Var n, Some s when s.recording ->
(match lookup ctx n with
| Some { blit = Some key; _ } when lit_kind key = Some `Box ->
lit_add s key (Up, Types.Unit, e.Ast.loc)
| _ -> ())
| _ -> ()
(* The literal local [n] names, while its uses are being recorded. *)
and lit_recorded ctx n =
match ctx.lits, lookup ctx n with
| Some s, Some { blit = Some key; _ } when s.recording -> Some key
| _ -> None
(* [v] read as arithmetic: the literal locals among its operands, and the
operands that are something else (a call, an index, a typed name). Number
literals are neither. The result's type is the join of all of them, so the
locals are merged with whatever [v] is stored into and the others are
what it brings. *)
and lit_parts ctx (v : Ast.expr) =
(* [env]: names a [let] inside [v] binds, with what they were bound to, so
(let [t a0] t) passes a0 through as (do a0) and an if's arms do. *)
let rec go env (v : Ast.expr) (vars, others) =
match v.Ast.e with
| Ast.Var n ->
(match List.assoc_opt n env with
| Some (Some (vs, os)) -> (vs @ vars, os @ others)
| Some None -> (vars, v :: others)
| None ->
match lookup ctx n with
| Some { blit = Some k; _ } when lit_kind k <> Some `Box -> (k :: vars, others)
| _ -> (vars, v :: others))
| Ast.Int _ | Ast.Float _ | Ast.Byte _ -> (vars, others)
| Ast.Call ({ Ast.e = Ast.Var ("+" | "-" | "*" | "/" | "%" | "min" | "max"); _ }, args)
when args <> [] ->
List.fold_left (fun acc a -> go env a acc) (vars, others) args
| Ast.Do (_ :: _ as xs) -> go env (List.nth xs (List.length xs - 1)) (vars, others)
| Ast.Let (bs, (_ :: _ as xs)) ->
let env =
List.fold_left
(fun env (b : Ast.binding) ->
(b.Ast.bname,
if b.Ast.bty = None then Some (go env b.Ast.bval ([], [])) else None)
:: env)
env bs
in
go env (List.nth xs (List.length xs - 1)) (vars, others)
| Ast.If (_, t, Some e) -> go env e (go env t (vars, others))
| _ -> (vars, v :: others)
in
go [] v ([], [])
(* A float literal, or an integer one past i32, anywhere in [v]'s arithmetic. *)
and lit_wide_literals (v : Ast.expr) =
let rec go (v : Ast.expr) =
match v.Ast.e with
| Ast.Float _ -> [ (Types.Float (float_default ()), v.Ast.loc) ]
| Ast.Int n when Int64.compare n (Int64.of_int32 Int32.max_int) > 0
|| Int64.compare n (Int64.of_int32 Int32.min_int) < 0 ->
[ (Types.Int Types.I64, v.Ast.loc) ]
| Ast.Call ({ Ast.e = Ast.Var ("+" | "-" | "*" | "/" | "%" | "min" | "max"); _ }, args) ->
List.concat_map go args
| _ -> []
in
go v
(* [v] stored into the literal local [key] (a [set] or a [recur]), while
recording: the literal locals it is arithmetic over are merged with [key],
and each other operand says, on its own terms, what it brings ([Down]) —
never the guess the round happens to have. Then [v] is checked at [key]'s
guess, quietly, since a want there is the guess and says nothing. *)
and lit_down ctx key pty (v : Ast.expr) =
let s = Option.get ctx.lits in
let quietly f =
let was = !lit_quiet in
lit_quiet := true;
Fun.protect ~finally:(fun () -> lit_quiet := was) f
in
let vars, others = lit_parts ctx v in
List.iter (lit_union s key) vars;
List.iter (fun (t, l) -> lit_add s key (Hint, t, l)) (lit_wide_literals v);
List.iter
(fun (o : Ast.expr) ->
match trial ctx (fun () -> check ctx o) with
| Ok e -> lit_add s key (Down, e.Tast.ty, o.Ast.loc)
| Error _ -> ())
others;
match trial ctx (fun () -> quietly (fun () -> check ctx ~want:pty v)) with
| Ok e -> e
| Error _ ->
s.dirty <- true;
quietly (fun () -> check ctx v)
(* The type a literal initialiser is checked at while a session is open —
its current guess, or the decision — numbering it while recording.
[None] for anything that is not a literal, or with no session open. *)
and lit_local ctx name (e : Ast.expr) =
match ctx.lits, lit_kind e with
| Some s, Some kind ->
if s.recording && not (Phys.mem s.ids e) then begin
Phys.replace s.ids e s.count;
s.count <- s.count + 1;
s.keys <- (e, name) :: s.keys
end;
Some (lit_guess ~subst:ctx.env.subst s e kind)
| _ -> None
(* A literal local's initialiser, at the type [lit_local] gave it: [Unit] is
a text or bracket literal's typed reading ([lit_default]). *)
and lit_init ctx t (e : Ast.expr) =
if Types.equal t Types.Unit then with_typed_literals (fun () -> check ctx e)
else check ctx ~want:t e
(* [run] is a [let] or a [loop] with some of [inits] literals, and the
outermost such form of this function: the session opens here. See
[lit_session]. *)
and with_lits : 'a. ctx -> Loc.t -> Ast.expr list -> (unit -> 'a) -> 'a =
fun ctx loc inits run ->
if ctx.lits <> None || not (List.exists (fun e -> lit_kind e <> None) inits)
then run ()
else begin
let s = { decided = Phys.create 16; recording = false; ids = Phys.create 16;
keys = []; count = 0; parent = Hashtbl.create 16;
cons = Hashtbl.create 16; dirty = false } in
ctx.lits <- Some s;
incr lit_depth;
let remember () =
let subst = ctx.env.subst in
List.iter
(fun (k, _) ->
let kind = Option.value (lit_kind k) ~default:`Int in
let t = lit_guess ~subst s k kind in
let others =
Option.value (Phys.find_opt lit_memo k) ~default:[]
|> List.filter (fun (sb, _) -> sb != subst)
in
Phys.replace lit_memo k ((subst, t) :: others))
s.keys
in
Fun.protect
~finally:(fun () ->
ctx.lits <- None;
decr lit_depth;
if !lit_depth = 0 then Phys.reset lit_memo)
@@ fun () ->
let unsettled = Loc.diag ~kind:"check/lit-unsettled" loc "unsettled" in
(* The decisions the uses recorded so far make, written into
[s.decided], and the locals whose decision moved. *)
let settle () =
let solved = lit_solve s in
let moved = ref [] in
List.iter
(fun (k, name, r) ->
match r with
| Ok t ->
let kind = Option.value (lit_kind k) ~default:`Int in
if not (Types.equal t (lit_guess ~subst:ctx.env.subst s k kind)) then begin
moved := (k, name, t) :: !moved;
Phys.replace s.decided k t
end
| Error _ -> ())
solved;
(solved, List.rev !moved)
in
let conflict solved =
List.find_map
(fun (k, name, r) -> match r with Error e -> Some (k, name, e) | Ok _ -> None)
solved
in
let log () =
if lit_has "log" then
Phys.iter
(fun k t ->
let d = lit_default k (Option.value (lit_kind k) ~default:`Int) in
if not (Types.equal t d) then
Printf.eprintf "LITINF %s:%d:%d %s -> %s\n" k.Ast.loc.Loc.file
k.Ast.loc.Loc.line k.Ast.loc.Loc.col (tyname loc d) (tyname loc t))
s.decided
in
let rec round n =
Phys.reset s.ids; s.keys <- []; s.count <- 0;
Hashtbl.reset s.parent; Hashtbl.reset s.cons; s.dirty <- false;
s.recording <- true;
incr lit_recording;
(* A ref, because [trial] is monomorphic inside this recursive group. *)
let answer = ref None in
(* What the settle inside the trial found, which has already written
its decisions: settling again outside would see nothing move. *)
let settled = ref None in
let outcome =
Fun.protect
~finally:(fun () -> decr lit_recording; s.recording <- false)
(fun () ->
trial ctx (fun () ->
let r = run () in
let solved, moved = settle () in
settled := Some (solved, moved);
(* The guesses held: this check is the answer. *)
if moved <> [] || s.dirty || conflict solved <> None then
raise (Loc.Error unsettled);
answer := Some r;
poison loc))
in
match outcome, !answer with
| Ok _, Some r -> log (); remember (); r
| _ ->
let solved, moved =
match !settled with Some sm -> sm | None -> settle ()
in
(match conflict solved with
| Some (k, name, ((t1, l1), (t2, l2))) -> lit_conflict k name t1 l1 t2 l2
| None -> ());
if moved <> [] && n < lit_rounds then round (n + 1)
else if moved <> [] then begin
(* Still moving: a local fed through more calls than the rounds
follow. It is the local that needs its type written. *)
let k, name, t = List.hd moved in
lit_unsettled k name t
end
else begin
(* Nothing left to learn: checked for real, so a refusal is the
ordinary one and a whole-file check goes on past it. *)
log ();
remember ();
run ()
end
in
round 1
end
(* A literal local whose uses kept changing its type past [lit_rounds]. *)
and lit_unsettled (k : Ast.expr) name t =
let lit = lit_spelling k in
let fix =
if fln_source k.Ast.loc then Printf.sprintf "let %s: %s = %s" name (tyname k.Ast.loc t) lit
else Printf.sprintf "(%s %s)" (tyname k.Ast.loc t) lit
in
Loc.failk "check/literal-unsettled" k.Ast.loc
"the type of %s depends on too long a chain of the values stored into it \
to be read off them. Write the type it should have: %s"
name fix
and lit_spelling (k : Ast.expr) =
let lit =
match k.Ast.e with
| Ast.Int n -> Int64.to_string n
| Ast.Float x -> Printf.sprintf "%g" x
| Ast.Byte b -> Printf.sprintf "\\%c" (Char.chr b)
| Ast.Call (_, [ { Ast.e = Ast.Int n; _ } ]) -> Int64.to_string (Int64.neg n)
| Ast.Call (_, [ { Ast.e = Ast.Float x; _ } ]) -> Printf.sprintf "%g" (-.x)
| _ -> "..."
in
let whole = lit <> "" && String.for_all (fun c -> (c >= '0' && c <= '9') || c = '-') lit in
if whole && lit_kind k = Some `Float then lit ^ ".0" else lit
(* Two uses of a literal local that no one type satisfies. *)
and lit_conflict (k : Ast.expr) name t1 l1 t2 l2 =
let lit =
match k.Ast.e with
| Ast.Int n -> Int64.to_string n
| Ast.Float x -> Printf.sprintf "%g" x
| Ast.Byte b -> Printf.sprintf "\\%c" (Char.chr b)
| Ast.Call (_, [ { Ast.e = Ast.Int n; _ } ]) -> Int64.to_string (Int64.neg n)
| Ast.Call (_, [ { Ast.e = Ast.Float x; _ } ]) -> Printf.sprintf "%g" (-.x)
| _ -> "..."
in
(* %g drops the point from a whole float; put it back so the fix reads as
a float literal. 1e+20 already does. *)
let whole = lit <> "" && String.for_all (fun c -> (c >= '0' && c <= '9') || c = '-') lit in
let lit = if whole && lit_kind k = Some `Float then lit ^ ".0" else lit in
let fix =
if fln_source k.Ast.loc then Printf.sprintf "let %s: %s = %s" name (tyname l1 t1) lit
else Printf.sprintf "(%s %s)" (tyname l1 t1) lit
in
Loc.failk "check/literal-uses" k.Ast.loc
~notes:[ Loc.note l1 (Printf.sprintf "%s is used as %s here" name (tyname l1 t1));
Loc.note l2 (Printf.sprintf "and as %s here" (tyname l2 t2)) ]
"%s is used as %s and as %s, and %s can have only one type. Write the \
one it should have: %s"
name (tyname l1 t1) (tyname l2 t2) lit fix
and check_let ctx ?(tail = false) ?(used = false) ?want ?(defer_ok = false) loc bs body =
with_lits ctx loc
(List.filter_map
(fun (b : Ast.binding) -> if b.Ast.bty = None then Some b.Ast.bval else None)
bs)
@@ fun () ->
scoped ctx (fun () ->
let bs =
map_lr
(fun (b : Ast.binding) ->
let want = Option.map (resolve ctx.env) b.Ast.bty in
let lit = if b.Ast.bty = None then lit_local ctx b.Ast.bname b.Ast.bval else None in
ctx.used <- true;
let v =
match lit with
| Some t -> lit_init ctx t b.Ast.bval
| None -> check ctx ?want b.Ast.bval
in
(match v.Tast.ty with
(* A refused initialiser, already reported: the name is bound to
the poison so that what follows is still checked. *)
| Types.Never when is_poison v -> ()
| Types.Unit | Types.Never ->
fail b.Ast.bloc "%s would be bound to %s, which is not a value"
b.Ast.bname (tyname loc v.Tast.ty)
| _ -> ());
(* Locals are assignable places; parameters are not. *)
let slot =
bind ctx b.Ast.bname v.Tast.ty ~assignable:true
?lit:(Option.map (fun _ -> b.Ast.bval) lit)
in
(slot, v))
bs
in
(* After the bindings, because checking each of them withdrew it. *)
ctx.tail <- tail;
ctx.used <- used;
let body = block ctx ?want ~defer_ok loc body in
mk loc body.Tast.ty (Tast.Let (bs, [ body ])))
(* (dotimes [i n] body...) is a counting loop, not a new IR node: bind [i] to 0
and the bound to a hidden slot — [n] is evaluated once, before the loop, so
a body that changes it cannot change the trip count — then step [i] at the
end of the body. [i] is not assignable, so the step below is the only writer. *)
(* What a loop still contributes to checking after the repeal is scoping, not
ownership: the entry below is what [break] and [continue] resolve against,
and the defer_block name is what makes a [defer] in here refused as "a loop
body" — it would fire once at function exit rather than once per iteration,
and the message says so. [fresh] and the iteration move-diff that used it
are gone with the flow analysis. *)
and in_loop ctx ?label ?entry f =
(* The loop goes on the stack before the body is checked and comes off after,
so a [break] inside it can see it and one outside it cannot. *)
let loops = ctx.loops in
ctx.loops <- (match entry with Some e -> e | None -> Lloop label) :: loops;
let blocker = ctx.defer_block in
ctx.defer_block <- "a loop body";
let r = f () in
ctx.defer_block <- blocker;
ctx.loops <- loops;
r
(* Which loop a [break] or a [continue] means, as a count of loops outwards
from the innermost — which is what [Tast.Break] carries and what [emit]
indexes. Refuses three things, each by its own reason: nothing to break out
of, a label naming no loop this form is inside, and a jump that would cross
a barrier. *)
and loop_target ctx loc verb label =
let rec go depth = function
| [] ->
(match label with
| None ->
fail loc "%s is only allowed inside a loop" verb
| Some l ->
fail loc
"no loop named :%s encloses this %s — a label names one of the \
loops this form is written inside" l verb)
| Lloop name :: rest ->
(match label with
| None -> depth
| Some l when name = Some l -> depth
| Some _ -> go (depth + 1) rest)
(* A [loop] answers with the value of its body. A jump out of one would
have to produce that value from somewhere and there is nowhere, so it is
a barrier like the others, named as what it is. A [while] written inside
a loop sits below this entry and keeps its own break. *)
| Lrecur _ :: _ ->
(match label with
| None ->
fail loc
"%s cannot leave a (loop ...), and that is the nearest loop. \
Answer with the value, or use a while"
verb
| Some l ->
fail loc
"%s :%s would leave a (loop ...), and it may not. Answer with the \
value, or use a while" verb l)
| Lbarrier what :: rest ->
(* Crossing it would skip whatever the construct does on the way out —
the handler or restart frames it pushed, or, for a defer, would jump
to a loop that is not there on the path the forms were copied into.
A loop nested inside the construct is below this entry and is never
reached here, which is the whole point of the rule being relative. *)
ignore rest;
(match label with
| None ->
fail loc
"%s cannot leave %s, and the nearest loop is outside it. Write the \
loop inside %s, or leave with a value and test that after"
verb what what
| Some l ->
fail loc
"%s :%s would leave %s, and it may not. Name a loop inside %s"
verb l what what)
in
go 0 ctx.loops
(* [(dotimes [i n] ...)], [(dotimes [i start stop] ...)] and
[(dotimes [i start stop step] ...)].
**The convention.** [stop] is exclusive, so [(dotimes [i 0 n] ...)] is the
same loop as [(dotimes [i n] ...)] — one rule rather than two, and the
shorter form stays the longer one with its defaults left off. A negative
step counts down and tests with [>] instead of [<], which is what makes
[(dotimes [i 9 -1 -1] ...)] run 9 down to 0.
**Each bound once, before the loop.** [start] is the counter's initial
value, [stop] and a non-literal [step] each get a hidden slot, and all three
are checked — and therefore evaluated — left to right, outside the counter's
scope. A body that assigns to what they were computed from cannot change the
trip count.
**The sign of the step.** When it is a literal the direction is known here
and the condition is the one comparison it always was, so nothing changed
for every loop anyone has written. A literal 0 is refused: it is an infinite
loop spelled as an accident. A step that is only known at run time gets a
condition that asks the sign first, and the cost lands on exactly the loops
that need it. A run-time 0 falls out of that test as a loop that runs no
times at all — neither arm of the sign test holds — which is deterministic
and terminating, the two things an accidental hang is not. *)
and check_dotimes ctx ~want loc label name (b : Ast.bounds) body =
(* Left to right, and all three before the counter is bound: they are
evaluated before it exists, so [(dotimes [i i (* outer i *)] ...)] reads
the outer name and the order a counter function sees is the written one. *)
let start = Option.map (fun e -> check ctx ~want:index_ty e) b.Ast.dstart in
let stop = check ctx ~want:index_ty b.Ast.dstop in
let step = Option.map (fun e -> check ctx ~want:index_ty e) b.Ast.dstep in
let int k = mk loc index_ty (Tast.Int (k, Types.I32)) in
(* A literal step, if that is what was written. The default is 1, which is a
literal too, so the one-bound form takes this path and emits exactly what
it has always emitted. *)
let literal =
match step with
| None -> Some 1L
| Some { Tast.e = Tast.Int (k, _); _ } -> Some k
| Some _ -> None
in
(match literal, step with
| Some 0L, Some s ->
fail s.Tast.loc
"a step of 0 never moves the counter. Give it a step that moves, as \
in (dotimes [i 0 10 2] ...); left out, the step is 1"
| _ -> ());
scoped ctx (fun () ->
let i = bind ctx name index_ty ~assignable:false in
let limit = fresh_slot ctx index_ty in
(* A slot only when the step is not a literal: a literal needs no slot to
be evaluated once, and the one-bound form's frame keeps the shape it
had. *)
let stepslot =
match literal with None -> Some (fresh_slot ctx index_ty) | Some _ -> None
in
let body = in_loop ctx ?label (fun () -> map_lr (fun b -> check ctx b) body) in
let iv = mk loc index_ty (Tast.Local i) in
let limitv = mk loc index_ty (Tast.Local limit) in
let stepv =
match literal, stepslot with
| Some k, _ -> int k
| None, Some s -> mk loc index_ty (Tast.Local s)
| None, None -> assert false
in
let cmp op = mk loc Types.Bool (Tast.Prim (op, [ iv; limitv ])) in
let cond =
match literal with
| Some k when k > 0L -> cmp Tast.Lt
| Some _ -> cmp Tast.Gt
| None ->
(* Both directions, asked in the order that leaves 0 with neither: the
counter has not passed the stop *and* the step is going that way.
Written as nested [If]s because that is what [and] and [or] already
become, so nothing new reaches a backend. *)
let sign op =
mk loc Types.Bool (Tast.Prim (op, [ stepv; int 0L ]))
in
mk loc Types.Bool
(Tast.If (sign Tast.Gt, cmp Tast.Lt,
mk loc Types.Bool
(Tast.If (sign Tast.Lt, cmp Tast.Gt,
mk loc Types.Bool (Tast.Bool false)))))
in
let advance =
mk loc Types.Unit
(Tast.Set (Tast.Plocal i,
mk loc index_ty (Tast.Prim (Tast.Add, [ iv; stepv ]))))
in
(* The step is the *latch* and not the last form of the body. Folded onto
the body it would be skipped by a [continue], which branches past the
rest of the body — so [i] would never advance and the loop would hang.
That is the whole reason [Tast.While] carries a third list. *)
let loop = mk loc Types.Unit (Tast.While (cond, body, [ advance ])) in
let binds =
(i, match start with Some s -> s | None -> int 0L)
:: (limit, stop)
:: (match stepslot, step with
| Some s, Some v -> [ (s, v) ]
| _ -> [])
in
expect ctx loc ~want (mk loc Types.Unit (Tast.Let (binds, [ loop ]))))
(* ── (loop [...] ...) and (recur ...) ───────────────────────────────────
A loop is a [let] over its names, a [While] whose condition is [true], and
two jumps: [recur] rebinds every name and continues, and falling off the end
of the body breaks. Nothing new reaches the backend, which is the whole
argument for [recur] over tail calls — the machinery is the one [while] and
the labelled [break]/[continue] already needed.
**The value.** A loop answers with the value of its body, so the result is
written into a slot of its own on the way out and read after the loop. A
body that is [Unit] needs no slot, and a body that is [Never] — one that
only ever recurs or returns — needs neither a slot nor the break, because
nothing falls off the end of it.
**Why [Set] of a [Never] body is safe.** [emit] closes a block at its
terminator and drops what follows ([ins] checks [f.live]), so when the body
ends in a jump the store is simply never written. The one ordering that
matters is inside [Tast.Set]: the place is resolved before the value, and a
local's place is an address with no instruction behind it.
**Why the invented [While] may carry jumps.** [tast.ml] says a [While] the
checker invents contains none, because the depths it would carry were minted
against a stack it is not on. This one is different and the difference is
the licence: it is pushed on [ctx.loops] like any other, so the [Break 0]
below and every [continue] a [recur] mints count from the same stack [emit]
indexes. *)
and check_loop ctx ?want loc bs body =
with_lits ctx loc (List.map snd bs) @@ fun () ->
scoped ctx (fun () ->
(* Each initial value is evaluated once, before the loop, exactly as a
[let]'s is and as [dotimes]'s bound is — and bound before the next is
checked, as a [let]'s is, so a later initialiser sees an earlier
name. *)
let binds =
map_lr
(fun (n, v0) ->
let lit = lit_local ctx n v0 in
let v =
match lit with Some t -> lit_init ctx t v0 | None -> check ctx v0
in
(match v.Tast.ty with
(* A refused initialiser, already reported: the name is bound to
the poison so that what follows is still checked. *)
| Types.Never when is_poison v -> ()
| Types.Unit | Types.Never ->
fail v.Tast.loc "%s would be bound to %s, which is not a value" n
(tyname loc v.Tast.ty)
| _ -> ());
(bind ctx n v.Tast.ty ~assignable:true
?lit:(Option.map (fun _ -> v0) lit), v))
bs
in
let names = List.map (fun (slot, v) -> (slot, v.Tast.ty)) binds in
(* The singleton is [in_loop]'s doing: it sits in this recursive group and
is therefore monomorphic, and every other caller hands it a list. *)
let tbody =
match
in_loop ctx ~entry:(Lrecur names) (fun () ->
[ scoped ctx (fun () ->
(* The body's last form is the loop's tail, which is the only
place a [recur] may stand. [block] distributes it. *)
ctx.tail <- true;
block ctx ?want loc body) ])
with
| [ b ] -> b
| _ -> assert false
in
let ty = tbody.Tast.ty in
let yes = mk loc Types.Bool (Tast.Bool true) in
let leave = mk loc Types.Never (Tast.Break 0) in
let inner, result =
if ty = Types.Never then ([ tbody ], None)
else if ty = Types.Unit then ([ tbody; leave ], None)
else
let r = fresh_slot ctx ty in
([ mk loc Types.Unit (Tast.Set (Tast.Plocal r, tbody)); leave ], Some r)
in
let loop = mk loc Types.Unit (Tast.While (yes, inner, [])) in
match result with
| None -> expect ctx loc ~want (mk loc ty (Tast.Let (binds, [ loop ])))
| Some r ->
expect ctx loc ~want
(mk loc ty
(Tast.Let (binds @ [ (r, mk loc ty (Tast.Zero ty)) ],
[ loop; mk loc ty (Tast.Local r) ]))))
(* Which loop a [recur] means, and what it has to rebind. The same walk
[break] and [continue] make, over the same stack and refusing on the same
barriers — [recur] asks "may this jump cross that" and gets the answer that
was already settled, not a second mechanism. *)
and recur_target ctx loc =
let rec go depth = function
| [] ->
fail loc
"recur is only allowed inside a (loop ...) — write the repetition as \
a loop with a recur in its tail"
| Lrecur names :: _ -> (depth, names)
(* Unreachable while the tail rule holds — a loop body is not a tail
position, so no [recur] is ever written inside one — but the depth is
counted rather than assumed, because it is what [emit] indexes. *)
| Lloop _ :: rest -> go (depth + 1) rest
| Lbarrier what :: _ ->
fail loc
"recur would leave %s, and it may not. Write the loop inside %s"
what what
in
go 0 ctx.loops
and check_recur ctx ~tail loc args =
let depth, names = recur_target ctx loc in
(* Checked, which is the whole of why this is better than a silent TCO: a
recur that is not in tail position is a compile error here, where under
tail calls it would have been a stack overflow at run time. *)
if not tail then
fail loc
"recur must be in the tail position of its loop — the last thing the \
body does, or the last thing in an if, match or let arm that is itself \
in the tail. Something here would still run afterwards";
let want = List.length names and got = List.length args in
if want <> got then
fail loc "this loop binds %d name%s and this recur passes %d" want
(if want = 1 then "" else "s") got;
let vals =
List.map2
(fun a (slot, ty) ->
match
List.find_opt (fun (_, (b : binding)) -> b.slot = slot) ctx.scope
with
| Some (_, { blit = Some key; _ })
when (match ctx.lits with Some s -> s.recording | None -> false) ->
lit_down ctx key ty a
| _ -> check ctx ~want:ty a)
args names
in
(* Every name is rebound at once. The new values go into temporaries first,
so that (recur y x) swaps rather than writing y over x and then reading it
back — the same reason Clojure's recur is simultaneous. *)
let temps = List.map2 (fun v (_, ty) -> (fresh_slot ctx ty, v)) vals names in
let sets =
List.map2
(fun (t, _) (slot, ty) ->
mk loc Types.Unit
(Tast.Set (Tast.Plocal slot, mk loc ty (Tast.Local t))))
temps names
in
mk loc Types.Never
(Tast.Let (temps, sets @ [ mk loc Types.Never (Tast.Continue depth) ]))
(* Every boolean-position test in the language funnels through here: [if]'s
own condition, [while]'s, and [not]'s argument. ([when] and [cond] are
sugar built out of [Ast.If] in parse.ml, so they get this for free
without a separate case. [and] and [or] are sugar too, and both their
tests and their answers get this for free the same way — see
[shortcircuit] in parse.ml, where each binds its test to a temp and
answers that temp on the path it decides, so the deciding operand
itself comes back rather than a bare bool.)
A dyn scrutinee is tested for truthiness, Clojure's rule: nil and false
are the only falsey values, and everything else — 0, "", an empty vec, an
empty map, a keyword — is truthy. A typed scrutinee stays strictly bool,
exactly as before.
The scrutinee is checked with no expectation first so its own type
decides which rule applies. That is fine for the passing cases — dyn, or
already bool — but a *refused* one has to be re-checked with the old
[want:Bool] rather than reported from here, and that covers two shapes of
"asked the wrong question first": a bare integer or float literal answers
differently to "what type is this" than to "is this a bool" (check.ml's
int_literal/float arms only give the nicer answer, "expected bool, found
the integer literal 5", when asked the second way), and [None] does not
even have an answer to the first question — "nothing here says what None
is an Option of" — where the second gets straight to "expected bool,
found None". Asking the first way first is what makes the dyn case work,
so the refusal path — success or exception both — asks the second way
again, after the fact, purely to get the sentence a typed if has always
given.
[loc] comes from [c] itself, not from the caller's [if]/[while]/[not] —
the built-in rt call and cast have to sit at the condition's own position
or an --x86 disassembly and the dev inspector point at the wrong column
when the two differ (an [if] whose test is not its first token).
The [exception Loc.Error _] arm swallows a rejection from arbitrary depth
inside [c] and re-runs the whole of [check ctx c] a second time, which is
sound only because every one of [ctx]'s save-restore sites — [barrier],
and the [in_frames]/[in_defer]/[loops]/[scope] plumbing [check] itself
uses — is not exception-safe: a failure mid-walk can leave one of those
pushed without its pop. Today that is harmless, because the second call
always either succeeds outright or raises again and this function's own
caller then aborts the compile — nothing downstream ever reads [ctx]
again on that path. It would stop being harmless the day some later
want-sensitive elaboration on this path can *succeed* by yielding a
concrete [Bool] on retry rather than failing a second time: then the
first, swallowed pass's half-restored state and any name or slot it
registered before raising would both still be live.
That same retry-on-failure is also, deliberately, not made cheaper by
only retrying at the leaf that actually needs a nicer message (an int or
float literal, or [None]) and re-raising everywhere else: the shorter
path was tried and shelved, because "everywhere else" is not safe to
generalise past [not]'s one level of nesting — a condition that is
itself a compound form carrying its own literals arbitrarily deep (an
[if] or [let] standing where a condition is expected) would need [want]
threaded through exactly as far as this function's own second call
already threads it, and stopping short changes which of *those*
literals gets the nicer message, not just the speed. The cost that
buys is real: nested [not] on a program that does not type-check re-runs
this whole function once per level of nesting inside the level above it,
which would be exponential in how deep the nesting goes. What keeps it
linear is [truthy_failed]: a condition this function has already refused,
in the same body, is refused again with the same diagnostic rather than
re-checked, so a retry re-walks its subtree once and stops at the first
condition below it that was settled. The message is the one the first
pass produced, so no message changes.
Keywords are a separate, deliberate loss rather than a bug: a bare
[:kw] used to be checked here with [want:Types.Bool] from the start, so
it hit the keyword arm's [Some other] case and refused by name — "is an
enum member where an enum is expected and a dyn keyword elsewhere, but
bool is expected here". Checking it here with no expectation first, as
every other scrutinee now is, resolves it as the dyn keyword instead
(there being no enum in play), and dyn keywords are unconditionally
truthy — so [(if :kw a b)] now takes [a], where it used to refuse
outright. The author's call: lispy truthiness wins wherever it can, so
this refusal is given up on purpose and not special-cased back in;
test_flan.ml pins the new answer down so it is not lost again by
accident. *)
and check_truthy ctx c =
(* Only where a refusal is raised: while recovering, one is recorded where
it happens and the check goes on, so a replayed one would hide others. *)
if ctx.env.recovering && ctx.env.speculating = 0 then check_truthy_once ctx c
else
match
List.find_opt
(fun (n, sc, r, _) -> n == c && r == ctx.ret && same_scope sc ctx.scope)
!truthy_failed
with
| Some (_, _, _, d) -> raise (Loc.Error d)
| None ->
let scope = ctx.scope in
incr truthy_depth;
Fun.protect
~finally:(fun () ->
decr truthy_depth;
if !truthy_depth = 0 then truthy_failed := [])
(fun () ->
try check_truthy_once ctx c
with Loc.Error d as ex ->
if !lit_recording = 0 then truthy_failed := (c, scope, ctx.ret, d) :: !truthy_failed;
raise ex)
and check_truthy_once ctx c =
let loc = c.Ast.loc in
(* Speculative, because a refusal here is answered by asking again at
[bool]; and that second ask is guarded, because its refusal is re-worded
below. Recovery sees each refusal once, in its final words. *)
match speculate ctx.env (fun () -> check ctx c) with
| c0 when c0.Tast.ty = Types.Dyn ->
widen loc Types.Bool (rt loc (Types.Int Types.I32) "flan_dyn_truthy" [ c0 ])
| c0 when Types.fits ~expected:Types.Bool ~actual:c0.Tast.ty -> c0
| c0 ->
(* Re-checked at [bool] first, and the answer is kept only when it is a
message that knows something this one does not: a literal names itself
("found the integer literal 1"), and [None] names itself, and both of
those point at the mistake better than a type name would. What comes
back as the *generic* mismatch — "expected bool, found i32", which is
true and tells a reader nothing they did not have — is the one replaced
below.
The rule, rather than the fact. [expected bool, found i32] is true and
says nothing a reader did not already know; what they do not know is
that this language has exactly two things a condition may be, and that
the dyn one is not the typed one. A dyn condition is Clojure's — nil
and false are false and 0 is true — so a message that told somebody to
compare against zero *in general* would be wrong about half the
language. It is said only of the typed side, which is where they are.
The comparison is spelled with the condition's own name where there is
one, because [(!= x 0)] is a thing to type and [(!= … 0)] is not.
Anything more complicated than a name gets the operator and no
template: a reconstructed expression would be a guess at code the
reader can see for themselves. *)
(ctx.env.guard_next <- true;
match check ctx ~want:Types.Bool c with
| c1 -> c1
| exception Loc.Error d when not (String.equal d.Loc.kind "check/type-mismatch") ->
refuse_or_poison ctx.env loc d
| exception Loc.Error _ ->
let how =
let zero = match c0.Tast.ty with Types.Float _ -> "0.0" | _ -> "0" in
let comparable =
match c0.Tast.ty with Types.Int _ | Types.Float _ -> true | _ -> false
in
match c.Ast.e, comparable with
| Ast.Var n, true -> Printf.sprintf " — test it, as (!= %s %s)" n zero
| _, true -> Printf.sprintf " — test it against %s with !=" zero
| _ -> ""
in
(try
Loc.failk "check/condition-not-bool" loc
"a condition is a bool or a dyn, and this is %s%s"
(tyname loc c0.Tast.ty) how
with Loc.Error d -> refuse_or_poison ctx.env loc d))
| exception Loc.Error _ -> check ctx ~want:Types.Bool c
and check_if ctx ?(tail = false) ?(used = false) ?want loc c t e =
if ctx.env.recovering && ctx.env.speculating = 0 then
check_if_once ctx ~tail ~used ?want loc c t e
else
match
List.find_opt
(fun (n, (sc, r), w, _) ->
n == c && r == ctx.ret && w = (want, used) && same_scope sc ctx.scope)
(Hashtbl.find_all if_failed c.Ast.loc)
with
| Some (_, _, _, d) -> raise (Loc.Error d)
| None ->
let scope = ctx.scope in
incr if_depth;
Fun.protect
~finally:(fun () ->
decr if_depth;
if !if_depth = 0 then Hashtbl.reset if_failed)
(fun () ->
try check_if_once ctx ~tail ~used ?want loc c t e
with Loc.Error d as ex ->
if !lit_recording = 0 then Hashtbl.add if_failed c.Ast.loc (c, (scope, ctx.ret), (want, used), d);
raise ex)
and check_if_once ctx ~tail ~used ?want loc c t e =
if as_binds c = [] then check_if_tested ctx ~tail ~used ?want loc c t e
else scoped ctx (fun () -> check_if_tested ctx ~tail ~used ?want loc c t e)
and check_if_tested ctx ~tail ~used ?want loc c t e =
let t =
match narrows c with
| [] -> t
| names -> { t with Ast.e = Ast.Narrow (names, t) }
in
let c, t =
match as_binds c with
| [] -> (check_truthy ctx c, t)
| _ ->
(* What an [as] named reaches the block through a name no reader can
write, bound in the scope this if was given; the else is checked
without it. *)
let cv, named = as_cond ctx c in
(cv, { t with Ast.e = Ast.Alias (named, t) })
in
(* Both arms are the tail, and a one-armed [if] counts: [(when c (recur ...))]
is how nearly every loop is written, and the branch is still the last
thing the body does. Both arms are kept when the [if] is. *)
let in_tail f = ctx.tail <- tail; ctx.used <- used; f () in
match e with
| None -> check_when ctx ~used ?want loc c (fun ?want () ->
branch ctx (fun () -> in_tail (fun () -> check ctx ?want t)))
(* A kept chain whose last else is missing — a [cond] with no [:else],
whose fallthrough is [(do)], or an [if] whose else is a [when] — is one
[when] spread over several tests: an Option, [None] when no test holds,
and [Some] of the arm that ran. *)
| Some e when kept_open ~used want e ->
(match e.Ast.e with
| Ast.Do [] ->
check_when ctx ~used:true ?want loc c (fun ?want () ->
branch ctx (fun () -> in_tail (fun () -> check ctx ?want t)))
| _ ->
let tw =
match want with
| Some (Types.Option i) -> Some i
| Some Types.Dyn -> Some Types.Dyn
| _ -> None
in
let arm w = branch ctx (fun () -> in_tail (fun () -> check ctx ?want:w t)) in
(* At an Option want the arm is asked the payload first, and the chain
wraps it; refused there, it is asked the Option itself, which it
then is (decision 140). So a T?? wanted of a T? arm is Some of it,
and the chain's own None stays the outer one. *)
let t, at_payload =
match want with
| Some (Types.Option _) ->
(match trial ctx (fun () -> arm tw) with
| Ok t -> t, true
| Error d ->
(match trial ctx (fun () -> arm want) with
| Ok t -> t, false
| Error _ -> raise (Loc.Error d)))
| _ -> arm tw, false
in
let rest ~used ?want () =
branch ctx (fun () ->
ctx.tail <- tail; ctx.used <- used; check ctx ?want e)
in
match t.Tast.ty with
| ty when at_payload && not (Types.equal ty Types.Never) ->
let oty = Option.get want in
let e = rest ~used:true ~want:oty () in
mk loc oty (Tast.If (c, mk loc oty (Tast.Some_ t), e))
| Types.Unit ->
expect ctx loc ~want
(mk loc Types.Unit (Tast.If (c, t, rest ~used:false ())))
| Types.Never ->
let e = rest ~used:true ?want () in
mk loc e.Tast.ty (Tast.If (c, t, e))
| Types.Dyn ->
let e = rest ~used:true ~want:Types.Dyn () in
expect ctx loc ~want (mk loc Types.Dyn (Tast.If (c, t, e)))
(* An arm that is already an Option is the chain's value as it is,
and [None] when no test holds — one level flattened (decision
140): an arm's None and no arm running are one answer. *)
| Types.Option _ as o ->
let e = rest ~used:true ~want:o () in
expect ctx loc ~want (mk loc o (Tast.If (c, t, e)))
| ty ->
let oty = Types.Option ty in
let e = rest ~used:true ~want:oty () in
expect ctx loc ~want
(mk loc oty (Tast.If (c, mk loc oty (Tast.Some_ t), e))))
(* Two literal arms meet at the wider of their own types, as two literal
elements of an array do: [(if c 1 2.5)] is an f64. *)
| Some e
when want = None && lone_literal t && lone_literal e
&& (match literal_join ctx t e with
| Some j -> not (Types.equal j (Types.Int Types.I32))
| None -> false) ->
let want = literal_join ctx t e in
let t = branch ctx (fun () -> in_tail (fun () -> check ctx ?want t)) in
let e = branch ctx (fun () -> in_tail (fun () -> check ctx ?want e)) in
mk loc t.Tast.ty (Tast.If (c, t, e))
| Some e when want = None
&& ((adapts t && not (adapts e || is_none_lit e))
(* [if c then None else 5]: the else arm decides T, and
None meets it at T? below, as the other order does. *)
|| (is_none_lit t && not (is_none_lit e)))
&& not (and_sentinel e) ->
(* A literal has no type of its own until something asks, so with no
expectation the other arm decides: [(if c 4000000 n)] over an i64 [n]
is an i64, as [(+ 4000000 n)] is. *)
let e = branch ctx (fun () -> in_tail (fun () -> check ctx e)) in
let twant = if e.Tast.ty = Types.Never then None else Some e.Tast.ty in
let then_at w = branch ctx (fun () -> in_tail (fun () -> check ctx ?want:w t)) in
(* [None] or [Some(1)] beside a plain T: the two meet at T?, the other
arm wrapped (decision 138). Tried only once the arm is refused at T,
so an arm that fits T is never an Option. *)
let t, e =
match e.Tast.ty with
| Types.Option _ | Types.Dyn | Types.Unit | Types.Never -> then_at twant, e
| ety ->
(match trial ctx (fun () -> then_at twant) with
| Ok t -> t, e
| Error _ ->
let oty = Types.Option ety in
(match trial ctx (fun () -> then_at (Some oty)) with
| Ok t -> t, expect ctx e.Tast.loc ~want:(Some oty) e
| Error _ -> then_at twant, e))
in
let ty = if e.Tast.ty = Types.Never then t.Tast.ty else e.Tast.ty in
mk loc ty (Tast.If (c, t, e))
| Some e ->
let t = branch ctx (fun () -> in_tail (fun () -> check ctx ?want t)) in
(* With no expectation the then-branch supplies one for the else-branch,
unless it diverges, in which case the else-branch decides. *)
(* A slice or a pointer from the then-branch is not the else-branch's
want: the two may differ only in const, and they meet at the
read-only one whichever side it is on — [Types.const_join]. *)
let free_join =
want = None
&& (match t.Tast.ty with Types.Slice _ | Types.Ptr _ -> true | _ -> false)
in
(* A bool arm and a dyn arm meet at dyn, the bool boxed — Clojure's rule,
so (or false (box "s")) answers "s" rather than unboxing the string at
bool and trapping. The other order already met at dyn, the then arm
deciding. So after a bool then arm the else arm is checked on its own
terms first, since checking it at bool is what unboxes it, and kept
when it is a bool or a dyn. Anything else is abandoned and checked at
bool as before, for that path's messages. A chain whose arms all fit
is checked once; a refused one re-checks each level below the refusal
once more, the square of its depth. *)
(* The else arm at the then arm's type first, as it always was: a value
that takes its type from what is asked of it — [(+ b 1)] beside an
i64, [nil] beside an Option — is asked the then arm's. Only when that
is refused as a mismatch is it checked on its own terms, and the two
meet at [arm_join], so [(if c x32 y64)] is the i64 [(if c y64 x32)]
is. A dyn opened at the then arm's type is not a meeting: the two
meet at dyn, as they do the other way round. The arm is checked once
each way at most, and a refusal at the then arm's type is kept
([arm_failed]) for the ifs above that check it again. *)
let joined =
if want <> None || free_join || t.Tast.ty = Types.Never
|| t.Tast.ty = Types.Bool || and_sentinel e || lone_literal e
then None
else
let alone () = branch ctx (fun () -> in_tail (fun () -> check ctx e)) in
let key = (ctx.scope, ctx.ret) in
let at_then () =
match
List.find_opt
(fun (n, (sc, r), w, _) ->
n == e && r == ctx.ret && Types.equal w t.Tast.ty
&& same_scope sc ctx.scope)
(Hashtbl.find_all arm_failed e.Ast.loc)
with
| Some (_, _, _, d) -> Error d
| None ->
match
trial ctx (fun () ->
branch ctx (fun () ->
in_tail (fun () -> check ctx ~want:t.Tast.ty e)))
with
| Ok v -> Ok v
| Error d ->
if !lit_recording = 0 then Hashtbl.add arm_failed e.Ast.loc (e, key, t.Tast.ty, d);
Error d
in
let meet v =
match arm_join t.Tast.ty v.Tast.ty with
| Some j -> Some (j, expect ctx v.Tast.loc ~want:(Some j) v)
| None -> None
in
(* The else arm refused at T and fine at T? — [None], [Some(1)] —
and the two meet at T?, the then arm wrapped (decision 138). *)
let at_option () =
match t.Tast.ty with
| Types.Option _ | Types.Dyn | Types.Unit -> None
| ty ->
let oty = Types.Option ty in
(match
trial ctx (fun () ->
branch ctx (fun () -> in_tail (fun () -> check ctx ~want:oty e)))
with
| Ok v when Types.equal v.Tast.ty oty -> Some (oty, v)
| _ -> None)
in
(* Only where the arms met nowhere else, so nothing that met before
meets differently: a dyn else arm still meets at dyn. *)
match
(match at_then () with
| Ok v ->
(match opened_dyn ~box:(to_dyn ctx) v with
| Some box -> Some (Types.Dyn, box)
| None -> Some (t.Tast.ty, v))
| Error _ when adapts e -> None
| Error d ->
(* A mismatch, or a dyn the then arm's type could not open: the
arm on its own terms meets the then arm. Anything else refused
it at the then arm's type, and that refusal is said. *)
(match
trial ctx (fun () ->
let v = alone () in
if is_mismatch d || Types.equal v.Tast.ty Types.Dyn then v
else raise (Loc.Error (Loc.diag ~kind:not_kept v.Tast.loc "")))
with
| Ok v -> meet v
| Error own when String.equal own.Loc.kind not_kept -> None
(* Refused on its own terms too, and not as a mismatch — an
unknown name, say: that is the real error, and nothing is said
about a type the arm was never going to have. *)
| Error own when is_mismatch d && not (is_mismatch own) ->
let v = alone () in
(match meet v with
| Some r -> Some r
| None ->
Some (t.Tast.ty, expect ctx v.Tast.loc ~want:(Some t.Tast.ty) v))
| Error _ -> None))
with
| None -> at_option ()
| j -> j
in
match joined with
| Some (j, v) ->
let t = expect ctx t.Tast.loc ~want:(Some j) t in
mk loc j (Tast.If (c, t, v))
| None ->
let own_else =
if want = None && t.Tast.ty = Types.Bool then
match
trial ctx (fun () ->
let v = branch ctx (fun () -> in_tail (fun () -> check ctx e)) in
match v.Tast.ty with
| Types.Bool | Types.Dyn | Types.Never -> v
| _ -> raise (Loc.Error (Loc.diag v.Tast.loc "not bool or dyn")))
with
| Ok v -> Some v
| Error _ -> None
else None
in
let t =
match own_else with
| Some v when v.Tast.ty = Types.Dyn ->
expect ctx t.Tast.loc ~want:(Some Types.Dyn) t
| _ -> t
in
let ewant =
match want with
| Some _ -> want
| None ->
if t.Tast.ty = Types.Never || free_join then None else Some t.Tast.ty
in
(* [(and a b c)] is [(let [t a] (if t (let [u b] (if u c u)) t))], so the
*last* operand of an [and] is the then arm and the sentinel that carries
the previous operand's location is the else arm. With no expectation
the then arm supplies one, the sentinel is checked against it, and the
mismatch was reported at the sentinel — which is a caret on the operand
before the one that is wrong. TODO.org, "and's last operand gets a
misdirected caret", records three fixes for this that
were rejected and one that was not: prefer the arm that is not a
compiler temp when deciding which to blame. That is this.
Only [and] needs it. In an [or] the chain sits in the else arm and the
sentinel in the then arm, so every operand is already blamed at its own
location; and with an expectation in hand both arms are checked against
it rather than against each other, so nothing here runs. *)
let e =
match own_else with
| Some v -> v
| None ->
let reworded = want = None && and_sentinel e in
match
branch ctx (fun () ->
in_tail (fun () ->
if reworded then ctx.env.guard_next <- true;
check ctx ?want:ewant e))
with
| v -> v
| exception Loc.Error d
when reworded && String.equal d.Loc.kind "check/type-mismatch" ->
(try
Loc.failk "check/shortcircuit-operand" t.Tast.loc
"an and answers false or its last operand, so the two have to be \
one type — this operand is %s, and false is a bool"
(tyname loc t.Tast.ty)
with Loc.Error d -> refuse_or_poison ctx.env e.Ast.loc d)
| exception Loc.Error d when reworded -> refuse_or_poison ctx.env e.Ast.loc d
in
let t, e =
match free_join, Types.const_join t.Tast.ty e.Tast.ty with
| true, Some j when e.Tast.ty <> Types.Never ->
expect ctx t.Tast.loc ~want:(Some j) t,
expect ctx e.Tast.loc ~want:(Some j) e
| _ -> t, e
in
let ty =
if t.Tast.ty = Types.Never then e.Tast.ty
else if e.Tast.ty = Types.Never then t.Tast.ty
else if Types.equal t.Tast.ty e.Tast.ty then t.Tast.ty
else
fail loc "the branches of this if have different types: %s and %s"
(tyname loc t.Tast.ty) (tyname loc e.Tast.ty)
in
mk loc ty (Tast.If (c, t, e))
(* A one-armed [if], which [when] is. As a statement it is Unit whatever its
branch evaluates to. Kept — a [let]'s value, an argument, a return, or
anything else with a type wanted of it — it answers (Option T): [Some] of
the branch when the test held and [None] when it did not. A branch that
is already an Option is that Option, flattened one level (decision 140,
Kotlin's [?.] rather than Rust's [bool::then]): [None] from the branch and
a failed test are one answer. A (Option (Option T)) branch stays one.
Dyn has no Option. Where a dyn is wanted, or the branch is a dyn, a false
test answers nil and a true one the branch's value — one absence, as a
dyn map's [get] has.
A branch with no value (Unit) or none at all (Never) keeps the statement's
Unit, so what is refused about binding one is refused as before. *)
and check_when ctx ~used ?want loc c
(branch_at : ?want:Types.t -> unit -> Tast.expr) =
let stmt t = expect ctx loc ~want (mk loc Types.Unit (Tast.If (c, t, unit_at loc))) in
let nil () = rt loc Types.Dyn "flan_dyn_nil" [] in
let valueless (t : Tast.expr) =
match t.Tast.ty with Types.Unit | Types.Never -> true | _ -> false
in
match want with
| Some (Types.Unit | Types.Never) -> stmt (branch_at ())
| Some Types.Dyn ->
let t = branch_at ~want:Types.Dyn () in
mk loc Types.Dyn (Tast.If (c, t, nil ()))
| Some (Types.Option inner as oty) ->
(* The payload first, wrapped; refused there, the Option itself, which
the branch then is (decision 140). *)
let t =
match trial ctx (fun () -> branch_at ~want:inner ()) with
| Ok t -> if t.Tast.ty = Types.Never then t else mk loc oty (Tast.Some_ t)
| Error d ->
(match trial ctx (fun () -> branch_at ~want:oty ()) with
| Ok t -> if t.Tast.ty = Types.Never then t else expect ctx loc ~want:(Some oty) t
| Error _ -> raise (Loc.Error d))
in
mk loc oty (Tast.If (c, t, mk loc oty Tast.None_))
| None when not used -> stmt (branch_at ())
| _ ->
let t = branch_at () in
if valueless t then stmt t
else if Types.equal t.Tast.ty Types.Dyn then
expect ctx loc ~want (mk loc Types.Dyn (Tast.If (c, t, nil ())))
else if (match t.Tast.ty with Types.Option _ -> true | _ -> false) then
expect ctx loc ~want (mk loc t.Tast.ty (Tast.If (c, t, mk loc t.Tast.ty Tast.None_)))
else
let oty = Types.Option t.Tast.ty in
expect ctx loc ~want
(mk loc oty (Tast.If (c, mk loc oty (Tast.Some_ t), mk loc oty Tast.None_)))
(* Whether a two-armed [if] is kept and its else chain ends without one —
[(do)], or a one-armed [if] — so the whole chain answers an Option. *)
and kept_open ~used want (e : Ast.expr) =
let kept =
match want with
| Some (Types.Unit | Types.Never) -> false
| Some _ -> true
| None -> used
in
let rec open_ (e : Ast.expr) =
match e.Ast.e with
| Ast.Do [] | Ast.If (_, _, None) | Ast.IfLet (_, _, None) -> true
| Ast.If (_, _, Some e') | Ast.IfLet (_, _, Some e') -> open_ e'
| _ -> false
in
kept && open_ e
(* The type two literals meet at, each at its own type — a wide integer at
u64, which is the only type that holds one. *)
and literal_join ctx (a : Ast.expr) (b : Ast.expr) =
let own (x : Ast.expr) =
match x.Ast.e with
| Ast.UInt _ -> Some (Types.Int Types.U64)
| _ -> probe ctx x.Ast.loc (fun () -> (check ctx x).Tast.ty)
in
match own a, own b with
| Some x, Some y -> literal_meet x y
| _ -> None
(* Whether a name would reach a callee if it were called — a global function, a
generic, or a local holding a function value. The three sources [named_call]
itself consults, in its own order; builtins are deliberately not among them,
so that [(println {.f v})] still reports what it reports today. *)
and callable ctx name =
Hashtbl.mem ctx.env.fns name
|| Hashtbl.mem ctx.env.gsigs name
|| (match lookup ctx name with
| Some b -> (match b.bty with Types.Fn _ -> true | _ -> false)
| None -> false)
(* [(Pair 1 2)] and [(Pair {.a 1 .b 2})]: which copy of a generic struct a
value builds. The position says, when a copy of this struct is wanted
there; otherwise the fields do, each given one's type binding the
template's variables the way a generic call's arguments bind its own. The
fields are only probed here — each check is abandoned — and the ordinary
constructor checks them again against the copy it is handed. *)
and generic_ctor ctx ~want loc name given =
let env = ctx.env in
let g = Hashtbl.find env.gstructs name in
match want with
| Some (Types.Named k)
when (match Hashtbl.find_opt struct_apps k with
| Some (h, _) -> String.equal h name
| None -> false) ->
realise env loc (Types.Named k); k
| _ ->
let open_key =
struct_copy env loc name (List.map (fun (p, _) -> Types.Var p) g.gparams)
in
let fields = (Hashtbl.find env.structs open_key).Tast.fields in
let pairs =
match given with
| `Positional args when List.length args = List.length fields ->
List.combine fields args
(* The wrong number of fields: the copy at variables is handed on, and
the constructor says what is wrong with the count in its own words. *)
| `Positional _ -> []
| `Named kvs ->
List.filter_map
(fun (f, v) ->
List.find_opt
(fun (fl : Tast.field) -> String.equal fl.Tast.fname f) fields
|> Option.map (fun fl -> (fl, v)))
kvs
in
let subst = ref [] and unsure = ref [] in
(* An untyped literal has no type of its own to bring, so the fields that
do have one bind first: [(Node 2 (addr c))] over a [(Node i64)] [c] is
a [(Node i64)], and the 2 takes its width from that. *)
let literal (a : Ast.expr) =
match a.Ast.e with
| Ast.Int _ | Ast.UInt _ | Ast.Float _ | Ast.Byte _ -> true
| _ -> false
in
(* A literal's own type, the one it has with nothing expected of it. *)
let literal_type (a : Ast.expr) =
match a.Ast.e with
| Ast.Float _ -> Types.Float (float_default ())
| Ast.UInt _ -> Types.Int Types.U64
| Ast.Byte b when b > 127 -> Types.Int Types.I32
| Ast.Byte _ -> Types.Int Types.U8
| _ -> Types.Int Types.I32
in
let pairs =
List.filter (fun (_, a) -> not (literal a)) pairs
@ List.filter (fun (_, a) -> literal a) pairs
in
(* Variables only literals have bound so far: a later literal may widen
them, as a generic call's literal arguments meet at the wider type —
[(Pair 1 2.5)] is a [(Pair f64)]. *)
let lit_only = ref [] in
(* Which field's value decided each variable, for the refusal of a
literal that does not fit what it decided. *)
let decided_by = ref [] in
List.iter
(fun ((f : Tast.field), (a : Ast.expr)) ->
match f.Tast.fty with
(* A literal at a variable a typed field already decided: it has to
be usable at that type, and when it is not the refusal names the
field that decided it. *)
| Types.Var v
when literal a && List.mem_assoc v !subst
&& not (List.mem v !lit_only) ->
let b = List.assoc v !subst in
(match a.Ast.e, b with
| Ast.Float x, Types.Int _ ->
let notes =
match List.assoc_opt v !decided_by with
| Some (fname, at) ->
[ Loc.note at
(Printf.sprintf ".%s is %s here, which decides $%s" fname
(tyname loc b) v) ]
| None -> []
in
Loc.failk "check/generic-struct-field" a.Ast.loc ~notes
"%s's .%s is $%s, which is %s here, and %g is a float literal. \
Write .%s as an integer, or give .%s a float type"
name f.Tast.fname v (tyname loc b) x f.Tast.fname
(match List.assoc_opt v !decided_by with
| Some (fname, _) -> fname
| None -> f.Tast.fname)
| _ -> ())
| Types.Var v when literal a && not (List.mem_assoc v !subst && not (List.mem v !lit_only)) ->
let t = (literal_type a) in
(match List.assoc_opt v !subst with
| None -> subst := (v, t) :: !subst; lit_only := v :: !lit_only
| Some b ->
(match literal_meet b t with
| Some j -> subst := (v, j) :: List.remove_assoc v !subst
| None ->
fail a.Ast.loc "%s's .%s is %s here, and this is %s"
(tyname loc (Types.Named open_key)) f.Tast.fname
(tyname loc b) (tyname loc t)))
| _ ->
if open_ty f.Tast.fty
&& not (literal a && subst_ty !subst f.Tast.fty |> open_ty |> not)
then begin
let seen = ref None in
let probe () =
seen := Some (check ctx a).Tast.ty;
Loc.fail a.Ast.loc "probe"
in
let refusal = match trial ctx probe with Error d -> Some d | Ok _ -> None in
match !seen with
(* No type of its own — [None], a bare {.field v} — is no
evidence; the constructor checks it against the copy the other
fields decide, and its refusal is the one given if they decide
nothing. *)
| None -> Option.iter (fun d -> unsure := d :: !unsure) refusal
| Some t ->
let before = !subst in
if bind_ty subst f.Tast.fty t then
List.iter
(fun (v, _) ->
if not (List.mem_assoc v before) then
decided_by := (v, (f.Tast.fname, a.Ast.loc)) :: !decided_by)
!subst
else
fail a.Ast.loc "%s's .%s is %s here, and this is %s"
(tyname loc (Types.Named open_key)) f.Tast.fname
(tyname loc (subst_ty !subst f.Tast.fty))
(tyname loc t)
end)
pairs;
(match given with
| `Positional args when List.length args <> List.length fields -> open_key
| _ ->
let targs =
List.map
(fun (p, _) ->
match List.assoc_opt p !subst with
| Some t -> t
| None ->
(match List.rev !unsure with
| d :: _ -> Loc.raise_diag d
| [] -> ());
Loc.failk "check/generic-struct-undetermined" loc
~notes:[ Loc.note g.gloc (name ^ " is declared here") ]
"%s's $%s is not decided by the fields given here. Name the \
type where the value goes, as in (the (%s %s) ...)"
name p name
(String.concat " "
(List.map
(fun (q, is_len) ->
if env.tyvars <> [] then "$" ^ q
else if is_len then "8"
else "i32")
g.gparams)))
g.gparams
in
struct_copy env loc name targs)
(* [(Cell 1 2)] — a struct built from its fields in declaration order.
The parser cannot make this one either, and for a sharper reason than the
bare literal above: [(Cell 1 2)] is character-for-character an ordinary call
and only the symbol table tells the two apart. So it is decided here, on the
last arm of [named_call], which is to say *after* a local of function type,
after a generic and after the global function table. Nothing can be shadowed
into a struct constructor by accident, because a name is one declaration:
[collect]'s [claimed] table spans every declaration kind, so a [defstruct
Cell] and a [defn Cell] cannot both exist. A [(defclass point [x y])]
constructor is a real [defn] that [Classes.expand] wrote before checking
began, so [(point 1 2)] resolves in [env.fns] two arms above this one and
never reaches here.
ARITY IS EXACT, and that is the decision worth writing down. ZII is not
withdrawn — it is what the designated form does, and [(Cell {.row 1})]
still zeroes [.col]. What positional construction cannot do is *say* which
field was left out: [(Cell 1)] reads as a Cell with one field given, and
which one depends on a declaration order that the author is free to change
later. A trailing field silently zeroed there is the field-reorder hazard
at its worst, so a short argument list is a refusal that names the first
field it did not reach, and points at the spelling that does mean "zero the
rest". Odin's positional literal takes the same line. *)
and positional_struct ctx ~want loc name args =
if String.equal name "String" then refuse_string_inside loc;
let s = Hashtbl.find ctx.env.structs name in
let fields = s.Tast.fields in
let n = List.length fields in
let given = List.length args in
let note = declared_note ctx.env name in
(* The constructor is written with the template's name for a generic
struct's copy, and the copy is spoken of as [(Pair i32)]. *)
let ctor =
match Hashtbl.find_opt struct_apps name with
| Some (g, _) when Hashtbl.mem ctx.env.copies name -> g
| _ -> name
in
let shown = tyname loc (Types.Named name) in
if given < n then begin
let missing = List.nth fields given in
Loc.failk "check/positional-too-few" loc ~notes:note
"%s has %d field%s and %d %s given positionally — .%s has no value. \
Positional construction gives every field, in declaration order; to \
give some of them and zero the rest, a struct value is written (%s \
{.field value ...})"
shown n (if n = 1 then "" else "s") given
(if given = 1 then "was" else "were") missing.Tast.fname ctor
end;
if given > n then begin
let extra = List.nth args n in
Loc.failk "check/positional-too-many" extra.Ast.loc ~notes:note
"%s has %d field%s, and this is argument %d — a struct value is written \
(%s {.field value ...}) or (%s %s)"
shown n (if n = 1 then "" else "s") (n + 1) ctor ctor
(String.concat " " (List.map (fun (f : Tast.field) -> f.Tast.fname) fields))
end;
(* Left to right, each against its own field's type, exactly as the argument
list of a call is checked against its parameters — same [map2_lr], same
[~want], so an untyped literal takes the field's type and a nested bare
literal (feature A above) gets an expectation here as well. The mismatch
is reported at the argument, by [expect], in the words a call's argument
already gets; what is added is a note saying which field that argument
was, because at a positional call site the field name is the one thing
the source does not show. The note is attached only to a failure raised
at this argument's own location, and it says nothing about the failure —
it names the position, which is true whatever went wrong there. *)
let fields =
map2_lr
(fun (f : Tast.field) (a : Ast.expr) ->
ctx.env.guard_next <- true;
try check ctx ~want:f.Tast.fty a with
| Loc.Error d when d.Loc.dloc = a.Ast.loc ->
refuse_or_poison ctx.env a.Ast.loc
(Loc.sort_notes
{ d with
Loc.notes =
d.Loc.notes
@ [ Loc.note a.Ast.loc
(Printf.sprintf "this is %s's field .%s" shown
f.Tast.fname) ]
@ note })
| Loc.Error d -> refuse_or_poison ctx.env a.Ast.loc d)
fields args
in
expect ctx loc ~want (mk loc (Types.Named name) (Tast.Make (name, fields)))
(* [{.f v}] with no type written in front of it, checked against whatever type
the position it stands in expects.
The whole of the feature is the one line that hands [kvs] to [check_struct]
with the expected type's name: from there a bare literal and a named one are
the same literal, checked by the same code. Duplicate fields, unknown
fields, their notes and their error kinds, and ZII zero-fill for the fields
left out are therefore not "the same rules" as the named form's — they are
the named form's, reached through the same call.
A named type is the only expectation that says anything. [Dyn] deliberately
does not: braces at a dyn want are the dyn map literal ({:key value}, and
[Ast.MapLit]) and always have been, and a [.field]-keyed brace was never
part of that spelling. So a dyn want is refused here rather than quietly
given a second meaning, and the message points at the map spelling because
that is what someone at a dyn want almost certainly wanted.
With no expectation at all there is nothing to infer from and the refusal
names both ways out: write the type, or move the literal somewhere a type
is known. *)
and check_bare ctx ~want loc kvs =
let written =
"{" ^ String.concat " " (List.map (fun (k, _) -> "." ^ k) kvs) ^ " ...}"
in
match want with
| Some (Types.Named n) -> check_struct ctx ~want loc n kvs
| Some Types.Dyn ->
Loc.failk "check/bare-struct-dyn" loc
"a dyn is expected here, and %s is a struct field list, not a dyn map — \
a dyn map's keys are keywords, as {:%s value ...}"
written
(match kvs with (k, _) :: _ -> k | [] -> "key")
| Some other ->
Loc.failk "check/bare-struct-want" loc
"%s is a struct field list and %s is expected here, which is not a \
struct type"
written (tyname loc other)
| None ->
Loc.failk "check/bare-struct-untyped" loc
"%s does not say which struct it builds — the fields alone do not name \
a type. Write it, as (Type %s), or put the literal where a type is \
already known: a function's return position, an argument of a call, a \
field of another literal, or a typed place being set. A let binding is \
none of those — a local takes its type from its value, so there is \
nothing there to read one off"
written written
(* A record-shaped literal: one form for both, because [(Name {.f v})] is the
same syntax whether [Name] is a struct or a data type case, and the two differ
only in what is built at the end. Deciding here rather than in the parser is
what lets the decision be made against the tables, exactly. *)
and check_struct ctx ~want loc name kvs =
if String.equal name "String" then refuse_string_inside loc;
match Hashtbl.find_opt ctx.env.structs name with
| None when Hashtbl.mem ctx.env.gstructs name ->
check_struct ctx ~want loc
(generic_ctor ctx ~want loc name (`Named kvs)) kvs
| None when Hashtbl.mem ctx.env.unions name ->
check_union ctx ~want loc name kvs
| None ->
(match Hashtbl.find_opt ctx.env.cases name with
(* The full spelling [U.C], which is how a data type value is written. Checked
before the diagnostics below, since the bare-name entry in the same
table is only ever a hint. *)
| Some (dname, c) when String.contains name '.' ->
check_case ctx ~want loc dname c kvs
(* A bare case name. This was a listed bug: [(A {.x 1})] on a case of a
data type reported "unknown
struct A", because nothing in [env] could tell a case name from a
misspelling. It can now, so it says what was meant. *)
| Some (dname, c) ->
fail loc
"%s is a case of the data type %s, not a struct — write (%s.%s \
{.field value ...})"
name dname dname c.Tast.vname
| None ->
if Hashtbl.mem ctx.env.datas name then
fail loc
"%s is a data type, so a value of it names a case — write (%s.%s \
{.field value ...}), for one of %s"
name name (first_case_name ctx.env name) (case_list ctx.env name)
(* [is_struct_map] (parse.ml) has two blind spots, not one: [(name
{})] reads as a struct literal with no fields regardless of what
[name] turns out to be, and so does [(name {.f v ...})] at ANY
size — a [.field]-first map is never a dyn map argument, only ever
this struct-literal shape, whatever [name] is. [(name {:a 1})] is
the one that keeps [name] an ordinary call, because a
keyword-keyed map is never mistaken for a struct literal.
The [.field]-keyed blind spot is no longer a blind spot, and that
is the argument half of the bare-literal feature. [(g {.row 2})] is
a call to [g] whose one argument is a bare struct literal — the
parameter is the expectation that names its type — and the only
reason it arrives here wearing a struct literal's clothes is that
the parser had to guess and guessed by shape. So it is handed back
to [named_call] as the call it was written as, with the fields
rebuilt into the [Ast.Bare] node the parser would have made had the
braces stood anywhere else. Only for a name that is actually
callable: an unknown name keeps the "unknown struct" report below,
because a misspelled struct name is what that shape usually is.
The empty braces keep their old refusal, because they are still
genuinely ambiguous — [{}] is the zero-field struct literal AND the
empty dyn map, with nothing in the shape to separate them — and
that one does have the let-binding fix the message names. *)
else if kvs <> [] && callable ctx name then
named_call ctx ~want loc name [ { Ast.e = Ast.Bare kvs; loc } ]
else if Hashtbl.mem ctx.env.fns name then
(match kvs with
| [] ->
fail loc
"%s is a function, not a struct — {} on its own is read as \
the zero-field struct literal, so it cannot be passed here \
as an empty map; bind it first, as (let [m {}] (%s m))"
name name
| _ ->
fail loc
"%s is a function, not a struct — {.field value ...} only \
ever builds a struct literal, never a map value, so it \
cannot be passed here as an argument; a dyn map's keys are \
keywords, as {:field value ...}"
name)
else
Loc.failk "check/unknown-struct" loc ~notes:(declared_note ctx.env name)
"unknown struct %s" name)
| Some s ->
let seen =
given_once ~noun:"field" kvs
~known:(fun k (v : Ast.expr) ->
if Tast.field_index s k = None then
Loc.failk "check/unknown-field" v.Ast.loc
~notes:(declared_note ctx.env name)
"%s has no field %s" (tyname loc (Types.Named name)) k)
in
let fields = zii_fill ctx loc seen s.Tast.fields in
expect ctx loc ~want (mk loc (Types.Named name) (Tast.Make (name, fields)))
(* [(U {.member v})] — an untagged union value.
At most one member, because the members are one storage: giving two would
be writing two values over each other and the result would be whichever the
compiler happened to store last. That is a real question with no answer, so
it is refused rather than ordered. Giving none is the ordinary ZII value and
is all-bytes-zero, the same as a struct with every field omitted.
The one member is lowered here into a zeroed temporary and a store, rather
than into a node of its own. A union value *is* a store into overlaid
storage — [Set] over [Pfield] is exactly that operation and every backend
already has it — so a [MakeUnion] node would have been the same three
instructions written a fourth and fifth time, in each backend, with the
layout rule spelled out again in each. Nothing downstream learns anything
new from this form. *)
and check_union ctx ~want loc name kvs =
let u = Hashtbl.find ctx.env.unions name in
List.iter
(fun (k, (v : Ast.expr)) ->
if Tast.field_index u k = None then
Loc.failk "check/unknown-field" v.Ast.loc
~notes:(declared_note ctx.env name)
"%s has no member %s" name k)
kvs;
(* Before the two-member refusal below, so [(U {.i 1 .i 2})] is told it named
one member twice rather than that [i] and [i] are the same bytes — which is
true and useless. Same helper, same note and the same [check/duplicate-
field] kind as the struct path, because it is the same mistake; only the
noun changes. The unknown-member refusal above stays its own full pass
rather than being handed to [~known], so that [(U {.i 1 .i 1 .bad 2})] is
still told about [.bad] first, as it is today. *)
ignore (given_once ~noun:"member" kvs : (string, Ast.expr) Hashtbl.t);
(match kvs with
| (a, _) :: (b, (second : Ast.expr)) :: _ ->
Loc.failk "check/union-two-members" second.Ast.loc
"%s is a union, so only one member can be written — give %s or %s, not \
both"
name a b
| _ -> ());
match kvs with
(* The two-member case left above, so this sees one or none. *)
| _ :: _ :: _ -> assert false
| [] -> expect ctx loc ~want (mk loc (Types.Named name) (Tast.Zero (Types.Named name)))
| [ (k, v) ] ->
let i = Option.get (Tast.field_index u k) in
let fty = (List.nth u.Tast.fields i).Tast.fty in
let v = check ctx ~want:fty v in
let slot = fresh_slot ctx (Types.Named name) in
let here = mk loc (Types.Named name) (Tast.Local slot) in
expect ctx loc ~want
(mk loc (Types.Named name)
(Tast.Let
([ (slot, mk loc (Types.Named name) (Tast.Zero (Types.Named name))) ],
[ mk loc Types.Unit (Tast.Set (Tast.Pfield (here, i), v)); here ])))
(* The cases of a data type, as written, for a message that has to name them. *)
and case_list env dname =
match Hashtbl.find_opt env.datas dname with
| None -> "its cases"
| Some u ->
String.concat ", "
(List.map (fun (c : Tast.variant) -> dname ^ "." ^ c.Tast.vname)
u.Tast.cases)
and first_case_name env dname =
match Hashtbl.find_opt env.datas dname with
| Some { Tast.cases = c :: _; _ } -> c.Tast.vname
| _ -> "Case"
(* [(U.C {.f v ...})]. The fields are checked and filled in exactly as a
struct's are — literally so: the same [given_once] and [zii_fill], with only
the index function and the name in the unknown-field message differing — and
the only other difference is the node at the end and the type it carries. *)
and check_case ctx ~want loc dname (c : Tast.variant) kvs =
let full = dname ^ "." ^ c.Tast.vname in
let seen =
given_once ~noun:"field" kvs
~known:(fun k (v : Ast.expr) ->
if Tast.vfield_index c k = None then
Loc.failk "check/unknown-field" v.Ast.loc
~notes:(declared_note ctx.env dname)
"%s has no field %s" full k)
in
let fields = zii_fill ctx loc seen c.Tast.vfields in
expect ctx loc ~want
(mk loc (Types.Named dname) (Tast.MakeCase (dname, c.Tast.vname, fields)))
(* Omitted fields are zeroed — ZII, the same rule as a declaration with no
initialiser (plan.org, Data model). Every field is present from here on, in
declaration order, so no backend has to know about omission. [seen] is what
[given_once] collected; [fields] is the declaration, and it is the
declaration that fixes the order. *)
and zii_fill ctx loc seen fields =
map_lr
(fun (f : Tast.field) ->
match Hashtbl.find_opt seen f.Tast.fname with
| Some v -> check ctx ~want:f.Tast.fty v
| None -> mk loc f.Tast.fty (Tast.Zero f.Tast.fty))
fields
and check_arr ctx ~want loc items =
let elem_want =
match want with
| Some (Types.Array (_, t)) -> Some t
| Some (Types.Slice (_, t)) -> Some t
| _ -> None
in
match elem_want, items with
| None, _ :: _ ->
(match arr_elem_type ctx items with
| Some t ->
let n = Int64.of_int (List.length items) in
expect ctx loc ~want
(check_arr ctx ~want:(Some (Types.Array (n, t))) loc items)
| None ->
(match
trial ctx (fun () ->
dyn_vec ctx loc (map_lr (fun i -> check ctx ~want:Types.Dyn i) items))
with
| Ok v -> expect ctx loc ~want v
| Error d -> mixed_refusal ctx items d))
| _ ->
let items = map_lr (fun i -> check ctx ?want:elem_want i) items in
let n = Int64.of_int (List.length items) in
let elem =
match elem_want, items with
| Some t, _ -> t
| None, first :: _ -> first.Tast.ty
| None, [] ->
fail loc
"an empty array literal needs a type — use it where one is expected, \
or name it, as in (the [0 i32] [])"
in
List.iter
(fun (i : Tast.expr) ->
if not (Types.fits ~expected:elem ~actual:i.Tast.ty) then
fail i.Tast.loc "this array's elements are %s, but this one is %s"
(tyname loc elem) (tyname loc i.Tast.ty))
items;
(match want with
| Some (Types.Array (m, _)) when not (Int64.equal m n) ->
fail loc "expected %Ld elements, found %Ld" m n
| _ -> ());
(* [n T] and [T] are distinct in type and in ownership (spec-memory.md), so
an array literal does not satisfy a slice expectation. *)
expect ctx loc ~want (mk loc (Types.Array (n, elem)) (Tast.Arr items))
(* The element type of an array literal nothing outside it names, or [None]
for a dyn vector. Every element is looked at on its own terms first, by
[probe], so nothing here is checked for real — [check_arr] does that once,
at the answer.
Elements that agree are a typed array: one type, or numbers that meet at
the wider of them the way two operands of [+] do. A literal takes the
others' type if it fits it, so [[(f32 1.0) 2.5]] is an [[2 f32]] and
[[(u8 1) 300]] an [[2 i32]]. An element that cannot be checked without
being told what it is — [None], a bare struct — takes the same type.
Elements that do not agree — [[10 "Hi"]], a dyn beside anything that is
not one — are a dyn vector, which is what the same brackets are where a
dyn is expected. Numbers that do not agree are refused instead; see
[numbers_disagree]. *)
and arr_elem_type ctx (items : Ast.expr list) : Types.t option =
let natural (i : Ast.expr) =
match i.Ast.e with
(* Refused with no want, and only a u64 holds one. *)
| Ast.UInt _ -> Some (Types.Int Types.U64)
| _ -> probe ctx i.Ast.loc (fun () -> (check ctx i).Tast.ty)
in
let fits t (i : Ast.expr) =
probe ctx i.Ast.loc (fun () -> ignore (check ctx ~want:t i)) <> None
in
let lits, rest = List.partition lone_literal items in
let typed, needs =
List.partition_map
(fun i ->
match natural i with Some t -> Left (i, t) | None -> Right i)
rest
in
let tys =
List.filter (fun t -> t <> Types.Never) (List.map snd typed)
in
let lit_tys = List.filter_map natural lits in
let join_with meet = function
| [] -> None
| t :: ts ->
List.fold_left
(fun acc t -> Option.bind acc (fun a -> meet a t)) (Some t) ts
in
let join_all = join_with Types.join in
let mixed_dyn =
List.mem Types.Dyn tys
&& (List.exists (fun t -> t <> Types.Dyn) tys || lits <> [])
in
let all_fit t = List.for_all (fits t) lits && List.for_all (fits t) needs in
(* A candidate the literals do not all fit is widened by the ones that do
not, once: [[x 2.5]] over an i32 [x] meets at f64. *)
let settle = function
| None -> None
| Some t when all_fit t -> Some t
| Some t ->
let t' =
List.fold_left
(fun acc i ->
if fits t i then acc
else
Option.bind acc (fun a -> Option.bind (natural i) (Types.join a)))
(Some t) lits
in
(match t' with
| Some t' when not (Types.equal t' t) && all_fit t' -> Some t'
| _ -> None)
in
let candidates =
if tys <> [] then [ join_all tys ]
else join_with literal_meet lit_tys :: List.map Option.some lit_tys
in
if mixed_dyn then None
else if tys = [] && lits = [] then
(match typed, needs with
| _ :: _, [] -> Some Types.Never
(* Nothing here says what any of them is. The first one's own refusal is
the one worth reading. *)
| _, first :: _ -> ignore (check ctx first); None
| [], [] -> None)
else
match
List.fold_left
(fun found c -> match found with Some _ -> found | None -> settle c)
None candidates
with
| Some t -> Some t
| None ->
let numeric t = match t with Types.Int _ | Types.Float _ -> true | _ -> false in
if needs = [] && List.for_all numeric (tys @ lit_tys) then
numbers_disagree ctx
(List.filter_map
(fun i -> Option.map (fun t -> (i, t)) (natural i)) items)
else None
(* Numbers with no type they all meet at — an i32 beside an f32, an i64 beside
a u64 — are refused rather than boxed into a dyn vector: the elements are
all numbers, and which one should move is the program's to say. The fix
named converts the second of the first disagreeing pair, into the float
when one of the two is a float and into the first's type otherwise. *)
and numbers_disagree : 'a. ctx -> (Ast.expr * Types.t) list -> 'a =
fun ctx elems ->
match elems with
| [] -> fail Loc.unknown "internal: an array of numbers with no elements"
| _ :: _ ->
(* A literal is not one of the disagreeing types when it fits the others:
each is checked at the type the rest meet at — or, with every element a
literal, at the u64 a wide one needs — and the first that does not fit
is the refusal, its own. *)
let lit (e, _) = lone_literal e in
let others = List.filter (fun p -> not (lit p)) elems in
let meet =
match others with
| [] ->
if List.exists (fun (e, _) -> match e.Ast.e with Ast.UInt _ -> true | _ -> false) elems
then Some (Types.Int Types.U64) else None
| (_, t) :: ts ->
List.fold_left (fun acc (_, u) -> Option.bind acc (fun a -> Types.join a u))
(Some t) ts
in
(match meet with
| Some (Types.Int _ as m) ->
List.iter
(fun (e, t) ->
if lone_literal e && (match t with Types.Int _ -> true | _ -> false)
then ignore (check ctx ~want:m e))
elems
| _ -> ());
let pool = if others = [] then elems else others in
let first, t1 = List.hd pool in
let second, t2 =
match List.find_opt (fun (_, t) -> Types.join t1 t = None) (List.tl pool) with
| Some p -> p
| None ->
(match List.find_opt (fun (_, t) -> Types.join t1 t = None) elems with
| Some p -> p
| None -> List.nth elems (List.length elems - 1))
in
let target, moved, moved_ty, other =
match t1, t2 with
| Types.Int _, Types.Float _ -> t2, first, t1, second
| _ -> t1, second, t2, first
in
ignore ctx;
let tn = tyname moved.Ast.loc target in
Loc.failk "check/array-numbers-disagree" moved.Ast.loc
~notes:[ Loc.note other.Ast.loc (Printf.sprintf "this element is %s" tn) ]
"this array's elements are %s and %s, and neither holds every value of \
the other — %s"
(tyname moved.Ast.loc moved_ty) tn
(match spell_arg "" moved with
| "" ->
Printf.sprintf "convert the %s element with the %s cast" (tyname moved.Ast.loc moved_ty) tn
| x -> Printf.sprintf "convert one, as in (%s %s)" tn x)
(* Elements that do not agree and cannot all become a dyn either: a struct
beside a number, a type variable beside a literal. The dyn vector's refusal
would be about dyn, which the program never mentioned, so the elements are
refused against each other instead — the first one's type is what the rest
are checked at, and the refusal points back at it. [d] is the answer if
that finds nothing. *)
and mixed_refusal : 'a. ctx -> Ast.expr list -> Loc.diag -> 'a =
fun ctx items d ->
(* Every check here only looks for a better sentence for [d]. *)
speculate ctx.env @@ fun () ->
match items with
| [] -> raise (Loc.Error d)
| first :: rest ->
let first = check ctx first in
let want = match first.Tast.ty with Types.Never -> None | t -> Some t in
List.iter
(fun (i : Ast.expr) ->
match check ctx ?want i with
| v ->
(match want with
| Some t when not (Types.fits ~expected:t ~actual:v.Tast.ty) ->
fail i.Ast.loc "this array's elements are %s, but this one is %s"
(tyname i.Ast.loc t) (tyname i.Ast.loc v.Tast.ty)
| _ -> ())
| exception Loc.Error e when e.Loc.dloc = i.Ast.loc && want <> None ->
raise
(Loc.Error
{ e with
Loc.notes =
e.Loc.notes
@ [ Loc.note first.Tast.loc
(Printf.sprintf
"this array's first element is %s, so every \
element is"
(tyname first.Tast.loc first.Tast.ty)) ] }))
rest;
raise (Loc.Error d)
(* A dyn vector built where it stands from elements already checked at dyn:
the runtime's own vec, pushed to in order. *)
and dyn_vec ctx loc (items : Tast.expr list) =
let v = fresh_slot ctx Types.Dyn in
let vval = mk loc Types.Dyn (Tast.Local v) in
let pushes =
List.map (fun x -> rt loc Types.Unit "flan_dyn_push" [ vval; x; here loc ])
items
in
mk loc Types.Dyn
(Tast.Let ([ (v, rt loc Types.Dyn "flan_dyn_vec_new" []) ], pushes @ [ vval ]))
(* ── (array-fill [r c] v) and (array-gen [r c] f) ──────────────────────
TODO.org, "A value-producing array constructor". [(array 4 T)] is
the zeroed array and [dotimes] is Unit, so between them there was no way to
write "an array of these" as an *expression* — which is what a defonce
initialiser has to be. These are that expression, at any rank.
**The lowering, and why it is not an aggregate value.** [Tast.Arr] is the
one the backends already have, and both build it element by element from a
list that is as long as the array: an [insertvalue] chain on LLVM, a store
per element on x86. A fill of [[600 800 u8]] is half a million elements and
there is no list to be had. So these lower to a *loop over a slot*: bind the
array to a slot, zero it, run one loop per dimension writing each element
through [Tast.Set] of a [Pindex], and answer with the slot. Nothing new
reaches a backend — it is [While], [Set] and [Pindex], which is the same
argument [check_loop] makes for [recur] — and both backends get the form
with no edit, the js one included.
The value stays value-like for all that: the slot is the form's own, nothing
else can name it, and the [Local] at the end is copied out exactly as any
other array-typed expression is. In a [defonce] initialiser the copy is the
store into the global that the startup function does; in a [let] it is the
binding's own store. An in-place fill of the *destination*, skipping the
temporary, would be the faster lowering and is deliberately not what this
does — the destination is not a thing an expression may know about, and
[mem2reg] plus the store-to-load forwarding both backends already get is
where that cost goes.
The slot is zeroed before the loops rather than left [Uninit]. An element
type of [dyn] is the reason it has to be: between the binding and the
store that overwrites it the collector may run, and it would read whatever
the frame happened to hold as a dyn word. The double write is the price and
it is one memset.
**Row-major, pinned.** The first dimension is the outermost loop, so
[[i][j]] runs with [j] fastest. A generator that prints, or counts, or
appends, observes that order, so it is a promise: this is the order, not
the order the nesting happened to come out in.
**Evaluated once.** The fill value and the generator *value* are each bound
to a slot before any loop starts, so [(array-fill [n] (next-id))] is one
call and n copies of its answer — not n calls. A generator's *body*, of
course, runs once per element; that is what it is for. *)
(* The dimensions, resolved by the same rule the [n T] type spelling uses —
[array_len] is literally that rule — with the one extra condition this form
has and the type spelling does not: the fill counts in i32, because every
index in the language is an i32, so a dimension that does not fit one has no
loop that could reach its end. *)
and array_dims ctx loc (dims : Ast.len list) =
List.map
(fun d ->
let n = array_len ctx.env loc d in
if n < 0L || Int64.compare n 2147483647L > 0 then
fail loc
"%Ld is not a dimension a fill can count to: an index in this \
language is an i32, and so is the loop that writes the elements"
n;
n)
dims
(* [r c] and an element type make [r [c T]], outermost first. *)
and array_of_dims ns elem =
List.fold_right (fun n t -> Types.Array (n, t)) ns elem
(* The element type an annotation asks for, peeled one [Array] per dimension.
[None] where the annotation is not an array of at least this rank: the
mismatch is then [expect]'s to report against the whole type, which is the
message that names both shapes rather than one of their leaves. *)
and array_elem_want rank want =
if rank = 0 then want
else
match want with
| Some (Types.Array (_, t)) -> array_elem_want (rank - 1) (Some t)
| _ -> None
(* The shared lowering. [pre] is bound before any loop runs — that is what
"evaluated once" means — and [element] is handed the index locals, in
dimension order, to build the value one element takes. *)
and array_build ctx loc ns elem ~pre ~element =
let aty = array_of_dims ns elem in
let arr = fresh_slot ctx aty in
let arrv = mk loc aty (Tast.Local arr) in
let islots = List.map (fun _ -> fresh_slot ctx index_ty) ns in
let ivals = List.map (fun s -> mk loc index_ty (Tast.Local s)) islots in
let zero = mk loc index_ty (Tast.Int (0L, Types.I32)) in
let one = mk loc index_ty (Tast.Int (1L, Types.I32)) in
let store =
mk loc Types.Unit (Tast.Set (Tast.Pindex (arrv, ivals), element ivals))
in
(* One [Let] and one [While] per dimension, the first dimension outermost.
The counter is bound *inside* the enclosing loop's body so that it is
re-zeroed on every pass of it, and the increment is the latch for the
reason [check_dotimes] gives. These loops carry no [break] and no
[continue], which is the condition [tast.ml] puts on a [While] the
checker invents. *)
let rec nest ns islots =
match ns, islots with
| [], [] -> store
| n :: ns, i :: islots ->
let iv = mk loc index_ty (Tast.Local i) in
let limit = mk loc index_ty (Tast.Int (n, Types.I32)) in
let cond = mk loc Types.Bool (Tast.Prim (Tast.Lt, [ iv; limit ])) in
let step =
mk loc Types.Unit
(Tast.Set (Tast.Plocal i,
mk loc index_ty (Tast.Prim (Tast.Add, [ iv; one ]))))
in
let loop =
mk loc Types.Unit (Tast.While (cond, [ nest ns islots ], [ step ]))
in
mk loc Types.Unit (Tast.Let ([ (i, zero) ], [ loop ]))
| _, _ -> fail loc "array fill: one counter per dimension"
in
mk loc aty
(Tast.Let (pre @ [ (arr, mk loc aty (Tast.Zero aty)) ],
[ nest ns islots; arrv ]))
(* (the T e): [e] with [T] as its expectation, which is every conversion an
annotation would make — a literal built at T, a narrower number widened —
and nothing more. A dyn operand is the exception: an expectation would
unbox it and trap at run time on a mismatch, and [the] is a statement about
the type rather than a conversion, so it is refused and the cast named.
[(the [T] [...])] asks for the literal's element type and answers the
[n T] the literal is, since an array literal is never a slice. *)
and check_the ctx ~want loc (t : Ast.texpr) (v : Ast.expr) =
let ty = resolve ctx.env t in
(* A typed .fln lambda, [fn(c: C) -> bool => ...], reads as [(the (Fn [C]
bool) (fn ...))]; where a CFn of the same signature is wanted, the
literal is that CFn, as an untyped one would be. *)
let ty =
match ty, v.Ast.e, want with
| Types.Fn (ps, r), Ast.Fn _, Some (Types.CFn (ps', r'))
when List.length ps = List.length ps'
&& fits_shape (Types.Fn (ps', r')) (Types.Fn (ps, r)) ->
(* A generic's [CFn($t) -> $t] binds [$t] from the literal's own
types, as it would from any other argument's. *)
Types.CFn (ps, r)
| _ -> ty
in
let is_nil = match v.Ast.e with Ast.Var "nil" -> true | _ -> false in
(* Quietly: what [v] is on its own terms is not a use of a literal local
inside it — [[x]] read with no want would pin x at its guess, and the
annotation's want below is the use that says what x is. *)
let own_ty () =
let was = !lit_quiet in
lit_quiet := true;
Fun.protect ~finally:(fun () -> lit_quiet := was) (fun () ->
probe ctx loc (fun () -> (check ctx v).Tast.ty))
in
if ty <> Types.Dyn && not is_nil
&& own_ty () = Some Types.Dyn
(* A keyword naming one of an enum's members is that member at the
enum's type, not a dyn: [let d: Dir = :north]. *)
&& not (match ty, v.Ast.e with Types.Enum _, Ast.Kw _ -> true | _ -> false)
(* An array literal that is a dyn vector only because its elements do
not agree among themselves — [[1, None]] — is built at the annotation
when every element fits it, as [x: [2 i32?] = [1, None]] (decision
138). Nothing is converted: the literal is built at T. *)
&& not (let rec holds_option = function
| Types.Option _ -> true
| Types.Array (_, t) | Types.Slice (_, t) -> holds_option t
| _ -> false
in
let rec elems_option = function
| Types.Array (_, t) | Types.Slice (_, t) -> holds_option t
| Types.Option t -> elems_option t
| _ -> false
in
match v.Ast.e with
| Ast.Arr _ when elems_option ty ->
probe ctx loc (fun () -> ignore (check ctx ~want:ty v)) <> None
| _ -> false)
then begin
let tn = tyname loc ty in
let numeric = match ty with Types.Int _ | Types.Float _ -> true | _ -> false in
(* In a .fln file the user wrote [x: T = v], not [the]. *)
let fln = fln_source loc in
let the_ = if fln then "a type annotation" else "the" in
if numeric then
fail v.Ast.loc
"%s checks a value as %s and does not convert one, and this is a dyn \
— %s"
the_ tn
(match spell_arg "" v with
| s when fln && s <> "" && not (String.exists (fun c -> c = ' ' || c = '(') s) ->
Printf.sprintf "write %s(%s) to convert it" tn s
| s when s = "" || fln -> Printf.sprintf "convert it with the %s cast instead" tn
| s -> Printf.sprintf "write (%s %s) to convert it" tn s)
else
(* What a dyn does at this type is the boundary's own answer, asked of
it rather than restated: some types take one where a value is passed,
returned or stored, and the rest do not take one at all. *)
let crosses =
probe ctx loc (fun () ->
ignore (expect ctx v.Ast.loc ~want:(Some ty) (check ctx v)))
in
match crosses with
| Some () ->
fail v.Ast.loc
"%s checks a value as %s and does not convert one, and this is a \
dyn — a dyn becomes a %s where a %s is passed, returned or stored"
the_ tn tn tn
| None ->
(match speculate ctx.env (fun () -> check ctx ~want:ty v) with
| _ ->
fail v.Ast.loc
"%s checks a value as %s and does not convert one, and this is \
a dyn" the_ tn
| exception Loc.Error d ->
fail v.Ast.loc
"%s checks a value as %s and does not convert one, and this is \
a dyn — %s" the_ tn d.Loc.dmsg)
end;
let r =
match ty, v.Ast.e with
| Types.Slice (_, elem), Ast.Arr items ->
check_arr ctx
~want:(Some (Types.Array (Int64.of_int (List.length items), elem)))
v.Ast.loc items
| _ -> expect ctx v.Ast.loc ~want:(Some ty) (check ctx ~want:ty v)
in
(* (the dyn nil) is a dyn value that holds nil, not the literal: at a typed
want it traps at run time as any dyn holding nil does, where a bare nil
is refused. (the dyn 1) is likewise a dyn, not a literal that brings
nothing dyn to an operator ([no_bare_nil]). [is_nil_lit] and
[boxed_literal] see through nothing, so the [Do] hides both. *)
let r =
if ty = Types.Dyn && (is_nil_lit r || boxed_literal r <> None) then
mk loc Types.Dyn (Tast.Do [ r ])
else r
in
expect ctx loc ~want r
(* [f] run for its answer alone: whatever it wrote into the context is put
back whether it succeeded or not, so a form can be checked once to see what
it is and then checked again for real. [None] if it was refused. *)
and probe : 'a. ctx -> Loc.t -> (unit -> 'a) -> 'a option = fun ctx loc f ->
let answer = ref None in
(match
trial ctx (fun () ->
answer := Some (f ());
raise (Loc.Error (Loc.diag loc "probe")))
with
| _ -> ());
!answer
and check_array_fill ctx ~want loc dims v =
let ns = array_dims ctx loc dims in
let elem_want = array_elem_want (List.length ns) want in
(* The annotation's element type is the [want] the value is checked against,
so a disagreement is reported at the value, in the ordinary
expected/found words, rather than as a whole-array mismatch a line up. *)
let v = check ctx ?want:elem_want v in
let elem = match elem_want with Some t -> t | None -> v.Tast.ty in
(* [resolve] refuses a fixed array of function values, because the elements
this form does not write would be zeroed and a zeroed function value is a
null pointer. The type is built here without going through [resolve], so
the same guard has to be asked here. *)
no_zeroed_fn loc "a fixed array's element" elem;
let vs = fresh_slot ctx elem in
let vv = mk loc elem (Tast.Local vs) in
expect ctx loc ~want
(array_build ctx loc ns elem ~pre:[ (vs, v) ] ~element:(fun _ -> vv))
and check_array_gen ctx ~want loc dims f =
let ns = array_dims ctx loc dims in
let rank = List.length ns in
let plural n = if n = 1 then "" else "s" in
let f =
match f.Ast.e with
(* The canonical inline form, [(array-gen [3 4] (fn [i j] ...))]. On its
own [check] would refuse the fn — it takes its types from its position,
and only an argument position names them — but *this* position knows
them just as well: one i32 index per dimension, and the annotated
element type as the return where the annotation reaches this deep.
With no annotation the return is left for the body to say, which is
the same inference the fill value gets. Arity is checked here so the
refusal talks about dimensions and indices, not about parameters some
(Fn ...) want expected. *)
| Ast.Fn (ps, fbody) ->
let got = List.length ps in
if got <> rank then
fail f.Ast.loc
"this array-gen has %d dimension%s, so its generator is called with \
%d index%s — and this one takes %d argument%s"
rank (plural rank) rank
(if rank = 1 then "" else "es") got (plural got);
check_fn ctx ~want:None
~gen:(List.init rank (fun _ -> index_ty), array_elem_want rank want)
f.Ast.loc ps fbody
| _ -> check ctx f
in
let elem =
(* Either function type: the generator is called and nothing here cares
whether an environment rides along. *)
match fn_sig f.Tast.ty with
| Some (ps, r) ->
let got = List.length ps in
if got <> rank then
fail f.Tast.loc
"this array-gen has %d dimension%s, so its generator is called with \
%d index%s — and this one takes %d argument%s"
rank (plural rank) rank
(if rank = 1 then "" else "es") got (plural got);
List.iteri
(fun k p ->
if not (Types.equal p index_ty) then
fail f.Tast.loc
"an index is an i32, and this generator's argument %d is %s"
(k + 1) (tyname loc p))
ps;
r
| None ->
fail f.Tast.loc
"array-gen's second element is a function value, called once per \
element with one i32 index per dimension, and this is %s — for one \
value repeated, write array-fill"
(tyname loc f.Tast.ty)
in
no_zeroed_fn loc "a fixed array's element" elem;
let fs = fresh_slot ctx f.Tast.ty in
let fv = mk loc f.Tast.ty (Tast.Local fs) in
expect ctx loc ~want
(array_build ctx loc ns elem ~pre:[ (fs, f) ]
~element:(fun idxs -> mk loc elem (Tast.CallPtr (fv, idxs))))
and check_match ctx ?(tail = false) ?(used = false) ?(stmt = false) ?(opt = false)
?(flat = false) ?opt_rest ?want loc scrutinee arms =
(* [stmt] is an [if let] with no else: a statement, Unit whatever its arm
answers, as a one-armed [if] is when nothing keeps it. [opt] is one that
is kept: its arm answers [Some], and the arm with no body [None]. *)
let used = used && not stmt in
let arms_ast_for_opt = arms in
let want0 = want in
let want =
if stmt then None
else if opt then
(match want with
| Some (Types.Option _) when flat -> want
| Some (Types.Option i) -> Some i
| _ -> None)
else want
in
let s = check ctx scrutinee in
(* What the arms are alternatives over. An [Option] is a two-case data type
wearing a special coat, so the two shapes below are the same shape: a set
of case names, an arity and a payload type per case, and a tag. Keeping
them apart here rather than desugaring [Option] into a declared data type is
deliberate — [Option] is generic and no declared data type is, so the coat is
the part that cannot yet be taken off. *)
let subject =
match s.Tast.ty with
| Types.Option t -> `Option t
| Types.Named n when Hashtbl.mem ctx.env.datas n ->
`Data (Hashtbl.find ctx.env.datas n)
(* An enum is an i32 at run time and its members are all known, so the
arms are a chain of [=] over a temporary, built at the foot of this
function — a desugaring, not a new IR node. The exhaustiveness check is
the one a data type gets. *)
| Types.Enum n -> `Enum (n, Hashtbl.find ctx.env.enums n)
(* An untagged union has nothing for the arms to be alternatives over.
This is not a milestone and not a missing lowering: [match] reads a tag
and decides, and the absence of a tag is the whole definition of this
type. Said by name, because the two kinds of union are one keyword
apart in the source and someone will write it. *)
| Types.Named n when Hashtbl.mem ctx.env.unions n ->
fail loc
"%s is a union, and nothing in one records which member was written, \
so there is nothing to match on. Read the member you mean with \
(.member u), or use a defdata" n
(* A number, a string or a dyn: the arms are literals, and each is the
test (= t lit) over one temporary — the enum's chain, with [=]'s own
two lowerings for the test, so a match over a dyn means what [=] over
it means. [is_equatable]'s set minus the enums, which are above, and
minus bool, below. *)
| (Types.Int _ | Types.Float _ | Types.Char | Types.String | Types.Dyn) as t -> `Lit t
(* A bool is a two-member enum spelled true and false: the same chain,
and exhaustive without a [_] once both are named. *)
| Types.Bool -> `Bool
| other ->
fail loc
"match works on an Option, a data type, an enum, a bool, a number, a \
string or a dyn, not on %s"
(tyname loc other)
in
(* A literal arm, spelled as it was written, for the refusals that name one. *)
let spell (e : Ast.expr) =
match e.Ast.e with
| Ast.Int n -> Int64.to_string n
| Ast.UInt (_, t) -> t
| Ast.Float x when Float.is_integer x && Float.abs x < 1e15 ->
Printf.sprintf "%.1f" x
| Ast.Float x ->
(* The shortest spelling that reads back as the same float. *)
let rec go p =
let t = Printf.sprintf "%.*g" p x in
if p >= 17 || float_of_string t = x then t else go (p + 1)
in
go 1
| Ast.Byte b -> Form.byte_repr b
| Ast.Str t -> Printf.sprintf "%S" t
| Ast.Kw k -> ":" ^ k
| Ast.Var b -> b
| _ -> "this literal"
in
let what_ty t = match t with Types.Dyn -> "a dyn" | t -> tyname loc t in
(* A literal match that compiles, over the scrutinee's own name where it
has one, for the refusals that need to show the shape. *)
let lit_arms_fix t =
let name =
match scrutinee.Ast.e with Ast.Var n -> n | _ -> "t"
in
Printf.sprintf "(match %s %s)" name
(match t with
| Types.String -> "\"yes\" 1 _ 0"
| Types.Float _ -> "0.5 1 _ 0"
| Types.Dyn -> "5 1 :go 2 _ 0"
| _ -> "5 1 _ 0")
in
let bool_fix () =
let name = match scrutinee.Ast.e with Ast.Var n -> n | _ -> "b" in
Printf.sprintf "(match %s true 1 false 0)" name
in
(* The checked literal of each literal arm, by the key [resolve_pat] gave it. *)
let lits : (string, Tast.expr) Hashtbl.t = Hashtbl.create 8 in
let lit_values = ref [] in
(* A literal arm over [t]: its checked value under a fresh key in [lits],
refused if [t] cannot hold it or an earlier arm already equals it. *)
let lit_arm (a : Ast.arm) t (e : Ast.expr) =
let v =
(* [=]'s dyn pair checks its literal at dyn, which boxes it. *)
match trial ctx (fun () -> check ctx ~want:t e) with
| Ok v -> v
| Error _ ->
(* A literal that does not fit is refused, where [=] would widen
the pair and let the arm quietly never match. The literal's
own refusal is not repeated: its fixes are casts, and a cast
is not a pattern. *)
(match t with
| Types.Dyn ->
fail a.Ast.aloc
"this match is over a dyn, which holds a number as an i64 or \
an f64, and %s fits in neither. Change the arm to a value an \
i64 holds, or remove it" (spell e)
| _ -> ());
let tn = tyname loc t in
let an =
match tn.[0] with
| 'a' | 'e' | 'f' | 'i' | 'o' -> "an " ^ tn
| _ -> "a " ^ tn
in
let why =
match e.Ast.e, t with
| Ast.Str _, _ -> "is a string"
| _, Types.String -> "is a number"
| (Ast.Int _ | Ast.UInt _ | Ast.Float _), Types.Char -> "is a number"
| Ast.Float x, Types.Int _ when not (Float.is_integer x) ->
"is not a whole number"
| Ast.Float _, Types.Int _ -> "is a float"
| _ -> "does not fit in one"
in
fail a.Ast.aloc
"this match is over %s, so each arm has to be %s, and %s %s. \
Change the arm to a value %s holds, or remove it"
tn an (spell e) why an
in
(* The arm's value at the scrutinee's type, and a second arm [=] could
not tell from an earlier one is refused, since it can never be
reached: 97 and \a are one u8, 0.1 and 0.10000000001 are one f32,
and over a dyn 1 and 1.0 are equal. Compared pairwise rather than
hashed, because dyn = between an integer and a float goes through
the float and is not transitive past 2^53. *)
let value =
let f32 x = Int32.float_of_bits (Int32.bits_of_float x) in
let num x =
match t with Types.Float Types.F32 -> `F (f32 x) | _ -> `F x
in
match e.Ast.e, t with
| Ast.Str s, _ -> `S s
| (Ast.Int n | Ast.UInt (n, _)), Types.Float _ -> num (Int64.to_float n)
| Ast.Byte b, Types.Float _ -> num (float_of_int b)
| (Ast.Int n | Ast.UInt (n, _)), _ -> `I n
| Ast.Byte b, _ -> `I (Int64.of_int b)
| Ast.Float x, _ -> num x
| Ast.Kw k, _ -> `K k
| Ast.Var b, _ -> `B b
| _ -> assert false
in
let same x y =
match x, y with
| `I a, `I b -> Int64.equal a b
| `F a, `F b -> a = b
| `I a, `F b | `F b, `I a -> Int64.to_float a = b
| `S a, `S b | `K a, `K b | `B a, `B b -> String.equal a b
| _ -> false
in
(match List.find_opt (fun (w, _) -> same value w) !lit_values with
| Some (_, earlier) when earlier = spell e ->
fail a.Ast.aloc "this match has two %s arms" earlier
| Some (_, earlier) ->
fail a.Ast.aloc
"this match has two %s arms — %s equals it as %s, so this arm is \
never reached. Remove it"
earlier (spell e)
(match t with
| Types.Dyn -> "a dyn"
| t ->
let tn = tyname loc t in
(match tn.[0] with
| 'a' | 'e' | 'f' | 'i' | 'o' -> "an " ^ tn
| _ -> "a " ^ tn))
| None -> ());
lit_values := (value, spell e) :: !lit_values;
let key = string_of_int (Hashtbl.length lits) in
Hashtbl.replace lits key v;
Some key, []
in
(* Which case each arm names, and the type of each name it binds. This is the
whole of what differs between the two subjects; everything below it is
shared. *)
let resolve_pat (a : Ast.arm) =
match subject, a.Ast.pat with
| _, Ast.Pwild -> None, []
| `Option elem, Ast.Pctor ("Some", [ x ]) -> Some "Some", [ (x, elem) ]
| `Option _, Ast.Pctor ("Some", _) ->
fail a.Ast.aloc "the Some pattern binds exactly one name"
| `Option _, Ast.Pctor ("None", []) -> Some "None", []
| `Option _, Ast.Pctor ("None", _) -> fail a.Ast.aloc "None binds no names"
| `Option _, Ast.Pctor (c, _) ->
fail a.Ast.aloc
"%s is not a case of Option — the cases are Some and None" c
| `Enum (n, members), Ast.Pkw k ->
if not (List.mem_assoc k members) then begin
let all =
String.concat " " (List.map (fun (m, _) -> ":" ^ m) members)
in
match member_near_miss members k with
| Some (m, _) ->
fail a.Ast.aloc "%s has no member :%s — did you mean :%s? It has %s"
n k m all
| None -> fail a.Ast.aloc "%s has no member :%s — it has %s" n k all
end;
Some k, []
(* [Dir.north ->], the member named through its enum. *)
| `Enum (n, members), Ast.Pctor (c, [])
when String.length c > String.length n + 1
&& String.sub c 0 (String.length n + 1) = n ^ "." ->
let k = String.sub c (String.length n + 1) (String.length c - String.length n - 1) in
if not (List.mem_assoc k members) then
fail a.Ast.aloc "%s has no member %s — it has %s" n k
(String.concat " " (List.map (fun (m, _) -> n ^ "." ^ m) members));
Some k, []
| `Enum (n, members), Ast.Pctor (c, _) ->
fail a.Ast.aloc
"this match is over the enum %s, and %s is not one of its members. An \
arm names a member as a keyword: %s" n c
(String.concat " " (List.map (fun (m, _) -> ":" ^ m) members))
| `Lit t, Ast.Plit e -> lit_arm a t e
(* Over a dyn a keyword is a value like any other, so :north is the arm
(= d :north), and true and false are the arms (= d true) and
(= d false). *)
| `Lit Types.Dyn, Ast.Pkw k ->
lit_arm a Types.Dyn { Ast.e = Ast.Kw k; loc = a.Ast.aloc }
| `Lit Types.Dyn, Ast.Pctor (("true" | "false") as b, []) ->
lit_arm a Types.Dyn { Ast.e = Ast.Var b; loc = a.Ast.aloc }
| `Bool, Ast.Pctor (("true" | "false") as b, []) -> Some b, []
| `Bool, Ast.Pctor (c, _) ->
fail a.Ast.aloc
"%s names a case, and this match is over a bool, whose arms are true \
and false, as in %s" c (bool_fix ())
| `Bool, Ast.Pkw k ->
fail a.Ast.aloc
":%s is a keyword, and this match is over a bool, whose arms are true \
and false, as in %s" k (bool_fix ())
| `Bool, Ast.Plit e ->
fail a.Ast.aloc
"%s is a literal, and this match is over a bool, whose arms are true \
and false, as in %s" (spell e) (bool_fix ())
| `Lit t, Ast.Pkw k ->
fail a.Ast.aloc
":%s is an enum member, and this match is over %s, whose arms are \
literals, as in %s" k (what_ty t) (lit_arms_fix t)
| `Lit t, Ast.Pctor (c, _) ->
fail a.Ast.aloc
"%s names a case, and this match is over %s, whose arms are \
literals, as in %s" c (what_ty t) (lit_arms_fix t)
| `Option _, Ast.Plit e ->
fail a.Ast.aloc
"%s is a literal, and this match is over an Option, whose arms are \
(Some x) and None" (spell e)
| `Enum (n, members), Ast.Plit e ->
fail a.Ast.aloc
"%s is a literal, and this match is over the enum %s, whose arms \
name its members as keywords: %s" (spell e) n
(String.concat " " (List.map (fun (m, _) -> ":" ^ m) members))
| `Data u, Ast.Plit e ->
fail a.Ast.aloc
"%s is a literal, and this match is over the data type %s, whose \
arms name its cases: %s" (spell e) u.Tast.dname
(String.concat ", "
(List.map (fun (v : Tast.variant) -> v.Tast.vname) u.Tast.cases))
| `Option _, Ast.Pkw k ->
fail a.Ast.aloc
":%s is an enum member, and this match is over an Option, whose arms \
are (Some x) and None" k
| `Data u, Ast.Pkw k ->
fail a.Ast.aloc
":%s is an enum member, and this match is over the data type %s, \
whose arms name its cases: %s" k u.Tast.dname
(String.concat ", "
(List.map (fun (v : Tast.variant) -> v.Tast.vname) u.Tast.cases))
| `Data u, Ast.Pctor (c, names) ->
(* A pattern names the case bare: the scrutinee's type already says which
data type, so [(Node l r)] is unambiguous even where two data types
share the case name. The qualified spelling is accepted too, since
that is how
the value was written and writing it again should not be an error. *)
let bare =
let full = u.Tast.dname ^ "." in
let n = String.length full in
if String.length c > n && String.sub c 0 n = full then
String.sub c n (String.length c - n)
else c
in
(match Tast.case_index u bare with
| None ->
fail a.Ast.aloc "%s is not a case of %s — the cases are %s" c
u.Tast.dname
(String.concat ", "
(List.map (fun (v : Tast.variant) -> v.Tast.vname) u.Tast.cases))
| Some (_, v) ->
(* Positional, in declaration order, and all of them or none: a
pattern that bound some of a case's fields would be silently
reading the wrong one after a field is inserted. Refused with the
count, which is the thing that is wrong. *)
if List.length names <> List.length v.Tast.vfields then
fail a.Ast.aloc
"%s.%s has %d field%s, and this pattern binds %d — a case pattern \
binds every field, in declaration order (%s)"
u.Tast.dname bare (List.length v.Tast.vfields)
(if List.length v.Tast.vfields = 1 then "" else "s")
(List.length names)
(String.concat " "
(List.map (fun (f : Tast.field) -> f.Tast.fname)
v.Tast.vfields));
Some bare,
List.map2 (fun n (f : Tast.field) -> (n, f.Tast.fty))
names v.Tast.vfields)
in
let want = ref want in
let seen = Hashtbl.create 8 in
let saw_wild = ref false in
let resolved =
map_lr
(fun (a : Ast.arm) ->
let ctor, binds = resolve_pat a in
(match ctor with
| None -> saw_wild := true
| Some c ->
if Hashtbl.mem seen c then
fail a.Ast.aloc "this match has two %s arms"
(match subject, a.Ast.pat with
| `Enum _, _ -> ":" ^ c
| _, Ast.Plit e -> spell e
| _ -> c);
Hashtbl.add seen c ());
(a, ctor, binds))
arms
in
(* With nothing expected of the match, the first arm's type is every arm's —
unless that arm is a bare literal, which has no type until asked. So the
arms whose value is a literal are checked last, and take their type from
the others, as an [if]'s literal arm does. The order is only the order
they are checked in; they are put back in source order below. *)
let literal_arm ((a : Ast.arm), _, _) =
match List.rev a.Ast.body with last :: _ -> adapts last | [] -> false
in
(* Every arm a literal: they meet at the wider of their own types, as an
[if]'s two do. *)
(if !want = None && resolved <> [] && List.for_all literal_arm resolved then
let lasts =
List.map (fun ((a : Ast.arm), _, _) -> List.hd (List.rev a.Ast.body))
resolved
in
match lasts with
| first :: rest ->
let j =
List.fold_left
(fun acc x ->
Option.bind acc (fun a ->
Option.bind (literal_join ctx first x) (literal_meet a)))
(literal_join ctx first first) rest
in
(match j with
| Some t when not (Types.equal t (Types.Int Types.I32)) -> want := Some t
| _ -> ())
| [] -> ());
(* With nothing expected, the arms meet at [arm_join], as an [if]'s do, in
whatever order they are written: each typed arm is checked on its own
terms, the join so far grows with it, and every arm is brought to the
final join at the end. An arm that needs an expectation — [nil], a bare
struct — is checked at the join so far, as it was at the first arm's
type before. *)
let free = !want = None in
let order =
let idx = List.mapi (fun i r -> (i, r)) resolved in
if !want <> None then idx
else
let typed = List.filter (fun (_, r) -> not (literal_arm r)) idx in
(* A bare [None] that would be checked first has nothing to take its
type from and was always refused; it goes after the others, so it
meets their T at T? (decision 138). Only the leading ones move, so
no order that checked before changes. *)
let none_arm (_, ((a : Ast.arm), _, _)) =
match List.rev a.Ast.body with last :: _ -> is_none_lit last | [] -> false
in
let rec lead acc = function
| x :: rest when none_arm x -> lead (x :: acc) rest
| rest -> (List.rev acc, rest)
in
let nones, typed =
match lead [] typed with
| _ :: _ as nones, rest when List.length nones < List.length idx -> nones, rest
| _ -> [], typed
in
typed @ List.filter (fun (_, r) -> literal_arm r) idx @ nones
in
let checked =
map_lr
(fun (i, ((a : Ast.arm), ctor, binds)) ->
i,
branch ctx (fun () ->
(* What each name in this arm is, in words, for the one refusal
that needs it: a case pattern binds fields positionally, so the
i'th name is the i'th field of the case the arm named. Derived
here rather than carried out of [resolve_pat], because the case
and the subject are both still in hand and the alternative was
widening that function's result for one message. *)
let fields =
match subject, ctor with
| `Data u, Some c ->
(match Tast.case_index u c with
| Some (_, v) ->
List.map
(fun (fd : Tast.field) ->
Printf.sprintf "%s.%s's field %s" u.Tast.dname c
fd.Tast.fname)
v.Tast.vfields
| None -> [])
| `Option _, Some "Some" -> [ "the Option's payload" ]
| _ -> []
in
let binds =
List.mapi
(fun i (n, ty) ->
let what = List.nth_opt fields i in
bind ctx ?what n ty ~assignable:false)
binds
in
(* Every arm is the tail, exactly as an [if]'s two arms are.
Restored here because checking the scrutinee withdrew it. *)
ctx.tail <- tail;
ctx.used <- used;
let arm = (a, ctor, binds) in
let empty = opt && a.Ast.body = [] in
let body =
if empty then unit_at a.Ast.aloc else
if free && !want <> None && not (literal_arm arm) then
(* As an [if]'s else arm: at the join so far first, on its
own terms only when that is refused as a mismatch. *)
let at w () = block ctx ?want:w a.Ast.aloc a.Ast.body in
let w = Option.get !want in
let head = List.hd a.Ast.body in
let at_join () =
match
List.find_opt
(fun (n, (sc, r), w', _) ->
n == head && r == ctx.ret && Types.equal w' w
&& same_scope sc ctx.scope)
(Hashtbl.find_all arm_failed head.Ast.loc)
with
| Some (_, _, _, d) -> Error d
| None ->
(match trial ctx (at (Some w)) with
| Ok b -> Ok b
| Error d ->
if !lit_recording = 0 then Hashtbl.add arm_failed head.Ast.loc
(head, (ctx.scope, ctx.ret), w, d);
Error d)
in
(* Where it would be refused: an arm fine at T? — [None],
[Some(1)] — meets a T join at T? (decision 138), and
[arm_join] wraps the arms before it. *)
let refused () =
let fallback () = at !want () in
match w with
| Types.Option _ | Types.Dyn | Types.Unit -> fallback ()
| _ ->
let oty = Types.Option w in
(match trial ctx (at (Some oty)) with
| Ok b when Types.equal b.Tast.ty oty -> b
| _ -> fallback ())
in
match at_join () with
| Ok b ->
(match opened_dyn ~box:(to_dyn ctx) b with
| Some box -> want := Some Types.Dyn; box
| None -> b)
| Error d ->
(match
trial ctx (fun () ->
let b = at None () in
if is_mismatch d || Types.equal b.Tast.ty Types.Dyn then b
else
raise (Loc.Error (Loc.diag ~kind:not_kept b.Tast.loc "")))
with
| Ok b -> b
| Error own when String.equal own.Loc.kind not_kept ->
refused ()
| Error own when is_mismatch d && not (is_mismatch own) ->
at None ()
| Error _ -> refused ())
else block ctx ?want:!want a.Ast.aloc a.Ast.body
in
let body =
if stmt && body.Tast.ty <> Types.Unit && body.Tast.ty <> Types.Never
then mk body.Tast.loc Types.Unit (Tast.Do [ body; unit_at body.Tast.loc ])
else body
in
(if body.Tast.ty <> Types.Never && not stmt && not empty then
match !want with
| None -> want := Some body.Tast.ty
| Some w when free ->
(match arm_join w body.Tast.ty with
| Some j -> want := Some j
| None -> ())
| Some _ -> ());
{ Tast.acase = ctor; binds; abody = [ body ] }))
order
in
let checked =
match free, !want with
| true, Some j ->
(* Said at the arm's value, its last form, as a refusal checked at the
join would have been — not at its pattern. *)
let rec value (x : Ast.expr) =
match x.Ast.e with
| Ast.Do (_ :: _ as xs) | Ast.Let (_, (_ :: _ as xs)) ->
value (List.hd (List.rev xs))
| _ -> x.Ast.loc
in
let value_loc i =
let (a : Ast.arm), _, _ = List.nth resolved i in
match List.rev a.Ast.body with
| x :: _ -> value x
| [] -> a.Ast.aloc
in
(* Each arm refused on its own, so every arm that cannot meet the join
is said, as each would be checked at it. *)
List.map
(fun (i, (arm : Tast.arm)) ->
let empty =
opt && (let (a : Ast.arm), _, _ = List.nth resolved i in a.Ast.body = [])
in
match arm.Tast.abody with
| [ b ] when not (empty || Types.equal b.Tast.ty j || b.Tast.ty = Types.Never) ->
let at = value_loc i in
let b =
try expect ctx at ~want:(Some j) b
with Loc.Error d -> refuse_or_poison ctx.env at d
in
(i, { arm with Tast.abody = [ b ] })
| _ -> (i, arm))
checked
| _ -> checked
in
let arms =
List.map snd (List.sort (fun (i, _) (j, _) -> compare i j) checked)
in
(* [opt]: what the pattern's arm answered decides the whole, as a
one-armed [if]'s branch does — no value is a statement, Never stays
Never, a dyn is the value or nil, and anything else is [Some] of it. The
arm with no body is the rest of the chain, [opt_rest], checked now that
its want is known, or [None] when the chain ends here. *)
let opt_result = ref None in
let arms =
if not opt then arms
else
let raw =
List.fold_left2
(fun acc (a : Ast.arm) (arm : Tast.arm) ->
match a.Ast.body, arm.Tast.abody with
| _ :: _, [ b ] -> Some b.Tast.ty
| _ -> acc)
None arms_ast_for_opt arms
in
let rest ~used ?want () =
match opt_rest with
| Some f -> Some (f ~used ?want ())
| None -> None
in
let fill body_of wild_of =
List.map2
(fun (a : Ast.arm) (arm : Tast.arm) ->
match a.Ast.body, arm.Tast.abody with
| [], _ -> { arm with Tast.abody = [ wild_of a ] }
| _, [ b ] -> { arm with Tast.abody = [ body_of b ] }
| _ -> arm)
arms_ast_for_opt arms
in
match raw with
| None | Some Types.Unit ->
opt_result := Some Types.Unit;
let r = rest ~used:false () in
fill Fun.id (fun a ->
match r with
| Some e when e.Tast.ty = Types.Unit || e.Tast.ty = Types.Never -> e
| Some e -> mk e.Tast.loc Types.Unit (Tast.Do [ e; unit_at e.Tast.loc ])
| None -> unit_at a.Ast.aloc)
| Some Types.Never ->
(match rest ~used:true ?want:want0 () with
| Some e ->
opt_result := Some e.Tast.ty;
fill Fun.id (fun _ -> e)
| None ->
(match want0 with
| Some (Types.Option _ as o) ->
opt_result := Some o;
fill Fun.id (fun a -> mk a.Ast.aloc o Tast.None_)
| _ ->
opt_result := Some Types.Unit;
fill Fun.id (fun a -> unit_at a.Ast.aloc)))
| Some Types.Dyn ->
opt_result := Some Types.Dyn;
let r = rest ~used:true ~want:Types.Dyn () in
fill Fun.id (fun a ->
match r with
| Some e -> e
| None -> rt a.Ast.aloc Types.Dyn "flan_dyn_nil" [])
(* Already an Option: the whole, one level flattened (decision 140). *)
| Some (Types.Option _ as o) when flat || want0 = None ->
opt_result := Some o;
let r = rest ~used:true ~want:o () in
fill Fun.id (fun a ->
match r with
| Some e -> e
| None -> mk a.Ast.aloc o Tast.None_)
| Some t ->
let oty = Types.Option t in
opt_result := Some oty;
let r = rest ~used:true ~want:oty () in
fill (fun b -> mk b.Tast.loc oty (Tast.Some_ b)) (fun a ->
match r with
| Some e -> e
| None -> mk a.Ast.aloc oty Tast.None_)
in
(* Exhaustiveness is refused, not defaulted. A match that silently fell
through would have to produce a value of the match's type out of nothing,
and there is no such value for most types; and the case a data type grows
tomorrow is exactly the one a reader wants to be told about today. A [_]
arm is the way to say "the rest", written where it can be seen. *)
let missing =
match subject with
| `Option _ -> List.filter (fun c -> not (Hashtbl.mem seen c)) [ "Some"; "None" ]
| `Data u ->
List.filter_map
(fun (c : Tast.variant) ->
if Hashtbl.mem seen c.Tast.vname then None
else Some (u.Tast.dname ^ "." ^ c.Tast.vname))
u.Tast.cases
| `Enum (_, members) ->
List.filter_map
(fun (m, _) -> if Hashtbl.mem seen m then None else Some (":" ^ m))
members
| `Bool ->
List.filter (fun c -> not (Hashtbl.mem seen c)) [ "true"; "false" ]
| `Lit _ -> []
in
(* No list of literals covers a number, a string or a dyn, so a literal
match always needs its [_]. Refused here, before the chain below, which
would otherwise run a lone last arm untested as the enum's does. *)
(match subject with
| `Lit t when not !saw_wild ->
Loc.failk "check/non-exhaustive-match" loc
"this match is not exhaustive — its arms are literals, and no list of \
them covers every %s. Add a _ arm for the rest, as in %s"
(match t with Types.Dyn -> "dyn value" | t -> tyname loc t)
(lit_arms_fix t)
| _ -> ());
if not !saw_wild && missing <> [] then
(* The data type's declaration, because that is where the case list this match
failed to cover actually lives, and because adding a case there is what
makes a match non-exhaustive in the first place. *)
Loc.failk "check/non-exhaustive-match" loc
~notes:(match subject with
| `Data u -> declared_note ctx.env u.Tast.dname
| `Enum (n, _) -> declared_note ctx.env n
| `Option _ | `Bool | `Lit _ -> [])
"this match is not exhaustive — %s %s no arm. Add %s, or a _ arm for \
the rest"
(String.concat ", " missing)
(if List.length missing = 1 then "has" else "have")
(if List.length missing = 1 then "it" else "them");
let ty =
if stmt then Types.Unit
else if opt then (match !opt_result with Some t -> t | None -> Types.Never)
else match !want with Some t -> t | None -> Types.Never
in
match subject with
| `Option _ | `Data _ -> mk loc ty (Tast.Match (s, arms))
| `Enum _ | `Bool | `Lit _ ->
(* The scrutinee once, into a temporary, and then an [if] per arm in the
order written. A [_] arm ends the chain, and so does the last arm of a
match with none: it is exhaustive by the check above, so the last
member's test is the only one left and cannot fail on a value the enum
declares. *)
let slot = fresh_slot ctx s.Tast.ty in
let local = mk loc s.Tast.ty (Tast.Local slot) in
let body (a : Tast.arm) =
match a.Tast.abody with [ b ] -> b | bs -> mk loc ty (Tast.Do bs)
in
let rec chain = function
| [] -> unit_at loc
| ({ Tast.acase = None; _ } as a) :: _ -> body a
| [ a ] -> body a
| ({ Tast.acase = Some m; _ } as a) :: rest ->
let test =
match subject with
| `Enum (_, members) ->
let v =
mk loc s.Tast.ty (Tast.Int (List.assoc m members, Types.I32))
in
mk loc Types.Bool (Tast.Prim (Tast.Eq, [ local; v ]))
| `Bool when String.equal m "true" -> local
| `Bool -> mk loc Types.Bool (Tast.Prim (Tast.Not, [ local ]))
| `Lit Types.Dyn -> dyn_eq loc local (Hashtbl.find lits m)
| _ ->
mk loc Types.Bool (Tast.Prim (Tast.Eq, [ local; Hashtbl.find lits m ]))
in
mk loc ty (Tast.If (test, body a, chain rest))
in
mk loc ty (Tast.Let ([ (slot, s) ], [ chain arms ]))
(* [if let P = v] — (if-let [P v] then else) — is the two-arm match
[(match v P then _ else)]. With no else it is a statement, Unit whatever
[then] answers, as a one-armed [if] is.
A pattern that cannot fail — [_], or a plain name, which is a name to bind
and not a case — tests nothing, and is refused toward [let]. A bare name is
a case when some data type, Option or bool has a case of that name, or it
names an enum member through its enum. *)
and check_if_let ctx ~tail ~used ?want loc scrutinee (arm : Ast.arm) els =
let fln = fln_source loc in
let is_case n =
List.mem n [ "None"; "Some"; "true"; "false" ]
|| String.contains n '.'
|| Hashtbl.fold
(fun _ u acc -> acc || Tast.case_index u n <> None)
ctx.env.datas false
in
let irrefutable name =
match name with
| Some n ->
Loc.failk "check/if-let-irrefutable" arm.Ast.aloc
"the pattern %s is a plain name, which always matches, so this if let \
has nothing to test. Bind the value with %s"
n
(if fln then Printf.sprintf "let %s = ..." n
else Printf.sprintf "(let [%s ...] ...)" n)
| None ->
Loc.failk "check/if-let-irrefutable" arm.Ast.aloc
"the pattern _ always matches, so this if let has nothing to test. \
Use the value directly, or match on it"
in
match arm.Ast.pat with
| Ast.Pwild -> irrefutable None
| Ast.Pctor (n, []) when not (is_case n) ->
if_let_name ctx ~tail ~used ?want loc scrutinee n arm els
| _ ->
let wild body = { Ast.pat = Ast.Pwild; body; aloc = loc } in
(* Kept with no else at the end of its chain, it is a [when] over a
pattern: [Some] of the arm that ran and [None] when none did — or, where
a dyn is wanted, the value or nil. *)
let open_end =
match els with None -> true | Some e -> kept_open ~used:true None e
in
let kept =
match want with
| Some (Types.Unit | Types.Never) -> false
| Some _ -> true
| None -> used
in
let at (x : Ast.expr) e = { Ast.e; loc = x.Ast.loc } in
match els with
| _ when kept && open_end ->
let rest = match els with Some e -> e | None -> at scrutinee (Ast.Var "nil") in
(match want with
| Some Types.Dyn ->
check_match ctx ~tail ~used:true ?want loc scrutinee [ arm; wild [ rest ] ]
| _ ->
(* The rest of the chain is checked once the arm's own type is
known, as a one-armed [if]'s else is: see [opt] in [check_match]. *)
let opt_rest =
Option.map
(fun e ~used ?want () ->
branch ctx (fun () ->
ctx.tail <- tail; ctx.used <- used; check ctx ?want e))
els
in
(* The arm at the payload first, as [check_when] asks it; refused
there, at the Option itself (decision 140). *)
let go ~flat () =
check_match ctx ~tail ~used:true ~opt:true ~flat ?opt_rest ?want loc scrutinee
[ arm; wild [] ]
in
expect ctx loc ~want
(match want with
| Some (Types.Option _) ->
(match trial ctx (go ~flat:false) with
| Ok r -> r
| Error d ->
(match trial ctx (go ~flat:true) with
| Ok r -> r
| Error _ -> raise (Loc.Error d)))
| _ -> go ~flat:false ()))
| Some e ->
check_match ctx ~tail ~used ?want loc scrutinee [ arm; wild [ e ] ]
| None ->
expect ctx loc ~want
(check_match ctx ~tail ~stmt:true loc scrutinee [ arm; wild [] ])
(* [if let g = x] with a plain name: over an Option it is [if let Some(g) =
x], and over a dyn it binds [g] when [x] is not nil. [x] is checked once
and held under a name no reader can produce; the rest is the form it
stands for, checked as that form is. Anything else always holds a value,
so there is nothing to test. *)
and if_let_name ctx ~tail ~used ?want loc scrutinee n (arm : Ast.arm) els =
let sv = check ctx scrutinee in
let fln = fln_source loc in
let held f =
scoped ctx (fun () ->
incr held_n;
let h = Printf.sprintf "~if%d" !held_n in
let s = bind ctx h sv.Tast.ty ~assignable:false in
let hv = { Ast.e = Ast.Var h; loc = scrutinee.Ast.loc } in
let r = f hv in
mk loc r.Tast.ty (Tast.Let ([ (s, sv) ], [ r ])))
in
match sv.Tast.ty with
| Types.Option _ ->
held (fun hv ->
check_if_let ctx ~tail ~used ?want loc hv
{ arm with Ast.pat = Ast.Pctor ("Some", [ n ]) } els)
| Types.Dyn ->
held (fun hv ->
let at e = { Ast.e; loc = arm.Ast.aloc } in
let test = at (Ast.Call (at (Ast.Var "!="), [ hv; at (Ast.Var "nil") ])) in
let bnd = { Ast.bname = n; bty = None; bval = hv; bloc = arm.Ast.aloc } in
let body = { Ast.e = Ast.Let ([ bnd ], arm.Ast.body); loc = arm.Ast.aloc } in
ctx.tail <- tail;
ctx.used <- used;
check ctx ?want { Ast.e = Ast.If (test, body, els); loc })
| t ->
Loc.failk "check/if-let-irrefutable" arm.Ast.aloc
"%s is %s, which always holds a value, so this if let has nothing to \
test. A plain name in an if let binds what an Option or a dyn holds \
when it holds something. Bind this value with %s"
(spell_arg "the value" scrutinee) (tyname loc t)
(if fln then Printf.sprintf "let %s = ..." n
else Printf.sprintf "(let [%s ...] ...)" n)
(* The locals a condition tests with [x?], through [and]: those it narrows
in the block it guards (decision 133). Not through [or] or [not], where
the test holding says nothing about [x]. *)
and narrows (c : Ast.expr) =
match Ast.unpause c with
| Some x -> narrows x
| None ->
match c.Ast.e with
| Ast.Call ({ Ast.e = Ast.Var "?"; _ }, [ { Ast.e = Ast.Var x; _ } ]) -> [ x ]
| Ast.If (p, q, Some { Ast.e = Ast.Var "false"; _ }) -> narrows p @ narrows q
| Ast.IfLet (_, { Ast.pat = Ast.Pctor (g, []); body = [ q ]; _ },
Some { Ast.e = Ast.Var "false"; _ }) when as_name g -> narrows q
| _ -> []
and as_binds c = Ast.as_binds c
and as_name g = Ast.as_name g
(* A condition with [as] in it, as the bool it tests, and each name it binds
with the hidden name the block reads it through. The chain runs left to
right and stops at the first test that fails, so each value is found
once. Each name an [as] binds is one slot, read by the rest of the chain
and, through [Ast.Alias], by the block. *)
and as_cond ctx (c : Ast.expr) =
let named = ref [] in
let no loc = mk loc Types.Bool (Tast.Bool false) in
let rec go (c : Ast.expr) =
let loc = c.Ast.loc in
match Ast.unpause c, c.Ast.e with
(* A pause mark on a test of the chain stops before it and leaves the
chain as it was. *)
| Some x, Ast.Do [ pause; _ ] ->
let pv = check ctx pause in
let xv = go x in
mk loc Types.Bool (Tast.Do [ pv; xv ])
| _, _ ->
match c.Ast.e with
| Ast.If (p, q, Some { Ast.e = Ast.Var "false"; _ }) when as_binds q <> [] ->
let pv = check_truthy ctx p in
let qv = with_narrowed ctx (narrows p) (fun () -> go q) in
mk loc Types.Bool (Tast.If (pv, qv, no loc))
| Ast.IfLet (e, { Ast.pat = Ast.Pctor (g, []); body = [ q ]; _ },
Some { Ast.e = Ast.Var "false"; _ }) when as_name g ->
let ev = check ctx e in
let refuse t =
Loc.failk "check/as-not-optional" e.Ast.loc
"%s is %s, which always holds a value, so as has nothing to test. \
as names what an Option or a dyn holds, when it holds something. \
It is not a conversion: a number is converted with its type's \
name, as in i32(x)"
(source_text e) (tyname loc t)
in
(* [g] is a slot of its own under its own name, so locals, the stepper,
the inspector and the watch view show it as the program reads it. A
dyn is held there directly; an Option is held in a hidden slot and
its payload copied into [g]'s once the test holds. *)
let bound ty =
scoped ctx (fun () ->
let slot = bind ctx ~what:as_tag g ty ~assignable:false in
ctx.as_slots <- slot :: ctx.as_slots;
let b = Option.get (lookup ctx g) in
incr held_n;
named := (g, Printf.sprintf "~as%d" !held_n, b) :: !named;
(slot, go q))
in
(match ev.Tast.ty with
| Types.Option t ->
let hs = fresh_slot ctx ev.Tast.ty in
let hv = mk loc ev.Tast.ty (Tast.Local hs) in
let slot, qv = bound t in
mk loc Types.Bool
(Tast.Let ([ (hs, ev) ],
[ mk loc Types.Bool
(Tast.If (opt_is_some loc hv,
mk loc Types.Bool
(Tast.Let ([ (slot, opt_payload loc t hv) ], [ qv ])),
no loc)) ]))
| Types.Dyn ->
let slot, qv = bound Types.Dyn in
let sv = mk loc Types.Dyn (Tast.Local slot) in
mk loc Types.Bool
(Tast.Let ([ (slot, ev) ], [ mk loc Types.Bool (Tast.If (dyn_not_nil loc sv, qv, no loc)) ]))
| t -> refuse t)
| _ -> check_truthy ctx c
in
let cv = go c in
let named = List.rev !named in
List.iter (fun (_, h, b) -> ctx.scope <- (h, b) :: ctx.scope) named;
(cv, List.map (fun (g, h, _) -> (g, h)) named)
(* [f] with each of [names] that is a local (Option T) read as its payload:
the same slot, so a field set through it lands in the Option itself. A
dyn stays as it is; a name that is not a local is not narrowed. Assigning
the name a T writes the payload and it stays present; assigning it an
Option is refused ([narrowed_set]). *)
and with_narrowed : 'a. ctx -> string list -> (unit -> 'a) -> 'a = fun ctx names f ->
if names = [] then f ()
else
let held = ref [] in
let r =
scoped ctx (fun () ->
List.iter
(fun n ->
match lookup ctx n with
| Some { bty = Types.Option _; _ } when List.mem n !unnarrowable ->
held := n :: !held
| Some ({ bty = Types.Option t; _ } as b) when b.bwhat <> Some narrowed_tag ->
ctx.scope <- (n, { b with bty = t; bwhat = Some narrowed_tag; blit = None })
:: ctx.scope
| _ -> ())
names;
(* A name something else can clear stays an Option; a refusal in
the block about it says why, and how to copy what it holds. *)
if !held = [] then f ()
else
let note (d : Loc.diag) =
let has s sub =
let ls = String.length s and lb = String.length sub in
let rec go i = i + lb <= ls && (String.sub s i lb = sub || go (i + 1)) in
go 0
in
let about = if has d.Loc.dmsg "Option" then !held else [] in
match about with
| [] -> d
| n :: _ ->
{ d with
Loc.notes =
d.Loc.notes
@ [ Loc.note d.Loc.dloc
(Printf.sprintf
"%s? does not make %s its payload here: %s's address \
is taken, or a fn assigns it, in this function, so \
something else could clear it. Write if %s as g, \
which copies what it holds into g"
n n n n) ] }
in
(* Raised, or recorded while recovering: noted either way. *)
let before = ctx.env.recovered in
let r =
try f () with
| Loc.Error d -> raise (Loc.Error (note d))
| Loc.Errors ds -> raise (Loc.Errors (List.map note ds))
in
let rec fresh l =
if l == before then l
else match l with d :: t -> note d :: fresh t | [] -> []
in
ctx.env.recovered <- fresh ctx.env.recovered;
r)
in
r
(* [x?]: whether x holds a value — an Option that is Some, a dyn that is not
nil. *)
and check_present ctx ~want loc (args : Ast.expr list) =
match args with
| [ x ] ->
let xv = check ctx x in
let s = fresh_slot ctx xv.Tast.ty in
let sv = mk loc xv.Tast.ty (Tast.Local s) in
let held e = mk loc Types.Bool (Tast.Let ([ (s, xv) ], [ e ])) in
(match xv.Tast.ty with
| Types.Option _ -> expect ctx loc ~want (held (opt_is_some loc sv))
| Types.Dyn -> expect ctx loc ~want (held (dyn_not_nil loc sv))
| t ->
let hint =
match x.Ast.e with
| Ast.Var n ->
Printf.sprintf ". A name cannot end in ? either: a yes-or-no name \
starts with is- or has-, as in is-%s" n
| _ -> ""
in
fail loc "%s is %s, which always holds a value, so %s? has nothing to \
test%s" (source_text x) (tyname loc t) (source_text x) hint)
| _ -> fail loc "? tests one value: x?"
(* The tag test and the payload of an Option held in a local, and the nil
test of a dyn: the shapes [box_option] and [unbox_option] build. *)
and opt_is_some loc (sv : Tast.expr) =
mk loc Types.Bool
(Tast.Prim (Tast.Ne,
[ mk loc (Types.Int Types.I8) (Tast.Field (sv, 0));
mk loc (Types.Int Types.I8) (Tast.Int (0L, Types.I8)) ]))
and opt_payload loc t (sv : Tast.expr) = mk loc t (Tast.Field (sv, 1))
and dyn_not_nil loc (sv : Tast.expr) =
mk loc Types.Bool
(Tast.Prim (Tast.Eq,
[ rt loc (Types.Int Types.I32) "flan_dyn_is_nil" [ sv ];
mk loc (Types.Int Types.I32) (Tast.Int (0L, Types.I32)) ]))
(* [x ?? d]: what [x] holds, or [d] when it holds nothing — [None], or nil
over a dyn. [d] is evaluated only then. A chain, [(?? a b c)], is read
from the right, [a ?? (b ?? c)], so a default may itself be an Option and
the whole is then an Option, as in Swift. *)
and check_coalesce ctx ~want loc (args : Ast.expr list) =
match args with
| [] | [ _ ] -> fail loc "?? takes a value and a default: x ?? d"
| x :: rest ->
let d =
match rest with
| [ d ] -> d
| d :: _ -> { Ast.e = Ast.Call ({ Ast.e = Ast.Var "??"; loc }, rest); loc = d.Ast.loc }
| [] -> assert false
in
let xv = check ctx x in
let s = fresh_slot ctx xv.Tast.ty in
let sv = mk loc xv.Tast.ty (Tast.Local s) in
let held ty e = mk loc ty (Tast.Let ([ (s, xv) ], [ mk loc ty e ])) in
(match xv.Tast.ty with
| Types.Option t ->
(match trial ctx (fun () -> expect ctx d.Ast.loc ~want:(Some t) (check ctx ~want:t d)) with
| Ok dv -> expect ctx loc ~want (held t (Tast.If (opt_is_some loc sv, opt_payload loc t sv, dv)))
| Error first ->
let o = Types.Option t in
(match trial ctx (fun () -> expect ctx d.Ast.loc ~want:(Some o) (check ctx ~want:o d)) with
| Ok dv -> expect ctx loc ~want (held o (Tast.If (opt_is_some loc sv, sv, dv)))
| Error _ -> raise (Loc.Error first)))
| Types.Dyn ->
let dv = check ctx ~want:Types.Dyn d in
expect ctx loc ~want (held Types.Dyn (Tast.If (dyn_not_nil loc sv, sv, dv)))
| t ->
fail x.Ast.loc
"the left side of ?? is %s, which always holds a value, so there is \
nothing to fall back from. ?? takes an Option or a dyn"
(tyname loc t))
(* The source text of [e], for a message the program prints when it runs. *)
and source_text (e : Ast.expr) =
match Loc.snippet ~lim:48 e.Ast.loc with
| Some t -> t
| None -> spell_arg "the value" e
(* [x!]: what [x] holds, and a trap at this site naming [x] when it holds
nothing. The trap never returns; the zero after it only gives the arm its
type, so no backend needs a call typed Never. *)
and check_unwrap ctx ~want loc (args : Ast.expr list) =
match args with
| [ x ] ->
let xv = check ctx x in
let s = fresh_slot ctx xv.Tast.ty in
let sv = mk loc xv.Tast.ty (Tast.Local s) in
let text = source_text x in
let fail_with none zero ty =
mk loc ty
(Tast.Do
[ rt loc Types.Unit "flan_unwrap_fail"
[ here loc;
mk loc Types.String
(Tast.Str (Printf.sprintf "%s is %s, so %s! has no value to give"
text none text)) ];
zero ])
in
let held ty e = mk loc ty (Tast.Let ([ (s, xv) ], [ mk loc ty e ])) in
(match xv.Tast.ty with
| Types.Option t ->
expect ctx loc ~want
(held t (Tast.If (opt_is_some loc sv, opt_payload loc t sv,
fail_with "None" (mk loc t (Tast.Zero t)) t)))
| Types.Dyn ->
expect ctx loc ~want
(held Types.Dyn
(Tast.If (dyn_not_nil loc sv, sv,
fail_with "nil" (rt loc Types.Dyn "flan_dyn_nil" []) Types.Dyn)))
| t ->
fail x.Ast.loc
"%s is %s, which always holds a value, so ! has nothing to unwrap. \
Leave the ! out" text (tyname loc t))
| _ -> fail loc "! unwraps one value: x!"
(* [a?.b]: [(?. [n a] body)] — [body] over what [a] holds, bound to [n], or
None when it holds nothing (nil over a dyn). [body] already an Option is
not wrapped again, so [a?.b?.c] is one Option; [body] with no value makes
the whole a statement. *)
and check_chain ctx ~want loc n (v : Ast.expr) (body : Ast.expr) =
let hv = check ctx v in
let s = fresh_slot ctx hv.Tast.ty in
let sv = mk loc hv.Tast.ty (Tast.Local s) in
let arm t ~dyn =
scoped ctx (fun () ->
let p = bind ctx n t ~assignable:false in
let bv = if dyn then check ctx ~want:Types.Dyn body else check ctx body in
(p, bv))
in
let held ty e = mk loc ty (Tast.Let ([ (s, hv) ], [ mk loc ty e ])) in
match hv.Tast.ty with
| Types.Option t ->
let p, bv = arm t ~dyn:false in
let inner ty e = mk loc ty (Tast.Let ([ (p, opt_payload loc t sv) ], [ e ])) in
(match bv.Tast.ty with
| Types.Unit | Types.Never ->
expect ctx loc ~want
(held Types.Unit
(Tast.If (opt_is_some loc sv, inner Types.Unit bv, mk loc Types.Unit Tast.Unit)))
| Types.Option _ as o ->
expect ctx loc ~want
(held o (Tast.If (opt_is_some loc sv, inner o bv, mk loc o Tast.None_)))
| b ->
let o = Types.Option b in
expect ctx loc ~want
(held o (Tast.If (opt_is_some loc sv, inner o (mk loc o (Tast.Some_ bv)),
mk loc o Tast.None_))))
| Types.Dyn ->
let p, bv = arm Types.Dyn ~dyn:true in
let ty = match bv.Tast.ty with Types.Unit | Types.Never -> Types.Unit | _ -> Types.Dyn in
let none = if ty = Types.Unit then mk loc Types.Unit Tast.Unit
else rt loc Types.Dyn "flan_dyn_nil" [] in
expect ctx loc ~want
(held ty (Tast.If (dyn_not_nil loc sv, mk loc ty (Tast.Let ([ (p, sv) ], [ bv ])), none)))
| t ->
fail v.Ast.loc
"%s is %s, which always holds a value, so ?. has nothing to test. \
Write . instead" (source_text v) (tyname loc t)
(* ── Places ────────────────────────────────────────────────────────── *)
(* The fields a name has, whether it is a struct or an untagged union. The two
are one record and differ only in what the offsets come out as, which is a
question for the layout and not for this — so [.x] is one path and not two,
and a union member is read with the accessor everything else is read with.
That is the whole of what makes punning ordinary code. *)
(* Every name a value could be standing under here: what is in scope, the
globals, the functions — generic ones included, since a call to one is
written exactly like a call to any other. No type names: a symbol written
where a value goes was not a mistyped struct, and offering one would send
the reader to the wrong file. *)
and value_candidates ctx =
List.map fst ctx.scope
@ Hashtbl.fold (fun k _ acc -> k :: acc) ctx.env.globals []
@ Hashtbl.fold (fun k _ acc -> k :: acc) ctx.env.fns []
@ Hashtbl.fold (fun k _ acc -> k :: acc) ctx.env.gsigs []
(* The name nothing answers to, refused with whatever this position can still
tell the reader.
Two readings get in ahead of the bare refusal. The first is the dot: [p.x]
is how C, Go and Odin spell field access and it is the habit everyone
arrives with, so a symbol with a dot in it and a lowercase head is almost
never a name — it is an accessor written the way the last language wrote
it. The head is looked up, so the sentence can say what [p] actually is
rather than guess, and the struct's declaration comes along as a note when
there is one. Capitalised heads are left alone: [Shape.Circle] is a real
spelling in this language and a typo in one is a mistyped case, not a
dot-infix habit.
The second is the near miss, over values only — see [value_candidates]. *)
and unknown_name : 'a. ?setting:bool -> ctx -> Loc.t -> string -> 'a =
fun ?(setting = false) ctx loc name ->
(* A retired randomness name gets its own sentence and gets it first. It has
no dot in it, and the near miss below would answer "did you mean rand?"
for [rand-f32] — true and not much use, where the line in [no_such_rand]
names the function and spells the call. Everything else falls through to
the two readings this was built for. *)
(match no_such_rand name with
| Some msg -> Loc.failk "check/unknown-name" loc "%s" msg
| None -> ());
(* In the indented syntax a binary operator needs spaces, so [x-1], [i+1]
and [x/2] are one name each. When the parts either side of an operator
character are a value in scope and a number or another value, that is
almost certainly the arithmetic, and the sentence says how to spell it. *)
(if Source.indented_at loc then begin
let known s =
s <> ""
&& (String.for_all (fun c -> (c >= '0' && c <= '9') || c = '.') s
|| lookup ctx s <> None
|| Hashtbl.mem ctx.env.globals s)
in
let n = String.length name in
let rec scan i =
if i < n - 1 then
match name.[i] with
| ('-' | '+' | '*' | '/') as c
when i > 0 && known (String.sub name 0 i)
&& known (String.sub name (i + 1) (n - i - 1)) ->
Loc.failk "check/unknown-name" loc
"unknown name %s — an operator needs a space on each side, so \
this is one name and not arithmetic. Did you mean %s %c %s?"
name (String.sub name 0 i) c (String.sub name (i + 1) (n - i - 1))
| _ -> scan (i + 1)
in
scan 0
end);
let dot = String.index_opt name '.' in
let head, field =
match dot with
| Some i when i > 0 && i + 1 < String.length name ->
String.sub name 0 i, String.sub name (i + 1) (String.length name - i - 1)
| _ -> "", ""
in
let lower = head <> "" && head.[0] = Char.lowercase_ascii head.[0]
&& head.[0] <> Char.uppercase_ascii head.[0] in
if lower then begin
let ty =
match lookup ctx head with
| Some b -> Some b.bty
| None -> Option.map fst (Hashtbl.find_opt ctx.env.globals head)
in
let sname =
match ty with
| Some (Types.Named n) when fields_named ctx.env n <> None -> Some n
| Some (Types.Ptr (_, (Types.Named n))) when fields_named ctx.env n <> None -> Some n
| _ -> None
in
match sname, ty with
| Some sn, _ ->
let s = Option.get (fields_named ctx.env sn) in
let notes = declared_note ctx.env sn in
(* In a [set] the accessor is the *place*, so the spelling to give is
[(set (.x p) 1)] and not [(.x p)] on its own. Saying "read" at an
assignment would be a sentence that does not apply to the form it is
printed under. *)
let how =
if setting then Printf.sprintf "a field is assigned through an \
accessor, so write (set (.%s %s) ...)"
field head
else Printf.sprintf "a field is read with an accessor, so write (.%s %s)"
field head
in
if Tast.field_index s field <> None then
Loc.failk "check/dot-access" loc ~notes "unknown name %s — %s" name how
else
Loc.failk "check/dot-access" loc ~notes
"unknown name %s — %s, and %s has no field %s" name how sn field
| None, Some Types.Dyn ->
let how =
if setting then Printf.sprintf "(set (.%s %s) ...)" field head
else Printf.sprintf "(.%s %s)" field head
in
Loc.failk "check/dot-access" loc
"unknown name %s — a dot is part of the name here, not field access. \
%s is dyn, and its :%s is reached with %s"
name head field how
| None, Some t ->
Loc.failk "check/dot-access" loc
"unknown name %s — a dot is part of the name here, not field access. \
A field is reached through an accessor, (.%s %s), and %s is %s, \
which has no fields"
name field head head (tyname loc t)
| None, None ->
Loc.failk "check/unknown-name" loc
"unknown name %s — nothing named %s is in scope either. A field is \
reached through an accessor, (.%s %s), not with a dot"
name head field head
end
else
(* [x++]: a name may end in +, so the increment of another language
reads as one unknown name. *)
let n = String.length name in
let stem = if n > 2 then String.sub name 0 (n - 2) else "" in
let suffix = if n > 2 then String.sub name (n - 2) 2 else "" in
if (suffix = "++" || suffix = "--") && List.mem stem (value_candidates ctx) then
Loc.failk "check/unknown-name" loc
"unknown name %s — to %s %s, write %s"
name (if suffix = "++" then "add one to" else "take one from") stem
(if Source.indented_at loc then
Printf.sprintf "%s(%s) or %s %s= 1" suffix stem stem (String.make 1 suffix.[0])
else Printf.sprintf "(%s %s)" suffix stem)
else
match nearest (value_candidates ctx) name with
| Some m ->
Loc.failk "check/unknown-name" loc "unknown name %s — did you mean %s?" name m
| None -> Loc.failk "check/unknown-name" loc "unknown name %s" name
(* 1st, 2nd, 3rd, and every other one. *)
and ordinal n =
let suffix =
if n mod 100 >= 11 && n mod 100 <= 13 then "th"
else match n mod 10 with 1 -> "st" | 2 -> "nd" | 3 -> "rd" | _ -> "th"
in
string_of_int n ^ suffix
(* Check one argument of a call to [name], and if the refusal is the plain
type mismatch raised against *this* argument's own span, say the two things
the caret cannot: which argument of which function this is, and where the
parameter that wanted the other type is declared.
The span test is what keeps the claim true. A mismatch deeper inside the
argument — an element of a vec literal, an argument of a nested call — is
raised against its own location and is re-raised untouched, because calling
that "the 2nd argument of add" would be a sentence that reads well and
points at the wrong form. The rekind is what stops a nested call from being
named twice: once enriched, it is no longer the kind this looks for. *)
and check_arg ctx name i (want : Types.t) (a : Ast.expr) =
ctx.env.guard_next <- true;
match check ctx ~want a with
| e -> e
| exception Loc.Error d
when String.equal d.Loc.kind "check/type-mismatch"
&& d.Loc.dloc == a.Ast.loc ->
let which = ordinal (i + 1) in
let notes =
match Hashtbl.find_opt ctx.env.fparams name with
| Some ps when List.length ps > i ->
let p = List.nth ps i in
[ Loc.note p.Ast.floc
(Printf.sprintf "%s's %s parameter %s is declared %s"
(written_name name) which p.Ast.fname (tyname p.Ast.floc want)) ]
| _ -> []
in
refuse_or_poison ctx.env a.Ast.loc
(Loc.diag ~kind:"check/argument-type" ~notes a.Ast.loc
(Printf.sprintf "%s — this is the %s argument of %s" d.Loc.dmsg which
(written_name name)))
| exception Loc.Error d -> refuse_or_poison ctx.env a.Ast.loc d
and fields_named env n : Tast.structure option =
match Hashtbl.find_opt env.structs n with
| Some s -> Some s
| None -> Hashtbl.find_opt env.unions n
(* The target of [.field] is a struct or an untagged union, or one level of
pointer to one. The auto-deref is inserted here as a real node, so no
backend re-derives it. The target comes checked, because every caller
looks first for a dyn, whose [.name] is a map entry and not a field. *)
and struct_of ctx (target : Ast.expr) (t : Tast.expr) : Tast.expr * string =
let has n = fields_named ctx.env n <> None in
(match t.Tast.ty with
| Types.Named "String" | Types.Ptr (_, Types.Named "String") ->
refuse_string_inside target.Ast.loc
| _ -> ());
match t.Tast.ty with
| Types.Named n when has n -> t, n
| Types.Ptr (_, (Types.Named n)) when has n ->
mk t.Tast.loc (Types.Named n) (Tast.Deref t), n
(* A data type's fields belong to one case, and which case it is holding is
only known after the tag has been read. [.field] would have to be a read
that might be reading something else, so it is not one: [match] is how a
data type is opened, and it binds the fields it has proved are there. *)
| (Types.Named n | Types.Ptr (_, Types.Named n))
when Hashtbl.mem ctx.env.datas n ->
fail target.Ast.loc
"%s is a data type, and its fields belong to a case — reach them with \
(match ...), whose arms bind the fields of the case they matched"
n
| other ->
(* The pattern bound this, and it looks like a destructuring that did not
take: [(match s (Circle c) (.r c))] over a one-field case binds [c] to
the payload itself. "f64 is not a struct" is true and is a type fact
where the reader needs to be told the value is already in hand. *)
(match target.Ast.e with
| Ast.Var n ->
(match lookup ctx n with
| Some { bwhat = Some w; _ } when w = narrowed_tag ->
fail target.Ast.loc
"%s is tested with %s? above, so here it is what the Option \
holds, %s, and %s has no fields"
n n (tyname target.Ast.loc other) (tyname target.Ast.loc other)
| Some { bwhat = Some w; _ } when w <> as_tag ->
fail target.Ast.loc
"%s is %s — the pattern bound it to %s, so the value is already \
in hand and there is no field left to read"
n (tyname target.Ast.loc other) w
| _ -> ())
| _ -> ());
fail target.Ast.loc "%s is not a struct, so it has no fields"
(tyname target.Ast.loc other)
(* (at s i) reads a string's byte, and reading is the whole of what a string
does here: it is a view of bytes the program does not own — a literal's
are in constant storage, where a store is dropped on one backend and
faults on the other — so there is no address of one to hand out either.
[addr] asks the same question and gets the same answer, so the refusal
names taking the address rather than only assigning.
It is a function and not a case inside [check_place] because [check_place]
is no longer the only way to a [Pindex]: the single-index [set] arm checks
its target itself and calls [indexed] directly, and [indexed] accepts a
string. Both call this, so neither can drift away from the other. *)
and refuse_string_place loc (ty : Types.t) =
if Types.equal ty Types.String then
fail loc
"a string is read-only, so (at s i) is a value and not a place. Copy \
the bytes into a buffer you own and write that"
(* Whether a checked place is read-only storage: reached through a
[[const T]] or a (Ptr const T), or a byte of a string. Its address is a
(Ptr const T). *)
and place_const (p : Tast.place) =
match p with
| Tast.Plocal _ | Tast.Pglobal _ -> false
| Tast.Pfield (t, _) -> const_reached t <> None
| Tast.Pderef t ->
(match t.Tast.ty with Types.Ptr (Types.Const, _) -> true | _ -> false)
| Tast.Pindex (t, idx) ->
let rec through_string ty n =
n > 0
&& (match ty with
| Types.String -> true
| Types.Array (_, e) | Types.Slice (_, e) -> through_string e (n - 1)
| _ -> false)
in
const_steps (const_reached t) t.Tast.ty (List.length idx) <> None
|| through_string t.Tast.ty (List.length idx)
(* Growing, shrinking or freeing a Vec or a Map that is read-only storage.
The backends hand the runtime the container's address, so this is the
store [check_place] refuses, made through the header instead of through a
[set]. Writing into the Vec's own buffer is not refused: the const is
shallow. *)
and refuse_const_change _ctx loc (target : Tast.expr) =
match const_reached target with
| None -> ()
| Some view ->
let t = tyname loc target.Tast.ty in
let holder =
match view with Types.Slice (_, e) | Types.Ptr (_, e) -> e | t -> t
in
Loc.failk "check/store-through-const" loc
"this changes a %s reached through a %s, which can only be read. Where \
it has to change, take the %s it lives in as a [%s] or a (Ptr %s) \
instead"
t (tyname loc view) (tyname loc holder)
(tyname loc holder) (tyname loc holder)
and check_place ?(store = true) ctx loc (p : Ast.place) : Tast.place * Types.t =
match p with
| Ast.Pvar name ->
(* Scope first, and the capture refusal only where scope did not settle
it. A body's *own* [let] may shadow a name the enclosing function also
has, and a store into that one is an ordinary store — asking about the
capture before looking would refuse it with a message about a copy that
is not the thing being written to. *)
(match lookup ctx name with
| Some b ->
if not b.assignable then begin
(* Not assignable, so it is either a parameter or a captured copy.
Which one decides the message, and the copy's reason is its
own. *)
(match List.assoc_opt name ctx.caught with
| Some (_, slot) when slot = b.slot -> captured_set ctx loc name
| _ -> ());
if b.bwhat = Some as_tag then
fail loc
"%s names what an as test found, and it cannot be given a new \
value. To change it, copy it into a local first: let %s2 = %s"
name name name;
fail loc
"%s is a parameter, and a parameter is not assignable — bind a \
local with let" name
end;
if b.bwhat = Some narrowed_tag then
(Tast.Pfield (mk loc (Types.Option b.bty) (Tast.Local b.slot), 1), b.bty)
else
Tast.Plocal b.slot, b.bty
| None ->
(* Not in scope here at all: a name of the enclosing function, which a
lifted body may read as a copy and may not write to. *)
captured_set ctx loc name;
match Hashtbl.find_opt ctx.env.globals name with
| Some (_, true) ->
(* Four words, before: the name and the fact, and nothing about what
to do or where the decision was made. [no_container_defconst] is
the house's own shape for this and the note is [declared_note]'s.
A defconst is what the linker writes into the image, so there is
no assignment to allow — the fix is the other declaration. *)
let notes =
match Hashtbl.find_opt ctx.env.global_locs name with
| Some at -> [ Loc.note at (name ^ " is declared a constant here") ]
| None -> []
in
Loc.failk "check/set-constant" loc ~notes
"%s is a constant, and a constant is not assignable. Declare it \
with defonce if it has to change" name
| Some (ty, false) -> Tast.Pglobal name, ty
| None -> unknown_name ~setting:true ctx loc name)
| Ast.Pfield (target, name) ->
let t = check_target ctx target in
if t.Tast.ty = Types.Dyn then begin
let x = match target.Ast.e with Ast.Var x -> x | _ -> "x" in
if Source.indented_at loc then
fail loc
"%s.%s is an entry of a dyn map, and has no address. Read it into \
a local: let v = %s.%s" x name x name
else
fail loc
"(.%s %s) is an entry of a dyn map, and has no address. Read it \
into a local: (let [v (.%s %s)] ...)" name x name x
end;
field_place ~store ctx loc target t name
| Ast.Pindex (target, idx) ->
let target = check_target ctx target in
(match target.Tast.ty with
(* The same bounds and epoch check the value form gets, through the same
helper: an element of a Vec is a place because a Vec element is
assignable, and a set that skipped the checks would be the asymmetry
[nth] was removed for. *)
| Types.Vec _ ->
let p, ty = vec_at ctx loc target idx in
Tast.Pderef p, ty
| _ ->
let idx, ty = indexed ~place:loc ~store ctx target idx in
Tast.Pindex (target, idx), ty)
| Ast.Pderef target ->
let target = check ctx target in
(match target.Tast.ty with
| Types.Ptr (Types.Const, _) as view when store ->
refuse_const_place ctx.env loc view
| Types.Ptr (_, t) -> Tast.Pderef target, t
| other ->
fail loc "deref takes a (Ptr T), found %s" (tyname loc other))
(* Only [set] writes a class slot, and it has its own arm above. A slot
lives in a map the collector may move entries of, so it has no address
to hand out. *)
| Ast.Pslot _ ->
fail loc
"a class slot (get inst :slot) is written with set and has no address. \
Read it into a local with let"
(* The dyn keyword [:name], for a dyn's [.name]. *)
and dyn_kw ctx loc name = check ctx ~want:Types.Dyn { Ast.e = Ast.Kw name; loc }
(* A struct field as a place, over a target already checked. *)
and field_place ~store ctx loc target t name =
let target, sname = struct_of ctx target t in
let s = Option.get (fields_named ctx.env sname) in
match Tast.field_index s name with
| None ->
Loc.failk "check/unknown-field" loc ~notes:(declared_note ctx.env sname)
"%s has no field %s" (tyname loc (Types.Named sname)) name
| Some i ->
if store then Option.iter (refuse_const_place ctx.env loc) (const_reached target);
Tast.Pfield (target, i), (List.nth s.Tast.fields i).Tast.fty
(* An index or a slice bound that is a literal is known now, so it is an error
now rather than a trap later. Only literals: a [defconst] is a global in the
typed IR, not a folded constant, so [(at arr size)] still traps at runtime —
which is what the emitted bounds check is for. A negative literal is wrong
whatever the target, but a length is static only for [n T]. *)
and static_index loc (ty : Types.t) ~past_end what k =
if k < 0L then
fail loc "%s %Ld is negative — indices count from 0" what k;
match ty with
(* [past_end] is the difference between an index and a slice bound: the last
valid index is len - 1, but a slice may end at len. *)
| Types.Array (n, _) when if past_end then k > n else k >= n ->
fail loc "%s %Ld is out of bounds for length %Ld" what k n
| _ -> ()
(* The literal value of a checked expression, if it has one. *)
and literal (e : Tast.expr) =
match e.Tast.e with Tast.Int (k, _) -> Some k | _ -> None
(* An index is [i32] internally, but a *narrower* integer may be written as
one: indexing is not arithmetic on the value, so there is nothing for a
visible cast to warn about, and requiring (i32 c) at every subscript would
be noise. A u32 is included because it cannot lose a value the bounds check
would then miss — anything above 2^31 truncates to a negative i32, which
the unsigned comparison rejects. i64 and u64 are not: 2^32 + 5 truncates to
5 and would read the wrong element with no trap at all, so those need the
cast written out. *)
and index_expr ctx (e : Ast.expr) =
(* No [want]: an expectation of [i32] would reject a [u32] index outright,
before there is anything here to convert. An untyped literal still
defaults to [i32] on its own. *)
let v = check ctx e in
match v.Tast.ty with
| Types.Int Types.I32 -> v
| Types.Int k when Types.bits k <= 32 ->
{ v with Tast.ty = index_ty;
Tast.e = Tast.Prim (Tast.Cast index_ty, [ v ]) }
| Types.Int k ->
fail e.Ast.loc
"an index is an i32, and %s is wider — write (i32 …)"
(Types.ikind_name k)
| other ->
fail e.Ast.loc "an index is an integer, found %s" (tyname e.Ast.loc other)
(* [(at a i)] and [(at grid row col)]: one index per dimension.
[~place] carries the location of the form being assigned into, and is
given by the two callers that are building somewhere to store. It is asked
at *every* dimension rather than once about the target: [(at g 0 0)] over a
[[2 string]] reaches a string at the last step and nowhere before it, so a
question asked only of [g] would miss it. *)
and indexed ?place ?(store = true) ctx (target : Tast.expr) (idx : Ast.expr list) =
(match place with
| Some l when store ->
Option.iter (refuse_const_place ctx.env l)
(const_steps (const_reached target) target.Tast.ty (List.length idx))
| _ -> ());
let rec go ty = function
| [] -> [], ty
| i :: rest ->
let elem =
match ty with
| Types.Array (_, t) | Types.Slice (_, t) -> t
(* A string indexes to its bytes, and only to read them. *)
| Types.String ->
if store then Option.iter (fun l -> refuse_string_place l ty) place;
Types.Int Types.U8
| Types.Named "String" ->
refuse_string_index i.Ast.loc ~store:(store && place <> None)
| other ->
fail i.Ast.loc "%s cannot be indexed" (tyname i.Ast.loc other)
in
let loc = i.Ast.loc in
let i = index_expr ctx i in
(match literal i with
| Some k -> static_index loc ty ~past_end:false "index" k
| None -> ());
let rest, ty = go elem rest in
i :: rest, ty
in
go target.Tast.ty idx
(* ── Calls ─────────────────────────────────────────────────────────── *)
and check_call ctx ~want loc (head : Ast.expr) (args : Ast.expr list) =
match head.Ast.e with
(* The resource notes [Shim] writes into a tracked binding's wrapper; see
[res_key]. The reader never produces a name beginning with '%'. *)
| Ast.Var (("%res-acquire" | "%res-release") as which) ->
(match args with
| [ v; { Ast.e = Ast.Str owner; _ } ] ->
let v = check ctx v in
let sym =
if which = "%res-acquire" then "flan_dev_reg_note_res_acquire"
else "flan_dev_reg_note_res_release"
in
expect ctx loc ~want
(rt loc Types.Unit sym
[ res_key loc ctx.env v;
mk loc Types.String (Tast.Str (Types.to_string v.Tast.ty));
mk loc Types.String (Tast.Str owner) ])
| _ -> fail loc "internal: %s takes a local and a name — a compiler bug" which)
| Ast.Var "%res-done" ->
(match args with
| [ { Ast.e = Ast.Str owner; _ } ] ->
expect ctx loc ~want
(rt loc Types.Unit "flan_dev_reg_note_res_done"
[ mk loc Types.String (Tast.Str owner) ])
| _ -> fail loc "internal: %%res-done takes a name — a compiler bug")
(* [i32?] where a value goes: the type, not a value. *)
| Ast.Var "Option" when fln_source loc
&& (match args with
| [ { Ast.e = Ast.Var n; _ } ] -> lookup ctx n <> None
| _ -> false) ->
let n = match args with [ { Ast.e = Ast.Var n; _ } ] -> n | _ -> "" in
fail loc
"%s? reads as the type Option(%s), because a capitalised name before ? \
is taken for a type. To test the local %s, give it a lowercase name, \
as in %s?" n n n (String.uncapitalize_ascii n)
| Ast.Var "Option" when fln_source loc ->
fail loc
"%s is an Option type, and a value is wanted here. On a value that may \
hold nothing, x?.field reads through it, x! unwraps it and x ?? d gives \
a default"
(match Loc.snippet loc with Some t -> t | None -> "this")
| Ast.Var name -> named_call ctx ~want loc name args
(* ((Ptr Color) p): a pointer cast, spelled the way (i32 x) is — the type
is the head. It changes what the pointer is said to point at and nothing
else, and checks nothing: the caller is promising the bytes are that
type, as with [slice-from]. Both backends already lower a Ptr-to-Ptr
[Cast] to no instruction. Adding const is allowed; dropping it is not,
or a cast would undo the const [addr] put there. The rule is about the
outer pointer only: the cast is unchecked, so ((Ptr (Ptr u8)) q) over a
(Ptr const (Ptr const u8)) is refused for the outer const but a
(Ptr (Ptr const u8)) casts to (Ptr (Ptr u8)) — what lies deeper is the
writer's promise, like everything else the cast asserts. There is no cast
between a pointer and an integer, and no pointer arithmetic. *)
| Ast.Call ({ Ast.e = Ast.Var "Ptr"; _ }, _)
when (match type_of_expr head with Some _ -> true | None -> false) ->
let target = resolve ctx.env (Option.get (type_of_expr head)) in
let spelled = tyname loc target in
(match args with
| [ a ] ->
let a_loc = a.Ast.loc in
let spelled_a = spell_arg "p" a in
let a = check ctx a in
(match a.Tast.ty, target with
| Types.Ptr (Types.Const, _), Types.Ptr (Types.Mut, u) ->
fail loc
"%s is a %s, which cannot be written through, and %s would allow \
writes. Write (%s %s)"
spelled_a (tyname loc a.Tast.ty) spelled
(tyname loc (Types.Ptr (Types.Const, u))) spelled_a
| Types.Ptr _, _ ->
expect ctx loc ~want
(mk loc target (Tast.Prim (Tast.Cast target, [ a ])))
| (Types.Slice _ | Types.Array _ | Types.String), _ ->
fail a_loc
"%s converts a pointer, found %s. The address of the first \
element is (addr (at %s 0)); write (%s (addr (at %s 0)))"
spelled (tyname loc a.Tast.ty) spelled_a spelled spelled_a
| Types.Int _, _ ->
fail a_loc
"%s converts a pointer, found %s. There is no conversion between \
an integer and a pointer"
spelled (tyname loc a.Tast.ty)
| Types.Dyn, _ ->
fail a_loc
"%s converts a pointer, found dyn. A dyn value never holds a \
pointer; give %s a pointer type"
spelled spelled_a
| other, _ ->
fail a_loc
"%s converts a pointer, found %s. The address of a place is \
(addr %s); write (%s (addr %s))"
spelled (tyname loc other) spelled_a spelled spelled_a)
| _ ->
fail loc "%s takes one pointer, given %d" spelled (List.length args))
(* A computed head: ((choose k) 3). The head is an ordinary expression and
the only thing asked of it is that it be a function. *)
| _ -> call_value ctx ~want loc (check ctx head) args
(* The indirect call, once the callee is checked. Shared by the computed head
above and by a name that resolved to a local or a parameter of function
type, which is the shape every caller of [map] has. *)
and call_value ctx ~want loc (callee : Tast.expr) args =
(* Either function type: calling one is calling the other, and the
difference — whether an environment rides along — is the backend's to
lower. Nothing here has to know which. *)
match fn_sig callee.Tast.ty with
| Some (params, ret) ->
if List.length args <> List.length params then
fail loc "this function value takes %d argument%s, given %d"
(List.length params)
(if List.length params = 1 then "" else "s")
(List.length args);
let args = map2_lr (fun p a -> check ctx ~want:p a) params args in
expect ctx loc ~want (mk loc ret (Tast.CallPtr (callee, args)))
| None ->
fail loc "this is a %s and not a function, so it cannot be called"
(tyname loc callee.Tast.ty)
(* A builtin's arity. The count is the builtin's and can only be the
builtin's: a defn of the same name written in the program now takes the
call over before any builtin arm is reached ([shadows_builtin] at the top
of [named_call]), so a call measured here is a call to the builtin and
there is no second signature for the reader to have meant. The note that
used to say otherwise — "this is the builtin get, which a defn of the same
name does not replace" — described a resolution order this compiler no
longer has. *)
and arity _ctx loc name n args =
if List.length args <> n then
fail loc "%s takes %d argument%s, given %d" name n
(if n = 1 then "" else "s") (List.length args)
(* The operators that take two operands or more: [+ - * /], [min]/[max], the
three bitwise combining operators, and the six comparisons. The first ten
fold left; the ordered comparisons chain and [!=] asks about every pair,
which [cmp_over] and the two pair-pickers under it explain. [%] and the
shifts are not in that set — a chain of remainders or of shifts has no
reading a reader would agree on in advance, so there the arity error is the
useful answer.
Two is the floor, and the two missing cases are refused rather than
invented. Zero operands would have to mean an identity element, 0 for + and
1 for *, and a sum with no terms in it is a typo far more often than it is
an intent. One operand is refused for every operator but [-], whose one
operand form is negation and is [named_call]'s. For [/] it would be the
reciprocal, and integer division makes that a trap: [(/ 3)] would be 0.
A one-operand comparison would have to be [true] — there is no pair to
disagree, and nothing for a lone value to be distinct from — and a test
that is true whatever it is handed is a typo with a value, which is the
worst kind. So it is refused here with the rest. *)
and fold_arity loc name args =
match args with
| _ :: _ :: _ -> ()
| [ _ ] when String.equal name "/" ->
fail loc
"/ takes two arguments or more, given 1 — there is no reciprocal; \
write (/ 1.0 x)"
| [ _ ] when String.equal name "!=" ->
fail loc
"!= takes two arguments or more, given 1 — a test for distinctness \
needs something to be distinct from, as in (!= x y)"
| [ _ ] when is_comparison name ->
fail loc
"%s takes two arguments or more, given 1 — a comparison needs a second \
value to compare against, as in (%s x y)" name name
| _ ->
fail loc "%s takes two arguments or more, given %d" name (List.length args)
and is_comparison = function
| "=" | "!=" | "<" | "<=" | ">" | ">=" -> true
| _ -> false
(* The first two operands decide the type — [binary] picks which of them is
allowed to, and that decision is not re-made per pair — and every operand
after them is checked against it. *)
(* The operand, not the form. "[+] takes numbers, found string" with the caret
over the whole [(+ "a" "b")] is the exact "whole form vs operand" shape the
[check_truthy] work already fixed once: the reader has to find which of the
operands is the one being talked about, and the compiler knew.
And a text operand gets the extra clause, because [+] on two strings is a
reach for concatenation and the answer is a function rather than an
operator here. Named without a call shape on purpose: [concat] and [join]
take a slice of byte slices and the spelling that builds one from string
literals is not a clause in a sentence. *)
and not_numeric name what (a : Tast.expr) =
let text =
match a.Tast.ty with
| Types.String -> true
| Types.Slice (_, (Types.Int Types.U8)) -> true
| _ -> false
in
let where = a.Tast.loc in
if a.Tast.ty = Types.Bool && String.equal what "integers" then
bool_bits where name
else if a.Tast.ty = Types.Char then char_arith where name
else if text then
fail where
"%s takes %s, and this is %s — there is no %s on text. The prelude \
concatenates with concat and join"
name what (tyname where a.Tast.ty) name
else
fail where "%s takes %s, found %s" name what (tyname where a.Tast.ty)
(* What a char refuses: every operator but [+] and [-] with an integer and
[-] with a char ([char_step], decision 131). The refusal names the two
conversions. *)
and char_arith loc name =
let fln = fln_source loc in
Loc.failk "check/char-arithmetic" loc
"%s. Take its code point with %s, and make a char of one with %s"
(match name with
| "+" -> "+ adds an integer to a char, and not a char to a char"
| "-" -> "- takes an integer or a char from a char, and not a char from \
an integer"
| _ -> name ^ " does no arithmetic on a char")
(if fln then "i32(c)" else "(i32 c)")
(if fln then "char(n)" else "(char n)")
(* One step of char arithmetic (decision 131, Kotlin's rules): a char plus or
minus an integer is a char, checked to be a scalar value — at compile time
when both sides are constants, at run time otherwise — and a char minus a
char is the distance between them, an integer at the width the site wants
(i32 when it wants none), as any integer expression is. A step with no char
in it is the ordinary one. Anything else with a char in it is refused. *)
and char_step ~want loc name (a : Tast.expr) (b : Tast.expr) : Tast.expr =
let i64 e = widen loc dyn_i64 e in
let const (e : Tast.expr) =
match e.Tast.e with Tast.Int (n, _) -> Some n | _ -> None
in
let op = if String.equal name "+" then Tast.Add else Tast.Sub in
let scalar n =
Int64.compare n 0L >= 0 && Int64.compare n 0x10ffffL <= 0
&& not (Int64.compare n 0xd800L >= 0 && Int64.compare n 0xdfffL <= 0)
in
let not_scalar n =
Loc.failk "check/char-range" loc
"this is %s, which is not a Unicode scalar value, so it is not a \
char. A char is a code point from 0 to 0x10FFFF, outside 0xD800 \
to 0xDFFF" n
in
let kind = match want with Some (Types.Int k) -> k | _ -> Types.I32 in
match a.Tast.ty, b.Tast.ty with
| Types.Char, Types.Char when String.equal name "-" ->
let t = Types.Int kind in
(match const a, const b with
| Some x, Some y -> int_literal loc ~want:(Some t) (Int64.sub x y)
| _ -> mk loc t (Tast.Prim (Tast.Sub, [ widen loc t a; widen loc t b ])))
| Types.Char, Types.Int k | Types.Int k, Types.Char
when not (String.equal name "-" && b.Tast.ty = Types.Char) ->
let c, n = if a.Tast.ty = Types.Char then a, b else b, a in
(match const c, const n with
(* A u64 past the largest i64 is held as a negative i64: no char. *)
| Some _, Some v when k = Types.U64 && Int64.compare v 0L < 0 ->
not_scalar (Printf.sprintf "past 0x10FFFF")
| Some x, Some y ->
let r = if op = Tast.Add then Int64.add x y else Int64.sub x y in
if scalar r then mk loc Types.Char (Tast.Int (r, Types.U32))
else not_scalar (Int64.to_string r)
(* The runtime takes the integer as itself — a u64 unsigned, anything
else as an i64 — and checks the sum for overflow, so no large one
wraps round to a char or to a wrong number in the trap. *)
| _ ->
rt loc Types.Char
(if k = Types.U64 then "flan_char_step_u64" else "flan_char_step_i64")
[ widen loc (Types.Int Types.I32) c;
(if k = Types.U64 then n else i64 n);
mk loc (Types.Int Types.I32)
(Tast.Int ((if op = Tast.Sub then 1L else 0L), Types.I32));
here loc ])
| _ ->
char_arith (if a.Tast.ty = Types.Char then a.Tast.loc else b.Tast.loc) name;
assert false
(* Whether [e] may be a char, read off its form without checking it: a name
bound to one, a (char n), a function returning one, or [+]/[-] over any
of those. What decides whether a [+] or [-] is checked as char
arithmetic before the want of its site reaches its operands. A char
literal is not on the list: beside a number it is that number. *)
and maybe_char ctx (e : Ast.expr) =
match e.Ast.e with
| Ast.Var n ->
(match lookup ctx n with
| Some b -> b.bty = Types.Char
| None ->
match peek_outer ctx n with
| Some b -> b.bty = Types.Char
| None ->
(match Hashtbl.find_opt ctx.env.globals n with
| Some (t, _) -> t = Types.Char
| None -> false))
| Ast.Call ({ Ast.e = Ast.Var "char"; _ }, [ _ ]) -> true
| Ast.Call ({ Ast.e = Ast.Var ("+" | "-"); _ }, args) ->
List.exists (maybe_char ctx) args
| Ast.Call ({ Ast.e = Ast.Var f; _ }, _) ->
(match Hashtbl.find_opt ctx.env.fns f with
| Some (_, r) -> r = Types.Char
| None -> false)
| _ -> false
(* ── A conversion whose operand is a type variable ─────────────────────
[(i32 x)] where [x] is a [$t]. The concrete question — is this a number —
has no answer during the abstract pass, and asking it anyway is what
refused [(i32 (at xs i))] inside a body bounded [is-integer]. The question
the bound can answer is the one asked here: does what the [where] clause
declares about the variable entail the predicate this conversion needs.
Which predicate that is comes from the *target*, and the rule is the
concrete arm's rule read off a set rather than a type: a conversion legal
at every type the bound admits is legal at the variable, and one illegal at
any of them is refused. A number target needs [is-numeric] — every type it
admits converts to every numeric target today, truncating or rounding by
the same rule a written f64 follows. An enum target needs [is-integer],
because [is-numeric] admits f32 and f64 and a float has no enum reading.
[is-ordered], [is-equal] and [is-hashable] are refused: they say what can be
compared, not what is a number, and nothing about a bound that only orders
says a conversion means anything. That they happen to admit only numbers
and enums today is a fact about [Types.is_comparable], not about what the
predicate claims — keying conversions to it would make widening [is-ordered]
to strings a silent change to what converts.
The message says what the variable is known to be and what to write. Both
spellings compile as written, and the clause spelling is [unconstrained]'s
so that the family says it one way: a body with no clause is given the
whole clause, and a body that already has one is told which predicate to
add rather than a clause that would drop the ones it has. *)
and cast_operand ctx loc name ~needs ?also ~what ~is v =
if declares ctx.env.tvpreds v needs
|| (match also with
| Some (p, _) -> declares ctx.env.tvpreds v p
| None -> false)
then ()
else
(* A conversion two bounds license is refused naming both, since which one
the reader meant is theirs to say. *)
let is = match also with Some (_, is') -> is ^ " or " ^ is' | None -> is in
let declared =
List.filter_map
(fun (p : Ast.pred) ->
if String.equal p.Ast.pvar v then Some p.Ast.pname else None)
ctx.env.tvpreds
in
let known =
match declared with
| [] -> Printf.sprintf "Nothing here says %s is %s" v is
| ps ->
Printf.sprintf
"The where clause says %s is %s, and that does not make it %s" v
(String.concat " and " (List.map pred_word ps)) is
in
let fix =
let alt clause =
match also with
| Some (p, is') ->
Printf.sprintf ", or %s for %s"
(Printf.sprintf clause p v) is'
| None -> ""
in
if ctx.env.tvpreds = [] then
Printf.sprintf "write %s at the head of the body%s"
(where_text loc needs ("$" ^ v))
(if Source.indented_at loc then alt "where %s($%s)" else alt "{:where (%s $%s)}")
else if Source.indented_at loc then
Printf.sprintf "add %s($%s) to the where clause%s" needs v (alt "%s($%s)")
else
Printf.sprintf "add (%s $%s) to the where clause%s" needs v
(alt "(%s $%s)")
in
Loc.failk "check/unconstrained-type-variable" loc
"%s converts %s. %s — %s" name what known fix
and fold_left_prim ctx ~want loc name p ~needs ok what args =
refuse_kept_when ctx name args;
let x, y, rest =
match args with x :: y :: rest -> x, y, rest | _ -> assert false
in
let charish = String.equal name "+" || String.equal name "-" in
let nwant = numeric_want want in
(* A char literal is a char here, not the number, when no number is
wanted; one that may be a char by its form keeps the want off the pair,
which a char minus a char answers at the want's width itself. *)
let int_lit (e : Ast.expr) = match e.Ast.e with Ast.Int _ -> true | _ -> false in
(* A char literal later in the chain is a char only while everything
before it is an integer literal too; beside a typed number it is that
number, as in (+ b c \0) over bytes. *)
let untyped = ref (int_lit x && int_lit y) in
let char_lit (e : Ast.expr) =
match e.Ast.e with Ast.Byte _ -> nwant = None && !untyped | _ -> false
in
let a, b =
try
(match x.Ast.e, y.Ast.e with
| Ast.Int _, Ast.Byte _ when charish && nwant = None ->
raise_notrace (Char_pair (check ctx x, check ctx y))
| _ -> ());
let pwant = if charish && (maybe_char ctx x || maybe_char ctx y) then None else nwant in
char_operands ctx ~charish name [ x; y ] (fun () ->
binary ctx ~dyn_ok:true ~char_ok:charish name loc ~want:pwant [ x; y ])
with Char_pair (a, b) -> a, b
in
(* One dyn operand makes the whole fold dyn, whichever side it is on. The
typed side is boxed by [dyn_fold]; a literal was already built at dyn by
[binary], so [(+ x 1)] over a dyn x folds an i64 one. *)
let is_char (e : Tast.expr) = e.Tast.ty = Types.Char in
if charish && a.Tast.ty <> Types.Dyn && b.Tast.ty <> Types.Dyn
&& (is_char a || is_char b
|| List.exists (maybe_char ctx) rest
|| (let rec any = function
| [] -> false
| r :: tl -> char_lit r || (untyped := !untyped && int_lit r; any tl)
in
let was = !untyped in
let r = any rest in
untyped := was; r)) then
(* Left to right, each step char arithmetic when a char is in it and the
ordinary join when none is: (- \z \a 1) is 25 - 1, and (+ 1 2 \a) is
3 + \a. *)
let rec steps acc = function
| [] -> expect ctx loc ~want acc
| (arg : Ast.expr) :: tl ->
let lit = char_lit arg in
untyped := !untyped && int_lit arg;
if is_char acc || maybe_char ctx arg || lit then
let v = check ctx arg in
if v.Tast.ty = Types.Dyn then dyn_fold ctx ~want loc name [ acc; v ] tl
else if is_char acc || is_char v then
steps (char_step ~want:nwant loc name acc v) tl
else
match fold_operand ctx acc.Tast.ty (expect ctx arg.Ast.loc ~want:(Some acc.Tast.ty) v) with
| `Typed v -> steps (mk loc acc.Tast.ty (Tast.Prim (p, [ acc; v ]))) tl
| `Dyn d -> dyn_fold ctx ~want loc name [ acc; d ] tl
else
match fold_arg ctx acc.Tast.ty arg with
| `Typed v -> steps (mk loc acc.Tast.ty (Tast.Prim (p, [ acc; v ]))) tl
| `Dyn d -> dyn_fold ctx ~want loc name [ acc; d ] tl
in
let first =
if is_char a || is_char b then char_step ~want:nwant loc name a b
else mk loc a.Tast.ty (Tast.Prim (p, [ a; b ]))
in
steps first rest
else if a.Tast.ty = Types.Dyn || b.Tast.ty = Types.Dyn then
dyn_fold ctx ~want loc name [ a; b ] rest
else begin
(* [~needs] is the operator's own bound: [is-numeric] for the arithmetic,
[is-integer] for the bitwise fold. Asking the tighter question here is what
keeps a bitwise body's refusal at the *definition* — under [is-numeric] the
abstract pass admitted [(bit-and x 1)] and the refusal arrived from
inside the generic's source at whichever call site first instantiated at
a float, which is the misplaced diagnostic the pass exists to avoid. *)
unconstrained ctx.env loc name ~needs a.Tast.ty;
(* Past [unconstrained] a variable here is one the [where] clause admitted,
so the concrete predicate below has nothing to say about it — it is
answered again, per copy, at the instantiation. *)
if not (ok a.Tast.ty || generic_ty a.Tast.ty) then not_numeric name what a;
let ty = a.Tast.ty in
let rec steps acc = function
| [] -> expect ctx loc ~want acc
| arg :: tl ->
match fold_arg ctx ty arg with
| `Typed v -> steps (mk loc ty (Tast.Prim (p, [ acc; v ]))) tl
| `Dyn d -> dyn_fold ctx ~want loc name [ acc; d ] tl
in
steps (mk loc ty (Tast.Prim (p, [ a; b ]))) rest
end
(* An operand past a fold's first pair, checked at the type so far. A dyn one
is taken back as the dyn it was, not opened at that type: a typed operand
beside a dyn gives dyn (rule 117), so from there on the fold is the dyn
runtime's, and (+ 1 2 d) is (+ 3 d). *)
and fold_operand ctx ty (v : Tast.expr) =
if v.Tast.ty = Types.Dyn && not (Types.equal ty Types.Dyn) then `Dyn v
else
match opened_dyn ~box:(to_dyn ctx) v with
| Some d -> `Dyn d
| None -> `Typed v
(* An operand past a fold's first pair, from the source: at the type so far,
and on its own terms only when that is refused, as [binary_pair] checks
the second of the pair. A dyn it turns out to be joins the fold as a dyn,
so (+ 1 2 (the dyn nil)) traps at run time as (+ (the dyn nil) 1 2) does
instead of being refused for a nil with no value at i32, and a dyn beside
a [$t] is not opened at the variable. Anything else is checked at the type
again, for real, so its refusal is the one it always gave and recovery
records it. *)
and fold_arg ctx ty (arg : Ast.expr) =
match trial_at ctx arg ty with
| Ok v -> fold_operand ctx ty v
| Error _ ->
(* Only asked, and asked with the literal locals' uses unrecorded: a
[trial] does not take back what the session recorded, and the operand
on its own terms is not how the program reads it unless it is a dyn —
(+ x y) over an int x and a float y would merge the two and move the
refusal onto x. A dyn is then checked again, recorded. *)
let unrecorded f =
match ctx.lits with
| Some s when s.recording ->
s.recording <- false;
Fun.protect ~finally:(fun () -> s.recording <- true) f
| _ -> f ()
in
let is_dyn =
probe ctx arg.Ast.loc (fun () -> unrecorded (fun () -> (check ctx arg).Tast.ty))
= Some Types.Dyn
in
if is_dyn then `Dyn (to_dyn ctx (check ctx arg))
else fold_operand ctx ty (check ctx ~want:ty arg)
(* A pair an arithmetic operator refused, when one operand is a char: that
is the refusal to give, rather than the mismatch between the two. Asked
only after the refusal, so a pair that checks costs nothing more. *)
and char_operands ctx ?(charish = false) name (args : Ast.expr list) f =
try f ()
with Loc.Error _ as ex ->
(* [+] and [-] take a char beside an integer (decision 131): the pair is
read again on its own terms, and [char_step] decides. *)
let own () =
List.map (fun a -> trial ctx (fun () -> check ctx a)) args
in
(match charish, args with
| true, [ x; y ] ->
(match own () with
| [ Ok a; Ok b ]
when (a.Tast.ty = Types.Char
&& (Types.is_integer b.Tast.ty || b.Tast.ty = Types.Char))
|| (b.Tast.ty = Types.Char && Types.is_integer a.Tast.ty) ->
raise_notrace (Char_pair (check ctx x, check ctx y))
| _ -> ())
| _ -> ());
(* A char literal beside a number is that number, so it says nothing
unless every operand is a literal. *)
let all_lit = List.for_all is_literal args in
List.iter
(fun (a : Ast.expr) ->
let lit = match a.Ast.e with Ast.Byte _ -> true | _ -> false in
if all_lit || not lit then
match trial ctx (fun () -> check ctx a) with
| Ok e when e.Tast.ty = Types.Char -> char_arith a.Ast.loc name
| _ -> ())
args;
raise ex
(* An operand is kept, so a form with no else at its end — a [when], a
[cond] or an [if]/[if let] chain with no final else, or a [do] or [let]
ending in one — answers an Option there. Beside a number that is refused
at the form itself, before the operands are checked against each other,
where the number beside it would be blamed instead. One over a dyn answers
a dyn, and that is left to the operator. *)
and refuse_kept_when ctx name (args : Ast.expr list) =
(* The form with no else, found at the end of [a]. *)
let rec else_less (a : Ast.expr) =
match a.Ast.e with
| Ast.If (_, _, None) | Ast.IfLet (_, _, None) -> Some a
| Ast.If (_, _, Some e) | Ast.IfLet (_, _, Some e) ->
if open_tail e then Some a else None
| Ast.Do (_ :: _ as xs) | Ast.Let (_, (_ :: _ as xs)) ->
else_less (List.nth xs (List.length xs - 1))
| _ -> None
and open_tail (e : Ast.expr) =
match e.Ast.e with
| Ast.Do [] -> true
| _ -> else_less e <> None
in
let ty (a : Ast.expr) = probe ctx a.Ast.loc (fun () -> (check ctx a).Tast.ty) in
let number (a : Ast.expr) =
match a.Ast.e with
| Ast.Int _ | Ast.UInt _ | Ast.Float _ | Ast.Byte _ -> true
| _ when else_less a <> None -> false
| _ -> (match ty a with Some t -> Types.is_numeric t | None -> false)
in
List.iter
(fun (a : Ast.expr) ->
match else_less a with
| Some form ->
(match ty a with
| Some (Types.Option _ as t)
when List.exists (fun b -> b != a && number b) args ->
let fln = fln_source form.Ast.loc in
let what =
match form.Ast.e with
| Ast.If (_, _, None) -> if fln then "if without an else" else "when"
| Ast.IfLet _ -> "if let without an else"
| _ -> if fln then "if chain without an else" else "chain without an else"
in
Loc.failk "check/kept-when" form.Ast.loc
"this %s is an operand of %s, so its value is kept, and there it \
gives %s: Some of its value when a test holds, None when none \
does. The other side is a number. Give it an else, or unwrap \
what it gives with match"
what name (tyname form.Ast.loc t)
| _ -> ())
| None -> ())
args
(* The dyn lowering of a fold: one call per operator application, left to
right, each taking and answering a dyn word. The typed side of a mixed pair
is boxed on the way in — [box] is the identity on something already dyn, so
this needs no case analysis of its own. *)
and dyn_fold ctx ~want loc name first rest =
let sym =
match name with
| "+" -> "flan_dyn_add" | "-" -> "flan_dyn_sub"
| "*" -> "flan_dyn_mul" | "/" -> "flan_dyn_div"
| "%" -> "flan_dyn_rem"
| "min" -> "flan_dyn_min" | "max" -> "flan_dyn_max"
| _ -> dyn_bits_sym name
in
(* A bitwise fold takes integers on both sides, and the typed side of a
mixed pair can be asked now rather than at run time. *)
let bitwise = not (List.mem name [ "+"; "-"; "*"; "/"; "%"; "min"; "max" ]) in
if bitwise then
List.iter
(fun (v : Tast.expr) ->
if v.Tast.ty <> Types.Dyn then bits_operand ctx v.Tast.loc name v)
first;
(* The site travels with the operands. A dyn arithmetic trap is this
language's type error, and until now it printed with no file, no line and
no column — [here loc] is the same string literal [cast_dyn] hands the
runtime, and the runtime prints it as a GNU prefix. *)
let apply acc b = rt loc Types.Dyn sym [ acc; box ~ctx loc b; here loc ] in
let operand arg =
if bitwise then begin
let v = check ctx arg in
if v.Tast.ty <> Types.Dyn then bits_operand ctx v.Tast.loc name v;
v
end
else check ctx ~want:Types.Dyn arg
in
let rest = map_lr operand rest in
no_bare_nil (first @ rest);
let acc =
match first with
| [ a; b ] -> apply (box ~ctx loc a) b
| _ -> assert false
in
let acc = List.fold_left apply acc rest in
expect ctx loc ~want acc
(* The runtime's entry point for each bit operation on a dyn int. *)
and dyn_bits_sym name =
match name with
| "bit-and" -> "flan_dyn_bitand" | "bit-or" -> "flan_dyn_bitor"
| "bit-xor" -> "flan_dyn_bitxor" | "bit-not" -> "flan_dyn_bitnot"
| "<<" -> "flan_dyn_shl" | ">>" -> "flan_dyn_shr"
| "rotate-left" -> "flan_dyn_rotl" | "rotate-right" -> "flan_dyn_rotr"
| "popcount" -> "flan_dyn_popcount" | "leading-zeros" -> "flan_dyn_clz"
| "trailing-zeros" -> "flan_dyn_ctz"
| _ -> invalid_arg ("dyn_bits_sym " ^ name)
(* An operand of a bit operation, once it is known not to be dyn: an integer,
or a type variable the where clause bounds by [is-integer]. A bool is the
likeliest thing to arrive here — [a && b] is logical and in C — so it is
answered with the operator that does what was meant. *)
and bits_operand ctx loc name (v : Tast.expr) =
match v.Tast.ty with
| Types.Int _ -> ()
| t when generic_ty t -> unconstrained ctx.env loc name ~needs:"is-integer" t
| Types.Bool -> bool_bits v.Tast.loc name
| Types.Char -> char_arith v.Tast.loc name
| other -> fail loc "%s takes integers, found %s" name (tyname loc other)
(* A bool operand is refused before the operands are joined, and not left to
[bits_operand]: the join sees a bool beside an integer as a plain mismatch,
"expected i32, found bool", which says nothing of [and]. Each operand's own
type is asked in a trial that is always abandoned, so the check leaves no
trace — no slot, no lifted lambda, no recorded refusal — and the real check
below is the only one that counts. A literal is never a bool, and a call to
an arithmetic or bit operator answers a number or a dyn, so neither is
asked. Nor is anything asked while a probe is running: the probe wants a
type, and asking again inside it would check a nest of these once per
level for every level above it, which doubles with each level. *)
and bool_operands ctx name (args : Ast.expr list) =
if not !probing then
let never_bool (a : Ast.expr) =
match a.Ast.e with
| Ast.Int _ | Ast.UInt _ | Ast.Float _ | Ast.Byte _ | Ast.Str _ | Ast.Kw _ ->
true
| Ast.Call ({ Ast.e = Ast.Var h; _ }, _) ->
List.mem h
[ "+"; "-"; "*"; "/"; "%"; "bit-and"; "bit-or"; "bit-xor"; "bit-not";
"&&"; "||"; "^^"; "~~"; "<<"; ">>"; "rotate-left"; "rotate-right";
"popcount"; "leading-zeros"; "trailing-zeros" ]
&& not (shadows_builtin ctx a.Ast.loc h)
| _ -> false
in
let is_bool (a : Ast.expr) =
(not (never_bool a))
&&
let ty = ref None in
probing := true;
Fun.protect ~finally:(fun () -> probing := false) (fun () ->
ignore
(trial ctx (fun () ->
let v = check ctx a in
ty := Some v.Tast.ty;
Loc.failk "check/probe" a.Ast.loc "abandoned")));
!ty = Some Types.Bool
in
List.iter (fun a -> if is_bool a then bool_bits a.Ast.loc name) args
and bool_bits loc name =
let fln = fln_source loc in
let shown =
if not fln then name
else match name with
| "bit-and" -> "&&" | "bit-or" -> "||" | "bit-xor" -> "^^"
| "bit-not" -> "~~" | n -> n
in
let logic =
match name with
| "bit-and" -> Some (if fln then "a and b" else "(and a b)")
| "bit-or" -> Some (if fln then "a or b" else "(or a b)")
| "bit-xor" -> Some (if fln then "a != b" else "(!= a b)")
| "bit-not" -> Some (if fln then "not a" else "(not a)")
| _ -> None
in
Loc.failk "check/bits-of-bool" loc
"%s works on the bits of an integer, and this is a bool. %s" shown
(match logic with
| Some l -> Printf.sprintf "For true and false, write %s" l
| None -> "True and false are combined with and, or and not")
(* A comparison over three operands or more asks about more than one pair, and
every operand is bound to a slot before any pair is looked at. That is what
makes "left to right, exactly once" true of the lowering and not only of
the source: an operand two pairs name is written down once. The spelling a
reader would reach for, [(and (< a b) (< b c))], evaluates b twice, which
is wrong the moment b is a call — that is the whole reason this is a form
the compiler builds rather than a macro.
The pairs are then required to hold, and the conjunction stops at the first
one that does not. That costs nothing observable, because by the time any
pair is compared every operand has already been evaluated — stopping skips
a machine compare, never a call.
[pairs] says which pairs this operator asks about and [link] builds one
comparison, so the two readings below and the dyn lowering of each are the
same code with two arguments changed. *)
and cmp_over ctx loc ty ~pairs ~link ops =
let binds = List.map (fun (e : Tast.expr) -> fresh_slot ctx ty, e) ops in
let locals = List.map (fun (s, _) -> mk loc ty (Tast.Local s)) binds in
let rec conj = function
| [ t ] -> t
| t :: rest ->
mk loc Types.Bool
(Tast.If (t, conj rest, mk loc Types.Bool (Tast.Bool false)))
| [] -> assert false
in
let tests = List.map (fun (a, b) -> link a b) (pairs locals) in
mk loc Types.Bool (Tast.Let (binds, [ conj tests ]))
(* The ordered comparisons chain: [(< a b c)] asks whether a is below b and b
is below c. The other reading, the left fold [(< (< a b) c)], compares a
bool against a number, and there is no program that meant it. So the pairs
are the adjacent ones, n-1 of them, and the operand in the middle is the
one two of them share. *)
and adjacent_pairs xs =
match xs with
| a :: (b :: _ as rest) -> (a, b) :: adjacent_pairs rest
| _ -> []
(* [!=] is the one that does not chain. "Is this sequence increasing" and "are
these values all different" are different questions, and only the first is
about adjacent pairs: under chaining [(!= 1 2 1)] would be true, because
each neighbour differs from the next, while the thing anyone means by it is
false. So [!=] asks about *every* pair — Common Lisp's [/=] — which is
n(n-1)/2 comparisons rather than n-1.
That growth is fine at the sizes anyone writes: four operands is six
compares of values already in slots. It is also invisible to every program
there is today, because two operands is one pair either way and does not
come through here at all. *)
and all_pairs xs =
match xs with
| [] -> []
| x :: rest -> List.map (fun y -> (x, y)) rest @ all_pairs rest
(* ── Allocation failure, spec-memory.md ────────────────────────────────
No allocating operation returns an error and none can fail silently. When
the allocator cannot satisfy a request the operation signals
(StorageExhausted {.bytes n .align a .allocator id})
with [error] — whose type is Never — inside a [restart-case] offering
[retry]. That is one rule over every allocating operation, which is what
keeps [push] and [reserve] at Unit, [clone] at the container, and no
signature anywhere growing a Result. Odin's [append] returns an ignorable
Allocator_Error and its type-erased path returns the old length on a failed
reserve; an append that appends nothing and says nothing is the outcome this
rule exists to make impossible.
It is *compiler-emitted at the point of failure*, which spec-memory.md names
as the exception to plan.org's "restarts go at the resync point, once": a
restart established at a parser's top-level loop cannot re-attempt an
allocation, and only the allocation site can.
The shape is built out of nodes that already exist — a while, a restart-case
and an error — so the backend learns nothing new about allocation:
(let [ok false]
(while (not ok)
(restart-case
(do (set ok ATTEMPT)
(if (not ok) (error (StorageExhausted {...}))))
(retry []))))
A handler that frees something, releases a scratch region or grows the arena
and then invokes [retry] lands in the clause, the clause falls through, and
the while re-tests and re-attempts the *same* request. With nothing handling
it, [error] stops the program on the frame that erred, as §2 says.
[attempt] must be a call that can be repeated: every argument to it is bound
to a slot before the loop, so a retry does not re-evaluate the element
expression a push was given. *)
and alloc_guard ctx loc (attempt : Tast.expr) =
let ok = fresh_slot ctx Types.Bool in
let okv = mk loc Types.Bool (Tast.Local ok) in
let notok () = mk loc Types.Bool (Tast.Prim (Tast.Not, [ okv ])) in
let i8 n = mk loc (Types.Int Types.I8) (Tast.Int (n, Types.I8)) in
(* The runtime answers 1 or 0 and never reports failure any other way. *)
let attempt = mk loc Types.Bool (Tast.Prim (Tast.Ne, [ attempt; i8 0L ])) in
(* A value struct on the signalling frame's stack, with fixed numeric fields
and no rendered message: formatting would allocate, and this is the one
path that must not. Rendering happens in the handler or the break loop,
where a working allocator is known. *)
let cond =
mk loc (Types.Named "StorageExhausted")
(Tast.Make
("StorageExhausted",
[ rt loc (Types.Int Types.I64) "flan_alloc_fail_bytes" [];
rt loc (Types.Int Types.I64) "flan_alloc_fail_align" [];
rt loc (Types.Int Types.I64) "flan_alloc_fail_id" [] ]))
in
let signal =
mk loc Types.Never
(Tast.Signal (Tast.Serror, condition_desc ctx loc "StorageExhausted",
cond))
in
let attempt_then_signal =
mk loc Types.Unit
(Tast.Do
[ mk loc Types.Unit (Tast.Set (Tast.Plocal ok, attempt));
mk loc Types.Unit (Tast.If (notok (), signal, unit_at loc)) ])
in
let clause =
(* Compiler-emitted, so it takes no parameters: nothing outside can hand
this one a value. [rsig] is therefore the empty signature, and its hash
the same one a written [(retry [] ...)] gets — the two must agree, since
an [invoke-restart] cannot tell them apart. *)
let sg = restart_sig [] in
{ Tast.rname_id = type_id "retry"; rname = "retry"; rparams = [];
rsig = sg; rsig_id = type_id sg; rbody = [ unit_at loc ];
rloc = loc; rreport = "Try the allocation again"; rhidden = false }
in
let body =
mk loc Types.Unit (Tast.RestartCase ([ clause ], attempt_then_signal))
in
mk loc Types.Unit
(Tast.Let ([ (ok, mk loc Types.Bool (Tast.Bool false)) ],
[ mk loc Types.Unit (Tast.While (notok (), [ body ], [])) ]))
(* ── File failure, decisions 2 and 5 ───────────────────────────────────
The same shape [alloc_guard] has, for the same reason and out of the same
nodes: the operation signals inside a [restart-case] it establishes itself,
so nothing anywhere grows a Result and neither [slurp] nor [barf] can fail
silently. Compiler-emitted at the point of failure, which spec-memory.md
already names as the exception to plan.org's "restarts go at the resync
point, once" — a restart at an outer loop cannot re-open a file.
Two restarts, and they are the textbook pair Common Lisp establishes for a
file-error:
retry the file may be there now — the handler made a
directory, mounted something, or waited.
use-value [p string] try this other path instead.
[use-value]'s parameter *is* the path slot, so the clause body is [unit]:
emit.ml's [bind_params] stores the invoker's argument straight into the slot
the attempt reads, the clause falls through, and the while re-tests and
re-attempts against the new path. Typed restarts landed this session and
this is the first thing the compiler itself emits one for.
[attempt] must be repeatable, so the path is a slot read at each turn of the
loop rather than an expression re-evaluated. *)
and file_guard ctx loc ~path_slot ~op mk_steps =
let ok = fresh_slot ctx Types.Bool in
let okv = mk loc Types.Bool (Tast.Local ok) in
let notok () = mk loc Types.Bool (Tast.Prim (Tast.Not, [ okv ])) in
let i8 n = mk loc (Types.Int Types.I8) (Tast.Int (n, Types.I8)) in
(* Fixed fields and no rendered message, exactly as StorageExhausted: the
condition is built on the failing frame's stack and formatting is the
handler's job. [path] is whatever the attempt last used, so a handler that
supplied one through [use-value] sees the path that actually failed. *)
let cond =
mk loc (Types.Named "FileError")
(Tast.Make
("FileError",
[ mk loc Types.String (Tast.Local path_slot);
mk loc (Types.Int Types.I32) (Tast.Int (Int64.of_int op, Types.I32));
mk loc (Types.Int Types.I32)
(Tast.Prim (Tast.Cast (Types.Int Types.I32),
[ rt loc (Types.Int Types.I64)
"flan_file_fail_reason" [] ])) ]))
in
let signal () =
mk loc Types.Never
(Tast.Signal (Tast.Serror, condition_desc ctx loc "FileError", cond))
in
(* One step of the attempt: run the runtime call, record whether it worked,
and signal if it did not. The last step a caller gives is what leaves [ok]
true, which is what stops the loop. *)
let try_ (attempt : Tast.expr) =
mk loc Types.Unit
(Tast.Do
[ mk loc Types.Unit
(Tast.Set (Tast.Plocal ok,
mk loc Types.Bool (Tast.Prim (Tast.Ne, [ attempt; i8 0L ]))));
mk loc Types.Unit (Tast.If (notok (), signal (), unit_at loc)) ])
in
let clause name report params =
let sg = restart_sig (List.map snd params) in
{ Tast.rname_id = type_id name; rname = name; rparams = params;
rsig = sg; rsig_id = type_id sg; rbody = [ unit_at loc ];
rloc = loc; rreport = report; rhidden = false }
in
let body =
mk loc Types.Unit
(Tast.RestartCase
([ clause "retry" "Try the file operation again" [];
clause "use-value" "Try again with another path"
[ (path_slot, Types.String) ] ],
mk loc Types.Unit (Tast.Do (mk_steps try_))))
in
mk loc Types.Unit
(Tast.Let ([ (ok, mk loc Types.Bool (Tast.Bool false)) ],
[ mk loc Types.Unit (Tast.While (notok (), [ body ], [])) ]))
(* Is this bare symbol the name of a type? Every table [resolve_name] will look
in, and the data type table is one of them: a data type is [Named] exactly as a
struct is, so (vec-new Form) is as ordinary as (vec-new Cell). It was left
out when data types landed, which made the prelude's own (vec-new Form) fail
with "nothing here says what (vec-new) is a Vec of" — a message about a
missing annotation for a program that had written one. One list, read by
both callers, so the next kind of type added cannot be added to one of
them. *)
(* A global value that a bare name in a type position would reach instead of
a type. A type variable in scope is not shadowed by one: the prelude's
generics write [(vec-new t)], and a program's [(defonce t ...)] must not
change what the prelude means. *)
and global_value ctx n =
Hashtbl.mem ctx.env.globals n && not (tyvar_in_scope ctx.env n)
(* An argument written as a type: a type expression, or a bare name that is a
type and not a local or a global of the same spelling. *)
and type_arg ctx (a : Ast.expr) =
type_of_expr ~generic:(Hashtbl.mem ctx.env.gstructs) a <> None
|| (match a.Ast.e with
| Ast.Var n ->
lookup ctx n = None && not (global_value ctx n) && type_named ctx n
| _ -> false)
and type_named ctx n =
(* A type variable names a type here too, which is what lets [(vec-new t)]
and [(vec-new $t)] be written in a generic body: inside an instantiation
[resolve_name] answers with the concrete element type, and during the
abstract pass it answers [Var t] and the [Vec] that comes back is a
[(Vec t)] — generic, and refused by anything that needs a size. *)
tyvar_in_scope ctx.env n
(* A sigil is only ever written where a type goes, so a name carrying one is
answered here even when nothing binds it: [resolve_name] then says that a
variable has no binding site outside a defn signature, which is the
mistake, instead of this form reporting a missing element type. *)
|| n <> tyvar_bare n
|| List.mem n Types.primitive_names
(* Answered as a type so [resolve_name] says it is spelled [str], rather
than this form saying no element type was written. *)
|| n = "string"
|| Hashtbl.mem ctx.env.structs n
|| Hashtbl.mem ctx.env.datas n
|| Hashtbl.mem ctx.env.unions n
|| Hashtbl.mem ctx.env.enums n
|| Hashtbl.mem ctx.env.aliases n
(* A local or global that shares a type's name — [str] is a common name for
text — is the binding in (vec-new str), and then nothing names the type.
Said here so the refusal names the binding rather than a missing type. *)
and shadowed_type_arg ctx loc what args =
match args with
| { Ast.e = Ast.Var n; _ } :: _
when type_named ctx n && (lookup ctx n <> None || global_value ctx n) ->
fail loc
"%s here is the value named %s and not the type, so nothing says what \
(%s) makes — rename that binding to write the type" n n what
(* [vec-new(grain?)] for a lowercase type: where a value is written, ? after
anything but a capitalised or primitive name is the test [x?]. *)
| { Ast.e = Ast.Call ({ Ast.e = Ast.Var "?"; _ }, [ { Ast.e = Ast.Var n; _ } ]); _ } :: _
when type_named ctx n && lookup ctx n = None && not (global_value ctx n) ->
fail loc
"%s? here is the test that a value is present, and %s is a type. Where a \
value is written, the Option of a type is Option(%s): %s(Option(%s))"
n n n what n
| _ -> ()
(* The element type for [vec-new]: a leading bare symbol naming a type, a
leading type expression — [(vec-new [u8])], [(vec-new (Ptr Cell))], which
Parse has already read as one — or the expectation at the site. A bare
symbol shadowed by a local or a global is that binding — an allocator, in
practice — and not a type. *)
and vec_new_elem ctx ~want loc args =
let named =
match args with
| a :: rest when type_of_expr ~generic:(Hashtbl.mem ctx.env.gstructs) a <> None ->
Some
(resolve ctx.env
(Option.get (type_of_expr ~generic:(Hashtbl.mem ctx.env.gstructs) a)),
rest)
| { Ast.e = Ast.Var n; _ } :: rest
when lookup ctx n = None && not (global_value ctx n) && type_named ctx n ->
Some (resolve_name ctx.env ~seen:[] loc n, rest)
| _ -> None
in
match named with
| Some (t, rest) -> t, rest
| None ->
(match want with
| Some (Types.Vec t) -> t, args
| _ ->
shadowed_type_arg ctx loc "vec-new" args;
fail loc
"nothing here says what (vec-new) is a Vec of — write the element \
type, as (vec-new i32)")
(* A type written as an argument to vec-new or map-new, read back out of the
expression Parse made of it. Only the shapes that cannot be a value there:
brackets — an allocator is never an array — or a parenthesised Ptr,
Option, Vec, Map, Fn or CFn. A bare name is not one of them, because there
it may be an allocator's name; the callers ask about that themselves. *)
and type_of_expr ?(generic = fun _ -> false) (e : Ast.expr) : Ast.texpr option =
let mk t = { Ast.t; tloc = e.Ast.loc } in
let inner (e : Ast.expr) =
match e.Ast.e with
| Ast.Var s -> Some { Ast.t = Ast.Tname s; tloc = e.Ast.loc }
| _ -> type_of_expr ~generic e
in
let all es =
let ts = List.filter_map inner es in
if List.length ts = List.length es then Some ts else None
in
match e.Ast.e with
| Ast.TypeArg t -> Some t
(* Before the [[n T]] arm below, which would read [const] as a length. *)
| Ast.Arr [ { Ast.e = Ast.Var "const"; _ }; x ] ->
Option.map (fun t -> mk (Ast.Tslice (true, t))) (inner x)
| Ast.Arr [ x ] -> Option.map (fun t -> mk (Ast.Tslice (false, t))) (inner x)
| Ast.Arr [ { Ast.e = Ast.Int n; _ }; x ] ->
Option.map (fun t -> mk (Ast.Tarray (Ast.Lint n, t))) (inner x)
| Ast.Arr [ { Ast.e = Ast.Var n; _ }; x ] ->
Option.map (fun t -> mk (Ast.Tarray (Ast.Lname n, t))) (inner x)
| Ast.Call ({ Ast.e = Ast.Var (("Fn" | "CFn") as which); _ },
[ { Ast.e = Ast.Arr ps; _ }; r ]) ->
(match all ps, inner r with
| Some ps, Some r -> Some (mk (Ast.Tfn (which = "Fn", ps, r)))
| _ -> None)
| Ast.Call ({ Ast.e = Ast.Var (("Ptr" | "Option" | "Vec" | "Map") as c); _ },
(_ :: _ as args)) ->
Option.map (fun ts -> mk (Ast.Tapp (c, ts))) (all args)
(* A generic struct applied to its arguments, [(vec-new (Small 8 i32))]:
the caller says which heads are ones, since only the env knows. An
integer argument is a length. *)
| Ast.Call ({ Ast.e = Ast.Var c; _ }, (_ :: _ as args)) when generic c ->
let arg (a : Ast.expr) =
match a.Ast.e with
| Ast.Int n -> Some { Ast.t = Ast.Tlen n; tloc = a.Ast.loc }
| _ -> inner a
in
let ts = List.filter_map arg args in
if List.length ts = List.length args then Some (mk (Ast.Tapp (c, ts)))
else None
| _ -> None
(* The key and value types, or the reason this is not a Map. *)
and map_kv loc what (t : Types.t) =
match t with
| Types.Map (k, v) -> k, v
| other -> fail loc "%s takes a (Map K V), found %s" what (tyname loc other)
(* The key and value for [map-new]: two leading bare symbols naming types, or
the expectation at the site. The same rule [vec-new] uses, with the same
escape for a symbol that is really a binding — an allocator, in practice —
and the pair is written together or not at all, because (map-new str)
says half of a type and half is not a type. *)
and map_new_types ctx ~want loc args =
let is_type n =
lookup ctx n = None && not (global_value ctx n) && type_named ctx n
in
(* A type position holds a bare name or a type expression Parse has read
as one, as [vec-new]'s does. *)
let as_type (a : Ast.expr) =
match a.Ast.e, type_of_expr ~generic:(Hashtbl.mem ctx.env.gstructs) a with
| _, Some t -> Some (resolve ctx.env t)
| Ast.Var n, None when is_type n -> Some (resolve_name ctx.env ~seen:[] loc n)
| _ -> None
in
match args with
| k :: v :: rest when as_type k <> None && as_type v <> None ->
Option.get (as_type k), Option.get (as_type v), rest
| a :: _ when type_of_expr ~generic:(Hashtbl.mem ctx.env.gstructs) a <> None ->
fail loc
"(map-new) names a key and no value — write both, as (map-new str \
i32)"
| { Ast.e = Ast.Var k; _ } :: rest when is_type k && rest = [] ->
fail loc
"(map-new %s) names a key and no value — write both, as (map-new %s \
i32)" k k
| _ ->
(match want with
| Some (Types.Map (k, v)) -> k, v, args
| _ ->
shadowed_type_arg ctx loc "map-new" args;
fail loc
"nothing here says what (map-new) maps — write the key and value \
types, as (map-new str i32)")
(* The element type, or the reason this is not a Vec. *)
and vec_elem loc what (t : Types.t) =
match t with
| Types.Vec e -> e
| other -> fail loc "%s takes a (Vec T), found %s" what (tyname loc other)
(* The allocator an operation uses: the one named at the site, or the current
implicit one. spec-memory.md: an operation never falls back to a hidden
global allocator, and an explicit allocator can override the context. *)
and allocator_arg ctx loc = function
| [] -> rt loc raw_alloc "flan_context_use" [ here loc ]
| [ a ] -> alloc_value ctx loc a
| _ -> fail loc "at most one allocator may be named here"
(* An Allocator value, checked and opened: the record every runtime call
takes, after [use_alloc] has compared the incarnation. *)
and alloc_value ctx loc e = use_alloc ctx loc (check ctx ~want:Types.Alloc e)
(* A copy of [src]'s elements — a string's bytes or a slice's elements — into
a block from [a], answered as a slice over it: (bytes s), (clone xs) and the
number conversions. The lowering mirrors [vec-new]: a hidden (Vec T) temp
holds the block so the allocation registry can read its extent, the attempt
sits under [alloc_guard] so a failure signals StorageExhausted with retry,
and the answer is the [slice] of the whole of it. The slice carries no
allocator: (free s) hands the block back to the context allocator or the
one named, and a dev build's registry — which the note gives the Vec's
allocator — refuses the wrong one.
The source is bound before the guard's loop, so a retry re-attempts the
same copy rather than re-evaluating the expression that produced it. Same
rule as [push]'s element. No [region_check]: that guard compares a Vec
header being *stored* against the region it lands in, and the header here
is a temp nothing stores. *)
and dup_elems ctx loc elem (src : Tast.expr) (a : Tast.expr) =
let sty = src.Tast.ty in
let sv = fresh_slot ctx sty in
let v = fresh_slot ctx (Types.Vec elem) in
let out = fresh_slot ctx (Types.Slice (Types.Mut, elem)) in
let attempt =
rt loc (Types.Int Types.I8) "flan_bytes_dup"
[ mk loc (Types.Vec elem) (Tast.Local v); a;
mk loc sty (Tast.Local sv); size_of loc elem; align_of loc elem;
here loc ]
in
let fill =
rt loc Types.Unit "flan_vec_as_slice"
[ mk loc (Types.Vec elem) (Tast.Local v);
addr_of loc (mk loc (Types.Slice (Types.Mut, elem)) (Tast.Local out));
mk loc index_ty (Tast.Int (0L, Types.I32));
mk loc index_ty (Tast.Int (-1L, Types.I32));
size_of loc elem; here loc ]
in
mk loc (Types.Slice (Types.Mut, elem))
(Tast.Let
([ (sv, src);
(v, mk loc (Types.Vec elem) (Tast.Zero (Types.Vec elem)));
(out, mk loc (Types.Slice (Types.Mut, elem)) (Tast.Zero (Types.Slice (Types.Mut, elem)))) ],
[ with_note loc (alloc_guard ctx loc attempt)
(reg_note loc "flan_dev_reg_note_slice"
(mk loc (Types.Vec elem) (Tast.Local v))
[ size_of loc elem ] elem);
fill;
mk loc (Types.Slice (Types.Mut, elem)) (Tast.Local out) ]))
(* The address of an element, bounds-checked, with the allocator's epoch
checked first. Both the value form [(at v i)] and the place form
[(set (at v i) x)] come through here, so they cannot drift apart — which is
the asymmetry [nth] was removed for. *)
(* A new, empty (Vec elem) from the opened allocator [a]: (vec-new)'s lowering,
which (string-new) shares for the Vec under a String. *)
and vec_init ?note ctx loc elem (a : Tast.expr) =
let note = Option.value note ~default:elem in
let v = fresh_slot ctx (Types.Vec elem) in
let attempt =
rt loc (Types.Int Types.I8) "flan_vec_init"
[ mk loc (Types.Vec elem) (Tast.Local v); a; i64_at loc 0L;
size_of loc elem; align_of loc elem; here loc ]
in
mk loc (Types.Vec elem)
(Tast.Let ([ (v, mk loc (Types.Vec elem) (Tast.Zero (Types.Vec elem))) ],
[ with_note loc (alloc_guard ctx loc attempt)
(reg_note loc "flan_dev_reg_note_vec"
(mk loc (Types.Vec elem) (Tast.Local v))
[ size_of loc elem ] note);
region_check ctx.env loc
(mk loc (Types.Vec elem) (Tast.Local v))
(mk loc (Types.Vec elem) (Tast.Local v)) ]))
and vec_at ctx loc (target : Tast.expr) (idx : Ast.expr list) =
let elem = vec_elem loc "at" target.Tast.ty in
match idx with
| [ i ] ->
let i = index_expr ctx i in
rt loc (Types.Ptr (Types.Mut, elem)) "flan_vec_at"
[ target; i; size_of loc elem; here loc ], elem
| _ ->
fail loc
"a Vec takes exactly one index, as (at v i)"
(* [(get xs i ...)] over an array, a slice, a string or a Vec: the element
as [(Some e)], or [None] when any index is out of range, negative
included, where [at] would trap. One index per dimension, as [at] takes.
The indices are evaluated once, left to right, before any test. An array's
length is static, so the array itself is read once and is not copied; a
slice's, a string's or a Vec's is read off the value, so that value is
put in a slot first unless it is already a name. Each level is tested
before the next is reached, because a Vec of Vecs has no inner length to
test until the outer index is known to be in range. The element is then
read by [at] as usual, whose own check can no longer fail. *)
and checked_get ctx ~want loc (target : Tast.expr) (idx : Ast.expr list) =
let rec result ty = function
| [] -> ty
| (i : Ast.expr) :: rest ->
(match ty with
| Types.Array (_, t) | Types.Slice (_, t) | Types.Vec t -> result t rest
| Types.String -> result (Types.Int Types.U8) rest
| other ->
fail i.Ast.loc
"get takes an array, a slice, a string, a Vec, a Map or a dyn, and \
%s cannot be indexed" (tyname i.Ast.loc other))
in
let oty = Types.Option (result target.Tast.ty idx) in
let none () = mk loc oty Tast.None_ in
let is_name (e : Tast.expr) =
match e.Tast.e with Tast.Local _ | Tast.Global _ -> true | _ -> false
in
(* A value a call answered is bound before the indices run, so the target
is still evaluated first. *)
let pre = ref [] in
let target =
match target.Tast.e with
| Tast.Call _ | Tast.CallPtr _ ->
let s = fresh_slot ctx target.Tast.ty in
pre := [ (s, target) ];
mk loc target.Tast.ty (Tast.Local s)
| _ -> target
in
let islots =
map_lr
(fun (i : Ast.expr) ->
let v = index_expr ctx i in
let s = fresh_slot ctx index_ty in
(s, v))
idx
in
let ivar (s, _) = mk loc index_ty (Tast.Local s) in
let i32 k = mk loc index_ty (Tast.Int (k, Types.I32)) in
let within i len =
let ge = mk loc Types.Bool (Tast.Prim (Tast.Ge, [ i; i32 0L ])) in
let lt = mk loc Types.Bool (Tast.Prim (Tast.Lt, [ i; len ])) in
mk loc Types.Bool (Tast.If (ge, lt, mk loc Types.Bool (Tast.Bool false)))
in
(* The value reached so far is [base] indexed by [path], innermost last. *)
let reached base path ty =
if path = [] then base
else mk loc ty (Tast.Prim (Tast.At, base :: List.rev path))
in
(* A value whose length is read as well as indexed, in a slot unless it is
a name already. *)
let named cur k =
if is_name cur then k cur
else
let s = fresh_slot ctx cur.Tast.ty in
mk loc oty
(Tast.Let ([ (s, cur) ], [ k (mk loc cur.Tast.ty (Tast.Local s)) ]))
in
let rec go base path ty = function
| [] -> mk loc oty (Tast.Some_ (reached base path ty))
| i :: rest ->
let i = ivar i in
(match ty with
| Types.Array (n, t) ->
mk loc oty (Tast.If (within i (i32 n), go base (i :: path) t rest, none ()))
| Types.Slice _ | Types.String ->
let t =
match ty with Types.Slice (_, t) -> t | _ -> Types.Int Types.U8
in
named (reached base path ty) (fun cur ->
let len = mk loc index_ty (Tast.Prim (Tast.Len, [ cur ])) in
mk loc oty (Tast.If (within i len, go cur [ i ] t rest, none ())))
| Types.Vec t ->
named (reached base path ty) (fun cur ->
let n = rt loc (Types.Int Types.I64) "flan_vec_len" [ cur; here loc ] in
let len = mk loc index_ty (Tast.Prim (Tast.Cast index_ty, [ n ])) in
let p =
rt loc (Types.Ptr (Types.Mut, t)) "flan_vec_at"
[ cur; i; size_of loc t; here loc ]
in
mk loc oty
(Tast.If (within i len, go (mk loc t (Tast.Deref p)) [] t rest,
none ())))
| _ -> assert false)
in
expect ctx loc ~want
(mk loc oty (Tast.Let (!pre @ islots, [ go target [] target.Tast.ty islots ])))
(* [(get d k ...)] over a dyn: a map's value at the key, a vec's or a text's
element at the index, or nil when there is none — dyn has no Option. More
than one key walks a level per key, and a nil level answers nil. *)
and dyn_get ctx ~want loc (target : Tast.expr) (keys : Ast.expr list) =
let nil () = rt loc Types.Dyn "flan_dyn_nil" [] in
let one v k = rt loc Types.Dyn "flan_dyn_get_at" [ v; k; here loc ] in
(* A slot per key only when there are several: one key goes straight to
[flan_dyn_get_at]. Over a text or a vec that runs [at]'s own body in the
runtime, so [get] and [at] count a text the same way whatever [at]
comes to count. *)
let slotted = List.length keys > 1 in
let keys =
map_lr
(fun k ->
let v = check ctx ~want:Types.Dyn k in
((if slotted then fresh_slot ctx Types.Dyn else -1), v))
keys
in
let kvar (s, _) = mk loc Types.Dyn (Tast.Local s) in
let rec go v = function
| [] -> v
| k :: rest when rest = [] -> one v (kvar k)
| k :: rest ->
let s = fresh_slot ctx Types.Dyn in
let sv = mk loc Types.Dyn (Tast.Local s) in
let is_nil =
mk loc Types.Bool
(Tast.Prim (Tast.Ne,
[ rt loc (Types.Int Types.I32) "flan_dyn_is_nil" [ sv ];
mk loc (Types.Int Types.I32) (Tast.Int (0L, Types.I32)) ]))
in
mk loc Types.Dyn
(Tast.Let ([ (s, one v (kvar k)) ],
[ mk loc Types.Dyn (Tast.If (is_nil, nil (), go sv rest)) ]))
in
match keys with
| [ (_, k) ] -> expect ctx loc ~want (one target k)
| _ ->
let ts = fresh_slot ctx Types.Dyn in
expect ctx loc ~want
(mk loc Types.Dyn
(Tast.Let ((ts, target) :: keys,
[ go (mk loc Types.Dyn (Tast.Local ts)) keys ])))
(* [(slice v)], [(slice v lo)] and [(slice v lo hi)] over a Vec — the arm for
it is in [slice], and this is the half that differs from an array's.
The result is a non-owning view: copying it copies ptr+len and never the
elements, and it carries no allocator, so freeing through one is not
expressible. What makes a Vec's view different from an array's is that the
storage it names can move — a [push], a [put] or a [reserve] may reallocate
and leave the view addressing the old block. Nothing checks that, which is
the explicit Zig/Odin contract spec-memory.md chose over a borrow checker,
and it is written down where a reader meets it: beside [push] in BUILT.md
and in spec-memory.md's "Borrowing".
It used to be spelled [as-slice], on the theory that a second name warns
about that. It does not: the input type already decides which of the two
things happens, so there was no choice at the call site for the name to
express — and the moment it warned about was the moment the view is taken,
while the danger arrives later, at the push.
No slot and no length read at any arity: -1 is the runtime's "to the end",
so the short forms pass a constant where an array passes its length, and
the target appears exactly once in all three.
A Vec a *call* returned is accepted, where an array a call returned is
refused a screen down. The array is a dangle — the view outlives a
temporary the frame reuses — and this is not: the storage a returned Vec
owns lives to its allocator's free-all or destroy, so the view reads what
it says it reads. What a returned Vec loses is the owner, and losing the
owner is a leak, which this language has already decided is defined
behaviour (spec-memory.md on overwriting a global Vec: "overwrites the
first block and leaks it; there is no drop"). [(length (mk))] and
[(at (mk) 0)] lose exactly the same owner and are accepted; refusing the
third of those three would be a rule about one spelling rather than about
a hazard, and under a region allocator there is nothing to leak at all. *)
and vec_slice ctx ~want loc (target : Tast.expr) elem (bounds : Ast.expr list) =
let int k = mk loc index_ty (Tast.Int (k, Types.I32)) in
(* Every bound the reader wrote goes through here, and the -1 below does
not: a negative literal is refused exactly as it is on an array, in the
same words, and that refusal has to happen on the bounds a person wrote
rather than on the pair that comes out of this — the sentinel *is* a -1,
and checking afterwards would refuse (slice v) itself. There is no static
length to check the other direction against. *)
let bound (b : Ast.expr) =
let v = index_expr ctx b in
(match literal v with
| Some k ->
static_index b.Ast.loc (Types.Vec elem) ~past_end:true "slice bound" k
| None -> ());
v
in
(* -1 is the runtime's "to the end". A Vec's length is not static, so unlike
an array there is no constant to fold and the runtime reads the length
word the header is carrying anyway. *)
let to_end () = int (-1L) in
let lo, hi =
match bounds with
| [] -> int 0L, to_end ()
| [ lo ] -> bound lo, to_end ()
| [ lo; hi ] ->
let lo = bound lo and hi = bound hi in
(* The same refusal the other targets get, and it is asked only here,
where both ends are bounds somebody wrote. *)
(match literal lo, literal hi with
| Some a, Some b when a > b ->
fail loc "slice [%Ld %Ld) runs backwards — lo must not exceed hi" a b
| _ -> ());
lo, hi
| _ -> assert false
in
let out = fresh_slot ctx (Types.Slice (Types.Mut, elem)) in
let fill =
rt loc Types.Unit "flan_vec_as_slice"
[ target; addr_of loc (mk loc (Types.Slice (Types.Mut, elem)) (Tast.Local out));
lo; hi; size_of loc elem; here loc ]
in
expect ctx loc ~want
(mk loc (Types.Slice (Types.Mut, elem))
(Tast.Let ([ (out, mk loc (Types.Slice (Types.Mut, elem))
(Tast.Zero (Types.Slice (Types.Mut, elem)))) ],
[ fill; mk loc (Types.Slice (Types.Mut, elem)) (Tast.Local out) ])))
(* ── String ───────────────────────────────────────────────────────────
The prelude's (defstruct String [bytes (Vec u8)]) is valid UTF-8 because
nothing outside the prelude can reach the Vec: its field cannot be named
and the struct cannot be built ([refuse_string_inside]), and it is not
indexed ([refuse_string_index]). Every byte arrives through an arm below.
Text the checker cannot prove valid is checked at run time, at the site
that stores it, and a bad byte stops the program there (flan_utf8_check,
flan_rune_check) — a str may hold any bytes, since (str b) does not check.
A string literal and an integer literal are checked here instead, so text
written in the source costs nothing at run time. A [const u8] is refused
rather than checked: bytes become text through (str b) or (bytes->string
v), which says at the call that a check is wanted.
Positions are characters and lengths are bytes, the rule str has. The
character index is found by a walk (flan_string_index), which signals
BoundsError with the character count as the length. *)
and refuse_string_inside loc =
if not (String.equal loc.Loc.file Prelude.file) then begin
let fln = fln_source loc in
Loc.failk "check/string-private" loc
"a String keeps its bytes to itself, which is how they stay valid \
UTF-8. Make one with %s, read it with %s or %s, and change it with \
append, insert and remove"
(if fln then "string-new(text)" else "(string-new text)")
(if fln then "str(s)" else "(str s)")
(if fln then "bytes-view(s)" else "(bytes-view s)")
end
and refuse_string_index loc ~store =
let fln = fln_source loc in
if store then
Loc.failk "check/string-set-index" loc
"a String cannot be changed one byte at a time. A character in UTF-8 \
is one to four bytes, so writing a single byte can leave text that is \
not valid. Change it by character position instead: %s, then %s"
(if fln then "remove(s, i)" else "(remove s i)")
(if fln then "insert(s, i, c)" else "(insert s i c)")
else
Loc.failk "check/string-index" loc
"a String is not indexed, because in UTF-8 a byte position and a \
character position are different numbers. Read byte i with %s, or \
walk the characters with %s"
(if fln then "str(s)[i]" else "(at (str s) i)")
(if fln then "runes(s)" else "(runes s)")
(* An argument that may be a String: checked with no expectation when its
type does not depend on one — a name, a call, a field — and against
[otherwise] when it does, as the builtin always checked it. *)
and maybe_string ctx ~otherwise (x : Ast.expr) =
match x.Ast.e with
| Ast.Var _ | Ast.Call _ | Ast.Field _ ->
let e = check ctx x in
if string_or_ptr e.Tast.ty then e
else expect ctx x.Ast.loc ~want:(Some otherwise) e
| _ -> check ctx ~want:otherwise x
(* A String's bytes as a [const u8]: a view of the Vec, costing nothing, and
good until the String next grows — (slice v)'s contract. *)
and string_bytes ctx loc (s : Tast.expr) =
let v = vec_slice ctx ~want:None loc (string_vec loc s) u8_ty [] in
{ v with Tast.ty = Types.Slice (Types.Const, u8_ty) }
(* A str, a String or a [const u8], as a [const u8]: what rune-count and
runes read. *)
and text_bytes ctx what (x : Ast.expr) =
let e =
match x.Ast.e with
| Ast.Str _ -> check ctx ~want:Types.String x
| _ -> check ctx x
in
let loc = x.Ast.loc in
match e.Tast.ty with
| Types.String ->
mk loc (Types.Slice (Types.Const, u8_ty)) (Tast.Prim (Tast.Bytes, [ e ]))
| t when string_or_ptr t -> string_bytes ctx loc e
| Types.Slice (_, Types.Int Types.U8) ->
{ e with Tast.ty = Types.Slice (Types.Const, u8_ty) }
| other ->
fail loc "%s takes a str, a String or a [const u8], found %s" what
(tyname loc other)
(* The String an operation changes, written as the String or a pointer to
one, and refused when it is reached through something read-only. *)
and string_target ctx loc what (t : Tast.expr) =
let t =
match t.Tast.ty with
| Types.Ptr (_, ty) when is_string_ty ty -> mk loc ty (Tast.Deref t)
| ty when is_string_ty ty -> t
| other ->
fail loc "%s takes a String to change, found %s" what (tyname loc other)
in
refuse_const_change ctx loc t;
t
(* What append and insert were given to store: text, as a [const u8] and
whether it is known valid, or a code point. *)
and string_piece ctx what (x : Ast.expr) =
let loc = x.Ast.loc in
let rune e =
(match literal e with
| Some c when c < 0L || c > 0x10ffffL || (c >= 0xd800L && c <= 0xdfffL) ->
fail loc
"%Ld is not a Unicode scalar value, so it has no UTF-8 encoding and \
a String cannot hold it" c
| _ -> ());
`Rune e
in
match x.Ast.e with
| Ast.Str lit ->
if not (String.is_valid_utf_8 lit) then
fail loc
"this text is not valid UTF-8, and a String holds only valid UTF-8";
let e = check ctx ~want:Types.String x in
`Text (mk loc (Types.Slice (Types.Const, u8_ty)) (Tast.Prim (Tast.Bytes, [ e ])),
true)
| Ast.Int _ | Ast.Byte _ -> rune (check ctx ~want:(Types.Int Types.I32) x)
| _ ->
let e = check ctx x in
match e.Tast.ty with
| Types.String ->
`Text (mk loc (Types.Slice (Types.Const, u8_ty)) (Tast.Prim (Tast.Bytes, [ e ])),
false)
| t when string_or_ptr t -> `Text (string_bytes ctx loc e, true)
| Types.Int Types.I32 -> rune e
| Types.Char -> rune (widen loc (Types.Int Types.I32) e)
| Types.Int k when Types.widens_to ~from:(Types.Int k) ~into:(Types.Int Types.I32) ->
rune (widen loc (Types.Int Types.I32) e)
| Types.Int _ ->
fail loc
"a code point is an i32, and this is %s. Write %s"
(tyname loc e.Tast.ty)
(if fln_source loc then "i32(c)" else "(i32 c)")
| Types.Slice (_, Types.Int Types.U8) ->
fail loc
"%s takes text, and bytes are not text until they are checked. \
Write %s, which is checked when it is stored"
what (if fln_source loc then "str(b)" else "(str b)")
| other ->
fail loc "%s takes a str, a String, a char or a code point, found %s" what
(tyname loc other)
(* One allocating store into a String's Vec, under the retry guard, with the
dev registry told of the block it ends up in — push's shape. Every argument
is bound to a slot before the loop, so a retry re-attempts only the call. *)
and string_store ctx loc (v : Tast.expr) (binds : (int * Tast.expr) list)
(checks : Tast.expr list) (attempt : Tast.expr) =
mk loc Types.Unit
(Tast.Let
(binds,
checks
@ [ region_check ctx.env loc v
(with_note loc (alloc_guard ctx loc attempt)
(reg_note loc "flan_dev_reg_note_vec" v [ size_of loc u8_ty ]
string_ty)) ]))
(* A text piece or a code point stored at byte offset [off] of the String's
Vec [v], -1 for the end. *)
and string_put ctx loc (v : Tast.expr) (off : Tast.expr) piece =
let i64 = Types.Int Types.I64 in
let o = fresh_slot ctx i64 in
let ov = mk loc i64 (Tast.Local o) in
let at_end = match off.Tast.e with Tast.Int (-1L, _) -> true | _ -> false in
match piece with
| `Rune c ->
let r = fresh_slot ctx (Types.Int Types.I32) in
string_store ctx loc v [ (r, c); (o, off) ] []
(rt loc (Types.Int Types.I8) "flan_string_put_rune"
[ v; ov; mk loc (Types.Int Types.I32) (Tast.Local r); here loc ])
| `Text (b, valid) ->
let bty = Types.Slice (Types.Const, u8_ty) in
let bs = fresh_slot ctx bty in
let bv = mk loc bty (Tast.Local bs) in
let checks =
if valid then [] else [ rt loc Types.Unit "flan_utf8_check" [ bv; here loc ] ]
in
string_store ctx loc v [ (bs, b); (o, off) ] checks
(if at_end then
rt loc (Types.Int Types.I8) "flan_vec_append"
[ v; bv; size_of loc u8_ty; align_of loc u8_ty; here loc ]
else
rt loc (Types.Int Types.I8) "flan_vec_insert"
[ v; ov; bv; size_of loc u8_ty; align_of loc u8_ty; here loc ])
(* A character position as a byte offset, signalling BoundsError when it is
not one. [past_end] admits the position after the last character. *)
and string_index ctx loc (v : Tast.expr) (i : Ast.expr) ~past_end =
let i = index_expr ctx i in
rt loc (Types.Int Types.I64) "flan_string_index"
[ v; i; mk loc (Types.Int Types.I32)
(Tast.Int ((if past_end then 1L else 0L), Types.I32));
here loc ]
(* A prelude function by the name it was written under, which a program's
own function of that name moves aside (see [shadow_prelude]). *)
and prelude_fn ctx name =
let moved = "prelude~/" ^ name in
if Hashtbl.mem ctx.env.fns moved then moved else name
(* Whether [name] is the prelude's own function. *)
and prelude_defined ctx name =
match Hashtbl.find_opt ctx.env.fn_locs name with
| Some at -> String.equal at.Loc.file Prelude.file
| None -> false
(* A call to a prelude function that answers a String, its text arguments
checked here, at the call the program wrote, before the call is made. The
builders — (to-lower b), (join parts sep) — take bytes, and their answer is
valid exactly when every piece of text they were given is (UTF-8 is
self-synchronising, so a valid [from] matches a valid [s] only on
character boundaries). Each builder also checks its own answer, the
backstop for a builder reached through a function value; on this path that
check has nothing left to find, and stops nobody at the prelude's line. *)
and prechecked_call ctx loc name ret (args : Tast.expr list) =
let bytes = function
| Types.Slice (_, Types.Int Types.U8) | Types.String -> true
| _ -> false
in
let binds, checks, uses =
List.fold_right
(fun (a : Tast.expr) (bs, cs, us) ->
let check =
match a.Tast.ty with
| t when bytes t -> Some "flan_utf8_check"
| Types.Slice (_, t) when bytes t -> Some "flan_utf8_check_parts"
| _ -> None
in
match check with
| None -> (bs, cs, a :: us)
| Some sym ->
let sl = fresh_slot ctx a.Tast.ty in
let v = mk a.Tast.loc a.Tast.ty (Tast.Local sl) in
((sl, a) :: bs, rt loc Types.Unit sym [ v; here loc ] :: cs, v :: us))
args ([], [], [])
in
let call = mk loc ret (Tast.Call (name, uses)) in
if checks = [] then call
else mk loc ret (Tast.Let (binds, checks @ [ call ]))
(* Whether an operand is a String, read off what the operand is without
checking it: a local or a global of that type, a call to a function that
answers one, a field of that type, or one of the builtins that make one. A
comparison asks this of every operand, so it must not walk them. *)
and peeks_string ctx (a : Ast.expr) =
let rec ty (a : Ast.expr) =
match a.Ast.e with
| Ast.Var n ->
(match lookup ctx n with
| Some b -> Some b.bty
| None ->
(match Hashtbl.find_opt ctx.env.globals n with
| Some (t, _) -> Some t
| None -> None))
| Ast.Call ({ Ast.e = Ast.Var ("string-new" | "bytes->string"); _ }, _) ->
Some string_ty
| Ast.Call ({ Ast.e = Ast.Var "deref"; _ }, [ p ]) ->
(match ty p with Some (Types.Ptr (_, t)) -> Some t | _ -> None)
| Ast.Call ({ Ast.e = Ast.Var f; _ }, _) ->
(match Hashtbl.find_opt ctx.env.fns f with
| Some (_, ret) -> Some ret
| None -> None)
| Ast.Field (t, name) ->
(match ty t with
| Some (Types.Named n | Types.Ptr (_, Types.Named n)) ->
(match fields_named ctx.env n with
| Some st ->
(match Tast.field_index st name with
| Some i -> Some (List.nth st.Tast.fields i).Tast.fty
| None -> None)
| None -> None)
| _ -> None)
| _ -> None
in
match ty a with Some t -> string_or_ptr t | None -> false
(* = and != over a String and a String or a str: the texts' bytes compared,
through the str view each has, which is the comparison str already has.
The orderings are refused as they are on a str, naming is-bytes-less. *)
and string_compare ctx ~want loc name p args =
(match name with
| "=" | "!=" -> ()
| _ ->
fail loc
"%s orders machine numbers and enums, and a String is neither. Text is \
ordered by its bytes with %s"
name
(if fln_source loc then "is-bytes-less(bytes-view(a), bytes-view(b))"
else "(is-bytes-less (bytes-view a) (bytes-view b))"));
let as_str (x : Ast.expr) =
let e =
match x.Ast.e with
| Ast.Str _ -> check ctx ~want:Types.String x
| _ -> check ctx x
in
match e.Tast.ty with
| Types.String -> e
| t when string_or_ptr t ->
mk x.Ast.loc Types.String
(Tast.Prim (Tast.StrOfBytes, [ string_bytes ctx x.Ast.loc e ]))
| other ->
fail x.Ast.loc "%s compares a String with a String or a str, found %s"
name (tyname x.Ast.loc other)
in
let ops = List.map as_str args in
let link u v = mk loc Types.Bool (Tast.Prim (p, [ u; v ])) in
match ops with
| [ a; b ] -> expect ctx loc ~want (link a b)
| _ ->
let pairs = if String.equal name "!=" then all_pairs else adjacent_pairs in
expect ctx loc ~want (cmp_over ctx loc Types.String ~pairs ~link ops)
and string_call ctx ~want loc name args =
let i64 n = mk loc (Types.Int Types.I64) (Tast.Int (n, Types.I64)) in
match name, args with
(* (string-new), (string-new text), (string-new a), (string-new text a):
vec-new's shape, the text copied in — checked at run time when it is a
str, since a str may hold any bytes. *)
| "string-new", ([] | [ _ ] | [ _; _ ]) ->
let text, a =
match args with
| [] -> None, allocator_arg ctx loc []
| [ x ] ->
(match x.Ast.e with
| Ast.Str _ -> Some (string_piece ctx name x), allocator_arg ctx loc []
| _ ->
let e = check ctx x in
if Types.equal e.Tast.ty Types.Alloc then None, use_alloc ctx loc e
else
(* Checked already, so classified from what it turned out to be
and never checked a second time. *)
let piece =
match e.Tast.ty with
| Types.String ->
`Text (mk loc (Types.Slice (Types.Const, u8_ty))
(Tast.Prim (Tast.Bytes, [ e ])), false)
| t when string_or_ptr t -> `Text (string_bytes ctx loc e, true)
| other ->
fail x.Ast.loc
"string-new takes a str or a String to copy, an allocator, \
or both, found %s" (tyname loc other)
in
Some piece, allocator_arg ctx loc [])
| [ x; a ] ->
let piece =
match string_piece ctx name x with
| `Rune _ ->
fail x.Ast.loc "string-new copies a str or a String, not a code point"
| p -> p
in
Some piece, allocator_arg ctx loc [ a ]
| _ -> assert false
in
let sl = fresh_slot ctx string_ty in
let s = mk loc string_ty (Tast.Local sl) in
let v = vec_init ~note:string_ty ctx loc u8_ty a in
let fill =
match text with
| None -> []
| Some p -> [ string_put ctx loc (string_vec loc s) (i64 (-1L)) p ]
in
expect ctx loc ~want
(mk loc string_ty
(Tast.Let ([ (sl, mk loc string_ty (Tast.Make ("String", [ v ]))) ],
fill @ [ s ])))
| "string-new", _ ->
fail loc "string-new is (string-new), (string-new text), (string-new a) \
or (string-new text a)"
(* (bytes->string v) and (bytes->string v a): the bytes of a (Vec u8) copied
into a new String once they are checked, here. A copy, because v — and
any slice taken of it — could otherwise go on writing into the String's
block; v is untouched and still the caller's to free.
The prelude's builders are the one exception: each hands over a Vec
nothing else can reach, so there the Vec becomes the String with no copy,
still checked, and re-noted so a dev build's registry calls the block a
String. *)
| "bytes->string", (x :: rest) when List.length rest <= 1 ->
let v = check ctx ~want:string_vec_ty x in
if String.equal loc.Loc.file Prelude.file && rest = [] then begin
let sl = fresh_slot ctx string_vec_ty in
let vv = mk loc string_vec_ty (Tast.Local sl) in
let view = vec_slice ctx ~want:None loc vv u8_ty [] in
expect ctx loc ~want
(mk loc string_ty
(Tast.Let ([ (sl, v) ],
[ rt loc Types.Unit "flan_utf8_check" [ view; here loc ];
reg_note loc "flan_dev_reg_note_vec" vv
[ size_of loc u8_ty ] string_ty;
mk loc string_ty (Tast.Make ("String", [ vv ])) ])))
end else begin
let a = allocator_arg ctx loc rest in
let src = fresh_slot ctx string_vec_ty in
let view =
vec_slice ctx ~want:None loc (mk loc string_vec_ty (Tast.Local src)) u8_ty []
in
let sl = fresh_slot ctx string_ty in
let s = mk loc string_ty (Tast.Local sl) in
let bytes = { view with Tast.ty = Types.Slice (Types.Const, u8_ty) } in
expect ctx loc ~want
(mk loc string_ty
(Tast.Let
([ (src, v);
(sl, mk loc string_ty
(Tast.Make ("String", [ vec_init ~note:string_ty ctx loc u8_ty a ]))) ],
[ string_put ctx loc (string_vec loc s) (i64 (-1L)) (`Text (bytes, false));
s ])))
end
| "bytes->string", _ ->
fail loc "bytes->string is (bytes->string v) or (bytes->string v a), over a (Vec u8)"
| "append", [ target; x ] ->
let t = check_target ctx target in
(match t.Tast.ty with
(* A run of bytes onto a (Vec u8), or through a pointer to one: the
prelude's builders' tool, and raw bytes, so nothing is checked. The
runtime finds a run that lies inside the Vec's own block before it
grows it, so (append (addr b) (slice b)) reads what it meant to. *)
| Types.Vec (Types.Int Types.U8)
| Types.Ptr (_, Types.Vec (Types.Int Types.U8)) ->
let v =
match t.Tast.ty with
| Types.Ptr (_, ty) -> mk loc ty (Tast.Deref t)
| _ -> t
in
refuse_const_change ctx loc v;
note_grown ctx "append" loc v;
let bty = Types.Slice (Types.Const, u8_ty) in
let b = check ctx ~want:bty x in
let bs = fresh_slot ctx bty in
let attempt =
rt loc (Types.Int Types.I8) "flan_vec_append"
[ v; mk loc bty (Tast.Local bs); size_of loc u8_ty;
align_of loc u8_ty; here loc ]
in
expect ctx loc ~want
(mk loc Types.Unit
(Tast.Let
([ (bs, b) ],
[ region_check ctx.env loc v
(with_note loc (alloc_guard ctx loc attempt)
(reg_note loc "flan_dev_reg_note_vec" v
[ size_of loc u8_ty ] u8_ty)) ])))
| ty when string_or_ptr ty ->
let s = string_target ctx loc name t in
let piece = string_piece ctx name x in
expect ctx loc ~want (string_put ctx loc (string_vec loc s) (i64 (-1L)) piece)
| other ->
fail loc "append takes a String, or a (Vec u8) to add bytes to, found %s"
(tyname loc other))
| "insert", [ target; i; x ] ->
let s = string_target ctx loc name (check_target ctx target) in
let v = string_vec loc s in
let piece = string_piece ctx name x in
let off = string_index ctx loc v i ~past_end:true in
expect ctx loc ~want (string_put ctx loc v off piece)
| "remove", [ target; i ] ->
let s = string_target ctx loc name (check_target ctx target) in
let v = string_vec loc s in
let off = string_index ctx loc v i ~past_end:false in
expect ctx loc ~want
(rt loc (Types.Int Types.I32) "flan_string_remove" [ v; off; here loc ])
| "runes", [ x ] ->
let b = text_bytes ctx name x in
expect ctx loc ~want
(mk loc (Types.Named "Runes") (Tast.Make ("Runes", [ b ])))
| "rune-count", [ x ] ->
let b = text_bytes ctx name x in
expect ctx loc ~want
(mk loc (Types.Int Types.I32) (Tast.Call (prelude_fn ctx "rune-count", [ b ])))
| ("append" | "insert" | "remove" | "runes" | "rune-count"), _ ->
let shape =
match name with
| "append" -> "(append s x)"
| "insert" -> "(insert s i x)"
| "remove" -> "(remove s i)"
| "runes" -> "(runes s)"
| _ -> "(rune-count s)"
in
fail loc "%s is %s" name shape
| _ -> assert false
(* Every arm below is a name an editor can be asked about and no program ever
wrote down, so each one needs a line in [builtins] further down this file.
A new arm without an entry fails the build — test_flan reads both. *)
and named_call ?(qualified = false) ctx ~want loc name args =
let prim p ty args = expect ctx loc ~want (mk loc ty (Tast.Prim (p, args))) in
match name with
(* [builtin/length], before anything else including the shadowing guard below.
The prefix is stripped and the same dispatch runs again with [qualified],
which is the one thing the guard consults: a qualified call has said
which of the two it means, so there is nothing left for shadowing to
decide. Everything after this point sees the bare name, so an arity or a
type refusal on [(builtin/length 1 2)] reads exactly as it does on
[(length 1 2)] — which is the point of the spelling, not a loss of detail.
Recursion rather than a flag threaded through the arms because there is
only one thing to skip. It cannot loop: the stripped name has no second
[builtin/] on it unless somebody wrote [builtin/builtin/length], which is
stripped once and then refused by name. *)
| _ when not qualified && qualified_builtin name <> None ->
let bare = Option.get (qualified_builtin name) in
if not (Hashtbl.mem builtin_set bare) then not_a_builtin loc bare;
named_call ~qualified:true ctx ~want loc bare args
(* The user's own definition, ahead of every builtin arm below — and behind
the qualifier above, which is the one spelling it does not take over.
Clojure's rule: a [(defn get ...)] takes the name over, and a call
written in the program that defines it reaches that definition rather
than the builtin it is named after. The defn site is warned about once
(see [shadowed_builtins]); the call sites say nothing, because at a call
site there is nothing surprising left — the name means what the file
says it means.
What this arm does NOT do is let one file's definition reach into
another's: [shadows_builtin] answers false for a call in the prelude and
for a call in imported package code, which is the same visibility rule a
defn has everywhere else. *)
| _ when (not qualified) && shadows_builtin ctx loc name ->
ordinary_call ctx ~want loc name args
(* .fln's [x ?? d] and [x!]; no .fln name can take them over. *)
| "??" -> check_coalesce ctx ~want loc args
| "?" -> check_present ctx ~want loc args
| "!!" -> check_unwrap ctx ~want loc args
(* ── arithmetic and comparison ─────────────────────────────────── *)
(* (- x) negates, Clojure's rule. A literal operand is the negative literal,
so it takes its type from the site as any literal does. A float is
subtracted from -0.0, which is exact negation — 0.0 - 0.0 would answer
+0.0 — and an integer from 0, which wraps as (- 0 x) does. *)
| "-" when List.length args = 1 ->
let x = List.hd args in
(match x.Ast.e, literal_arith x with
(* Integer arithmetic over literals alone negates to a literal, so
[(- (- 1))] is the literal 1 and fits a u8. *)
| _, Some n when n <> Int64.min_int ->
check ctx ?want { Ast.e = Ast.Int (Int64.neg n); loc }
| Ast.Float v, _ -> check ctx ?want { Ast.e = Ast.Float (-.v); loc }
| _ ->
let v = check ctx ?want:(numeric_want want) x in
if v.Tast.ty = Types.Char then
Loc.failk "check/char-arithmetic" x.Ast.loc
"- does not negate a char. Take its code point with %s, and make a \
char of one with %s"
(if fln_source loc then "i32(c)" else "(i32 c)")
(if fln_source loc then "char(n)" else "(char n)");
if v.Tast.ty = Types.Dyn then
expect ctx loc ~want (rt loc Types.Dyn "flan_dyn_neg" [ v; here loc ])
else begin
unconstrained ctx.env loc name ~needs:"is-numeric" v.Tast.ty;
if not (Types.is_numeric v.Tast.ty || generic_ty v.Tast.ty) then
not_numeric name "numbers" v;
let zero =
match v.Tast.ty with
| Types.Float k -> mk loc v.Tast.ty (Tast.Float (-0.0, k))
| ty -> int_literal loc ~want:(Some ty) ~preds:ctx.env.tvpreds 0L
in
expect ctx loc ~want (mk loc v.Tast.ty (Tast.Prim (Tast.Sub, [ zero; v ])))
end)
| "+" | "-" | "*" | "/" ->
let p = match name with
| "+" -> Tast.Add | "-" -> Tast.Sub | "*" -> Tast.Mul
| _ -> Tast.Div
in
fold_arity loc name args;
fold_left_prim ctx ~want loc name p ~needs:"is-numeric" Types.is_numeric
"numbers" args
(* Remainder stays at two: (% a b c) is (% (% a b) c), which is a thing
nobody writes on purpose. *)
| "%" ->
arity ctx loc name 2 args;
refuse_kept_when ctx name args;
let a, b =
char_operands ctx name args (fun () ->
binary ctx ~dyn_ok:true name loc ~want:(numeric_want want) args)
in
if a.Tast.ty = Types.Dyn || b.Tast.ty = Types.Dyn then
dyn_fold ctx ~want loc name [ a; b ] []
else begin
unconstrained ctx.env loc name ~needs:"is-numeric" a.Tast.ty;
if not (Types.is_numeric a.Tast.ty || generic_ty a.Tast.ty) then
not_numeric name "numbers" a;
prim Tast.Rem a.Tast.ty [ a; b ]
end
| "=" | "!=" | "<" | "<=" | ">" | ">=" ->
let p = match name with
| "=" -> Tast.Eq | "!=" -> Tast.Ne | "<" -> Tast.Lt
| "<=" -> Tast.Le | ">" -> Tast.Gt | _ -> Tast.Ge
in
fold_arity loc name args;
let x, y, rest =
match args with x :: y :: rest -> x, y, rest | _ -> assert false
in
refuse_kept_when ctx name args;
if List.exists (fun a -> peeks_string ctx a) args then
string_compare ctx ~want loc name p args
else
(* The first pair decides the type, and whether this is a dyn comparison
at all, exactly as it does for the folding operators: [binary] joins
the two, and every operand after them is checked against the answer.
Past the first pair nothing widens, which is [fold_left_prim]'s rule
and not a second one. *)
let a, b =
try binary ctx ~dyn_ok:true name loc ~want:None [ x; y ]
with Loc.Error _ as ex ->
(* A char beside an integer: said as the char's rule, not as the
mismatch (decision 131). *)
(* Neither a literal, whose own refusal already says what it is. *)
(match
if is_literal x || is_literal y then []
else List.map (fun a -> trial ctx (fun () -> check ctx a)) [ x; y ]
with
| [ Ok a; Ok b ]
when (a.Tast.ty = Types.Char && Types.is_integer b.Tast.ty)
|| (b.Tast.ty = Types.Char && Types.is_integer a.Tast.ty) ->
let fln = fln_source loc in
Loc.failk "check/char-compare" loc
"%s compares a char only with a char, and this is %s beside it. \
Take its code point with %s, or make a char with %s" name
(tyname loc (if a.Tast.ty = Types.Char then b.Tast.ty else a.Tast.ty))
(if fln then "i32(c)" else "(i32 c)")
(if fln then "char(n)" else "(char n)")
| _ -> raise ex)
in
(* Which pairs this operator asks about. Every one but [!=] chains, and
[!=] asks about all of them — see [all_pairs]. At two operands the two
readings are one pair and the same answer, which is why the two-operand
path below is the same code it always was. *)
let pairs = if String.equal name "!=" then all_pairs else adjacent_pairs in
(* A comparison with a dyn operand answers a *bool*, not a dyn, even though
the runtime's own entry point answers a dyn holding one. The reason is
where the result goes: a comparison is overwhelmingly the test of an
[if] or a [while], and those want an i1. So the need_bool is applied
here, once, and a program that really wants the comparison as a dyn
value boxes it again on the way into wherever it is going — which [box]
does for free at that boundary.
[=] and [!=] are the pair that never traps: the runtime compares
structurally and answers false for values of unrelated types, because
two things being unalike is the answer to "are these equal", not an
error. The orderings do trap, and rightly — there is no true answer to
whether a string is less than a vector. *)
(* [ops] are every operand, not yet boxed. *)
let dyn_chain ops =
let sym =
match name with
| "=" | "!=" -> "flan_dyn_eq"
| "<" -> "flan_dyn_lt" | "<=" -> "flan_dyn_le"
| ">" -> "flan_dyn_gt" | _ -> "flan_dyn_ge"
in
(* [eq] traps only on a view whose storage is gone, and [dyn_eq] gives
it the site for that; the four orderings trap on a mismatch, and get
one, for the reason [dyn_fold] gives. Every pair of a chain gets the
same site — the whole comparison is written at one place, and a trap
from any of its pairs happened there. *)
let site = if String.equal sym "flan_dyn_eq" then [] else [ here loc ] in
(* [!=] has no entry point of its own: there is one structural equality
and the negation is an [i1] flip the backend folds away. *)
let link u v =
let cmp =
if String.equal sym "flan_dyn_eq" then dyn_eq loc u v
else unbox loc Types.Bool (rt loc Types.Dyn sym ([ u; v ] @ site))
in
if String.equal name "!=" then
mk loc Types.Bool (Tast.Prim (Tast.Not, [ cmp ]))
else cmp
in
let ops =
match rest with
| [] -> [ a; b ]
| _ -> ops ()
in
if not (String.equal sym "flan_dyn_eq") then no_bare_nil ops;
let r =
match List.map (box ~ctx loc) ops with
| [ a; b ] -> link a b
| ops -> cmp_over ctx loc Types.Dyn ~pairs ~link ops
in
expect ctx loc ~want r
in
if a.Tast.ty = Types.Dyn || b.Tast.ty = Types.Dyn then
dyn_chain (fun () ->
a :: b :: map_lr (fun e -> check ctx ~want:Types.Dyn e) rest)
else begin
(* [=] and [!=] admit types [<] does not. A handle is one: a pair of
numbers in one word and where being the same entity is the question the
type exists to answer — ordering handles would order a slot index,
which is a free-list artefact and means nothing. A string is the other,
and for the opposite reason: being the same entity is not the question
at all, being the same bytes is, and that has a true answer with no
ordering attached — plan.org, Types calls out an ordering as a
collation the language has not picked. Backend codegen (emit.ml,
x86.ml) has a [Types.String] case in the [Eq]/[Ne] arm and nowhere
else. A bool is the third: two values and no order between them. *)
let ok =
match name with
| "=" | "!=" -> Types.is_equatable a.Tast.ty
| _ -> Types.is_comparable a.Tast.ty
in
unconstrained ctx.env loc name
~needs:(match name with "=" | "!=" -> "is-equal" | _ -> "is-ordered")
a.Tast.ty;
if not (ok || generic_ty a.Tast.ty) then
(match name with
| "=" | "!=" ->
fail loc
"%s compares numbers, chars, enums, strings and bools, and %s \
is none of those" name (tyname loc a.Tast.ty)
| _ ->
fail loc
"%s orders machine numbers, chars and enums, and %s is none of \
those" name
(tyname loc a.Tast.ty));
match rest with
| [] -> prim p Types.Bool [ a; b ]
| _ ->
let ty = a.Tast.ty in
let link u v = mk loc Types.Bool (Tast.Prim (p, [ u; v ])) in
let rest = map_lr (fold_arg ctx ty) rest in
(* A dyn past the first pair makes the whole chain the dyn runtime's,
for [fold_operand]'s reason: a chain is its pairs, and a pair with a
dyn in it is a dyn comparison. *)
if List.exists (function `Dyn _ -> true | `Typed _ -> false) rest then
dyn_chain (fun () ->
a :: b :: List.map (function `Dyn d -> d | `Typed v -> v) rest)
else
let ops =
a :: b :: List.map (function `Typed v -> v | `Dyn d -> d) rest in
expect ctx loc ~want (cmp_over ctx loc ty ~pairs ~link ops)
end
| "not" ->
arity ctx loc name 1 args;
(* Same truthiness as [if]: a dyn argument is negated on nil/false vs.
everything else, not narrowed to a strict bool first. *)
prim Tast.Not Types.Bool [ check_truthy ctx (List.hd args) ]
(* Bitwise operators are integers-only. They take the ordinary join — an
operand that widens into the other does, so (bit-and u8-flags u32-mask) is
a u32 and — and the shifts below do not, which is the one carve-out
widening has (TODO.org, "Implicit numeric widening is legal;
narrowing stays a hard error"). *)
| "bit-and" | "bit-or" | "bit-xor" ->
let p = match name with
| "bit-and" -> Tast.BitAnd | "bit-or" -> Tast.BitOr
| _ -> Tast.BitXor
in
fold_arity loc name args;
bool_operands ctx name args;
fold_left_prim ctx ~want loc name p ~needs:"is-integer" Types.is_integer
"integers" args
(* The .fln operators, which the indented reader already spells as the words
above; a form built some other way may still carry them. [~qualified]
skips the shadowing arm, because a program that means its own [&&] has
been answered by that arm already under this name. *)
| "&&" | "||" | "^^" | "~~" ->
let canon = match name with
| "&&" -> "bit-and" | "||" -> "bit-or" | "^^" -> "bit-xor"
| _ -> "bit-not"
in
named_call ~qualified:true ctx ~want loc canon args
| "bit-not" | "popcount" | "leading-zeros" | "trailing-zeros" ->
arity ctx loc name 1 args;
bool_operands ctx name args;
let v = check ctx ?want:(numeric_want want) (List.hd args) in
if v.Tast.ty = Types.Dyn then
expect ctx loc ~want (rt loc Types.Dyn (dyn_bits_sym name) [ v; here loc ])
else begin
bits_operand ctx loc name v;
let p = match name with
| "bit-not" -> Tast.BitNot | "popcount" -> Tast.Popcount
| "leading-zeros" -> Tast.Clz | _ -> Tast.Ctz
in
prim p v.Tast.ty [ v ]
end
(* The shifts stay at two, and not only because a shift chain reads badly:
each count would be checked against the same width below, so (<< x 30 30)
would pass two legal shifts and still shift the value away entirely.
[~join:false] is the one place widening is deliberately not symmetric.
The count still widens *to* the value's type — (<< i64-x u8-n) is fine —
but the value never widens to the count's, which the general rule would do
for (<< u8-x i32-n). It would be the wrong answer twice over: the result's
type and the width the shift wraps at would be taken from a number that is
only saying how far, and the range check just below, along with [emit]'s
mask, is keyed to the *value's* width. A count wider than the value is
refused and is told to write the cast.
The rotations share the rule and not the range check: a rotation by the
width is the value unchanged, so every count means something and is taken
modulo the width. *)
| "<<" | ">>" | "rotate-left" | "rotate-right" ->
let p = match name with
| "<<" -> Tast.Shl | ">>" -> Tast.Shr | "rotate-left" -> Tast.Rotl
| _ -> Tast.Rotr
in
arity ctx loc name 2 args;
bool_operands ctx name args;
let a, b =
binary ctx ~dyn_ok:true ~join:false name loc ~want:(numeric_want want) args
in
if a.Tast.ty = Types.Dyn || b.Tast.ty = Types.Dyn then begin
(* The typed side of a mixed pair still has to be an integer: the dyn
half is asked at run time, and this half can be asked now. *)
List.iter
(fun (v : Tast.expr) ->
if v.Tast.ty <> Types.Dyn then bits_operand ctx v.Tast.loc name v)
[ a; b ];
no_bare_nil [ a; b ];
expect ctx loc ~want
(rt loc Types.Dyn (dyn_bits_sym name) [ box ~ctx loc a; box ~ctx loc b; here loc ])
end else begin
bits_operand ctx loc name a;
(* A shift by the operand's own width or more is poison in LLVM, which at
-O2 turns the whole function into an undefined value rather than into a
wrong number. A literal count is rejected here — that is the typo — and
[emit] masks a computed one, so no shift can reach the hardware out of
range. *)
(match a.Tast.ty, b.Tast.e with
| Types.Int k, Tast.Int (n, _) when p = Tast.Shl || p = Tast.Shr ->
let w = Int64.of_int (Types.bits k) in
if Int64.unsigned_compare n w >= 0 then
fail loc
"%s by %Ld is out of range for %s, which is %d bits wide" name n
(tyname loc a.Tast.ty) (Types.bits k)
| _ -> ());
prim p a.Tast.ty [ a; b ]
end
(* (min a b) and (max a b) evaluate each operand once — hence the slots —
because a min over two calls must not call either of them twice.
Which is also why this one does not go through [fold_left_prim]: there is
no Prim to fold, and the pair it folds is a whole comparison. Each step
puts *both* of its sides in slots, the accumulated pick included, so the
three-operand form is two nested lets and still exactly one evaluation of
each operand — where reusing the previous [If] as an operand of the next
would have duplicated everything inside it. *)
| "min" | "max" ->
fold_arity loc name args;
let x, y, rest =
match args with x :: y :: rest -> x, y, rest | _ -> assert false
in
let a, b = binary ctx ~dyn_ok:true name loc ~want:(numeric_want want) [ x; y ] in
if a.Tast.ty = Types.Dyn || b.Tast.ty = Types.Dyn then
dyn_fold ctx ~want loc name [ a; b ] rest
else begin
(* [min] and [max] are [<] with a pick, so [is-ordered] is what they want —
not [is-numeric]. A generic that declares [is-ordered] gets both.
They stay builtins now that generics could express them, and the reason
is the two lines above rather than the type system: they are variadic,
and each step puts both of its sides in slots so that every operand is
evaluated exactly once. A prelude [(defn min [a $t b $t] $t ...)] would
be binary and would have to be nested at the call site, which is where
the double evaluation this arm exists to prevent would come back. The
generic half is already theirs — [is-ordered] admits them inside any
body that declares it — so collapsing them would cost the arity and
the evaluation rule and buy nothing. *)
unconstrained ctx.env loc name ~needs:"is-ordered" a.Tast.ty;
(* A char orders, so it has a least and a greatest too. *)
if not (Types.is_numeric a.Tast.ty || a.Tast.ty = Types.Char
|| generic_ty a.Tast.ty) then
not_numeric name "numbers" a;
let ty = a.Tast.ty in
let cmp = if String.equal name "min" then Tast.Lt else Tast.Gt in
let pick a b =
let sa = fresh_slot ctx ty and sb = fresh_slot ctx ty in
let la = mk loc ty (Tast.Local sa) and lb = mk loc ty (Tast.Local sb) in
let test = mk loc Types.Bool (Tast.Prim (cmp, [ la; lb ])) in
mk loc ty (Tast.Let ([ (sa, a); (sb, b) ],
[ mk loc ty (Tast.If (test, la, lb)) ]))
in
let rec steps acc = function
| [] -> expect ctx loc ~want acc
| arg :: tl ->
match fold_arg ctx ty arg with
| `Typed v -> steps (pick acc v) tl
| `Dyn d -> dyn_fold ctx ~want loc name [ acc; d ] tl
in
steps (pick a b) rest
end
(* A type handed to the prelude's slice reductions: the reach for the
type-limit constants under the name of the reduction beside them. *)
| ("max-of" | "min-of")
when (not (shadows_builtin ctx loc name))
&& (match args with [ a ] -> type_arg ctx a | _ -> false) ->
let which = if String.equal name "max-of" then "max-value" else "min-value" in
fail loc
"%s reduces a slice to its %s element, and this is a type — the %s value \
of a type is (%s %s)"
name (if which = "max-value" then "largest" else "least")
(if which = "max-value" then "largest" else "least") which
(spell_arg "i32" (List.hd args))
(* (max-value T) and (min-value T): the type-limit constants, by type, so a
generic body can name its own type's. Odin's max(T) and min(T), and the
same answer for a float: the largest finite value and its negation, not
the smallest positive one. *)
| "max-value" | "min-value" ->
arity ctx loc name 1 args;
if not (type_arg ctx (List.hd args)) then
fail (List.hd args).Ast.loc "%s takes a type, as in (%s i32)" name name;
let a = List.hd args in
let ty =
match type_of_expr ~generic:(Hashtbl.mem ctx.env.gstructs) a, a.Ast.e with
| Some t, _ -> resolve ctx.env t
| _, Ast.Var n -> resolve_name ctx.env ~seen:[] a.Ast.loc n
| _ -> fail a.Ast.loc "internal: %s's type argument is not a type" name
in
let max = String.equal name "max-value" in
let v =
match ty with
| Types.Int k ->
let b = Types.bits k in
let n =
if Types.signed k then
let top = Int64.shift_left 1L (b - 1) in
if max then Int64.sub top 1L else Int64.neg top
else if not max then 0L
else if b = 64 then -1L
else Int64.sub (Int64.shift_left 1L b) 1L
in
mk loc ty (Tast.Int (n, k))
| Types.Float k ->
let m =
match k with
| Types.F32 -> Int32.float_of_bits 0x7f7fffffl
| Types.F64 -> Float.max_float
in
mk loc ty (Tast.Float ((if max then m else -.m), k))
| Types.Var v ->
if not (declares ctx.env.tvpreds v "is-numeric") then
Loc.failk "check/unconstrained-type-variable" a.Ast.loc
"%s is a limit of a numeric type, and nothing declares $%s \
numeric — write {:where (is-numeric $%s)} at the head of the body"
name v v;
int_literal loc ~want:(Some ty) ~preds:ctx.env.tvpreds 0L
| _ ->
fail a.Ast.loc
"%s takes a numeric type (is-numeric), and %s is not one — as in (%s i32)" name
(tyname loc ty) name
in
expect ctx loc ~want v
(* (zeroed) is the all-bytes-zero value of whatever it is being stored into,
so it only means anything where a type is expected of it. *)
| "zeroed" ->
arity ctx loc name 0 args;
(match want with
| Some ty when ty <> Types.Never ->
no_zeroed_fn loc "this" ty;
mk loc ty (Tast.Zero ty)
| _ ->
fail loc
"zeroed needs to know the type it is zeroing — use it where one is \
expected, or name it, as in (the [4 i32] (zeroed))")
(* [zeroed]'s two siblings, and the same shape exactly: a value of whatever
type is expected of it, so [(set grid (filled 0xFF))] is how a place is
filled and there is no second spelling to learn. What they add over
[zeroed] is the bytes — [(filled b)] repeats one the program picks, and
[(dead-beef)] repeats four — and with them the question [zeroed] never
has to ask: zero is a value every type can have, and 0xDE is not.
[unfillable] above is the whole of the answer.
[dead-beef] takes the pattern or leaves it out, and leaving it out is
defined as writing the default: the [None] arm below builds the same
literal the source would have, so [(dead-beef)] and
[(dead-beef 0xDEADBEEF)] are the same node by construction and no
backend has a second path for the bare form.
The pattern is an ordinary u32 expression, which is the byte arm's rule
at four times the width — a literal out of range meets [in_range]'s
located "does not fit in u32", and anything computed is guaranteed by
its type instead. Both builtins take a value rather than only a literal
for the same reason: refusing one would be a restriction with no
mechanism behind it, since neither backend needs the number early. *)
| "filled" | "dead-beef" ->
let is_byte = String.equal name "filled" in
if is_byte then arity ctx loc name 1 args
else if List.length args > 1 then
fail loc "%s takes the pattern or nothing at all, given %d arguments"
name (List.length args);
(match want with
| Some ty when ty <> Types.Never ->
(match unfillable ctx.env [] ty with
| Some bad ->
Loc.failk "check/fill-not-plain-data" loc
"%s writes raw bytes over %s, and %s is not plain data — %s. \
Fill only numbers and pointers, and structs, unions and fixed \
arrays built out of them"
name (tyname loc ty)
(if Types.equal bad ty then "it" else tyname loc bad)
(match bad with
| Types.Dyn ->
"a dyn is one word the collector walks by descriptor, and a \
filled one is a root pointing at nothing"
| Types.Vec _ | Types.Map _ | Types.Alloc ->
"it owns its storage through a pointer and an allocator, and \
a filled header frees a wild address"
| Types.String | Types.Slice _ ->
"it is a pointer and a length every bounds check believes"
| Types.Bool ->
"a bool is an i1 to LLVM and a whole byte to the x86 backend, \
so a filled one would not even agree with itself across the \
two"
| Types.Option _ ->
"it carries a tag saying whether the value is there, and a \
filled one says yes over a payload nobody wrote"
| Types.Fn _ | Types.CFn _ ->
"it is a code address, and a call through a filled one jumps \
into whatever 0xDE bytes happen to address"
| Types.Named n when Hashtbl.mem ctx.env.datas n ->
"it carries a tag that names a case, and no byte pattern \
names a real one"
| Types.Enum _ ->
"an enum's values are the members it declared, and no byte \
pattern is one of them"
| _ ->
"it is not one of the types this rule admits")
| None -> ());
if is_byte then
let b = check ctx ~want:(Types.Int Types.U8) (List.hd args) in
mk loc ty (Tast.Fill (ty, b))
else
let pat =
match args with
| [ a ] -> check ctx ~want:(Types.Int Types.U32) a
(* The bare form, written out. Not a default a backend applies:
the node that leaves here is the one the spelled-out call would
have left, which is what makes the equivalence a fact about the
IR rather than a promise two emitters keep separately.
Masked to 32 bits, and that is the whole of why this is not
[Int64.of_int32] on its own: [dead_beef_default] is an [int32]
whose top bit is set, so widening it signed would put
-559038737 on a node tagged [u32] — where the same pattern
*written out* arrives as 3735928559, because [in_range] admits
it as the unsigned value it is. Two spellings of one builtin
would then carry two different payloads, and "the same node by
construction" would be false for anything that reads one. *)
| _ ->
mk loc (Types.Int Types.U32)
(Tast.Int
(Int64.logand
(Int64.of_int32 Tast.dead_beef_default) 0xFFFFFFFFL,
Types.U32))
in
mk loc ty (Tast.DeadBeef (ty, pat))
| _ ->
fail loc
"%s needs to know the type it is filling — use it where one is \
expected, or name it, as in (the [4 u32] (%s))"
name (if is_byte then "filled 0xFF" else name))
(* The one half of a destructuring [let] that [Parse] cannot do on its own.
Everything else about a pattern is bindings and field accesses it already
wrote; the arity is a *type* question — how many elements the value has —
and there are no types in the parser. So the pattern's shape travels here
as arguments: which element this binding wants, how many names the pattern
binds, and whether that count is exact or a minimum (it is a minimum when
the pattern ends in [& rest]).
No source symbol can contain a [~] — the reader makes it a delimiter — so
this name is unspellable and nothing but [Parse] can reach it. *)
| "destructure~nth" ->
(match args with
| [ target;
{ Ast.e = Ast.Int i; _ }; { Ast.e = Ast.Int n; _ };
{ Ast.e = Ast.Int exact; _ } ] ->
let plural k = if Int64.equal k 1L then "" else "s" in
lit_typed_use ctx target;
let target = check ctx target in
(match target.Tast.ty with
| Types.Array (m, elem) ->
if Int64.equal exact 1L && not (Int64.equal m n) then
fail loc
"this pattern binds %Ld name%s, but %s has %Ld element%s — a \
pattern over a fixed array names every element, or ends in \
[& rest]"
n (plural n) (tyname loc target.Tast.ty) m (plural m);
if Int64.equal exact 0L && Int64.compare m n < 0 then
fail loc
"this pattern binds %Ld name%s before the &, but %s has only %Ld \
element%s" n (plural n) (tyname loc target.Tast.ty) m
(plural m);
prim Tast.At elem
[ target; mk loc index_ty (Tast.Int (i, Types.I32)) ]
(* The asymmetry is real and is the reason this is refused rather than
lowered to a bounds-checked [at]: a fixed array's length is in its
type, so [[a b]] over a [[2 f32]] is a claim the checker can settle,
and over a [[T]] it is a claim about a number that does not exist
until the program runs. Turning it into a runtime trap would be a
pattern that type checks and then kills the program, which is the
trade this language does not make. *)
| Types.Slice _ ->
fail loc
"a pattern cannot destructure %s — a slice's length is not known \
until the program runs, so nothing here can check it has %Ld \
element%s. Use %s and test %s yourself"
(tyname loc target.Tast.ty) n (plural n)
(if fln_source loc then "s[i]" else "(at s i)")
(if fln_source loc then "length(s)" else "(length s)")
| other ->
fail loc
"%s is not a fixed array, so [a b ...] cannot destructure it"
(tyname loc other))
| _ ->
fail loc
"destructure~nth is written by the compiler and cannot be called")
(* ── allocators, spec-memory.md ────────────────────────────────── *)
(* Every one of these is an ordinary named call, which is the whole of the
escape TODO.org's "The escape was real: a value the compiler builds trips
no function-value refusal" describes: [check_call] already routes a named call through
here, so none of the four function-value refusals is anywhere near it. *)
(* A *user-written* allocator, and the reason it is still refused now that
function values exist. It was diagnosed as needing "a defn's name in value
position"; it has that, and it is still two things short, both of them
nameable and neither of them a function-value question any more.
The built-in set needs none of it: heap-allocator and arena-new are C
symbols the emitter names, and no Flan type mentions them. *)
| "make-allocator" | "allocator-from" | "allocator" ->
fail loc
"a user-written allocator is not implemented yet — use (arena-new ...) \
with a backing buffer"
| "heap-allocator" ->
arity ctx loc name 0 args;
expect ctx loc ~want
(seal_alloc ctx loc (rt loc raw_alloc "flan_heap_allocator" []))
(* The capacity is explicit and there is no growing backing store: an arena
whose size is decided by the program is one a program can reason about,
and it is the only shape under which "exhausted" is a state a test can
reach on purpose. *)
| "arena-new" ->
arity ctx loc name 1 args;
let cap = check ctx ~want:(Types.Int Types.I64) (List.hd args) in
expect ctx loc ~want
(seal_alloc ctx loc (rt loc raw_alloc "flan_arena_new" [ cap ]))
(* Hands the pages back, which [free-all] deliberately does not — see
TODO.org, "Allocators, (Vec T) and StorageExhausted". *)
| "arena-destroy" ->
arity ctx loc name 1 args;
let a = alloc_value ctx loc (List.hd args) in
expect ctx loc ~want
(mk loc Types.Unit (Tast.Prim (Tast.Rt "flan_arena_destroy", [ a ])))
(* One of spec-memory.md's two release points. It takes the source location
as a string so that an allocator with no region to release names the site
rather than the runtime. *)
| "free-temp" ->
arity ctx loc name 0 args;
expect ctx loc ~want (rt loc Types.Unit "flan_free_temp" [])
| "free-all" ->
arity ctx loc name 1 args;
let a = alloc_value ctx loc (List.hd args) in
expect ctx loc ~want
(mk loc Types.Unit
(Tast.Prim (Tast.Rt "flan_alloc_free_all", [ a; here loc ])))
(* The capability set, read off the allocator value. Odin asks its procedure
(Query_Features returning an Allocator_Mode_Set); a field is the same
answer without the round trip, which is the call this made. *)
| "can-free" ->
arity ctx loc name 1 args;
let a = alloc_value ctx loc (List.hd args) in
expect ctx loc ~want
(mk loc Types.Bool
(Tast.Prim (Tast.Ne,
[ mk loc (Types.Int Types.I8)
(Tast.Prim (Tast.Rt "flan_alloc_can_free", [ a ]));
mk loc (Types.Int Types.I8) (Tast.Int (0L, Types.I8)) ])))
| "can-free-all" ->
arity ctx loc name 1 args;
let a = alloc_value ctx loc (List.hd args) in
expect ctx loc ~want
(mk loc Types.Bool
(Tast.Prim (Tast.Ne,
[ mk loc (Types.Int Types.I8)
(Tast.Prim (Tast.Rt "flan_alloc_can_free_all", [ a ]));
mk loc (Types.Int Types.I8) (Tast.Int (0L, Types.I8)) ])))
(* The counter [free-all] bumps. A container records it and traps if it
moved; this is the same number, readable, so a program can say what it
saw. *)
| "alloc-epoch" ->
arity ctx loc name 1 args;
let a = alloc_value ctx loc (List.hd args) in
expect ctx loc ~want
(mk loc (Types.Int Types.I64)
(Tast.Prim (Tast.Rt "flan_alloc_epoch", [ a ])))
(* The allocator's identity — its address — which is what the condition's
:allocator field carries, so a handler holding several regions can tell
which one ran out. *)
| "alloc-id" ->
arity ctx loc name 1 args;
let a = alloc_value ctx loc (List.hd args) in
expect ctx loc ~want
(mk loc (Types.Int Types.I64) (Tast.Prim (Tast.Rt "flan_alloc_id", [ a ])))
(* A ceiling on live bytes, 0 for none. spec-memory.md's retry restart is
answerable only by a handler that can make the *same* request succeed, and
for a fixed backing store the handler that works is the one that grows it:
releasing the region a container lives in invalidates the container, which
is what the epoch check catches. So the spec's "grows the arena and then
invokes retry" needs a ceiling to raise, and this is it. It is also how a
program exhausts an allocator on purpose. *)
| "alloc-budget" ->
arity ctx loc name 1 args;
let a = alloc_value ctx loc (List.hd args) in
expect ctx loc ~want
(mk loc (Types.Int Types.I64)
(Tast.Prim (Tast.Rt "flan_alloc_budget", [ a ])))
| "set-alloc-budget" ->
arity ctx loc name 2 args;
(match args with
| [ a; n ] ->
let a = alloc_value ctx loc a in
let n = check ctx ~want:(Types.Int Types.I64) n in
expect ctx loc ~want
(mk loc Types.Unit (Tast.Prim (Tast.Rt "flan_alloc_set_budget", [ a; n ])))
| _ -> assert false)
(* "Did you forget to free" is an allocator-tier question and this is the
tier answering it — spec-memory.md, "Leaking is defined behaviour". *)
| "alloc-live-blocks" ->
arity ctx loc name 1 args;
let a = alloc_value ctx loc (List.hd args) in
expect ctx loc ~want
(mk loc (Types.Int Types.I64)
(Tast.Prim (Tast.Rt "flan_alloc_live_blocks", [ a ])))
(* (with-allocator A BODY...). It rebinds and releases nothing: not at the
end of the body, not anywhere. spec-memory.md is explicit that this is not
a scope-end release point and that it is the point on which Odin's
[defer delete] and Carp's scope-end frees were both rejected. *)
| "with-allocator" ->
(match args with
| [] -> fail loc "with-allocator is (with-allocator allocator body ...)"
| a :: body ->
(* Two values in this frame, handed to the runtime by address: the one
to install, checked here so a stale one traps at this site, and the
room for the one it displaces, so the restore puts that back with
its incarnation (flan_rt.c, flan_context_set). *)
let a =
let pair = Types.Array (2L, Types.Alloc) in
let s = fresh_slot ctx pair in
let first = addr_of loc (mk loc pair (Tast.Local s)) in
mk loc raw_alloc
(Tast.Let
([ (s, mk loc pair
(Tast.Arr [ check ctx ~want:Types.Alloc a;
mk loc Types.Alloc (Tast.Zero Types.Alloc) ])) ],
[ rt loc raw_alloc "flan_alloc_use" [ first; here loc ];
addr_of loc (mk loc pair (Tast.Local s)) ]))
in
let body, ty =
scoped ctx (fun () ->
match body with
| [] -> [ unit_at loc ], Types.Unit
| _ ->
let rec go = function
| [ last ] -> let l = check ctx ?want last in [ l ], l.Tast.ty
| e :: rest ->
let e = check ctx e in
let rest, ty = go rest in
e :: rest, ty
| [] -> assert false
in
go body)
in
expect ctx loc ~want (mk loc ty (Tast.WithAlloc (a, body))))
(* ── (Vec T), spec-memory.md ───────────────────────────────────── *)
(* Every one of these is a named call over a type-erased runtime, with
size_of and align_of produced here because here is where the concrete
element type is known. No generics are involved and none are needed. *)
(* (vec-new), (vec-new T), (vec-new a), (vec-new T a).
[let] has no type annotation — parse.ml settles that a triple binding is
ambiguous and types are inferred — so a local Vec has nowhere to say what
it holds, and the element type is written at the call instead. This is not
the explicit instantiation syntax the generics section rules out: nothing
here is generic, and the name is resolved as an ordinary type, not bound
to a type variable. Where the context does say — a defonce's type, a
function's return type, an argument — it is not needed and may be left
out. *)
| "vec-new" ->
let elem, args = vec_new_elem ctx ~want loc args in
(* [(vec-new dyn)] is not a [(Vec dyn)]. At milestone 1 the heterogeneous
container is the dyn runtime's own object, and its type is [dyn] like
everything else the runtime hands back — which is what lets [push], [at]
and [length] on it go through the dyn operations rather than through a
type-erased Vec over eight-byte elements.
The two could be made to coincide later, and the reason not to now is
the collector: a Flan Vec's storage comes from an allocator the program
named, and the words in it would be roots the collector has to find
inside a block it does not own. The runtime's own vector is storage the
collector already knows about. *)
if elem = Types.Dyn then begin
if args <> [] then
fail loc
"(vec-new dyn) takes no allocator — its storage is the dyn \
runtime's";
expect ctx loc ~want (rt loc Types.Dyn "flan_dyn_vec_new" [])
end else
expect ctx loc ~want (vec_init ctx loc elem (allocator_arg ctx loc args))
(* Unit, not a Result and not an ignorable error code: see [alloc_guard]. *)
| "push" ->
arity ctx loc name 2 args;
(match args with
| [ target; x ] ->
let target = check_target ctx target in
refuse_const_change ctx loc target;
(* A push into a dyn container is a call and nothing else: no allocation
guard, no restart, no region check. The dyn runtime owns the storage
and answers a failure to grow it on its own terms — the guard and the
retry restart exist for an allocator the *program* named, and here
there is none to name. *)
if target.Tast.ty = Types.Dyn then
expect ctx loc ~want
(rt loc Types.Unit "flan_dyn_push"
[ target; check ctx ~want:Types.Dyn x; here loc ])
else begin
let elem = vec_elem loc "push" target.Tast.ty in
note_grown ctx "push" loc target;
let x = check ctx ~want:elem x in
(* The element is bound before the loop so that a [retry] re-attempts
the allocation and not the expression that produced the value. *)
let e = fresh_slot ctx elem in
let attempt =
rt loc (Types.Int Types.I8) "flan_vec_push"
[ target; addr_of loc (mk loc elem (Tast.Local e));
size_of loc elem; align_of loc elem; here loc ]
in
(* Re-noted after every push, not only the first: a push that grows the
Vec moves the storage, and the note is keyed on the base address, so
an unmoved block costs a probe and an overwrite with the same
numbers. This is the insert per allocation TODO.org's "The
allocation registry" settles on, and
the settled answer to what it costs is "measure a real program". *)
expect ctx loc ~want
(mk loc Types.Unit
(Tast.Let ([ (e, x) ],
[ region_check ctx.env loc target
(with_note loc (alloc_guard ctx loc attempt)
(reg_note loc "flan_dev_reg_note_vec" target
[ size_of loc elem ] elem)) ])))
end
| _ -> assert false)
| "reserve" ->
arity ctx loc name 2 args;
(match args with
| [ target; n ] ->
let target = check_target ctx target in
refuse_const_change ctx loc target;
let n = check ctx ~want:index_ty n in
note_grown ctx "reserve" loc target;
let n64 =
mk loc (Types.Int Types.I64) (Tast.Prim (Tast.Cast (Types.Int Types.I64), [ n ]))
in
(* Deferred: the sizes are known abstractly but the hash is not, so
the node is a unit no-op and the copy builds the real one. *)
if (match target.Tast.ty with
| Types.Map (k, _) -> deferred_key ctx.env loc "reserve" k
| _ -> false) then
expect ctx loc ~want (mk loc Types.Unit Tast.Unit)
else
let attempt, note =
match target.Tast.ty with
(* For a map the number is entries, not slots: the runtime sizes the
block so that [n] still sits under the load factor, which is
the only reading of "room for n" that does not reallocate on the
nth put. *)
| Types.Map (k, v) ->
let hash, _ = key_fns ctx.env loc k in
rt loc (Types.Int Types.I8) "flan_map_reserve"
[ target; n64; size_of loc k; size_of loc v; hash; here loc ],
reg_note loc "flan_dev_reg_note_map" target
[ size_of loc k; size_of loc v ] target.Tast.ty
| _ ->
let elem = vec_elem loc "reserve" target.Tast.ty in
rt loc (Types.Int Types.I8) "flan_vec_reserve"
[ target; n64; size_of loc elem; align_of loc elem; here loc ],
reg_note loc "flan_dev_reg_note_vec" target
[ size_of loc elem ] elem
in
expect ctx loc ~want
(region_check ctx.env loc target
(with_note loc (alloc_guard ctx loc attempt) note))
| _ -> assert false)
(* spec-memory.md's first release point. Since the repeal, what it consumes
it consumes at run time only: nothing marks the binding dead, so a second
[free] or a read after this one type-checks and misbehaves at run time —
the allocator aborts on a double free it can see, and the epoch word
traps a read through a released region. That is the Odin contract: free
is a thing you write, and writing it twice is yours to not do. *)
| "free" ->
(match args with
| [ _ ] | [ _; _ ] -> ()
| _ -> fail loc "free is (free v), or (free s allocator) for a slice");
let target = check_target ctx (List.hd args) in
refuse_const_change ctx loc target;
(match target.Tast.ty, args with
| (Types.Vec _ | Types.Map _), [ _; _ ] ->
fail loc
"a %s knows the allocator it came from, so free takes only the \
container. Write (free %s)"
(tyname loc target.Tast.ty) (spell_arg "v" (List.hd args))
| _ -> ());
(* A container of owning elements is refused here, and a reader will
assume the opposite — that [free] recurses — so this says why it does
not and what does.
The bytes this container holds are element *headers*, and releasing the
block those headers sit in says nothing about the blocks they point at.
Nothing type-erased can walk them: the runtime sees a size and an
alignment and has never heard of the element type. That is the same
fact the type-level refusals used to state, and the arena did not change
it — what the arena changed is that it no longer matters there, because
the inner blocks came out of the same region and [free-all] takes them
with everything else.
So the honest answer is not to recurse, and it is not to release the
backing store quietly either. Releasing the outer block alone would be
"I freed it" spelt over a program that leaked everything inside, and
this file refuses that collapse everywhere else — [flan_alloc_free_all]
traps rather than no-op for the same reason. It is refused instead, at
the one place a reader is looking when they want to know.
The guard at construction is what makes the advice reachable: such a
container is region-allocated or it does not exist, so there is always a
[free-all] to point at. *)
(match target.Tast.ty with
| t when is_string_ty t ->
(match args with
| [ _; _ ] ->
fail loc
"a String knows the allocator it came from, so free takes only \
the String. Write %s"
(if fln_source loc then "free(" ^ spell_arg "s" (List.hd args) ^ ")"
else "(free " ^ spell_arg "s" (List.hd args) ^ ")")
| _ -> ());
expect ctx loc ~want
(rt loc Types.Unit "flan_vec_free"
[ string_vec loc target; size_of loc u8_ty; align_of loc u8_ty;
here loc ])
| (Types.Vec _ | Types.Map _)
when region_only ctx.env target.Tast.ty ->
fail loc
"%s holds elements that own storage, and free releases only the \
block those elements sit in. Write (free-all a) on the region it \
was built against"
(tyname loc target.Tast.ty)
| Types.Vec elem ->
expect ctx loc ~want
(rt loc Types.Unit "flan_vec_free"
[ target; size_of loc elem; align_of loc elem; here loc ])
| Types.Map (k, v) ->
expect ctx loc ~want
(rt loc Types.Unit "flan_map_free"
[ target; size_of loc k; size_of loc v; here loc ])
(* A slice (bytes s) or (clone xs) answered: its block goes back to the
allocator it came from, which a slice does not carry — so it is the
context allocator, as Odin's delete defaults to, or the one named. A
dev build checks the block against the allocation registry and traps
on a slice that is not the start of a block, or on the wrong
allocator, instead of handing one allocator another's block. *)
| Types.Slice (Types.Const, _) ->
fail loc
"%s can only be read, so it cannot be freed. Free the [%s] it was \
copied into"
(tyname loc target.Tast.ty)
(match target.Tast.ty with
| Types.Slice (_, e) -> tyname loc e
| t -> tyname loc t)
(* A view written right here — (slice ...) or (slice-from ...) —
is storage something else owns, known without running anything. *)
| Types.Slice (Types.Mut, _)
when (match (List.hd args).Ast.e with
| Ast.Call ({ Ast.e = Ast.Var ("slice" | "slice-from"); _ }, _) ->
true
| _ -> false) ->
fail loc
"this is a view of storage something else owns, so it cannot be \
freed. Only a slice (bytes s) or (clone xs) made can be"
| Types.Slice (Types.Mut, elem) ->
let a = allocator_arg ctx loc (List.tl args) in
expect ctx loc ~want
(rt loc Types.Unit "flan_slice_free"
[ target; size_of loc elem; align_of loc elem; a; here loc ])
| other ->
(* A field is never freed on its own: it would leave its owner partly
dead with no way to say so. *)
fail loc
"free takes a Vec, a Map, or a slice (bytes s) or (clone xs) made — \
found %s"
(tyname loc other))
(* Emitted by the prelude's [into] when no (map f) is in the chain, so that
every element pushed is a source element as it stands. A push copies an
element's header, and for an element that owns storage the copy and the
source then share one block: growing an element through either side
reallocates it and frees the block the other still points at. That is a
use after free the program never wrote, under a name that promised a
copy, so it is refused here. A bare (push w (at v 0)) is not: it copies a
header in plain sight, the Odin contract every container follows.
Arguments are the source, then the destination and the transforms as
written — those two only to be spelled back in the fix, never checked. *)
| "into-copies-elements" ->
(match args with
| src :: dst :: transforms ->
let s = check ctx src in
let elem =
match s.Tast.ty with
| Types.Vec e | Types.Slice (_, e) | Types.Array (_, e) -> Some e
| _ -> None
in
(match elem with
| Some e when owning ctx.env e ->
let v = spell_arg "v" src in
let et = tyname loc e in
let fix =
if clone_accepts ctx.env e then
match spell_form dst, List.map spell_form transforms with
| Some d, ts when not (List.mem None ts) ->
Printf.sprintf
"Add (map clone) to the chain, which copies what each \
element owns: (into %s)"
(String.concat " "
((v :: d :: List.filter_map Fun.id ts) @ [ "(map clone)" ]))
| _ ->
"Add (map clone) to the chain, which copies what each element \
owns"
else
Printf.sprintf
"Nothing copies what a %s owns, so no copy of %s can stand on \
its own: read the elements where they are, or build each new \
element and push that"
et v
in
Loc.failk "check/into-shares-elements" src.Ast.loc
"into copies each element of %s as it stands, and an element of \
%s is a %s, which owns storage — the copy would share each \
element's block with %s, and growing either one frees the block \
the other points at. %s"
v v et v fix
| _ -> ());
expect ctx loc ~want (mk loc Types.Unit Tast.Unit)
| _ -> expect ctx loc ~want (mk loc Types.Unit Tast.Unit))
(* (clone v) uses the current allocator, (clone v a) names one. A deep,
independent copy: spec-memory.md's "copying is always explicit". *)
| "clone" ->
(match args with
| target :: rest when List.length rest <= 1 ->
lit_typed_use ctx target;
(* Checked once, then dispatched on what it turned out to be: checking
it inside a guard as well would allocate the target's slots twice and
evaluate whatever it was written as twice. *)
let target = check_target ctx target in
let a = allocator_arg ctx loc rest in
(match target.Tast.ty with
(* A String's copy is a copy of its bytes, which are valid already. *)
| t when is_string_ty t ->
let v = string_vec loc target in
let d = fresh_slot ctx string_vec_ty in
let attempt =
rt loc (Types.Int Types.I8) "flan_vec_clone"
[ mk loc string_vec_ty (Tast.Local d); v; a;
size_of loc u8_ty; align_of loc u8_ty; here loc ]
in
expect ctx loc ~want
(mk loc string_ty
(Tast.Make
("String",
[ mk loc string_vec_ty
(Tast.Let ([ (d, mk loc string_vec_ty (Tast.Zero string_vec_ty)) ],
[ with_note loc (alloc_guard ctx loc attempt)
(reg_note loc "flan_dev_reg_note_vec"
(mk loc string_vec_ty (Tast.Local d))
[ size_of loc u8_ty ] string_ty);
mk loc string_vec_ty (Tast.Local d) ])) ])))
(* The refusal that did *not* come down with the type-level ones, and
the distinction is worth being exact about, because the sentence
they all used to share bundled two different failures: that a clone
would duplicate inner headers instead of copying, and that a free
would leak what those headers own. Only the second was about
teardown, and only the second is answered by a region.
What disqualifies [clone] is not that it copies a header — so do
[at] and [get], and they are fine, because they promise nothing and
hand back an alias into a region nobody individually frees. It is
that [clone] *allocates a new block and promises independence*.
spec-memory.md calls it a deep copy; what a memcpy of the slots
delivers is a second container whose elements still point into the
first one's blocks. Two containers, one set of inner buffers, under
a name that says otherwise — and putting the copy in a second arena
makes it worse, not better, because tipping that arena leaves the
copy's elements pointing into an arena that is still live while its
own storage is gone.
Refused at the operation rather than at the type, because that is
where the promise is made. *)
| (Types.Vec _ | Types.Map _) when region_only ctx.env target.Tast.ty ->
fail loc
"%s cannot be cloned — its elements own storage, and nothing here \
can walk one to copy what it owns. %s"
(tyname loc target.Tast.ty)
(insert_copies ~loc:target.Tast.loc ctx.env target.Tast.ty)
(* A slice's elements, copied into a block from the allocator and
answered as a slice over it — what (bytes s) does for a string's
bytes, and the same lowering. The same refusal as a Vec's, for the
same reason: a copy of owning elements is a copy of their headers. *)
| Types.Slice (_, elem) when owning ctx.env elem ->
fail loc
"%s cannot be cloned — its elements own storage, and nothing here \
can walk one to copy what it owns. %s"
(tyname loc target.Tast.ty)
(insert_copies ~loc:target.Tast.loc ctx.env target.Tast.ty)
(* The copy is a block from an allocator, which the collector does not
walk, so a dyn in it would be a root nothing marks. *)
| Types.Slice (_, elem) when holds_dyn ctx.env elem ->
fail loc
"%s cannot be cloned — its elements hold a dyn, and the copy would \
live in allocator storage the collector does not look in. Build \
a dyn vector from the elements instead"
(tyname loc target.Tast.ty)
| Types.Slice (_, elem) ->
expect ctx loc ~want (dup_elems ctx loc elem target a)
(* A map's clone reinserts rather than copying the block, because the
seed is derived from the block's address — see flan_rt.c. That is
the runtime's business; from here it is one more allocating call
under the same guard. *)
| Types.Map (k, v) when deferred_key ctx.env loc "clone" k ->
(* Deferred, and the placeholder is a zeroed map of the same type —
the value a (map-new) starts from, so everything written around
the clone still checks against the type it will have. *)
let mty = Types.Map (k, v) in
expect ctx loc ~want (mk loc mty (Tast.Zero mty))
| Types.Map (k, v) ->
let mty = Types.Map (k, v) in
let hash, _ = key_fns ctx.env loc k in
let d = fresh_slot ctx mty in
let attempt =
rt loc (Types.Int Types.I8) "flan_map_clone"
[ mk loc mty (Tast.Local d); target; a;
size_of loc k; size_of loc v; hash; here loc ]
in
expect ctx loc ~want
(mk loc mty
(Tast.Let ([ (d, mk loc mty (Tast.Zero mty)) ],
[ with_note loc (alloc_guard ctx loc attempt)
(reg_note loc "flan_dev_reg_note_map"
(mk loc mty (Tast.Local d))
[ size_of loc k; size_of loc v ] mty);
mk loc mty (Tast.Local d) ])))
| _ ->
let elem = vec_elem loc "clone" target.Tast.ty in
let d = fresh_slot ctx (Types.Vec elem) in
let attempt =
rt loc (Types.Int Types.I8) "flan_vec_clone"
[ mk loc (Types.Vec elem) (Tast.Local d); target; a;
size_of loc elem; align_of loc elem; here loc ]
in
expect ctx loc ~want
(mk loc (Types.Vec elem)
(Tast.Let ([ (d, mk loc (Types.Vec elem)
(Tast.Zero (Types.Vec elem))) ],
[ with_note loc (alloc_guard ctx loc attempt)
(reg_note loc "flan_dev_reg_note_vec"
(mk loc (Types.Vec elem) (Tast.Local d))
[ size_of loc elem ] elem);
mk loc (Types.Vec elem) (Tast.Local d) ]))))
| _ -> fail loc "clone is (clone v) or (clone v allocator)")
(* ── String, the prelude's owned text ─────────────────────────────
Each is [string_call]'s, which says what each one checks. *)
| "string-new" | "bytes->string" | "append" | "insert" | "remove" ->
string_call ctx ~want loc name args
| "runes" | "rune-count" ->
string_call ctx ~want loc name args
(* ── (Map K V), spec-memory.md ─────────────────────────────────── *)
(* Every one of these is a named call over the same type-erased runtime the
Vec uses, with the two sizes and the key's hash and equality pair produced
here because here is where the concrete types are known. No generics are
involved and none are needed — which is exactly what Odin's Map_Info says
too, being two sizes and two contextless procs. *)
(* (map-new), (map-new K V), (map-new a), (map-new K V a). The same shape
[vec-new] has and for the same reason: a [let] has no type annotation, so
a local map has nowhere else to say what it holds. Where the context does
say — a defonce's type, a parameter, a return type — the pair may be left
out. *)
| "map-new" ->
let k, v, args = map_new_types ctx ~want loc args in
let a = allocator_arg ctx loc args in
let mty = map_type ~preds:ctx.env.tvpreds loc k v in
let m = fresh_slot ctx mty in
let attempt =
rt loc (Types.Int Types.I8) "flan_map_init"
[ mk loc mty (Tast.Local m); a; size_of loc k; size_of loc v;
here loc ]
in
expect ctx loc ~want
(mk loc mty
(Tast.Let ([ (m, mk loc mty (Tast.Zero mty)) ],
[ with_note loc (alloc_guard ctx loc attempt)
(reg_note loc "flan_dev_reg_note_map"
(mk loc mty (Tast.Local m))
[ size_of loc k; size_of loc v ] mty);
region_check ctx.env loc (mk loc mty (Tast.Local m))
(mk loc mty (Tast.Local m)) ])))
(* (put m k v) — the upsert. Unit, not a Result and not an ignorable error
code: see [alloc_guard]. spec-memory.md is explicit that it either
inserts or replaces, and that (set (get m k) v) is not map syntax. *)
| "put" ->
arity ctx loc name 3 args;
(match args with
| [ target; k; v ] ->
let target = check_target ctx target in
refuse_const_change ctx loc target;
(* A put into a dyn map is a call and nothing else, the way a push into
a dyn vec is: the runtime owns the storage, so there is no guard, no
restart and no region check. An equal key's value is replaced. The
site rides along for the one refusal a put can meet, a class
instance's typed slot. *)
if target.Tast.ty = Types.Dyn then
expect ctx loc ~want
(rt loc Types.Unit "flan_dyn_map_put"
[ target; check ctx ~want:Types.Dyn k;
check ctx ~want:Types.Dyn v; here loc ])
else begin
let kt, vt = map_kv loc "put" target.Tast.ty in
note_grown ctx "put" loc target;
let k = check ctx ~want:kt k in
let v = check ctx ~want:vt v in
(* Deferred: the arguments are checked — so a move here is still a move
and a borrow still a borrow — and the node itself is a unit no-op,
thrown away with the rest of the abstract pass. *)
if deferred_key ctx.env loc "put" kt then
expect ctx loc ~want (mk loc Types.Unit Tast.Unit)
else
(* Both are bound before the loop, so that a [retry] re-attempts the
allocation and not the expressions that produced the key and the
value. The same rule [push] follows for its element. *)
let ks = fresh_slot ctx kt and vs = fresh_slot ctx vt in
let hash, eq = key_fns ctx.env loc kt in
let attempt =
rt loc (Types.Int Types.I8) "flan_map_put"
[ target; addr_of loc (mk loc kt (Tast.Local ks));
addr_of loc (mk loc vt (Tast.Local vs));
size_of loc kt; size_of loc vt; hash; eq; here loc ]
in
expect ctx loc ~want
(mk loc Types.Unit
(Tast.Let ([ (ks, k); (vs, v) ],
[ region_check ctx.env loc target
(with_note loc (alloc_guard ctx loc attempt)
(reg_note loc "flan_dev_reg_note_map" target
[ size_of loc kt; size_of loc vt ]
target.Tast.ty)) ])))
end
| _ -> assert false)
(* (get m k) -> (Option V). Absence is None, not an untyped nil, and the
answer is a copy of the value's bytes — for an owning value, a copy of
its header, aliasing what the map's slot points at.
There is no allocation here and therefore no guard: a lookup that finds
nothing is an answer, not a failure. *)
| "get" ->
(match args with
| target :: (_ :: _ :: _ as idx) ->
(* Two indices or more: an array, a slice or a Vec, one per
dimension, or a dyn walked a level per index. A map takes one key. *)
let target = check_target ctx target in
(match target.Tast.ty with
| Types.Map _ ->
fail loc "a map's get takes one key, as (get m k), and this has %d"
(List.length idx)
| Types.Named "String" ->
(ignore (refuse_string_index (List.hd idx).Ast.loc ~store:false); assert false)
| Types.Dyn -> dyn_get ctx ~want loc target idx
| _ -> checked_get ctx ~want loc target idx)
| [ target; k ] ->
let target = check_target ctx target in
(match target.Tast.ty with
| Types.Array _ | Types.Slice _ | Types.String | Types.Vec _ ->
checked_get ctx ~want loc target [ k ]
(* The same refusal [at] gives: a String is not indexed. *)
| Types.Named "String" -> (ignore (refuse_string_index k.Ast.loc ~store:false); assert false)
| Types.Dyn -> dyn_get ctx ~want loc target [ k ]
| _ ->
(* A dyn map's absence is nil, not None: the typed map can promise an
(Option V) because V was written down, and a dyn map has nothing to
write. nil is an ordinary dyn value the caller compares against —
and (contains? m k) is the question to ask when nil might also be
stored under the key. *)
let kt, vt = map_kv loc "get" target.Tast.ty in
let k = check ctx ~want:kt k in
(* Deferred, and the placeholder is [None] rather than [Unit]: this
form answers an (Option V), and the abstract pass still has to
type-check whatever the body does with the answer. *)
if deferred_key ctx.env loc "get" kt then
expect ctx loc ~want (mk loc (Types.Option vt) Tast.None_)
else
map_lookup ctx ~want loc "flan_map_get" target kt vt k)
| _ -> arity ctx loc name 2 args; assert false)
(* (keyword s) -> the interned dyn keyword named by the bytes, for a name
that only exists at run time — a reader building :texture-path out of a
token's text. A literal :foo never comes through here. *)
| "keyword" ->
arity ctx loc name 1 args;
(match args with
| [ s ] ->
let s = check ctx s in
(match s.Tast.ty with
| Types.String | Types.Slice (_, (Types.Int Types.U8)) ->
expect ctx loc ~want (rt loc Types.Dyn "flan_dyn_kw" [ s ])
| other ->
fail loc "keyword takes a string or a [u8], found %s"
(tyname loc other))
| _ -> assert false)
(* (class-of v) -> the class's name as a keyword, or nil. It is the dyn
side's one question about shape, and the dispatch a (defgeneric ...)
compiles to is this call and a comparison — so what a class dispatcher
is, exactly, is the shape tag of the first argument as the dispatch
function, which is what makes the CLOS half and the Clojure half one
mechanism rather than two.
Anything that is not an instance answers nil rather than trapping: an
ordinary map, a number, nil itself. Asking is not a claim, and the
question is askable of every value — the same line [get] takes about an
absent key. *)
| "class-of" ->
arity ctx loc name 1 args;
(match args with
| [ v ] ->
expect ctx loc ~want
(rt loc Types.Dyn "flan_dyn_class_of" [ check ctx ~want:Types.Dyn v ])
| _ -> assert false)
(* (type-of v) -> the value's kind as a keyword, or a class instance's
class name. A typed argument crosses into dyn first, as it does for
class-of and every other dyn builtin, so it answers the kind the crossing
makes of it: a typed i32 is :int and an f32 is :float. That keeps one
answer per value whichever side of the program holds it. *)
| "type-of" ->
arity ctx loc name 1 args;
(match args with
| [ v ] ->
expect ctx loc ~want
(rt loc Types.Dyn "flan_dyn_type_of" [ check ctx ~want:Types.Dyn v ])
| _ -> assert false)
(* (chars t) and (text x): a dyn text as a vec of its chars, and back. A
dyn text is immutable, so a vec of chars is how one is edited. *)
| "chars" | "text" ->
arity ctx loc name 1 args;
let sym = if name = "chars" then "flan_dyn_chars" else "flan_dyn_text" in
expect ctx loc ~want
(rt loc Types.Dyn sym
[ check ctx ~want:Types.Dyn (List.hd args); here loc ])
(* (map-remove m k) -> (Option V): the value that was there, or None when
the key was not. The same answer [get] gives, for the same reason — a key
that is not in the map is an answer and not a failure — and the value
comes back rather than being dropped on the floor, which is what makes
"take this out and use it" one call instead of a get and a remove that
hash the key twice.
It allocates nothing and releases nothing, so unlike [put] there is no
alloc_guard and no region check around it: a key and a value live inside
the one block the map allocated, and removal moves entries within that
block. That is what makes it mean the same thing on a map backed by an
arena — or by any allocator that refuses can-free — as on a heap-backed
one. Nothing is freed per entry because nothing was allocated per entry. *)
| "map-remove" ->
arity ctx loc name 2 args;
(match args with
| [ target; k ] ->
let target = check_target ctx target in
refuse_const_change ctx loc target;
let kt, vt = map_kv loc "map-remove" target.Tast.ty in
let k = check ctx ~want:kt k in
(* Deferred exactly as [get] is, and with [None] for the same reason:
the abstract pass still has to check whatever the body does with the
answer. *)
if deferred_key ctx.env loc "map-remove" kt then
expect ctx loc ~want (mk loc (Types.Option vt) Tast.None_)
else
map_lookup ctx ~want loc "flan_map_remove" target kt vt k
| _ -> assert false)
(* (map-next m (addr cur) (addr k) (addr v)) -> bool, and the whole of map
iteration. Before it there was no way to read a map's keys or its values
at all: every other map operation addresses one entry by hashing it, and
nothing walked the block.
Three out-pointers rather than a returned pair, because there are no
tuples and a (Option K) would answer only half of an entry — the value
would then cost a second hash of the key just answered. The cursor is an
i64 the caller owns and the loop reads as one:
(let [cur 0 k 0 v 0]
(while (map-next m (addr cur) (addr k) (addr v))
...))
The prelude's map-keys and map-values are this loop over a generic key.
No hash and no equality pair go with it — walking asks nothing about a
key — so this is the one map entry point whose signature carries neither,
and the sizes are still needed because the runtime is type-erased. *)
(* (map-next m cur k) walks the keys alone. It is what lets a walk need no
place for a value, which matters when the value is a function value: one
cannot be zeroed to make the place, and a key never is one. *)
| "map-next" ->
(match args with
| [ _; _; _ ] | [ _; _; _; _ ] -> ()
| _ ->
fail loc
"map-next is (map-next m (addr cursor) (addr k) (addr v)) or, for \
the keys alone, (map-next m (addr cursor) (addr k)) — given %d \
arguments" (List.length args));
(match args with
| target :: cur :: k :: rest ->
let target = check_target ctx target in
let kt, vt = map_kv loc "map-next" target.Tast.ty in
let cur = check ctx ~want:(Types.Ptr (Types.Mut, (Types.Int Types.I64))) cur in
let k = check ctx ~want:(Types.Ptr (Types.Mut, kt)) k in
let vp = Types.Ptr (Types.Mut, vt) in
let v = match rest with
| [ v ] -> check ctx ~want:vp v
| _ -> mk loc vp (Tast.Zero vp)
in
let found =
rt loc (Types.Int Types.I8) "flan_map_next"
[ target; cur; k; v; size_of loc kt; size_of loc vt; here loc ]
in
expect ctx loc ~want
(mk loc Types.Bool
(Tast.Prim
(Tast.Ne,
[ found;
mk loc (Types.Int Types.I8) (Tast.Int (0L, Types.I8)) ])))
| _ -> assert false)
(* (has-key m k). (get m k) answers the same question, but through an
Option the caller then has to match; this is the form a condition wants,
and it copies no value. *)
| "has-key" ->
arity ctx loc name 2 args;
(match args with
| [ target; k ] ->
let target = check_target ctx target in
(* The dyn map's question, one word with the typed one. It exists on
the dyn side because absence there is nil, and a map can also store
nil under a key — (get m k) answering nil cannot tell the two
apart, and this can. The bool comes back unboxed the way a dyn
comparison does, because a presence test is overwhelmingly an if's
condition. *)
if target.Tast.ty = Types.Dyn then
expect ctx loc ~want
(unbox loc Types.Bool
(rt loc Types.Dyn "flan_dyn_map_contains_at"
[ target; check ctx ~want:Types.Dyn k; here loc ]))
else begin
let kt, vt = map_kv loc "has-key" target.Tast.ty in
let k = check ctx ~want:kt k in
(* Deferred, and the placeholder is a [bool] — the form a condition
wants, so the condition around it still has to check. *)
if deferred_key ctx.env loc "has-key" kt then
expect ctx loc ~want (mk loc Types.Bool (Tast.Bool false))
else
let hash, eq = key_fns ctx.env loc kt in
let ks = fresh_slot ctx kt in
let found =
rt loc (Types.Int Types.I8) "flan_map_has"
[ target; addr_of loc (mk loc kt (Tast.Local ks));
size_of loc kt; size_of loc vt; hash; eq; here loc ]
in
expect ctx loc ~want
(mk loc Types.Bool
(Tast.Let ([ (ks, k) ],
[ mk loc Types.Bool
(Tast.Prim
(Tast.Ne,
[ found;
mk loc (Types.Int Types.I8)
(Tast.Int (0L, Types.I8)) ])) ])))
end
| _ -> assert false)
(* ── Assets, decision 1: embedded at compile time ──────────────
Odin's #load and #load_directory are the model (src/parser.cpp,
src/check_builtin.cpp's check_load_directive), and the reason it is the
right answer here is TODO.org's "Assets are embedded at compile time":
it is a *compiler* feature, so
it needs no build flags, no linker arguments and no per-target packaging,
and it works identically on desktop and web. That matters more here than
it does for Odin, because [Load] gives link flags only to a directory
package — the single file doing (rl/load-texture "brush.png") is
structurally the one file with no link channel. Embedding has no such
hole.
Odin's `#` is not imported. An s-expression language already has a head
position for a name, so these are ordinary named calls spelled [embed] and
[embed-dir], resolved here exactly as [vec-new] and [heap-allocator] are.
The result costs nothing at run time: the bytes become a
`private unnamed_addr constant` string, the same one every string literal
already becomes, and emit.ml's [escape] is byte-exact, so a PNG survives
the round trip through the .ll. Bound with [defconst], an [embed-dir]
becomes an LLVM constant outright (emit.ml's [const]).
The slice this hands back points into .rodata, so it is a [const u8]: a
store through it would segfault at -O0 and be deleted at -O2, and the
type refuses it at compile time instead. Copy the bytes for a writable
buffer. *)
| "embed" ->
(match args with
| [ p ] | [ p; _ ] ->
(* The spelling is settled before the file is opened, so a program that
asks for a type embed cannot read a file as is told that, rather than
being told the file is missing and left to discover the other half
after fixing it. *)
(match args with
| [ _; { Ast.e = Ast.Var "str"; _ } ] | [ _ ] -> ()
| [ _; t ] ->
fail t.Ast.loc
"embed's second argument is str, or nothing for a [const u8]"
| _ -> ());
let data = read_embed_file (embed_path loc p) p.Ast.loc in
let as_string () = mk loc Types.String (Tast.Str data) in
(* A [Str] node typed [const u8] rather than a [Bytes] prim over one. [Bytes]
is identity — emit.ml lowers String and Slice _ to the same %slice —
and the prim would make the node non-constant, so an (embed-dir) in a
defconst could not be an LLVM constant. Both of emit.ml's string
emitters take the bytes and ignore the node's type, so this is the
same constant either way, and it is one a global can hold. *)
let as_bytes () = mk loc (Types.Slice (Types.Const, Types.Int Types.U8)) (Tast.Str data) in
(* Two spellings rather than one that changes type with its context.
Odin threads a type_hint everywhere and can afford (embed "p") to
mean a string here and a []u8 there; with structural equality and a
container that never converts to another container -- implicit
widening is numbers only -- the same text meaning
two types would be a wart. [want] is a fallback only, and nothing
depends on it. *)
(match args with
| [ _; { Ast.e = Ast.Var "str"; _ } ] ->
expect ctx loc ~want (as_string ())
| _ ->
(match want with
| Some Types.String -> as_string ()
| _ -> expect ctx loc ~want (as_bytes ())))
| _ ->
fail loc
"embed is (embed \"path\") for a [const u8], or (embed \"path\" str)")
(* ── What a macro says when it has to refuse ───────────────────
The one thing a macro could not do, written down in the prelude where
[unless] settles for it: "a macro has no error facility: it runs inside
the compiler and anything it signals aborts the compile with no location.
So a malformed (unless) answers a name nothing defines, and the report is
'unknown name unless-takes-a-test-and-a-body' at the call site, which is
the right place and the wrong sentence."
A name nothing defines carries a name. It cannot carry a sentence, and a
type provider's refusals are all sentence: the third element of this
vector is a string where the first two were integers; there is no file at
assets/x.edn; :size is a map with keys of two kinds. Those name a position
in a *data* file, which no symbol the expansion could invent will hold.
So a macro that has to refuse expands to a call to this, and the string is
the report. The string is a form the macro built, so it has no line of its
own and takes the call site's — the [defedn] the author wrote. The
location is theirs and the sentence is the macro's, which is the two
halves the prelude's note says are never both right at once.
A builtin and not a declaration, because it has to fail *here*: a declared
function would compile, link and run, and the compile it was meant to stop
would have succeeded. The whole of it is one arm, and the argument is a
literal for the same reason [embed]'s path is one — there is nothing at
this point in a compile to compute a string from. *)
| "compile-error" ->
arity ctx loc name 1 args;
(match (List.hd args).Ast.e with
| Ast.Str s -> fail loc "%s" s
| _ ->
fail (List.hd args).Ast.loc
"compile-error takes a literal string")
| "embed-dir" ->
arity ctx loc name 1 args;
let arg = List.hd args in
let entries = read_embed_dir (embed_path loc arg) arg.Ast.loc in
if not (Hashtbl.mem ctx.env.structs "EmbedFile") then
fail loc
"embed-dir answers a [n EmbedFile], and EmbedFile is not in scope";
let ety = Types.Named "EmbedFile" in
let elems =
List.map
(fun (nm, data) ->
mk loc ety
(Tast.Make
("EmbedFile",
[ mk loc Types.String (Tast.Str nm);
mk loc (Types.Slice (Types.Const, Types.Int Types.U8)) (Tast.Str data) ])))
entries
in
expect ctx loc ~want
(mk loc (Types.Array (Int64.of_int (List.length entries), ety))
(Tast.Arr elems))
(* ── slurp and barf, decisions 2 and 5 ─────────────────────────
[slurp] reads a whole file and answers a (Vec u8). It allocates, which is
why it waited for Vec, and it follows spec-memory.md's rule to the letter:
no allocating operation returns an error, so there is no Result here and
no out-parameter — a failure to allocate is StorageExhausted under [retry]
and a failure to read is FileError under [retry] and [use-value].
The two guards nest rather than merge, and that is the point: they are two
different failures with two different answerable questions, and a handler
that grows an arena is not the handler that supplies another path.
Everything is inside the file loop, so a [use-value] that names a
different file re-measures it and re-allocates for its size. The Vec is
freed at the top of each turn, which is why a retry does not leak; freeing
a Vec that never allocated is a no-op (flan_rt.c, flan_vec_free). *)
| "slurp" ->
(match args with
| path :: rest when List.length rest <= 1 ->
let path = check ctx ~want:Types.String path in
let a = allocator_arg ctx loc rest in
let ps = fresh_slot ctx Types.String in
let psv () = mk loc Types.String (Tast.Local ps) in
let u8 = Types.Int Types.U8 in
let vt = Types.Vec u8 in
let v = fresh_slot ctx vt in
let vv () = mk loc vt (Tast.Local v) in
let n = fresh_slot ctx (Types.Int Types.I64) in
let nv () = mk loc (Types.Int Types.I64) (Tast.Local n) in
let steps try_ =
[ (* The size first, because it is the step that does not allocate:
a missing file is found before any storage is committed to it. *)
try_ (rt loc (Types.Int Types.I8) "flan_file_size"
[ psv (); addr_of loc (nv ()) ]);
(* Previous turn's storage, if a retry brought us back here. *)
rt loc Types.Unit "flan_vec_free"
[ vv (); size_of loc u8; align_of loc u8; here loc ];
with_note loc
(alloc_guard ctx loc
(rt loc (Types.Int Types.I8) "flan_vec_init"
[ vv (); a; nv (); size_of loc u8; align_of loc u8;
here loc ]))
(reg_note loc "flan_dev_reg_note_vec" (vv ())
[ size_of loc u8 ] u8);
(* Fills the Vec the line above sized. A file that grew since the
measurement is truncated to the buffer; one that shrank leaves a
shorter Vec. Both are successful reads of what was there. *)
try_ (rt loc (Types.Int Types.I8) "flan_slurp_into"
[ vv (); psv (); size_of loc u8; here loc ]) ]
in
expect ctx loc ~want
(mk loc vt
(Tast.Let
([ (ps, path);
(n, i64_at loc 0L);
(v, mk loc vt (Tast.Zero vt)) ],
[ file_guard ctx loc ~path_slot:ps ~op:0 steps; vv () ])))
| _ -> fail loc "slurp is (slurp path) or (slurp path allocator)")
(* [barf] writes a whole file, and on the web target it signals — every time,
with the path in the condition. Decision 2, and the reason is worth having
at the call site: Flan has NO conditional compilation, so "isolate this to
desktop" is not expressible in source and a build-time refusal would be
unusable; a silent no-op is worse than either, because that is how a save
file disappears with nothing said. So the program gets a condition and
decides. Nothing here reads the target — the refusal is flan_rt.c's, one
#ifdef in the host layer, which is exactly where the two targets are
already implemented twice. *)
| "barf" ->
arity ctx loc name 2 args;
(match args with
| [ path; data ] ->
let path = check ctx ~want:Types.String path in
let data = byte_slice ctx data in
let ps = fresh_slot ctx Types.String in
let ds = fresh_slot ctx (Types.Slice (Types.Const, Types.Int Types.U8)) in
let steps try_ =
[ try_ (rt loc (Types.Int Types.I8) "flan_file_write"
[ mk loc Types.String (Tast.Local ps);
mk loc (Types.Slice (Types.Const, Types.Int Types.U8)) (Tast.Local ds) ]) ]
in
(* Both operands are bound before the loop so that a retry re-attempts
the write and not the expressions that produced it — the same rule
alloc_guard states for push. *)
expect ctx loc ~want
(mk loc Types.Unit
(Tast.Let ([ (ps, path); (ds, data) ],
[ file_guard ctx loc ~path_slot:ps ~op:1 steps ])))
| _ -> assert false)
(* ── the three that change the filesystem ──────────────────────────
[delete-file], [rename-file] and [make-directory] are [barf]'s shape with
a different runtime call, and they are here rather than as prelude
[declare]s for the one thing a declare cannot do: signal [FileError] with
the two restarts the compiler emits. A declare could only answer a bool,
and "the delete failed, here is a boolean" is the shape decision 5 exists
to keep out of this language — a handler that made the parent directory
and wants [retry], or that has another path and wants [use-value], has
nothing to hold onto.
Each answers [()] and not a bool for the same reason [barf] does: the
failure is the condition, so a return value would only ever be true. The
questions that are *not* failures — does this exist, how big is it —
answer a value instead, and those two are prelude functions over one
[declare] because nothing about them needs a restart.
[op] continues the FileError numbering the prelude names: 0 read, 1 write,
and 2, 3, 4 here. A handler matching on it is matching on the prelude's
[file-op-delete] and friends, not on a literal. *)
| "delete-file" | "make-directory" ->
arity ctx loc name 1 args;
let sym, op =
if String.equal name "delete-file" then "flan_file_delete", 2
else "flan_file_mkdir", 4
in
let path = check ctx ~want:Types.String (List.hd args) in
let ps = fresh_slot ctx Types.String in
let steps try_ =
[ try_ (rt loc (Types.Int Types.I8) sym
[ mk loc Types.String (Tast.Local ps) ]) ]
in
expect ctx loc ~want
(mk loc Types.Unit
(Tast.Let ([ (ps, path) ],
[ file_guard ctx loc ~path_slot:ps ~op steps ])))
(* Two paths and one restart slot, so the guard holds the *source*: a
[use-value] renames a different file to the same destination. That is the
direction a handler can act on — the destination it asked for is the one
thing it already knows — and it is written down here because the other
reading is equally plausible until somebody says which it is.
The destination is bound before the loop, exactly as [barf] binds its
data, so a retry re-attempts the rename and not the expression that
computed where to. *)
| "rename-file" ->
arity ctx loc name 2 args;
(match args with
| [ from_; to_ ] ->
let from_ = check ctx ~want:Types.String from_ in
let to_ = check ctx ~want:Types.String to_ in
let ps = fresh_slot ctx Types.String in
let ds = fresh_slot ctx Types.String in
let steps try_ =
[ try_ (rt loc (Types.Int Types.I8) "flan_file_rename"
[ mk loc Types.String (Tast.Local ps);
mk loc Types.String (Tast.Local ds) ]) ]
in
expect ctx loc ~want
(mk loc Types.Unit
(Tast.Let ([ (ps, from_); (ds, to_) ],
[ file_guard ctx loc ~path_slot:ps ~op:3 steps ])))
| _ -> assert false)
(* ── containers ────────────────────────────────────────────────── *)
(* [at] and [length] were already the names for a fixed array and a slice,
so a Vec extends them rather than adding a parallel pair — which is the
asymmetry [nth] was removed for. A Vec's length is i32 like every other
length here (index_ty): widening indices is one change across all of them
and not a Vec question. *)
| "length" ->
arity ctx loc name 1 args;
let target = List.hd args in
let a = check_target ctx target in
(match a.Tast.ty with
| Types.Array _ | Types.Slice _ | Types.String ->
prim Tast.Len index_ty [ a ]
| Types.Vec _ ->
let n = rt loc (Types.Int Types.I64) "flan_vec_len" [ a; here loc ] in
expect ctx loc ~want (mk loc index_ty (Tast.Prim (Tast.Cast index_ty, [ n ])))
(* Bytes, as on a str (decision 107); rune-count counts characters. *)
| t when is_string_ty t ->
let n =
rt loc (Types.Int Types.I64) "flan_vec_len" [ string_vec loc a; here loc ]
in
expect ctx loc ~want (mk loc index_ty (Tast.Prim (Tast.Cast index_ty, [ n ])))
(* Extended rather than given a name of its own, for the reason [at] and
[length] were extended over Vec: one question, one word. *)
| Types.Map _ ->
let n = rt loc (Types.Int Types.I64) "flan_map_len" [ a; here loc ] in
expect ctx loc ~want (mk loc index_ty (Tast.Prim (Tast.Cast index_ty, [ n ])))
(* A dyn length is an i32 like every other length here, not a dyn holding
one. [length] is what an index loop compares against, and handing back a
boxed number would make [(< i (length xs))] a dyn comparison and a
pair of allocations per iteration. The runtime answers a dyn; it is unboxed at
once and narrowed the way the Vec's i64 above is. *)
| Types.Dyn ->
let n = unbox loc (Types.Int Types.I64) (rt loc Types.Dyn "flan_dyn_len_at" [ a; here loc ]) in
expect ctx loc ~want (mk loc index_ty (Tast.Prim (Tast.Cast index_ty, [ n ])))
| other ->
fail loc
"length takes an array, a slice, a str, a String, a Vec or a Map, \
found %s"
(tyname loc other))
| "at" ->
(match args with
| target :: idx when idx <> [] ->
let target = check_target ctx target in
(match target.Tast.ty with
| Types.Vec _ ->
let p, elem = vec_at ctx loc target idx in
expect ctx loc ~want (mk loc elem (Tast.Deref p))
(* One index, because a dyn container is one dimension: the nested
[(at grid r c)] spelling walks a type the compiler can see through,
and here it cannot. [(at (at g r) c)] is the spelling that works and
is what the refusal names. *)
| Types.Dyn ->
(match idx with
| [ i ] ->
expect ctx loc ~want
(rt loc Types.Dyn "flan_dyn_at"
[ target; check ctx ~want:Types.Dyn i; here loc ])
| _ ->
fail loc
"(at ...) over a dyn takes one index — write (at (at x i) j)")
| _ ->
let idx, ty = indexed ctx target idx in
prim Tast.At ty (target :: idx))
| _ -> fail loc "%s is (%s collection index ...)" name name)
(* (slice a), (slice a lo) and (slice a lo hi). The two short forms are
written out here into the three-argument one and are that form after
this line: same node, same checks, same code. Nothing is added at run
time, because neither missing argument needs anything computed —
[lo] is 0, and [hi] is the length, which on a fixed array is the
constant [length] already folds to and on a slice or a string is the
length word the value is carrying anyway.
The target is read twice when [hi] is the implicit length, so a target
that is not already a name goes into a slot first: [(slice (f x))] must
call [f] once. An array target never needs the slot, because its length
is a constant and the target appears exactly once.
One name over every target that has elements, a Vec included. There used
to be a second, [as-slice], for the Vec alone; the input type already
decides which semantics apply — a Vec can only be borrowed, an array can
only be viewed, and no call site chooses — so the second name expressed
nothing and is gone. [vec_slice] above is the Vec's half. *)
| "slice" ->
(match args with
| [] | _ :: _ :: _ :: _ :: _ ->
fail loc
"slice is (slice a), (slice a lo) or (slice a lo hi) — given %d \
arguments" (List.length args)
| target :: bounds ->
lit_typed_use ctx target;
let target = check_target ctx target in
let ty = target.Tast.ty in
match ty with
(* A Vec leaves here: everything below is written around a length the
compiler can see, and a Vec's is a word the runtime reads. *)
| Types.Vec elem -> vec_slice ctx ~want loc target elem bounds
(* A dyn leaves too, as [at] over one does: the bounds are dyn, like
[at]'s index, and a missing [hi] is nil, which the runtime reads as
the length. *)
| Types.Dyn ->
let bound b = check ctx ~want:Types.Dyn b in
let nil () = rt loc Types.Dyn "flan_dyn_nil" [] in
let lo, hi = match bounds with
| [] -> box loc (mk loc dyn_i64 (Tast.Int (0L, Types.I64))), nil ()
| [ lo ] -> bound lo, nil ()
| [ lo; hi ] -> bound lo, bound hi
| _ -> assert false
in
expect ctx loc ~want
(rt loc Types.Dyn "flan_dyn_slice" [ target; lo; hi; here loc ])
| _ ->
(* A string slices to a string, not to a [u8]: the result views the
same bytes and is read-only for the same reason the source is, and
calling it a byte slice would hand out a writable-looking view of
storage the program does not own. *)
let result = match ty with
(* An array reached through a [[const T]] or a (Ptr const T) is
read-only storage, and so is a view of it. *)
| Types.Array (_, t) when const_reached target <> None ->
Types.Slice (Types.Const, t)
| Types.Array (_, t) -> Types.Slice (Types.Mut, t)
| Types.Slice (m, t) -> Types.Slice (m, t)
| Types.String -> Types.String
| other ->
fail loc
"slice takes an array, a slice, a string or a Vec, found %s"
(tyname loc other)
in
(* An array that came back from a call is a value in a temporary this
expression does not own: the slice would outlive it and view
whatever the frame reused those bytes for, with nothing to trap on.
A [let] gives it a name and a lifetime, so that is what the refusal
names. An array *literal* is not this case — it is written here and
the frame holds it for as long as the form it is written in. *)
(match ty, target.Tast.e with
| Types.Array _, (Tast.Call _ | Tast.CallPtr _) ->
fail loc
"this slices an array a call returned, which is a temporary the \
slice would outlive. Bind it first: (let [a (…)] (slice a …))"
| _ -> ());
let int k = mk loc index_ty (Tast.Int (k, Types.I32)) in
(* [hi] is wanted twice only when it is the implicit length of
something whose length is not static. *)
(* An array literal is also given a slot, so that what the slice views
lives for the whole function in both backends: the x86 backend
otherwise holds it in an expression temporary, reclaimed as soon as
the slice has been made, and a later temporary — (clone ...)'s own,
say — was written over it. *)
let needs_slot =
(match ty, target.Tast.e with
| Types.Array _, Tast.Arr _ -> true
| _ -> false)
|| List.length bounds < 2
&& (match ty with Types.Array _ -> false | _ -> true)
&& (match target.Tast.e with
| Tast.Local _ | Tast.Global _ -> false
| _ -> true)
in
let slot = if needs_slot then Some (fresh_slot ctx ty) else None in
let src () = match slot with
| Some s -> mk loc ty (Tast.Local s)
| None -> target
in
let whole_len () = match ty with
| Types.Array (n, _) -> int n
| _ -> mk loc index_ty (Tast.Prim (Tast.Len, [ src () ]))
in
(* [index_expr] and not an [i32] expectation, which is what this used
to be. One builtin cannot answer two ways about the same bound, and
the question is settled by what every *other* subscript in the
language already does: [indexed] and [vec_at] both take their index
through here, so [(at a c)] over a [u32] compiles and [(slice a c)]
used to not. A bound is a subscript; it takes the subscript rule.
Nothing is loosened by it that a bounds check does not still catch —
[index_expr] admits an integer narrower than 32 bits and a u32,
whose out-of-range values truncate to a negative i32 the unsigned
comparison rejects, and refuses i64 and u64 by name. *)
let lo_loc, lo, hi_loc, hi =
match bounds with
| [] -> loc, int 0L, loc, whole_len ()
| [ lo ] -> lo.Ast.loc, index_expr ctx lo, loc, whole_len ()
| [ lo; hi ] ->
lo.Ast.loc, index_expr ctx lo, hi.Ast.loc, index_expr ctx hi
| _ -> assert false
in
(* A bound may sit one past the end, so the length is checked against
lo and hi both, not against the last valid index. *)
(match literal lo with
| Some k -> static_index lo_loc ty ~past_end:true "slice bound" k
| None -> ());
(match literal hi with
| Some k -> static_index hi_loc ty ~past_end:true "slice bound" k
| None -> ());
(match literal lo, literal hi with
| Some a, Some b when a > b ->
fail loc "slice [%Ld %Ld) runs backwards — lo must not exceed hi" a b
| _ -> ());
let body = mk loc result (Tast.Prim (Tast.Slice, [ src (); lo; hi ])) in
(match slot with
| None -> expect ctx loc ~want body
| Some s ->
expect ctx loc ~want
(mk loc result (Tast.Let ([ (s, target) ], [ body ])))))
(* (slice-from p n) — TODO.org, "A pointer from C needs a length before
it can be indexed". A (Ptr T) that came back from C is readable at
element 0 through [deref] and nowhere else, because [indexed] takes an
Array or a Slice and a pointer is neither. C hands back an address and no
length, so the length has to come from the caller, and this is the form
that says so out loud.
**What the caller is promising**, and the compiler checks none of it: that
[p] really addresses [n] consecutive [T], that they are initialised, and
that they outlive every use of the result. Get it wrong and this reads
memory that is not there — the bounds check the result carries will agree
with a length that was a lie, because the length *is* the lie. It is the
same trust [declare-c] already extends, written at the one site where
somebody had to know the answer anyway.
No marker on the name. A [?] in this language means *asks*
([is-font-valid]), and this does not ask; [zeroed], the nearest
neighbour — a value conjured rather than derived — carries no marker
either. The argument's type is the marker: only a (Ptr T) is accepted,
and a (Ptr T) only ever arrives from a [declare-c], an [addr] or a
pointer cast, so the site already says where the promise comes from.
**The count is any integer.** It is widened to i64 here, sign- or
zero-extended by its own kind, so both backends see one i64 and the
length word is what the caller wrote. The [n >= 0] test the backends
plant runs in every build, release included and [--no-bounds-checks]
included: it is not a bounds check against a known length (there is
none), it is the claim that the word being stored is a count at all. A
u64 above 2^63 fails it too, and should — no pointer has that many
elements behind it.
**It owns nothing.** The result is a [Types.Slice], the same non-owning
view (slice v) answers; (free s) on it is the program's error, which a
dev build's registry traps as a slice no allocator handed out. It carries
no allocator epoch either — a slice is two words, see TODO.org "A stale
slice reads poison in a dev build" — so a view made over arena memory
that is later freed reads the dev build's poison and does not trap. *)
| "slice-from" ->
arity ctx loc name 2 args;
(match args with
| [ target; n ] ->
let target_loc = target.Ast.loc in
let spelled_target = spell_arg "p" target in
let target = check ctx target in
let elem =
match target.Tast.ty with
| Types.Ptr (_, t) -> t
| other ->
fail target_loc
"slice-from takes a (Ptr T) and the number of elements behind \
it, found %s. A slice or an array already has a length; \
(slice v lo hi) views part of one"
(tyname loc other)
in
let n_loc = n.Ast.loc in
let spelled_n = spell_arg "n" n in
let n = check ctx n in
(match n.Tast.ty with
| Types.Int _ -> ()
| Types.Var v ->
cast_operand ctx n_loc name ~needs:"is-integer"
~what:"an element count" ~is:"an integer" v
| other ->
fail n_loc
"slice-from counts elements with an integer, found %s. Write \
(slice-from %s (i64 %s))"
(tyname loc other) spelled_target spelled_n);
(* A negative literal is a lie the checker can see, so it does not wait
for the run-time test emit.ml plants beside it. *)
(match literal n with
| Some k when k < 0L ->
fail n_loc
"slice-from length %Ld is negative" k
| _ -> ());
let i64 = Types.Int Types.I64 in
let n =
if Types.equal n.Tast.ty i64 then n
else mk n.Tast.loc i64 (Tast.Prim (Tast.Cast i64, [ n ]))
in
(* A read-only pointer gives a read-only slice, or [slice-from]
would undo the const [addr] put there. *)
let m = match target.Tast.ty with Types.Ptr (m, _) -> m | _ -> Types.Mut in
prim Tast.SliceFrom (Types.Slice (m, elem)) [ target; n ]
| _ -> assert false)
(* ── pointers ──────────────────────────────────────────────────── *)
| "addr" ->
arity ctx loc name 1 args;
let a = List.hd args in
lit_typed_use ctx a;
(match place_of_expr a with
| None ->
fail a.Ast.loc
"addr takes the address of a place — a name, (.field x), (at a i) \
or (deref p)"
| Some p ->
let p, ty = check_place ~store:false ctx a.Ast.loc p in
let m = if place_const p then Types.Const else Types.Mut in
expect ctx loc ~want (mk loc (Types.Ptr (m, ty)) (Tast.Addr p)))
| "deref" ->
arity ctx loc name 1 args;
let a = check ctx (List.hd args) in
(match a.Tast.ty with
| Types.Ptr (_, t) -> expect ctx loc ~want (mk loc t (Tast.Deref a))
| other -> fail loc "deref takes a (Ptr T), found %s"
(tyname loc other))
(* ── Option ────────────────────────────────────────────────────── *)
(* (Some nil) cannot be built. Some marks a value present; nil is dyn's own
way of saying absent; a present absence is what would make nil and None
the same case of an (Option dyn) and break nil <-> None at the boundary
in both directions. Refused here at the literal, which the checker can
see the same way it sees any other [nil]; a dyn value that only turns
out to be nil once the program runs is caught by the runtime guard on
the value instead, named for what it refuses rather than just that it
does. *)
| "Some" ->
arity ctx loc name 1 args;
let arg = List.hd args in
let inner = match want with Some (Types.Option t) -> Some t | _ -> None in
(* A literal [nil] is refused by this form's own message below, not by
[expect]'s bare-T refusal — checking it against [inner] here would
let a bare T's "wrap the type in Option" reach the reader even though
the type here is already wrapped in one. Checked with no want instead,
which is exactly what a bare [nil] resolves against on its own (see
[var]'s "nil" arm), so it arrives below still dyn and still nil. *)
let ast_nil = match arg.Ast.e with Ast.Var "nil" -> true | _ -> false in
let a = check ctx ?want:(if ast_nil then None else inner) arg in
let a =
if not (Types.equal a.Tast.ty Types.Dyn) then a
else if is_nil_lit a then
Loc.failk "check/some-nil" loc
"(Some nil) cannot be built — Some marks a value present, and nil \
is dyn's absence. Use None instead"
else rt loc Types.Dyn "flan_dyn_need_not_nil" [ a ]
in
expect ctx loc ~want (mk loc (Types.Option a.Tast.ty) (Tast.Some_ a))
(* ── the milestone-2 host primitives (plan.org) ────────────────── *)
(* (bytes-view s): the string's own storage seen as a [const u8], costing
nothing. The slice aliases the string, and a literal's bytes are in
read-only memory — a store through them would trap at -O0 and be deleted
as undefined at -O2 — so the view is one that can only be read, and a
store through it is refused here rather than at run time. (bytes s) is
the writable copy.
Reading through it is the whole use: is-bytes-equal, split, index-of-bytes and
every other comparison walks a string's bytes without copying them. *)
| "bytes-view" ->
arity ctx loc name 1 args;
let a = maybe_string ctx ~otherwise:Types.String (List.hd args) in
if string_or_ptr a.Tast.ty then expect ctx loc ~want (string_bytes ctx loc a)
else prim Tast.Bytes (Types.Slice (Types.Const, Types.Int Types.U8)) [ a ]
(* (bytes s) / (bytes s a): a *writable copy* of the string's bytes, from
the context allocator or one named — never a hidden malloc, which is
spec-memory.md's frozen rule over every allocating operation. It used to
be the zero-cost reinterpret above, and the author's in-place sort over
(bytes "INSERTIONSORT") wrote into the string constant; "I would expect
bytes to copy" is the ruling this implements. The lowering is
[dup_elems], which (clone xs) shares for any slice. *)
| "bytes" ->
(match args with
| s :: rest when List.length rest <= 1 ->
let s = check ctx ~want:Types.String s in
let a = allocator_arg ctx loc rest in
expect ctx loc ~want (dup_elems ctx loc (Types.Int Types.U8) s a)
| _ -> fail loc "bytes is (bytes s) or (bytes s allocator)")
(* (str b): a [u8] seen as a str. The mirror of (bytes-view s),
spelled the same way — a type name in head position, like (bytes s) and
unlike the numeric casts, which go through [is_cast] and really do
convert.
It costs nothing. emit.ml lowers Types.String and Types.Slice _ to the
same %slice, 16 bytes at align 8, so a string and a [u8] are already the
identical value at run time; both this and [Bytes] emit as the argument
itself. What changes is only what the checker will let the value be
passed to — which is the whole gap: i64->bytes answers a [u8] and every
declare-c text parameter wants a string, and nothing joined them.
Two decisions are baked in here.
1. It does NOT check UTF-8, because `str` does not claim UTF-8. The
prelude settles this: is-valid-utf8 is an ordinary function you call when
you care, decode-rune/rune-at/rune-count all take [u8] rather than
string, and decode-rune answers {.ok false .width 1} on a malformed
byte rather than assuming its input is well-formed. The one place the
runtime treats a string differently from a byte slice is
flan_escape_bytes, for a string nested in a printed structure, and that
is a byte-wise escape table with no decoding in it. So there is no code
that would be wrong about a string of arbitrary bytes, and a check here
would be the only enforcement point in the language — a claim the rest
of it does not make.
2. It takes a [const u8], so a [u8] and a (bytes-view s) are both
accepted. This direction only loses the ability to write — a string
is read-only everywhere — so the result of (str b) can reach
strictly fewer stores than b could.
The text i64->bytes and f64->bytes answer lives in the temp allocator
until the next (free-temp); calling it a string does not copy it, so text
kept past the frame is cloned first. *)
| "str" ->
arity ctx loc name 1 args;
(* A String's str is a view of its bytes, as (str (slice v)) is of a
Vec's: it costs nothing and lasts until the String next grows. *)
let a =
maybe_string ctx ~otherwise:(Types.Slice (Types.Const, Types.Int Types.U8))
(List.hd args)
in
if string_or_ptr a.Tast.ty then
prim Tast.StrOfBytes Types.String [ string_bytes ctx loc a ]
else prim Tast.StrOfBytes Types.String [ a ]
| "bytes->f64" ->
arity ctx loc name 1 args;
prim Tast.BytesToF64 (Types.Float Types.F64) [ byte_slice ctx (List.hd args) ]
| "bytes->i64" ->
arity ctx loc name 1 args;
prim Tast.BytesToI64 (Types.Int Types.I64) [ byte_slice ctx (List.hd args) ]
(* The number's text in the temp allocator: flan_i64_temp and flan_f64_temp
render it and bump-allocate the bytes there in one call, so the slice
outlives the frame — a function may return one and a Vec may hold one —
until the next (free-temp). A number drawn every frame is reclaimed every
frame; text kept longer is cloned. The number is bound before the guard's
loop, so a retry does not evaluate it twice.
The prelude's calls — append-i64, append-f64, format-f64, gensym — are
answered with a frame slot instead ([to_bytes]): each copies the bytes
into a Vec before the next conversion, so the temp copy would be work
thrown away. *)
| "f64->bytes" | "i64->bytes" ->
arity ctx loc name 1 args;
let f64 = name = "f64->bytes" in
let nty = if f64 then Types.Float Types.F64 else Types.Int Types.I64 in
let x = check ctx ~want:nty (List.hd args) in
let bslice = Types.Slice (Types.Mut, Types.Int Types.U8) in
expect ctx loc ~want
(if String.equal loc.Loc.file Prelude.file then
to_bytes ctx loc (if f64 then Tast.F64ToBytes else Tast.I64ToBytes) x
else
let xs = fresh_slot ctx nty and out = fresh_slot ctx bslice in
let attempt () =
rt loc (Types.Int Types.I8)
(if f64 then "flan_f64_temp" else "flan_i64_temp")
[ mk loc nty (Tast.Local xs);
addr_of loc (mk loc bslice (Tast.Local out)) ]
in
(* The guard — a retry restart around the attempt — is entered only
once a first attempt has failed, so the common case pays one call
and a compare. A failure then re-attempts under the guard exactly
as it would have. *)
let failed =
mk loc Types.Bool
(Tast.Prim (Tast.Eq,
[ attempt ();
mk loc (Types.Int Types.I8) (Tast.Int (0L, Types.I8)) ]))
in
mk loc bslice
(Tast.Let
([ (xs, x); (out, mk loc bslice (Tast.Zero bslice)) ],
[ mk loc Types.Unit
(Tast.If (failed, alloc_guard ctx loc (attempt ()),
unit_at loc));
mk loc bslice (Tast.Local out) ])))
| "write-stdout" ->
arity ctx loc name 1 args;
prim Tast.WriteStdout Types.Unit [ byte_slice ctx (List.hd args) ]
(* (println x) and (print x): the structural printer, selected on the type
the argument checked to. plan.org, Milestone 5 — "compiler-provided,
per concrete type". That is not overloading and needs no type variables:
there is no dispatch at run time and no user-supplied printer to pick
between. The walk itself is render.ml, shared with the REPL, which is what
stops the two from drifting apart.
[min]/[max]/[zeroed] above dispatch on the resolved argument type the same
way. The slots the slice arm needs come out of the frame of whatever
function this call is written in, via [fresh_slot] — allocated once per
call site, at check time, not once per iteration of a loop around it.
A string prints raw here and quoted inside a structure. Those are not in
conflict: (println "hello") has to print hello or it is useless, and
(println b) where b has a string field has to quote it or the field
cannot be told from the punctuation. The split is exactly top level vs
nested, which is why it lives here and not in the walk. *)
| "print" | "println" ->
(* Variadic, with Clojure's spacing: every argument prints in order with a
single space between each pair, and [println] ends the line. No arity
check — (println) is the newline alone and (print) is nothing, which is
also Clojure's answer. Each argument gets the same walk it would get
alone, so one call mixes typed and dyn values freely, and a bad argument
is refused at its own location: the arguments are checked one by one
below, each carrying its own loc, and render.ml fails on the
expression's loc rather than the form's. *)
(* Printing is a read, not a move: the walk goes over the value and keeps
nothing. Without this, (println v) would consume a Vec and every
printing of one would be its last. *)
let checked = List.map (fun target -> check ctx target) args in
(* ── The allow-list, and what it takes to get on it ───────────────
plan.org names [println] as the one compiler-provided exception — it
"selects a structural printer at each concrete instantiation" — and
that cannot be reconciled with an abstract pass as written: a pass that
decides an operator's legality *without* substituting cannot make an
exception for the one operator whose legality is only decidable after
substituting. So the exception is made explicit: these two forms are
*deferred* to instantiation, and every other operator is answered where
it is written.
Every member of this list is a place where a refusal moves from the
definition to a call site, which is the thing the abstract pass exists
to prevent. **That cost is not the same for every member, and the list
is not closed.** What makes it bearable is whether the call site has a
*stated requirement* to be refused against.
[print] and [println] have none and need none: every type prints, so
there is no [where] predicate for printability — one would always hold
and would be noise on a signature — and there is correspondingly no
call site these can be refused at. They are deferred and then always
succeed. That is the cheapest possible membership.
The map operations — [put], [get], [has-key], [map-remove], [reserve], [clone],
through [deferred_key] beside [key_fns] — are the other kind, and they
are here on a different argument. They *can* fail at a concrete type,
so deferring them does move a refusal. But [{:where (is-hashable $t)}] is
in the signature, and it is the author's own written requirement: an
instantiation at a non-hashable type is refused against that clause, by
name, at the call that asked for the type. That is a refusal the caller
can act on and one the generic's author chose to be responsible for —
categorically different from an unconstrained [(+ a b)] failing deep in
a body with no signature to blame, which is the case the abstract pass
exists to prevent and which stays refused at the definition. A generic
that does *not* declare the predicate gets no deferral: [deferred_key]
checks first, and [map_type] has usually refused the signature already.
So the rule for adding to this list is not a headcount. It is: either
the operation cannot fail after substituting, or a declared predicate
gives its failure a place to land. Anything else is answered here.
The node produced here is a unit no-op, thrown away with the rest of
the abstract pass. The real printer is selected when the copy is
checked with [t] concrete. One generic argument defers the whole call:
the printers for its neighbours would be re-selected at instantiation
anyway, so building them here would be work thrown away twice. *)
if List.exists (fun a -> open_ty a.Tast.ty) checked then
mk loc Types.Unit Tast.Unit
else
let bslice = Types.Slice (Types.Mut, (Types.Int Types.U8)) in
let write x = mk loc Types.Unit (Tast.Prim (Tast.WriteStdout, [ x ])) in
(* One frame slot per conversion the printer emits, which is what
[to_bytes] is for. The printer writes each number out before making the
next, so a shared buffer would in fact have served it — but the slot is
what the node now carries, and a printer that assembled its own buffer
would be a second answer to the same question. [escape] is the one that
still renders into a static: it is reachable from nowhere but here, and
its 1KB buffer per printed string field is a frame cost with no bug
behind it. Said here so the asymmetry is a decision and not an
oversight. *)
let conv pr x = to_bytes ctx loc pr x in
let emitter : Render.emitter =
{ Render.ebytes = write;
estr = (fun x -> write (mk loc bslice
(Tast.Prim (Tast.EscapeBytes, [ x ]))));
ei64 = (fun x -> write (conv Tast.I64ToBytes x));
eu64 = (fun x -> write (conv Tast.U64ToBytes x));
ef64 = (fun x -> write (conv Tast.F64ToBytes x));
edyn = (fun x -> mk loc Types.Unit (Tast.Prim (Tast.Rt "flan_dyn_print_at", [ x; here loc ])));
enested = (fun x -> mk loc Types.Unit (Tast.Prim (Tast.Rt "flan_dyn_print_nested_at", [ x; here loc ]))) }
in
let rc = render_ctx ctx emitter in
let c_top = emitter.Render.edyn in
let render_one a =
match a.Tast.ty with
| Types.String | Types.Slice (_, (Types.Int Types.U8)) ->
[ write (mk loc bslice (Tast.Prim (Tast.Bytes, [ a ]))) ]
(* A char at the top prints as the character itself (129a), through the
runtime's dyn printer so the two sides agree. *)
| Types.Char ->
[ c_top (box loc a) ]
(* A String prints as its text, raw at the top as a str does. *)
| t when is_string_ty t ->
[ write (mk loc bslice (Tast.Prim (Tast.Bytes, [ string_bytes ctx loc a ]))) ]
(* The walk names the value once per piece it reads — an option's tag
and then its payload, each field of a struct — so anything but a
plain variable is bound to a slot first, or [(println (small-pop s))]
pops once per piece. *)
| _ ->
(match a.Tast.e with
| Tast.Local _ | Tast.Global _ -> Render.render rc 0 a
| _ ->
let s = fresh_slot ctx a.Tast.ty in
[ mk loc Types.Unit
(Tast.Let
([ (s, a) ],
Render.render rc 0 (mk a.Tast.loc a.Tast.ty (Tast.Local s))))
])
in
(* Built fresh per use rather than shared: nothing else in this file puts
one node in two places of a tree, and a pass that hangs state off a
node would be entitled to assume it appears once. *)
let space () =
write (mk loc bslice
(Tast.Prim (Tast.Bytes, [ mk loc Types.String (Tast.Str " ") ])))
in
let parts =
match checked with
| [] -> []
| first :: rest ->
render_one first
@ List.concat_map (fun a -> space () :: render_one a) rest
in
let nl =
if String.equal name "println" then
[ write
(mk loc bslice
(Tast.Prim (Tast.Bytes, [ mk loc Types.String (Tast.Str "\n") ])))
]
else []
in
expect ctx loc ~want (mk loc Types.Unit (Tast.Do (parts @ nl)))
(* (watch "name" v) — v rendered into the dev watch table under the name.
The same walk as [print], with the pieces aimed at flan_dev.c's watch
slot instead of stdout, so a struct, a slice, an option or a dyn value
watches the way it prints.
The value is evaluated once, before the table is asked whether anyone is
looking, so a side effect in it happens whether or not a watch buffer is
open. The walk runs only when one is: [flan_dev_watch_begin_n] answers 0
when the table is not armed or is full, and the render is skipped.
Outside a dev build the backends drop the guarded [If] whole — see
[Tast.is_watch_guard] — so a release build evaluates the value and makes
no call at all. *)
| "watch" ->
arity ctx loc name 2 args;
let label = check ctx ~want:Types.String (List.hd args) in
let v = check ctx (List.nth args 1) in
if open_ty v.Tast.ty then mk loc Types.Unit Tast.Unit
else begin
let unit_rt sym args = mk loc Types.Unit (Tast.Prim (Tast.Rt sym, args)) in
let bslice = Types.Slice (Types.Mut, (Types.Int Types.U8)) in
let emitter : Render.emitter =
{ Render.ebytes = (fun x -> unit_rt "flan_dev_watch_emit" [ x ]);
estr = (fun x -> unit_rt "flan_dev_watch_emit_str" [ x ]);
ei64 = (fun x -> unit_rt "flan_dev_watch_emit_i64" [ x ]);
eu64 = (fun x -> unit_rt "flan_dev_watch_emit_u64" [ x ]);
ef64 = (fun x -> unit_rt "flan_dev_watch_emit_f64" [ x ]);
edyn = (fun x -> unit_rt "flan_dyn_emit_watch" [ x ]);
enested = (fun x -> unit_rt "flan_dyn_emit_watch" [ x ]) }
in
(* A place is read where it stands; anything else is bound to a slot of
this frame first, so the walk — which names its argument once per
field — does not run it once per field. *)
let bind, value =
match v.Tast.e with
| Tast.Local _ | Tast.Global _ -> [], v
| _ ->
let s = fresh_slot ctx v.Tast.ty in
[ (s, v) ], mk loc v.Tast.ty (Tast.Local s)
in
let body =
match value.Tast.ty with
(* A string watches quoted, as it renders inside a structure: the
table's rows are values, and an unquoted one could not be told from
a number. *)
| Types.String ->
[ emitter.Render.estr (mk loc bslice (Tast.Prim (Tast.Bytes, [ value ]))) ]
| _ ->
Render.render
~refuse:(fun _loc t ->
Printf.sprintf
"%s has no rendering, so it cannot be watched — watch the \
values you want out of it instead"
(tyname loc t))
(render_ctx ctx emitter) 0 value
in
let begin_ =
mk loc (Types.Int Types.I32)
(Tast.Prim (Tast.Rt Tast.watch_begin, [ label ]))
in
let zero = mk loc (Types.Int Types.I32) (Tast.Int (0L, Types.I32)) in
let open_ = mk loc Types.Bool (Tast.Prim (Tast.Ne, [ begin_; zero ])) in
let guarded =
mk loc Types.Unit
(Tast.If
( open_,
mk loc Types.Unit
(Tast.Do (body @ [ unit_rt "flan_dev_watch_end" [] ])),
mk loc Types.Unit Tast.Unit ))
in
expect ctx loc ~want
(match bind with
| [] -> guarded
| _ -> mk loc Types.Unit (Tast.Let (bind, [ guarded ])))
end
| "exit" ->
arity ctx loc name 1 args;
prim Tast.Exit Types.Never [ check ctx ~want:index_ty (List.hd args) ]
| "argv" ->
arity ctx loc name 0 args;
prim Tast.Argv (Types.Slice (Types.Mut, Types.String)) []
(* ── casts: (i32 x), (f64 x), and an enum both ways ────────────────
(i32 e) and (GamepadAxis n) are written here rather than in an arm of
their own because they are the same operation: an enum is an i32 at run
time — Types.Enum says so — and emit.ml's [cast] already reduces one to
its i32 before choosing an instruction. So both directions cost nothing:
src and target are equal after that reduction and [cast] answers the
value unchanged.
Why this does not give the typo back. The property worth keeping is that
:spcae at a call site is an error at that site, and it still is: a
keyword resolves against the parameter's enum and a bare integer does not
fit one. What changes is only that a program can *say* it means the
conversion, by name, at the site. The rule was never "an integer is
dangerous", it was "an integer must not arrive silently", and a written
(GamepadAxis i) is not silent.
Three sub-decisions:
1. enum → any numeric is always allowed and never checked. It is lossless
to i32 by construction, and a narrower target truncates by the same
rule every int→int cast already follows — no special case, and (f32 e)
means (f32 (i32 e)) rather than an arbitrary refusal.
2. integer → enum accepts a value that is not a declared member. raylib's
gesture is a bitfield and an OR of flags is a legal Gesture that is no
single member, so refusing it would refuse correct programs; and
session.ml's printer already falls through to the number for an
out-of-range enum, on purpose, so refusing to *construct* one while
blessing its display would be incoherent. An Option would make every
site unwrap for no safety bought, and a literal-only refusal would
catch nothing — the bitfield case is a run-time value.
3. Only an integer converts *to* an enum. Not a float, which has no
meaning here, and not another enum: an enum-to-enum hop goes through
(i32 x) so that both ends are written down. *)
| _ when Hashtbl.mem ctx.env.enums name ->
arity ctx loc name 1 args;
let target = resolve_name ctx.env ~seen:[] loc name in
let a = check ctx (List.hd args) in
(match a.Tast.ty with
| Types.Int _ -> ()
(* A type variable, answered by its bound rather than by a type it does
not have yet: [is-integer] admits exactly the integer kinds, which is
what sub-decision 3 above asks for, and [is-numeric] is a bound too wide
because it admits the floats that decision refuses. *)
| Types.Var v ->
cast_operand ctx loc name ~needs:"is-integer" ~what:"an integer to an enum"
~is:"an integer" v
| other ->
fail loc "%s converts an integer to an enum, found %s — an enum or a \
float goes through (i32 x) first" name
(tyname loc other));
prim (Tast.Cast target) target [ a ]
(* A cast to a *type variable*: [(t x)] or [($t x)] inside a generic body.
The name is not one [is_cast] knows, because [is_cast] asks whether the
name is a machine type and [t] is not — so this is its own arm, above the
ordinary one and below the enums, and it reaches the same [Cast] prim.
Inside an instantiation [resolve_name] has already answered with the
concrete target, so the copy casts to a real type and the emitter sees
nothing unusual. During the abstract pass the target is [Var t] and the
[where] clause is what says the cast means anything at all: a cast
produces a number, so [is-numeric] is what admits it.
A sigil on a name nothing binds comes here too, for the reason
[type_named] takes one: the character is only ever written where a type
goes, so [resolve_name] gets to say that a variable has no binding site
outside a defn signature. Otherwise [($u x)] would be an unknown function
in the same body where [(vec-new $u)] is an unbound variable — one
mistake told two ways.
Only where nothing else claims the name, though. Nothing stops a defn, a
struct or a binding from carrying the character, and a call to one is a
call and not a type: this arm sits above the arms that would have found
it — [ordinary_call] and, last of all, [positional_struct] — so it has to
decline first, once per table a name can be declared in. *)
| _ when (tyvar_in_scope ctx.env name
|| (name <> tyvar_bare name
&& lookup ctx name = None
&& not (Hashtbl.mem ctx.env.structs name)
&& not (Hashtbl.mem ctx.env.fns name)
&& not (Hashtbl.mem ctx.env.gsigs name)))
&& List.length args = 1 ->
let target = resolve_name ctx.env ~seen:[] loc name in
unconstrained ctx.env loc ("a cast to " ^ name) ~needs:"is-numeric" target;
let a = check ctx (List.hd args) in
(match a.Tast.ty with
| Types.Enum _ -> ()
| t when Types.is_numeric t -> ()
(* The operand's own bound, asked the same way the target's was one line
up. Accepting every [generic_ty] here took the target's [is-numeric] as
if it said something about the operand, so a second variable declared
only [is-ordered] passed the abstract pass. Nothing wrong was ever
emitted — [is-ordered] admits numbers and enums and both convert at the
instantiation — which is the point: the hole is only reachable the day
[is-ordered] admits a type that does not, and that day is why the
question is asked of the predicate and not of the set it denotes. *)
| Types.Var v ->
cast_operand ctx loc name ~needs:"is-numeric" ~also:("is-enum", "an enum")
~what:"a number or an enum" ~is:"a number" v
| t -> fail loc "%s converts a number, found %s" name (tyname loc t));
prim (Tast.Cast target) target [ a ]
(* (char n): a code point made a char. Only a Unicode scalar value is one,
so a literal is checked here and anything else at run time. *)
| "char" when List.length args = 1 ->
let x = List.hd args in
let char_lit n = mk loc Types.Char (Tast.Int (n, Types.U32)) in
let fln = fln_source loc in
let not_scalar n =
Loc.failk literal_at_want loc
"%Ld is not a Unicode scalar value, so it is not a char. A char is a \
code point from 0 to 0x10FFFF, outside 0xD800 to 0xDFFF" n
in
(match x.Ast.e, literal_arith x with
| Ast.Byte b, _ -> expect ctx loc ~want (char_lit (Int64.of_int b))
| _, Some n ->
if Int64.compare n 0L >= 0 && Int64.compare n 0x10ffffL <= 0
&& not (Int64.compare n 0xd800L >= 0 && Int64.compare n 0xdfffL <= 0)
then expect ctx loc ~want (char_lit n)
else not_scalar n
| _ ->
let a = check ctx x in
let checked i64 =
rt loc Types.Char "flan_char_of" [ i64; here loc ]
in
(match a.Tast.ty with
| Types.Char -> expect ctx loc ~want a
(* A u64 goes as itself, so one past 2^63 is named as the number
it is and not as the negative i64 with its bits. *)
| Types.Int Types.U64 ->
expect ctx loc ~want
(rt loc Types.Char "flan_char_of_u64" [ a; here loc ])
| Types.Int _ -> expect ctx loc ~want (checked (widen loc dyn_i64 a))
(* Explicit, so a dyn int converts as a typed one does. *)
| Types.Dyn ->
expect ctx loc ~want
(checked (rt loc dyn_i64 "flan_dyn_int_of" [ a ]))
| t ->
fail loc "char makes a char from an integer code point, found %s%s"
(tyname loc t)
(match t with
| Types.Float _ ->
if fln then " — convert it with i32(x) first"
else " — convert it with (i32 x) first"
| _ -> "")))
| _ when is_cast name && List.length args = 1 ->
let target = resolve_name ctx.env ~seen:[] loc name in
(* An integer literal too wide for the i32 it would default to is checked
at the target instead, so (u64 2935910691) and (i64 5000000000) are the
constants they say. One that fits i32 keeps the default and the cast,
which is what (u32 -1) has always meant. *)
let operand_want =
match (List.hd args).Ast.e, target with
| Ast.Int n, (Types.Int _ | Types.Float _)
when Int64.compare n (-2147483648L) < 0
|| Int64.compare n 2147483647L > 0 -> Some target
| Ast.UInt _, (Types.Int _ | Types.Float _) -> Some target
(* A char literal is the number at the target, so (u8 \é) is refused
as a u8 literal is. *)
| Ast.Byte _, (Types.Int _ | Types.Float _) -> Some target
(* A float literal likewise: (f64 0.1) is the f64 nearest 0.1 and not
the f32 one widened, and (u64 1.8e19) converts the f64 it says. *)
| _, Types.Float _ when lit_kind (List.hd args) = Some `Float -> Some target
| _, Types.Int _ when lit_kind (List.hd args) = Some `Float ->
Some (Types.Float Types.F64)
| _ -> None
in
let a = check ctx ?want:operand_want (List.hd args) in
(match a.Tast.ty with
| Types.Enum _ -> ()
(* A dyn opens here — [cast_dyn], TODO.org, "A numeric cast opens a dyn
box". Only on this arm:
the generic one above casts to a type *variable*, whose [where] clause
says the operand is numeric, and a dyn is not what a [is-numeric] bound
admits. *)
| Types.Dyn -> ()
| t when Types.is_numeric t -> ()
(* A char's code point, into any integer width. *)
| Types.Char when Types.is_integer target -> ()
| Types.Char ->
fail loc "%s converts a number, and a char converts only to an integer, \
as %s" name
(if fln_source loc then "i32(c)" else "(i32 c)")
(* The operand of a conversion inside a generic body. The target is a
machine type, so what is in question is only the operand, and the
[where] clause is what answers it. *)
| Types.Var v ->
cast_operand ctx loc name ~needs:"is-numeric" ~also:("is-enum", "an enum")
~what:"a number or an enum" ~is:"a number" v
| t -> fail loc "%s converts a number, found %s" name (tyname loc t));
(match a.Tast.ty with
| Types.Dyn -> expect ctx loc ~want (cast_dyn ctx loc target a)
| _ -> prim (Tast.Cast target) target [ a ])
(* ── ordinary calls ────────────────────────────────────────────── *)
| _ -> ordinary_call ctx ~want loc name args
(* Everything that is not a builtin arm: a local of function type, a generic
signature, the function table, and the refusals for a name that is none of
them. Reached two ways — by falling past every arm above, and by the
shadowing guard at the very top of [named_call], which sends a call whose
name the program has defined straight here. One function so that both
routes resolve a name by exactly the same rules. *)
and ordinary_call ctx ~want loc name args =
match () with
(* A local or a parameter holding a function value, called by the name it is
bound to — which is what the body of [map] looks like. It is checked
before the global function table: a binding shadows a defn of the same
name (one namespace, ordinary lexical scoping). A local of any *other*
type falls through to the table, so a program that shadows a function
name with an i32 and then calls the function still means the function.
A local of the *enclosing* function holding one is the same case: a
lifted body captures it by value and then calls the copy. [peek_outer]
rather than [capture] in the guard, because a guard must not take a copy
on its way to deciding what a form means. *)
(* A [_] function whose body failed has no return type to give; its own
errors are reported with its body, so a call to it stands in. *)
| _ when ctx.env.recovering && Hashtbl.mem ctx.env.infer_failed name
&& lookup ctx name = None ->
List.iter (fun a -> ignore (check ctx a)) args;
ctx.env.poison <- ctx.env.poison + 1;
poison loc
(* A local bound to a refused initialiser's stand-in, called: the refusal
is already reported, so the call stands in too, its arguments still
checked. *)
| _ when ctx.env.recovering
&& (match lookup ctx name with
| Some b -> Types.equal b.bty Types.Never
| None -> false) ->
List.iter (fun a -> ignore (check ctx a)) args;
ctx.env.poison <- ctx.env.poison + 1;
poison loc
| _ when (match lookup ctx name with
| Some b -> callable_ty b.bty
| None ->
match peek_outer ctx name with
| Some b -> callable_ty b.bty
| None -> false) ->
(* The binding the guard already found, read directly. Going back through
[check] would repeat the lookup. *)
(match lookup ctx name with
| Some b -> call_value ctx ~want loc (local_of loc b) args
| None ->
match capture ctx loc name with
| Some b -> call_value ctx ~want loc (mk loc b.bty (Tast.Local b.slot)) args
| None -> assert false)
(* A global holding a function value — a (CFn ...) table entry's cousin,
since a global is one of the zeroed positions a CFn may sit in. Called
by its name the way a local one is. *)
| _ when (match Hashtbl.find_opt ctx.env.globals name with
| Some (ty, _) -> callable_ty ty
| None -> false) ->
let ty, _ = Hashtbl.find ctx.env.globals name in
call_value ctx ~want loc (mk loc ty (Tast.Global name)) args
(* A name with several versions: the number of arguments picks one, and
the call is then a call to that version by its own name. *)
| _ when Hashtbl.mem ctx.env.versions name ->
let vs = Hashtbl.find ctx.env.versions name in
(match List.assoc_opt (List.length args) vs with
| Some v -> ordinary_call ctx ~want loc v args
| None ->
let n = List.length args in
Loc.failk "check/no-version" loc
"%s has no arity that takes %d argument%s. It has these:\n%s"
name n (if n = 1 then "" else "s")
(String.concat "\n"
(List.map (fun (_, v) -> " " ^ version_text ctx.env loc v) vs)))
| _ when Hashtbl.mem ctx.env.gsigs name ->
private_ref ctx loc name;
let vars, params, ret = Hashtbl.find ctx.env.gsigs name in
generic_call ctx ~want loc name vars params ret args
| _ ->
match Hashtbl.find_opt ctx.env.fns name with
| Some (params, ret) ->
private_ref ctx loc name;
if List.length args <> List.length params then
fail loc "%s takes %d argument%s, given %d" name
(List.length params)
(if List.length params = 1 then "" else "s")
(List.length args);
let i = ref (-1) in
let args =
map2_lr (fun p a -> incr i; check_arg ctx name !i p a) params args
in
let args = c_literals ctx name params args in
(match Hashtbl.find_opt ctx.env.tracks name with
| Some tr -> expect ctx loc ~want (tracked_call loc ctx.env name tr ret args)
| None when Hashtbl.mem ctx.env.classes name ->
(* A class's constructor, told where it was called from so that a
slot it refuses names this call and not only the defclass. The
arguments go into temps first: one may itself construct, and
the site is set last, immediately before the call, so nothing
between the two can replace it. The constructor takes it as its
first act; one reached through a function value finds none. *)
let temps =
List.map (fun (a : Tast.expr) -> (fresh_slot ctx a.Tast.ty, a)) args
in
let uses =
List.map
(fun (s, (a : Tast.expr)) -> mk a.Tast.loc a.Tast.ty (Tast.Local s))
temps
in
expect ctx loc ~want
(mk loc ret
(Tast.Let
(temps,
[ rt loc Types.Unit "flan_dyn_ctor_site" [ here loc ];
mk loc ret (Tast.Call (name, uses)) ])))
| None ->
let call = mk loc ret (Tast.Call (name, args)) in
expect ctx loc ~want
(if is_string_ty ret && prelude_defined ctx name
&& not (String.equal loc.Loc.file Prelude.file)
then prechecked_call ctx loc name ret args
else call))
| None ->
if Hashtbl.mem ctx.env.datas name then
fail loc
"%s is a data type, so a value of it names a case — write \
(%s.%s {.field value ...})"
name name (first_case_name ctx.env name)
else if Hashtbl.mem ctx.env.cases name then
(* [(U.C)] and [(C)]: a case written as a call. Both are how someone
reaches for a constructor, and neither is one. *)
let dname, c = Hashtbl.find ctx.env.cases name in
fail loc
"%s is a case of the data type %s — write (%s.%s {.field value ...}), \
or %s.%s on its own when it has no fields"
name dname dname c.Tast.vname dname c.Tast.vname
else if Hashtbl.mem ctx.env.structs name then
positional_struct ctx ~want loc name args
else if Hashtbl.mem ctx.env.gstructs name then
positional_struct ctx ~want loc
(generic_ctor ctx ~want loc name
(`Positional args)) args
else if List.mem_assoc name operator_aliases then
(* Asked before the package test, because [/=] and [=/=] have a slash
in them and are not package calls. The did-you-mean cannot reach
these: [not=] is one edit from [not], which is the wrong answer,
and [&&] is no edit at all from [and]. *)
let flan, what = List.assoc name operator_aliases in
Loc.failk "check/unknown-function" loc
"there is no %s. %s is %s. %s" name what flan
(alias_fix flan args)
else if String.contains name '/' then
unimplemented loc
(Printf.sprintf "the call %s into an imported package" name) 4
else if name = "as-slice" then
(* A name nothing defines, near enough to [slice] to be worth a
sentence rather than a did-you-mean: whatever [slice] is given
decides what the view means, so there is one word for it and this
says which. Asked here, after every table, so that a program that
defines an [as-slice] of its own still reaches its own. *)
(* The call is written back out rather than described, and every
argument the reader wrote that can be spelled is spelled
([spell_arg]), so the suggestion is always a form that compiles
rather than a form with a hole in it. *)
let spell = spell_arg in
let call =
match args with
| [] -> "(slice v)"
| t :: bounds ->
let names = [ "lo"; "hi" ] in
let bounds =
List.mapi
(fun i b ->
" " ^ spell (try List.nth names i with _ -> "n") b)
bounds
in
"(slice " ^ spell "v" t ^ String.concat "" bounds ^ ")"
in
Loc.failk "check/unknown-function" loc
"there is no as-slice. slice takes the view, and what it is given \
says what the view is: over a Vec it borrows the storage the Vec \
owns, over an array or a string it looks at the value itself. \
Write %s" call
else if name = "slice-from-ptr" then
(* The name this form had before; code written against it lands here.
Said as the form to write, with the reader's arguments, and not as
a rename — a first-time reader has no old name to be told about. *)
let call =
match args with
| [ p; n ] ->
"(slice-from " ^ spell_arg "p" p ^ " " ^ spell_arg "n" n ^ ")"
| _ -> "(slice-from p n)"
in
Loc.failk "check/unknown-function" loc
"there is no slice-from-ptr. A slice from a pointer and a count of \
the elements behind it is slice-from. Write %s" call
else if no_such_rand name <> None then
(* A retired randomness name, which is a name and not a near miss:
"did you mean rand?" for [rand-f32] would be true and would not say
what to write, and the line in [no_such_rand] does. *)
Loc.failk "check/unknown-function" loc "%s"
(Option.get (no_such_rand name))
else if name = "len" then
(* [len] is an ordinary name and the count is [length], so this is the
one sentence a program that reached for the short word needs. It is
said rather than guessed at because the did-you-mean below cannot
reach it: [len] and [length] are three edits apart, and the net is
one. Asked here, after every table and after the shadowing guard at
the head of the dispatch, so a program that defines a [len] of its
own reaches its own — this is only ever the answer for a name that
nothing in the program has taken.
The reader's own argument is spelled back only when there is one of
it. [length] takes exactly one, so writing three of them out would
produce a suggestion that is refused for a second reason the moment
it is pasted — and a suggestion that does not compile is the bug
this spelling exists to avoid. [as-slice] above can write every
argument out because [slice] takes one, two or three; this cannot,
and the difference is the arity and not the style. *)
let call =
match args with
| [ a ] -> "(length " ^ spell_arg "v" a ^ ")"
| _ -> "(length v)"
in
Loc.failk "check/unknown-function" loc
"there is no len. The number of elements in an array, a slice, a \
string, a Vec or a Map is length. Write %s" call
else
(* The did-you-mean comes first, and for a capitalised head it is asked
of the *type* tables as well: [(Piont 1 2)] with [Point] declared is
a typo, and the generics sentence below would be a confident answer
about a feature nobody was reaching for. Only a capitalised head
consults the types — a lowercase name written where a value goes
was not a mistyped struct, which is [value_candidates]' whole
point. *)
let capitalised =
name <> "" && name.[0] = Char.uppercase_ascii name.[0]
&& name.[0] <> Char.lowercase_ascii name.[0]
in
let guess =
match nearest (!builtin_names @ value_candidates ctx) name with
| Some _ as m -> m
| None -> if capitalised then near_miss ctx.env name else None
in
(* A near miss that names a value rather than a function is still the
near miss, but [(m)] would be refused in its turn, so the sentence
says how that name is written instead. *)
let callable m =
let fn_ty = function
| Types.Fn _ | Types.CFn _ | Types.Dyn -> true
| _ -> false
in
match lookup ctx m with
| Some b -> fn_ty b.bty
| None ->
match Hashtbl.find_opt ctx.env.globals m with
| Some (ty, _) -> fn_ty ty
| None ->
not (List.mem m [ "true"; "false"; "nil"; "None";
"context/allocator"; "context/temp" ])
in
(* [(string b)], [(byte x)]: another language's name for a type, used
as the conversion, which Flan spells with its own type name. *)
(match foreign_spelling name with
| Some m ->
Loc.failk "check/unknown-function" loc
"unknown function %s — Flan spells it %s" name m
| None -> ());
match guess with
| Some m when not (callable m) ->
if args = [] then
Loc.failk "check/unknown-function" loc
"unknown function %s — did you mean %s? It is a value and not a \
function, so it is written without parentheses" name m
else
Loc.failk "check/unknown-function" loc
"unknown function %s. The nearest name, %s, is a value and not a \
function" name m
| Some m ->
Loc.failk "check/unknown-function" loc
"unknown function %s — did you mean %s?" name m
| None ->
if args <> [] && capitalised then
(* [(defonce p (Pair i32))]. A capitalised head with arguments and
no near miss anywhere is somebody reaching for a parameterised
type, which is what the type resolver says about [(Pair i32)]
when the same text lands in a type position. Before defonce took
either reading, that is the message this text got; it says the
same thing here so the answer does not depend on which side of
the fork the form fell down. *)
Loc.failk "check/unknown-function" loc
"unknown function %s. A capitalised name is a type, and no \
struct or generic struct %s is declared — a generic struct is \
one whose fields introduce $t, as in (defstruct %s [x $t])"
name name name
else Loc.failk "check/unknown-function" loc "unknown function %s" name
(* Does the program's own definition of this name take this call over?
Two questions, in this order, and the order is what makes the guard cheap
enough to be the first arm of the dispatch.
Is the name a builtin's at all. One lookup in [builtin_set], false for
every call to an ordinary function — which is most calls in most programs
— and the question that stops the second from being asked at all. Asking
it first also keeps the arms that are not calls — an enum cast, a cast to
a type variable, a machine-type cast — exactly where they were, since a
name that reaches one of those is not a builtin's either.
Is there a definition of it that reaches this call: a local of function
type, or a defn — ordinary or generic — written in this same file.
And is the definition visible here, which is asked of the two files: the
one the definition was written in and the one this call is written in. A
definition shadows the builtin through its own file and no further, which
is the same visibility a defn has everywhere else — the prelude is the
language's own source and means the builtin wherever it writes one, and an
imported package keeps the builtin it was written against no matter what
the program importing it decides to call [get].
The file and not the enclosing function's name. A package's functions are
qualified at the import ([rl/get]), so asking whether the owner's name
carries a slash answers correctly everywhere a call sits inside a
function — and wrongly in the one place a call does not: a package's
global initialiser, which is checked with no owner at all. An importer
defining [length] reached inside an imported
[(defonce sz i32 (length "abcd"))]
and changed what it computed. The files were never wrong about it.
What it costs is the REPL: an expression evaluated with no file behind it
is not the file the defn was written in, so it reaches the builtin. That
is the conservative direction, and C-c C-c — which sends the buffer's own
path — is not affected. *)
(* A [defn-] is its module's own. A use of one — a call, or the name taken
as a value — is refused unless it is written inside the module that
declares it. A directory package is every file in its directory, so the
test there is the two files' directories; a single file imported outright
is that file alone, which [Load] records by narrowing the flag to
[Private_to_file]. Realpath'd, because one side is usually the path the
importer was given on the command line and the other the one [Load]
resolved.
Code the package's own macro wrote counts as inside, wherever it was
expanded: SBCL's rule, where a macro's expansion refers to its package's
internal symbols freely. What the importer wrote itself does not, even when
it is an argument the macro passed through. [Expand.unmarshal] tells the two
apart already: a node the macro invented carries the call site's location
with the macro's name on it and no separate call site, and a form the author
wrote keeps its own position with the call recorded beside it. The macro's
package is its qualifier, which is the function's when both come from the
same module, because an import qualifies every name a directory declares
under the one alias it is read as.
Only a qualified name is asked about. An unqualified one belongs to the
program being built, and nothing outside a program can name it. *)
and private_ref ctx loc name =
let qualifier n =
match String.rindex_opt n '/' with
| Some i -> Some (String.sub n 0 i)
| None -> None
in
let written_by_own_macro () =
match loc.Loc.macro, loc.Loc.msite with
| Some m, None -> qualifier m <> None && qualifier m = qualifier name
| _ -> false
in
match Hashtbl.find_opt ctx.env.privates name with
| Some (at, scope) when String.contains name '/' ->
let real file = try Unix.realpath file with Unix.Unix_error _ -> file in
let inside =
match scope with
| Ast.Private_to_file -> String.equal (real at.Loc.file) (real loc.Loc.file)
| _ ->
String.equal (Filename.dirname (real at.Loc.file))
(Filename.dirname (real loc.Loc.file))
in
if not inside && not (written_by_own_macro ()) then begin
let where =
match scope with
| Ast.Private_to_file -> real at.Loc.file
| _ -> "the files in " ^ Filename.dirname (real at.Loc.file)
in
let name = written_name name in
Loc.failk "check/private" loc
~notes:[ Loc.note at (Printf.sprintf "%s is declared here" name) ]
"%s is private to its package: it is declared with defn-, so only %s \
can use it. Declaring it with defn instead makes it usable from here"
name where
end
| _ -> ()
(* One version of a name, as a line in a message: its parameters, named and
typed, spelled the way the file around [loc] writes a function. *)
and version_text env loc v =
let base = match version_of v with Some (b, _) -> b | None -> v in
let names, tys =
match Hashtbl.find_opt env.fns v, Hashtbl.find_opt env.gsigs v with
| Some (ps, _), _ ->
( (match Hashtbl.find_opt env.fparams v with
| Some fs -> List.map (fun (f : Ast.field) -> f.Ast.fname) fs
| None -> []),
ps )
| None, Some (_, ps, _) ->
( (match Hashtbl.find_opt env.generics v with
| Some fn -> List.map (fun (f : Ast.field) -> f.Ast.fname) fn.Ast.params
| None -> []),
ps )
| None, None -> ([], [])
in
let param i t =
let n = match List.nth_opt names i with Some n -> n | None -> "_" in
if fln_source loc then n ^ ": " ^ tyname loc t
else n ^ " " ^ tyname loc t
in
let ps = List.mapi param tys in
if fln_source loc then base ^ "(" ^ String.concat ", " ps ^ ")"
else "(" ^ base ^ " [" ^ String.concat " " ps ^ "])"
and shadows_builtin ctx loc name =
(* Where the definition was written, if this name has one. A generic is in
[generics] and nowhere near [fn_locs], so both tables are asked. *)
let declared_in () =
let name =
match Hashtbl.find_opt ctx.env.versions name with
| Some ((_, v) :: _) -> v
| _ -> name
in
match Hashtbl.find_opt ctx.env.fn_locs name with
| Some at -> Some at.Loc.file
| None ->
(match Hashtbl.find_opt ctx.env.generics name with
| Some fn -> Some fn.Ast.nloc.Loc.file
| None -> None)
in
(* A local of function type is lexical: it cannot be in scope anywhere but
the file that bound it, so there is no file to compare. *)
let local_fn () =
match lookup ctx name with
| Some b -> (match b.bty with Types.Fn _ -> true | _ -> false)
| None -> false
in
Hashtbl.mem builtin_set name
&& (local_fn ()
|| (match declared_in () with
| Some file -> String.equal file loc.Loc.file
| None -> false))
(* ── A call to a generic function ───────────────────────────────────────
The whole of instantiation, and it is at the call site because the call
site is the only place the concrete types exist. Odin does the same thing
in the same place: [check_expr.cpp]'s
[find_or_generate_polymorphic_procedure] runs from call checking, builds
the concrete proc type from the operands, scans the base entity's
[gen_procs] for an [are_types_identical] match, and generates a new
[Entity] only on a miss. *)
and generic_call ctx ~want loc name vars pats pret args =
if List.length args <> List.length pats then
fail loc "%s takes %d argument%s, given %d" (written_name name) (List.length pats)
(if List.length pats = 1 then "" else "s") (List.length args);
(* Arguments first, and with no expectation where the parameter's type still
mentions a variable — there is nothing to expect until the argument has
said what it is. So an untyped literal falls to its own default and
[(id 3)] instantiates at i32, which is the one place inference at a
generic call site is weaker than at a monomorphic one.
A variable already bound by an earlier argument is substituted back into
the parameters still to come, so [(sort-by (slice ns 0 4) (fn [a b] (< a
b)))] works: by the time the [fn] is reached, [(Fn [$t $t] bool)] has
become [(Fn [i32 i32] bool)] and the literal has the position it needs to
take its types from. Left to right, which is the order Odin's operands
are gathered in and the order [map2_lr] already guarantees. *)
let subst = ref [] in
(* Does the signature bind [v] anywhere *inside* a type — [[$t]],
[(Fn [$t $t] bool)], [(Vec $t)]? A bare [$t] parameter is a scalar the
join below may move; a variable reached through a constructor is bound
exactly, because a container's elements cannot be rewritten and a
function value's type is its own. One scan, answered per variable. *)
let rec mentions v (t : Types.t) =
match t with
| Types.Var u -> String.equal u v
| Types.Slice (_, e) | Types.Array (_, e) | Types.Ptr (_, e) | Types.Vec e
| Types.Option e -> mentions v e
| Types.LArray (u, e) -> String.equal u v || mentions v e
| Types.Map (k, w) -> mentions v k || mentions v w
| Types.Fn (ps, r) | Types.CFn (ps, r) ->
List.exists (mentions v) ps || mentions v r
| Types.Named k ->
(match Hashtbl.find_opt struct_apps k with
| Some (_, args) -> List.exists (mentions v) args
| None -> false)
| _ -> false
in
let bound_exactly v =
List.exists
(fun (p : Types.t) ->
match p with Types.Var _ -> false | t -> mentions v t)
pats
in
(* Pairs that met no join while the arguments were walked. They are not
refused on the spot because a *later* argument can still settle them:
(f u32-x i32-y i64-z) has no join at the second argument and a perfectly
good one — i64, which both widen into — at the third. Each entry is
re-asked against the final binding below, so acceptance cannot depend on
the order the arguments were written in. *)
let pending = ref [] in
let targs =
map2_lr
(fun pat a ->
let p = subst_ty !subst pat in
(* Which variable, if any, this parameter *is* — written as a bare
[$t] and already bound by an argument to the left. That is the one
shape implicit widening can reach, because [Types.widens_to] admits
only numeric scalars: a variable bound inside [[$t]] or
[(Fn [$t $t] bool)] leaves a parameter no widening applies to, so
the [sort-by] path below is untouched by construction. *)
let bound_scalar =
match pat with
| Types.Var v when (not (open_ty p)) && Types.is_numeric p ->
Some v
| _ -> None
in
(* An untyped constant has no type of its own to keep, so it still
takes the variable's — [(clamp-to y 0 10)] with [y] an i64 means
three i64s and there is no conversion anywhere in it. Everything
else is checked on its own terms. *)
let untyped_literal =
match a.Ast.e with
| Ast.Int _ | Ast.UInt _ | Ast.Float _ | Ast.Byte _ -> true
| _ -> false
in
(* A bare [$t] an earlier argument bound to a slice or a pointer:
this argument may differ from it only in const, and the two meet
at the read-only one ([Types.const_join]), whichever came first.
So it is checked on its own terms rather than against the
binding. *)
let bound_view =
match pat, p with
| Types.Var v, (Types.Slice _ | Types.Ptr _)
when not (open_ty p || bound_exactly v) -> Some v
| _ -> None
in
(* A typed .fln lambda, [(the (Fn [i32] i32) (fn ...))], at a
[CFn($t) -> $t] parameter: the literal is a CFn at its own types,
and those bind [$t] below as any argument's type would. *)
let typed_cfn =
match p, a.Ast.e with
| Types.CFn _, Ast.The (t, { Ast.e = Ast.Fn _; _ }) ->
(match resolve ctx.env t with
| Types.Fn (ps, r) when fits_shape p (Types.CFn (ps, r)) ->
Some (Types.CFn (ps, r))
| _ -> None
| exception Loc.Error _ -> None)
| _ -> None
in
let a =
if typed_cfn <> None then check ctx ~want:(Option.get typed_cfn) a
else if open_ty p || bound_view <> None then check ctx a
else if bound_scalar <> None && not untyped_literal then
(* On its own terms first. A form that has no type without a want
— [(zeroed)] is the one that matters — refuses here and is
checked against the parameter as it always was; the trial
leaves no trace of the attempt. *)
(match trial ctx (fun () -> check ctx a) with
| Ok r -> r
| Error _ -> check ctx ~want:p a)
else check ctx ~want:p a
in
(* **Mixed widths at one variable join at the wider type.** The rule
used to refuse the pair both ways — TODO.org, "abs is one generic,
and a bound joins to the wider type", records the join as the
coherent alternative
and refusing as the direction that could be walked back. It was
walked back on 2026-09-20, by the author: a numeric argument at a
variable an earlier argument already bound resolves the variable
to whichever of the pair the other widens into, value-preserving
widening only, so [(is-eq2 (i8 3) (i64 3))] and its reverse are one
copy at i64. A pair with no join — u64 against i64 — is still
refused: there is no type that holds every value of both, and
inventing one would be picking a type neither argument was
written at.
Only where the variable is bound by bare scalars. A variable the
signature also reaches through a container is bound exactly —
a slice's elements cannot be rewritten to a wider width — so
those keep the refusal, in their own words. And only where the
pair is one widening has an opinion about: a string where $t was
bound to i64 is an ordinary mismatch and gets the ordinary
refusal below. *)
let handled =
match bound_view, Types.const_join p a.Tast.ty with
| Some v, Some j ->
subst := (v, j) :: List.remove_assoc v !subst;
true
| _ ->
match bound_scalar with
| Some v
when (not (Types.equal p a.Tast.ty))
&& Types.is_numeric a.Tast.ty ->
(match Types.join p a.Tast.ty with
| Some j when Types.equal j p ->
(* This argument widens into the binding; the wrap happens
with the others, once the binding is final. *)
true
| Some j when not (bound_exactly v) ->
subst := (v, j) :: List.remove_assoc v !subst;
true
| Some _ ->
Loc.failk "check/tyvar-no-widening" a.Tast.loc
"%s's $%s was bound to %s by an earlier argument, and this \
one is %s. The signature also binds $%s inside a \
container or function type, which binds its element \
exactly — the pair cannot join at the wider type there. \
Write the conversion — (%s x) — or pass the arguments at \
one type"
(written_name name) v (tyname loc p) (tyname loc a.Tast.ty) v
(tyname loc p)
| None ->
pending := (v, p, a.Tast.ty, a.Tast.loc) :: !pending;
true)
| _ -> false
in
(* [~widen]: this is the top of an argument's type, which is the one
place a widening thunk can be built around it. See [bind_ty]. *)
if (not handled) && not (bind_ty ~widen:true subst p a.Tast.ty) then
fail a.Tast.loc "%s expects %s here, found %s%s" (written_name name)
(tyname loc p) (tyname loc a.Tast.ty)
(match p, a.Tast.ty with
| Types.Slice (Types.Mut, _), Types.Slice (Types.Const, e) ->
Printf.sprintf
" — %s takes a slice it may write through, and a %s can \
only be read%s"
(written_name name) (tyname loc a.Tast.ty)
(match const_copy ctx.env e with
| Some c ->
Printf.sprintf ". %s copies v into one that can be written" c
| None -> "")
| _ -> "");
a)
pats args
in
(* Every variable has to be determined by an argument. A return-only
variable has nothing to bind it — there is no explicit instantiation
syntax by design (plan.org) — so it is refused here, where the signature
can be named, rather than producing a copy with a hole in it. *)
List.iter
(fun v ->
if not (List.mem_assoc v !subst) then
fail loc
"%s's type variable $%s is not determined by any argument" (written_name name) v)
vars;
(* The pairs that met no join, re-asked now that every argument has spoken.
A later, wider argument dissolves one — u32 and i32 both widen into an
i64 that arrived third — and one still standing is the real refusal:
these two widths meet at no type. *)
List.iter
(fun (v, t1, t2, ploc) ->
let final = List.assoc v !subst in
let fits t =
Types.equal t final || Types.widens_to ~from:t ~into:final
in
if not (fits t1 && fits t2) then
Loc.failk "check/tyvar-no-join" ploc
"this call binds %s's $%s to both %s and %s, and neither holds \
every value of the other. Write the conversion you mean at one \
of the arguments, or pass them at one type"
(written_name name) v (tyname loc t1) (tyname loc t2))
!pending;
(* The binding is final; the arguments it out-widened catch up. Only a bare
[$t] parameter can be here — [bound_exactly] kept every container-bound
variable at one exact type — and the cast is the same node the written
conversion would have built. *)
let targs =
map2_lr
(fun (pat : Types.t) a ->
match pat with
| Types.Var v ->
(match List.assoc_opt v !subst with
| Some f
when (not (Types.equal f a.Tast.ty))
&& Types.is_numeric a.Tast.ty
&& Types.widens_to ~from:a.Tast.ty ~into:f ->
widen a.Tast.loc f a
| Some f when Types.const_widens ~from:a.Tast.ty ~into:f ->
{ a with Tast.ty = f }
| _ -> a)
(* And the other widening, for the same reason and at the same
moment: a [CFn] argument against an [(Fn [$t] $t)] parameter.
A parameter that still mentioned a variable was checked with no
expectation at all — there was nothing to expect until the
argument had spoken — so [expect] never saw the pair and never
built the value the instance's signature needs. It is built here,
once the binding is final, exactly as the numeric catch-up above
is.
A *concrete* [Fn] parameter never reaches this: it was checked
with a want in the first pass and [expect] widened it there.
The arm is total over the pair, and that is the point of writing
it as an [if] rather than as a guard. [bind_ty]'s fallthrough is
[Types.fits], which admits [Never] where the instance's signature
wants a type — so a binding can succeed over a pair these two
words cannot bridge, and a fallthrough of "hand the argument over
unchanged" would pass one word where the instance declares two.
Every [CFn] arriving at an [Fn] parameter either gets its thunk
here or gets the refusal, which is the answer [expect] gives a
call with no type variables in it. *)
| _ ->
(match subst_ty !subst pat, a.Tast.ty with
(* A plain value at a [$t?] parameter, which [bind_ty] bound
through the Option: wrapped now that $t is known (decision
138). *)
| Types.Option _ as o, at
when (match at with Types.Option _ | Types.Dyn | Types.Never -> false | _ -> true) ->
expect ctx a.Tast.loc ~want:(Some o) a
| Types.Fn (ps, r), Types.CFn (ps', r') ->
if Types.equal (Types.Fn (ps, r)) (Types.Fn (ps', r')) then
mk a.Tast.loc (Types.Fn (ps, r))
(Tast.Thicken (thick_thunk ctx.env a.Tast.loc ps r, a))
else
fail a.Tast.loc "%s expects %s here, found %s" (written_name name)
(tyname loc (Types.Fn (ps, r)))
(tyname loc a.Tast.ty)
| _ -> a))
pats targs
in
(* **A type variable is not instantiated at dyn.** Nothing stopped it before:
[dyn] is an ordinary case of [Types.t], so it substituted like any other
type and a copy was generated at it. The copy then reached whatever the
body did with the value, and the dyn answers are not all there — [(Option
dyn)] has no descriptor the collector can find, [slice] over a
[(Vec dyn)] refuses. So the refusal existed, it just arrived from inside
the generic's own source: [(or-else (Some d) e)] over two dyns is reported
against [<prelude>:385], a line the caller did not write and cannot act
on. Every one of those is this refusal arriving late and in the wrong
place.
Refusing at the binding is also the honest statement of the split. Two
models answer "one body, many types" here and they are not rivals: this
one instantiates at compile time and keeps the types, and [defgeneric] /
[defmulti] dispatch at run time on a value that carries its own. A dyn
argument is asking the second question of the first machinery. The
message says so and names the other spelling.
Bounded variables were already refused — [pred_holds] says no to dyn for
all four predicates — so this closes the unbounded half, which is exactly
the half that reached the prelude-source diagnostics. A variable that
*does* carry a clause is left to that refusal deliberately: it names the
predicate the signature actually wrote down, which is the more specific
answer of the two, and the generic cast's pin depends on it. *)
let clause_on v =
match Hashtbl.find_opt ctx.env.generics name with
| None -> false
| Some gfn ->
List.exists
(fun (p : Ast.pred) -> String.equal p.Ast.pvar v) gfn.Ast.fwhere
in
List.iter
(fun (v, t) ->
if reaches_dyn t && not (clause_on v) then
Loc.failk "check/tyvar-at-dyn" loc
"this call would instantiate %s at $%s = %s, and a type variable \
is not instantiated at dyn. Write the type the value has, or use \
a defgeneric with a defmethod per class"
(written_name name) v (tyname loc t))
!subst;
let cparams = List.map (subst_ty !subst) pats in
let cret = subst_ty !subst pret in
List.iter (realise ctx.env loc) (cret :: cparams);
if List.exists open_ty cparams || open_ty cret then begin
(* One generic function calling another at its *own* variable, seen from
the abstract pass over the caller's body — [sort-by] calling [swap]
at [t]. There is no copy to make yet: [t] is not a type. The node is
built so the call still type-checks and is thrown away with the rest of
the abstract pass; the real copy is generated when the caller is
instantiated and the same call site resolves [t] to a concrete type.
But the callee's [where] clause is answerable here, and has to be. The
whole promise of the abstract pass is that a generic's refusals arrive
at its definition; if the predicate were left to the instantiation,
[(defn f [x $t] () (sort [x]))] would be accepted at its definition
and refused at whichever call site first instantiated it — a refusal in
code the caller did not write, which is the thing the pass exists to
avoid. So the caller has to declare at least what the callee asks for,
and [pred_entails] means [is-ordered] covers a callee wanting
[is-equal] without anyone writing both. *)
(match Hashtbl.find_opt ctx.env.generics name with
| None -> ()
| Some gfn ->
List.iter
(fun (p : Ast.pred) ->
match List.assoc_opt p.Ast.pvar !subst with
| Some (Types.Var v) when not (declares ctx.env.tvpreds v p.Ast.pname) ->
Loc.failk "check/predicate-not-carried" loc
"%s is written %s, and this call passes the \
type variable $%s, which nothing here declares %s. Add \
%s to this function's own clause"
(written_name name) (where_text loc p.Ast.pname ("$" ^ p.Ast.pvar)) v
(pred_word p.Ast.pname) (where_text loc p.Ast.pname ("$" ^ v))
| Some t when not (open_ty t) && not (pred_holds p.Ast.pname t) ->
Loc.failk "check/predicate-unsatisfied" loc
"%s is written %s, and this call passes %s, \
which is not %s"
(written_name name) (where_text loc p.Ast.pname ("$" ^ p.Ast.pvar)) (tyname loc t)
(pred_word p.Ast.pname)
| _ -> ())
gfn.Ast.fwhere);
expect ctx loc ~want (mk loc cret (Tast.Call (name, targs)))
end
else
let sym = instantiate ctx.env loc name vars !subst cparams cret in
expect ctx loc ~want (mk loc cret (Tast.Call (sym, targs)))
(* Cache or generate, Odin's loop. The key is the whole concrete signature
compared pairwise with [Types.equal] — [are_types_identical] — so calling
at the same type twice makes one copy. *)
and instantiate env loc gname vars subst cparams cret =
let cache =
match Hashtbl.find_opt env.insts gname with
| Some r -> r
| None -> let r = ref [] in jreplace env.insts gname r; r
in
let same (ps, r, _) =
List.length ps = List.length cparams
&& List.for_all2 Types.equal ps cparams && Types.equal r cret
in
match List.find_opt same !cache with
| Some (_, _, sym) -> sym
| None ->
let sym =
gname ^ "-"
^ String.concat "-" (List.map (fun v -> mangle_ty (List.assoc v subst)) vars)
in
(* Each instantiation checks the concrete types answer the [where] clause.
This is the half of the feature that only exists per copy: the abstract
pass took the predicates on trust, and here is where the trust is
settled, at the call site that asked, naming it.
Before the name-collision check, on purpose. The prelude keeps a
per-width family beside a generic where the generic's bound refuses
some widths — [abs] under [is-integer] beside the declared [abs-f32] and
[abs-f64] — so a float caller of [abs] computes the sym [abs-f64], and
"abs-f64 is already defined, rename one of them" is the wrong sentence
for what went wrong: the bound refused the type, and that is the
message with the fix in it. *)
let fn = Hashtbl.find env.generics gname in
List.iter
(fun (p : Ast.pred) ->
match List.assoc_opt p.Ast.pvar subst with
| None -> ()
| Some t ->
if not (pred_holds p.Ast.pname t) then
Loc.failk "check/predicate-unsatisfied" loc
"this call instantiates %s at $%s = %s, and %s is not %s. %s is \
written %s — pass a type the predicate admits"
(written_name gname) p.Ast.pvar (tyname loc t) (tyname loc t)
(pred_word p.Ast.pname) (written_name gname)
(where_text loc p.Ast.pname ("$" ^ p.Ast.pvar)))
fn.Ast.fwhere;
if Hashtbl.mem env.fns sym then
fail loc
"%s at these types is called %s, and %s is already defined — rename \
one" gname sym sym;
runaway env loc gname cparams;
(* The entry goes in *before* the body is checked, which is what makes a
recursive generic function terminate: the call to itself at the same
types finds this and does not generate a second copy. *)
jset cache ((cparams, cret, sym) :: !cache);
jreplace env.fns sym (cparams, cret);
let saved_subst = env.subst and saved_vars = env.tyvars
and saved_preds = env.tvpreds and saved_chain = env.chain in
(* Inside the copy there are no variables left: [resolve_name] answers
[t] with the concrete type, so every node the body produces is as
concrete as one written out by hand. The [where] clause goes out of
scope with them — there is nothing abstract left for it to permit, and
every operator is answered by the concrete type it now has. *)
let saved_lens = env.lenvars and saved_ph = env.len_placeholder in
env.subst <- List.map (fun v -> (v, List.assoc v subst)) vars;
env.tyvars <- [];
env.lenvars <- [];
env.len_placeholder <- false;
env.tvpreds <- [];
env.chain <- env.chain @ [ (gname, cparams, loc) ];
let restore () =
env.subst <- saved_subst; env.tyvars <- saved_vars;
env.tvpreds <- saved_preds; env.chain <- saved_chain;
env.lenvars <- saved_lens; env.len_placeholder <- saved_ph
in
if Hashtbl.mem env.refused_generics gname then begin
restore ();
sym
end else
let tfn =
(* Without recovery: a copy that does not check is refused whole, at
the call that asked for it, as it always was. *)
match speculate env (fun () -> !check_fn_ref env { fn with Ast.name = sym }) with
| tfn -> restore (); tfn
| exception e ->
restore ();
(* The refusal is inside the generic's source, which says nothing about
which call asked for this copy; the note names it. Nested copies
each add their own, so the notes walk the chain back to the call
the programmer wrote. *)
let at () =
String.concat ", "
(List.map
(fun v -> Printf.sprintf "$%s = %s" v
(tyname loc (List.assoc v subst)))
vars)
in
let in_prelude (l : Loc.t) = String.equal l.Loc.file Prelude.file in
let e =
match e with
(* A prelude generic's body is source nobody at this call wrote, and
an editor cannot jump to it. The refusal moves to the call that
asked for the copy, and the prelude's line comes along as a
note. *)
| Loc.Error d when in_prelude d.Loc.dloc && not (in_prelude loc) ->
(* Only the reason comes along. The rest of the body's message is
a fix to the body, which the caller cannot make. *)
let reason =
let cut sep m =
match find_sub m sep with
| Some i -> String.sub m 0 i
| None -> m
in
cut ". " (cut " — " d.Loc.dmsg)
in
Loc.Error
(Loc.sort_notes
{ d with
Loc.dloc = loc;
dmsg =
Printf.sprintf
"%s cannot be made at %s: its body in the prelude does \
not compile at that type. Pass a value of a type it \
takes, or write the operation here"
gname (at ());
notes =
d.Loc.notes
@ [ Loc.note d.Loc.dloc ("in the prelude, " ^ reason) ];
expansion = None })
| Loc.Error d when d.Loc.dloc <> loc ->
Loc.Error
(Loc.sort_notes
{ d with
Loc.notes =
d.Loc.notes
@ [ Loc.note loc
(Printf.sprintf "%s is instantiated at %s here"
gname (at ())) ] })
| e -> e
in
(* A copy whose body did not check is not a copy. Both entries go back
out, so a second call at the same types is the same refusal again
rather than a cache hit on a function that does not exist. *)
jset cache (List.filter (fun (_, _, s) -> s <> sym) !cache);
jremove env.fns sym;
raise e
in
env.instances <- tfn :: env.instances;
sym
and is_cast name =
Types.ikind_of_name name <> None || Types.fkind_of_name name <> None
and byte_slice ctx (a : Ast.expr) =
check ctx ~want:(Types.Slice (Types.Const, Types.Int Types.U8)) a
and numeric_want want =
match want with Some (Types.Int _ | Types.Float _) -> want | _ -> None
(* Both operands of a binary operator have one type, so one side has to decide
it. Check the side that carries the most information first: a non-literal
over a literal, and a float literal over an integer one, since an integer
constant converts to a float and not back.
Widening (TODO.org, "Implicit numeric widening is legal; narrowing stays a
hard error") does not retire that rule, it finishes it.
Three things decide, in this order:
1. An expectation, if the site has one, and it reaches *both* operands. So
(defn f [] i64 (+ a b)) over two i32s widens each operand and adds at
i64, rather than adding at i32 and widening the sum. That is the better
of the two readings and it costs nothing to prefer it, because no program
that compiled before can reach it — the pair used to be a refusal.
2. A literal, exactly as before: it takes its width from the other operand,
so (+ x 1) over a u64 x is still u64 arithmetic and (let [h fnv-offset])
over a u64 defconst still means what it meant. [needs_want] is what marks
the forms this applies to and it is untouched.
3. Otherwise the *wider* side decides — [Types.join], whichever operand the
other widens into, with a [Cast] put on the narrower one. (+ i32-var
i64-var) is an i64 add. Equal width across signedness has no join, by
construction: neither i32 nor u32 widens into the other, and the refusal
says which cast to write.
The mechanism for 3 is a *trial*: ask y for [a]'s type, and if that refusal
is the one widening was invented for, look again the other way round. Two
things have to be true for a trial to be honest, and both are below.
[trial] is the first. Checking is not a function of its argument — it
allocates frame slots and it opens scopes — so a check that is abandoned
has to leave no trace, and [scoped] cannot help: it restores the scope on
the way *out*, which an exception does not take. Without this a binding
from the abandoned pass outlives it, which is visible as a name that should
be unknown resolving anyway, and worse, as a shadow: the inner binding of
(let [t ...] ... (let [t ...] t) ... t) survives into the outer t's slot
with nothing ever stored in it. That is an uninitialised read, produced by
a program the compiler accepted.
[literal_at_want] is the second. A trial that refused because a *literal*
could not be built at the wanted type is not a pair of types that failed to
meet — the literal had no type of its own to bring — so looking again would
answer with the literal's default and quietly move (+ u8-thing 300) to i32.
Rule 2 above is not a description of the old language kept for continuity;
it is what the author decided, and the kind is how the trial obeys it. *)
and trial ctx f =
(* Everything a check writes into the context, put back if the check is
abandoned — and it is *everything* on purpose, not a chosen subset.
Picking the fields that looked like they mattered was tried twice and was
wrong twice. [scope] and the slot fields were the first round, found as an
uninitialised read. The second round was worse, because the symptom was
the other way up: a form that opens a window and closes it on the way out
— [check_frames] setting [in_frames], [loop] pushing onto [loops] — leaves
that window *open* when a trial inside it is abandoned, and then refuses
a perfectly good program.
(println (+ i32-x (handler-bind [] i64-y)))
(return 0)
compiled before this lane and was refused after it, with "return is not
allowed inside handler-bind yet" pointing at a line with no handler-bind
anywhere near it. A false refusal is not a lesser bug than a false accept;
it is just quieter about being one.
So the rule here is not judgement, it is the whole record. Three fields
would have self-healed anyway — [defer_ok] and [tail] are read and cleared
on entry to [check], [outer_what] is never written after the context is
built — and they are restored regardless, because "this one cannot
currently leak" is exactly the reasoning that produced two rounds of
leaks. The destructuring below is closed and warning 9 is turned on for
it, so a new field on [ctx] stops this function compiling until somebody
decides about it, rather than joining the list of things nobody noticed.
[env] is put back the same way, whole, by [snapshot_env]: a function
lifted, a generic copy made and cached, a struct registered — all of it
goes together, since keeping one of a pair without the other is how a
copy ends up calling a lambda that was never emitted.
Only [Loc.Error] is caught. A timeout or a stack overflow is not a
refusal to reconsider, and silently continuing past one would turn a
resource failure into a wrong answer. *)
let[@warning "+9"] { env = _; ret = _; lits = _; slots; slot_tys; slot_names; as_slots; scope;
defers; defer_slot; defer_ok; defer_block; outer = _;
outer_what; caught; place_ok; envslot; parent = _;
in_frames; loops; tail; used; kept; in_defer;
owner = _ } = ctx in
let undo, keep = snapshot_env ctx.env in
match speculate ctx.env f with
| r -> keep (); Ok r
| exception Loc.Error d ->
undo ();
ctx.slots <- slots; ctx.slot_tys <- slot_tys;
ctx.slot_names <- slot_names; ctx.as_slots <- as_slots; ctx.scope <- scope;
ctx.defers <- defers; ctx.defer_slot <- defer_slot;
ctx.defer_ok <- defer_ok; ctx.defer_block <- defer_block;
ctx.outer_what <- outer_what; ctx.in_frames <- in_frames;
ctx.caught <- caught; ctx.place_ok <- place_ok; ctx.envslot <- envslot;
ctx.loops <- loops; ctx.tail <- tail; ctx.used <- used; ctx.kept <- kept;
ctx.in_defer <- in_defer;
Error d
| exception e -> keep (); raise e
(* Whether the trial's refusal is one worth reconsidering. A literal that did
not fit is not, and neither is a refusal a program cannot make any use of
having a second opinion on. *)
and reconsiderable (d : Loc.diag) = not (String.equal d.Loc.kind literal_at_want)
(* [a] was checked, y refused [a]'s type, and [b] is y on its own terms — held
by the caller when it has one, taken here when it does not. If the pair has
a join it can only be [b]'s type (had it been [a]'s, the trial would have
passed), so [a] is the operand that moves. *)
and join_widen (a : Tast.expr) (b : Tast.expr) =
match Types.join a.Tast.ty b.Tast.ty with
| Some t when not (Types.equal t a.Tast.ty) ->
Some (widen a.Tast.loc t a, widen b.Tast.loc t b)
| _ -> None
and join_pair ctx (a : Tast.expr) (y : Ast.expr) (d : Loc.diag) =
(* Check y on its own terms to find out whether it was simply the wider
operand. This trial is guarded too: if y cannot check without an
expectation at all — [None], [(zeroed)] — the original refusal is the one
reported, so no form loses the expectation it used to get. *)
match trial ctx (fun () -> check ctx y) with
| Error _ -> raise (Loc.Error d)
| Ok b ->
(match join_widen a b with
| Some pair -> pair
| None -> raise (Loc.Error d))
(* [y] checked at [w] in a trial, and a refusal kept ([arm_failed]) so the
same operand asked again at the same type, in the same scope, is refused
without being walked: a chain of these nested in their second operands is
asked once per level above it. *)
and trial_at ctx (y : Ast.expr) (w : Types.t) =
match
List.find_opt
(fun (n, (sc, r), w', _) ->
n == y && r == ctx.ret && Types.equal w' w && same_scope sc ctx.scope)
(Hashtbl.find_all arm_failed y.Ast.loc)
with
| Some (_, _, _, d) -> Error d
| None ->
(match trial ctx (fun () -> check ctx ~want:w y) with
| Ok b -> Ok b
| Error d ->
if !lit_recording = 0 then Hashtbl.add arm_failed y.Ast.loc (y, (ctx.scope, ctx.ret), w, d);
Error d)
and binary ctx ?(dyn_ok = false) ?(join = true) ?(char_ok = false) name loc ~want args =
match args with
| [ x; y ] ->
lit_operands ctx x y (fun () -> binary_pair ctx ~dyn_ok ~join ~char_ok loc ~want x y)
| _ -> fail loc "%s takes two arguments" name
(* An operator's two operands, while literal locals' uses are recorded: one
beside a literal local says the type it meets it at ([Hint]), and two of
them are merged. Checked exactly as ever, so a round whose guesses hold is
the program. *)
and lit_operands ctx (x : Ast.expr) (y : Ast.expr) f =
let key (e : Ast.expr) =
match e.Ast.e with Ast.Var n -> lit_recorded ctx n | _ -> None
in
match ctx.lits, key x, key y with
| Some s, kx, ky when kx <> None || ky <> None ->
let float_lit (e : Ast.expr) = lit_kind e = Some `Float in
(* A char local beside an integer literal stays a char: the pair is char
arithmetic, or a comparison the checker refuses (decision 131). Only
typed code that wants a particular integer makes it a number. *)
let lit_add s k ((_, _, _) as c) (other : Ast.expr) =
match lit_kind k, lit_kind other with
| Some `Char, Some `Int -> ()
| _ -> lit_add s k c
in
(* Before the check, which refuses a float literal beside an integer
guess. *)
(match kx, ky with
| Some k, _ when float_lit y -> lit_add s k (Hint, Types.Float (float_default ()), y.Ast.loc) y
| _, Some k when float_lit x -> lit_add s k (Hint, Types.Float (float_default ()), x.Ast.loc) x
| _ -> ());
let saved = !lit_operand_locs in
lit_operand_locs := x.Ast.loc :: y.Ast.loc :: saved;
let a, b =
try Fun.protect ~finally:(fun () -> lit_operand_locs := saved) f
with Loc.Error _ as ex ->
(* Refused at the guess, as (+ acc x) is over an i32 guess and an
i64 x: what the other operand is on its own terms is the use. *)
let own (k, (other : Ast.expr)) =
match trial ctx (fun () -> check ctx other) with
| Ok e -> lit_add s k (Hint, e.Tast.ty, other.Ast.loc) other
| Error _ -> ()
in
(match kx, ky with
| Some k, None -> own (k, y)
| None, Some k -> own (k, x)
| _ -> ());
raise ex
in
(match kx, ky with
| Some k1, Some k2 -> lit_union s k1 k2
| Some k, None -> lit_add s k (Hint, b.Tast.ty, y.Ast.loc) y
| None, Some k -> lit_add s k (Hint, a.Tast.ty, x.Ast.loc) x
| None, None -> ());
a, b
| _ -> f ()
and binary_pair ctx ~dyn_ok ~join ~char_ok loc ~want (x : Ast.expr) (y : Ast.expr) =
(* A char defconst no local shadows reads as the literal it names. *)
let is_literal (e : Ast.expr) =
is_literal e
|| (match e.Ast.e with
| Ast.Var n ->
Hashtbl.mem char_consts n && lookup ctx n = None
&& peek_outer ctx n = None
| _ -> false)
in
let y_decides =
(is_literal x && not (is_literal y))
|| (match x.Ast.e, y.Ast.e with
| (Ast.Int _ | Ast.UInt _ | Ast.Byte _), Ast.Float _ -> true
| _ -> false)
in
(* A form that cannot be checked without being told what is wanted. A
literal takes its width from the expectation, and a keyword has no
meaning at all without one — [:lo] resolves against the enum the site
expects and there is no keyword type to fall back on. Everything else
checks on its own terms. *)
let needs_want (f : Ast.expr) =
is_literal f || (match f.Ast.e with Ast.Kw _ -> true | _ -> false)
in
(* A dyn the want opened is put back: beside a dyn a typed operand gives
dyn (rule 117), so the pair is the dyn runtime's and only its answer
is opened at the want. Opening the operand first made (+ p 1 1) at an
i32 want an i32 add that wraps, where (+ 1 1 p) was a dyn add whose
answer traps at the i32. *)
let reopen (v : Tast.expr) =
if not dyn_ok then v
else match opened_dyn ~box:(to_dyn ctx) v with Some d -> d | None -> v
in
if y_decides then begin
let b = reopen (check ctx ?want y) in
(* An integer literal before a char, under [+] or [-], is an integer:
the pair is char arithmetic ([char_step]). *)
let a =
match x.Ast.e with
| (Ast.Int _ | Ast.UInt _) when char_ok && b.Tast.ty = Types.Char ->
check ctx x
| _ -> check ctx ~want:b.Tast.ty x
in
a, b
end
(* [dyn_ok] is set by the operators that have a dyn lowering, and it exists
to stop the second operand being coerced to the first's type before
anybody has asked whether the pair is a dyn one.
Without it [(+ n x)] over an [i64] n and a dyn x threads [i64] into the
second check, [expect] does what an annotation site asked for and
unboxes, and the result is a *machine* add of a value the runtime was
never asked about: the program traps on a float instead of promoting,
and nothing in the source says why. The mirror image [(+ x n)] boxed
correctly, so the bug was visible only in one operand order.
Both sides are checked on their own terms here and the caller decides.
That is safe exactly when neither operand needs an expectation, which is
what [needs_want] settles — a literal still gets the first operand's
type, so [(+ x 1)] over a dyn x goes on building an i64 one. *)
else if dyn_ok && not (needs_want y) then begin
let a = reopen (check ctx ?want x) in
(* y at [a]'s type first, and on its own terms only if that is
refused: checking it both ways every time made a chain of these
nested in their second operands twice as slow per level. A dyn
opened at [a]'s type is seen for what it was. *)
let at_a =
if a.Tast.ty = Types.Dyn then None
else
Some (trial_at ctx y a.Tast.ty)
in
match at_a with
| Some (Ok b') ->
(match opened_dyn ~box:(to_dyn ctx) b' with Some box -> a, box | None -> a, b')
| _ ->
let b = check ctx y in
(* Nothing dyn about this pair after all, so it is put back the way the
typed path built it. Re-checking only when the types actually differ
keeps the common case to one check of each operand. *)
if a.Tast.ty = Types.Dyn || b.Tast.ty = Types.Dyn
|| Types.equal a.Tast.ty b.Tast.ty
then a, b
(* Asking y for [a]'s type stays the first thing tried, and not only for
continuity: y was checked above with no expectation at all, and an
expectation is information. A sum of two products handed to an f32
function is the case — test/programs/math.flan does it — because every
literal inside those products defaults to f64 on its own terms, so
reading the join off the two unexpected halves would answer f64 for a
form the site asked to be f32. The re-check builds them at f32 as it
always did.
[b] is only used when that re-check refuses, which is the direction
[expect] cannot serve: a is the narrower operand and it is the one
that has to move. Nothing is checked a third time — the own-terms [b]
already in hand is the answer. *)
else
(match
match at_a with
| Some (Error d) -> Error d
| _ -> trial_at ctx y a.Tast.ty
with
| Ok b' -> a, b'
| Error d ->
(match
if join && reconsiderable d then join_widen a b else None
with
| Some pair -> pair
| None -> raise (Loc.Error d)))
end
else begin
let a = reopen (check ctx ?want x) in
match trial_at ctx y a.Tast.ty with
| Ok b -> a, b
| Error d ->
(* The join moves [a] to something wider, and [a] already has the
type asked of the whole: that can only be refused, so y is not
walked on its own terms to find it out. *)
let doomed =
match want with Some w -> Types.equal a.Tast.ty w | None -> false
in
if join && reconsiderable d && not doomed then join_pair ctx a y d
else
(* Doomed: said the way the join would have been refused — the
pair at the wider type, at this form — when y's own refusal
names the wider type it found. *)
match want, (if doomed then Found.find_opt mismatch_found d else None) with
| Some w, Some found
when String.equal d.Loc.dloc.Loc.file y.Ast.loc.Loc.file
&& d.Loc.dloc = y.Ast.loc ->
(match Types.join w found with
| Some j when not (Types.equal j w) ->
ignore (expect ctx loc ~want:(Some w) (mk loc j Tast.Unit));
raise (Loc.Error d)
| _ -> raise (Loc.Error d))
| _ -> raise (Loc.Error d)
end
(* ── The builtins, said out loud ───────────────────────────────────────
A name, a signature and one line, for every name [named_call] and [var]
answer without the program having written it. The editor's C-c C-v used to
say "the running program defines no arena-new", which was true and useless:
a builtin is in the compiler, so it is in no program's symbol table and
[Dev.defs] had nothing to hand over. This is what it hands over instead.
It lives here rather than in dev.ml because it describes the arms above it,
and a table in another file drifts from them silently. [test_flan]'s
[builtin_table] reads this file and the arms and fails on either one having
a name the other does not, so the drift is a failing build rather than a
name that answers nothing.
The signature follows [Dev.signature_of_fn]'s shape — [name [params] ret] —
so eldoc reads a builtin the way it reads a defn. Where an arm does not
have one shape the signature says what is true rather than inventing one:
[?] marks an argument that may be left out, [|] separates the types an arm
really does accept, and the predicate names are the compiler's own
([is-numeric], [is-ordered], [is-equal] — check.ml's [where] evaluator), because
a generic's author already writes them. Three names have no shape at all
and carry a bare name instead of a bracket list; their line says why.
One line each, because this is read in an echo area and a help buffer. The
lines are the arms' own reasons, cut down — where an arm argues for a
decision above itself, the sentence a person needs at the call site is the
conclusion, not the argument. *)
let builtins : (string * string * string) list =
[ (* arithmetic and comparison *)
("+", "+ [is-numeric ...] is-numeric",
"Sum, folded left over two or more operands. Two operands of different \
numeric types meet at the wider one when that cannot lose — i32 and i64 \
add at i64 — and i32 with u32 has no such type and is refused.");
("-", "- [is-numeric ...] is-numeric",
"Difference, folded left: (- a b c) is ((a - b) - c). With one operand, \
its negation: (- x).");
("*", "* [is-numeric ...] is-numeric",
"Product, folded left over two or more operands of one numeric type.");
("/", "/ [is-numeric ...] is-numeric",
"Quotient, folded left. Integer division truncates toward zero.");
("%", "% [is-numeric is-numeric] is-numeric",
"Remainder, and it stays at two operands: (% a b c) would mean \
(% (% a b) c), which is a thing nobody writes on purpose.");
("=", "= [is-equal ...] bool",
"Equality, chained over two operands or more: (= a b c) is a = b and \
b = c, which is every operand alike. It admits two types < does not: a \
handle, where \"the same entity\" is the question the type exists to \
answer, and a string, compared bytewise by content rather than \
ordered.");
("!=", "!= [is-equal ...] bool",
"All different: (!= a b c) is true when every operand differs from every \
other, so (!= 1 2 1) is false. Over everything = accepts. A float NaN \
is != to everything, itself included.");
("<", "< [is-ordered ...] bool",
"Less than, chained: (< a b c) is a < b and b < c, and every operand is \
evaluated once. Machine numbers and enums only — ordering a handle \
would order a free-list slot index, which means nothing.");
("<=", "<= [is-ordered ...] bool", "Less than or equal, chained like <.");
(">", "> [is-ordered ...] bool", "Greater than, chained like <.");
(">=", ">= [is-ordered ...] bool",
"Greater than or equal, chained like <.");
("not", "not [bool] bool",
"Negates a bool. Nothing else in this language is a truth value.");
("bit-and", "bit-and [int ...] int",
"Bitwise and, folded left; a && b in a .fln file. Integers only; operands \
of different widths meet at the wider one, the way + does.");
("bit-or", "bit-or [int ...] int",
"Bitwise or, folded left over integers; a || b in a .fln file.");
("bit-xor", "bit-xor [int ...] int",
"Bitwise exclusive or, folded left over integers; a ^^ b in a .fln file.");
("bit-not", "bit-not [int] int",
"Every bit of an integer flipped; ~~a in a .fln file.");
("&&", "&& [int ...] int", "bit-and, by its .fln spelling.");
("||", "|| [int ...] int", "bit-or, by its .fln spelling.");
("^^", "^^ [int ...] int", "bit-xor, by its .fln spelling.");
("~~", "~~ [int] int", "bit-not, by its .fln spelling.");
("<<", "<< [int int] int",
"Left shift. The value's type decides — a narrower count widens to it, a \
wider one is refused — and a literal count at or past the value's width \
is refused too. On a dyn int, a count outside 0 to 63 traps.");
(">>", ">> [int int] int",
"Right shift, arithmetic on a signed type and logical on an unsigned one. \
The value's type decides and the count widens to it; a literal count at \
or past the width is refused, as it is for <<.");
("rotate-left", "rotate-left [int int] int",
"The bits of the value moved left by the count, the ones that fall off \
the top coming back in at the bottom. The count is taken modulo the \
width.");
("rotate-right", "rotate-right [int int] int",
"The bits of the value moved right by the count, wrapping round to the \
top. The count is taken modulo the width.");
("popcount", "popcount [int] int",
"How many bits of the integer are set. The answer has the operand's \
type.");
("leading-zeros", "leading-zeros [int] int",
"How many zero bits come before the highest set bit, counted within the \
operand's width: the width itself for 0.");
("trailing-zeros", "trailing-zeros [int] int",
"How many zero bits come after the lowest set bit: the width for 0.");
("min", "min [is-ordered ...] is-ordered",
"The smallest of two or more operands, each of them evaluated exactly \
once however many there are. Two widths meet at the wider: (min i8-x \
i16-y) is an i16.");
("max", "max [is-ordered ...] is-ordered",
"The largest of two or more operands, each evaluated exactly once.");
("max-value", "max-value [type] T",
"The largest value of a numeric type: (max-value u8) is 255, and at a \
float the largest finite value. Takes a type variable under \
{:where (is-numeric $t)}.");
("min-value", "min-value [type] T",
"The least value of a numeric type: (min-value i8) is -128, 0 at an \
unsigned type, and at a float the negation of the largest finite value.");
("zeroed", "zeroed [] T",
"The all-bytes-zero value of whatever it is being stored into, so it \
only means anything where a type is expected of it.");
("filled", "filled [u8] T",
"Every byte of whatever it is being stored into set to one byte, as in \
(set grid (filled 0xFF)). Numbers only, and structs and fixed arrays \
built out of them.");
("dead-beef", "dead-beef [u32?] T",
"A four-byte pattern repeating over whatever it is being stored into, \
written so a hex dump reads it left to right: 0xDEADBEEF with no \
argument, the u32 given otherwise. A size that is not a multiple of \
four ends on a prefix of the pattern. Same types [filled] takes.");
("destructure~nth", "destructure~nth [[n T] i32 i32 i32] T",
"Written by the compiler for a destructuring let, and unspellable: the \
reader makes ~ a delimiter, so no source symbol can name this.");
(* allocators, spec-memory.md *)
("make-allocator", "make-allocator",
"A user-written allocator, not implemented and refused wherever it is \
written. Use (arena-new n), which is the parameterised allocator that \
does exist.");
("allocator-from", "allocator-from",
"A user-written allocator, not implemented — see make-allocator.");
("allocator", "allocator",
"A user-written allocator, not implemented — see make-allocator.");
("heap-allocator", "heap-allocator [] Allocator",
"The process heap as an Allocator: it releases one block at a time, so \
can-free is true of it.");
("arena-new", "arena-new [i64] Allocator",
"A new arena of exactly this many bytes. The capacity is explicit and \
the backing store never grows, which is what makes \"exhausted\" a \
state a test can reach on purpose.");
("arena-destroy", "arena-destroy [Allocator] ()",
"Hands the arena's pages back to the system, which free-all \
deliberately does not.");
("free-temp", "free-temp [] ()",
"Releases everything in the temp allocator, context/temp — where \
i64->bytes and f64->bytes put their text. Called once a frame; a dev \
build also does it at every frame boundary the agent polls at. Text \
kept past the frame is cloned out first.");
("free-all", "free-all [Allocator] ()",
"Releases everything the allocator holds and bumps its epoch, keeping \
the capacity. It traps rather than quietly doing nothing when there is \
no region to release.");
("can-free", "can-free [Allocator] bool",
"Whether this allocator can release a single block, read off its \
capability set rather than asked of a query procedure.");
("can-free-all", "can-free-all [Allocator] bool",
"Whether this allocator can release everything it holds at once.");
("alloc-epoch", "alloc-epoch [Allocator] i64",
"The counter free-all bumps. A container records it and traps if it \
moved; this is the same number, readable, so a program can say what it \
saw.");
("alloc-id", "alloc-id [Allocator] i64",
"The allocator's identity — its address — which is what a condition's \
:allocator field carries, so a handler holding several regions can \
tell which one ran out.");
("alloc-budget", "alloc-budget [Allocator] i64",
"The ceiling on live bytes, 0 for none. A handler that answers \
StorageExhausted with retry is the one that raises it.");
("set-alloc-budget", "set-alloc-budget [Allocator i64] ()",
"Sets the ceiling on live bytes; 0 for none. It is also how a program \
exhausts an allocator on purpose.");
("alloc-live-blocks", "alloc-live-blocks [Allocator] i64",
"How many blocks are still live — \"did you forget to free\", answered \
at the tier that can answer it.");
("with-allocator", "with-allocator [Allocator body ...] T",
"Runs the body with this allocator in the context, and answers the \
body's last expression. It releases nothing: not at the end of the \
body, not anywhere.");
(* (Vec T) *)
("vec-new", "vec-new [T? Allocator?] (Vec T)",
"An empty Vec. A let has no type annotation, so the element type is \
written at the call — (vec-new i32) — wherever the context does not \
say it; an allocator may be named the same way.");
("push", "push [(Vec T) T] ()",
"Appends one element, growing the Vec through its allocator. Unit and \
not an error code: a failed allocation signals StorageExhausted.");
("reserve", "reserve [(Vec T)|(Map K V) i32] ()",
"Makes room for n more. For a map the number is entries rather than \
slots — the block is sized so that n still sits under the load \
factor.");
("free", "free [(Vec T)|(Map K V)|String|[T] Allocator?] ()",
"Releases the container's block. It does not recurse into elements that \
own storage — such a container is refused here, and releasing its \
region with free-all is the answer. A slice (bytes s) or (clone xs) \
made goes back to the current allocator, or the one named; a dev build \
traps on a slice from another allocator or not from one at all.");
("clone", "clone [(Vec T)|(Map K V)|String|[T] Allocator?] (Vec T)|(Map K V)|String|[T]",
"A deep, independent copy, from the current allocator or one named. \
A slice's copy is a slice over a new block, released by (free s) or by \
its allocator's free-all. Refused for elements that own \
storage: a bytewise copy would alias the original's blocks under a \
name promising otherwise.");
("into-copies-elements", "into-copies-elements [src dst transform...] ()",
"What into writes when its chain has no (map f). Refuses a source whose \
elements own storage, because pushing them as they stand would share \
their blocks. Not meant to be written by hand.");
(* String *)
("string-new", "string-new [(str|String)? Allocator?] String",
"A String: owned, growable text that is always valid UTF-8. Empty, or \
a copy of the text given; from the current allocator or one named, and \
released by (free s). A str is checked as it is copied in.");
("bytes->string", "bytes->string [(Vec u8) Allocator?] String",
"A new String holding a copy of the Vec's bytes, once they are checked \
to be UTF-8 — here, at run time. Bytes that are not stop the program \
at this call. The Vec is untouched and is still yours to free.");
("append", "append [String str|String|i32] () append [(Vec u8) [const u8]] ()",
"Adds text or one code point to the end of a String. A str is checked \
to be UTF-8 as it is stored and a code point to be a Unicode scalar \
value; a literal is checked when the program is compiled. Onto a \
(Vec u8), or a pointer to one, it adds raw bytes.");
("insert", "insert [String i32 str|String|i32] ()",
"Stores text or a code point before the character at position i, \
counting characters and not bytes; i may be the character count, \
which is the end. Checked as append checks, and a position past the \
end signals BoundsError.");
("remove", "remove [String i32] i32",
"Takes out the character at position i, counting characters, and \
answers its code point. A position past the end signals BoundsError.");
("runes", "runes [str|String|[const u8]] Runes",
"A cursor over the text's chars: (runes-next (addr it)) answers \
the next one, or None at the end. A malformed byte in a str comes back \
as U+FFFD.");
("rune-count", "rune-count [str|String|[const u8]] i32",
"How many characters — code points — the text holds, where length \
counts bytes. A malformed byte counts as one.");
(* (Map K V) *)
("map-new", "map-new [K? V? Allocator?] (Map K V)",
"An empty map. The key and value types are written at the call — \
(map-new string i32) — wherever the context does not say them.");
("put", "put [(Map K V) K V] ()",
"Inserts or replaces. Unit rather than an error code, and \
(set (get m k) v) is not map syntax.");
("get", "get [(Map K V) K]|[collection i32 ...] (Option V)|(Option T)",
"The value at the key, or None. Nothing signals here — a lookup that \
finds nothing is an answer — and the value comes back as a copy of \
its bytes. Over an array, a slice, a string or a Vec it is at that \
answers None for an index out of range, negative included, one index \
per dimension. Over a dyn it answers nil for an absent key or index, \
and more keys walk a level each.");
("map-remove", "map-remove [(Map K V) K] (Option V)",
"Removes the entry and answers the value it held, or None if there was \
none.");
("map-next", "map-next [(Map K V) (Ptr i64) (Ptr K) (Ptr V)] bool",
"Walks the map one entry per call through a cursor the caller owns, and \
is the whole of map iteration: (while (map-next m (addr cur) (addr k) \
(addr v)) ...).");
("??", "?? [(Option T)|dyn T ...] T",
"x ?? d in .fln: what x holds, or d when x is None (nil over a dyn). d is \
evaluated only then. a ?? b ?? c reads from the right, and a default \
that is itself an Option keeps the whole an Option.");
("?", "? [(Option T)|dyn] bool",
"x? in .fln: whether x holds a value — Some, or a dyn that is not nil. \
In if x?, elif x? and while x?, a local x is its payload in the block.");
("!!", "!! [(Option T)|dyn] T",
"x! in .fln: what x holds. When x is None (nil over a dyn) the program \
stops there, naming x.");
("has-key", "has-key [(Map K V) K] bool",
"Whether the key is present, copying no value — the form a condition \
wants, where get would hand back an Option to match on. Over a dyn map \
it is the question that stays askable when nil might also be stored \
under the key.");
("class-of", "class-of [dyn] dyn",
"The class's name as a keyword for a value built by a defclass \
constructor, and nil for everything else — an ordinary map included. \
It is what a defgeneric dispatches on, so a class dispatcher is this \
call over the first argument and a defmulti whose body is (class-of x) \
is the same generic function written the other way.");
("type-of", "type-of [dyn] dyn",
"The value's kind as a keyword — :nil :bool :int :float :text :vec \
:keyword :map :char — or, for a value built by a defclass constructor, the \
class's name as class-of answers it. A typed value answers the kind it \
has as a dyn value: an i32 is :int.");
("chars", "chars [dyn] dyn",
"A dyn text's characters, as a new dyn vector of chars. A dyn text \
counts characters, not bytes, in length, at and slice.");
("text", "text [dyn] dyn",
"The dyn text a dyn vector of chars, or a single char, spells: \
(text (chars t)) is t.");
("keyword", "keyword [str|[const u8]] dyn",
"The interned dyn keyword named by the bytes, for a name that only \
exists at run time — a reader building :texture-path out of a token's \
text. A literal :foo is already one.");
(* assets, embedded at compile time *)
("embed", "embed [\"path\" str?] [const u8]",
"The file's bytes, read at compile time and baked in as a constant; \
(embed \"p\" str) reads it as a str instead. The path is \
relative to the file the form is written in, and the slice points into \
read-only data.");
("embed-dir", "embed-dir [\"path\"] [n EmbedFile]",
"Every file in the directory, read at compile time, as a fixed array of \
EmbedFile. It does not descend.");
("compile-error", "compile-error [\"message\"] ()",
"Refuses the compile with that message, at the form it is written in. \
What a macro expands to when it has to say why: a name nothing defines \
carries a name, and this carries a sentence.");
(* files *)
("slurp", "slurp [str Allocator?] (Vec u8)",
"Reads a whole file. No Result and no out-parameter: a failure to read \
signals FileError under retry and use-value, and a failure to allocate \
signals StorageExhausted.");
("barf", "barf [str [const u8]] ()",
"Writes a whole file. On the web target it signals FileError every \
time, with the path — there is no conditional compilation, so the \
program decides rather than the build.");
("delete-file", "delete-file [str] ()",
"Removes the file, or signals FileError with retry and use-value. It \
answers () and not a bool, because the failure is the condition.");
("make-directory", "make-directory [str] ()",
"Creates the directory, or signals FileError. () for the reason \
delete-file answers one.");
("rename-file", "rename-file [str str] ()",
"Renames the first path to the second, or signals FileError. A \
use-value names a different source for the same destination, which is \
the direction a handler can act on.");
(* containers *)
("length", "length [[n T]|[T]|str|String|(Vec T)|(Map K V)] i32",
"How many elements. One question and one word across an array, a slice, \
a string, a Vec and a Map. A str and a String count bytes, as rune-count \
does not; a dyn text counts characters.");
("at", "at [collection i32 ...] T",
"The element at an index, bounds-checked — and for a Vec with the \
allocator's epoch checked first. On a string it is the byte, a u8. It \
is also a place, so (set (at v i) x) goes through the same check; a \
string is the exception, being a view it does not own.");
("slice", "slice [[n T]|[T]|str|(Vec T) i32? i32?] [T]|str",
"The half-open range [lo hi) as a non-owning view. lo defaults to 0 and \
hi to the length, so (slice a) is the whole of it and (slice a n) is \
the tail from n. A bound may sit one past the end; a literal pair that \
runs backwards is refused here. A string slices to a string. A view of \
a Vec is a borrow from storage the Vec owns, and a push, a put or a \
reserve on that Vec may invalidate it.");
("slice-from", "slice-from [(Ptr T) n] [T]",
"Puts a length on a pointer that came back from C; n is any integer \
type. The caller promises it addresses that many initialised T and that \
they outlive the result; the compiler checks none of it. A negative n \
traps in every build.");
("addr", "addr [place] (Ptr T)",
"The address of a place — a name, (.field x), (at a i) or (deref p) — \
and not of an arbitrary expression.");
("deref", "deref [(Ptr T)] T",
"The value behind a pointer, and a place, so (set (deref p) x) writes \
through it.");
(* Option *)
("Some", "Some [T] (Option T)",
"Wraps a value as a present Option. None is the other half, and is \
written as a name rather than as a call. Where an (Option T) is \
expected a T is wrapped with no Some written, one level at a time; an \
Option is never unwrapped that way.");
(* the host primitives *)
("bytes", "bytes [str Allocator?] [u8]",
"A writable copy of the string's bytes, from the current allocator or \
one named. It allocates like vec-new does — a failure signals \
StorageExhausted with retry — and (free b) releases it, through the \
current allocator or (free b a) through the one it came from. For reading without a copy, \
bytes-view.");
("bytes-view", "bytes-view [str|String] [const u8]",
"The string's own storage seen as a read-only byte slice. It costs \
nothing — both are a ptr and a length at run time — and it decodes \
nothing. A store through it is a compile error; bytes is the writable \
copy.");
("str", "str [[const u8]|String] str",
"A byte slice, or a String's text, seen as a str, and free at run time. \
A String's str lasts until the String next changes. It does not check \
UTF-8, because `str` does not claim UTF-8 — is-valid-utf8 is an \
ordinary function you call when you care.");
("char", "char [int|char|dyn] char",
"A code point as a char. Only a Unicode scalar value is one — 0 to \
0x10FFFF, outside 0xD800 to 0xDFFF: a literal is checked when it \
compiles and any other value when it runs. (i32 c) is the way back.");
("bytes->f64", "bytes->f64 [[const u8]] f64", "Parses a float out of the bytes.");
("bytes->i64", "bytes->i64 [[const u8]] i64",
"Parses an integer out of the bytes.");
("f64->bytes", "f64->bytes [f64] [u8]",
"The number's text, %g, in the temp allocator: it lasts until the next \
(free-temp). Clone it to keep it longer.");
("i64->bytes", "i64->bytes [i64] [u8]",
"The number's text, in the temp allocator: it lasts until the next \
(free-temp). Clone it to keep it longer.");
("write-stdout", "write-stdout [[const u8]] ()",
"Writes the bytes to standard output exactly as given: no newline and \
no formatting.");
("print", "print [T ...] ()",
"The structural printer, selected for each argument's concrete type; \
arguments print in order with a single space between them. A string \
prints raw at the top level and quoted inside a structure, and a Ptr \
or a Handle prints its address rather than being followed. Printing \
is a read, so it does not consume the value.");
("println", "println [T ...] ()",
"print, with a newline after it — (println) alone is the newline.");
("watch", "watch [str T] ()",
"Writes the value, rendered as print renders it, into the dev session's \
watch table under the name, where M-x flan-watch shows it. Does nothing \
when no watch buffer is open. Outside a dev build it only evaluates \
the value.");
("exit", "exit [i32] never",
"Ends the process with this status. It has no value, so nothing written \
after it runs.");
("argv", "argv [] [str]", "The command line, as a slice of strings.");
(* the names rather than calls — [var]'s arms. Their
signature is the [name type] shape [Dev.defs] gives a global, because
that is what they are at the site: a value, not a call. *)
("true", "true bool", "The true boolean literal.");
("false", "false bool", "The false boolean literal.");
("nil", "nil dyn",
"The absent dyn value: what (get m k) answers for a key a dyn map does \
not hold. It becomes None where an (Option T) is wanted, and stays dyn \
everywhere else.");
("None", "None (Option T)",
"The absent Option. It takes its type from its context — a return type, \
a parameter, or (the (Option i32) None) — because nothing about the \
word says what it is an Option of.");
("context/allocator", "context/allocator Allocator",
"The allocator in effect here: what with-allocator rebinds, and what an \
allocating operation uses when none is named at the site.");
("context/temp", "context/temp Allocator",
"The scratch allocator the calling convention carries beside \
context/allocator.")
]
(* The forward reference declared beside [nearest], filled the moment the table
it names exists. Nothing reads it before a call is checked, and no call is
checked before this module is loaded. *)
let () =
builtin_names := List.map (fun (n, _, _) -> n) builtins;
List.iter (fun (n, _, _) -> Hashtbl.replace builtin_set n ()) builtins
(* ── The one thing shadowing owes the reader ────────────────────────────
A defn named after a builtin is legal and it wins ([shadows_builtin]), and
that is a large thing to have happened in silence: every [(get m k)] in
the file now means something the reader has to go and look at. So it is
said once, where the decision was made, and never again at the call sites
— a footgun notice, not a lint.
It is a warning and it says so in the one way that matters: nothing raises
and the exit status does not move. Unlike [memory_sites] it is behind no
flag, because there is nothing to tune — a program either renamed a
builtin or it did not, and the line is one line and rare.
The second half of the sentence names the way out. Until [builtin/] there
was none: a file that defined [length] had given the builtin [length] up
for the
whole file, and a defn that meant to *wrap* it was unbounded recursion
instead. Saying so here is the cheapest place it can be said — the reader
is being told the name was taken over, and the next thing they want to
know is what is left.
The prelude is skipped: its defns are the language's own and a collision
there is a compiler bug rather than news for whoever is compiling. So are
qualified names, for the reason [shadows_builtin]'s [visible] gives —
[rl/get] is not [get] and shadows nothing. *)
let shadowed_builtins (decls : Ast.decl list) : Loc.diag list =
(* Once per name: the arities of one fn are one definition. *)
let said = Hashtbl.create 4 in
List.filter_map
(fun (d : Ast.decl) ->
match d.Ast.d with
| Ast.Defn fn
when Hashtbl.mem builtin_set fn.Ast.name
&& not (Hashtbl.mem said fn.Ast.name)
&& (Hashtbl.replace said fn.Ast.name (); true)
&& not (String.contains fn.Ast.name '/')
&& not (String.equal fn.Ast.nloc.Loc.file Prelude.file) ->
Some
(Loc.diag ~kind:"check/shadows-builtin"
(* A fn with several arities is warned about at its fn line. *)
(if fn.Ast.fgroup <> None then d.Ast.dloc else fn.Ast.nloc)
(Printf.sprintf
"%s shadows the builtin %s — every call in this program now \
reaches your definition — the builtin stays reachable as %s%s"
fn.Ast.name fn.Ast.name builtin_prefix fn.Ast.name))
| _ -> None)
decls
(* ── Declarations: pass 1, collect ─────────────────────────────────── *)
(* Constant folding, only over integers and only for defconst — enough for an
array length like (/ screen-height cell-size). *)
let rec const_int env (e : Ast.expr) : int64 option =
match e.Ast.e with
| Ast.Int n -> Some n
| Ast.Byte b -> Some (Int64.of_int b)
| Ast.Var n -> Hashtbl.find_opt env.consts n
| Ast.Call ({ Ast.e = Ast.Var "-"; _ }, [ x ]) ->
Option.map Int64.neg (const_int env x)
(* A conversion to an integer type, which is how a negative number is
written as an unsigned constant's bit pattern: [(u64 -1)]. Truncated to
the type's width and extended by its sign, as the cast does at run time. *)
| Ast.Call ({ Ast.e = Ast.Var k; _ }, [ x ])
when Types.ikind_of_name k <> None ->
let k = Option.get (Types.ikind_of_name k) in
let bits = Types.bits k in
Option.map
(fun n ->
if bits = 64 then n
else if Types.signed k then
Int64.shift_right (Int64.shift_left n (64 - bits)) (64 - bits)
else Int64.logand n (Int64.sub (Int64.shift_left 1L bits) 1L))
(const_int env x)
(* Left to right over any number of operands, because that is how the
checker reads the same form: an array length that type-checks as a
product of three literals and is then not a constant would be a
distinction with nothing behind it. [%] is still two, as it is there. *)
| Ast.Call ({ Ast.e = Ast.Var op; _ }, x :: y :: rest) ->
let step a b =
match op with
| "+" -> Some (Int64.add a b)
| "-" -> Some (Int64.sub a b)
| "*" -> Some (Int64.mul a b)
| "/" when b <> 0L -> Some (Int64.div a b)
| "%" when b <> 0L && rest = [] -> Some (Int64.rem a b)
| _ -> None
in
List.fold_left
(fun acc e ->
match acc, const_int env e with
| Some a, Some b -> step a b
| _ -> None)
(const_int env x) (y :: rest)
| _ -> None
(* [(defconst grid [rows [cols u8]])]. A two-element defconst has no type slot
— the second form is always a value — so the brackets were read as an array
*literal* and [u8] as a name in it, and the refusal that came out was
"unknown name u8", which sends the reader to look for a missing definition
of something the language has had all along.
A type name inside an array literal is unambiguous evidence, because a type
and a value cannot share a name: [collect]'s claimed table is over every
declaration kind there is. So finding one means the whole form was meant as
a type, and the form that takes one is [defonce]. *)
let rec defconst_type_shaped env gname (v : Ast.expr) =
match v.Ast.e with
| Ast.Arr items ->
List.iter
(fun (i : Ast.expr) ->
match i.Ast.e with
| Ast.Var n when is_type_name env n ->
Loc.failk "check/defconst-is-a-type" i.Ast.loc
"%s is a type, and this is a value: a two-element defconst has no \
type slot, so the brackets around it were read as an array \
literal and %s as a name in it. A global declared by its type is \
a defonce — write (defonce %s ...) with the same brackets"
n n gname
| _ -> defconst_type_shaped env gname i)
items
| _ -> ()
(* Every parent a struct names, now that every struct has its fields.
A parent has exactly [Error]'s two fields, [name string] and
[message string], and that is not a style rule: a handler that matched
through the link is handed the signal site's descriptor rather than the
condition, because the condition's layout is its own type's and the
handler's type is an ancestor's. The descriptor's first two fields are the
name and the sentence, so a parent shaped any other way would be read off
bytes that are not its fields. *)
let check_parents env =
Hashtbl.iter
(fun child parent ->
let loc =
Option.value (Hashtbl.find_opt env.locs child) ~default:Loc.unknown
in
if not (error_shaped env parent) then begin
let has =
match Hashtbl.find_opt env.structs parent with
| Some { Tast.fields = []; _ } -> "none"
| Some s ->
String.concat " "
(List.map
(fun (f : Tast.field) ->
f.Tast.fname ^ " " ^ Types.to_string f.Tast.fty)
s.Tast.fields)
| None -> "none"
in
let fix =
match Hashtbl.find_opt env.parents parent with
| Some _ -> Printf.sprintf "(defstruct %s :parent %s)" parent
(Hashtbl.find env.parents parent)
| None -> Printf.sprintf "(defstruct %s :parent Error)" parent
in
fail loc
"%s names %s as its parent, and a parent has exactly the fields \
[name string message string], because a handler for a parent is \
handed the name and the message of whatever it caught. %s has \
[%s]. Declare it with no field vector, %s, which gives it those \
two"
child parent parent has fix
end;
(* A cycle is a chain with no root; the walk stops at the repeat. *)
let chain = condition_chain env child in
match Hashtbl.find_opt env.parents (List.nth chain (List.length chain - 1)) with
| Some back ->
fail loc
"%s's parents go round in a loop, %s -> %s, and a chain of parents \
has to end at a type with no parent, such as Error"
child (String.concat " -> " chain) back
| None -> ())
env.parents
(* Untyped constants to a fixpoint, and for the same reason: one untyped
constant may be defined in terms of another declared after it. A constant
that still does not check once no progress is left has a real error, so
the last round is run without swallowing it. *)
(* ── One fn, several arities (decision 139) ─────────────────────────────
[fn f] with [(g: Grain) -> bool] and [(r: i32, c: i32) -> bool] under it
is one function with two arities, and a call picks one by how many
arguments it passes. [Parse.splice] made one [defn] per arity, all at the
form's location; nothing past the checker knows either: each arity is
renamed here to a name of its own, and from then on it is an ordinary
function with an ordinary symbol, cell and stale-call word. The [~] is
what keeps the renamed name out of a program's reach — it ends a symbol in
both readers — as it does for [prelude~].
A fn with one arity keeps its name, so its symbol is what it always was;
only a name with two or more is renamed, all of its arities alike, so no
arity is the plain name's by accident of order.
The form is the unit of definition, Clojure's: the arities are closed, and
a second [fn f] anywhere is [f] defined twice whatever its arity, since
letting it add one would make which arities exist depend on which files
were read. Types play no part in the choice. *)
let split_versions env (decls : Ast.decl list) : Ast.decl list =
let arities = Hashtbl.create 16 in
List.iter
(fun (d : Ast.decl) ->
match d.Ast.d with
| Ast.Defn fn ->
let n = fn.Ast.name and k = List.length fn.Ast.params in
let seen = Option.value ~default:[] (Hashtbl.find_opt arities n) in
(match seen with
| (_, _, group, gloc) :: _
when fn.Ast.fgroup = None || group <> fn.Ast.fgroup ->
let fln = fln_source d.Ast.dloc in
Loc.failk "check/defined-twice" d.Ast.dloc
~notes:[ Loc.note gloc (n ^ " is already defined here") ]
"%s is defined twice. A function with several arities is one \
definition, each arity under it:\n\n%s"
n
(if fln then
Printf.sprintf
" fn %s\n (a: T) -> R\n ...\n \
(a: T, b: T) -> R\n ..." n
else
Printf.sprintf " (defn %s ([a T] R ...) ([a T b T] R ...))" n)
(* [main] is called by the startup code, by its own name, so it has
one arity. *)
| (_, first, _, _) :: _ when String.equal n "main" ->
Loc.failk "check/defined-twice" fn.Ast.nloc
~notes:[ Loc.note first "main's other arity is here" ]
"main has one arity: the program's startup calls it by its \
name"
| _ -> ());
(match List.find_opt (fun (j, _, _, _) -> j = k) seen with
| Some (_, first, _, _) ->
Loc.failk "check/defined-twice" fn.Ast.nloc
~notes:[ Loc.note first "the other one is here" ]
"%s has two arities with %d parameter%s. Each arity takes a \
different number of arguments, which is how a call picks one"
n k (if k = 1 then "" else "s")
| None -> ());
Hashtbl.replace arities n
(seen @ [ (k, fn.Ast.nloc, fn.Ast.fgroup, d.Ast.dloc) ])
| _ -> ())
decls;
Hashtbl.iter
(fun n seen ->
if List.length seen > 1 then
Hashtbl.replace env.versions n
(List.sort compare
(List.map (fun (k, _, _, _) -> (k, version_name n k)) seen)))
arities;
if Hashtbl.length env.versions = 0 then decls
else
List.map
(fun (d : Ast.decl) ->
match d.Ast.d with
| Ast.Defn fn when Hashtbl.mem env.versions fn.Ast.name ->
let v = version_name fn.Ast.name (List.length fn.Ast.params) in
{ d with Ast.d = Ast.Defn { fn with Ast.name = v } }
| _ -> d)
decls
let settle_consts env consts =
let infer (_, v) =
with_typed_literals (fun () -> (check (invented_ctx env Types.Unit) v).Tast.ty)
in
let pending = ref consts in
let rec settle () =
let left =
List.filter
(fun ((n, _) as c) ->
match speculate env (fun () -> infer c) with
| ty -> Hashtbl.replace env.globals n (ty, true); false
| exception Loc.Error _ -> true)
!pending
in
let progressed = List.length left < List.length !pending in
pending := left;
if progressed && left <> [] then settle ()
in
settle ();
List.iter (fun c -> ignore (infer c)) !pending
(* Set by [collect], read by [build_program] once [infer_returns] ran. *)
let consts_after_infer : (string * Ast.expr) list ref = ref []
let collect env (decls : Ast.decl list) =
(* One pass over every declaration kind before any of the others, because
the tables below are per-kind — structs, data types, aliases, enums, functions
and globals each have their own — and a collision between two of them
would otherwise be found by LLVM, as [redefinition of function
'@flan.item'], or not at all. A [defn item] and a [defonce item] are two
declarations of one name and are rejected here. *)
(* The qualifier is reserved on this side too. [(defn builtin/length ...)]
reads — the reader treats [/] as an ordinary symbol character — and would
otherwise land in [env.fns] under a name nothing can ever call, because
[named_call] strips the prefix before any table is consulted. A
declaration that can only ever be dead is refused where it is written
rather than left to be discovered. [Load] refuses the same qualifier from
the other direction, at an import's alias. *)
List.iter
(fun (d : Ast.decl) ->
match Ast.declared_name d with
| Some n when qualified_builtin n <> None ->
fail d.Ast.dloc
"%s cannot be declared: %s is a reserved qualifier, so a name \
spelled with it reaches the compiler's builtins and never a \
declaration — nothing could call this one"
n builtin_prefix
(* [[const u8]] is a read-only slice only because no constant can be
named [const]: [[n T]] takes a constant's name for [n], and a
declaration of that name would make the brackets mean two things.
A local cannot be an array length, so only a declaration is
refused. *)
| Some "const" ->
Loc.failk "check/reserved-const" d.Ast.dloc
"const cannot be declared: it is reserved for the read-only slice \
type, [const T]. Choose another name"
| _ -> ())
decls;
let claimed = Hashtbl.create 64 in
let is_defn (d : Ast.decl) = match d.Ast.d with Ast.Defn _ -> true | _ -> false in
List.iter
(fun (d : Ast.decl) ->
match Ast.declared_name d with
| None -> ()
| Some n ->
(match Hashtbl.find_opt claimed n with
(* Two [defn]s of one name are either the arities of one fn or one
name defined twice, and the arities are known only once the
parameter vectors are paired — [split_versions], below
[pair_decls], says which. *)
| Some (_, true) when is_defn d -> ()
| Some (first, _) ->
(* The second one is the error, because it is the one to delete;
the first is the note, because without it the message is a
claim the reader has to go and verify. *)
Loc.failk "check/defined-twice" d.Ast.dloc
~notes:[ Loc.note first (n ^ " is already defined here") ]
"%s is defined twice" n
| None -> Hashtbl.add claimed n (d.Ast.dloc, is_defn d)))
decls;
(* A defstruct whose fields introduce a variable is a template. *)
let generic_fields (fs : Ast.field list) =
let vs, _, _ =
sigil_vars ~kinds_of:(fun _ -> None)
(List.map (fun (f : Ast.field) -> f.Ast.fty) fs)
in
vs <> []
in
let gpending = Hashtbl.create 4 in
(* Names first, so a struct may mention one declared below it. *)
List.iter
(fun (d : Ast.decl) ->
match d.Ast.d with
| Ast.Defstruct (n, fs, parent) when generic_fields fs ->
(match parent with
| Some t ->
fail t.Ast.tloc
"%s is generic, and a condition struct is not — a handler \
matches one type, and %s is a type only at its arguments" n n
| None -> ());
Hashtbl.replace env.locs n d.Ast.dloc;
Hashtbl.replace gpending n (fs, d.Ast.dloc)
| Ast.Defstruct (n, _, _) ->
Hashtbl.replace env.locs n d.Ast.dloc;
Hashtbl.replace env.structs n { Tast.sname = n; fields = [] }
| Ast.Defdata (n, _) ->
Hashtbl.replace env.locs n d.Ast.dloc;
Hashtbl.replace env.datas n { Tast.dname = n; cases = [] }
| Ast.Defunion (n, _) ->
Hashtbl.replace env.locs n d.Ast.dloc;
Hashtbl.replace env.unions n { Tast.sname = n; fields = [] }
(* [int] and [float] are builtin aliases (Types), and a program that
declared them itself — which this one's author did, before they were
builtin — must not quietly stop meaning what it says. The alias
table is never consulted for either name: [resolve_name] answers
from [ikind_of_name] first, so a [(defalias int i64)] left to
register would be read as [i32] at every use and nothing would ever
say so.
So the target decides. Spelled as the builtin's own type, the
declaration is true and is accepted as the no-op it is — the old
program still compiles, and deleting the line is a cleanup rather
than a fix. Spelled as anything else it is refused, because the only
alternative is to silently mean something else. Nothing is written
to the table either way: the name resolves without it. *)
| Ast.Defalias (n, { Ast.t = Ast.Tname target; _ })
when n = "int" || n = "float" ->
let builtin = if n = "int" then "i32" else "f32" in
if target <> builtin then
Loc.failk "check/builtin-alias" d.Ast.dloc
"%s is a builtin alias for %s and cannot be redefined as %s — \
delete this defalias, or give the type another name"
n builtin target
| Ast.Defalias (n, _) when n = "int" || n = "float" ->
let builtin = if n = "int" then "i32" else "f32" in
Loc.failk "check/builtin-alias" d.Ast.dloc
"%s is a builtin alias for %s and cannot be redefined — delete \
this defalias, or give the type another name"
n builtin
| Ast.Defalias (n, t) -> Hashtbl.replace env.aliases n t
| _ -> ())
decls;
(* Each template's parameters, which needs every other template's: a
template's length argument to another is a length of its own. A cycle
between templates reads the arguments on it as types; any length among
them is then refused where it is used. *)
let rec params_of visiting n =
match Hashtbl.find_opt env.gstructs n with
| Some g -> Some (List.map snd g.gparams)
| None ->
match Hashtbl.find_opt gpending n with
| None -> None
| Some _ when List.mem n visiting -> None
| Some (fs, gloc) ->
let _, _, vs =
sigil_vars ~kinds_of:(params_of (n :: visiting))
(List.map (fun (f : Ast.field) -> f.Ast.fty) fs)
in
Hashtbl.replace env.gstructs n { gparams = vs; gfields = fs; gloc };
Some (List.map snd vs)
in
Hashtbl.iter (fun n _ -> ignore (params_of [] n)) gpending;
(* Compile-time integer constants next, to a fixpoint, because an array
length may name a constant declared below it — top-level names in a
package are order-independent (plan.org, Modules). *)
let fold_consts () =
let progress = ref false in
List.iter
(fun (d : Ast.decl) ->
match d.Ast.d with
| Ast.Defconst (n, _, v) when not (Hashtbl.mem env.consts n) ->
(match const_int env v with
| Some i -> Hashtbl.replace env.consts n i; progress := true
| None -> ())
| _ -> ())
decls;
!progress
in
while fold_consts () do () done;
let field (f : Ast.field) : Tast.field =
(* The flag is reset through [Fun.protect] because a refusal here does not
end the run: [program_all] carries on collecting diagnostics, and a
flag left set would misword every later unknown-type message. *)
env.in_field <- true;
let fty =
Fun.protect ~finally:(fun () -> env.in_field <- false)
(fun () -> resolve env f.Ast.fty)
in
no_zeroed_fn f.Ast.fty.Ast.tloc
(Printf.sprintf "the field %s" f.Ast.fname) fty;
{ Tast.fname = f.Ast.fname; fty }
in
(* Constants with no declared type are inferred from their value, which needs
every other signature in hand — so they are deferred to a pass of their
own below. *)
let untyped = ref [] in
Hashtbl.reset char_consts;
(* Enums come first, in a pass of their own: a signature below may name one,
and [resolve] has to find it before it resolves that signature. *)
List.iter
(fun (d : Ast.decl) ->
match d.Ast.d with
| Ast.Defenum (n, members) ->
let names = List.map fst members in
if List.length (List.sort_uniq compare names) <> List.length names then
fail d.Ast.dloc "%s declares the same member twice" n;
Hashtbl.replace env.enums n members;
Hashtbl.replace env.locs n d.Ast.dloc
| _ -> ())
decls;
(* Every type name is registered by here — structs, data types and unions by
the names-first pass, aliases with them, enums by the pass just above — so
this is the first point at which a [defn]'s parameter vector can be paired.
It is done before the signature loop below rather than inside it, because a
signature may name a type declared further down and pairing must not depend
on the order the file was written in. *)
let decls = pair_decls env decls in
let decls = split_versions env decls in
(* And for the same reason, at the same point: a three-element defonce is a
type or a value by name, and every type name is registered by here. *)
let decls = settle_defvars env decls in
List.iter
(fun (d : Ast.decl) ->
let loc = d.Ast.dloc in
match d.Ast.d with
| Ast.Package _ -> ()
| Ast.Defenum _ -> ()
(* Imports are gone by now: [Load] resolved them into these very decls,
so one reaching the checker is a driver that skipped that step. *)
| Ast.Import (alias, _) ->
fail loc "internal: the import of %s was not resolved before checking"
alias
(* [Shim.expand] rewrote every one of these into a [Declare] and a
[Defn] before [collect] ran, so one arriving here is a driver that
skipped that step. *)
| Ast.DeclareC (fn, _) ->
fail loc "internal: the declare-c of %s was not expanded before checking"
fn.Ast.name
| Ast.Declare (fn, csym) ->
if Hashtbl.mem env.fns fn.Ast.name then
fail loc "%s is declared twice" fn.Ast.name;
let params =
List.map (fun (p : Ast.field) -> resolve env p.Ast.fty) fn.Ast.params
in
let ret =
match fn.Ast.ret with None -> Types.Unit | Some t -> resolve env t
in
(* What may cross the boundary. A slice or a string goes as ptr+len,
a scalar as itself; an aggregate does not go at all, because how
one is passed differs per target and reproducing that here would
be three calling conventions to maintain. Pass (Ptr T) instead and
let the C shim dereference it — that is what the shim is for. *)
let crossable what (t : Types.t) =
match t with
| Types.Int _ | Types.Float _ | Types.Bool | Types.Ptr _
| Types.Enum _ | Types.Unit -> ()
| Types.String | Types.Slice _ when what = "a parameter" -> ()
(* Its own arm, because the general advice below is wrong for it and
dangerously so. A dyn is one machine word and would cross without
complaint — [(Ptr dyn)] is not the fix and there is nothing for a
shim to read: what the C side would receive is a word whose
meaning only the dyn runtime knows, and C has no way to ask.
Refused by name rather than let through as an integer. *)
| Types.Dyn ->
fail loc
"%s of %s is dyn, which does not cross to C — take the value \
at a written type and pass that"
what fn.Ast.name
(* Its own arm too, because "pass (Ptr T)" is nonsense for a
function and the real objection is worth stating. A [CFn] is
one word and is the right *shape* for a C callback — that is
what it is for — but a Flan function's emitted signature still
ends with the transfer channel, and a C caller knows nothing
about one. So the address is not a C function pointer yet, and
what would make it one is dropping the channel from a signature
that cannot transfer (TODO.org, "CFn and C's calling
convention"). A [Fn] is two words
and is not even the right shape. *)
| Types.CFn _ | Types.Fn _ ->
fail loc
"%s of %s is %s, and a Flan function's address is not a C \
function pointer yet — not even a CFn's. Its signature ends \
with the transfer channel, and a C caller knows nothing \
about one; the C in CFn is about having no environment, \
which is what a C function pointer would need, and not about \
crossing today. Write the callback in C, or give the binding \
a (Ptr ()) and let the shim pass C's own"
what fn.Ast.name (tyname loc t)
| _ ->
fail loc
"%s of %s is %s, which cannot cross to C directly — pass \
(Ptr %s) and let the shim read it" what fn.Ast.name
(tyname loc t) (tyname loc t)
in
List.iter (crossable "a parameter") params;
crossable "the return type" ret;
Hashtbl.replace env.fns fn.Ast.name (params, ret);
Hashtbl.replace env.externs fn.Ast.name csym;
Hashtbl.replace env.extern_locs fn.Ast.name loc
| Ast.Defalias _ -> ()
| Ast.Defstruct (n, fs, _) when Hashtbl.mem env.gstructs n ->
let names = List.map (fun (f : Ast.field) -> f.Ast.fname) fs in
if List.length (List.sort_uniq compare names) <> List.length names then
fail loc "%s declares the same field twice" n;
(* The template is checked once, here, at its variables: an unknown
type in a field is refused at the defstruct rather than at the
first use of it. *)
let g = Hashtbl.find env.gstructs n in
(match
struct_copy ~at_definition:true env loc n
(List.map (fun (p, _) -> Types.Var p) g.gparams)
with
| _ -> ()
| exception Loc.Error d ->
Hashtbl.replace env.broken n ();
defer_or_raise env d)
| Ast.Defstruct (n, fs, parent) ->
let names = List.map (fun (f : Ast.field) -> f.Ast.fname) fs in
if List.length (List.sort_uniq compare names) <> List.length names then
fail loc "%s declares the same field twice" n;
(* The parent is recorded here and its shape checked once every
struct has its fields, below, since it may be declared later. *)
(match parent with
| None -> Hashtbl.remove env.parents n
| Some t ->
(match resolve env t with
| Types.Named pn when Hashtbl.mem env.structs pn ->
if String.equal pn n then
fail t.Ast.tloc "%s cannot be its own parent" n;
Hashtbl.replace env.parents n pn
| pt ->
fail t.Ast.tloc
"%s names %s as its parent, and a parent is a condition \
struct, such as Error, the root every error descends from"
n (tyname loc pt)));
let fields = List.map field fs in
(* Recorded before the refusal below rather than after it, because the
refusal asks [region_only], which walks this very declaration: a
recursive value's field is a [(Vec Value)] and answering for it
means reading [Value]'s own cases back out of the table. A [fail]
aborts the whole compilation, so an entry left behind by a
declaration that is about to be refused is never read. *)
(* A dyn field used to be refused here, for the reason a condition's
was: the collector's roots are the frames, and a struct outlives
the frame that built it, so its dyn field was a live value
reachable only through memory the marker never walked. The
per-type descriptors lifted that. A struct that holds a dyn now
gets a descriptor saying at which byte offsets its dyn words sit,
and every slot, global and temporary that holds one goes on the
collector's root stack with that descriptor beside it — see
runtime/flan_dyn.h's [flan_dyn_root_push_desc], which is also where
the reason no instance carries a header word is written down.
What is still refused is a dyn the descriptor cannot reach: one
inside a typed container, behind a pointer, or in a data type's
payload, where the offset is not a static property of the type.
That refusal is [hidden_dyn] below, and it is made over the whole
program rather than here, because the type that hides it may be
declared after the one that names it. *)
Hashtbl.replace env.structs n { Tast.sname = n; fields };
(* A struct field may own storage. Since the repeal a struct
holding a [(Vec i32)] is an ordinary value: assignment copies the
header bytes, the copies alias one buffer, and freeing through
two copies is the program's bug — Odin's contract, kept whole.
The region rule is separate and survives on its own ground: a
field whose container holds *owning* elements can only have been
built against a region allocator — the guard at its construction
is what makes sure of it (see [vec-new]) — so that graph is
released by one [free-all] and no teardown recurses anywhere. *)
| Ast.Defdata (n, vs) ->
(* A data type with no cases has no value, so nothing could ever be given
one, and a parameter of that type would be a function nothing can
call. It parses; it is refused here rather than surviving to a
layout with a tag and no case for the tag to name. *)
if vs = [] then
fail loc
"%s declares no cases — a data type is (defdata %s [(Case [field \
Type ...]) ...])" n n;
let cnames = List.map (fun (v : Ast.variant) -> v.Ast.vname) vs in
if List.length (List.sort_uniq compare cnames) <> List.length cnames
then fail loc "%s declares the same case twice" n;
let cases_with_loc =
List.map
(fun (v : Ast.variant) ->
let fnames =
List.map (fun (f : Ast.field) -> f.Ast.fname) v.Ast.vfields
in
if List.length (List.sort_uniq compare fnames)
<> List.length fnames then
fail v.Ast.vloc "%s.%s declares the same field twice"
n v.Ast.vname;
let vfields = List.map field v.Ast.vfields in
v.Ast.vloc, { Tast.vname = v.Ast.vname; vfields })
vs
in
let cases = List.map snd cases_with_loc in
(* Registered before the field refusal below, not after, for the
reason the struct's copy of this gives: [region_only] has to read
this data type's own cases back out to answer for a [(Vec Value)]
that names [Value]. The two declarations do this identically
because a data type that could hold a container where a struct
could not would be a hole in the same rule. *)
Hashtbl.replace env.datas n { Tast.dname = n; cases };
(* A case's fields are a struct and may own storage, on the struct's
terms since the repeal: copies alias, and the free is the
program's to write. The region rule still applies on its own
ground — a [(Vec Value)] case field can only have been built
against a region, so the recursive dynamic value parses into an
arena and one [free-all] releases the graph, no teardown
anywhere. *)
List.iter
(fun (c : Tast.variant) ->
Hashtbl.replace env.cases (n ^ "." ^ c.Tast.vname) (n, c);
Hashtbl.replace env.cases c.Tast.vname (n, c))
cases
(* Nothing records which member of a union is live, so nothing — the
program included — can free the right one through the union itself.
Since the repeal that is a fact about the value and not a refusal:
a member may own storage, and freeing it is done through whatever
tag the program keeps beside the union, as C does. *) | Ast.Defunion (n, ms) ->
if ms = [] then
fail loc
"%s declares no members — a union is (defunion %s [member Type \
...])" n n;
let names = List.map (fun (f : Ast.field) -> f.Ast.fname) ms in
if List.length (List.sort_uniq compare names) <> List.length names then
fail loc "%s declares the same member twice" n;
let fields = List.map field ms in
Hashtbl.replace env.unions n { Tast.sname = n; fields }
| Ast.Defn fn ->
missing_return_type env fn;
(* A signature that introduces a type variable is a *pattern*, not a
signature: it goes in [gsigs] and the function goes nowhere near
[fns], because nothing can be called at [t]. Every call site turns
it into an ordinary entry. *)
let vars, lens = signature_tyvars env fn in
(* The [where] clause is checked against the signature here, once,
rather than at every use of it: a predicate nobody has heard of,
or one about a variable the signature never bound, is a mistake
about this definition and is refused at this definition. *)
List.iter
(fun (p : Ast.pred) ->
if not (List.mem p.Ast.pname predicate_names) then
Loc.failk "check/unknown-predicate" p.Ast.ploc
"%s is not a type predicate. The ones there are: %s"
p.Ast.pname (String.concat ", " predicate_names);
if not (List.mem p.Ast.pvar vars) then
Loc.failk "check/unbound-predicate-variable" p.Ast.ploc
"$%s is not a type variable of %s%s"
p.Ast.pvar fn.Ast.name
(if vars = [] then " — it binds none"
else
" — it binds "
^ String.concat ", " (List.map (fun v -> "$" ^ v) vars));
(* A where clause takes type predicates, and a length is not a
type. Whether it should take value predicates over one is
an open question in TODO.org, not an accident to fall out
of this. *)
if List.mem p.Ast.pvar lens then
defer_or_raise env
(Loc.diag ~kind:"check/length-predicate" p.Ast.ploc
(Printf.sprintf
"$%s is a length, and a where clause takes type \
predicates only — %s is about a type"
p.Ast.pvar p.Ast.pname)))
fn.Ast.fwhere;
(* A predicate over a length was refused above; what is left is the
clause every copy is judged against. *)
let fn =
{ fn with
Ast.fwhere =
List.filter
(fun (p : Ast.pred) -> not (List.mem p.Ast.pvar lens))
fn.Ast.fwhere }
in
env.tyvars <- vars;
env.lenvars <- lens;
env.tvpreds <- fn.Ast.fwhere;
let params, ret =
Fun.protect
~finally:(fun () ->
env.tyvars <- []; env.lenvars <- []; env.tvpreds <- [])
(fun () ->
let params =
List.map (fun (p : Ast.field) -> resolve env p.Ast.fty)
fn.Ast.params
in
let ret =
match fn.Ast.ret with
| None -> Some Types.Unit
(* Read off the body by [infer_returns], once every
written signature is in [fns]; until then the name
has none. *)
| Some { Ast.t = Ast.Tinfer; tloc } ->
if vars <> [] then
Loc.failk "check/infer-generic" tloc
"%s is generic, and _ asks for its return type to be \
read off one body — but each call site makes its own \
copy. Write the return type, in terms of the $ \
variables"
fn.Ast.name;
None
| Some t -> Some (resolve env t)
in
params, ret)
in
if fn.Ast.fprivate <> Ast.Exported then
Hashtbl.replace env.privates fn.Ast.name
(fn.Ast.nloc, fn.Ast.fprivate);
if vars = [] then begin
Option.iter
(fun ret -> Hashtbl.replace env.fns fn.Ast.name (params, ret))
ret;
Hashtbl.replace env.fparams fn.Ast.name fn.Ast.params;
Hashtbl.replace env.fn_locs fn.Ast.name fn.Ast.nloc
end
else begin
let ret = Option.get ret in
Hashtbl.replace env.generics fn.Ast.name fn;
Hashtbl.replace env.gsigs fn.Ast.name (vars, params, ret);
Hashtbl.replace env.glens fn.Ast.name lens
end
| Ast.Defvar (n, t, _, k) ->
let ty = match t with
| Some t -> resolve env t
| None ->
fail loc "%s %s needs a type"
(match k with Ast.Once -> "defonce" | Ast.Every -> "def") n
in
Hashtbl.replace env.globals n (ty, false);
Hashtbl.replace global_forms n k;
Hashtbl.replace env.global_locs n loc
| Ast.Defconst (n, Some t, _) ->
Hashtbl.replace env.globals n (resolve env t, true);
Hashtbl.replace env.global_locs n loc
| Ast.Defconst (n, None, v) ->
(match v.Ast.e with
| Ast.Byte b -> Hashtbl.replace char_consts n b
| _ -> ());
defconst_type_shaped env n v;
Hashtbl.replace env.global_locs n loc;
untyped := (n, v) :: !untyped
(* [Classes.expand] ran at the top of [build_program] and left none of
these behind, the way [Shim.expand] leaves no [declare-c] behind. A
driver that assembled a declaration list and skipped that pass would
otherwise get a missing name from wherever the constructor was
called, with nothing pointing here. *)
| Ast.Defclass (n, _) ->
fail loc
"internal: the class %s reached the checker unpaired — \
pair_decls writes its constructor, and did not run" n
| Ast.Defgeneric { Ast.name = n; _ }
| Ast.Defmulti { Ast.name = n; _ } ->
fail loc
"internal: %s reached the checker unexpanded — Classes.expand did \
not run over this declaration list" n
| Ast.Defmethod m ->
fail loc
"internal: a method of %s reached the checker unexpanded — \
Classes.expand did not run over this declaration list" m.Ast.mgen)
decls;
(* The untyped constants ([settle_consts]). One that calls a [_] function
waits for [infer_returns], which needs every signature this pass
registers; it is settled after that, and its refusal is the one a
written return type would get. *)
let inferred_names =
List.filter_map
(fun (d : Ast.decl) ->
match d.Ast.d with
| Ast.Defn { Ast.name; ret = Some { Ast.t = Ast.Tinfer; _ }; _ } ->
Some name
| _ -> None)
decls
in
let waits (_, v) =
let acc = ref [] in
Load.expr_uses acc v;
List.exists (fun (n, _) -> List.mem n inferred_names) !acc
in
let late, now = List.partition waits (List.rev !untyped) in
consts_after_infer := late;
settle_consts env now;
check_parents env;
(* The paired declarations, handed back so that pass two checks the bodies of
the same functions whose signatures this pass registered. Pairing needs the
type names, which only this pass has; every pass after it needs the result,
and a [defn] still carrying an unpaired vector would check as a function of
no parameters at all. *)
decls
(* A type that contains itself by value has no finite size. [(Ptr T)] and a
slice are indirections and break the cycle; a fixed array does not, because
it is inline. Caught here rather than when a backend tries to lay the type
out or a zero value is built for it — which would not fail, it would hang. *)
let check_finite env =
let walk _ n = finite_from env n in
(* A generic struct's copy was asked this when it was made. *)
Hashtbl.iter
(fun n _ -> if not (Hashtbl.mem env.copies n) then walk [] n)
env.structs;
Hashtbl.iter (fun n _ -> walk [] n) env.datas;
Hashtbl.iter (fun n _ -> walk [] n) env.unions
(* No [bool] and no data type anywhere inside a union, at any depth — see the [Defunion] arm in
[collect] for why an [i1] read out of a union is the one punning hazard
Flan refuses rather than defines. It runs here, after [collect], because it
has to look through a member's *struct* to reach the fields inside it and
the struct table is only complete once every declaration has been walked. A
union that contains itself is impossible by [check_finite] above, so the
recursion terminates without a seen set — except through a [Ptr], which
this does not follow: a bool behind a pointer is a bool in someone else's
storage and is loaded from an address, not reinterpreted out of a blob. *)
let check_union_members env =
let rec walk uname where (t : Types.t) =
match t with
| Types.Bool ->
fail (Option.value (Hashtbl.find_opt env.locs uname) ~default:Loc.unknown)
"%s is a bool, and a union may not hold one at any depth — hold a u8 \
in the union and compare it yourself"
where
(* An [Option] is deliberately not on this list, and the difference is
worth stating because a reader will ask. Its [match] lowers to a test of
the tag byte and a branch, so a scribbled tag reads as a [Some] with a
garbage payload — a number nobody stored, which is exactly what this
language says a union read is. A data type's lowers to a chain of
comparisons with an [unreachable] after the last one. *)
| Types.Array (_, e) | Types.Option e -> walk uname where e
| Types.Named n when Hashtbl.mem env.datas n ->
fail (Option.value (Hashtbl.find_opt env.locs uname) ~default:Loc.unknown)
"%s is %s, a data type, and a union may not hold one at any depth — \
hold the %s beside the union"
where n n
| Types.Named n ->
(match Hashtbl.find_opt env.structs n with
| Some st ->
List.iter
(fun (f : Tast.field) ->
walk uname (where ^ "." ^ f.Tast.fname) f.Tast.fty)
st.Tast.fields
| None ->
match Hashtbl.find_opt env.unions n with
| None -> ()
| Some u ->
List.iter
(fun (f : Tast.field) ->
walk uname (where ^ "." ^ f.Tast.fname) f.Tast.fty)
u.Tast.fields)
| _ -> ()
in
Hashtbl.iter
(fun n (u : Tast.structure) ->
List.iter
(fun (f : Tast.field) -> walk n (n ^ "'s member " ^ f.Tast.fname) f.Tast.fty)
u.Tast.fields)
env.unions
(* ── Declarations: pass 2, check bodies ────────────────────────────── *)
(* The names a body hands back or stores into: every name mentioned in a value
it answers — its last form's tails, a [return]'s value — and the name at
the root of every [set] place. A parameter among them is not warned at for
growing: the grown copy goes back to the caller, or the copy is the
function's own business. *)
let escaping_names ~returns (body : Ast.expr list) : string list =
let names = ref [] in
let rec mentions (e : Ast.expr) =
(match e.Ast.e with Ast.Var n -> names := n :: !names | _ -> ());
ignore (Ast.map_children (fun x -> mentions x; x) e)
in
let rec tails (e : Ast.expr) =
match e.Ast.e with
| Ast.Do es | Ast.Let (_, es) ->
(match List.rev es with x :: _ -> tails x | [] -> ())
| Ast.If (_, a, b) -> tails a; Option.iter tails b
| Ast.IfLet (_, a, b) ->
(match List.rev a.Ast.body with x :: _ -> tails x | [] -> ());
Option.iter tails b
| Ast.Chain (_, _, b) -> tails b
| Ast.Match (_, arms) ->
List.iter
(fun (a : Ast.arm) ->
match List.rev a.Ast.body with x :: _ -> tails x | [] -> ())
arms
(* A value that is the parameter, a field of it, or a literal built with
it. A call's result is its callee's business, and a unit form — the
push itself, last in a function that returns nothing — answers
nothing. *)
| Ast.Var _ | Ast.Field _ | Ast.Struct _ | Ast.Bare _ | Ast.Arr _
| Ast.MapLit _ -> mentions e
| _ -> ()
in
let rec root (e : Ast.expr) =
match e.Ast.e with
| Ast.Var n -> names := n :: !names
| Ast.Field (x, _) -> root x
| Ast.Call ({ Ast.e = Ast.Var ("at" | "deref"); _ }, x :: _) -> root x
| _ -> ()
in
let rec walk (e : Ast.expr) =
(match e.Ast.e with
| Ast.Return (Some x) -> tails x
| Ast.Set (Ast.Pvar n, _) -> names := n :: !names
| Ast.Set ((Ast.Pfield (x, _) | Ast.Pindex (x, _) | Ast.Pderef x
| Ast.Pslot (x, _)), _) -> root x
| _ -> ());
ignore (Ast.map_children (fun x -> walk x; x) e)
in
List.iter walk body;
if returns then (match List.rev body with x :: _ -> tails x | [] -> ());
!names
let rec check_fn ?sign env (fn : Ast.fn) : Tast.fn =
let params, ret =
match sign with Some s -> s | None -> Hashtbl.find env.fns fn.Ast.name
in
(* A [_] body whose type could not be read stands in [fns] as Never (see
[infer_returns]); pass two reads it the same way again, so its errors
are reported here, once, with every other body's. *)
let ret =
match sign, fn.Ast.ret with
| None, Some { Ast.t = Ast.Tinfer; _ }
when Hashtbl.mem env.infer_failed fn.Ast.name ->
infer_ret
| _ -> ret
in
let ctx = { (invented_ctx env ret) with owner = fn.Ast.name } in
let was_unnarrowable = !unnarrowable in
unnarrowable := unnarrowable_in fn.Ast.fbody;
Fun.protect ~finally:(fun () -> unnarrowable := was_unnarrowable) @@ fun () ->
List.iter2
(fun (p : Ast.field) ty ->
if List.mem_assoc p.Ast.fname ctx.scope then begin
let first =
List.find_opt
(fun (q : Ast.field) -> q.Ast.fname = p.Ast.fname)
fn.Ast.params
in
let notes =
match first with
| Some q when q != p ->
[ Loc.note q.Ast.floc ("the first " ^ p.Ast.fname ^ " is here") ]
| _ -> []
in
Loc.failk "check/duplicate-parameter" p.Ast.floc ~notes
"%s has two parameters named %s" fn.Ast.name p.Ast.fname
end;
ignore (bind ctx p.Ast.fname ty ~assignable:false))
fn.Ast.params params;
let grow_before = !grow_warnings in
let grow_saved = !grow_params in
grow_params :=
( ctx,
List.filter_map
(fun (p : Ast.field) ->
Option.map (fun b -> (b.slot, p)) (List.assoc_opt p.Ast.fname ctx.scope))
fn.Ast.params )
:: grow_saved;
let escaping =
lazy
(let names =
escaping_names ~returns:(not (Types.equal ret Types.Unit)) fn.Ast.fbody
in
List.filter_map
(fun (p : Ast.field) ->
if List.mem p.Ast.fname names then Some p.Ast.floc else None)
fn.Ast.params)
in
Fun.protect
~finally:(fun () ->
grow_params := grow_saved;
let added =
List.filteri
(fun i _ -> i < List.length !grow_warnings - List.length grow_before)
!grow_warnings
in
if added <> [] then
grow_warnings :=
List.filter
(fun (d : Loc.diag) ->
not (List.mem d.Loc.dloc (Lazy.force escaping)))
added
@ grow_before)
@@ fun () ->
let body =
match fn.Ast.fbody with
| [] ->
if Types.equal ret Types.Unit || ret == infer_ret then []
else fail fn.Ast.nloc "%s returns %s but has no body" (written_name fn.Ast.name)
(tyname fn.Ast.nloc ret)
| body ->
(* The last form is the return value, unless the function returns Unit,
in which case whatever it evaluates to is discarded. *)
let want =
if Types.equal ret Types.Unit || ret == infer_ret then None
else Some ret
in
(* Every form here is at the top level of the function body, so every one
of them may carry a [defer] — and so may a form inside a [let] written
here, which is what [ctx.defer_ok] carries down. [check] registers it
and yields [unit]; the permission is granted again before each form
because [check] withdraws it as it starts.
A trailing [defer] is still a [defer] and not the return value, so the
expectation is not put to it: it would only ever report [Unit] against
the declared return type, which names the wrong problem. *)
let is_defer (e : Ast.expr) =
match e.Ast.e with Ast.Defer _ -> true | _ -> false
in
(* A dyn function whose value-giving form gives none — a [while], a
[set] — reaches [box]'s unit refusal, which can only say that () is
not a dyn value. Here the function is known, so the refusal is
restated as what went wrong with it. Only a refusal at the body's
own tail is: one deeper in the last form (a unit argument to a dyn
parameter) is about that argument and keeps its own message. *)
let rec tail_locs (e : Ast.expr) =
e.Ast.loc
:: (match e.Ast.e with
| Ast.Do es | Ast.Let (_, es) ->
(match List.rev es with x :: _ -> tail_locs x | [] -> [])
| Ast.If (_, a, b) ->
tail_locs a @ (match b with Some b -> tail_locs b | None -> [])
| _ -> [])
in
let restate_unit last (d : Loc.diag) =
let at (l : Loc.t) =
l.Loc.file = d.Loc.dloc.Loc.file && l.Loc.line = d.Loc.dloc.Loc.line
&& l.Loc.col = d.Loc.dloc.Loc.col
in
if d.Loc.kind = "check/dyn-unit" && Types.equal ret Types.Dyn
&& List.exists at (tail_locs last)
then
Loc.diag ~kind:"check/dyn-unit" d.Loc.dloc
(Printf.sprintf
"%s is declared to return dyn, but the last form of its body \
gives no value. End the body with the value to return (nil \
for none), or declare %s to return nothing: %s"
fn.Ast.name fn.Ast.name
(if fln_source d.Loc.dloc then
Printf.sprintf "fn %s(...) -> ()" fn.Ast.name
else Printf.sprintf "(defn %s [...] () ...)" fn.Ast.name))
else d
in
(* The refusal arrives either raised or, under recovery, recorded on
[env.recovered] while checking goes on; both are restated. *)
let check_last last =
let env = ctx.env in
let before = env.recovered in
let r =
try check ctx ?want last
with Loc.Error d -> Loc.raise_diag (restate_unit last d)
in
let rec fresh = function
| l when l == before -> l
| d :: rest -> restate_unit last d :: fresh rest
| [] -> []
in
env.recovered <- fresh env.recovered;
r
in
let rec go = function
| [ last ] ->
ctx.defer_ok <- true;
[ (if is_defer last then check ctx last else check_last last) ]
| x :: rest ->
ctx.defer_ok <- true;
let x = check ctx x in
x :: go rest
| [] -> assert false
in
go body
in
(* Function exit runs the defers, innermost first. An explicit [return] ran
its own (see [check]); this is the fall-off-the-end path. A trap does not
run them — it is [noreturn] and then [unreachable] — and that is the same
rule the bounds checks already follow. *)
let body =
let ends_never =
match List.rev body with
| (last : Tast.expr) :: _ -> Types.equal last.Tast.ty Types.Never
| [] -> false
in
match ctx.defers with
| [] -> body
(* A body read only for its type is thrown away after. *)
| _ when ret == infer_ret -> body
(* A body that never falls off the end — its last form a [return], say —
has no fall-off path to put the defers on, and a copy of them there is
code after a terminator. *)
| _ when ends_never -> body
| ds when Types.equal ret Types.Unit -> body @ ds
| ds ->
(* The result is computed before the defers run and returned after, so it
goes through a slot rather than staying the last form. *)
let rec split = function
| [ last ] -> ([], last)
| x :: rest -> let (init, last) = split rest in (x :: init, last)
| [] -> assert false
in
let init, last = split body in
let s = fresh_slot ctx ret in
let loc = last.Tast.loc in
init @ [ mk loc ret
(Tast.Let ([ (s, last) ], ds @ [ mk loc ret (Tast.Local s) ])) ]
in
(* The counter's zero, at the top of the body and above every store to it.
Nothing else in the function reads the slot, so this is the whole of its
cost on the path that never transfers. *)
let body =
match ctx.defer_slot with
| None -> body
| Some s -> defer_counter_zero s fn.Ast.nloc :: body
in
let checked =
{ Tast.name = fn.Ast.name; params;
slots = Array.of_list (List.rev ctx.slot_tys);
snames = Array.of_list (List.rev ctx.slot_names); as_slots = ctx.as_slots;
(* The same defers again, for the transfer exit path §5 describes. The
normal path has them spliced into [body] above; this one is guarded on
the count, because a transfer can start above a defer that the text has
not reached and cleanup over an unwritten binding is not cleanup. *)
ret; body;
fdefers =
(match ctx.defer_slot with
| None -> ctx.defers
| Some s -> guarded_defers s ctx.defers);
fenv = None; fparent = None; floc = fn.Ast.nloc }
in
let checked =
if sign = None && ret == infer_ret then
(* Pass two over a [_] body pass one could not read: its errors are
raised above; a body that checks has the refusal pass one made
about its exits, or waited on one that did, and stands as Never. *)
match Hashtbl.find_opt env.infer_failed fn.Ast.name with
| Some (Some d) -> raise (Loc.Error d)
| _ -> { checked with Tast.ret = Types.Never }
else checked
in
refuse_frame_escapes checked;
checked
(* The generic body, checked once with its variables abstract. Nothing is kept
— the [Tast.fn] it produces is thrown away, and so is anything it lifted —
because a generic function has no code: only its instantiations do. What is
kept is the *refusal*: an operator an unconstrained variable does not
support fails here, at the definition, naming the variable, rather than at
whichever call site happened to instantiate it at a type that worked.
The holes in it are real and are the report's business: [println] is
plan.org's one compiler-provided exception and this pass rejects it. *)
and check_generic env (fn : Ast.fn) =
let vars, params, ret = Hashtbl.find env.gsigs fn.Ast.name in
let saved_lifted = env.lifted and saved_vars = env.tyvars
and saved_preds = env.tvpreds and saved_lens = env.lenvars
and saved_ph = env.len_placeholder in
(* The body sees a length variable's array at [abstract_len], an ordinary
array every array operation already answers for; the signature keeps
its [Types.LArray] for call sites to bind against. *)
let rec at_placeholder (t : Types.t) =
match t with
| Types.LArray (_, e) -> Types.Array (abstract_len, at_placeholder e)
| Types.Slice (m, e) -> Types.Slice (m, at_placeholder e)
| Types.Array (n, e) -> Types.Array (n, at_placeholder e)
| Types.Ptr (m, e) -> Types.Ptr (m, at_placeholder e)
| Types.Vec e -> Types.Vec (at_placeholder e)
| Types.Option e -> Types.Option (at_placeholder e)
| Types.Map (k, v) -> Types.Map (at_placeholder k, at_placeholder v)
| Types.Fn (ps, r) -> Types.Fn (List.map at_placeholder ps, at_placeholder r)
| Types.CFn (ps, r) ->
Types.CFn (List.map at_placeholder ps, at_placeholder r)
| t -> t
in
let params = List.map at_placeholder params and ret = at_placeholder ret in
env.tyvars <- vars;
env.lenvars <-
Option.value (Hashtbl.find_opt env.glens fn.Ast.name) ~default:[];
env.len_placeholder <- true;
(* What the abstract pass may assume. Every operator the body reaches asks
[env.tvpreds] whether the variable was declared to support it, and every
instantiation asks the concrete type the same question again. *)
env.tvpreds <- fn.Ast.fwhere;
jreplace env.fns fn.Ast.name (params, ret);
let finish () =
jremove env.fns fn.Ast.name;
env.lifted <- saved_lifted;
env.tyvars <- saved_vars;
env.tvpreds <- saved_preds;
env.lenvars <- saved_lens;
env.len_placeholder <- saved_ph
in
(match check_fn env fn with
| _ -> finish ()
| exception e -> finish (); raise e)
(* ── Return types read off the body ([_]) ────────────────────────────
Between the two passes: every written signature is in [fns], and a [defn]
whose return slot is [_] gets its entry here, from its own body and
nothing else — never a call site (docs/SPIKE-INFERENCE.md, "The cheap
first step"). Its parameters are written, so the body checks exactly as it
would with the type written, and the Tast is thrown away: pass two checks
the body again against the type found, which is what keeps literals and
[return]s coerced the way a written signature would coerce them.
One such body may call another, so the bodies are read callees first and
then to a fixpoint, the untyped-[defconst] loop's shape. A cycle among
them never settles and is refused by name; a body that calls its own name
is the cycle of one. *)
(* The type one body gives, and the form that decided it. The exits — the
last form and every [return], less what never arrives — meet at
[arm_join], the function an [if]'s arms meet at, in any order: the typed
ones decide and the lone literals take their type; literals alone meet at
the wider of their own types. The body is then checked against that type
exactly as a written one would be, so whatever [if] refuses between its
arms is refused between exits, in the same words. None gives (); no value
beside a value is refused, since () does not take a value's place. *)
and read_return env (fn : Ast.fn) params =
(* One check of the body against [ret], thrown away with everything it
wrote into [env] ([snapshot_env]); pass two checks it again for real. *)
let attempt ret =
let undo, _ = snapshot_env env in
let seen = !infer_seen in
infer_seen := [];
let restore () = undo (); infer_seen := seen in
match speculate env (fun () -> check_fn ~sign:(params, ret) env fn) with
| exception e -> restore (); raise e
| tf ->
let returns = List.rev !infer_seen in
restore ();
(tf, returns)
in
let tf, returns = attempt infer_ret in
let last =
match List.rev tf.Tast.body, List.rev fn.Ast.fbody with
| (x : Tast.expr) :: _, (a : Ast.expr) :: _ ->
[ (x.Tast.ty, x.Tast.loc, adapts a) ]
| (x : Tast.expr) :: _, [] -> [ (x.Tast.ty, x.Tast.loc, false) ]
| [], _ -> []
in
let arrive =
List.filter (fun (t, _, _) -> not (Types.equal t Types.Never)) (returns @ last)
in
let unit (t, _, _) = Types.equal t Types.Unit in
(match List.find_opt unit arrive, List.find_opt (fun x -> not (unit x)) arrive with
| Some (_, bare, _), Some (t, valued, _) ->
Loc.failk "check/infer-mixed" bare
~notes:[ Loc.note valued ("this gives " ^ tyname valued t) ]
"%s gives no value here and %s on another path, and its return \
type is read off its body. Give this path a value too, or write \
the return type"
fn.Ast.name (tyname bare t)
| _ -> ());
match arrive with
| [] -> (Types.Unit, fn.Ast.nloc)
| (t, l, _) :: rest
when not (List.exists (fun (u, _, _) -> not (Types.equal t u)) rest) ->
(t, l)
| (t0, l0, _) :: _ ->
(* Where no join exists the first typed exit's type is the one checked
against, so the refusal is the one [if] gives its else arm. *)
let meet ?(join = arm_join) = function
| [] -> None
| ((t, l, _) :: _) as xs ->
let j =
List.fold_left
(fun acc (u, _, _) -> Option.bind acc (fun a -> join a u))
(Some t) xs
in
let at =
match j with
| Some j ->
(match List.find_opt (fun (u, _, _) -> Types.equal u j) xs with
| Some (_, l', _) -> l'
| None -> l)
| None -> l
in
Some (Option.value j ~default:t, at)
in
let decided =
match meet (List.filter (fun (_, _, lit) -> not lit) arrive) with
| Some d -> d
| None ->
(* Every exit a literal: they meet as two literals do. *)
Option.value (meet ~join:literal_meet arrive) ~default:(t0, l0)
in
ignore (attempt (fst decided));
decided
and infer_returns ~keep_going ?tolerate ?(previous = fun _ -> None) env
(decls : Ast.decl list) =
let pending =
List.filter_map
(fun (d : Ast.decl) ->
match d.Ast.d with
| Ast.Defn ({ Ast.ret = Some { Ast.t = Ast.Tinfer; _ }; _ } as fn)
when not (Hashtbl.mem env.gsigs fn.Ast.name) ->
Some fn
| _ -> None)
decls
in
if pending <> [] then begin
let names = List.map (fun (fn : Ast.fn) -> fn.Ast.name) pending in
(* The other pending names each body mentions, with where. A local of
the same name is counted too, which only matters once the fixpoint
has stalled on a real error, and then only to pick which to show. *)
let calls (fn : Ast.fn) =
let acc = ref [] in
List.iter (Load.expr_uses acc) fn.Ast.fbody;
List.filter (fun (n, _) -> List.mem n names) (List.rev !acc)
in
let deps = List.map (fun (fn : Ast.fn) -> (fn.Ast.name, calls fn)) pending in
let byname = List.map (fun (fn : Ast.fn) -> (fn.Ast.name, fn)) pending in
(* Callees first, so a program with no cycle settles in one round. *)
let order =
let visited = Hashtbl.create 16 and out = ref [] in
let rec visit n =
if not (Hashtbl.mem visited n) then begin
Hashtbl.replace visited n ();
List.iter (fun (m, _) -> visit m) (List.assoc n deps);
out := List.assoc n byname :: !out
end
in
List.iter visit names;
List.rev !out
in
let params_of (fn : Ast.fn) =
List.map (fun (p : Ast.field) -> resolve env p.Ast.fty) fn.Ast.params
in
let settle (fn : Ast.fn) =
let params = params_of fn in
let ret, cause = read_return env fn params in
Hashtbl.replace env.fns fn.Ast.name (params, ret);
Hashtbl.replace env.inferred fn.Ast.name cause
in
(* A body [tolerate] excuses keeps the signature it was compiled with,
[previous]'s, the way pass two keeps its compiled body: it is a stale
caller, not a change. *)
(* A body that fails for its own reasons is left to pass two, which
reports its errors with every other body's. Until then it stands as
Never, which fits anywhere, so its callers are not refused for its
sake; a [_] body that calls it cannot be read either, and waits the
same way. *)
let failed = env.infer_failed in
let fail_quietly ?refusal (fn : Ast.fn) =
Hashtbl.replace failed fn.Ast.name refusal;
Hashtbl.replace env.fns fn.Ast.name (params_of fn, Types.Never)
in
(* Only while every error is collected: a check that stops at the first
reports this body's own, here. *)
let excused (fn : Ast.fn) =
match settle fn with
| () -> ()
| exception (Loc.Error d as e) ->
(match tolerate, previous fn.Ast.name with
| Some ok, Some (params, ret) when ok env fn.Ast.name d ->
Hashtbl.replace env.fns fn.Ast.name (params, ret)
| _ -> if keep_going then fail_quietly ~refusal:d fn else raise e)
in
let rec rounds left =
let still =
List.filter
(fun (fn : Ast.fn) ->
if List.exists (fun (m, _) -> Hashtbl.mem failed m)
(List.assoc fn.Ast.name deps)
then begin fail_quietly fn; false end
else
match settle fn with
| () -> false
| exception Loc.Error _ -> true)
left
in
if still <> [] && List.length still < List.length left then rounds still
else still
in
(* A loop of [_] bodies that give no value on any way out — a
countdown that calls itself — is (): each is read with the others
taken as (), and kept only when every one of them gives () back. *)
let units stuck =
List.iter
(fun (fn : Ast.fn) ->
Hashtbl.replace env.fns fn.Ast.name (params_of fn, Types.Unit))
stuck;
let all_unit =
List.for_all
(fun (fn : Ast.fn) ->
match read_return env fn (params_of fn) with
| t, cause when Types.equal t Types.Unit ->
Hashtbl.replace env.inferred fn.Ast.name cause; true
| _ -> false
| exception Loc.Error _ -> false)
stuck
in
if not all_unit then
List.iter
(fun (fn : Ast.fn) ->
Hashtbl.remove env.fns fn.Ast.name;
Hashtbl.remove env.inferred fn.Ast.name)
stuck;
all_unit
in
let rec stalled left =
let stuck = rounds left in
if stuck <> [] then refuse stuck
and refuse stuck =
let stuck_names = List.map (fun (fn : Ast.fn) -> fn.Ast.name) stuck in
let waits n =
List.filter (fun (m, _) -> List.mem m stuck_names) (List.assoc n deps)
in
(* One that waits on nothing else stuck failed on its own body: check
it again, unswallowed, and its own error is the report. *)
(match List.find_opt (fun n -> waits n = []) stuck_names with
| Some n ->
excused (List.assoc n byname);
stalled (List.filter (fun (fn : Ast.fn) -> fn.Ast.name <> n) stuck)
| None ->
(* Every one waits on another, so following the first wait from
any of them comes back round: that loop is the cycle. *)
let rec walk path n =
if List.mem n path then
let rec from = function
| m :: rest when m = n -> m :: rest
| _ :: rest -> from rest
| [] -> []
in
from (List.rev path)
else walk (n :: path) (fst (List.hd (waits n)))
in
let cycle = walk [] (List.hd stuck_names) in
(* Told from the one written first, which is where a reader of the
file meets the loop. *)
let cycle =
let index n =
let rec at i = function
| [] -> max_int
| m :: rest -> if m = n then i else at (i + 1) rest
in
at 0 names
in
let start =
List.fold_left (fun a n -> if index n < index a then n else a)
(List.hd cycle) cycle
in
let rec rot = function
| m :: rest when m <> start -> rot (rest @ [ m ])
| l -> l
in
rot cycle
in
if units (List.map (fun n -> List.assoc n byname) cycle) then
stalled
(List.filter
(fun (fn : Ast.fn) -> not (List.mem fn.Ast.name cycle)) stuck)
else
let first = List.hd cycle in
let fn = List.assoc first byname in
let next i = List.nth cycle ((i + 1) mod List.length cycle) in
let notes =
List.mapi
(fun i n ->
let callee = next i in
let at = List.assoc callee (waits n) in
Loc.note at
(if n = callee then n ^ " calls itself here"
else Printf.sprintf "%s calls %s here" n callee))
cycle
in
(* Collected like any refusal when every error is: the loop's
first member carries it into pass two, the rest stand quietly. *)
match
(match cycle with
| [ n ] ->
Loc.failk "check/infer-recursive" fn.Ast.nloc ~notes
"%s calls itself, so its return type cannot be read off its \
body. Write the return type in its signature" n
| _ ->
Loc.failk "check/infer-recursive" fn.Ast.nloc ~notes
"%s call each other (%s), so %s of their return types can \
be read off their bodies. Write the return type of one of \
them in its signature"
(String.concat " and " cycle)
(String.concat " → " (cycle @ [ first ]))
(if List.length cycle = 2 then "neither" else "none"))
with
| () -> ()
| exception (Loc.Error d as e) ->
if not keep_going then raise e;
List.iter
(fun n ->
fail_quietly
?refusal:(if n = first then Some d else None)
(List.assoc n byname))
cycle;
stalled
(List.filter
(fun (fn : Ast.fn) -> not (List.mem fn.Ast.name cycle)) stuck))
in
stalled order
end
(* The knot from [instantiate]: a call site makes a copy, and making one is
checking a function. *)
let () = check_fn_ref := check_fn
(* A container's only compile-time *constant* is the zeroed one: a Vec's or a
Map's real value exists at run time, behind an allocator. That fact has not
changed. What changed is what follows from it.
It used to follow that a container global could only ever start zeroed, and
the argument was that there was nowhere for a computed initialiser to run:
[Emit.const] said "there is no init-at-startup path, by design", and the
backend that did run initialisers at startup ran them out of .init_array,
which a reload module deliberately has none of. A rule that held on one
backend and not the other would not be a rule, so the language refused the
form on both.
There is an init-at-startup path now, and it is the same one on both
backends: a computed initialiser is lifted into a function of its own and
the program calls it from [main], after the runtime is up and before any of
the program's own code runs. So the premise is gone and the refusal goes
with it. (defonce g (Vec u8) (slurp "level.edn")) is an ordinary program now,
and it is the shape the author kept writing.
What is still refused is [uninit] on one, and that is a different rule with
a reason of its own: a Vec's garbage pointer is not a garbage number. Every
operation on it dereferences a block address nobody wrote, where a zeroed
Vec is a real empty Vec — null block, zero length, zero capacity — and is
the value a program would have written anyway.
What a runtime-loaded global is for has not changed either. The data is
loaded once and it outlives main: a main that returns and is entered again
finds the global exactly as it left it, because nothing between the two runs
touches it — and that now includes a reload, which does not re-run
initialisers. Assigning a second time overwrites the first block and leaks
it — there is no [drop] and no cross-function flow analysis that could see
the second assignment, so that is the manual-memory answer and the
language's own: free is a thing you write. *)
let rec zero_only (t : Types.t) =
match t with
| Types.Vec _ | Types.Map _ -> true
| Types.Option e | Types.Array (_, e) -> zero_only e
| _ -> false
let container_global_init loc n (ty : Types.t) (init : Ast.init) =
if zero_only ty then
match init with
| Ast.Uninit ->
fail loc
"the global %s is %s, and uninit on one is refused. Write (defonce \
%s %s) with no initialiser — a zeroed %s is an empty one"
n (tyname loc ty) n (tyname loc ty) (tyname loc ty)
| _ -> ()
(* A container global has to be a [defonce]. A [defconst] is not an assignable
place — [check_place] refuses one by name — and a container's only constant
is the zeroed one, so a constant Vec could only ever hold the empty value
it was declared with: nothing could ever put the file's bytes in it.
Refused here, where the fix is one keyword, rather than at the (set ...)
that discovers it three forms later. *)
let no_container_defconst loc n (ty : Types.t) =
if zero_only ty then
fail loc
"the global %s is %s, and a %s global is a defonce, not a defconst — a \
defconst would stay the empty %s it was declared as. Write (defonce %s \
%s) and fill it in a function"
n (tyname loc ty) (tyname loc ty) (tyname loc ty)
n (tyname loc ty)
(* A union member written into a *constant* would have to be encoded into the
blob at link time, which is the byte-level encoder a data type case does not
have either: a constant is what the linker writes, and a union value is a
store. Refused here, where the message can name the way through, rather than
at the emitter as "this one is computed", which is true and says nothing.
A defonce is no longer any of this and no longer asks. Its computed
initialiser is lifted into a function that runs at startup, so the member is
written by exactly the store that writes one anywhere else — the encoder was
only ever needed because there was nothing to run.
A zeroed union needs none of this either and is the ordinary declaration. *)
let no_union_const env loc n (v : Tast.expr) =
match v.Tast.ty, v.Tast.e with
(* The all-bytes-zero value is a constant and needs none of this, so it is
the one initialiser that goes through — which is what makes (U {}) and a
declaration with no value the same thing here as everywhere else. *)
| _, (Tast.Zero _ | Tast.Uninit _) -> ()
| Types.Named un, _ when Hashtbl.mem env.unions un ->
fail loc
"the constant %s is the union %s, and a union member cannot be written \
into a constant. Leave it zeroed, or make it a defonce"
n un
| _ -> ()
(* A defconst's value is what the linker writes into the program's image, so it
has to be a value the linker can write: a literal, a zero, an aggregate of
those — [Tast.const_init]'s set, which is [Emit.const]'s accepted set asked
as a question. The integer arithmetic a defconst is allowed to be written as
is already gone by here: [collect]'s folding pass turned [(/ screen-height
cell-size)] into its answer before any type resolved, so what arrives here
is an [Int] and passes.
The author's rule, 2026-09-20: "defconst should not be computed, it's the
equivalent to a compiler const." Refused here rather than in a backend
because a backend can only refuse the program it is asked to emit, and the
two were not asking the same question — [Emit.const] refused a computed
defconst by name while the x86 backend ran it through the startup function
behind an [.init~once.] flag, like a defonce. One refusal in the checker is
the same program refused the same way on both, and it is the only place
that can say what to do instead.
A data type case says it with its own message, which is the one thing a
reader could not work out from "this is computed": the case would have to be
serialised into the payload blob, and that is an encoder rather than an
order of operations. It is searched for the way [Emit.const] used to find it
— down through the aggregates, because a case inside a struct literal is the
same unwritable value as a case on its own, and [(defconst g S (S {.u (U.B
{.x 1})}))] told about "computed" would be advice nobody could follow.
Left to right and stopping at the first value the image cannot hold, which
is [Emit.const]'s own order: it spelled the fields in order and failed at
the one it could not spell. So a computed field written before a case field
is still the general message, because that is the field a reader meets
first. *)
(* The first subexpression a constant image has no value for, descending
through the aggregates whose parts are themselves constants. [None] is a
value the linker can write, which is [Tast.const_init] arrived at from the
other side — the two walk the same nodes, and this one keeps the offender
rather than the verdict. *)
let rec unwritable (e : Tast.expr) =
match e.Tast.e with
| Tast.Int _ | Tast.Float _ | Tast.Bool _ | Tast.Str _ | Tast.Unit
| Tast.Zero _ | Tast.Uninit _ | Tast.None_ -> None
| Tast.Make (_, es) | Tast.Arr es -> List.find_map unwritable es
| Tast.Some_ v -> unwritable v
| _ -> Some e
let const_defconst_init env loc n (v : Tast.expr) =
match unwritable v with
| None -> ()
| Some { Tast.e = Tast.MakeCase (dname, case, _); _ } ->
fail loc
"a constant cannot be %s.%s. Make it a defonce, or declare it zeroed, \
which is %s.%s"
dname case dname
(match Hashtbl.find_opt env.datas dname with
| Some { Tast.cases = c :: _; _ } -> c.Tast.vname
| _ -> "its first case")
| Some _ ->
fail loc
"a constant's value must be a compile-time constant — the constant %s \
is computed. A defonce may have a computed initialiser, because it runs \
at startup and stores the result; a defconst is what the linker writes \
into the image and has nowhere to run. Write (defonce %s ...), or give \
the constant a literal — integer constants may also be written as \
arithmetic over literals and other constants, which is folded here"
n n
(* A global's initialiser runs at startup: from [main], after the runtime is
up, before a line of the program's own code. Nothing has established a
handler or a restart by then, and nothing outside the initialiser can — the
program has not started.
That used to be the reason all four of the condition forms were refused
inside one, and the refusal lived in [x86.ml] because that was the only
backend with an init-at-startup path. Its argument was that a transfer out
of an initialiser would return into the loader, which was true of a
.init_array constructor and is not true of a call from [main]. So the rule
is narrower now, and what is left of it is what is still true.
A [handler-bind] or a [restart-case] *inside* an initialiser is ordinary
code: it pushes its frames, runs, and pops them, all before the initialiser
returns, and nothing it does is visible outside. Both backends run it
exactly as they run it in any other function — which is what makes (defonce
data (Vec u8) (slurp "level.edn")) an ordinary program, since [slurp] is a
restart-case with its own signal inside it, and that is the shape the author
kept reaching for.
What is refused is a [signal] or an [invoke-restart] with no condition
machinery around it at all. Everywhere else in the language those two are
answered by a frame some *caller* established; in an initialiser there is no
caller, so one with nothing around it is inert by construction — a signal
nothing can hear, or an invoke-restart that can only fail at the invoke
site. Either form counts as enclosure, including a restart-case around a
signal: that pair is [slurp], and a restart-case says what the initialiser
wants to happen when nobody answers, which is the thing an unenclosed one
cannot say.
A clause's body is not in this walk at all — a handler-bind clause is lifted
into a function of its own — so the [invoke-restart] a handler writes is
never the one refused here. *)
let no_transfer_in_init n (v : Tast.expr) =
(* The nodes that are under a handler or a restart within this initialiser,
by identity: [Tast.walk] visits nodes rather than paths, so the enclosure
is recorded in one pass and asked in the next. *)
let covered = ref [] in
Tast.walk
(fun (e : Tast.expr) ->
let cover body = List.iter (Tast.walk (fun x -> covered := x :: !covered)) body in
match e.Tast.e with
| Tast.Handled (_, body) -> cover body
| Tast.RestartCase (cs, body) ->
cover [ body ];
List.iter (fun (c : Tast.rclause) -> cover c.Tast.rbody) cs
| _ -> ())
v;
Tast.walk
(fun (e : Tast.expr) ->
let bad what can =
fail e.Tast.loc
"%s in the initialiser of the global %s, with no handler-bind or \
restart-case around it, can only %s. Write one inside the \
initialiser, or move the whole thing into a function"
what n can
in
if List.memq e !covered then ()
else
match e.Tast.e with
| Tast.Signal _ -> bad "signal" "go unheard"
| Tast.InvokeRestart _ -> bad "invoke-restart" "fail at the invoke site"
| _ -> ())
v
(* A computed initialiser, lifted into a function of its own that returns the
value. The global's initialiser becomes the call, which is the whole of what
the backends had to learn: one of them already lowers an initialiser as
ordinary code and now lowers a call, and the other emits the global zeroed
and stores the call's result at startup.
A function rather than the expression left in place, for a reason that is
not tidiness: an initialiser can contain a [let], and a [let] needs a frame.
The slots were allocated on a context this function discarded, so what the
backend got was a slot index into a frame of size zero — [(defonce c i64 (let
[x (i64 5)] (+ x 1)))] crashed the x86 backend with an out-of-bounds index,
and there was no frame to give it without inventing one. This is that frame,
and it is the one every other body already has.
[fparent] is the global rather than a function, which is a small widening of
what the field means: nobody wrote this name, so completing it or jumping to
it is meaningless, and the one reader that asks — [Dev]'s [defs] — wants
exactly that answer. The others are unaffected: a whole-program build emits
a cell for every function it emits, and a redefinition module reaches this
one through neither, because a reload does not run initialisers. *)
let lift_ginit ctx loc n ty (v : Tast.expr) =
no_transfer_in_init n v;
let fname = "global/" ^ n in
ctx.env.lifted <-
{ Tast.name = fname; params = [];
slots = Array.of_list (List.rev ctx.slot_tys);
snames = Array.of_list (List.rev ctx.slot_names); as_slots = ctx.as_slots;
(* An initialiser is a nested form as far as [defer_ok] is concerned, so
nothing can register one here and both of these are empty. Written the
same way [check_fn] writes them anyway, so that the day the rule
widens this does not quietly become the one exit path that runs a
defer nobody reached. *)
ret = ty;
body =
(match ctx.defer_slot with
| None -> [ v ]
| Some s -> [ defer_counter_zero s loc; v ]);
fdefers =
(match ctx.defer_slot with
| None -> ctx.defers
| Some s -> guarded_defers s ctx.defers);
fenv = None; fparent = Some n; floc = loc }
:: ctx.env.lifted;
{ Tast.e = Tast.Call (fname, []); ty; loc }
let check_global env (d : Ast.decl) : Tast.global option =
let ctx () = invented_ctx env Types.Unit in
match d.Ast.d with
| Ast.Defvar (n, _, init, kind) ->
let ty, _ = Hashtbl.find env.globals n in
no_zeroed_fn d.Ast.dloc (Printf.sprintf "the global %s" n) ty;
container_global_init d.Ast.dloc n ty init;
(* A [def]'s initialiser is lifted into [global/<n>] whatever it is — a
zero, a literal, a computed expression — where a [defonce]'s is lifted
only when it is computed. The lifting is what makes the form's promise
reachable: the host's startup function calls the initialiser through
its function cell, so a re-evaluated [def] swaps the cell and the next
re-run stores the *edited* value. A constant left inline would be
baked into the host's startup body, and every re-run would paint the
stale value back. The same lifted function is what [Session.eval]'s
store thunk calls to assign the new value straight away, which is the
other half of what [defparameter] means. [uninit] is the one exception
on both forms: there is nothing to run, so there is nothing to
lift. *)
let lift_always = (match kind with Ast.Once -> false | Ast.Every -> true) in
let ginit =
match init with
| Ast.Zeroed when lift_always ->
let c = ctx () in
lift_ginit c d.Ast.dloc n ty { Tast.e = Tast.Zero ty; ty; loc = d.Ast.dloc }
| Ast.Zeroed -> { Tast.e = Tast.Zero ty; ty; loc = d.Ast.dloc }
| Ast.Uninit ->
(* Everywhere else [uninit] is an opt-out from ZII and the bytes are
whatever they were: a garbage f64 is a garbage number. A data type is
the one type where that is qualitatively worse — the tag steers
control flow, a tag no case names falls past every comparison in a
[match], and the block after them is [unreachable], which LLVM is
entitled to assume cannot happen. So the one place where garbage
becomes "the optimiser may do anything" is refused by name, and the
zeroed form, which is the first declared case, is named beside it. *)
(match ty with
| Types.Named un when Hashtbl.mem env.datas un ->
fail d.Ast.dloc
"%s is a data type, and uninit on one is refused. Drop the \
uninit — a zeroed %s is %s"
(tyname d.Ast.dloc ty) un
(match Hashtbl.find_opt env.datas un with
| Some { Tast.cases = c :: _; _ } -> un ^ "." ^ c.Tast.vname
| _ -> "its first case")
| _ -> ());
{ Tast.e = Tast.Uninit ty; ty; loc = d.Ast.dloc }
| Ast.Init v ->
let c = ctx () in
(match v.Ast.e with
| Ast.UInt (_, s) when ty = Types.Dyn ->
Loc.failk literal_at_want v.Ast.loc "%s. Give %s the type u64"
(wide_at_dyn s) n
| _ -> ());
let v =
view_global_init := Some (n, kind);
Fun.protect ~finally:(fun () -> view_global_init := None)
(fun () -> check c ~want:ty v)
in
if Tast.const_init v && not lift_always then v
else lift_ginit c d.Ast.dloc n ty v
(* [settle_defvars] turned every one of these into a [Zeroed] or an
[Init] during [collect], and this pass runs over the list that pass
handed back. One arriving here is a driver that checked a global
without collecting first. *)
| Ast.Ambiguous _ ->
fail d.Ast.dloc
"internal: the third element of (%s %s ...) was never decided"
(match kind with Ast.Once -> "defonce" | Ast.Every -> "def") n
in
Some { Tast.gname = n; gty = ty; ginit; gconst = false; gfolded = false;
grerun = (match kind with Ast.Once -> false | Ast.Every -> true) }
| Ast.Defconst (n, _, v) ->
let ty, _ = Hashtbl.find env.globals n in
no_zeroed_fn d.Ast.dloc (Printf.sprintf "the global %s" n) ty;
no_container_defconst d.Ast.dloc n ty;
(* [collect] already folded the integer constants, because an array length
has to be known before any type resolves. Use that value here rather
than the expression it came from: a global's initialiser has to be a
compile-time constant, and [(/ screen-height cell-size)] is one — the
folding pass is the only thing that knows it. *)
(* A folded conversion is still a value of the type it converts to. *)
(match v.Ast.e, ty with
| Ast.Call ({ Ast.e = Ast.Var c; _ }, [ _ ]), Types.Int kind
when (match Types.ikind_of_name c with
| Some k ->
k <> kind
&& not (Types.widens_to ~from:(Types.Int k) ~into:(Types.Int kind))
| None -> false) ->
fail v.Ast.loc "expected %s, found %s" (Types.ikind_name kind) c
| _ -> ());
let ginit =
match Hashtbl.find_opt env.consts n, ty with
| Some k, Types.Int kind ->
(* Still range-checked: this path skips [check], and [in_range] is the
only thing that rejects 300 as a u8. *)
{ Tast.e =
Tast.Int
(in_range
~pattern:(match v.Ast.e with Ast.Int _ -> false | _ -> true)
v.Ast.loc kind k, kind); ty;
loc = d.Ast.dloc }
| _ -> with_typed_literals (fun () -> check (ctx ()) ~want:ty v)
in
no_union_const env d.Ast.dloc n ginit;
(* After the union's own refusal, so a computed union member keeps the
message that names its way through rather than the general one. *)
const_defconst_init env d.Ast.dloc n ginit;
(* [env.consts] holds exactly the constants the folding pass consumed, so
membership is the question "is this value in the program's shape?" *)
Some { Tast.gname = n; gty = ty; ginit; gconst = true;
gfolded = Hashtbl.mem env.consts n; grerun = false }
| _ -> None
(* The entry point, plan.org: (defn main [args [string]] i32), with both the
parameter and the return type optional. *)
(* Where [main] was written. [env.locs] is the table of where each *type* was
declared — [collect] fills it for structs, data types, unions and enums and
for nothing else — so a function's own location is not in it and the
[find_opt] idiom the rest of this file uses does not apply here. The
declaration list does have it, and the caller is holding the list anyway.
Without this both refusals below opened with <unknown>:0:0, which tells a
reader that a rule exists and not where they broke it, and gives
[next-error] nothing to jump to. A [main] that arrived some other way — a
[declare], say — still has no [defn] to point at, so that case keeps the
unknown span rather than inventing one. *)
let main_loc (decls : Ast.decl list) =
let is_main (d : Ast.decl) =
match d.Ast.d with Ast.Defn fn -> fn.Ast.name = "main" | _ -> false
in
match List.find_opt is_main decls with
| Some { Ast.d = Ast.Defn fn; _ } -> fn.Ast.nloc
| _ -> Loc.unknown
let check_main env decls =
let at = main_loc decls in
match Hashtbl.find_opt env.fns "main" with
| None -> () (* a library, or a file being checked on its own *)
| Some (params, ret) ->
let ok_params =
match params with
| [] -> true
| [ Types.Slice (_, Types.String) ] -> true
| _ -> false
in
if not ok_params then
fail at
"main takes no parameters or one [str], not (%s)"
(String.concat (if Source.indented_at at then ", " else " ")
(List.map (tyname at) params));
if not (Types.equal ret Types.Unit || Types.equal ret (Types.Int Types.I32))
then
fail at "main returns i32 or nothing, not %s"
(tyname at ret)
(* The environment as well as the program. A session needs it to check an
expression typed at a REPL against the program the process is running — and
it has to be this one rather than anything rebuilt from declarations,
because [program] prepends the prelude and no accumulated AST contains it. *)
(* Separate entry points below rather than a flag on the one the session calls,
for the reason [Parse] gives at the same fork: [Loc.Errors] is a second
exception that the session and the daemon do not catch, so the guarantee
that they never see one should be structural and not a default argument. *)
(* ── The order the initialisers run in ─────────────────────────────── *)
(* Declaration order is the order a program's globals are started in, and it is
the wrong one as soon as one of them is computed from another: [(defonce b
i64 (+ a 10))] written above [(defonce a i64 (+ 1 2))] read a zero and
answered 10 without saying anything. So the computed ones are sorted
by what they need, which is what Odin does (src/checker.cpp,
[calculate_global_init_order]) and for the same reason — the alternative is
a rule about where in the file a global has to be written, which is a rule
about text rather than about meaning.
Only the computed globals are sorted, and only against each other. A
constant initialiser cannot read a global at all — [Tast.const_init]'s
accepted set has no [Global] in it — so a constant is already there before
anything runs: it is in the object image on one backend and written from
.init_array before main on the other. That makes the partition total and the
sort small, and it is why a global initialised from a [defconst] needs no
edge.
The dependency is transitive through calls, not just through what the
initialiser names: [(defonce a i64 (f))] where [f] reads [b] needs [b]
started first, and an analysis that only looked at the initialiser's own
text would order that pair by luck. Odin's graph is transitive for the same
reason.
A cycle is refused rather than broken. Some global in it would have to be
started from another's zero, and which one that is cannot be read off the
program — the two spellings of the same cycle would differ only in which
line the compiler happened to reach first. *)
let init_order (globals : Tast.global list) (fns : Tast.fn list) =
let computed =
List.filter
(fun (g : Tast.global) -> not (Tast.const_init g.Tast.ginit))
globals
in
if computed = [] then globals
else begin
let is_computed = Hashtbl.create 8 in
List.iter
(fun (g : Tast.global) -> Hashtbl.replace is_computed g.Tast.gname ())
computed;
let ftbl = Hashtbl.create 64 in
List.iter (fun (f : Tast.fn) -> Hashtbl.replace ftbl f.Tast.name f) fns;
(* What each function reads, to a fixpoint over the call graph: its own
references, plus everything its callees read. A round that changes
nothing is the answer, which needs no special case for a recursive
function and no visited set to get wrong. *)
let reads = Hashtbl.create 64 in
let calls = Hashtbl.create 64 in
let add tbl k v =
let cur = try Hashtbl.find tbl k with Not_found -> [] in
if not (List.mem v cur) then Hashtbl.replace tbl k (v :: cur)
in
List.iter
(fun (f : Tast.fn) ->
let note n =
if Hashtbl.mem is_computed n then add reads f.Tast.name n
else if Hashtbl.mem ftbl n then add calls f.Tast.name n
in
List.iter (Reach.expr_refs note) f.Tast.body;
List.iter (Reach.expr_refs note) f.Tast.fdefers)
fns;
let changed = ref true in
while !changed do
changed := false;
Hashtbl.iter
(fun caller callees ->
List.iter
(fun callee ->
List.iter
(fun g ->
let cur = try Hashtbl.find reads caller with Not_found -> [] in
if not (List.mem g cur) then begin
Hashtbl.replace reads caller (g :: cur);
changed := true
end)
(try Hashtbl.find reads callee with Not_found -> []))
callees)
(Hashtbl.copy calls)
done;
(* And what each computed global needs, which is the same walk over its
initialiser — whose one node is a call to the function the initialiser
was lifted into, so the answer is that function's reads. *)
let needs (g : Tast.global) =
let acc = ref [] in
let note n =
let put r = if not (List.mem r !acc) then acc := r :: !acc in
if Hashtbl.mem is_computed n then put n
else List.iter put (try Hashtbl.find reads n with Not_found -> [])
in
Reach.expr_refs note g.Tast.ginit;
if List.mem g.Tast.gname !acc then
fail g.Tast.ginit.Tast.loc
"the global %s is initialised from itself — leave it zeroed and \
load it in a function"
g.Tast.gname;
!acc
in
let deps = List.map (fun (g : Tast.global) -> (g.Tast.gname, needs g)) computed in
let deps_of n = try List.assoc n deps with Not_found -> [] in
(* Kahn's, in declaration order: of the globals that are ready, the one
written first goes first, so the emitted order is the source's wherever
the source's order was possible at all. *)
let done_ = Hashtbl.create 8 in
let order = ref [] in
let progress = ref true in
while !progress do
progress := false;
List.iter
(fun (g : Tast.global) ->
if not (Hashtbl.mem done_ g.Tast.gname)
&& List.for_all (fun d -> Hashtbl.mem done_ d) (deps_of g.Tast.gname)
then begin
Hashtbl.replace done_ g.Tast.gname ();
order := g :: !order;
progress := true
end)
computed
done;
(match
List.filter
(fun (g : Tast.global) -> not (Hashtbl.mem done_ g.Tast.gname))
computed
with
| [] -> ()
| (g : Tast.global) :: _ ->
(* The whole ring, not one name out of it. A cycle is a fact about a set
of globals and a message naming one of them leaves the reader to find
the rest; naming each edge says which read to break. *)
let stuck n = not (Hashtbl.mem done_ n) && List.mem_assoc n deps in
let rec ring path n =
if List.mem n path then
let rec cut = function
| [] -> []
| x :: r -> if String.equal x n then x :: r else cut r
in
cut path
else
match List.find_opt stuck (deps_of n) with
| None -> path @ [ n ]
| Some d -> ring (path @ [ n ]) d
in
let r = ring [] g.Tast.gname in
let edges =
List.mapi
(fun i n ->
Printf.sprintf "%s needs %s's value" n
(List.nth r ((i + 1) mod List.length r)))
r
in
fail g.Tast.ginit.Tast.loc
"the globals %s initialise each other: %s. Leave one zeroed and load \
it in a function"
(String.concat " and " r) (String.concat ", " edges));
(* The sorted sequence, dropped back into the slots the computed globals
already occupied. Everything else — a constant, a zeroed container —
stays exactly where it was declared, so a diff of the emitted image
shows the reordering and nothing else. *)
let seq = ref (List.rev !order) in
List.map
(fun (g : Tast.global) ->
if Hashtbl.mem is_computed g.Tast.gname then
match !seq with
| x :: rest -> seq := rest; x
| [] -> g
else g)
globals
end
(* ── What a per-type descriptor can reach ───────────────────────────────
*
* The struct dyn field is no longer refused: a type that holds dyn words at
* static offsets gets a descriptor naming those offsets, and every place a
* value of it can live — a frame slot, a global, a temporary a call answered
* with — goes on the collector's root stack with that descriptor beside it.
* runtime/flan_dyn.h's [flan_dyn_root_push_desc] is where the whole of that
* argument is written, including why no instance carries a header word.
*
* What a static offset cannot express is what is left, and it is refused here
* rather than emitted as a descriptor that quietly omits a field:
*
* - a dyn inside a typed container. A [(Vec S)] holds its elements in
* allocator memory of a length nothing static knows, so the dyn words of
* one are not a list of offsets. The M2 queue's item 3 is the
* descriptor that can say it — pointer, length and element type — and it is
* a different shape from this one, deliberately. A [(Map K V)] is the same
* fact twice over.
* - a dyn in a data type's payload. The cases overlay one another, so which
* words are dyn depends on the tag, which is a run-time question. The same
* goes for a C union's members.
* - a dyn inside an [(Option T)], whose payload exists only under the tag: the
* words of a [None] are zero and marking them is harmless, but that is a
* fact about how this compiler happens to build one and not something the
* type says, and a descriptor that relied on it would be relying on it
* silently.
*
* A [(Ptr S)] and a [[S]] are deliberately *not* on that list, and the reason
* is worth stating because it looks like an omission. Neither owns storage.
* The only storage this compiler hands out for a type that holds dyn is a
* frame slot, a global, or a fixed array inside one of those — and all three
* are rooted with their descriptor already, so a pointer or a slice into one
* addresses bytes the collector is marking. That is what makes a condition's
* payload work at all: a handler clause is lifted to a function taking a
* [(Ptr Cond)], and the value it points at is in the signalling frame with a
* descriptor beside it. Storage that came from C is the program's business the
* way every other pointer from C is.
*
* And one cap, which is not a representation question but an arithmetic one:
* the offsets of a fixed array are flattened one element at a time, so an
* array of a million structs would be a million-word table in .rodata. The
* repeat form that avoids it is item 3's machinery, so this says so instead of
* building half of it. *)
let desc_offsets_max = 4096
(* Is there a dyn in the storage a value of this type *is*, which is not the
same as whether its printed form mentions dyn anywhere. A pointer and a
slice are stopped at, because what they address is somebody else's storage
and is rooted where it was declared. That is the same line [hidden_dyn]
takes for a bare [(Ptr S)], and taking it here as well is what lets
[(Vec (Ptr Cond))] be written — a vector of pointers to condition structs
holds no dyn words of its own, and refusing it with a sentence about the dyn
inside it was wrong twice over. *)
let rec dyn_reach ~through p seen (t : Types.t) =
let go = dyn_reach ~through p seen in
match t with
| Types.Dyn -> true
| Types.Array (_, e) | Types.Vec e | Types.Option e -> go e
| Types.Map (k, v) -> go k || go v
| Types.Ptr (_, e) | Types.Slice (_, e) -> through && go e
| Types.Fn _ -> false
| Types.Named n when not (List.mem n seen) ->
let seen = n :: seen in
let field (fl : Tast.field) = dyn_reach ~through p seen fl.Tast.fty in
(match List.find_opt (fun (s : Tast.structure) -> s.Tast.sname = n)
p.Tast.structs with
| Some s -> List.exists field s.Tast.fields
| None ->
match List.find_opt (fun (u : Tast.data) -> u.Tast.dname = n)
p.Tast.datas with
| Some u ->
List.exists
(fun (c : Tast.variant) -> List.exists field c.Tast.vfields)
u.Tast.cases
| None ->
match List.find_opt (fun (u : Tast.structure) -> u.Tast.sname = n)
p.Tast.unions with
| Some u -> List.exists field u.Tast.fields
| None -> false)
| _ -> false
let dyn_anywhere p seen t = dyn_reach ~through:false p seen t
(* The other question, and it is a different one: is there a dyn reachable from
here *at all*, pointers and slices followed. Only the foreign boundary asks
it, and it has to — the storage on the far side of a pointer is rooted where
it was declared when the declaration was Flan's, and is rooted nowhere at
all when it was C's. Keeping the two apart is the whole of the fix: the
narrowing above is right for a Flan type, and reusing it at the boundary
made [(Ptr (Ptr S))] and [(Ptr [S])] answer no. *)
let dyn_through p seen t = dyn_reach ~through:true p seen t
(* Is a dyn reachable from here only by going through a pointer or a slice.
This is the parameter question. Flan supplies the storage one level down
from a foreign parameter — it passes the address of a place, and a place is
a frame slot, a global or an array inside one, all of them rooted with their
descriptor — so a [(Ptr S)] parameter is an ordinary borrow and stays
writable. What is *below* that level is C's, and a pointer or a slice found
there is a hop into storage nothing rooted. *)
let rec dyn_behind_pointer p seen (t : Types.t) =
let go = dyn_behind_pointer p seen in
match t with
(* The crossing, and the [seen] set does *not* travel across it. The two
walks ask different questions — this one does not count a direct dyn, the
one below it does — so a name already visited on the way here would be
pruned from a question it was never asked. That is not a nicety: a struct
with a pointer to itself and a dyn field is exactly the shape that hits
it, and [(defstruct Node [next (Ptr Node) x dyn])] at [(Ptr Node)] was
accepted while the same thing unrolled into two types was refused. C
hung a node off [next], put a dyn in it, and the collector freed it —
which is the one failure this whole boundary exists to prevent.
It still terminates. This walk's own [seen] guards its own [Named]
recursion, and each crossing starts a separate finite walk of its own. *)
| Types.Ptr (_, e) | Types.Slice (_, e) -> dyn_through p [] e
| Types.Array (_, e) | Types.Vec e | Types.Option e -> go e
| Types.Map (k, v) -> go k || go v
| Types.Dyn | Types.Fn _ -> false
| Types.Named n when not (List.mem n seen) ->
let seen = n :: seen in
let field (fl : Tast.field) = dyn_behind_pointer p seen fl.Tast.fty in
(match List.find_opt (fun (s : Tast.structure) -> s.Tast.sname = n)
p.Tast.structs with
| Some s -> List.exists field s.Tast.fields
| None ->
match List.find_opt (fun (u : Tast.data) -> u.Tast.dname = n)
p.Tast.datas with
| Some u ->
List.exists
(fun (c : Tast.variant) -> List.exists field c.Tast.vfields)
u.Tast.cases
| None ->
match List.find_opt (fun (u : Tast.structure) -> u.Tast.sname = n)
p.Tast.unions with
| Some u -> List.exists field u.Tast.fields
| None -> false)
| _ -> false
(* How many dyn words a descriptor for this type would name, which is what the
cap above is about. Only the by-value shapes contribute; the rest are
refused by [hidden_dyn] before this number matters. *)
(* Saturated at one past the cap, because the number only ever has to be
compared with it. That is not tidiness: [(defonce big [4611686018427387904
S])] is a length an [Int64.to_int] multiplication wraps *negative* on, so an
honest product made the test [n > desc_offsets_max] false, the declaration
was accepted, and the emitter then sat building the offset list until
something killed it. A refusal that overflows into an acceptance is worse
than no refusal. Every arm below stays at or under [desc_offsets_max + 1],
so nothing here can multiply two numbers large enough to wrap. *)
let sat n = if n > desc_offsets_max then desc_offsets_max + 1 else n
let rec dyn_words p seen (t : Types.t) =
match t with
| Types.Dyn -> 1
| Types.Array (n, e) ->
let w = dyn_words p seen e in
(* Out of range in either direction saturates. A negative length is
nonsense and the layout refuses it further down, but this arm has to
answer *something*, and the one thing it must not answer is a small
number: [Int64.to_int (-1L) * w] is negative, which reads as under the
cap and is how the overflow above got through in the first place. *)
if w = 0 then 0
else if Int64.compare n 0L < 0
|| Int64.compare n (Int64.of_int (desc_offsets_max + 1)) > 0 then
desc_offsets_max + 1
else sat (Int64.to_int n * w)
| Types.Named nm when not (List.mem nm seen) ->
(match List.find_opt (fun (s : Tast.structure) -> s.Tast.sname = nm)
p.Tast.structs with
| Some s ->
List.fold_left
(fun acc (fl : Tast.field) ->
sat (acc + dyn_words p (nm :: seen) fl.Tast.fty))
0 s.Tast.fields
| None -> 0)
| _ -> 0
(* The first place under this type where a dyn sits that no descriptor reaches,
as the type to name in the refusal. *)
let rec hidden_dyn p seen (t : Types.t) : Types.t option =
let under e = if dyn_anywhere p seen e then Some t else None in
match t with
| Types.Dyn -> None
| Types.Array (_, e) -> hidden_dyn p seen e
| Types.Vec e | Types.Option e -> under e
(* A Map's values are walked through the value type's own descriptor, so a
dyn there is found wherever that descriptor finds one. A key never holds
one: dyn is not a key type. *)
| Types.Map (k, v) ->
if dyn_anywhere p seen k then Some t else hidden_dyn p seen v
(* A pointer and a slice are views of storage something else roots; see the
note above. What they point at is checked where it is declared. *)
| Types.Ptr (_, e) | Types.Slice (_, e) -> hidden_dyn p seen e
| Types.Fn _ -> None
| Types.Named n when not (List.mem n seen) ->
let seen = n :: seen in
(match List.find_opt (fun (s : Tast.structure) -> s.Tast.sname = n)
p.Tast.structs with
| Some s ->
List.fold_left
(fun acc (fl : Tast.field) ->
match acc with
| Some _ -> acc
| None -> hidden_dyn p seen fl.Tast.fty)
None s.Tast.fields
| None ->
(* A data type's payload and a union's members both overlay, so any dyn
in one is hidden by the type itself and not by a member of it. *)
match List.find_opt (fun (u : Tast.data) -> u.Tast.dname = n)
p.Tast.datas with
| Some u ->
if List.exists
(fun (c : Tast.variant) ->
List.exists
(fun (fl : Tast.field) -> dyn_anywhere p seen fl.Tast.fty)
c.Tast.vfields)
u.Tast.cases
then Some t else None
| None ->
match List.find_opt (fun (u : Tast.structure) -> u.Tast.sname = n)
p.Tast.unions with
| Some u ->
if List.exists
(fun (fl : Tast.field) -> dyn_anywhere p seen fl.Tast.fty)
u.Tast.fields
then Some t else None
| None -> None)
| _ -> None
(* Every place a value can live: a global, a parameter, a return, a frame slot.
Two passes want exactly this list — the descriptor refusal below and the
[--no-gc] site collection at the bottom of the file — and each walked it
itself until the phrase naming an unnamed slot drifted between the two. One
walk now; the callers keep their own filters, which is where they really
differ.
[visit] is handed the location, the phrase naming the place, and the type.
[~slot] says whether this is a frame slot: that is the one distinction a
caller filters on, and a caller cannot recover it from the type. [?after_fn]
runs at the end of each function, before the next is begun, so that a caller
which also walks the body emits its diagnostics in the order a single loop
over [p.Tast.fns] gave them. *)
let value_sites (p : Tast.program) ?(after_fn = fun (_ : Tast.fn) -> ())
(visit : slot:bool -> _) =
List.iter
(fun (g : Tast.global) ->
visit ~slot:false g.Tast.ginit.Tast.loc
(Printf.sprintf "the global %s" g.Tast.gname) g.Tast.gty)
p.Tast.globals;
List.iter
(fun (fn : Tast.fn) ->
List.iteri
(fun i t ->
visit ~slot:false fn.Tast.floc
(Printf.sprintf "parameter %d of %s" (i + 1) fn.Tast.name) t)
fn.Tast.params;
visit ~slot:false fn.Tast.floc
(Printf.sprintf "the return type of %s" fn.Tast.name) fn.Tast.ret;
Array.iteri
(fun i t ->
(* A slot the program named is called by that name; one the checker
synthesised has none to give, and "a local" is the phrase both
passes now use for it. The preposition is [in] either way,
because a named slot has always read "%s in %s". *)
let named =
if i < Array.length fn.Tast.snames then fn.Tast.snames.(i)
else None
in
visit ~slot:true fn.Tast.floc
(Printf.sprintf "%s in %s"
(match named with Some n -> n | None -> "a local")
fn.Tast.name)
t)
fn.Tast.slots;
after_fn fn)
p.Tast.fns
(* Over the whole program rather than at each declaration, because the type
that hides a dyn may be declared after the one that names it — and because
a struct nobody ever holds a value of costs nothing either way. *)
(* Does a value of this type hold an (Fn ...) in its own storage — the
function values a collector-allocated environment may hang off. A
pointer and a slice are views of storage checked where it is declared. *)
let rec holds_fn p seen (t : Types.t) =
let go = holds_fn p seen in
match t with
| Types.Fn _ -> true
| Types.Array (_, e) | Types.Vec e | Types.Option e -> go e
| Types.Map (k, v) -> go k || go v
| Types.Named n when not (List.mem n seen) ->
let seen = n :: seen in
let field (fl : Tast.field) = holds_fn p seen fl.Tast.fty in
(match List.find_opt (fun (s : Tast.structure) -> s.Tast.sname = n)
p.Tast.structs with
| Some s -> List.exists field s.Tast.fields
| None ->
match List.find_opt (fun (u : Tast.data) -> u.Tast.dname = n)
p.Tast.datas with
| Some u ->
List.exists
(fun (c : Tast.variant) -> List.exists field c.Tast.vfields)
u.Tast.cases
| None ->
match List.find_opt (fun (u : Tast.structure) -> u.Tast.sname = n)
p.Tast.unions with
| Some u -> List.exists field u.Tast.fields
| None -> false)
| _ -> false
let dyn_descriptors (p : Tast.program) =
let check loc what (t : Types.t) =
(match hidden_dyn p [] t with
| Some at ->
Loc.failk "check/dyn-descriptor" loc
"%s is %s, and the dyn inside %s is one no descriptor can find. The \
collector marks a struct's dyn fields by their byte offsets, which \
%s does not have — its storage is not part of the value. Hold the \
dyn in a struct field, or wait for the typed container view"
what (tyname loc t) (tyname loc at) (tyname loc at)
| None -> ());
(* The count is saturated, so the message says more-than rather than a
figure the reader could check — which is the honest thing to print,
since the figure it would otherwise print is the one that wrapped. *)
if dyn_words p [] t > desc_offsets_max then
Loc.failk "check/dyn-descriptor" loc
"%s is %s, whose descriptor would name more than %d dyn words. The \
offsets of an array are flattened one element at a time, and %d is \
the most this compiler will write out — the repeat form that would \
avoid it arrives with the typed container view"
what (tyname loc t) desc_offsets_max desc_offsets_max
in
(* The foreign boundary, which is the one place the note above admits an
honest hole. Every slot, global and array this compiler hands out for a
type that holds dyn is rooted with its descriptor. Storage that came from
C is not: nothing pushed a root for it, nothing ever will, and a dyn word
sitting in it is a live value the collector cannot see and will free. A
bare dyn is already refused by name at this boundary for a different
reason — C has no way to ask what the word means — and this is the same
sentence at one remove.
The rule is about *ownership* and not about shape, so the two directions
are asked different questions and the answer to "is a (Ptr S) allowed" is
"which way is it going":
- A return, and anything reachable from it however many pointers deep, is
C's storage. [(Ptr S)], [(Ptr (Ptr S))] and [(Ptr [S])] are all refused,
and the last two are the ones a one-level check missed — following a
pointer is exactly what [dyn_anywhere] stops doing, which is right for
a Flan type and wrong here.
- A parameter's outermost level is Flan's. The compiler passes the address
of a *place*, and a place is a frame slot, a global or an array inside
one — rooted with its descriptor and marked for the whole call. So
[(Ptr S)] and [[S]] as parameters are ordinary borrows and stay
writable, which is what a read-only C inspector wants and what
shim.ml's own advice tells people to write. Below that level the
storage is C's again: a [(Ptr (Ptr S))] parameter is an out-parameter,
and what C writes into it is a pointer of C's own. *)
List.iter
(fun (e : Tast.extern) ->
let refuse what (t : Types.t) why =
Loc.failk "check/dyn-descriptor" e.Tast.eloc
"%s of %s (the C symbol %s) is %s, and a dyn is reachable through \
it. %s, so the collector cannot mark that word and will free what \
it names — pass the fields across at written types instead"
what e.Tast.ename e.Tast.esym (tyname e.Tast.eloc t) why
in
(* One level in, because that level is the compiler's own: what a
foreign parameter of pointer or slice type receives is the address
of a place. Below it the question is [dyn_behind_pointer]'s again. *)
let below (t : Types.t) =
match t with Types.Ptr (_, e) | Types.Slice (_, e) -> e | t -> t
in
List.iteri
(fun i t ->
if dyn_behind_pointer p [] (below t) then
refuse (Printf.sprintf "parameter %d" (i + 1)) t
"The outermost level is this compiler's own storage and is \
rooted, but what lies below it is C's and nothing rooted that")
e.Tast.eparams;
if dyn_through p [] e.Tast.eret then
refuse "the return type" e.Tast.eret
"What C hands back points at storage this compiler never rooted")
p.Tast.externs;
value_sites p (fun ~slot:_ loc what t -> check loc what t)
(* See lib/closures.ml. *)
let is_env_struct = Closures.is_env_struct
let heap_env = Closures.heap_env
let place_closures fns = Closures.place ~dev:false fns
(* A program's function named as a prelude function takes the name over, the
way a definition of a builtin's name does: every call written in the file
that defines it reaches the program's, and every call anywhere else — the
prelude's own among them, which were written against the prelude's
signature — keeps reaching the prelude's. The prelude's is renamed out of
the way, under a qualifier no source can spell, rather than dropped.
Functions only: a type or a global of the prelude's name is still defined
twice. *)
let prelude_alias = "prelude~"
(* Off for a check whose warnings were already printed for the same source:
the dev program re-creating the session its launcher built and warned for. *)
let print_warnings = ref true
(* Where a dev eval's own forms are, so its warnings are said for those and
not again, on every later eval, for everything the session holds. [None]
is a build or a check, which says everything. *)
let warn_within : Loc.t list option ref = ref None
(* The warnings [say_warnings] kept since [build_program] began, in order:
what a dev eval hands the editor in its reply as well as printing. *)
let said_warnings : Loc.diag list ref = ref []
let say_warnings (ds : Loc.diag list) =
let within (at : Loc.t) =
match !warn_within with
| None -> true
| Some spans ->
List.exists
(fun (s : Loc.t) ->
String.equal s.Loc.file at.Loc.file
&& s.Loc.line <= at.Loc.line && at.Loc.line <= max s.Loc.line s.Loc.eline)
spans
in
let ds = List.filter (fun (d : Loc.diag) -> within d.Loc.dloc) ds in
said_warnings := !said_warnings @ ds;
if !print_warnings then
List.iter
(fun (d : Loc.diag) ->
prerr_endline
(Loc.entry ~mark:'~' ~label:"warning: " d.Loc.dloc d.Loc.dmsg))
ds
(* A name the renaming above made, which nobody wrote: left out of every
listing a person reads, and shown as whose it is where a frame has to be. *)
let internal_name n = String.starts_with ~prefix:(prelude_alias ^ "/") n
let shown_name n =
let n = written_name n in
if internal_name n then
let p = String.length prelude_alias + 1 in
"the prelude's " ^ String.sub n p (String.length n - p)
else n
let shadow_prelude (prelude : Ast.decl list) (decls : Ast.decl list) =
(* A value name, and whether it is a function's. A program's global takes
a prelude function's name over as a program's function does: both are
names a call or a read reaches, and the prelude's own uses keep the
prelude's. *)
let value_name (d : Ast.decl) =
match d.Ast.d with
| Ast.Defn fn | Ast.Declare (fn, _) | Ast.DeclareC (fn, _) ->
Some (fn.Ast.name, true)
| Ast.Defvar (n, _, _, _) | Ast.Defconst (n, _, _) -> Some (n, false)
| _ -> None
in
let theirs = List.map fst (List.filter_map value_name prelude) in
let taken =
List.filter_map
(fun (d : Ast.decl) ->
match value_name d with
| Some (n, f) when List.mem n theirs -> Some (n, d.Ast.dloc, f)
| _ -> None)
decls
in
let warnings =
List.map
(fun (n, at, f) ->
Loc.diag ~kind:"check/shadows-prelude" at
(Printf.sprintf
"%s shadows the prelude's %s — every %s in this file now \
reaches your definition"
n n (if f then "call" else "use")))
taken
in
let taken = List.map (fun (n, at, _) -> (n, at)) taken in
let prelude, decls =
List.fold_left
(fun (prelude, decls) (n, (at : Loc.t)) ->
( List.map (Load.rename_refs [ n ] prelude_alias) prelude,
List.map
(fun (d : Ast.decl) ->
if String.equal d.Ast.dloc.Loc.file at.Loc.file then d
else Load.rename_refs [ n ] prelude_alias d)
decls ))
(prelude, decls) taken
in
(prelude @ decls, warnings)
(* ── A function that calls itself on every path (decision 142) ─────────
Rust's [unconditional_recursion], and like it only the obvious case, with
no false positives: from entry, every path reaches a direct call of the
same name at the same arity before anything can leave. So a branch (an
[if], [match], [if-let], [?.], [??]; [when] and [and]/[or] are [If] by
now), a loop body, a handler, a restart and a closure each end the search
rather than being looked into, and anything that may leave — [return],
[some]/[try], a signal, a restart, [break]/[continue] out of the fn, or a
call to a function whose type is [Never] — ends it before the call. A name
the body binds itself (a parameter, a [let]) is not the function, so a
body binding it is not looked at at all. Read over the AST after [collect],
where the parameters are paired, a fn's arities are renamed apart and
[infer_returns] has settled every callee's return type. *)
let recursion_warnings : Loc.diag list ref = ref []
let unconditional_recursion env (decls : Ast.decl list) : Loc.diag list =
let children (e : Ast.expr) =
let acc = ref [] in
ignore (Ast.map_children (fun c -> acc := c :: !acc; c) e);
!acc
in
let rec binds n (e : Ast.expr) =
let pat (a : Ast.arm) =
match a.Ast.pat with Ast.Pctor (_, ns) -> List.mem n ns | _ -> false
in
(match e.Ast.e with
| Ast.Let (bs, _) -> List.exists (fun (b : Ast.binding) -> b.Ast.bname = n) bs
| Ast.Loop (bs, _) -> List.mem_assoc n bs
| Ast.Fn (ps, _) -> List.mem n ps
| Ast.Dotimes (_, v, _, _) | Ast.Chain (v, _, _) -> v = n
| Ast.Match (_, arms) -> List.exists pat arms
| Ast.IfLet (_, a, _) -> pat a
| Ast.HandlerBind (cs, _) | Ast.HandlerCase (_, cs) ->
List.exists (fun (c : Ast.hclause) -> c.Ast.hname = n) cs
| Ast.RestartCase (_, cs) ->
List.exists
(fun (c : Ast.rclause) ->
List.exists (fun (p : Ast.field) -> p.Ast.fname = n) c.Ast.rparams)
cs
| _ -> false)
|| List.exists (binds n) (children e)
in
let never n nargs =
let ret n =
match Hashtbl.find_opt env.fns n with
| Some (_, r) -> Some r
| None ->
Option.map (fun (_, _, r) -> r) (Hashtbl.find_opt env.gsigs n)
in
let r =
match Hashtbl.find_opt env.versions n with
| Some vs -> Option.bind (List.assoc_opt nargs vs) ret
| None -> ret n
in
r = Some Types.Never
|| ((n = "exit" || n = builtin_prefix ^ "exit") && r = None)
in
(* Whether [e] may leave the function, or reach a [break]/[continue] of a
loop outside it. [loops] counts the loops of [e] itself around the node. *)
let rec leaves loops (e : Ast.expr) =
match e.Ast.e with
| Ast.Fn _ | Ast.Defer _ -> false
| Ast.Return _ | Ast.Unwrap _ | Ast.Signal _ | Ast.InvokeRestart _
| Ast.Recur _ -> true
| Ast.Break l | Ast.Continue l -> loops = 0 || l <> None
| Ast.Call ({ Ast.e = Ast.Var n; _ }, args) when never n (List.length args) ->
true
| Ast.While _ | Ast.Loop _ | Ast.Dotimes _ ->
List.exists (leaves (loops + 1)) (children e)
| _ -> List.exists (leaves loops) (children e)
in
let check_fn (d : Ast.decl) (fn : Ast.fn) =
let name, arity =
match version_of fn.Ast.name with
| Some (b, k) when Hashtbl.mem env.versions b -> (b, k)
| _ -> (fn.Ast.name, List.length fn.Ast.params)
in
(* The self-call every path reaches first, if there is one. *)
let rec reaches (e : Ast.expr) =
match e.Ast.e with
| Ast.Call ({ Ast.e = Ast.Var n; _ }, args)
when n = name && List.length args = arity ->
(match seq args with
| Some _ as c -> c
| None -> if List.exists (leaves 0) args then None else Some e.Ast.loc)
(* [??] evaluates its fallbacks only when what comes before is empty,
so only its first operand is certain to run. Every other builtin
evaluates all of its arguments. *)
| Ast.Call ({ Ast.e = Ast.Var "??"; _ }, a :: _) -> seq [ a ]
| Ast.Call (f, args) -> seq (f :: args)
| Ast.Return (Some x) -> seq [ x ]
| Ast.Do es -> seq es
| Ast.Let (bs, es) -> seq (List.map (fun (b : Ast.binding) -> b.Ast.bval) bs @ es)
| Ast.If (c, _, _) | Ast.Match (c, _) | Ast.IfLet (c, _, _)
| Ast.Chain (_, c, _) -> seq [ c ]
| Ast.Field (x, _) | Ast.The (_, x) | Ast.Unwrap (_, x) | Ast.Signal (_, x)
| Ast.Narrow (_, x) | Ast.Alias (_, x) -> seq [ x ]
| Ast.Set (Ast.Pvar _, v) -> seq [ v ]
| Ast.Struct (_, fs) | Ast.Bare fs -> seq (List.map snd fs)
| Ast.Arr es -> seq es
| _ -> None
and seq = function
| [] -> None
(* A loop that may never end: what follows it may never run. *)
| { Ast.e = Ast.While (_, { Ast.e = Ast.Var "true"; _ }, _) | Ast.Loop _; _ } :: _ ->
None
| e :: rest ->
(match reaches e with
| Some _ as c -> c
| None -> if leaves 0 e then None else seq rest)
in
if String.equal d.Ast.dloc.Loc.file Prelude.file
|| List.exists (fun (p : Ast.field) -> p.Ast.fname = name) fn.Ast.params
|| List.exists (binds name) fn.Ast.fbody
then None
else
Option.map
(fun (at : Loc.t) ->
let fln = fln_source at in
Loc.diag ~kind:"check/unconditional-recursion" at
(Printf.sprintf
"%s cannot return without first calling itself on line %d, \
so it never returns. If that call was meant to come after %s, it is %s \
by mistake; otherwise %s needs a base case, a path that \
returns without calling itself"
name at.Loc.line
(if fln then "the function" else "the defn")
(if fln then "indented into the body" else "inside the defn's body")
name))
(seq fn.Ast.fbody)
in
List.filter_map
(fun (d : Ast.decl) ->
match d.Ast.d with Ast.Defn fn -> check_fn d fn | _ -> None)
decls
let build_program ~keep_going ?tolerate ?previous (decls : Ast.decl list) :
Tast.program * env * string list =
let env = new_env () in
said_warnings := [];
Hashtbl.reset arm_failed;
(* ── A declaration left as it was compiled ───────────────────────────
A dev session installs a function whose signature changed, and a
caller compiled against the old one is still in the running program and
still in the declaration list. Its unchanged source may no longer check
against the new signature — but it is not being recompiled, the process
is running the body it was built with, and that body is what the break
loop has to describe if the call stops. So [tolerate] may answer yes for
a declaration whose body fails here, and the declaration is left out of
the program rather than refused; the session puts the checked body it
already had in its place. Everything the failed check registered on its
way down — a lifted clause, a generic copy — is taken back, so nothing
half-checked reaches the backend.
Only a body. A signature is collected in pass one and nothing here
excuses it, and with no [tolerate] this is the compiler it always was. *)
let tolerated = ref [] in
let tolerant name f =
match tolerate with
| None -> f ()
| Some ok ->
(* Everything the failed body wrote into [env] goes with it
([snapshot_env]): a copy it asked for would otherwise stay cached,
and the next body asking for it would be handed the name of a copy
the program does not have. *)
let undo, keep = snapshot_env env in
(match f () with
| x -> keep (); x
| exception ((Loc.Error d | Loc.Errors (d :: _)) as e) ->
if ok env name d then begin
undo ();
tolerated := name :: !tolerated;
None
end
else (keep (); raise e)
| exception e -> keep (); raise e)
in
let decls, prelude_warnings =
shadow_prelude (Parse.program (Prelude.forms ())) decls
in
(* Before anything is collected: every (declare-c ...) becomes an ordinary
flattened [declare] with a Flan [defn] over it, and the C that does the
flattening comes back to be compiled into the build. Nothing below this
line knows the form exists. *)
List.iter (fun (n, t) -> Hashtbl.replace env.tracks n t) (Shim.resources decls);
let decls, cshim = Shim.expand decls in
(* And on the same line: every class and generic function becomes the
[defn]s it stands for. It runs over the whole list because a method may
be written anywhere in it, which is also what makes a reload rebuild
every dispatch from the session's declarations — see lib/classes.ml. *)
let decls = Classes.expand decls in
(* And with the declaration list in its final shape — the classes expanded,
the shims flattened, the imports already qualified by [Load] — the one
warning this compiler prints unasked. Here rather than in [bin/main.ml]
beside [print_memory_warnings] because every route into the compiler
passes through this function: build, check, run, and the dev daemon's
reload, which is where a defn is most likely to be written. Printed in
the shape [Loc] gives an error, so a checker in an editor parses it the
same way. *)
say_warnings (shadowed_builtins decls @ prelude_warnings);
(* Pass one, and it stops at the first thing it refuses. That is not
laziness: every name, type and signature in the file comes from here, so a
declaration this pass could not make sense of leaves a hole that pass two
would report once per mention. A wrong signature is one error; the thirty
"unknown name" lines under it are not errors, they are the same one.
Pass two is where the volume is, and it is where collecting pays. By the
time it runs every signature is sound, so a body that fails to check
cannot make the next body fail — which is what makes a declaration a
resync point that needs no resynchronising. *)
if keep_going then env.deferred <- Some [];
grow_warnings := [];
let decls = collect env decls in
say_warnings (List.rev !pairing_warnings);
check_finite env;
check_union_members env;
infer_returns ~keep_going ?tolerate ?previous env decls;
recursion_warnings := unconditional_recursion env decls;
say_warnings !recursion_warnings;
(let late = !consts_after_infer in
consts_after_infer := [];
settle_consts env late);
let s = Loc.sink ~on:keep_going in
(match env.deferred with
| Some ds -> s.Loc.found <- ds; env.deferred <- None
| None -> ());
ignore (Loc.caught s (fun () -> check_main env decls));
(* Every generic body, checked once with its variables left abstract, and
the result thrown away. This is the pass plan.org's rule needs and Odin
has no equivalent of: Odin checks a polymorphic body only per
instantiation, so [a + b] over a [$T] compiles there and fails only if
nobody ever calls it at a numeric type. plan.org says the opposite — an
unconstrained variable supports only what every type supports, and [=],
[<], [+] and [hash] over one are *rejected, not silently instantiated*.
Rejecting them means type-checking the body with nothing substituted,
which is this, and it is a second pass over the same source. *)
List.iter
(fun (d : Ast.decl) ->
match d.Ast.d with
| Ast.Defn fn when Hashtbl.mem env.gsigs fn.Ast.name ->
(match
Loc.caught s (fun () ->
tolerant fn.Ast.name (fun () ->
with_recovery env ~on:keep_going (fun () ->
Some (check_generic env fn))))
with
| None -> Hashtbl.replace env.refused_generics fn.Ast.name ()
| Some _ -> ())
| _ -> ())
decls;
let globals =
List.filter_map
(fun (d : Ast.decl) ->
Option.join
(Loc.caught s (fun () ->
let checked () =
with_recovery env ~on:keep_going (fun () -> check_global env d)
in
match Ast.declared_name d with
| Some n -> tolerant n checked
| None -> checked ())))
decls
in
let fns =
List.filter_map
(fun (d : Ast.decl) ->
match d.Ast.d with
(* A generic [defn] does not reach the typed IR at all. Only its
instantiations do, and they are collected below. *)
| Ast.Defn fn when Hashtbl.mem env.gsigs fn.Ast.name -> None
| Ast.Defn fn ->
Option.join
(Loc.caught s (fun () ->
tolerant fn.Ast.name (fun () ->
with_recovery env ~on:keep_going (fun () ->
Some (check_fn env fn)))))
| _ -> None)
decls
in
say_warnings (List.rev !grow_warnings);
Loc.finish s;
(* The placeholder a [_] body is read against is never a type anything
downstream may see; a signature carrying it would be emitted as a
struct named _. *)
List.iter
(fun (f : Tast.fn) ->
if f.Tast.ret == infer_ret
|| List.exists (fun t -> t == infer_ret) f.Tast.params
then
fail f.Tast.floc "internal: %s left the checker with its return \
type unread" f.Tast.name)
fns;
(* The handler clauses lifted out along the way. They are ordinary functions
from here down; nothing in the backend knows they were written inside
something else. *)
let fns = fns @ List.rev env.lifted in
(* The copies generics turned into, in the order they were generated. Like a
lifted clause they are ordinary functions from here down — but unlike one
they are reached *by name* from arbitrary call sites, so they carry no
[fparent] and a dev build gives each its own cell. *)
let fns = fns @ List.rev env.instances in
(* Which capturing fns outlive their frame; see [place_closures]. *)
let fns = place_closures fns in
(* And the order the computed initialisers run in, which needs the whole
function list: what a global reads is transitive through what it calls. *)
let globals = init_order globals fns in
(* Sorted, so the emitted IR is reproducible build to build: a Hashtbl's
fold order is not. *)
let values name tbl =
Hashtbl.fold (fun _ v acc -> v :: acc) tbl []
|> List.sort (fun a b -> String.compare (name a) (name b))
in
let externs =
Hashtbl.fold
(fun name esym acc ->
let eparams, eret = Hashtbl.find env.fns name in
let eloc =
match Hashtbl.find_opt env.extern_locs name with
| Some l -> l
| None -> Loc.unknown
in
{ Tast.ename = name; esym; eparams; eret; eloc } :: acc)
env.externs []
|> List.sort (fun (a : Tast.extern) b -> String.compare a.Tast.esym b.Tast.esym)
in
let p =
{ Tast.structs =
(* A struct copy at variables was only ever for an abstract pass. *)
List.filter
(fun (s : Tast.structure) ->
Hashtbl.find_opt env.copies s.Tast.sname <> Some true)
(values (fun (s : Tast.structure) -> s.Tast.sname) env.structs);
datas = values (fun (u : Tast.data) -> u.Tast.dname) env.datas;
unions = values (fun (u : Tast.structure) -> u.Tast.sname) env.unions;
globals; externs; fns; cshim }
in
(* Last, over the finished program: which dyn words a per-type descriptor can
reach and which it cannot. It needs every declaration in hand, which is
what makes it a pass here rather than a check at each one. *)
dyn_descriptors p;
(p, env, List.rev !tolerated)
(** The program and the environment, stopping at the first refusal. What a
session needs, and it raises [Loc.Error] and never [Loc.Errors]. *)
let program_with_env (decls : Ast.decl list) : Tast.program * env =
let p, env, _ = build_program ~keep_going:false decls in
(p, env)
(** The same, with [tolerate] deciding which body failures leave a
declaration out rather than refuse it — see [build_program]. The names
left out come back beside the program; nothing else about it changes. *)
let program_tolerant ?(keep_going = false) ~tolerate ?previous
(decls : Ast.decl list) =
build_program ~keep_going ~tolerate ?previous decls
let program (decls : Ast.decl list) : Tast.program =
let p, _, _ = build_program ~keep_going:false decls in
p
(** The same, reporting every declaration whose body it refuses rather than the
first. Raises [Loc.Errors], so only a caller prepared for a list should be
calling it. *)
let program_all (decls : Ast.decl list) : Tast.program =
let p, _, _ = build_program ~keep_going:true decls in
p
(* ── What a session needs to know about instantiations ──────────────────
A generic [defn] never reaches [Tast.fns] — only its copies do — so the
editor's [C-c C-c], which installs the bodies named by the form it was
sent, would install nothing at all for a generic. These are what
[Session.eval] expands the name with. They are here rather than there
because [env]'s tables are the only record that a symbol was ever generic:
past this module an instantiation is an ordinary function and nothing knows
it was written once. *)
(* Is this name a generic definition rather than an ordinary one? *)
let is_generic env n = Hashtbl.mem env.gsigs n
(* Every copy of [gname] this check produced, by symbol. Transitivity needs no
walk: a whole-program check has already generated every copy every call
site asked for, including the ones a generic pulled in by calling another
generic at its own variable. *)
let instantiations env gname =
match Hashtbl.find_opt env.insts gname with
| None -> []
| Some l -> List.rev_map (fun (_, _, sym) -> sym) !l
(* The generic a symbol came from, and the types it was asked for — [None] for
an ordinary function. What a refusal about [sort-i32] needs in order to
say which line the programmer should look at, since [sort-i32] appears
nowhere in the source. *)
let instantiation_origin env sym =
Hashtbl.fold
(fun gname l acc ->
match acc with
| Some _ -> acc
| None ->
(match List.find_opt (fun (_, _, s) -> String.equal s sym) !l with
| Some (ps, _, _) -> Some (gname, ps)
| None -> None))
env.insts None
(* Checking one expression against a live session can *generate* a copy: the
first [C-x C-e] of [(id 3)] instantiates [id] at [i32] and the copy is in
[env.instances] and in no program anywhere. Without these two the module
that gets built calls a symbol it never defined. A mark before and the
difference after is the whole protocol. *)
let instance_mark env = List.length env.instances
let instances_since env mark =
let fresh = List.length env.instances - mark in
List.rev
(List.filteri (fun i _ -> i < fresh) env.instances)
(* The same protocol for a body an expression lifted — an [fn] literal or a
handler clause — and the environment struct each one captured into. Both
are in [env] and in no program, and a module that calls one or lays one
out needs them. *)
let lifted_mark env = List.length env.lifted
let lifted_since env mark =
let fresh = List.length env.lifted - mark in
List.rev (List.filteri (fun i _ -> i < fresh) env.lifted)
(* The struct copies this env made that [have] does not hold: what an
expression checked against a running session named for the first time —
[(Pair 1 2)] typed at a REPL makes [(Pair i32)] — which the module built
for it has to lay out, and the session has to keep. *)
let fresh_copies env (have : Tast.structure list) =
Hashtbl.fold
(fun k at_vars acc ->
if at_vars
|| List.exists (fun (s : Tast.structure) -> String.equal s.Tast.sname k)
have
then acc
else
match Hashtbl.find_opt env.structs k with
| Some s -> s :: acc
| None -> acc)
env.copies []
|> List.sort (fun (a : Tast.structure) b -> String.compare a.Tast.sname b.Tast.sname)
let env_structs env (fns : Tast.fn list) =
List.filter_map
(fun (f : Tast.fn) -> Hashtbl.find_opt env.structs ("env/" ^ f.Tast.name))
fns
(* Expressions checked against a program that is already running, all of them
into *one* frame. It is empty to start with — a REPL expression has no
parameters and no enclosing function — so the slots it ends up with are
whatever their own [let]s allocate.
One frame and not one each, which is what the inspector's write verb needs
and what it must not assemble by hand. Two expressions checked separately
both number their slots from zero, so splicing them into one thunk would
have the second one's [let] reading and writing the first one's storage — a
frame that is two frames wearing one frame's clothes. Sharing the [ctx] is
the whole of the fix, and it is a fix because there is exactly one allocator
of slot indices in this compiler and it is this record's counter.
They are otherwise independent: none of them binds a name for the next,
because the list is a list of values being stored and not a sequence.
[want] is the write verb too, and [C-x C-e] passes none: a store into a
slot of type [f32] has an expectation to offer and a typed expression does
not. The whole value of passing it is that [3] arrives as an [f32] rather
than as an [i32] the store would then have to be refused for. It flows
through [check] the way an expectation flows anywhere — that is what
bidirectional means — and [expect] at the end catches the arms that ignore
it, so the refusal is the checker's own "expected f32, found string" rather
than a second sentence written here that would drift from it. *)
let expressions env (es : (Types.t option * Ast.expr) list) :
Tast.expr list * Types.t array * string option array =
let ctx = invented_ctx env Types.Unit in
(* Folded rather than mapped, because [List.map]'s order is unspecified and
every one of these calls has a side effect on [ctx] — the slot counter it
shares. An order nobody chose is one that can differ between builds, and
two frames laid out differently for the same edit is the kind of thing
that is found by somebody else, much later. *)
let ts =
List.rev
(List.fold_left
(fun acc (want, (e : Ast.expr)) ->
expect ctx e.Ast.loc ~want (check ctx ?want e) :: acc)
[] es)
in
(ts, Array.of_list (List.rev ctx.slot_tys),
Array.of_list (List.rev ctx.slot_names))
(* One expression checked with [scope]'s names already bound, in order, so a
later entry shadows an earlier one of the same name: evaluating in a stopped
frame, whose locals the expression may name. Each is bound to a slot of the
expression's own frame, and which slot is answered beside the name, so the
caller can point every use of it at the stopped frame's storage instead
([Tast.rewrite_locals]). *)
let expression_in_scope env ~(scope : (string * Types.t * bool * bool) list)
(e : Ast.expr) :
Tast.expr * Types.t array * string option array * (string * int) list =
let ctx = invented_ctx env Types.Unit in
let bound =
List.map
(fun (name, ty, assignable, by_as) ->
let what = if by_as then Some as_tag else None in
(name, bind ctx ?what name ty ~assignable:(assignable && not by_as)))
scope
in
let t = expect ctx e.Ast.loc ~want:None (check ctx e) in
(t, Array.of_list (List.rev ctx.slot_tys),
Array.of_list (List.rev ctx.slot_names), bound)
(* The one-expression case, which is every caller but the write verb. *)
let expression env ?want (e : Ast.expr) :
Tast.expr * Types.t array * string option array =
match expressions env [ (want, e) ] with
| [ t ], tys, names -> (t, tys, names)
| _ -> assert false
(* ── --no-gc ────────────────────────────────────────────────────────────
The flag that says this program is to be compiled with no collector in it,
and the way to keep that promise is to refuse every dyn rather than to emit
a different program. A dyn value is a value the runtime allocates and the
collector owns; there is no smaller version of it to fall back to, and
quietly leaking instead would be a memory model nobody asked for.
So this is a pass and not a flag. It runs between [Check] and [Emit], it
answers unit or it refuses, and nothing downstream of it is told the flag
exists — which is what makes a fully annotated program's output byte for
byte identical with the flag and without it. Emit has no [no_gc] field to
branch on, and that is deliberate: a field would be one more thing that
could change a comment, a name or an ordering, and the identity is worth
more than the branch would ever buy.
Every site is named, the way the global cycle refusal names the whole ring
rather than one member of it. A reader who has to annotate their program
wants the list, not the first one and then another compile. *)
let dyn_sites (p : Tast.program) : Loc.diag list =
let found = ref [] in
let add loc what = found := (loc, `Dyn what) :: !found in
(* A type that *holds* a dyn and not only the type [dyn] itself. A struct
with a dyn field is a collected value as much as a bare one is, and since
the per-type descriptors it is a value a program can have without any
expression in it ever having the type [dyn] — a zeroed one, never filled,
whose dyn word the collector is still asked to mark. *)
let holds t = dyn_anywhere p [] t in
value_sites p
(* The filter this pass has and the descriptor pass does not: a frame slot
whose type is bare [dyn] is passed over here. A parameter or a global of
that type is still named. *)
(fun ~slot loc what t ->
if holds t && not (slot && t = Types.Dyn) then add loc what)
~after_fn:(fun (fn : Tast.fn) ->
(* The body's own dyn values, which are the ones a signature does not
show: a let bound to a boxed literal, a (vec-new dyn) deep inside an
expression. Reported at the node, because that is the character to
change. *)
List.iter
(Tast.walk
(fun (e : Tast.expr) ->
match e.Tast.e with
| Tast.Prim (Tast.Rt sym, _)
when e.Tast.ty = Types.Dyn
&& String.length sym > 8
&& String.sub sym 0 8 = "flan_dyn" ->
add e.Tast.loc (Printf.sprintf "this value in %s" fn.Tast.name)
(* A capturing fn: its environment is a collector allocation,
which is a different sentence from a dyn and has a different
fix. *)
| Tast.Closure (_, env) when heap_env env ->
found := (e.Tast.loc, `Closure) :: !found
| _ -> ()))
fn.Tast.body);
List.rev_map
(fun (loc, site) ->
Loc.diag ~kind:"check/no-gc" loc
(match site with
| `Dyn what ->
Printf.sprintf
"%s holds a dyn, and --no-gc says this program carries no \
collector. A \
dyn value is one the runtime allocates and the collector owns, \
so there is nothing smaller to compile it to — write the type"
what
| `Closure ->
"this fn captures and outlives the frame it was made in, and \
--no-gc says this program carries no collector. The copies of an \
fn that outlives its frame live in an environment the collector \
allocates — call it or pass it down instead of keeping it, or \
pass what it names in as parameters"))
!found
let no_gc (p : Tast.program) =
match dyn_sites p with [] -> () | ds -> raise (Loc.Errors ds)
(* ── Memory diagnostics ─────────────────────────────────────────────────
"Which of these lines allocates?", answered on demand. Clojure's
[*warn-on-boxed*] crossed with Rider's heap-allocation squiggles, and the
same shape [dyn_sites] above has: a pass over the finished program, off
unless somebody asks, and nothing downstream is told it exists. Asking for
it cannot change what compiles.
Two classes, because the two heaps are not the same heap and a reader wants
to know which one a line is spending. [kind] carries it — ["memory/gc"] is
the dyn runtime's collected heap, ["memory/native"] is an allocator the
program named — so the CLI and the daemon dispatch on one field and neither
has to parse a message.
**Precision over completeness.** A site named here allocates, and a site
that only *might* says so in the first two words. That rule is what decides
the table below, and it decided it against the obvious guesses more than
once — every claim here was read out of runtime/flan_rt.c and
runtime/flan_dyn.c rather than assumed:
- [(vec-new T)] does not allocate. The lowering passes a capacity of zero
(see the [flan_vec_init] call in the ["vec-new"] arm) and
[flan_vec_init]'s body returns before [flan_vec_grow] when [cap <= 0].
The block arrives at the first push. [(map-new K V)] is the same: its
[flan_map_init] leaves [data] NULL and says so on its own line.
[(vec-new dyn)] and [(map-new dyn)] are the *other* answer — those are
the dyn runtime's own objects and [gc_alloc] runs at the call.
- A dyn immediate does not allocate: nil, a bool, an f64, a keyword, and
an int inside the payload. The payload is 48 bits
([DYN_PAYMASK]/[DYN_INT_MAX] in flan_dyn.c), so only an i64 that can
leave ±2^47 is a "may allocate", and a value widened from a narrower
integer type provably cannot.
- A keyword is interned and immortal — [flan_dyn_kw]'s entry is not a GC
object and the collector never traces one — so it is not named here.
- A dyn container growing itself is not named, and this one is a judgement
rather than a fact about the runtime: [flan_dyn_push] and
[flan_dyn_map_set] provably may [gc_alloc], and they are still left out.
The unit this pass reports is a line the programmer can act on — crossing
into dyn is a choice, pushing onto an allocator's Vec is a choice — and a
dyn vector taking a block to hold what was just put in it is the only
thing it could do. Marking it would squiggle every [(push dv x)] in a
program that chose dyn. Written down in TODO.org, "Memory diagnostics on
demand", because it is the one
row here that the precision rule alone does not decide.
- Dyn arithmetic is not named. [flan_dyn_add] and its siblings end in
[flan_dyn_from_i64], so a wide enough result spills, but nothing static
knows the operands and a squiggle on every [(+ a b)] over dyn is the
false positive this pass exists not to have. *)
(* The payload's range, restated from [DYN_INT_MAX]/[DYN_INT_MIN] in
runtime/flan_dyn.c: 2^47-1 and its negation less one. Restated rather than
read, the way every other number this compiler shares with the runtime is,
and wrong only in the direction of a missing warning if the runtime ever
widens it. *)
let dyn_payload_max = 140737488355327L
let dyn_payload_min = -140737488355328L
(* An integer type that cannot reach the payload's edge whatever its value. *)
let narrower_than_payload (t : Types.t) =
match t with
| Types.Int (Types.I8 | Types.I16 | Types.I32
| Types.U8 | Types.U16 | Types.U32) -> true
| _ -> false
(* Can this argument to [flan_dyn_from_i64] spill onto the heap?
One level of unwrapping and no more: [box] widens with a single
[Cast i64], and peeling further would walk through a *narrowing* cast the
programmer wrote and report a range the value cannot have. *)
let int_may_spill (e : Tast.expr) =
let e =
match e.Tast.e with
| Tast.Prim (Tast.Cast (Types.Int Types.I64), [ inner ])
when narrower_than_payload inner.Tast.ty -> inner
| _ -> e
in
match e.Tast.e with
| Tast.Int (n, _) -> n > dyn_payload_max || n < dyn_payload_min
| _ -> not (narrower_than_payload e.Tast.ty)
(* A [flan_vec_init] whose capacity is a literal zero takes no block. That is
every [(vec-new T)]; [slurp] passes the file's size and is the caller that
makes this a test rather than a constant. *)
let vec_init_allocates (args : Tast.expr list) =
match args with
| _ :: _ :: cap :: _ ->
(match cap.Tast.e with Tast.Int (n, _) -> n > 0L | _ -> true)
| _ -> true
(* The classifier. [Some (kind, message)] for a site that allocates or may,
[None] for everything else — and [None] is the answer for every symbol not
named here, which is what keeps a new runtime entry point silent rather
than guessed at. *)
let memory_class (sym : string) (args : Tast.expr list) =
let gc m = Some ("memory/gc", m) and native m = Some ("memory/native", m) in
match sym with
(* ── The collected heap ── *)
| "flan_dyn_from_bytes" ->
gc "allocates: a string crossing into dyn is copied onto the \
collector's heap"
| "flan_dyn_vec_new" ->
gc "allocates: a dyn vector is an object on the collector's heap"
| "flan_dyn_map_new" ->
gc "allocates: a dyn map is an object on the collector's heap"
| "flan_dyn_map_new_class" ->
gc "allocates: a class instance is a dyn map on the collector's heap, \
with the class's name in its header"
| "flan_dyn_view_slice" | "flan_dyn_view_at" ->
gc "allocates: a typed container crossing into dyn takes a view record \
on the collector's heap — the elements are not copied, the record is"
| "flan_dyn_from_i64" when (match args with [ x ] -> int_may_spill x | _ -> true) ->
gc "may allocate: an i64 outside ±2^47 does not fit a dyn's payload and \
spills onto the collector's heap"
| "flan_dyn_from_u64" when (match args with x :: _ -> int_may_spill x | [] -> true) ->
gc "may allocate: a u64 above 2^47 does not fit a dyn's payload and \
spills onto the collector's heap"
(* ── An allocator the program named ── *)
| "flan_arena_new" ->
native "allocates: an arena takes its whole region from the host here"
| "flan_vec_init" when vec_init_allocates args ->
native "allocates: the Vec is sized up front and takes its block from \
its allocator here"
| "flan_vec_push" ->
native "may allocate: a push past the Vec's capacity grows it through \
its allocator"
| "flan_vec_reserve" ->
native "may allocate: a reserve past the Vec's capacity grows it through \
its allocator"
| "flan_map_put" ->
native "may allocate: a put past the map's load factor grows its block \
through its allocator"
| "flan_map_reserve" ->
native "may allocate: a reserve past the map's load factor grows its \
block through its allocator"
| "flan_vec_clone" ->
native "may allocate: cloning a non-empty Vec takes a new block from its \
allocator"
| "flan_map_clone" ->
native "may allocate: cloning a non-empty map takes a new block from its \
allocator"
| _ -> None
(** Every site in the program that allocates, or may. Ordered by source
position, one diagnostic per location and class — a lowering emits several
runtime calls at one location and a reader wants the line named once.
[?file] narrows it to one source file, which is what a command that was
handed a path wants: the prelude pushes onto Vecs on a dozen lines and an
import has its own, and neither is a line the person who asked can do
anything about. Left out, everything the program holds is reported — which
is what a client that does its own filtering, the editor among them,
should ask for. *)
let memory_sites ?file (p : Tast.program) : Loc.diag list =
let found = ref [] in
let seen = Hashtbl.create 64 in
let look (e : Tast.expr) =
let cls =
match e.Tast.e with
| Tast.Prim (Tast.Rt sym, args) -> memory_class sym args
| Tast.Closure (_, env) when heap_env env ->
Some ("memory/gc",
"allocates: an fn that captures and outlives its frame keeps its \
copies in an environment on the collector's heap")
| _ -> None
in
match cls with
| None -> ()
| Some (kind, msg) ->
let loc = e.Tast.loc in
let key = (loc.Loc.file, loc.Loc.line, loc.Loc.col, kind) in
if (match file with None -> true | Some f -> String.equal f loc.Loc.file)
&& not (Hashtbl.mem seen key) then begin
Hashtbl.replace seen key ();
found := Loc.diag ~kind loc msg :: !found
end
in
(* A global's initialiser runs at startup and allocates there as much as a
body does — [(defonce names (vec-new dyn))] is a heap object before main
has a line of its own — so the globals are walked and not only the
functions. *)
List.iter (fun (g : Tast.global) -> Tast.walk look g.Tast.ginit) p.Tast.globals;
List.iter
(fun (fn : Tast.fn) -> List.iter (Tast.walk look) fn.Tast.body)
p.Tast.fns;
let placed (d : Loc.diag) = d.Loc.dloc.Loc.line > 0 in
List.stable_sort
(fun a b ->
match (placed a, placed b) with
| true, false -> -1
| false, true -> 1
| _ -> Loc.before a.Loc.dloc b.Loc.dloc)
(List.rev !found)
(* The form that decided an inferred return type, for a name whose return
slot was [_]; [None] for one whose type was written. *)
let inferred_cause env name = Hashtbl.find_opt env.inferred name