Types.is_equatable splits from is_comparable: a string answers equal? now, bytewise, but still answers no to ordered? — there is no collation the language has picked, so < and friends keep the refusal they had. The comparison itself is one new runtime entry point, flan_str_eq (runtime/flan_rt.c), length-mismatch and same-pointer fast paths ahead of the memcmp, called identically from both backends: emit.ml pulls a string's ptr and length out of the %slice SSA value and calls it directly in the Eq/Ne arm; x86.ml adds an arm ahead of the generic scalar comparison that reaches it through call_native, flipping the answer for != the same way Not already flips a bool. test_flan.ml covers the checker side directly and through a generic instantiated at string, including the two different ways ordered? and equal? fail at that type. test/programs/string-eq.flan is the survey program — same pointer, differing lengths, equal content at distinct addresses (a literal against a fresh heap string), a difference in the last byte, and the empty-string cases — with acceptance rows for LLVM, -O0 and --x86 in test_acceptance.ml.
180 lines
8.3 KiB
OCaml
180 lines
8.3 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"; "dyn"; "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
|
|
(* [Dyn] is equal to itself and to nothing else. Two dyn values may hold
|
|
different things at run time, which is the point of the type and is not
|
|
this function's question: this is identity of *static* types, and there is
|
|
one dyn type the way there is one string type. *)
|
|
| Bool, Bool | String, String | Unit, Unit | Never, Never | Dyn, Dyn -> 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 is defined on machine types and on nothing else — structs and
|
|
slices have no built-in [<], because an unconstrained type supports only
|
|
what every type supports (plan.org, Types). A string has no ordering
|
|
either: there is no true answer to whether one string is less than another
|
|
until the language picks a collation, and byte order is not it. *)
|
|
let is_comparable = function Enum _ -> true | t -> is_numeric t
|
|
|
|
(* Equality admits one type ordering does not: a string, grown in by the M2
|
|
queue's item 5 — bytewise, by content and not by address, so two
|
|
separately built strings with the same bytes are equal. *)
|
|
let is_equatable = function String -> 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
|