flan/lib/types.ml
Joseph Ferano 3e68089cde A parameter with no type is dyn, decided where the type names are all known
The type itself, the ABI its operations call into, and the one decision the
feature could not avoid: (defn f [x y]) is one parameter or two, and which one
depends on whether y names a type.

Parse does not decide it. That lookup is the one its defn comment records being
removed for being wrong twice in one day -- the set of type names is incomplete
at parse time by construction, and macros generating definitions is what
widened the failure. So the vector is carried undecided, as Ast.pitems, and
paired in Check, after every file is loaded, every macro expanded and every
header imported. The set is complete there. It is not complete across time, and
the comment says so: a defstruct written later changes a signature with no edit
to the function.

The return slot stays mandatory and dyn is written out in it. The ambiguity
there has no syntactic resolution at all -- a capitalised head in a list is both
a type application and a struct literal -- so the third state the parameters
needed does not exist for the return type, and ret = None goes on meaning Unit.

What the feature costs, and what is taken back: a slot with no type used to be a
syntax error, so a mistyped type now reads as an extra parameter with no
diagnostic. A name within one edit of a type's gets the resolver's own
did-you-mean, and an unknown capitalised name is reported as the unknown type it
is -- not one parameter in the corpus is capitalised. A lowercase name
resembling no type is the feature working, and is the residual.

The x86 backend refuses dyn by name; both callers already name --llvm, and the
daemon takes that backend by default, so this is the first thing a user of dyn
sees. The JS dialect refuses it too, for the opposite reason -- every value
there is already dynamic and what is missing is only the lowering.

runtime/flan_dyn.h is the fixed ABI. flan_dyn_stub.c stands in until the real
collector lands and says in its header that it verifies nothing about roots.
2026-09-19 05:47:49 +07:00

171 lines
7.7 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
| Option of t (* (Option T) *)
| Fn of t list * t (* (Fn [T ...] R) *)
| Var of string (* a type variable — milestone 5 *)
(* [dyn]: one machine word whose contents the runtime knows and this module
does not. It is a written type — [(defvar x dyn 5)] boxes the 5 — and it
is also what an unannotated [defn] parameter means, which is why it is a
case here and not a Named type the prelude declares: the checker has to
recognise it to choose the boxing and the dyn op lowering, and a name in a
table cannot be matched on.
Nothing about the representation is stated here on purpose. The word is
opaque to the compiler — runtime/flan_dyn.h owns which bits are a tag —
so that milestone 2 can change the encoding without touching Emit. *)
| Dyn
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
| 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 ^ ")"
| 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
| Dyn -> "dyn"
let is_numeric = function Int _ | Float _ -> true | _ -> 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 *)
| _ -> 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
let is_equatable = is_comparable
(* [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