Work in progress: it builds and the runtime is exercised and green, but no Flan program can reach it yet — the checker half is not written, so (Map K V) is still refused where it is resolved. runtime/flan_rt.c is Odin's map, followed deliberately: open-addressed Robin Hood hashing at a 75% load factor, cache-line cell packing so no key or value straddles a line, and the probe loop kept to pointer-width integers. One type-erased runtime over (key size, value size) plus a hash and equality pair, the same arrangement the Vec runtime has over (size, align). Two departures from Odin, both deliberate and both commented where they are made. There are no tombstones, because removal is deferred by spec-memory.md, and that deletes the backward-shift loop entirely — it is the single largest reason this is shorter than the original. And the header does not stuff log2cap into the low bits of the data pointer: Odin does that because Raw_Map must be three words, whereas this header already carries an allocator, a generation and an epoch, so the tagging would buy nothing, cost a mask on every access, and make correctness depend on the block being 64-byte aligned rather than merely faster when it is. The scaffolding around it: a Map is 48 bytes and six words like a Vec, it crosses to the runtime by address because it is move-only and must be mutated in place, and it has a DWARF type showing all six fields. Tast.FnAddr is new — the address of a function, either one this compiler emitted or a runtime C symbol. It is not a function value: nothing in the surface language can produce one, name its type or call through it. Odin's Map_Info reaches its hash and equality pair exactly this way. reach.ml learns that edge, because a function reached only by address is invisible to the reachability walk otherwise, which is the same hazard handler-bind clauses already had. The hash and equality pair carries the transfer channel as its last parameter, because a pair emitted for a struct key is an ordinary Flan function and every Flan function's signature ends with one.
165 lines
7.2 KiB
OCaml
165 lines
7.2 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
|
|
| 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. *)
|
|
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 -> "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 "{%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
|
|
|
|
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
|
|
| Vec _ | Map _ -> 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 *)
|
|
| _ -> 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
|
|
|
|
(* [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
|