diff --git a/TODO.org b/TODO.org
index cbacacb1..01a819fc 100644
--- a/TODO.org
+++ b/TODO.org
@@ -147,14 +147,7 @@ CLOSED: [2026-09-25]
=f64-inf=, =f64-nan=, =f32-inf= and =f32-nan= are names the checker supplies
(=Check.special_float=), reached only after every local, global and function has
missed, so a program's own binding of one wins. Negative infinity is
-=(- 0.0 f64-inf)=: the decision wrote =(- f64-inf)=, and there is no unary minus.
-Rules out Clojure's =##Inf= reader literal.
-
-** TODO (!= x x) is false for a NaN
-=!== on floats is LLVM's ordered =one= on both backends (=lib/emit.ml= =fcmp_op=,
-=lib/x86.ml= =float_cc=), so =(!= f64-nan f64-nan)= is =false= where C, Odin and
-IEEE 754 say =true=; =(not (= x x))= is the only NaN test that works. Changing it
-to =une= is a decision about what =!== means.
+=(- f64-inf)=. Rules out Clojure's =##Inf= reader literal.
** DONE A u64 constant above 2^63 cannot be written in decimal
CLOSED: [2026-09-25]
@@ -162,8 +155,8 @@ An integer written at or above 2^63 — a decimal up to 2^64 - 1, or hex with th
top bit set — reads as =Form.UInt=, its pattern and its spelling. It is accepted
where the type is =u64=, a =(u64 ...)= cast included, and refused everywhere else
in the spelling it was written in. Hex with the top bit set was accepted as a
-negative at any integer type before this; it is refused now too. A negative
-decimal is still a =u64= bit pattern. A cast's integer literal that does not fit
+negative at any integer type before this; it is refused now too. A cast's
+integer literal that does not fit
=i32= is checked at the cast's type; one that fits keeps the =i32= default, so
=(u32 -1)= still means what it did. A wide literal passed to a macro as an
argument comes back wide: it crosses as an =Int= with a token in the unused
@@ -571,13 +564,10 @@ dispatch values. With single dispatch on literal values there is no specificity
question, and inheritance or multiple dispatch would create one. Unknown-slot
checking needs class-typed tracking the dyn side deliberately does not have.
-** NEXT update-instance-for-redefined-class, the user hook
-Decided 2026-09-25: build it after typed class slots land, shaped for the REPL — written and installed from a live session as a one-time "here is how to migrate this", without restarting. It receives the instance with the added and discarded slots and their old values, and runs at each instance's lazy migration. A hook that signals parks in the break buffer with a restart that falls back to name-matching migration.
-Left out of v1 because name matching is the half that makes redefinition usable
-and the hook is what makes it expressive. The obvious spelling is a generic riding
-the dispatch that exists, and the migration already computes both the added and
-the discarded lists. Rolling a failed migration back becomes a real question the
-day this lands.
+** DONE update-instance-for-redefined-class, the user hook
+CLOSED: [2026-09-25]
+Taking =migrate-by-name= keeps the name-matched instance, not SBCL's obsolete one,
+and retries nothing; a transfer from the method to a restart below it traps.
** DONE The module system stays directory-as-package
Several files in one directory are one module; a loose file is a module of one,
@@ -957,19 +947,13 @@ type an expression cannot hold, such as =(Fn [i32] ())=, is parsed as
** DONE An array literal cannot say it is [f32]
CLOSED: [2026-09-25]
-With nothing outside an array literal naming its element type, the first
-element's type is the want for the rest, so =[(f32 1.0) 2.5]= is a =[2 f32]=. A
-refusal of a later element carries a note at the first saying it set the type.
-Rules out a =1.0f= suffix for now.
+=(the [f32] [1 2.5])= names the element type; with nothing naming one, a literal
+element takes the other elements' type. Rules out a =1.0f= suffix for now.
-** NEXT A let binding takes no type annotation
-Decided 2026-09-25: =(the T expr)=, Common Lisp's special operator, gives any expression its want; checked at compile time like any other want, and it compiles to nothing. =let= is unchanged. On a =dyn= operand it is refused, naming the cast. The refusals that say "annotate the binding" — =None=, an empty =[]=, and =(zeroed)=/=(filled)=/=(dead-beef)= with no want — suggest it instead, because today their suggestion cannot compile.
-Everything under the surface is there — the binding carries a type slot and the
-checker consumes it as the want — and only the way it is written is open, because
-=let= is a flat list of pairs and cannot disambiguate by count. No longer the
-blocker it was, since =(array 4 T)= answers the case that raised it. plan.org's
-rule is "annotate function signatures, infer locals", so a general annotation is a
-deliberate absence.
+** DONE A let binding takes no type annotation
+CLOSED: [2026-09-25]
+=(the T expr)= gives any expression its want and =let= stays a flat list of
+pairs. Rules out a type slot in =let=.
** NEXT A read-only slice type
Decided 2026-09-25: =[const u8]=, Zig's spelling in Flan's brackets. =bytes-view= answers one and a =set= through it is a compile error; a =[T]= converts to =[const T]= and not back, and the prelude's read-only functions take it. =const= is reserved as a name, since =[n T]= accepts a constant's name for =n=.
@@ -1025,10 +1009,9 @@ died in the backend as a redefinition of a symbol, a message with no source
location.
** DONE A u64 literal is its 64-bit pattern
-The cost of accepting the pattern is that a negative decimal literal is accepted
-as a =u64=, because the reader records the value and not how it was written.
-Narrower unsigned types keep the strict check, which is where a typo like =300=
-for a =u8= shows up.
+A negative literal fits no unsigned type, u64 included; =(u64 -1)= is how the
+pattern is written, and a constant folds it. Rules out a negative decimal as a
+u64's bit pattern.
** DONE A folded constant does not skip the range check
The folding pass makes its own call to the range test, because a global's
@@ -1090,11 +1073,6 @@ An unknown call whose near miss is a value — =(context-allocator)= against
=context/allocator=, or a global — says the name is a value written without
parentheses, and names no call at all when the call had arguments.
-** NEXT (max-value T) and (min-value T)
-Decided 2026-09-25: the type-limit constants as a form taking a type, Odin's
-max(T), valid at any numeric type or a numeric?-bounded variable. For a float,
-min-of is the most negative finite value.
-
** NEXT (Ptr const T), the pointer beside [const T]
Decided 2026-09-25: addr through a read-only slice gives a (Ptr const T), which
nothing writes through; (Ptr T) widens to it and never back; a C parameter
@@ -1422,7 +1400,7 @@ sibling".
** DONE A redefined defclass migrates its instances lazily
CLOSED: [2026-09-20]
-CLHS 4.3.6 minus the user hook. Nothing is enumerated and no heap is walked — the
+CLHS 4.3.6. Nothing is enumerated and no heap is walked — the
redefinition is constant time and each instance pays once, at its next touch.
Neither printer migrates, so a stale instance shows its old slots to the editor
until something touches it. The registry is advisory: a key the class never
@@ -1575,14 +1553,19 @@ incarnation it was made for; every use compares the incarnation, so a destroyed
arena traps whether or not a later arena-new reused its record. Rules out
static tracking of destroy, which is move semantics.
-** NEXT A mixed array literal with no want is a dyn vector
-Decided 2026-09-25: with nothing expected of it, an array literal whose elements
-agree (numbers widening together) is typed; one whose elements mix — [10 "Hi"],
-[nil 1] — is a dyn vector. (the [T] ...) forces a typed one, and a want from
-context still wins. Replaces the first-element carry-over.
+** DONE A mixed array literal with no want is a dyn vector
+CLOSED: [2026-09-25]
+Elements that agree, numbers meeting at the wider, are typed; elements that mix
+are a dyn vector, except numbers with no common type, which are refused. Rules
+out the first element typing the rest.
* Dev loop
+** TODO A prelude function shadowed live is reached by the prelude's own calls
+A defn of a prelude function's name sent to a running =flan dev= installs into the
+host's cell for that name, so the prelude's calls compiled into the host follow it;
+a rebuild gives them the prelude's again, as =Check.shadow_prelude= intends.
+
** DONE The dev loop, step 1: the reload primitive
A list of top-level forms is recompiled and installed into a running process, and
call sites compiled before those forms existed follow them through an indirection
@@ -2041,18 +2024,10 @@ specification's own branch — the flag is the command and the printed shape is
error pattern, so anyone who wants one has the four lines, and the manual carries
them.
-** NEXT defclass slots take types, checked on write
-Decided 2026-09-25: slots are name/type pairs checked on write; an untyped slot stays legal and holds any =dyn=. A migration keeps a stored value that no longer fits the new type, warns once, and the next write is checked. One lane with the =set= entry below.
-=(defclass State [pause bool step bool])= reads as four untyped slots and
-reports a duplicate =bool=. Wanted: the slot list is name/type pairs, as CLOS
-does it. The type is a declaration about the values and not a layout — an
-instance stays a map, so redefinition and lazy migration are unchanged. SBCL
-checks it on write (=src/pcl/slots.lisp:160=, the typecheck before the store),
-which is where the bad value is, so =put= is the site here.
-
-Open: what migration does with a stored value that no longer fits a changed
-slot type, and whether an untyped slot stays legal (it should — =dyn= is a type
-and writing nothing should mean it).
+** DONE defclass slots take types, checked on write
+CLOSED: [2026-09-25]
+Constructor parameters stay dyn and each store checks at run time; an int widens
+into a float slot only if it round-trips, and nil fits only an (Option T) slot.
** DONE println takes up to a second to appear
CLOSED: [2026-09-25]
@@ -2064,15 +2039,10 @@ are gone. The poll stays for the stop and park edges. Rules out a faster
poll and pushes to clients that did not ask for them. See docs/BUILT.md,
"Output and the watch table are pushed".
-** NEXT set writes a class slot; put is for maps
-Decided 2026-09-25: as written; one lane with typed slots.
-=put= exists because an absent map key has no location to store into, which is
-why =(get m k)= is refused as a place (=lib/parse.ml:1159=). A class instance is
-not in that situation: its slots are fixed by the =defclass=, so a declared slot
-always exists and =(set (get state :pause) true)= is a field store like
-=(set (.velocity g) 0.0)=. Make =set= take it, and leave =put= to maps, where
-insertion is real. Writing an undeclared slot through =set= is then a refusal
-naming the class.
+** DONE set writes a class slot; put is for maps
+CLOSED: [2026-09-25]
+=put= on an instance still checks a declared slot's type and still inserts an
+undeclared key; only =set= refuses one, since a slot it writes has to exist.
** NEXT update: change a place by applying a function to it
Decided 2026-09-25: every place evaluates each of its subexpressions once, C's compound-assignment rule, which also fixes =++= and =--=; =update= is built on that. Rules out refusing side effects in a place.
diff --git a/bin/main.ml b/bin/main.ml
index f4564016..0f1db3e2 100644
--- a/bin/main.ml
+++ b/bin/main.ml
@@ -362,6 +362,7 @@ let () =
p.globals;
List.iter
(fun (f : Flan.Tast.fn) ->
+ if not (Flan.Check.internal_name f.name) then
Printf.printf "defn %s : (Fn [%s] %s) %d slots\n" f.name
(String.concat " "
(List.map Flan.Types.to_string f.params))
@@ -736,6 +737,8 @@ let () =
if List.mem warn_memory_flag rest then
print_memory_warnings ~file:path p)
in
+ Flan.Build.need_main ~file:path ~doing:"flan build has nothing to link"
+ f.program;
ignore (Flan.Build.executable
~opts:{ Flan.Build.default with checks; dev; debug; sanitize;
target; x86;
@@ -915,6 +918,8 @@ let () =
(Printf.sprintf "flan-run-%d" (Unix.getpid ()))
in
let f = Flan.Front.linked ~all:true path in
+ Flan.Build.need_main ~file:path ~doing:"flan run has nothing to run"
+ f.program;
ignore (Flan.Build.executable
~opts:{ Flan.Build.default with checks; debug; sanitize;
x86;
diff --git a/emacs/flan-mode.el b/emacs/flan-mode.el
index 05b2747c..e297b3a7 100644
--- a/emacs/flan-mode.el
+++ b/emacs/flan-mode.el
@@ -129,7 +129,7 @@
(defconst flan--special
'("quote" "do" "let" "if" "when" "cond" "and" "or"
"while" "until" "break" "continue" "return" "set"
- "array" "array-fill" "array-gen" "match" "fn" "dotimes" "loop" "recur"
+ "array" "array-fill" "array-gen" "the" "match" "fn" "dotimes" "loop" "recur"
"defer" "some" "try" "signal" "error"
"handler-bind" "handler-case" "restart-case" "invoke-restart")
"The heads `Parse.form' dispatches on — the forms with a meaning of their own.
diff --git a/lib/ast.ml b/lib/ast.ml
index 67902059..7aaf7f76 100644
--- a/lib/ast.ml
+++ b/lib/ast.ml
@@ -126,6 +126,10 @@ and expr_kind =
dimension; [ArrayFill]'s is the element value itself, evaluated once. *)
| ArrayFill of len list * expr
| ArrayGen of len list * expr
+ (* (the T e) — [e] checked with [T] as its expectation, Common Lisp's
+ special operator. A binding has no type slot, and this is what gives any
+ expression one; it compiles to [e]. *)
+ | The of texpr * expr
(* These bind names or alter control flow, so none of them can be a call. *)
| Fn of string list * expr list (* (fn [x y] ...) *)
(* (dotimes :o [i n] ...), (dotimes [i start stop] ...) and
@@ -199,6 +203,11 @@ and place =
| Pfield of expr * string (* (set (.hp e) v) *)
| Pindex of expr * expr list (* (set (at grid r c) v) *)
| Pderef of expr (* (set (deref p) v) *)
+ (* (set (get inst :slot) v) — a class instance's declared slot. A map has
+ no such place: an absent key has no location, and [put] is how one is
+ written. Which of the two a value is, is known only at run time, so
+ this is a runtime store that refuses a plain map. *)
+ | Pslot of expr * expr
and arm = { pat : pattern; body : expr list; aloc : Loc.t }
@@ -283,8 +292,10 @@ and decl_kind =
| Defvar of string * texpr option * init * reinit
| Defconst of string * texpr option * expr
(* ── The dyn side's classes and generic functions ──────────────────
- None of these four reaches [Check]. [Classes.expand] turns the whole set
- into ordinary [Defn]s before pass one collects anything, the way [Shim]
+ None of these four reaches [Check]'s signature pass. [Classes.expand]
+ turns the generic forms into ordinary [Defn]s before pass one collects
+ anything, and [Check.pair_decls] turns a [Defclass] into its constructor
+ once its slot vector can be paired, the way [Shim]
already turns a [DeclareC] into a [Declare] plus a [Defn]: a class is a
constructor, and a generic function is one function whose body is a
dispatch over the methods written for it.
@@ -294,8 +305,11 @@ and decl_kind =
anywhere in the file, or arrive at a reload long after the generic did,
and a macro sees one form. *)
- (* (defclass point [x y]) — the slot names, in constructor order. *)
- | Defclass of string * (string * Loc.t) list
+ (* (defclass point [x y]) or (defclass state [pause bool step bool]) — the
+ slot vector, in constructor order, left unpaired for the reason a
+ [defn]'s is: [[x y]] is two slots or one slot [x] of type [y] depending
+ on whether [y] names a type. [Check.pair_decls] pairs it. *)
+ | Defclass of string * pitem list
(* (defgeneric area [self] dyn) — CLOS's class dispatch: the dispatch value
is the shape tag of the first argument. The parameter vector and the
return slot are a [defn]'s, and there is no body. *)
@@ -414,6 +428,7 @@ let map_children f (e : expr) : expr =
| Pfield (x, n) -> Pfield (ex x, n)
| Pindex (x, is) -> Pindex (ex x, List.map ex is)
| Pderef x -> Pderef (ex x)
+ | Pslot (x, k) -> Pslot (ex x, ex k)
in
let kind =
match e.e with
@@ -438,6 +453,7 @@ let map_children f (e : expr) : expr =
subexpressions. The dimensions are [len]s and hold none. *)
| ArrayFill (ds, v) -> ArrayFill (ds, ex v)
| ArrayGen (ds, f) -> ArrayGen (ds, ex f)
+ | The (t, x) -> The (t, ex x)
| Fn (ps, es) -> Fn (ps, List.map ex es)
| Dotimes (l, n, b, es) ->
Dotimes (l, n,
diff --git a/lib/build.ml b/lib/build.ml
index 899310d2..70e94449 100644
--- a/lib/build.ml
+++ b/lib/build.ml
@@ -744,6 +744,22 @@ let compile_c ~opts ?tflags ?(warn = []) ~src ~name () =
end;
obj
+(* A program with no [main] builds every function and then fails at the link,
+ as an undefined reference from the C startup code — a message about crt1.o
+ for a mistake in the .flan file. Asked here, by the commands that make an
+ executable, rather than inside [executable], whose callers include hosts
+ and tests that supply their own [main]. *)
+let need_main ~file ~doing (p : Tast.program) =
+ if not (List.exists (fun (f : Tast.fn) -> f.Tast.name = "main") p.Tast.fns)
+ then
+ failwith
+ (Printf.sprintf
+ "%s has no main, so %s. A program starts at a function named main, \
+ for example:\n\n\
+ \ (defn main [] i32\n\
+ \ 0)"
+ file doing)
+
(* [csrcs] and [lflags] come from the imported packages (see [Load]): the C
shim a package binds through, and the arguments needed to link the library
it binds to.
diff --git a/lib/check.ml b/lib/check.ml
index 80b92b58..3f29bd6f 100644
--- a/lib/check.ml
+++ b/lib/check.ml
@@ -63,6 +63,22 @@ type binding = {
bwhat : string option;
}
+(* A class slot's type: what a value stored into it is checked against. A
+ dyn value's tag is all a store can ask of it, so the scalar types a tag
+ answers for, an instance of a class, and (Option T) of either, which
+ admits nil as well. *)
+type slot_ty =
+ | Sany (* no type written: any dyn value *)
+ | Sval of Types.t (* bool, an integer type, f32, f64, string *)
+ | Sclass of string (* an instance of this class *)
+ | Sopt of slot_ty (* nil, or a value of the inner type *)
+
+let rec slot_text = function
+ | Sany -> "dyn"
+ | Sval t -> Types.to_string t
+ | Sclass c -> c
+ | Sopt s -> "(Option " ^ slot_text s ^ ")"
+
type env = {
structs : (string, Tast.structure) Hashtbl.t;
datas : (string, Tast.data) Hashtbl.t;
@@ -181,6 +197,13 @@ type env = {
flag is what lets [resolve_name] say the honest thing in each place
instead of a suggestion that cannot be followed. *)
mutable in_field : bool;
+ (* Every [defclass], by name: its slots in constructor order, each with the
+ type a value stored in it must have — [Types.Dyn] for a slot written
+ with no type. Filled by [pair_decls], which is where a slot vector is
+ first readable. The type is a declaration about the values and not a
+ layout: an instance is a dyn map whatever this says, and what reads it
+ is [class_spec], which is what the runtime checks a store against. *)
+ classes : (string, (string * slot_ty) list) Hashtbl.t;
(* The bindings a dev build counts at every call: [Shim.resources], read off
the declare-c forms before [Shim.expand] rewrites them. Keyed by the Flan
name a program calls. *)
@@ -214,6 +237,7 @@ let new_env () = {
tvpreds = [];
chain = [];
in_field = false;
+ classes = Hashtbl.create 8;
tracks = Hashtbl.create 16;
}
@@ -1545,7 +1569,9 @@ let dyn_param_or_typo env n loc =
parameters are lowercase"
n
-let pair_params env (items : Ast.pitem list) : Ast.field list =
+let pair_params ?(also = fun _ -> false) env (items : Ast.pitem list)
+ : Ast.field list =
+ let is_type_name env n = is_type_name env n || also n in
let dyn loc = { Ast.t = Ast.Tname "dyn"; tloc = loc } in
let rec go = function
| [] -> []
@@ -1574,10 +1600,122 @@ let pair_params env (items : Ast.pitem list) : Ast.field list =
in
go items
+(* A class slot's type, held to the set a stored dyn value can be checked
+ against. A class's name is a type here, and only here: it is not a type
+ anywhere else in the language, since an instance is a dyn value. Every
+ other type a slot could name — a struct, a Vec, a pointer — does not cross
+ into dyn at all, so a slot of one could never be written. *)
+(* A class named in [cls]'s slot vector: [n] as written, or [n] in [cls]'s own
+ package, since [Load] leaves a bare name in a slot vector unqualified. *)
+let class_named ~classes cls n =
+ (* The class's own package first: an importer may declare a class of the
+ same bare name, and a slot the package wrote means the package's. *)
+ let own =
+ match String.rindex_opt cls '/' with
+ | Some i ->
+ let q = String.sub cls 0 (i + 1) ^ n in
+ if List.mem q classes then Some q else None
+ | None -> None
+ in
+ match own with
+ | Some _ -> own
+ | None -> if List.mem n classes then Some n else None
+
+let rec slot_of env ~classes cls fname (t : Ast.texpr) : slot_ty =
+ let refuse what =
+ Loc.failk "check/slot-type" t.Ast.tloc
+ "the slot %s of %s is declared %s, and a class slot holds a dyn value, \
+ which can be checked as bool, an integer type, f32, f64, string, a \
+ class, or (Option T) of one of those. Write one of those, or leave the \
+ type out and the slot holds any dyn value: [%s]"
+ fname cls what fname
+ in
+ match t.Ast.t with
+ | Ast.Tname n when class_named ~classes cls n <> None ->
+ Sclass (Option.get (class_named ~classes cls n))
+ | Ast.Tapp ("Option", [ inner ]) ->
+ (match slot_of env ~classes cls fname inner with
+ | (Sval _ | Sclass _) as s -> Sopt s
+ | Sany -> refuse "(Option dyn)"
+ | Sopt _ as s -> refuse ("(Option " ^ slot_text s ^ ")"))
+ | _ ->
+ (match resolve env t with
+ | Types.Dyn -> Sany
+ | (Types.Bool | Types.Int _ | Types.Float _ | Types.String) as t -> Sval t
+ | other -> refuse (Types.to_string other))
+
+(* The type's word in the string the runtime reads: the scalar type's name,
+ [#name] for a class, [?] in front for an Option. See [slot_type_of] in
+ runtime/flan_dyn.c, which is the reader. *)
+let rec slot_word = function
+ | Sany -> ""
+ | Sval t -> Types.to_string t
+ | Sclass c -> "#" ^ c
+ | Sopt s -> "?" ^ slot_word s
+
+(* What the runtime is told a class is: one line per slot, in constructor
+ order, the slot's name and then its type's word after a space — no type
+ for a dyn slot. The same string goes to [flan_dyn_map_new_class] from the
+ constructor and to [flan_dyn_class_def] from a reload, so the two cannot
+ describe one class differently. *)
+let class_spec_of (slots : (string * slot_ty) list) =
+ String.concat "\n"
+ (List.map
+ (fun (n, t) -> match t with Sany -> n | t -> n ^ " " ^ slot_word t)
+ slots)
+
+let class_slots env n = Hashtbl.find_opt env.classes n
+
+(* A class's slot vector, paired by [pair_params]'s rule with the program's
+ class names counted as types — so [[owner point]] is one slot holding a
+ point.
+
+ A lowercase name after a name that is neither a type nor a class is a
+ second untyped slot, which is the rule for a [defn]'s parameters and is
+ not changed here. In a vector that types none of its slots that is the
+ plain reading — [[x y]] is two slots and says nothing more. In one that
+ types some of them, two untyped names side by side are as likely a type
+ nobody has declared, so that is said, at the second name, and the slot
+ stays what the rule makes it. *)
+let pair_slots env ~classes cls (items : Ast.pitem list) : Ast.field list =
+ let fields =
+ pair_params ~also:(fun n -> class_named ~classes cls n <> None) env items
+ in
+ let untyped (f : Ast.field) =
+ match f.Ast.fty.Ast.t with Ast.Tname "dyn" -> true | _ -> false
+ in
+ let written_dyn =
+ List.exists (function Ast.Pname ("dyn", _) -> true | _ -> false) items
+ in
+ if List.exists (fun f -> not (untyped f)) fields && not written_dyn then begin
+ let rec scan = function
+ | (a : Ast.field) :: ((b : Ast.field) :: _ as rest) ->
+ if untyped a && untyped b then
+ prerr_endline
+ (Loc.entry ~mark:'~' ~label:"warning: " b.Ast.floc
+ (Printf.sprintf
+ "%s reads as a slot of %s with no type, because no type or \
+ class is named %s. If it was meant as the type of %s, \
+ declare it; if it is a slot, write its type or write \
+ [%s dyn] to say it holds any value"
+ b.Ast.fname cls b.Ast.fname a.Ast.fname b.Ast.fname));
+ scan rest
+ | _ -> ()
+ in
+ scan fields
+ end;
+ fields
+
(* Every [defn] in the program, with its parameter vector paired. Run as a pass
of its own, after the type names are registered and before any signature is
resolved, so that nothing downstream ever sees an unpaired one. *)
let pair_decls env (decls : Ast.decl list) : Ast.decl list =
+ let classes =
+ List.filter_map
+ (fun (d : Ast.decl) ->
+ match d.Ast.d with Ast.Defclass (n, _) -> Some n | _ -> None)
+ decls
+ in
let fn (f : Ast.fn) =
match f.Ast.praw with
| None -> f
@@ -1586,6 +1724,18 @@ let pair_decls env (decls : Ast.decl list) : Ast.decl list =
List.map
(fun (d : Ast.decl) ->
match d.Ast.d with
+ (* A class's slot vector is paired here and nowhere earlier, for the
+ reason a [defn]'s is, and its constructor is written from the
+ pairs — [Classes.expand] left the declaration as it was for exactly
+ this. *)
+ | Ast.Defclass (n, items) ->
+ let slots = pair_slots env ~classes n items in
+ Hashtbl.replace env.classes n
+ (List.map
+ (fun (f : Ast.field) ->
+ (f.Ast.fname, slot_of env ~classes n f.Ast.fname f.Ast.fty))
+ slots);
+ Classes.constructor n slots d.Ast.dloc
| Ast.Defn f -> { d with Ast.d = Ast.Defn (fn f) }
| Ast.Declare (f, c) -> { d with Ast.d = Ast.Declare (fn f, c) }
| Ast.DeclareC (f, c) -> { d with Ast.d = Ast.DeclareC (fn f, c) }
@@ -2074,12 +2224,20 @@ let mk loc ty e : Tast.expr = { Tast.e; ty; loc }
let unit_at loc = mk loc Types.Unit Tast.Unit
+(* The compiler temp an [and] leaves in its else arm; see [check_if]. *)
+let and_sentinel (x : Ast.expr) =
+ match x.Ast.e with
+ | Ast.Var n -> String.length n > 4 && String.sub n 0 4 = "and~"
+ | _ -> false
+
(* Integer arithmetic over literals alone, folded. Unlike [const_int] no name
is read: a defconst has a type of its own, and only an untyped constant may
stand at a type variable. *)
let rec literal_arith (e : Ast.expr) : int64 option =
match e.Ast.e with
| Ast.Int n -> Some n
+ | Ast.Call ({ Ast.e = Ast.Var "-"; _ }, [ x ]) ->
+ Option.map Int64.neg (literal_arith x)
| Ast.Call ({ Ast.e = Ast.Var op; _ }, x :: y :: rest) ->
let step a b =
match op with
@@ -2098,6 +2256,10 @@ let rec literal_arith (e : Ast.expr) : int64 option =
(literal_arith x) (y :: rest)
| _ -> None
+(* A value with no type until one is asked of it: a literal, or arithmetic
+ over literals alone. *)
+let lone_literal (e : Ast.expr) = is_literal e || literal_arith e <> None
+
(* The environment for a lifted body, built once its own body has been checked
and [caught] is therefore final. spec-memory.md's case 2, and the whole of
@@ -3571,6 +3733,28 @@ let rec check ctx ?want (e : Ast.expr) : Tast.expr =
let tail = ctx.tail in
ctx.tail <- false;
match e.Ast.e with
+ (* A negative literal in a generic body, at an instantiation that made it
+ unsigned. The cast the ordinary refusal names would be wrong at every
+ other type the function is called at, so the fix is one that needs no
+ negative number at all, and the refusal says which call asked. *)
+ | Ast.Int n
+ when Int64.compare n 0L < 0 && ctx.env.chain <> []
+ && (match want with
+ | Some (Types.Int k) -> not (Types.signed k)
+ | _ -> false) ->
+ let t = Option.get want in
+ let gname, _, at = List.nth ctx.env.chain (List.length ctx.env.chain - 1) in
+ let var =
+ match List.find_opt (fun (_, u) -> Types.equal u t) ctx.env.subst with
+ | Some (v, _) -> Printf.sprintf "$%s = %s" v (Types.to_string t)
+ | None -> Types.to_string t
+ in
+ Loc.failk literal_at_want loc
+ ~notes:[ Loc.note at (Printf.sprintf "%s is instantiated at %s here" gname var) ]
+ "%Ld does not fit in %s, which holds no negative number, and %s is called \
+ at %s — the body has to work at every type it is called at, so write \
+ it with no negative literal, as in (- x %Ld) in place of (+ x %Ld)"
+ n (Types.to_string t) gname var (Int64.neg n) n
| Ast.Int n -> int_literal loc ~want ~preds:ctx.env.tvpreds n
| Ast.UInt (n, s) -> wide_literal loc ~want n s
| Ast.Byte b ->
@@ -3669,12 +3853,19 @@ let rec check ctx ?want (e : Ast.expr) : Tast.expr =
| Ast.MapLit (tag, kvs) ->
let m = fresh_slot ctx Types.Dyn in
let mval = mk loc Types.Dyn (Tast.Local m) in
+ (* A class's constructor stores through [flan_dyn_slot_init], which is
+ the plain store plus the slot's type check, worded for the
+ constructor rather than for a [put] nobody wrote. *)
let sets =
List.map
- (fun (k, v) ->
- rt loc Types.Unit "flan_dyn_map_set"
- [ mval; check ctx ~want:Types.Dyn k;
- check ctx ~want:Types.Dyn v ])
+ (fun ((k : Ast.expr), v) ->
+ let args =
+ [ mval; check ctx ~want:Types.Dyn k; check ctx ~want:Types.Dyn v ]
+ in
+ (* The key's location is the slot's, in the defclass: the
+ constructor has no other place of its own to name. *)
+ if tag = None then rt loc Types.Unit "flan_dyn_map_set" args
+ else rt loc Types.Unit "flan_dyn_slot_init" (args @ [ here k.Ast.loc ]))
kvs
in
(* A shape tag, if this is the literal a class's constructor was written
@@ -3685,8 +3876,19 @@ let rec check ctx ?want (e : Ast.expr) : Tast.expr =
match tag with
| None -> rt loc Types.Dyn "flan_dyn_map_new" []
| Some cls ->
+ (* The class's slots and their types ride along, so the first
+ instance built registers the class with the runtime and every
+ store after it — this literal's own included — is checked. A
+ class registered already, by an earlier instance or by a reload,
+ keeps what it has: redefining one is a reload's business. *)
+ let spec =
+ match Hashtbl.find_opt ctx.env.classes cls with
+ | Some slots -> class_spec_of slots
+ | None -> ""
+ in
rt loc Types.Dyn "flan_dyn_map_new_class"
- [ rt loc Types.Dyn "flan_dyn_kw" [ mk loc Types.String (Tast.Str cls) ] ]
+ [ rt loc Types.Dyn "flan_dyn_kw" [ mk loc Types.String (Tast.Str cls) ];
+ mk loc Types.String (Tast.Str spec) ]
in
expect ctx loc ~want
(mk loc Types.Dyn (Tast.Let ([ (m, empty) ], sets @ [ mval ])))
@@ -3805,6 +4007,23 @@ let rec check ctx ?want (e : Ast.expr) : Tast.expr =
let v = check ctx ~want:pty v in
expect ctx loc ~want (mk loc Types.Unit (Tast.Set (p, v)))
end
+ (* (set (get inst :slot) x) — a class instance's declared slot. A call and
+ not a place for [flan_dyn_set_at]'s reason: the runtime has to look at
+ the value to know it is an instance, which of its slots the key names,
+ and whether [x] fits the type that slot was declared with, and it traps
+ on each with a sentence of its own. A typed map has no such place; its
+ entries are written with [put]. *)
+ | Ast.Set (Ast.Pslot (target, k), v) ->
+ let target = check ctx target in
+ if target.Tast.ty <> Types.Dyn then
+ fail loc
+ "(get m k) is a place only on a class instance, and this is %s. A \
+ map's entries are written with put"
+ (Types.to_string target.Tast.ty);
+ let k = check ctx ~want:Types.Dyn k in
+ let v = check ctx ~want:Types.Dyn v in
+ expect ctx loc ~want
+ (rt loc Types.Unit "flan_dyn_slot_set" [ target; k; v; here loc ])
| Ast.Set (p, v) ->
let p, pty = check_place ctx loc p in
let v = check ctx ~want:pty v in
@@ -3826,18 +4045,7 @@ let rec check ctx ?want (e : Ast.expr) : Tast.expr =
{:xs [1 2]} mean what it reads as. Everywhere else brackets stay the
fixed-array literal they always were. *)
| Ast.Arr items when want = Some Types.Dyn ->
- let v = fresh_slot ctx Types.Dyn in
- let vval = mk loc Types.Dyn (Tast.Local v) in
- let pushes =
- List.map
- (fun x ->
- rt loc Types.Unit "flan_dyn_push"
- [ vval; check ctx ~want:Types.Dyn x; here loc ])
- items
- in
- mk loc Types.Dyn
- (Tast.Let ([ (v, rt loc Types.Dyn "flan_dyn_vec_new" []) ],
- pushes @ [ vval ]))
+ dyn_vec ctx loc (map_lr (fun x -> check ctx ~want:Types.Dyn x) items)
| Ast.Arr items -> check_arr ctx ~want loc items
(* (array 4 rl/Vector2). Parse already assembled the whole array type, so
there is nothing to infer: resolve it and hand back its all-bytes-zero
@@ -3852,6 +4060,7 @@ let rec check ctx ?want (e : Ast.expr) : Tast.expr =
fail loc "this is a type, and a value is wanted here"
| Ast.ArrayFill (dims, v) -> check_array_fill ctx ~want loc dims v
| Ast.ArrayGen (dims, f) -> check_array_gen ctx ~want loc dims f
+ | Ast.The (t, v) -> check_the ctx ~want loc t v
| Ast.Match (scrutinee, arms) -> check_match ctx ~tail ?want loc scrutinee arms
(* Constant integer arithmetic where a type variable is wanted is folded to
the literal it computes first, so [(+ x (+ 1 2))] is admitted wherever
@@ -4069,24 +4278,34 @@ and wide_literal loc ~want n s =
(* Arithmetic wraps, but a literal that does not fit its type is a typo, not a
wrap — 300 is never what someone meant by a u8. *)
-and in_range loc k n =
+and in_range ?(pattern = false) loc k n =
let bits = Types.bits k in
let ok =
if Types.signed k then
bits = 64
|| (Int64.compare n (Int64.neg (Int64.shift_left 1L (bits - 1))) >= 0
&& Int64.compare n (Int64.shift_left 1L (bits - 1)) < 0)
- else if bits = 64 then
- (* A literal at or above 2^63 is a [UInt] and never reaches here; see
- [wide_literal]. A negative decimal is accepted as a u64's bit pattern,
- which is a settled rule. Narrower unsigned types keep the strict
- check, which is where a typo like 300 for a u8 actually shows up. *)
- true
+ (* A literal at or above 2^63 is a [UInt] and never reaches here as a
+ literal; see [wide_literal]. [pattern] is the folded-constant path,
+ which holds a u64 as its 64-bit pattern and cannot tell 2^64 - 1 from
+ -1, so there every pattern is a u64. *)
+ else if bits = 64 then pattern || Int64.compare n 0L >= 0
else
Int64.compare n 0L >= 0
&& Int64.compare n (Int64.shift_left 1L bits) < 0
in
if ok then n
+ else if Int64.compare n 0L < 0 && not (Types.signed k) then
+ (* A negative number at an unsigned type is never the value it reads as.
+ The cast is how to ask for the bit pattern, and names what it is. *)
+ let mask =
+ if bits = 64 then -1L else Int64.sub (Int64.shift_left 1L bits) 1L
+ in
+ let tn = Types.ikind_name k in
+ Loc.failk literal_at_want loc
+ "%Ld does not fit in %s, which holds no negative number — write (%s %Ld) \
+ for the %s with the same bits, %Lu"
+ n tn tn n tn (Int64.logand n mask)
else Loc.failk literal_at_want loc "%Ld does not fit in %s" n
(Types.ikind_name k)
@@ -4129,8 +4348,8 @@ and var ctx ?(qualified = false) loc ~want name =
fail loc "expected %s, found None" (Types.to_string other)
| _ ->
fail loc
- "nothing here says what None is an Option of — annotate the \
- function's return type or the binding")
+ "nothing here says what None is an Option of — use it where an \
+ Option is expected, or name one, as in (the (Option i32) None)")
(* spec-memory.md puts the allocator in the calling convention as
[context/allocator] and [context/temp]. They read as names rather than
calls because that is how the spec writes them, and they are dynamic
@@ -5321,6 +5540,27 @@ and check_if ctx ?(tail = false) ?want loc c t e =
no value on the missing side. `when` desugars to this. *)
let t = branch ctx (fun () -> in_tail (fun () -> check ctx t)) in
expect ctx loc ~want (mk loc Types.Unit (Tast.If (c, t, unit_at loc)))
+ (* Two literal arms meet at the wider of their own types, as two literal
+ elements of an array do: [(if c 1 2.5)] is an f64. *)
+ | Some e
+ when want = None && lone_literal t && lone_literal e
+ && (match literal_join ctx t e with
+ | Some j -> not (Types.equal j (Types.Int Types.I32))
+ | None -> false) ->
+ let want = literal_join ctx t e in
+ let t = branch ctx (fun () -> in_tail (fun () -> check ctx ?want t)) in
+ let e = branch ctx (fun () -> in_tail (fun () -> check ctx ?want e)) in
+ mk loc t.Tast.ty (Tast.If (c, t, e))
+ | Some e when want = None && lone_literal t && not (lone_literal e)
+ && not (and_sentinel e) ->
+ (* A literal has no type of its own until something asks, so with no
+ expectation the other arm decides: [(if c 4000000 n)] over an i64 [n]
+ is an i64, as [(+ 4000000 n)] is. *)
+ let e = branch ctx (fun () -> in_tail (fun () -> check ctx e)) in
+ let twant = if e.Tast.ty = Types.Never then None else Some e.Tast.ty in
+ let t = branch ctx (fun () -> in_tail (fun () -> check ctx ?want:twant t)) in
+ let ty = if e.Tast.ty = Types.Never then t.Tast.ty else e.Tast.ty in
+ mk loc ty (Tast.If (c, t, e))
| Some e ->
let t = branch ctx (fun () -> in_tail (fun () -> check ctx ?want t)) in
(* With no expectation the then-branch supplies one for the else-branch,
@@ -5344,12 +5584,6 @@ and check_if ctx ?(tail = false) ?want loc c t e =
sentinel in the then arm, so every operand is already blamed at its own
location; and with an expectation in hand both arms are checked against
it rather than against each other, so nothing here runs. *)
- let and_sentinel (x : Ast.expr) =
- match x.Ast.e with
- | Ast.Var n ->
- String.length n > 4 && String.sub n 0 4 = "and~"
- | _ -> false
- in
let e =
match branch ctx (fun () -> in_tail (fun () -> check ctx ?want:ewant e)) with
| v -> v
@@ -5371,6 +5605,18 @@ and check_if ctx ?(tail = false) ?want loc c t e =
in
mk loc ty (Tast.If (c, t, e))
+(* The type two literals meet at, each at its own type — a wide integer at
+ u64, which is the only type that holds one. *)
+and literal_join ctx (a : Ast.expr) (b : Ast.expr) =
+ let own (x : Ast.expr) =
+ match x.Ast.e with
+ | Ast.UInt _ -> Some (Types.Int Types.U64)
+ | _ -> probe ctx x.Ast.loc (fun () -> (check ctx x).Tast.ty)
+ in
+ match own a, own b with
+ | Some x, Some y -> Types.join x y
+ | _ -> None
+
(* Whether a name would reach a callee if it were called — a global function, a
generic, or a local holding a function value. The three sources [named_call]
itself consults, in its own order; builtins are deliberately not among them,
@@ -5696,63 +5942,31 @@ and check_arr ctx ~want loc items =
| Some (Types.Slice t) -> Some t
| _ -> None
in
- (* With nothing outside saying what the elements are, the first one says:
- [[(f32 1.0) 2.5]] is an [[2 f32]], its [2.5] checked at [f32] the way it
- would be at an [f32] parameter. *)
- let items =
- match elem_want, items with
- | Some _, _ | None, [] -> map_lr (fun i -> check ctx ?want:elem_want i) items
- | None, first :: rest ->
- let first_ast = first in
- let first = check ctx first in
- let want =
- match first.Tast.ty with Types.Never -> None | t -> Some t
- in
- (* A refusal of the element itself says where its type came from. *)
- let one (i : Ast.expr) =
- (match i.Ast.e, want with
- | Ast.UInt (_, text), Some (Types.Int k) when k <> Types.U64 ->
- let first_src =
- match first_ast.Ast.e with
- | Ast.Int _ | Ast.Byte _ -> Some (spell_arg "" first_ast)
- | _ -> None
- in
- Loc.failk literal_at_want i.Ast.loc
- ~notes:
- [ Loc.note first.Tast.loc
- (Printf.sprintf
- "this array's first element is %s, so every element is"
- (Types.ikind_name k)) ]
- "%s does not fit in %s, and only a u64 holds it%s" text
- (Types.ikind_name k)
- (match first_src with
- | Some f ->
- Printf.sprintf " — write the first element as (u64 %s) for an \
- array of u64" f
- | None -> " — make the first element a u64 for an array of u64")
- | _ -> ());
- try check ctx ?want i with
- | Loc.Error d when d.Loc.dloc = i.Ast.loc && want <> None ->
- raise
- (Loc.Error
- { d with
- Loc.notes =
- d.Loc.notes
- @ [ Loc.note first.Tast.loc
- (Printf.sprintf
- "this array's first element is %s, so every \
- element is"
- (Types.to_string first.Tast.ty)) ] })
- in
- first :: map_lr one rest
- in
+ match elem_want, items with
+ | None, _ :: _ ->
+ (match arr_elem_type ctx items with
+ | Some t ->
+ let n = Int64.of_int (List.length items) in
+ expect ctx loc ~want
+ (check_arr ctx ~want:(Some (Types.Array (n, t))) loc items)
+ | None ->
+ (match
+ trial ctx (fun () ->
+ dyn_vec ctx loc (map_lr (fun i -> check ctx ~want:Types.Dyn i) items))
+ with
+ | Ok v -> expect ctx loc ~want v
+ | Error d -> mixed_refusal ctx items d))
+ | _ ->
+ let items = map_lr (fun i -> check ctx ?want:elem_want i) items in
let n = Int64.of_int (List.length items) in
let elem =
match elem_want, items with
| Some t, _ -> t
| None, first :: _ -> first.Tast.ty
| None, [] ->
- fail loc "an empty array literal needs a type — annotate the binding"
+ fail loc
+ "an empty array literal needs a type — use it where one is expected, \
+ or name it, as in (the [0 i32] [])"
in
List.iter
(fun (i : Tast.expr) ->
@@ -5768,6 +5982,206 @@ and check_arr ctx ~want loc items =
an array literal does not satisfy a slice expectation. *)
expect ctx loc ~want (mk loc (Types.Array (n, elem)) (Tast.Arr items))
+(* The element type of an array literal nothing outside it names, or [None]
+ for a dyn vector. Every element is looked at on its own terms first, by
+ [probe], so nothing here is checked for real — [check_arr] does that once,
+ at the answer.
+
+ Elements that agree are a typed array: one type, or numbers that meet at
+ the wider of them the way two operands of [+] do. A literal takes the
+ others' type if it fits it, so [[(f32 1.0) 2.5]] is an [[2 f32]] and
+ [[(u8 1) 300]] an [[2 i32]]. An element that cannot be checked without
+ being told what it is — [None], a bare struct — takes the same type.
+ Elements that do not agree — [[10 "Hi"]], a dyn beside anything that is
+ not one — are a dyn vector, which is what the same brackets are where a
+ dyn is expected. Numbers that do not agree are refused instead; see
+ [numbers_disagree]. *)
+and arr_elem_type ctx (items : Ast.expr list) : Types.t option =
+ let natural (i : Ast.expr) =
+ match i.Ast.e with
+ (* Refused with no want, and only a u64 holds one. *)
+ | Ast.UInt _ -> Some (Types.Int Types.U64)
+ | _ -> probe ctx i.Ast.loc (fun () -> (check ctx i).Tast.ty)
+ in
+ let fits t (i : Ast.expr) =
+ probe ctx i.Ast.loc (fun () -> ignore (check ctx ~want:t i)) <> None
+ in
+ let lits, rest = List.partition lone_literal items in
+ let typed, needs =
+ List.partition_map
+ (fun i ->
+ match natural i with Some t -> Left (i, t) | None -> Right i)
+ rest
+ in
+ let tys =
+ List.filter (fun t -> t <> Types.Never) (List.map snd typed)
+ in
+ let lit_tys = List.filter_map natural lits in
+ let join_all = function
+ | [] -> None
+ | t :: ts ->
+ List.fold_left
+ (fun acc t -> Option.bind acc (fun a -> Types.join a t)) (Some t) ts
+ in
+ let mixed_dyn =
+ List.mem Types.Dyn tys
+ && (List.exists (fun t -> t <> Types.Dyn) tys || lits <> [])
+ in
+ let all_fit t = List.for_all (fits t) lits && List.for_all (fits t) needs in
+ (* A candidate the literals do not all fit is widened by the ones that do
+ not, once: [[x 2.5]] over an i32 [x] meets at f64. *)
+ let settle = function
+ | None -> None
+ | Some t when all_fit t -> Some t
+ | Some t ->
+ let t' =
+ List.fold_left
+ (fun acc i ->
+ if fits t i then acc
+ else Option.bind acc (fun a -> Option.bind (natural i) (Types.join a)))
+ (Some t) lits
+ in
+ (match t' with
+ | Some t' when not (Types.equal t' t) && all_fit t' -> Some t'
+ | _ -> None)
+ in
+ let candidates =
+ if tys <> [] then [ join_all tys ]
+ else join_all lit_tys :: List.map Option.some lit_tys
+ in
+ if mixed_dyn then None
+ else if tys = [] && lits = [] then
+ (match typed, needs with
+ | _ :: _, [] -> Some Types.Never
+ (* Nothing here says what any of them is. The first one's own refusal is
+ the one worth reading. *)
+ | _, first :: _ -> ignore (check ctx first); None
+ | [], [] -> None)
+ else
+ match
+ List.fold_left
+ (fun found c -> match found with Some _ -> found | None -> settle c)
+ None candidates
+ with
+ | Some t -> Some t
+ | None ->
+ let numeric t = match t with Types.Int _ | Types.Float _ -> true | _ -> false in
+ if needs = [] && List.for_all numeric (tys @ lit_tys) then
+ numbers_disagree ctx
+ (List.filter_map
+ (fun i -> Option.map (fun t -> (i, t)) (natural i)) items)
+ else None
+
+(* Numbers with no type they all meet at — an i32 beside an f32, an i64 beside
+ a u64 — are refused rather than boxed into a dyn vector: the elements are
+ all numbers, and which one should move is the program's to say. The fix
+ named converts the second of the first disagreeing pair, into the float
+ when one of the two is a float and into the first's type otherwise. *)
+and numbers_disagree : 'a. ctx -> (Ast.expr * Types.t) list -> 'a =
+ fun ctx elems ->
+ match elems with
+ | [] -> fail Loc.unknown "internal: an array of numbers with no elements"
+ | _ :: _ ->
+ (* A literal is not one of the disagreeing types when it fits the others:
+ each is checked at the type the rest meet at — or, with every element a
+ literal, at the u64 a wide one needs — and the first that does not fit
+ is the refusal, its own. *)
+ let lit (e, _) = lone_literal e in
+ let others = List.filter (fun p -> not (lit p)) elems in
+ let meet =
+ match others with
+ | [] ->
+ if List.exists (fun (e, _) -> match e.Ast.e with Ast.UInt _ -> true | _ -> false) elems
+ then Some (Types.Int Types.U64) else None
+ | (_, t) :: ts ->
+ List.fold_left (fun acc (_, u) -> Option.bind acc (fun a -> Types.join a u))
+ (Some t) ts
+ in
+ (match meet with
+ | Some (Types.Int _ as m) ->
+ List.iter
+ (fun (e, t) ->
+ if lone_literal e && (match t with Types.Int _ -> true | _ -> false)
+ then ignore (check ctx ~want:m e))
+ elems
+ | _ -> ());
+ let pool = if others = [] then elems else others in
+ let first, t1 = List.hd pool in
+ let second, t2 =
+ match List.find_opt (fun (_, t) -> Types.join t1 t = None) (List.tl pool) with
+ | Some p -> p
+ | None ->
+ (match List.find_opt (fun (_, t) -> Types.join t1 t = None) elems with
+ | Some p -> p
+ | None -> List.nth elems (List.length elems - 1))
+ in
+ let target, moved, moved_ty, other =
+ match t1, t2 with
+ | Types.Int _, Types.Float _ -> t2, first, t1, second
+ | _ -> t1, second, t2, first
+ in
+ ignore ctx;
+ let tn = Types.to_string target in
+ Loc.failk "check/array-numbers-disagree" moved.Ast.loc
+ ~notes:[ Loc.note other.Ast.loc (Printf.sprintf "this element is %s" tn) ]
+ "this array's elements are %s and %s, and neither holds every value of \
+ the other — %s"
+ (Types.to_string moved_ty) tn
+ (match spell_arg "" moved with
+ | "" ->
+ Printf.sprintf "convert the %s element with the %s cast" (Types.to_string moved_ty) tn
+ | x -> Printf.sprintf "convert one, as in (%s %s)" tn x)
+
+(* Elements that do not agree and cannot all become a dyn either: a struct
+ beside a number, a type variable beside a literal. The dyn vector's refusal
+ would be about dyn, which the program never mentioned, so the elements are
+ refused against each other instead — the first one's type is what the rest
+ are checked at, and the refusal points back at it. [d] is the answer if
+ that finds nothing. *)
+and mixed_refusal : 'a. ctx -> Ast.expr list -> Loc.diag -> 'a =
+ fun ctx items d ->
+ match items with
+ | [] -> raise (Loc.Error d)
+ | first :: rest ->
+ let first = check ctx first in
+ let want = match first.Tast.ty with Types.Never -> None | t -> Some t in
+ List.iter
+ (fun (i : Ast.expr) ->
+ match check ctx ?want i with
+ | v ->
+ (match want with
+ | Some t when not (Types.fits ~expected:t ~actual:v.Tast.ty) ->
+ fail i.Ast.loc "this array's elements are %s, but this one is %s"
+ (Types.to_string t) (Types.to_string v.Tast.ty)
+ | _ -> ())
+ | exception Loc.Error e when e.Loc.dloc = i.Ast.loc && want <> None ->
+ raise
+ (Loc.Error
+ { e with
+ Loc.notes =
+ e.Loc.notes
+ @ [ Loc.note first.Tast.loc
+ (Printf.sprintf
+ "this array's first element is %s, so every \
+ element is"
+ (match first.Tast.ty with
+ | Types.Var v -> "$" ^ v
+ | t -> Types.to_string t)) ] }))
+ rest;
+ raise (Loc.Error d)
+
+(* A dyn vector built where it stands from elements already checked at dyn:
+ the runtime's own vec, pushed to in order. *)
+and dyn_vec ctx loc (items : Tast.expr list) =
+ let v = fresh_slot ctx Types.Dyn in
+ let vval = mk loc Types.Dyn (Tast.Local v) in
+ let pushes =
+ List.map (fun x -> rt loc Types.Unit "flan_dyn_push" [ vval; x; here loc ])
+ items
+ in
+ mk loc Types.Dyn
+ (Tast.Let ([ (v, rt loc Types.Dyn "flan_dyn_vec_new" []) ], pushes @ [ vval ]))
+
(* ── (array-fill [r c] v) and (array-gen [r c] f) ──────────────────────
TODO.org, "A value-producing array constructor". [(array 4 T)] is
@@ -5886,6 +6300,78 @@ and array_build ctx loc ns elem ~pre ~element =
(Tast.Let (pre @ [ (arr, mk loc aty (Tast.Zero aty)) ],
[ nest ns islots; arrv ]))
+(* (the T e): [e] with [T] as its expectation, which is every conversion an
+ annotation would make — a literal built at T, a narrower number widened —
+ and nothing more. A dyn operand is the exception: an expectation would
+ unbox it and trap at run time on a mismatch, and [the] is a statement about
+ the type rather than a conversion, so it is refused and the cast named.
+
+ [(the [T] [...])] asks for the literal's element type and answers the
+ [n T] the literal is, since an array literal is never a slice. *)
+and check_the ctx ~want loc (t : Ast.texpr) (v : Ast.expr) =
+ let ty = resolve ctx.env t in
+ let is_nil = match v.Ast.e with Ast.Var "nil" -> true | _ -> false in
+ if ty <> Types.Dyn && not is_nil
+ && probe ctx loc (fun () -> (check ctx v).Tast.ty) = Some Types.Dyn
+ then begin
+ let tn = Types.to_string ty in
+ let numeric = match ty with Types.Int _ | Types.Float _ -> true | _ -> false in
+ if numeric then
+ fail v.Ast.loc
+ "the checks a value as %s and does not convert one, and this is a dyn \
+ — %s"
+ tn
+ (match spell_arg "" v with
+ | "" -> Printf.sprintf "convert it with the %s cast instead" tn
+ | s -> Printf.sprintf "write (%s %s) to convert it" tn s)
+ else
+ (* What a dyn does at this type is the boundary's own answer, asked of
+ it rather than restated: some types take one where a value is passed,
+ returned or stored, and the rest do not take one at all. *)
+ let crosses =
+ probe ctx loc (fun () ->
+ ignore (expect ctx v.Ast.loc ~want:(Some ty) (check ctx v)))
+ in
+ match crosses with
+ | Some () ->
+ fail v.Ast.loc
+ "the checks a value as %s and does not convert one, and this is a \
+ dyn — a dyn becomes a %s where a %s is passed, returned or stored"
+ tn tn tn
+ | None ->
+ (match check ctx ~want:ty v with
+ | _ ->
+ fail v.Ast.loc
+ "the checks a value as %s and does not convert one, and this is \
+ a dyn" tn
+ | exception Loc.Error d ->
+ fail v.Ast.loc
+ "the checks a value as %s and does not convert one, and this is \
+ a dyn — %s" tn d.Loc.dmsg)
+ end;
+ let r =
+ match ty, v.Ast.e with
+ | Types.Slice elem, Ast.Arr items ->
+ check_arr ctx
+ ~want:(Some (Types.Array (Int64.of_int (List.length items), elem)))
+ v.Ast.loc items
+ | _ -> expect ctx v.Ast.loc ~want:(Some ty) (check ctx ~want:ty v)
+ in
+ expect ctx loc ~want r
+
+(* [f] run for its answer alone: whatever it wrote into the context is put
+ back whether it succeeded or not, so a form can be checked once to see what
+ it is and then checked again for real. [None] if it was refused. *)
+and probe : 'a. ctx -> Loc.t -> (unit -> 'a) -> 'a option = fun ctx loc f ->
+ let answer = ref None in
+ (match
+ trial ctx (fun () ->
+ answer := Some (f ());
+ raise (Loc.Error (Loc.diag loc "probe")))
+ with
+ | _ -> ());
+ !answer
+
and check_array_fill ctx ~want loc dims v =
let ns = array_dims ctx loc dims in
let elem_want = array_elem_want (List.length ns) want in
@@ -6080,7 +6566,7 @@ and check_match ctx ?(tail = false) ?want loc scrutinee arms =
let want = ref want in
let seen = Hashtbl.create 8 in
let saw_wild = ref false in
- let arms =
+ let resolved =
map_lr
(fun (a : Ast.arm) ->
let ctor, binds = resolve_pat a in
@@ -6091,6 +6577,48 @@ and check_match ctx ?(tail = false) ?want loc scrutinee arms =
fail a.Ast.aloc "this match has two %s arms"
(match subject with `Enum _ -> ":" ^ c | _ -> c);
Hashtbl.add seen c ());
+ (a, ctor, binds))
+ arms
+ in
+ (* With nothing expected of the match, the first arm's type is every arm's —
+ unless that arm is a bare literal, which has no type until asked. So the
+ arms whose value is a literal are checked last, and take their type from
+ the others, as an [if]'s literal arm does. The order is only the order
+ they are checked in; they are put back in source order below. *)
+ let literal_arm ((a : Ast.arm), _, _) =
+ match List.rev a.Ast.body with last :: _ -> lone_literal last | [] -> false
+ in
+ (* Every arm a literal: they meet at the wider of their own types, as an
+ [if]'s two do. *)
+ (if !want = None && resolved <> [] && List.for_all literal_arm resolved then
+ let lasts =
+ List.map (fun ((a : Ast.arm), _, _) -> List.hd (List.rev a.Ast.body))
+ resolved
+ in
+ match lasts with
+ | first :: rest ->
+ let j =
+ List.fold_left
+ (fun acc x ->
+ Option.bind acc (fun a ->
+ Option.bind (literal_join ctx first x) (Types.join a)))
+ (literal_join ctx first first) rest
+ in
+ (match j with
+ | Some t when not (Types.equal t (Types.Int Types.I32)) -> want := Some t
+ | _ -> ())
+ | [] -> ());
+ let order =
+ let idx = List.mapi (fun i r -> (i, r)) resolved in
+ if !want <> None then idx
+ else
+ List.filter (fun (_, r) -> not (literal_arm r)) idx
+ @ List.filter (fun (_, r) -> literal_arm r) idx
+ in
+ let checked =
+ map_lr
+ (fun (i, ((a : Ast.arm), ctor, binds)) ->
+ i,
branch ctx (fun () ->
(* What each name in this arm is, in words, for the one refusal
that needs it: a case pattern binds fields positionally, so the
@@ -6126,7 +6654,10 @@ and check_match ctx ?(tail = false) ?want loc scrutinee arms =
if !want = None && body.Tast.ty <> Types.Never then
want := Some body.Tast.ty;
{ Tast.acase = ctor; binds; abody = [ body ] }))
- arms
+ order
+ in
+ let arms =
+ List.map snd (List.sort (fun (i, _) (j, _) -> compare i j) checked)
in
(* Exhaustiveness is refused, not defaulted. A match that silently fell
through would have to produce a value of the match's type out of nothing,
@@ -6457,6 +6988,13 @@ and check_place ctx loc (p : Ast.place) : Tast.place * Types.t =
| Types.Ptr t -> Tast.Pderef target, t
| other ->
fail loc "deref takes a (Ptr T), found %s" (Types.to_string other))
+ (* Only [set] writes a class slot, and it has its own arm above. A slot
+ lives in a map the collector may move entries of, so it has no address
+ to hand out. *)
+ | Ast.Pslot _ ->
+ fail loc
+ "a class slot (get inst :slot) is written with set and has no address. \
+ Read it into a local with let"
(* An index or a slice bound that is a literal is known now, so it is an error
now rather than a trap later. Only literals: a [defconst] is a global in the
@@ -6609,12 +7147,9 @@ and arity _ctx loc name n args =
Two is the floor, and the two missing cases are refused rather than
invented. Zero operands would have to mean an identity element, 0 for + and
1 for *, and a sum with no terms in it is a typo far more often than it is
- an intent. One operand would have to mean negation for [-] and reciprocal
- for [/], and this language has no unary minus anywhere: the prelude writes
- every negation as [(- 0 n)] or [(- 0.0 x)], and [(- x)] meaning something
- else than the [-] two lines above it is a rule a reader has to carry rather
- than see. Integer division makes the reciprocal worse still: [(/ 3)] would
- be 0.
+ an intent. One operand is refused for every operator but [-], whose one
+ operand form is negation and is [named_call]'s. For [/] it would be the
+ reciprocal, and integer division makes that a trap: [(/ 3)] would be 0.
A one-operand comparison would have to be [true] — there is no pair to
disagree, and nothing for a lone value to be distinct from — and a test
@@ -6623,10 +7158,6 @@ and arity _ctx loc name n args =
and fold_arity loc name args =
match args with
| _ :: _ :: _ -> ()
- | [ _ ] when String.equal name "-" ->
- fail loc
- "- takes two arguments or more, given 1 — there is no unary minus; \
- write (- 0 x) to negate"
| [ _ ] when String.equal name "/" ->
fail loc
"/ takes two arguments or more, given 1 — there is no reciprocal; \
@@ -7013,6 +7544,16 @@ and file_guard ctx loc ~path_slot ~op mk_steps =
missing annotation for a program that had written one. One list, read by
both callers, so the next kind of type added cannot be added to one of
them. *)
+(* An argument written as a type: a type expression, or a bare name that is a
+ type and not a local or a global of the same spelling. *)
+and type_arg ctx (a : Ast.expr) =
+ type_of_expr a <> None
+ || (match a.Ast.e with
+ | Ast.Var n ->
+ lookup ctx n = None && (not (Hashtbl.mem ctx.env.globals n))
+ && type_named ctx n
+ | _ -> false)
+
and type_named ctx n =
(* A type variable names a type here too, which is what lets [(vec-new t)]
and [(vec-new $t)] be written in a generic body: inside an instantiation
@@ -7283,6 +7824,33 @@ and named_call ?(qualified = false) ctx ~want loc name args =
| _ when (not qualified) && shadows_builtin ctx loc name ->
ordinary_call ctx ~want loc name args
(* ── arithmetic and comparison ─────────────────────────────────── *)
+ (* (- x) negates, Clojure's rule. A literal operand is the negative literal,
+ so it takes its type from the site as any literal does. A float is
+ subtracted from -0.0, which is exact negation — 0.0 - 0.0 would answer
+ +0.0 — and an integer from 0, which wraps as (- 0 x) does. *)
+ | "-" when List.length args = 1 ->
+ let x = List.hd args in
+ (match x.Ast.e, literal_arith x with
+ (* Integer arithmetic over literals alone negates to a literal, so
+ [(- (- 1))] is the literal 1 and fits a u8. *)
+ | _, Some n when n <> Int64.min_int ->
+ check ctx ?want { Ast.e = Ast.Int (Int64.neg n); loc }
+ | Ast.Float v, _ -> check ctx ?want { Ast.e = Ast.Float (-.v); loc }
+ | _ ->
+ let v = check ctx ?want:(numeric_want want) x in
+ if v.Tast.ty = Types.Dyn then
+ expect ctx loc ~want (rt loc Types.Dyn "flan_dyn_neg" [ v; here loc ])
+ else begin
+ unconstrained ctx.env loc name ~needs:"numeric?" v.Tast.ty;
+ if not (Types.is_numeric v.Tast.ty || generic_ty v.Tast.ty) then
+ not_numeric name "numbers" v;
+ let zero =
+ match v.Tast.ty with
+ | Types.Float k -> mk loc v.Tast.ty (Tast.Float (-0.0, k))
+ | ty -> int_literal loc ~want:(Some ty) ~preds:ctx.env.tvpreds 0L
+ in
+ expect ctx loc ~want (mk loc v.Tast.ty (Tast.Prim (Tast.Sub, [ zero; v ])))
+ end)
| "+" | "-" | "*" | "/" ->
let p = match name with
| "+" -> Tast.Add | "-" -> Tast.Sub | "*" -> Tast.Mul
@@ -7508,6 +8076,67 @@ and named_call ?(qualified = false) ctx ~want loc name args =
expect ctx loc ~want
(List.fold_left (fun acc arg -> pick acc (check ctx ~want:ty arg))
(pick a b) rest)
+ (* A type handed to the prelude's slice reductions: the reach for the
+ type-limit constants under the name of the reduction beside them. *)
+ | ("max-of" | "min-of")
+ when (not (shadows_builtin ctx loc name))
+ && (match args with [ a ] -> type_arg ctx a | _ -> false) ->
+ let which = if String.equal name "max-of" then "max-value" else "min-value" in
+ fail loc
+ "%s reduces a slice to its %s element, and this is a type — the %s value \
+ of a type is (%s %s)"
+ name (if which = "max-value" then "largest" else "least")
+ (if which = "max-value" then "largest" else "least") which
+ (spell_arg "i32" (List.hd args))
+ (* (max-value T) and (min-value T): the type-limit constants, by type, so a
+ generic body can name its own type's. Odin's max(T) and min(T), and the
+ same answer for a float: the largest finite value and its negation, not
+ the smallest positive one. *)
+ | "max-value" | "min-value" ->
+ arity ctx loc name 1 args;
+ if not (type_arg ctx (List.hd args)) then
+ fail (List.hd args).Ast.loc "%s takes a type, as in (%s i32)" name name;
+ let a = List.hd args in
+ let ty =
+ match type_of_expr a, a.Ast.e with
+ | Some t, _ -> resolve ctx.env t
+ | _, Ast.Var n -> resolve_name ctx.env ~seen:[] a.Ast.loc n
+ | _ -> fail a.Ast.loc "internal: %s's type argument is not a type" name
+ in
+ let max = String.equal name "max-value" in
+ let v =
+ match ty with
+ | Types.Int k ->
+ let b = Types.bits k in
+ let n =
+ if Types.signed k then
+ let top = Int64.shift_left 1L (b - 1) in
+ if max then Int64.sub top 1L else Int64.neg top
+ else if not max then 0L
+ else if b = 64 then -1L
+ else Int64.sub (Int64.shift_left 1L b) 1L
+ in
+ mk loc ty (Tast.Int (n, k))
+ | Types.Float k ->
+ let m =
+ match k with
+ | Types.F32 -> Int32.float_of_bits 0x7f7fffffl
+ | Types.F64 -> Float.max_float
+ in
+ mk loc ty (Tast.Float ((if max then m else -.m), k))
+ | Types.Var v ->
+ if not (declares ctx.env.tvpreds v "numeric?") then
+ Loc.failk "check/unconstrained-type-variable" a.Ast.loc
+ "%s is a limit of a numeric type, and nothing declares $%s \
+ numeric — write {:where (numeric? $%s)} at the head of the body"
+ name v v;
+ int_literal loc ~want:(Some ty) ~preds:ctx.env.tvpreds 0L
+ | _ ->
+ fail a.Ast.loc
+ "%s takes a numeric? type, and %s is not one — as in (%s i32)" name
+ (Types.to_string ty) name
+ in
+ expect ctx loc ~want v
(* (zeroed) is the all-bytes-zero value of whatever it is being stored into,
so it only means anything where a type is expected of it. *)
| "zeroed" ->
@@ -7519,7 +8148,7 @@ and named_call ?(qualified = false) ctx ~want loc name args =
| _ ->
fail loc
"zeroed needs to know the type it is zeroing — use it where one is \
- expected, as in (set grid (zeroed))")
+ expected, or name it, as in (the [4 i32] (zeroed))")
(* [zeroed]'s two siblings, and the same shape exactly: a value of whatever
type is expected of it, so [(set grid (filled 0xFF))] is how a place is
@@ -7616,7 +8245,7 @@ and named_call ?(qualified = false) ctx ~want loc name args =
| _ ->
fail loc
"%s needs to know the type it is filling — use it where one is \
- expected, as in (set grid (%s))"
+ expected, or name it, as in (the [4 u32] (%s))"
name (if is_byte then "filled 0xFF" else name))
(* The one half of a destructuring [let] that [Parse] cannot do on its own.
@@ -8122,12 +8751,14 @@ and named_call ?(qualified = false) ctx ~want loc name args =
let target = check ctx target in
(* A put into a dyn map is a call and nothing else, the way a push into
a dyn vec is: the runtime owns the storage, so there is no guard, no
- restart and no region check. An equal key's value is replaced. *)
+ restart and no region check. An equal key's value is replaced. The
+ site rides along for the one refusal a put can meet, a class
+ instance's typed slot. *)
if target.Tast.ty = Types.Dyn then
expect ctx loc ~want
- (rt loc Types.Unit "flan_dyn_map_set"
+ (rt loc Types.Unit "flan_dyn_map_put"
[ target; check ctx ~want:Types.Dyn k;
- check ctx ~want:Types.Dyn v ])
+ check ctx ~want:Types.Dyn v; here loc ])
else begin
let kt, vt = map_kv loc "put" target.Tast.ty in
let k = check ctx ~want:kt k in
@@ -9431,6 +10062,27 @@ and ordinary_call ctx ~want loc name args =
in
(match Hashtbl.find_opt ctx.env.tracks name with
| Some tr -> expect ctx loc ~want (tracked_call loc ctx.env name tr ret args)
+ | None when Hashtbl.mem ctx.env.classes name ->
+ (* A class's constructor, told where it was called from so that a
+ slot it refuses names this call and not only the defclass. The
+ arguments go into temps first: one may itself construct, and
+ the site is set last, immediately before the call, so nothing
+ between the two can replace it. The constructor takes it as its
+ first act; one reached through a function value finds none. *)
+ let temps =
+ List.map (fun (a : Tast.expr) -> (fresh_slot ctx a.Tast.ty, a)) args
+ in
+ let uses =
+ List.map
+ (fun (s, (a : Tast.expr)) -> mk a.Tast.loc a.Tast.ty (Tast.Local s))
+ temps
+ in
+ expect ctx loc ~want
+ (mk loc ret
+ (Tast.Let
+ (temps,
+ [ rt loc Types.Unit "flan_dyn_ctor_site" [ here loc ];
+ mk loc ret (Tast.Call (name, uses)) ])))
| None -> expect ctx loc ~want (mk loc ret (Tast.Call (name, args))))
| None ->
if Hashtbl.mem ctx.env.datas name then
@@ -10352,7 +11004,8 @@ let builtins : (string * string * string) list =
numeric types meet at the wider one when that cannot lose — i32 and i64 \
add at i64 — and i32 with u32 has no such type and is refused.");
("-", "- [numeric? ...] numeric?",
- "Difference, folded left: (- a b c) is ((a - b) - c).");
+ "Difference, folded left: (- a b c) is ((a - b) - c). With one operand, \
+ its negation: (- x).");
("*", "* [numeric? ...] numeric?",
"Product, folded left over two or more operands of one numeric type.");
("/", "/ [numeric? ...] numeric?",
@@ -10368,7 +11021,8 @@ let builtins : (string * string * string) list =
ordered.");
("!=", "!= [equal? ...] bool",
"All different: (!= a b c) is true when every operand differs from every \
- other, so (!= 1 2 1) is false. Over everything = accepts.");
+ other, so (!= 1 2 1) is false. Over everything = accepts. A float NaN \
+ is != to everything, itself included.");
("<", "< [ordered? ...] bool",
"Less than, chained: (< a b c) is a < b and b < c, and every operand is \
evaluated once. Machine numbers and enums only — ordering a handle \
@@ -10399,6 +11053,13 @@ let builtins : (string * string * string) list =
i16-y) is an i16.");
("max", "max [ordered? ...] ordered?",
"The largest of two or more operands, each evaluated exactly once.");
+ ("max-value", "max-value [type] T",
+ "The largest value of a numeric type: (max-value u8) is 255, and at a \
+ float the largest finite value. Takes a type variable under \
+ {:where (numeric? $t)}.");
+ ("min-value", "min-value [type] T",
+ "The least value of a numeric type: (min-value i8) is -128, 0 at an \
+ unsigned type, and at a float the negation of the largest finite value.");
("zeroed", "zeroed [] T",
"The all-bytes-zero value of whatever it is being stored into, so it \
only means anything where a type is expected of it.");
@@ -10643,9 +11304,9 @@ let builtins : (string * string * string) list =
not hold. It becomes None where an (Option T) is wanted, and stays dyn \
everywhere else.");
("None", "None (Option T)",
- "The absent Option. It takes its type from its context — a return type \
- or an annotated binding — because nothing about the word says what it \
- is an Option of.");
+ "The absent Option. It takes its type from its context — a return type, \
+ a parameter, or (the (Option i32) None) — because nothing about the \
+ word says what it is an Option of.");
("context/allocator", "context/allocator Allocator",
"The allocator in effect here: what with-allocator rebinds, and what an \
allocating operation uses when none is named at the site.");
@@ -10711,6 +11372,22 @@ let rec const_int env (e : Ast.expr) : int64 option =
| Ast.Int n -> Some n
| Ast.Byte b -> Some (Int64.of_int b)
| Ast.Var n -> Hashtbl.find_opt env.consts n
+ | Ast.Call ({ Ast.e = Ast.Var "-"; _ }, [ x ]) ->
+ Option.map Int64.neg (const_int env x)
+ (* A conversion to an integer type, which is how a negative number is
+ written as an unsigned constant's bit pattern: [(u64 -1)]. Truncated to
+ the type's width and extended by its sign, as the cast does at run time. *)
+ | Ast.Call ({ Ast.e = Ast.Var k; _ }, [ x ])
+ when Types.ikind_of_name k <> None ->
+ let k = Option.get (Types.ikind_of_name k) in
+ let bits = Types.bits k in
+ Option.map
+ (fun n ->
+ if bits = 64 then n
+ else if Types.signed k then
+ Int64.shift_right (Int64.shift_left n (64 - bits)) (64 - bits)
+ else Int64.logand n (Int64.sub (Int64.shift_left 1L bits) 1L))
+ (const_int env x)
(* Left to right over any number of operands, because that is how the
checker reads the same form: an array length that type-checks as a
product of three literals and is then not a constant would be a
@@ -11150,7 +11827,11 @@ let collect env (decls : Ast.decl list) =
driver that assembled a declaration list and skipped that pass would
otherwise get a missing name from wherever the constructor was
called, with nothing pointing here. *)
- | Ast.Defclass (n, _) | Ast.Defgeneric { Ast.name = n; _ }
+ | Ast.Defclass (n, _) ->
+ fail loc
+ "internal: the class %s reached the checker unpaired — \
+ pair_decls writes its constructor, and did not run" n
+ | Ast.Defgeneric { Ast.name = n; _ }
| Ast.Defmulti { Ast.name = n; _ } ->
fail loc
"internal: %s reached the checker unexpanded — Classes.expand did \
@@ -11758,12 +12439,26 @@ let check_global env (d : Ast.decl) : Tast.global option =
than the expression it came from: a global's initialiser has to be a
compile-time constant, and [(/ screen-height cell-size)] is one — the
folding pass is the only thing that knows it. *)
+ (* A folded conversion is still a value of the type it converts to. *)
+ (match v.Ast.e, ty with
+ | Ast.Call ({ Ast.e = Ast.Var c; _ }, [ _ ]), Types.Int kind
+ when (match Types.ikind_of_name c with
+ | Some k ->
+ k <> kind
+ && not (Types.widens_to ~from:(Types.Int k) ~into:(Types.Int kind))
+ | None -> false) ->
+ fail v.Ast.loc "expected %s, found %s" (Types.ikind_name kind) c
+ | _ -> ());
let ginit =
match Hashtbl.find_opt env.consts n, ty with
| Some k, Types.Int kind ->
(* Still range-checked: this path skips [check], and [in_range] is the
only thing that rejects 300 as a u8. *)
- { Tast.e = Tast.Int (in_range d.Ast.dloc kind k, kind); ty;
+ { Tast.e =
+ Tast.Int
+ (in_range
+ ~pattern:(match v.Ast.e with Ast.Int _ -> false | _ -> true)
+ v.Ast.loc kind k, kind); ty;
loc = d.Ast.dloc }
| _ -> check (ctx ()) ~want:ty v
in
@@ -12427,6 +13122,68 @@ let is_env_struct = Closures.is_env_struct
let heap_env = Closures.heap_env
let place_closures fns = Closures.place ~dev:false fns
+(* A program's function named as a prelude function takes the name over, the
+ way a definition of a builtin's name does: every call written in the file
+ that defines it reaches the program's, and every call anywhere else — the
+ prelude's own among them, which were written against the prelude's
+ signature — keeps reaching the prelude's. The prelude's is renamed out of
+ the way, under a qualifier no source can spell, rather than dropped.
+ Functions only: a type or a global of the prelude's name is still defined
+ twice. *)
+let prelude_alias = "prelude~"
+
+(* Off for a check whose warnings were already printed for the same source:
+ the dev program re-creating the session its launcher built and warned for. *)
+let print_warnings = ref true
+
+(* A name the renaming above made, which nobody wrote: left out of every
+ listing a person reads, and shown as whose it is where a frame has to be. *)
+let internal_name n = String.starts_with ~prefix:(prelude_alias ^ "/") n
+
+let shown_name n =
+ if internal_name n then
+ let p = String.length prelude_alias + 1 in
+ "the prelude's " ^ String.sub n p (String.length n - p)
+ else n
+
+let shadow_prelude (prelude : Ast.decl list) (decls : Ast.decl list) =
+ let fn_name (d : Ast.decl) =
+ match d.Ast.d with
+ | Ast.Defn fn | Ast.Declare (fn, _) | Ast.DeclareC (fn, _) -> Some fn.Ast.name
+ | _ -> None
+ in
+ let theirs = List.filter_map fn_name prelude in
+ let taken =
+ List.filter_map
+ (fun (d : Ast.decl) ->
+ match fn_name d with
+ | Some n when List.mem n theirs -> Some (n, d.Ast.dloc)
+ | _ -> None)
+ decls
+ in
+ let warnings =
+ List.map
+ (fun (n, at) ->
+ Loc.diag ~kind:"check/shadows-prelude" at
+ (Printf.sprintf
+ "%s shadows the prelude's %s — every call in this file now \
+ reaches your definition"
+ n n))
+ taken
+ in
+ let prelude, decls =
+ List.fold_left
+ (fun (prelude, decls) (n, (at : Loc.t)) ->
+ ( List.map (Load.rename_refs [ n ] prelude_alias) prelude,
+ List.map
+ (fun (d : Ast.decl) ->
+ if String.equal d.Ast.dloc.Loc.file at.Loc.file then d
+ else Load.rename_refs [ n ] prelude_alias d)
+ decls ))
+ (prelude, decls) taken
+ in
+ (prelude @ decls, warnings)
+
let build_program ~keep_going ?tolerate (decls : Ast.decl list) :
Tast.program * env * string list =
let env = new_env () in
@@ -12482,7 +13239,9 @@ let build_program ~keep_going ?tolerate (decls : Ast.decl list) :
end
else raise e)
in
- let decls = Parse.program (Prelude.forms ()) @ decls in
+ let decls, prelude_warnings =
+ shadow_prelude (Parse.program (Prelude.forms ())) decls
+ in
(* Before anything is collected: every (declare-c ...) becomes an ordinary
flattened [declare] with a Flan [defn] over it, and the C that does the
flattening comes back to be compiled into the build. Nothing below this
@@ -12502,11 +13261,12 @@ let build_program ~keep_going ?tolerate (decls : Ast.decl list) :
reload, which is where a defn is most likely to be written. Printed in
the shape [Loc] gives an error, so a checker in an editor parses it the
same way. *)
+ if !print_warnings then
List.iter
(fun (d : Loc.diag) ->
prerr_endline
(Loc.entry ~mark:'~' ~label:"warning: " d.Loc.dloc d.Loc.dmsg))
- (shadowed_builtins decls);
+ (shadowed_builtins decls @ prelude_warnings);
(* Pass one, and it stops at the first thing it refuses. That is not
laziness: every name, type and signature in the file comes from here, so a
declaration this pass could not make sense of leaves a hole that pass two
diff --git a/lib/classes.ml b/lib/classes.ml
index a6b7325b..cc006999 100644
--- a/lib/classes.ml
+++ b/lib/classes.ml
@@ -4,9 +4,11 @@
[(defclass point [x y])] is a constructor. [(defgeneric area [self] dyn)]
and [(defmulti describe [x] dyn (get x :kind))] are each one function whose
body is a dispatch, and [(defmethod area point [p] ...)] is a branch of
- one. Nothing below this pass knows any of the four forms exists: what it
- writes is [defn]s, and they are checked, emitted, rooted, redefined and
- inspected as any other function is.
+ one. What it writes is [defn]s, and they are checked, emitted, rooted,
+ redefined and inspected as any other function is. The one exception is
+ [defclass], which passes through untouched: its slot vector reads like a
+ [defn]'s and cannot be paired until every type name is known, so
+ [Check.pair_decls] pairs it and calls [constructor] below.
**Why a pass and not a macro.** A macro sees one form. This needs the
whole declaration list, because a method may be written anywhere — above
@@ -47,6 +49,46 @@ let dispatch_slot = "~dispatch"
the prelude; this is the only place that builds one. *)
let no_method = "NoMethod"
+(* CLHS's update-instance-for-redefined-class: what a redefined class does to
+ each of its instances, run once per instance at the first [get], [put] or
+ [set] that reaches it after the redefinition. By then the instance already
+ holds the new slots, each kept one with its old value and each gained one
+ at its type's zero value, or nil for an untyped, Option or class slot;
+ [added] is a vec of the gained slots' keywords and [discarded] a map
+ from each lost slot's keyword to the value it held. A method is written for
+ a class, from a live session, and is how a migration does more than match
+ slots by name:
+
+ (defmethod update-instance-for-redefined-class point [p added discarded]
+ (set (get p :radius) (get discarded :r))
+ nil)
+
+ A method that signals stops in the break loop with [migrate-by-name] on
+ offer, which keeps the instance as name-matching left it.
+
+ The generic and its :else method, which does nothing, are written here
+ rather than in the prelude, and only into a program that has a class or a
+ method of the generic: a program with neither would otherwise carry a dyn
+ function and pay for the collector it never uses. [Session] registers the
+ dispatcher's body with the runtime whenever a reload could have changed
+ it. *)
+let migrate_generic = "update-instance-for-redefined-class"
+
+let migrate_decls loc : Ast.decl list =
+ let p n = { Ast.fname = n; fty = dyn_at loc; floc = loc } in
+ let fn body =
+ { Ast.name = migrate_generic;
+ params = [ p "instance"; p "added"; p "discarded" ]; praw = None;
+ ret = Some (dyn_at loc); fwhere = []; fbody = body; nloc = loc;
+ fprivate = Ast.Exported }
+ in
+ [ { Ast.d = Ast.Defgeneric (fn []); dloc = loc };
+ { Ast.d =
+ Ast.Defmethod
+ { Ast.mgen = migrate_generic; mkey = Ast.Delse;
+ mfn = fn [ ex loc (Ast.Var "nil") ]; mkloc = loc };
+ dloc = loc } ]
+
(* ── Collecting ────────────────────────────────────────────────────── *)
type generic = {
@@ -65,23 +107,7 @@ let collect (decls : Ast.decl list) =
List.iter
(fun (d : Ast.decl) ->
match d.Ast.d with
- | Ast.Defclass (n, slots) ->
- (* Two slots of one name would write one entry and read one value,
- and the constructor would take two arguments for it. The duplicate
- parameter that falls out of it is refused by the checker anyway;
- this says which declaration it came from. *)
- let seen = Hashtbl.create 8 in
- List.iter
- (fun (s, sloc) ->
- if Hashtbl.mem seen s then
- Loc.failk "check/duplicate-slot" sloc
- "%s names the slot %s twice. A slot is a key in the \
- instance's map, so the second would replace the first and \
- the constructor would take an argument that goes nowhere"
- n s;
- Hashtbl.replace seen s ())
- slots;
- Hashtbl.replace classes n d.Ast.dloc
+ | Ast.Defclass (n, _) -> Hashtbl.replace classes n d.Ast.dloc
| Ast.Defgeneric fn ->
Hashtbl.replace generics fn.Ast.name
{ gkind = `Class; gfn = fn; gloc = d.Ast.dloc; gms = [] }
@@ -162,14 +188,37 @@ let collect (decls : Ast.decl list) =
omitted slot meaning nil — is deferred, and so is refusing an unknown slot
at [(get p :z)]. Both are recorded in TODO.org, "Class features deferred,
each with its reason". *)
-let constructor n slots loc : Ast.decl =
+let constructor n (slots : Ast.field list) loc : Ast.decl =
+ (* Two slots of one name would write one entry and read one value, and the
+ constructor would take two arguments for it. The duplicate parameter
+ that falls out of it is refused by the checker anyway; this says which
+ declaration it came from. *)
+ let seen = Hashtbl.create 8 in
+ List.iter
+ (fun (f : Ast.field) ->
+ if Hashtbl.mem seen f.Ast.fname then
+ Loc.failk "check/duplicate-slot" f.Ast.floc
+ "%s names the slot %s twice. A slot is a key in the instance's \
+ map, so the second would replace the first and the constructor \
+ would take an argument that goes nowhere"
+ n f.Ast.fname;
+ Hashtbl.replace seen f.Ast.fname ())
+ slots;
+ (* Every parameter is dyn whatever its slot's type. The type is checked
+ where the value is stored — the constructor's own stores included — so
+ a caller holding a dyn passes it as it is, and a caller holding an i32
+ boxes it; neither has to convert to the slot's type first. *)
let params =
List.map
- (fun (s, sloc) -> { Ast.fname = s; fty = dyn_at sloc; floc = sloc })
+ (fun (f : Ast.field) ->
+ { Ast.fname = f.Ast.fname; fty = dyn_at f.Ast.floc; floc = f.Ast.floc })
slots
in
let pairs =
- List.map (fun (s, sloc) -> (ex sloc (Ast.Kw s), ex sloc (Ast.Var s))) slots
+ List.map
+ (fun (f : Ast.field) ->
+ (ex f.Ast.floc (Ast.Kw f.Ast.fname), ex f.Ast.floc (Ast.Var f.Ast.fname)))
+ slots
in
{ Ast.d =
Ast.Defn
@@ -339,11 +388,34 @@ let expand (decls : Ast.decl list) : Ast.decl list =
in
if not has then decls
else begin
+ let decls =
+ let wants =
+ List.find_opt
+ (fun (d : Ast.decl) ->
+ match d.Ast.d with
+ | Ast.Defclass _ -> true
+ | Ast.Defmethod m -> String.equal m.Ast.mgen migrate_generic
+ | _ -> false)
+ decls
+ and declared =
+ List.exists
+ (fun (d : Ast.decl) ->
+ match d.Ast.d with
+ | Ast.Defgeneric f -> String.equal f.Ast.name migrate_generic
+ | _ -> false)
+ decls
+ in
+ match wants with
+ | Some d when not declared -> decls @ migrate_decls d.Ast.dloc
+ | _ -> decls
+ in
let _classes, generics = collect decls in
List.filter_map
(fun (d : Ast.decl) ->
match d.Ast.d with
- | Ast.Defclass (n, slots) -> Some (constructor n slots d.Ast.dloc)
+ (* Kept: its slot vector cannot be paired until every type name is
+ known, so [Check.pair_decls] writes the constructor. *)
+ | Ast.Defclass _ -> Some d
| Ast.Defgeneric fn | Ast.Defmulti fn ->
Some (dispatcher (Hashtbl.find generics fn.Ast.name))
(* Gone: its body is inside its generic's dispatch. *)
diff --git a/lib/dev.ml b/lib/dev.ml
index 2346f35d..1fb4ff7c 100644
--- a/lib/dev.ml
+++ b/lib/dev.ml
@@ -1532,7 +1532,10 @@ let describe t =
ok
[ ":fns "
^ Wire.strings
- (List.map (fun (f : Tast.fn) -> f.Tast.name)
+ (List.filter_map
+ (fun (f : Tast.fn) ->
+ if Check.internal_name f.Tast.name then None
+ else Some f.Tast.name)
t.session.Session.program.Tast.fns);
":globals "
^ Wire.strings
@@ -1618,17 +1621,29 @@ let defs t =
[M-.] on a prelude macro from "the prelude is not a file on disk" into a
shrug about the daemon having no location. *)
let macro_locs = Hashtbl.create 16 in
- (* The classes, off the session's declarations: [Classes.expand] turns a
- [defclass] into its constructor [defn] before the checker runs, so the
- class is not in [Tast.program] or the checker's environment, and the
- declarations are the one place that still has it. Its constructor is
- dropped from the [fn] rows for the macro rows' reason: one name, one row,
- and [class] is what was written. *)
+ (* The classes: where each was written, off the session's declarations,
+ and its slots as the checker paired them, off its environment — the
+ slot vector is unreadable before pairing. The constructor the checker
+ wrote is dropped from the [fn] rows for the macro rows' reason: one name,
+ one row, and [class] is what was written. *)
let classes =
List.filter_map
(fun (d : Ast.decl) ->
match d.Ast.d with
- | Ast.Defclass (n, slots) -> Some (n, List.map fst slots, d.Ast.dloc)
+ | Ast.Defclass (n, _) ->
+ let slots =
+ Option.value ~default:[]
+ (Check.class_slots t.session.Session.env n)
+ in
+ Some
+ (n,
+ List.map
+ (fun (s, ty) ->
+ match ty with
+ | Check.Sany -> s
+ | ty -> s ^ " " ^ Check.slot_text ty)
+ slots,
+ d.Ast.dloc)
| _ -> None)
t.session.Session.decls
in
@@ -1644,6 +1659,7 @@ let defs t =
Hashtbl.replace macro_locs f.Tast.name (Loc.to_string f.Tast.floc);
None
| None when List.mem f.Tast.name class_names -> None
+ | None when Check.internal_name f.Tast.name -> None
| None ->
Some
(entry ~name:f.Tast.name ~kind:"fn" ~sign:(signature_of_fn f)
@@ -2091,7 +2107,7 @@ let backtrace_op t =
(List.map
(fun (name, loc, mine, nslots, _sig, _rsig) ->
Wire.list
- [ Wire.quote name; Wire.quote loc;
+ [ Wire.quote (Check.shown_name name); Wire.quote loc;
Wire.quote (if mine then "program" else "eval");
string_of_int nslots ])
frames);
@@ -5998,7 +6014,10 @@ let merged_setup () =
marshalling a [Session.t] through a file, which buys nothing: the source
cannot have changed between the two, because the build that produced
this binary is the one that exec'd it. *)
+ (* Its warnings were printed by the launcher over the same source. *)
+ Check.print_warnings := false;
let session, _, _ = Session.create_dev ~debug ~x86 ~file () in
+ Check.print_warnings := true;
(* The program's output has to reach an editor exactly as it did when the
daemon held the other end of a pipe. Same pipe, one process: fd 1 is
replaced before the program starts, and the accept loop drains it —
diff --git a/lib/emit.ml b/lib/emit.ml
index 0e27575b..6f63ba31 100644
--- a/lib/emit.ml
+++ b/lib/emit.ml
@@ -2277,8 +2277,11 @@ let icmp_op signed = function
| Tast.Ge -> if signed then "sge" else "uge"
| _ -> assert false
+(* [!=] is unordered and the rest are ordered, so a NaN is unequal to
+ everything, itself included, and neither less, greater nor equal: IEEE 754's
+ answers, and C's and Odin's. *)
let fcmp_op = function
- | Tast.Eq -> "oeq" | Tast.Ne -> "one" | Tast.Lt -> "olt"
+ | Tast.Eq -> "oeq" | Tast.Ne -> "une" | Tast.Lt -> "olt"
| Tast.Le -> "ole" | Tast.Gt -> "ogt" | Tast.Ge -> "oge"
| _ -> assert false
@@ -4777,9 +4780,14 @@ declare i64 @flan_dyn_from_bool(i32)
declare i64 @flan_dyn_from_bytes(ptr, i64)
declare i64 @flan_dyn_vec_new()
declare i64 @flan_dyn_map_new()
-declare i64 @flan_dyn_map_new_class(i64)
+declare i64 @flan_dyn_map_new_class(i64, ptr, i64)
+declare void @flan_dyn_slot_set(i64, i64, i64, ptr, i64)
+declare void @flan_dyn_slot_init(i64, i64, i64, ptr, i64)
+declare void @flan_dyn_ctor_site(ptr, i64)
+declare void @flan_dyn_map_put(i64, i64, i64, ptr, i64)
declare i64 @flan_dyn_class_of(i64)
declare void @flan_dyn_class_def(i64, ptr, i64)
+declare void @flan_dyn_class_hook(ptr)
declare i64 @flan_dyn_kw(ptr, i64)
declare i64 @flan_dyn_map_get(i64, i64)
declare void @flan_dyn_map_set(i64, i64, i64)
@@ -4793,6 +4801,7 @@ declare i64 @flan_dyn_sub(i64, i64, ptr, i64)
declare i64 @flan_dyn_mul(i64, i64, ptr, i64)
declare i64 @flan_dyn_div(i64, i64, ptr, i64)
declare i64 @flan_dyn_rem(i64, i64, ptr, i64)
+declare i64 @flan_dyn_neg(i64, ptr, i64)
declare i64 @flan_dyn_lt(i64, i64, ptr, i64)
declare i64 @flan_dyn_le(i64, i64, ptr, i64)
declare i64 @flan_dyn_gt(i64, i64, ptr, i64)
diff --git a/lib/load.ml b/lib/load.ml
index 17d9b0c8..ad55acf0 100644
--- a/lib/load.ml
+++ b/lib/load.ml
@@ -315,6 +315,7 @@ let rec rename_expr owned alias bound (e : Ast.expr) : Ast.expr =
other reference to it. *)
| Ast.ArrayFill (ds, v) -> Ast.ArrayFill (List.map (rename_len owned alias) ds, go v)
| Ast.ArrayGen (ds, v) -> Ast.ArrayGen (List.map (rename_len owned alias) ds, go v)
+ | Ast.The (t, v) -> Ast.The (rename_texpr owned alias t, go v)
| Ast.Fn (ps, body) ->
Ast.Fn (ps, List.map (rename_expr owned alias (ps @ bound)) body)
| Ast.Dotimes (l, i, b, body) ->
@@ -390,6 +391,7 @@ and rename_place owned alias bound (p : Ast.place) : Ast.place =
| Ast.Pfield (t, f) -> Ast.Pfield (go t, f)
| Ast.Pindex (t, idx) -> Ast.Pindex (go t, List.map go idx)
| Ast.Pderef t -> Ast.Pderef (go t)
+ | Ast.Pslot (t, k) -> Ast.Pslot (go t, go k)
let rename_field owned alias (f : Ast.field) : Ast.field =
{ f with Ast.fty = rename_texpr owned alias f.Ast.fty }
@@ -501,12 +503,23 @@ let qualify_decl owned alias (d : Ast.decl) : Ast.decl =
and the rename is the ordinary one: the declared name, plus whatever
inside them is a name of this package.
- A class's slots are not renamed. They are keywords in the map the
+ A class's slot names are not renamed. They are keywords in the map the
constructor builds, and a keyword belongs to nobody — the same line the
- [MapLit] arm above takes about a map literal's keys. The *class's* name
- is qualified, so [pkg/point] is what an instance's shape tag reads and
- two packages' [point] classes are two classes. *)
- | Ast.Defclass (n, slots) -> Ast.Defclass (qualify alias n, slots)
+ [MapLit] arm above takes about a map literal's keys. The vector is
+ unpaired, so a bare symbol in it may be a slot's name, and none is
+ touched; a type written as a form is renamed as any type is. A bare
+ class name in a type position is found by [Check.pair_slots] against
+ the class's own package instead. The *class's* name is qualified, so
+ [pkg/point] is what an instance's shape tag reads and two packages'
+ [point] classes are two classes. *)
+ | Ast.Defclass (n, slots) ->
+ Ast.Defclass
+ (qualify alias n,
+ List.map
+ (function
+ | Ast.Pname _ as p -> p
+ | Ast.Ptype t -> Ast.Ptype (rename_texpr owned alias t))
+ slots)
(* A generic's parameters are dyn and were written out by the parser, so
there is no unpaired vector here and [bound] is exactly the parameter
names. *)
@@ -554,6 +567,37 @@ let qualify_decl owned alias (d : Ast.decl) : Ast.decl =
in
{ d with Ast.d = k }
+(* [qualify_decl]'s rename of every use of an [owned] name, without the rename
+ of the declaration's own name unless that name is one of them. How a
+ program's definition of a name the prelude also defines takes the name
+ over: the prelude's declaration and its uses outside the program's file
+ move to the qualified name, and the program's file keeps the bare one. *)
+let rename_refs owned alias (d : Ast.decl) : Ast.decl =
+ match Ast.declared_name d, d.Ast.d with
+ | _, (Ast.Package _ | Ast.Import _) -> d
+ | Some n, _ when List.mem n owned -> qualify_decl owned alias d
+ | _ ->
+ let q = qualify_decl owned alias d in
+ let named (fn : Ast.fn) (o : Ast.fn) = { fn with Ast.name = o.Ast.name } in
+ let k =
+ match q.Ast.d, d.Ast.d with
+ | Ast.Declare (fn, c), Ast.Declare (o, _) -> Ast.Declare (named fn o, c)
+ | Ast.DeclareC (fn, c), Ast.DeclareC (o, _) -> Ast.DeclareC (named fn o, c)
+ | Ast.Defn fn, Ast.Defn o -> Ast.Defn (named fn o)
+ | Ast.Defgeneric fn, Ast.Defgeneric o -> Ast.Defgeneric (named fn o)
+ | Ast.Defmulti fn, Ast.Defmulti o -> Ast.Defmulti (named fn o)
+ | Ast.Defenum (_, ms), Ast.Defenum (n, _) -> Ast.Defenum (n, ms)
+ | Ast.Defalias (_, t), Ast.Defalias (n, _) -> Ast.Defalias (n, t)
+ | Ast.Defconst (_, t, v), Ast.Defconst (n, _, _) -> Ast.Defconst (n, t, v)
+ | Ast.Defstruct (_, fs), Ast.Defstruct (n, _) -> Ast.Defstruct (n, fs)
+ | Ast.Defunion (_, fs), Ast.Defunion (n, _) -> Ast.Defunion (n, fs)
+ | Ast.Defdata (_, vs), Ast.Defdata (n, _) -> Ast.Defdata (n, vs)
+ | Ast.Defvar (_, t, i, r), Ast.Defvar (n, _, _, _) -> Ast.Defvar (n, t, i, r)
+ | Ast.Defclass (_, ss), Ast.Defclass (n, _) -> Ast.Defclass (n, ss)
+ | k, _ -> k
+ in
+ { q with Ast.d = k }
+
(* ── Qualifying a package's macros ──────────────────────────────────
The rename above works over the Ast and a macro cannot go that way. By the
time [Parse] is finished with a [defmacro] its quasiquote has been desugared
@@ -790,6 +834,7 @@ let rec expr_uses acc (e : Ast.expr) =
| Ast.MapLit (_, kvs) -> List.iter (fun (k, v) -> go k; go v) kvs
| Ast.Arr items -> gos items
| Ast.ArrayOf t | Ast.TypeArg t -> texpr_uses acc t
+ | Ast.The (t, v) -> texpr_uses acc t; go v
(* A dimension written as a name is a use of that constant, exactly as it is
inside [Tarray]. *)
| Ast.ArrayFill (ds, v) | Ast.ArrayGen (ds, v) ->
@@ -832,6 +877,7 @@ and place_uses acc loc (p : Ast.place) =
| Ast.Pfield (t, _) -> expr_uses acc t
| Ast.Pindex (t, idx) -> expr_uses acc t; List.iter (expr_uses acc) idx
| Ast.Pderef t -> expr_uses acc t
+ | Ast.Pslot (t, k) -> expr_uses acc t; expr_uses acc k
let decl_uses acc (d : Ast.decl) =
let field (f : Ast.field) = texpr_uses acc f.Ast.fty in
@@ -873,9 +919,14 @@ let decl_uses acc (d : Ast.decl) =
| Ast.Zeroed | Ast.Uninit -> ())
| Ast.Defconst (_, t, v) ->
Option.iter (texpr_uses acc) t; expr_uses acc v
- (* A class's slots are keywords and name nothing. Its constructor's body is
- written by [Classes.expand], long after this, out of the slots alone. *)
- | Ast.Defclass _ -> ()
+ (* A class's slot names are keywords and name nothing; its slot types are
+ uses, recorded the way an unpaired [defn] vector's are. *)
+ | Ast.Defclass (_, slots) ->
+ List.iter
+ (function
+ | Ast.Pname (n, loc) -> acc := (n, loc) :: !acc
+ | Ast.Ptype t -> texpr_uses acc t)
+ slots
| Ast.Defgeneric f | Ast.Defmulti f -> fn f
(* The generic is a use — a method in one package extending another's has to
pull that package in — and so is the class in the dispatch slot, for the
diff --git a/lib/parse.ml b/lib/parse.ml
index 519103b1..828b4129 100644
--- a/lib/parse.ml
+++ b/lib/parse.ml
@@ -489,6 +489,15 @@ and form f mk (head : Form.t) (args : Form.t list) : Ast.expr =
array of integers — the wrong reading, and a silent one. Read here, the
brackets are [len]s: the same integer-or-constant's-name the [n T] type
spelling takes, refused by [len] when they are anything else. *)
+ (* ── (the T e) ──────────────────────────────────────────────────── *)
+ | Sym "the" ->
+ (match args with
+ | [ t; v ] -> mk (Ast.The (texpr t, expr v))
+ | _ ->
+ fail f
+ "the is (the TYPE value), as in (the u8 0) — the value, checked as \
+ a TYPE")
+
| Sym (("array-fill" | "array-gen") as which) ->
let usage () =
fail f
@@ -1198,13 +1207,19 @@ and place (f : Form.t) : Ast.place =
either inserts or replaces — so there is no store into a lookup, and an
entry that is absent has no location to store into. Refused here rather
than parsed into a place form the language does not have. *)
+ (* A class instance's slot: declared by its defclass, so it always exists
+ and is a place, where a map's absent key is not. Whether the value is an
+ instance or a map is a run-time fact, so the store is a run-time call
+ and a map there is refused by it. *)
+ | List [ { v = Sym "get"; _ }; target; key ] ->
+ Ast.Pslot (expr target, expr key)
| List ({ v = Sym "get"; _ } :: _) ->
fail f "(get m k) is not a place — a map is written with (put m k v)"
| List [ { v = Sym "deref"; _ }; p ] -> Ast.Pderef (expr p)
| _ ->
fail f
"%s is not assignable. set takes a name, (.field x), (at a i ...), \
- or (deref p)"
+ (deref p), or a class slot (get inst :slot)"
(Form.to_string f)
and arms f (items : Form.t list) : Ast.arm list =
@@ -1469,20 +1484,15 @@ let rec decl (f : Form.t) : Ast.decl =
only names can be read here. *)
| List ({ v = Sym "defclass"; _ } :: args) ->
(match args with
+ (* The slot vector is a [defn]'s parameter vector in every respect —
+ [[x y]] two dyn slots, [[pause bool step bool]] two typed ones — and
+ is carried undecided for the same reason. *)
| [ n; { v = Vec slots; _ } ] ->
- mk (Ast.Defclass
- (dname n,
- List.map
- (fun (s : Form.t) ->
- match s.v with
- | Sym name -> no_sigil s; (name, s.loc)
- | _ ->
- fail s
- "a class slot is a name — its value is dyn, so there \
- is no type to write. Read one with (get p :%s)"
- (Form.to_string s))
- slots))
- | _ -> fail f "defclass is (defclass Name [slot ...])")
+ List.iter
+ (fun (s : Form.t) -> match s.v with Sym _ -> no_sigil s | _ -> ())
+ slots;
+ mk (Ast.Defclass (dname n, pitems slots))
+ | _ -> fail f "defclass is (defclass Name [slot Type ...])")
| List ({ v = Sym ("defgeneric" | "defmulti" as which); _ } :: args) ->
let generic = String.equal which "defgeneric" in
diff --git a/lib/prelude.ml b/lib/prelude.ml
index 880a720a..9eb5ce04 100644
--- a/lib/prelude.ml
+++ b/lib/prelude.ml
@@ -427,6 +427,7 @@ let source = {flan|
;; or more numbers, and a defn cannot shadow a builtin: nothing shadows [+]
;; either. These reduce a slice, which is a different operation with a
;; different arity, so the different name is honest rather than a workaround.
+;; A type's own limits are (min-value T) and (max-value T).
(defn min-of [s [$t]] (Option $t)
{:where (ordered? $t)}
(if (= (length s) 0)
@@ -716,7 +717,7 @@ let source = {flan|
;; The floats are three questions and not two, which is why there is no
;; f32-min here to sit beside f32-max.
;;
-;; A float's least value is just the negation of its greatest — (- 0.0 f32-max)
+;; A float's least value is just the negation of its greatest — (- f32-max)
;; — so a constant for it would say nothing the language cannot. What a caller
;; actually reaches for under the name "min" is the smallest positive one, and
;; that is a different number entirely. Naming it f32-min would make the two
diff --git a/lib/session.ml b/lib/session.ml
index 96766b23..d2cba1f1 100644
--- a/lib/session.ml
+++ b/lib/session.ml
@@ -186,6 +186,10 @@ let stale_sites ?(live = SM.empty) ?(running = false) built (p : Tast.program) :
m acc
in
from ~kept:true live (from ~kept:false built [])
+ (* A caller or callee the prelude-shadowing rename made is not the
+ program's, and there is nothing in the program to recompile for it. *)
+ |> List.filter (fun s ->
+ not (Check.internal_name s.caller || Check.internal_name s.target))
|> List.sort (fun a b ->
match String.compare a.at.Loc.file b.at.Loc.file with
| 0 -> Loc.before a.at b.at
@@ -995,7 +999,8 @@ let eval ?(origin = " A class is a named dyn map with a shape tag. Classes and generic functions
defclass names its
-slots, which carry no types; the constructor is the class's own name and is
-positional; and class-of answers the tag, or nil for
-anything that is not an instance. The slots are map keys, so nothing was added to
-read or write one.[x y] is two slots that hold any value, and [pause bool]
+is one that holds only a bool. The type is checked whenever a value is stored,
+and a slot may be bool, an integer type, f32,
+f64, string, a class, or (Option T) of one of
+those, which also admits nil. The constructor is the class's own name
+and is positional, and class-of answers the tag, or nil
+for anything that is not an instance. The slots are map keys: get
+reads one, and set writes one, as in
+(set (get s :pause) true). put writes one too, and is
+also how a key the class does not declare is added.
Dispatch comes in the two styles and they are one mechanism.
defgeneric dispatches on the class of the first argument, which is
@@ -725,8 +732,8 @@ which is last whatever order it was written in. The generic states the return ty
once, for every method; a method has no return slot; and every parameter of both is
dyn, written or not.
;; A class is a named dyn map with a shape tag. Its slots are names and
-;; carry no types, and its constructor is the class's own name, positional.
+;; A class is a named dyn map with a shape tag. A slot with no type holds
+;; any value, and its constructor is the class's own name, positional.
(defclass point [x y])
(defclass circle [r])
@@ -2214,9 +2221,18 @@ reason is the whole difference between the two: an instance carries a header nam
its class and a flat struct does not. Redefining one re-registers the class and
bumps a generation counter, which is O(1) and walks no heap; every live instance
migrates at its next touch. Slots matched by name keep their values, a gained slot
-appears as nil, a dropped one goes, the object is the same object, and
+starts at its type's zero value — false, 0,
+0.0 or "", and nil for a slot with no type,
+an (Option T) or a class — a dropped one goes, the object is the same object, and
class-of still answers the same tag, so every method still reaches it.
-That is CLHS 4.3.6's protocol without the user hook, which is not built.
+That is CLHS 4.3.6's protocol. Its user hook is
+update-instance-for-redefined-class: a method of it written for a
+class, from the running session, runs on each instance as it migrates, with a
+vec of the slots it gained and a map from each slot it lost to the value that slot
+held. A method that signals stops the program with migrate-by-name
+on offer, which keeps the instance as matching by name left it. A kept value that
+no longer fits its slot's new type is kept, with a warning, and the next write to
+the slot is checked.
C-c C-x rebuilds, relaunches and reconnects, and is the way out while the
above is true. It costs the program's state, which is why it is a key you press rather than