flan/lib/types.ml
Joseph Ferano ff2c949361 The tagged sum is defdata, and the old spelling is an error by name
Flan's tagged sum has been spelled defunion since it landed, which was
accurate right up until the language wanted C's untagged union as well.
Both cannot be called the same thing, and the tagged one is the one with
an alternative name that says what it is: a case, its fields, and a tag
that steers which case is live is a data type, not a union.

So the form is defdata everywhere -- the parser, the AST, the checker,
both backends, the prelude's Form, the editor's font-locking and imenu,
the docs and every .flan file in the tree. The internal vocabulary moves
with it: Tast.union is Tast.data, uname is dname, the tables the checker
and the emitter keep are datas. Leaving them would have inverted the
words permanently, with surface defunion meaning one thing and
env.unions meaning the other, which is exactly the kind of drift the
comments in those files exist to prevent. What did not move is case,
variant and vfields: a tagged sum still has cases, and it still has one
live at a time.

defunion is not kept as an alias. An alias would compile the day the
untagged form lands and mean the opposite of what it used to -- the same
silent misparse that made defn's return type mandatory, and worse,
because the reader would have no reason to look. The old spelling is a
named refusal instead, parse/defunion-renamed, which says what it is now
called and that the name is reserved for something else. It fires on the
head alone, so (defunion U [A B]) -- which would otherwise have parsed
cleanly as one field A of type B -- is refused with the rest.
2026-09-17 19:03:27 +07:00

208 lines
9.9 KiB
OCaml

