The author's decision, and it removes the one syntax question generics
had. A return type can no longer be written in braces, so a {...} after
the signature is unambiguously the constraint map and there is no
structural rule to explain.
The reasons for the record: the brace's value meaning and its type
meaning do not correspond the way the bracket's do - [1 2 3] is a value
whose type is [3 i32], but {.x 1} is a value whose type is a name, and a
map value is built by map-new with no braces anywhere - and dropping it
reserves {} in type position for anonymous struct types.
Braces in a type are refused with the surviving spelling named rather
than falling through to "expected a type". Types.to_string and
Cimport's source printer both print (Map K V) now, and Shim refuses the
application spelling where it used to refuse only Ast.Tmap.
208 lines
9.9 KiB
OCaml
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 union declared in the file *)
|
|
(* 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 (* {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
|