(** Resolved types: what [Ast.texpr] means once names are looked up.
The AST's type expressions are surface syntax — [Tname "Ptr"] and
[Tapp ("Option", ...)] are just names there. Here they are the real thing,
and two types are the same type exactly when they are structurally equal.
Milestone 2 has no generics, so there is no unification and no substitution:
a type variable is parsed, carried, and rejected the moment a value would
have to have it. That rejection lives in [Check]; this module only names
the shape. *)
(* Machine integer types. Signedness and width are both part of the type —
there is no implicit widening anywhere, per plan.org. *)
type ikind = I8 | I16 | I32 | I64 | U8 | U16 | U32 | U64
type fkind = F32 | F64
type t =
| Int of ikind
| Float of fkind
| Bool
| String
| Unit (* the zero-sized type, not C's void *)
| Never (* return, exit, error: no value at all *)
| Named of string (* a struct or data type, declared here *)
(* A C enum: an i32 at run time, but its own type, so a keyword at a call
site has something to resolve against and a plain integer does not fit. *)
| Enum of string
| Slice of t (* [T] ptr+len, non-owning *)
| Array of int64 * t (* [n T] inline, a value, copies *)
| Map of t * t (* (Map K V) *)
| Ptr of t (* (Ptr T) *)
(* [Allocator]: a builtin opaque type, the way [string] is a builtin
ptr+len. It is a [Types.t] case with no user-writable constructor, which
is what lets spec-memory.md's "procedure plus an opaque data pointer" be
expressed with none of milestone 5's function values — the procedure is a
C symbol the emitter names and no Flan type ever mentions it. At run time
it is a pointer to the runtime's [flan_allocator], never a copy of one:
the capability set and the epoch have to be shared by every container
made from it, and a copy would give each its own. *)
| Alloc
(* [(Vec T)]: ptr + len + cap + allocator, owning and move-only. One
type-erased runtime over (size, align) stands behind every instantiation,
so this is a container without generics — the concrete type is known only
at the call site, which is exactly where the two numbers are produced. *)
| Vec of t
(* [(Pool T)]: slab storage handed out behind [(Handle T)]. Owning and
move-only exactly as a [Vec] is, and built on the same type-erased
runtime over (size, align). It is not a second [Vec]: a [Vec]'s indices
shift when something is removed and a [Pool]'s slot index never moves,
which is the whole reason a handle into one stays meaningful. *)
| Pool of t
(* [(Handle T)]: a reference to something that can die, which reports that
it died rather than silently resolving to whatever reused its slot
(spec-memory.md, "Borrowing" — "Cross-referencing long-lived objects uses
(Handle a) into a pool, never a raw pointer or slice. A stale handle is
detectable").
It is a plain 64-bit number — a slot index in the low 32 bits and that
slot's generation counter in the high 32 — so it copies, compares and
zeroes like an integer and owns nothing. A zeroed handle is generation 0,
and a live slot's generation is always odd, so [Zero] of a handle is a
handle that resolves to nothing rather than one that resolves to slot 0.
See runtime/flan_rt.c's pool section for the packing. *)
| Handle of t
| Option of t (* (Option T) *)
| Fn of t list * t (* (Fn [T ...] R) *)
| Var of string (* a type variable — milestone 5 *)
let signed = function
| I8 | I16 | I32 | I64 -> true
| U8 | U16 | U32 | U64 -> false
let bits = function
| I8 | U8 -> 8 | I16 | U16 -> 16 | I32 | U32 -> 32 | I64 | U64 -> 64
let bits_f = function F32 -> 32 | F64 -> 64
let ikind_of_name = function
| "i8" -> Some I8 | "i16" -> Some I16 | "i32" -> Some I32 | "i64" -> Some I64
| "u8" -> Some U8 | "u16" -> Some U16 | "u32" -> Some U32 | "u64" -> Some U64
| _ -> None
let fkind_of_name = function
| "f32" -> Some F32 | "f64" -> Some F64 | _ -> None
(* Every name the resolver accepts as a primitive type. The list exists so a
near-miss can be reported as the typo it is. [Unit] is on it because the
resolver still answers to that name -- [Cimport] builds [Tname "Unit"] for
C's void, and never goes through the parser -- but nobody writes it: unit
is spelled [()] in source, and [Parse.texpr] refuses the word. *)
let primitive_names =
[ "i8"; "i16"; "i32"; "i64"; "u8"; "u16"; "u32"; "u64";
"f32"; "f64"; "bool"; "string"; "Unit"; "Never"; "Allocator" ]
let ikind_name k =
(if signed k then "i" else "u") ^ string_of_int (bits k)
let fkind_name = function F32 -> "f32" | F64 -> "f64"
(* Structural equality is the whole story: no subtyping, no coercion between
machine types, no variance. Written out rather than using [=] so that adding
a case with a function or a mutable field cannot silently break it. *)
let rec equal a b =
match a, b with
| Int x, Int y -> x = y
| Float x, Float y -> x = y
| Bool, Bool | String, String | Unit, Unit | Never, Never -> true
| Named x, Named y | Enum x, Enum y -> String.equal x y
| Slice x, Slice y -> equal x y
| Array (n, x), Array (m, y) -> Int64.equal n m && equal x y
| Map (k, v), Map (k', v') -> equal k k' && equal v v'
| Ptr x, Ptr y -> equal x y
| Alloc, Alloc -> true
| Vec x, Vec y -> equal x y
| Pool x, Pool y -> equal x y
| Handle x, Handle y -> equal x y
| Option x, Option y -> equal x y
| Fn (ps, r), Fn (ps', r') ->
List.length ps = List.length ps'
&& List.for_all2 equal ps ps'
&& equal r r'
| Var x, Var y -> String.equal x y
| _ -> false
let rec to_string = function
| Int k -> ikind_name k
| Float k -> fkind_name k
| Bool -> "bool"
| String -> "string"
| Unit -> "()"
| Never -> "Never"
| Named n | Enum n -> n
| Slice t -> "[" ^ to_string t ^ "]"
| Array (n, t) -> Printf.sprintf "[%Ld %s]" n (to_string t)
| Map (k, v) -> Printf.sprintf "(Map %s %s)" (to_string k) (to_string v)
| Ptr t -> "(Ptr " ^ to_string t ^ ")"
| Alloc -> "Allocator"
| Vec t -> "(Vec " ^ to_string t ^ ")"
| Pool t -> "(Pool " ^ to_string t ^ ")"
| Handle t -> "(Handle " ^ to_string t ^ ")"
| Option t -> "(Option " ^ to_string t ^ ")"
| Fn (ps, r) ->
Printf.sprintf "(Fn [%s] %s)"
(String.concat " " (List.map to_string ps)) (to_string r)
| Var n -> n
let is_numeric = function Int _ | Float _ -> true | _ -> false
(* Move-only: binding, passing or returning one transfers ownership and the
source binding is dead afterwards (spec-memory.md, "The four container
types"). That rule is what makes a double free unrepresentable, which is why
[free] needs no analysis of its own. A struct that owns one is move-only
too; that arrives with [drop], which is the step after this one. *)
let rec is_move_only = function
(* A [Pool] owns its storage; a [Handle] into one owns nothing, which is the
point of it — handles are copied freely, and the pool is the single
owner that [free] applies to. *)
| Vec _ | Map _ | Pool _ -> true
| Option t -> is_move_only t
| Array (_, t) -> is_move_only t
| _ -> false
(* The key types the first Map implementation admits (spec-memory.md, "Maps —
first implementation"): integers, enums, strings, fixed arrays, and value
structs composed recursively from those. Equality and hashing for them are
compiler-provided structural operations, so this is the whole of what the
emitted hash and equality pair has to cover — there is no dispatch to design
and no type class anywhere.
A struct is [Named], and whether its fields qualify cannot be decided here:
this module has no field table. [Check] finishes the job by walking them,
which is also where it emits the pair. Everything this does say no to says
no for a reason that will not change with a milestone: a [Ptr] or a [Slice]
key would hash an address, and hashing an address is a different operation
from hashing what it points at. *)
let rec keyable = function
| Int _ | Enum _ | Bool | String -> true
| Float _ -> false (* NaN /= NaN, and 0.0 and -0.0 differ bytewise *)
| Array (_, t) -> keyable t
| Named _ -> true (* [Check] decides, by walking the fields *)
(* A [Handle] is not a map key, for the reason a [Ptr] is not: hashing an
identity is a different operation from hashing what it names, and a
handle whose slot has been reused hashes the same as it always did while
naming nothing. The type exists to make that difference visible, so
burying it under a key is the one thing it must not do. *)
| _ -> false
(* Ordering and equality are defined on machine types and on nothing else at
milestone 2 — strings, structs and slices have no built-in [=], because an
unconstrained type supports only what every type supports (plan.org, Types). *)
let is_comparable = function Enum _ -> true | t -> is_numeric t
(* [=] and [!=] admit one more type than [<] does. A [Handle] is a pair of
numbers in a 64-bit word, so "is this the same entity" is one integer
compare and is worth having — two handles are equal exactly when they name
the same slot at the same generation, so a stale handle is never equal to
the live one that replaced it. Ordering handles would compare a slot index,
which means nothing: allocation order is a free-list artefact. Hence two
predicates rather than one. *)
let is_equatable = function Handle _ -> true | t -> is_comparable t
(* [Never] is the type of an expression that does not produce a value: return,
an early-returning `some`, exit. It fits anywhere, and that is the only
place anything resembling subtyping exists. *)
let fits ~expected ~actual =
match actual with Never -> true | _ -> equal expected actual