Merge master

This commit is contained in:
Joseph Ferano 2026-09-25 13:36:54 +07:00
commit 5a78cdd202
40 changed files with 2996 additions and 410 deletions

100
TODO.org
View File

@ -147,14 +147,7 @@ CLOSED: [2026-09-25]
=f64-inf=, =f64-nan=, =f32-inf= and =f32-nan= are names the checker supplies =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 (=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 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. =(- f64-inf)=. Rules out Clojure's =##Inf= reader literal.
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.
** DONE A u64 constant above 2^63 cannot be written in decimal ** DONE A u64 constant above 2^63 cannot be written in decimal
CLOSED: [2026-09-25] 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 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 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 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 negative at any integer type before this; it is refused now too. A cast's
decimal is still a =u64= bit pattern. A cast's integer literal that does not fit integer literal that does not fit
=i32= is checked at the cast's type; one that fits keeps the =i32= default, so =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 =(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 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 question, and inheritance or multiple dispatch would create one. Unknown-slot
checking needs class-typed tracking the dyn side deliberately does not have. checking needs class-typed tracking the dyn side deliberately does not have.
** NEXT update-instance-for-redefined-class, the user hook ** DONE 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. CLOSED: [2026-09-25]
Left out of v1 because name matching is the half that makes redefinition usable Taking =migrate-by-name= keeps the name-matched instance, not SBCL's obsolete one,
and the hook is what makes it expressive. The obvious spelling is a generic riding and retries nothing; a transfer from the method to a restart below it traps.
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 The module system stays directory-as-package ** DONE The module system stays directory-as-package
Several files in one directory are one module; a loose file is a module of one, 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] ** DONE An array literal cannot say it is [f32]
CLOSED: [2026-09-25] CLOSED: [2026-09-25]
With nothing outside an array literal naming its element type, the first =(the [f32] [1 2.5])= names the element type; with nothing naming one, a literal
element's type is the want for the rest, so =[(f32 1.0) 2.5]= is a =[2 f32]=. A element takes the other elements' type. Rules out a =1.0f= suffix for now.
refusal of a later element carries a note at the first saying it set the type.
Rules out a =1.0f= suffix for now.
** NEXT A let binding takes no type annotation ** DONE 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. CLOSED: [2026-09-25]
Everything under the surface is there — the binding carries a type slot and the =(the T expr)= gives any expression its want and =let= stays a flat list of
checker consumes it as the want — and only the way it is written is open, because pairs. Rules out a type slot in =let=.
=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.
** NEXT A read-only slice type ** 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=. 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. location.
** DONE A u64 literal is its 64-bit pattern ** DONE A u64 literal is its 64-bit pattern
The cost of accepting the pattern is that a negative decimal literal is accepted A negative literal fits no unsigned type, u64 included; =(u64 -1)= is how the
as a =u64=, because the reader records the value and not how it was written. pattern is written, and a constant folds it. Rules out a negative decimal as a
Narrower unsigned types keep the strict check, which is where a typo like =300= u64's bit pattern.
for a =u8= shows up.
** DONE A folded constant does not skip the range check ** 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 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 =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. 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] ** 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 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 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 ** DONE A redefined defclass migrates its instances lazily
CLOSED: [2026-09-20] 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. 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 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 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 arena traps whether or not a later arena-new reused its record. Rules out
static tracking of destroy, which is move semantics. static tracking of destroy, which is move semantics.
** NEXT A mixed array literal with no want is a dyn vector ** DONE 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 CLOSED: [2026-09-25]
agree (numbers widening together) is typed; one whose elements mix — [10 "Hi"], Elements that agree, numbers meeting at the wider, are typed; elements that mix
[nil 1] — is a dyn vector. (the [T] ...) forces a typed one, and a want from are a dyn vector, except numbers with no common type, which are refused. Rules
context still wins. Replaces the first-element carry-over. out the first element typing the rest.
* Dev loop * 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 ** DONE The dev loop, step 1: the reload primitive
A list of top-level forms is recompiled and installed into a running process, and 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 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 error pattern, so anyone who wants one has the four lines, and the manual carries
them. them.
** NEXT defclass slots take types, checked on write ** DONE 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. CLOSED: [2026-09-25]
=(defclass State [pause bool step bool])= reads as four untyped slots and Constructor parameters stay dyn and each store checks at run time; an int widens
reports a duplicate =bool=. Wanted: the slot list is name/type pairs, as CLOS into a float slot only if it round-trips, and nil fits only an (Option T) slot.
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 println takes up to a second to appear ** DONE println takes up to a second to appear
CLOSED: [2026-09-25] 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, poll and pushes to clients that did not ask for them. See docs/BUILT.md,
"Output and the watch table are pushed". "Output and the watch table are pushed".
** NEXT set writes a class slot; put is for maps ** DONE set writes a class slot; put is for maps
Decided 2026-09-25: as written; one lane with typed slots. CLOSED: [2026-09-25]
=put= exists because an absent map key has no location to store into, which is =put= on an instance still checks a declared slot's type and still inserts an
why =(get m k)= is refused as a place (=lib/parse.ml:1159=). A class instance is undeclared key; only =set= refuses one, since a slot it writes has to exist.
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.
** NEXT update: change a place by applying a function to it ** 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. 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.

View File

@ -362,6 +362,7 @@ let () =
p.globals; p.globals;
List.iter List.iter
(fun (f : Flan.Tast.fn) -> (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 Printf.printf "defn %s : (Fn [%s] %s) %d slots\n" f.name
(String.concat " " (String.concat " "
(List.map Flan.Types.to_string f.params)) (List.map Flan.Types.to_string f.params))
@ -736,6 +737,8 @@ let () =
if List.mem warn_memory_flag rest then if List.mem warn_memory_flag rest then
print_memory_warnings ~file:path p) print_memory_warnings ~file:path p)
in in
Flan.Build.need_main ~file:path ~doing:"flan build has nothing to link"
f.program;
ignore (Flan.Build.executable ignore (Flan.Build.executable
~opts:{ Flan.Build.default with checks; dev; debug; sanitize; ~opts:{ Flan.Build.default with checks; dev; debug; sanitize;
target; x86; target; x86;
@ -915,6 +918,8 @@ let () =
(Printf.sprintf "flan-run-%d" (Unix.getpid ())) (Printf.sprintf "flan-run-%d" (Unix.getpid ()))
in in
let f = Flan.Front.linked ~all:true path 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 ignore (Flan.Build.executable
~opts:{ Flan.Build.default with checks; debug; sanitize; ~opts:{ Flan.Build.default with checks; debug; sanitize;
x86; x86;

View File

@ -129,7 +129,7 @@
(defconst flan--special (defconst flan--special
'("quote" "do" "let" "if" "when" "cond" "and" "or" '("quote" "do" "let" "if" "when" "cond" "and" "or"
"while" "until" "break" "continue" "return" "set" "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" "defer" "some" "try" "signal" "error"
"handler-bind" "handler-case" "restart-case" "invoke-restart") "handler-bind" "handler-case" "restart-case" "invoke-restart")
"The heads `Parse.form' dispatches on — the forms with a meaning of their own. "The heads `Parse.form' dispatches on — the forms with a meaning of their own.

View File

@ -126,6 +126,10 @@ and expr_kind =
dimension; [ArrayFill]'s is the element value itself, evaluated once. *) dimension; [ArrayFill]'s is the element value itself, evaluated once. *)
| ArrayFill of len list * expr | ArrayFill of len list * expr
| ArrayGen 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. *) (* These bind names or alter control flow, so none of them can be a call. *)
| Fn of string list * expr list (* (fn [x y] ...) *) | Fn of string list * expr list (* (fn [x y] ...) *)
(* (dotimes :o [i n] ...), (dotimes [i start stop] ...) and (* (dotimes :o [i n] ...), (dotimes [i start stop] ...) and
@ -199,6 +203,11 @@ and place =
| Pfield of expr * string (* (set (.hp e) v) *) | Pfield of expr * string (* (set (.hp e) v) *)
| Pindex of expr * expr list (* (set (at grid r c) v) *) | Pindex of expr * expr list (* (set (at grid r c) v) *)
| Pderef of expr (* (set (deref p) 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 } and arm = { pat : pattern; body : expr list; aloc : Loc.t }
@ -283,8 +292,10 @@ and decl_kind =
| Defvar of string * texpr option * init * reinit | Defvar of string * texpr option * init * reinit
| Defconst of string * texpr option * expr | Defconst of string * texpr option * expr
(* ── The dyn side's classes and generic functions ────────────────── (* ── The dyn side's classes and generic functions ──────────────────
None of these four reaches [Check]. [Classes.expand] turns the whole set None of these four reaches [Check]'s signature pass. [Classes.expand]
into ordinary [Defn]s before pass one collects anything, the way [Shim] 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 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 constructor, and a generic function is one function whose body is a
dispatch over the methods written for it. 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, anywhere in the file, or arrive at a reload long after the generic did,
and a macro sees one form. *) and a macro sees one form. *)
(* (defclass point [x y]) — the slot names, in constructor order. *) (* (defclass point [x y]) or (defclass state [pause bool step bool]) — the
| Defclass of string * (string * Loc.t) list 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 (* (defgeneric area [self] dyn) — CLOS's class dispatch: the dispatch value
is the shape tag of the first argument. The parameter vector and the is the shape tag of the first argument. The parameter vector and the
return slot are a [defn]'s, and there is no body. *) 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) | Pfield (x, n) -> Pfield (ex x, n)
| Pindex (x, is) -> Pindex (ex x, List.map ex is) | Pindex (x, is) -> Pindex (ex x, List.map ex is)
| Pderef x -> Pderef (ex x) | Pderef x -> Pderef (ex x)
| Pslot (x, k) -> Pslot (ex x, ex k)
in in
let kind = let kind =
match e.e with match e.e with
@ -438,6 +453,7 @@ let map_children f (e : expr) : expr =
subexpressions. The dimensions are [len]s and hold none. *) subexpressions. The dimensions are [len]s and hold none. *)
| ArrayFill (ds, v) -> ArrayFill (ds, ex v) | ArrayFill (ds, v) -> ArrayFill (ds, ex v)
| ArrayGen (ds, f) -> ArrayGen (ds, ex f) | ArrayGen (ds, f) -> ArrayGen (ds, ex f)
| The (t, x) -> The (t, ex x)
| Fn (ps, es) -> Fn (ps, List.map ex es) | Fn (ps, es) -> Fn (ps, List.map ex es)
| Dotimes (l, n, b, es) -> | Dotimes (l, n, b, es) ->
Dotimes (l, n, Dotimes (l, n,

View File

@ -744,6 +744,22 @@ let compile_c ~opts ?tflags ?(warn = []) ~src ~name () =
end; end;
obj 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 (* [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 shim a package binds through, and the arguments needed to link the library
it binds to. it binds to.

File diff suppressed because it is too large Load Diff

View File

@ -4,9 +4,11 @@
[(defclass point [x y])] is a constructor. [(defgeneric area [self] dyn)] [(defclass point [x y])] is a constructor. [(defgeneric area [self] dyn)]
and [(defmulti describe [x] dyn (get x :kind))] are each one function whose 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 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 one. What it writes is [defn]s, and they are checked, emitted, rooted,
writes is [defn]s, and they are checked, emitted, rooted, redefined and redefined and inspected as any other function is. The one exception is
inspected as any other function 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 **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 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. *) the prelude; this is the only place that builds one. *)
let no_method = "NoMethod" 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 ────────────────────────────────────────────────────── *) (* ── Collecting ────────────────────────────────────────────────────── *)
type generic = { type generic = {
@ -65,23 +107,7 @@ let collect (decls : Ast.decl list) =
List.iter List.iter
(fun (d : Ast.decl) -> (fun (d : Ast.decl) ->
match d.Ast.d with match d.Ast.d with
| Ast.Defclass (n, slots) -> | Ast.Defclass (n, _) -> Hashtbl.replace classes n d.Ast.dloc
(* 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.Defgeneric fn -> | Ast.Defgeneric fn ->
Hashtbl.replace generics fn.Ast.name Hashtbl.replace generics fn.Ast.name
{ gkind = `Class; gfn = fn; gloc = d.Ast.dloc; gms = [] } { 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 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, at [(get p :z)]. Both are recorded in TODO.org, "Class features deferred,
each with its reason". *) 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 = let params =
List.map 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 slots
in in
let pairs = 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 in
{ Ast.d = { Ast.d =
Ast.Defn Ast.Defn
@ -339,11 +388,34 @@ let expand (decls : Ast.decl list) : Ast.decl list =
in in
if not has then decls if not has then decls
else begin 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 let _classes, generics = collect decls in
List.filter_map List.filter_map
(fun (d : Ast.decl) -> (fun (d : Ast.decl) ->
match d.Ast.d with 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 -> | Ast.Defgeneric fn | Ast.Defmulti fn ->
Some (dispatcher (Hashtbl.find generics fn.Ast.name)) Some (dispatcher (Hashtbl.find generics fn.Ast.name))
(* Gone: its body is inside its generic's dispatch. *) (* Gone: its body is inside its generic's dispatch. *)

View File

@ -1532,7 +1532,10 @@ let describe t =
ok ok
[ ":fns " [ ":fns "
^ Wire.strings ^ 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); t.session.Session.program.Tast.fns);
":globals " ":globals "
^ Wire.strings ^ 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 [M-.] on a prelude macro from "the prelude is not a file on disk" into a
shrug about the daemon having no location. *) shrug about the daemon having no location. *)
let macro_locs = Hashtbl.create 16 in let macro_locs = Hashtbl.create 16 in
(* The classes, off the session's declarations: [Classes.expand] turns a (* The classes: where each was written, off the session's declarations,
[defclass] into its constructor [defn] before the checker runs, so the and its slots as the checker paired them, off its environment — the
class is not in [Tast.program] or the checker's environment, and the slot vector is unreadable before pairing. The constructor the checker
declarations are the one place that still has it. Its constructor is wrote is dropped from the [fn] rows for the macro rows' reason: one name,
dropped from the [fn] rows for the macro rows' reason: one name, one row, one row, and [class] is what was written. *)
and [class] is what was written. *)
let classes = let classes =
List.filter_map List.filter_map
(fun (d : Ast.decl) -> (fun (d : Ast.decl) ->
match d.Ast.d with 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) | _ -> None)
t.session.Session.decls t.session.Session.decls
in in
@ -1644,6 +1659,7 @@ let defs t =
Hashtbl.replace macro_locs f.Tast.name (Loc.to_string f.Tast.floc); Hashtbl.replace macro_locs f.Tast.name (Loc.to_string f.Tast.floc);
None None
| None when List.mem f.Tast.name class_names -> None | None when List.mem f.Tast.name class_names -> None
| None when Check.internal_name f.Tast.name -> None
| None -> | None ->
Some Some
(entry ~name:f.Tast.name ~kind:"fn" ~sign:(signature_of_fn f) (entry ~name:f.Tast.name ~kind:"fn" ~sign:(signature_of_fn f)
@ -2091,7 +2107,7 @@ let backtrace_op t =
(List.map (List.map
(fun (name, loc, mine, nslots, _sig, _rsig) -> (fun (name, loc, mine, nslots, _sig, _rsig) ->
Wire.list 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"); Wire.quote (if mine then "program" else "eval");
string_of_int nslots ]) string_of_int nslots ])
frames); frames);
@ -5998,7 +6014,10 @@ let merged_setup () =
marshalling a [Session.t] through a file, which buys nothing: the source marshalling a [Session.t] through a file, which buys nothing: the source
cannot have changed between the two, because the build that produced cannot have changed between the two, because the build that produced
this binary is the one that exec'd it. *) 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 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 (* 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 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 — replaced before the program starts, and the accept loop drains it —

View File

@ -2277,8 +2277,11 @@ let icmp_op signed = function
| Tast.Ge -> if signed then "sge" else "uge" | Tast.Ge -> if signed then "sge" else "uge"
| _ -> assert false | _ -> 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 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" | Tast.Le -> "ole" | Tast.Gt -> "ogt" | Tast.Ge -> "oge"
| _ -> assert false | _ -> 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_from_bytes(ptr, i64)
declare i64 @flan_dyn_vec_new() declare i64 @flan_dyn_vec_new()
declare i64 @flan_dyn_map_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 i64 @flan_dyn_class_of(i64)
declare void @flan_dyn_class_def(i64, ptr, 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_kw(ptr, i64)
declare i64 @flan_dyn_map_get(i64, i64) declare i64 @flan_dyn_map_get(i64, i64)
declare void @flan_dyn_map_set(i64, 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_mul(i64, i64, ptr, i64)
declare i64 @flan_dyn_div(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_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_lt(i64, i64, ptr, i64)
declare i64 @flan_dyn_le(i64, i64, ptr, i64) declare i64 @flan_dyn_le(i64, i64, ptr, i64)
declare i64 @flan_dyn_gt(i64, i64, ptr, i64) declare i64 @flan_dyn_gt(i64, i64, ptr, i64)

View File

@ -315,6 +315,7 @@ let rec rename_expr owned alias bound (e : Ast.expr) : Ast.expr =
other reference to it. *) other reference to it. *)
| Ast.ArrayFill (ds, v) -> Ast.ArrayFill (List.map (rename_len owned alias) ds, go v) | 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.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, body) ->
Ast.Fn (ps, List.map (rename_expr owned alias (ps @ bound)) body) Ast.Fn (ps, List.map (rename_expr owned alias (ps @ bound)) body)
| Ast.Dotimes (l, i, b, 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.Pfield (t, f) -> Ast.Pfield (go t, f)
| Ast.Pindex (t, idx) -> Ast.Pindex (go t, List.map go idx) | Ast.Pindex (t, idx) -> Ast.Pindex (go t, List.map go idx)
| Ast.Pderef t -> Ast.Pderef (go t) | 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 = let rename_field owned alias (f : Ast.field) : Ast.field =
{ f with Ast.fty = rename_texpr owned alias f.Ast.fty } { 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 and the rename is the ordinary one: the declared name, plus whatever
inside them is a name of this package. 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 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 [MapLit] arm above takes about a map literal's keys. The vector is
is qualified, so [pkg/point] is what an instance's shape tag reads and unpaired, so a bare symbol in it may be a slot's name, and none is
two packages' [point] classes are two classes. *) touched; a type written as a form is renamed as any type is. A bare
| Ast.Defclass (n, slots) -> Ast.Defclass (qualify alias n, slots) 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 (* 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 there is no unpaired vector here and [bound] is exactly the parameter
names. *) names. *)
@ -554,6 +567,37 @@ let qualify_decl owned alias (d : Ast.decl) : Ast.decl =
in in
{ d with Ast.d = k } { 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 ────────────────────────────────── (* ── Qualifying a package's macros ──────────────────────────────────
The rename above works over the Ast and a macro cannot go that way. By the 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 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.MapLit (_, kvs) -> List.iter (fun (k, v) -> go k; go v) kvs
| Ast.Arr items -> gos items | Ast.Arr items -> gos items
| Ast.ArrayOf t | Ast.TypeArg t -> texpr_uses acc t | 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 (* A dimension written as a name is a use of that constant, exactly as it is
inside [Tarray]. *) inside [Tarray]. *)
| Ast.ArrayFill (ds, v) | Ast.ArrayGen (ds, v) -> | 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.Pfield (t, _) -> expr_uses acc t
| Ast.Pindex (t, idx) -> expr_uses acc t; List.iter (expr_uses acc) idx | Ast.Pindex (t, idx) -> expr_uses acc t; List.iter (expr_uses acc) idx
| Ast.Pderef t -> expr_uses acc t | 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 decl_uses acc (d : Ast.decl) =
let field (f : Ast.field) = texpr_uses acc f.Ast.fty in 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.Zeroed | Ast.Uninit -> ())
| Ast.Defconst (_, t, v) -> | Ast.Defconst (_, t, v) ->
Option.iter (texpr_uses acc) t; expr_uses acc 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 (* A class's slot names are keywords and name nothing; its slot types are
written by [Classes.expand], long after this, out of the slots alone. *) uses, recorded the way an unpaired [defn] vector's are. *)
| Ast.Defclass _ -> () | 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 | Ast.Defgeneric f | Ast.Defmulti f -> fn f
(* The generic is a use — a method in one package extending another's has to (* 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 pull that package in — and so is the class in the dispatch slot, for the

View File

@ -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 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 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. *) 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) -> | Sym (("array-fill" | "array-gen") as which) ->
let usage () = let usage () =
fail f 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 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 entry that is absent has no location to store into. Refused here rather
than parsed into a place form the language does not have. *) 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"; _ } :: _) -> | List ({ v = Sym "get"; _ } :: _) ->
fail f "(get m k) is not a place — a map is written with (put m k v)" 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) | List [ { v = Sym "deref"; _ }; p ] -> Ast.Pderef (expr p)
| _ -> | _ ->
fail f fail f
"%s is not assignable. set takes a name, (.field x), (at a i ...), \ "%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) (Form.to_string f)
and arms f (items : Form.t list) : Ast.arm list = 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. *) only names can be read here. *)
| List ({ v = Sym "defclass"; _ } :: args) -> | List ({ v = Sym "defclass"; _ } :: args) ->
(match args with (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; _ } ] -> | [ n; { v = Vec slots; _ } ] ->
mk (Ast.Defclass List.iter
(dname n, (fun (s : Form.t) -> match s.v with Sym _ -> no_sigil s | _ -> ())
List.map slots;
(fun (s : Form.t) -> mk (Ast.Defclass (dname n, pitems slots))
match s.v with | _ -> fail f "defclass is (defclass Name [slot Type ...])")
| 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 ({ v = Sym ("defgeneric" | "defmulti" as which); _ } :: args) -> | List ({ v = Sym ("defgeneric" | "defmulti" as which); _ } :: args) ->
let generic = String.equal which "defgeneric" in let generic = String.equal which "defgeneric" in

View File

@ -427,6 +427,7 @@ let source = {flan|
;; or more numbers, and a defn cannot shadow a builtin: nothing shadows [+] ;; or more numbers, and a defn cannot shadow a builtin: nothing shadows [+]
;; either. These reduce a slice, which is a different operation with a ;; either. These reduce a slice, which is a different operation with a
;; different arity, so the different name is honest rather than a workaround. ;; 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) (defn min-of [s [$t]] (Option $t)
{:where (ordered? $t)} {:where (ordered? $t)}
(if (= (length s) 0) (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 ;; The floats are three questions and not two, which is why there is no
;; f32-min here to sit beside f32-max. ;; 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 ;; — 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 ;; 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 ;; that is a different number entirely. Naming it f32-min would make the two

View File

@ -186,6 +186,10 @@ let stale_sites ?(live = SM.empty) ?(running = false) built (p : Tast.program) :
m acc m acc
in in
from ~kept:true live (from ~kept:false built []) 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 -> |> List.sort (fun a b ->
match String.compare a.at.Loc.file b.at.Loc.file with match String.compare a.at.Loc.file b.at.Loc.file with
| 0 -> Loc.before a.at b.at | 0 -> Loc.before a.at b.at
@ -995,7 +999,8 @@ let eval ?(origin = "<eval>") ?base ?forms ?pause ?(running = true) t src : chan
List.filter_map List.filter_map
(fun (d : Ast.decl) -> (fun (d : Ast.decl) ->
match d.Ast.d with match d.Ast.d with
| Ast.Defclass (n, slots) -> Some (n, List.map fst slots) | Ast.Defclass (n, _) ->
Some (n, Option.value ~default:[] (Check.class_slots env n))
| _ -> None) | _ -> None)
incoming incoming
in in
@ -1153,21 +1158,40 @@ let eval ?(origin = "<eval>") ?base ?forms ?pause ?(running = true) t src : chan
definition the registry has never seen has to arrive somehow. *) definition the registry has never seen has to arrive somehow. *)
let class_body = let class_body =
let str s : Tast.expr = { Tast.e = Tast.Str s; ty = Types.String; loc } in let str s : Tast.expr = { Tast.e = Tast.Str s; ty = Types.String; loc } in
List.map (* And the hook a migration calls, re-registered by every module that
could have changed what it should be: one carrying a class, since
that is what makes migrations happen, and one carrying a method of
the generic, since that is what changes the body. The address is the
cell's contents at the time the thunk runs — after this module's
bodies are published — so it is the body just installed. *)
let hook =
let n = Classes.migrate_generic in
if incoming_classes <> [] || List.mem n names then
let ty =
Types.CFn ([ Types.Dyn; Types.Dyn; Types.Dyn ], Types.Dyn)
in
[ { Tast.e =
Tast.Prim (Tast.Rt "flan_dyn_class_hook",
[ { Tast.e = Tast.FnAddr (Tast.Fnval n); ty; loc } ]);
ty = Types.Unit; loc } ]
else []
in
hook @ List.map
(fun (n, slots) : Tast.expr -> (fun (n, slots) : Tast.expr ->
let kw : Tast.expr = let kw : Tast.expr =
{ Tast.e = Tast.Prim (Tast.Rt "flan_dyn_kw", [ str n ]); { Tast.e = Tast.Prim (Tast.Rt "flan_dyn_kw", [ str n ]);
ty = Types.Dyn; loc } ty = Types.Dyn; loc }
in in
(* The slot names in one string, newline between: the runtime (* The slots in one string, a line each with the slot's type
splits them. A dyn vector would have been the obvious shape after its name: the runtime splits them. A dyn vector would
and is the wrong one — it is a collector object, so the have been the obvious shape and is the wrong one — it is a
registry would hold something the marker has to reach, where collector object, so the registry would hold something the
a packed string reaches interned keywords that are immortal marker has to reach, where a packed string reaches interned
already. *) keywords that are immortal already. The constructor carries
the same string, from the same function. *)
{ Tast.e = { Tast.e =
Tast.Prim (Tast.Rt "flan_dyn_class_def", Tast.Prim (Tast.Rt "flan_dyn_class_def",
[ kw; str (String.concat "\n" slots) ]); [ kw; str (Check.class_spec_of slots) ]);
ty = Types.Unit; loc }) ty = Types.Unit; loc })
incoming_classes incoming_classes
in in

View File

@ -1318,18 +1318,20 @@ let int_cc ~signed (p : Tast.prim) =
(* Parity, which on [ucomis] means "unordered": one of the operands was a NaN. (* Parity, which on [ucomis] means "unordered": one of the operands was a NaN.
Nothing else in this file reads it. *) Nothing else in this file reads it. *)
let cc_np = 11 let cc_p = 10 and cc_np = 11
(* [ucomis] sets the flags the *unsigned* codes read, whichever way the (* [ucomis] sets the flags the *unsigned* codes read, whichever way the
operands are signed, so a float comparison never uses l/g — and it sets operands are signed, so a float comparison never uses l/g — and it sets
CF, ZF and PF all at once when either operand is a NaN. CF, ZF and PF all at once when either operand is a NaN.
That last part is why this is not simply the unsigned table. Every That last part is why this is not simply the unsigned table. Every
comparison Flan has is LLVM's *ordered* one ([emit.ml]'s [fcmp_op]: oeq, comparison Flan has but one is LLVM's *ordered* one ([emit.ml]'s
one, olt, ...), which answers false for a NaN, and [setb] after an [fcmp_op]: oeq, olt, ...), which answers false for a NaN, and [setb] after
unordered compare answers true. So [<] and [<=] swap their operands and ask an unordered compare answers true. So [<] and [<=] swap their operands and
for a/ae, which are the two codes a NaN makes false; [=] and [!=] cannot be ask for a/ae, which are the two codes a NaN makes false; [=] cannot be
spelled by one code at all and take a second [setnp] beside them. spelled by one code at all and takes a second [setnp] beside it. The one is
[!=], which is [une] — true for a NaN, as IEEE 754 and C have it — and is
[setne] or'd with [setp].
[(not (= x x))] is how [format-f64] in the prelude detects a NaN, and it is [(not (= x x))] is how [format-f64] in the prelude detects a NaN, and it is
the whole of the difference: with [sete] alone, [(/ 0.0 0.0)] formatted as the whole of the difference: with [sete] alone, [(/ 0.0 0.0)] formatted as
@ -1345,7 +1347,7 @@ let float_cc (p : Tast.prim) =
| _ -> unsupported "not a comparison" | _ -> unsupported "not a comparison"
let float_ordered (p : Tast.prim) = let float_ordered (p : Tast.prim) =
match p with Tast.Eq | Tast.Ne -> true | _ -> false match p with Tast.Eq -> true | _ -> false
let is_cmp (p : Tast.prim) = let is_cmp (p : Tast.prim) =
match p with match p with
@ -3274,6 +3276,12 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) dst =
movzx8 f.b ~dst:rcx ~src:rcx; movzx8 f.b ~dst:rcx ~src:rcx;
and_rr f.b ~dst:rax ~src:rcx and_rr f.b ~dst:rax ~src:rcx
end end
else if p = Tast.Ne then begin
movzx8 f.b ~dst:rax ~src:rax;
setcc f.b ~cc:cc_p ~dst:rcx;
movzx8 f.b ~dst:rcx ~src:rcx;
or_rr f.b ~dst:rax ~src:rcx
end
end else begin end else begin
load_loc f ~reg:rax la a.Tast.ty; load_loc f ~reg:rax la a.Tast.ty;
load_loc f ~reg:rcx lb b.Tast.ty; load_loc f ~reg:rcx lb b.Tast.ty;

View File

@ -1475,8 +1475,8 @@ flan_dyn flan_dyn_map_new(void) {
* *
* The registry a redefined (defclass ...) updates, and the lazy migration * The registry a redefined (defclass ...) updates, and the lazy migration
* that makes the instances built against the old definition answer the new * that makes the instances built against the old definition answer the new
* one. This is CLHS 4.3.6 — [update-instance-for-redefined-class] — with the * one. This is CLHS 4.3.6, [update-instance-for-redefined-class] included —
* user hook left out; docs/SBCL-REDEFINITION-NOTES.md is where the protocol * see [class_hook]; docs/SBCL-REDEFINITION-NOTES.md is where the protocol
* was read off and candidate C is this. * was read off and candidate C is this.
* *
* **Why a registry at all, when a class instance is already just a map.** * **Why a registry at all, when a class instance is already just a map.**
@ -1494,14 +1494,17 @@ flan_dyn flan_dyn_map_new(void) {
* of dyn vectors would have needed both, and would have needed them to * of dyn vectors would have needed both, and would have needed them to
* survive a collection triggered from inside a migration. * survive a collection triggered from inside a migration.
* *
* **What the registry does not do.** It does not constrain [put]. A class * **What the registry constrains.** A store into a slot the class declares
* — the constructor's, [put]'s, [set]'s — is checked against the slot's
* type. A key the class does not declare is not refused by [put]: a class
* instance is an open map — TODO.org, "Class features deferred, each with its * instance is an open map — TODO.org, "Class features deferred, each with its
* reason", already defers unknown-slot checking — so a key nobody declared * reason", defers unknown-slot checking — so a key nobody declared can be
* can be written to one, and the migration below will *drop* it at the next * written to one, and the migration below will *drop* it at the next
* redefinition, because its rule is that an instance's keys are the class's * redefinition, because its rule is that an instance's keys are the class's
* slots. That is real data loss and it is written down as such in TODO.org, * slots. That is real data loss and it is written down as such in TODO.org,
* "A redefined defclass migrates its instances lazily", rather than dressed * "A redefined defclass migrates its instances lazily", rather than dressed
* up as enforcement. * up as enforcement. [set] does refuse an undeclared key, because a slot it
* writes has to exist.
* *
* **Where a migration happens.** [want_map], so every [get], [put] and * **Where a migration happens.** [want_map], so every [get], [put] and
* [has-key?]; [flan_dyn_len]'s map arm; and [dyn_equal]'s, so two instances * [has-key?]; [flan_dyn_len]'s map arm; and [dyn_equal]'s, so two instances
@ -1541,11 +1544,36 @@ flan_dyn flan_dyn_map_new(void) {
* runs ahead of its definition declares itself. */ * runs ahead of its definition declares itself. */
flan_dyn flan_dyn_kw(const uint8_t *p, int64_t n); flan_dyn flan_dyn_kw(const uint8_t *p, int64_t n);
/* What a slot may hold. A dyn value's tag is the whole of what can be asked
* of it, so these are the tags — plus a range on top of the int tag for a
* narrower integer type, the significand a float slot holds exactly, a
* class for a slot declared with one, and whether nil is admitted, which is
* what (Option T) says. [word] is a scalar type's name, for the sentence a
* refusal prints. */
enum { ST_ANY, ST_BOOL, ST_INT, ST_FLOAT, ST_TEXT, ST_CLASS };
typedef struct slot_type {
uint8_t kind;
uint8_t opt; /* nil admitted: (Option T) */
uint8_t fbits; /* ST_FLOAT: 53 for f64, 24 for f32 */
int64_t lo, hi; /* ST_INT only */
kw_entry *cls; /* ST_CLASS only */
const char *word; /* static; NULL for ST_ANY and ST_CLASS */
} slot_type;
typedef struct class_entry { typedef struct class_entry {
kw_entry *name; kw_entry *name;
kw_entry **slots; /* interned, immortal, in declaration order */ kw_entry **slots; /* interned, immortal, in declaration order */
slot_type *types; /* one per slot, same order */
/* Per slot, the generation a migration last warned about a value that no
* longer fits the slot's type. One warning per slot per redefinition,
* however many instances carry such a value. */
uint32_t *warned;
int64_t nslots; int64_t nslots;
uint32_t gen; uint32_t gen;
/* Whether any slot has a type. A class with none pays nothing at a store
* beyond reading this. */
int typed;
} class_entry; } class_entry;
static class_entry *classes; static class_entry *classes;
@ -1559,74 +1587,165 @@ static class_entry *class_find(kw_entry *name) {
} }
/* The generation a new instance of [name] is stamped with. Zero for a class /* The generation a new instance of [name] is stamped with. Zero for a class
* no definition has been registered for, which is every class in a program * no definition has been registered for — which, now that the constructor
* that was built and never reloaded: nothing has changed shape, so nothing * registers its class, is only an instance built by something other than a
* needs to migrate, and the registry earns its keep only once an editor has * constructor: test/dyn_ops.c, calling the runtime directly. */
* sent a new definition. */
static uint32_t class_gen(kw_entry *name) { static uint32_t class_gen(kw_entry *name) {
class_entry *e = class_find(name); class_entry *e = class_find(name);
return e == NULL ? 0u : e->gen; return e == NULL ? 0u : e->gen;
} }
/* One class's current slot list, as the compiler's per-reload thunk hands it /* One slot's type, as [Check.class_spec_of] writes it: a scalar type's name,
* over: the class's name as a keyword, and the slot names packed into one * [#name] for a class, and a leading [?] for an (Option T). */
* string, newline between and no leading colons — the shape a string literal static slot_type slot_type_of(const uint8_t *w, int64_t n) {
* already crosses in, rather than a dyn vector this would have to root. static const struct {
* const char *w; uint8_t kind, fbits; int64_t lo, hi;
* The generation is bumped only when the list actually differs. That is what } known[] = {
* makes C-c C-k idempotent: reloading a file re-runs every one of its class { "bool", ST_BOOL, 0, 0, 0 },
* definitions, and a bump per reload would migrate every instance in the { "string", ST_TEXT, 0, 0, 0 },
* program every time anybody saved, for no change. */ { "f32", ST_FLOAT, 24, 0, 0 },
void flan_dyn_class_def(flan_dyn name, const uint8_t *slots, int64_t n) { { "f64", ST_FLOAT, 53, 0, 0 },
kw_entry *k; { "i8", ST_INT, 0, INT8_MIN, INT8_MAX },
kw_entry **list = NULL; { "i16", ST_INT, 0, INT16_MIN, INT16_MAX },
{ "i32", ST_INT, 0, INT32_MIN, INT32_MAX },
{ "i64", ST_INT, 0, INT64_MIN, INT64_MAX },
{ "u8", ST_INT, 0, 0, UINT8_MAX },
{ "u16", ST_INT, 0, 0, UINT16_MAX },
{ "u32", ST_INT, 0, 0, UINT32_MAX },
{ "u64", ST_INT, 0, 0, INT64_MAX },
};
slot_type t = { ST_ANY, 0, 0, 0, 0, NULL, NULL };
size_t i;
if (n > 0 && w[0] == '?') {
t = slot_type_of(w + 1, n - 1);
if (t.kind != ST_ANY) t.opt = 1;
return t;
}
if (n > 1 && w[0] == '#') {
t.kind = ST_CLASS;
t.cls = dyn_kw(flan_dyn_kw(w + 1, n - 1));
return t;
}
for (i = 0; i < sizeof known / sizeof known[0]; i++)
if ((int64_t)strlen(known[i].w) == n && memcmp(known[i].w, w, (size_t)n) == 0) {
t.kind = known[i].kind;
t.fbits = known[i].fbits;
t.lo = known[i].lo;
t.hi = known[i].hi;
t.word = known[i].w;
return t;
}
/* A word this table does not know is a compiler newer than this runtime.
Holding anything is the answer that loses no data. */
return t;
}
static int slot_type_eq(const slot_type *a, const slot_type *b) {
return a->kind == b->kind && a->opt == b->opt && a->fbits == b->fbits
&& a->lo == b->lo && a->hi == b->hi && a->cls == b->cls;
}
/* The type as it was written, for a sentence. */
static void slot_type_text(const slot_type *t, char *buf, size_t cap) {
char base[96];
if (t->kind == ST_CLASS)
snprintf(base, sizeof base, "%.*s", (int)t->cls->len,
(const char *)(t->cls + 1));
else
snprintf(base, sizeof base, "%s", t->word != NULL ? t->word : "dyn");
if (t->opt) snprintf(buf, cap, "(Option %s)", base);
else snprintf(buf, cap, "%s", base);
}
/* Whether [v] may be stored in a slot of type [t], and what is stored: [v]
* itself, or — for an int into a float slot — the float it widens to. The
* widening is the typed side's rule read off the value rather than off a
* static type: an integer the float holds exactly is admitted as that float,
* and one it does not is refused, as (f64 x) would be for the
* type that could hold it. A float into an f32 slot has to be one an f32
* holds, which is the typed side refusing f64 into f32. */
static int slot_admit(const slot_type *t, flan_dyn v, flan_dyn *out) {
int tag = flan_dyn_tag(v);
*out = v;
if (t->kind == ST_ANY) return 1;
if (tag == FLAN_DYN_TAG_NIL) return t->opt;
switch (t->kind) {
case ST_BOOL: return tag == FLAN_DYN_TAG_BOOL;
case ST_TEXT: return tag == FLAN_DYN_TAG_TEXT;
case ST_CLASS:
return tag == FLAN_DYN_TAG_MAP && dyn_obj(v)->u.v.klass == t->cls;
case ST_FLOAT:
if (tag == FLAN_DYN_TAG_FLOAT) {
double d = dyn_num_value(v);
return t->fbits == 53 || d != d || (double)(float)d == d;
}
if (tag == FLAN_DYN_TAG_INT) {
/* Exact is a round trip, not a range: 2^54 is an f64 exactly and
2^53+1 is not. The range test before the cast back is what keeps
that cast defined, since INT64_MAX rounds up to 2^63. */
int64_t x = dyn_int_value(v);
double d = t->fbits == 53 ? (double)x : (double)(float)x;
if (!(d >= -9223372036854775808.0 && d < 9223372036854775808.0)
|| (int64_t)d != x)
return 0;
*out = flan_dyn_from_f64(d);
return 1;
}
return 0;
case ST_INT: {
int64_t x;
if (tag != FLAN_DYN_TAG_INT) return 0;
x = dyn_int_value(v);
return x >= t->lo && x <= t->hi;
}
default: return 1;
}
}
static int slot_fits(const slot_type *t, flan_dyn v) {
flan_dyn ignored;
return slot_admit(t, v, &ignored);
}
/* A class's slots as the compiler hands them over: one line per slot, the
* slot's name and then, after a space, its type's name — nothing for a slot
* written with no type. The same string comes from a constructor and from a
* reload, so it is read in one place. The count is returned; both arrays are
* NULL for a class with no slots, which allocates nothing. */
static int64_t class_spec(const uint8_t *spec, int64_t n, kw_entry ***names,
slot_type **types) {
int64_t count = 0, i, start; int64_t count = 0, i, start;
class_entry *e;
if (flan_dyn_tag(name) != FLAN_DYN_TAG_KEYWORD)
/* No location: the caller is the thunk a reload runs, which has no
source position of its own — the class's own [defclass] is where a
reader would look, and it is not on any stack by the time this runs.
Unreachable from written Flan in any case; only the compiler emits
this call, and it emits a keyword. */
trap1(NULL, 0, TYPE_TRAP, "class definition",
"a class name is a keyword", name);
k = dyn_kw(name);
if (n < 0) n = 0; if (n < 0) n = 0;
/* Count first, then fill: one allocation of the right size, and an empty *names = NULL;
* class — (defclass marker []) is in the corpus — allocates nothing. */ *types = NULL;
for (i = 0, start = 0; i <= n; i++) for (i = 0, start = 0; i <= n; i++)
if (i == n ? i > start : slots[i] == '\n') { if (i == n ? i > start : spec[i] == '\n') {
if (i > start) count++; if (i > start) count++;
start = i + 1; start = i + 1;
} }
if (count > 0) { if (count == 0) return 0;
list = (kw_entry **)malloc((size_t)count * sizeof *list); *names = (kw_entry **)malloc((size_t)count * sizeof **names);
if (list == NULL) trap_oom(NULL, 0, count * (int64_t)sizeof *list); *types = (slot_type *)malloc((size_t)count * sizeof **types);
if (*names == NULL || *types == NULL)
trap_oom(NULL, 0, count * (int64_t)(sizeof **names + sizeof **types));
count = 0; count = 0;
for (i = 0, start = 0; i <= n; i++) for (i = 0, start = 0; i <= n; i++)
if (i == n ? i > start : slots[i] == '\n') { if (i == n ? i > start : spec[i] == '\n') {
if (i > start) if (i > start) {
list[count++] = dyn_kw(flan_dyn_kw(slots + start, i - start)); int64_t sp = start;
while (sp < i && spec[sp] != ' ') sp++;
(*names)[count] = dyn_kw(flan_dyn_kw(spec + start, sp - start));
(*types)[count] = sp < i ? slot_type_of(spec + sp + 1, i - sp - 1)
: slot_type_of(NULL, 0);
count++;
}
start = i + 1; start = i + 1;
} }
} return count;
e = class_find(k); }
if (e != NULL) {
int same = e->nslots == count; static void class_add(kw_entry *k, kw_entry **list, slot_type *types,
if (same) int64_t count) {
for (i = 0; i < count; i++)
if (e->slots[i] != list[i]) { same = 0; break; }
if (same) { free(list); return; }
free(e->slots);
e->slots = list;
e->nslots = count;
/* Wrapping is not a correctness question — what matters is that the new
* generation differs from the one the live instances carry — but zero is
* reserved for "no definition registered", so it is stepped over. */
e->gen = e->gen + 1u;
if (e->gen == 0u) e->gen = 1u;
return;
}
if (classes_n == classes_cap) { if (classes_n == classes_cap) {
int64_t cap = classes_cap ? classes_cap * 2 : 8; int64_t cap = classes_cap ? classes_cap * 2 : 8;
class_entry *t = class_entry *t =
@ -1637,7 +1756,18 @@ void flan_dyn_class_def(flan_dyn name, const uint8_t *slots, int64_t n) {
} }
classes[classes_n].name = k; classes[classes_n].name = k;
classes[classes_n].slots = list; classes[classes_n].slots = list;
classes[classes_n].types = types;
classes[classes_n].warned =
count > 0 ? (uint32_t *)calloc((size_t)count, sizeof(uint32_t)) : NULL;
if (count > 0 && classes[classes_n].warned == NULL)
trap_oom(NULL, 0, count * (int64_t)sizeof(uint32_t));
classes[classes_n].nslots = count; classes[classes_n].nslots = count;
classes[classes_n].typed = 0;
{
int64_t j;
for (j = 0; j < count; j++)
if (types[j].kind != ST_ANY) classes[classes_n].typed = 1;
}
/* One, never zero: an instance built before this registration carries zero /* One, never zero: an instance built before this registration carries zero
* and has to be seen as stale, because the definition it was built from is * and has to be seen as stale, because the definition it was built from is
* exactly the one nobody recorded. */ * exactly the one nobody recorded. */
@ -1645,11 +1775,201 @@ void flan_dyn_class_def(flan_dyn name, const uint8_t *slots, int64_t n) {
classes_n++; classes_n++;
} }
/* One class's current definition, as the compiler's per-reload thunk hands
* it over: the class's name as a keyword, and [class_spec]'s string.
*
* The generation is bumped only when the definition actually differs — a
* slot's name or its type. That is what makes C-c C-k idempotent: reloading
* a file re-runs every one of its class definitions, and a bump per reload
* would migrate every instance in the program every time anybody saved, for
* no change. */
void flan_dyn_class_def(flan_dyn name, const uint8_t *slots, int64_t n) {
kw_entry *k;
kw_entry **list;
slot_type *types;
int64_t count, i;
class_entry *e;
if (flan_dyn_tag(name) != FLAN_DYN_TAG_KEYWORD)
/* No location: the caller is the thunk a reload runs, which has no
source position of its own — the class's own [defclass] is where a
reader would look, and it is not on any stack by the time this runs.
Unreachable from written Flan in any case; only the compiler emits
this call, and it emits a keyword. */
trap1(NULL, 0, TYPE_TRAP, "class definition",
"a class name is a keyword", name);
k = dyn_kw(name);
count = class_spec(slots, n, &list, &types);
e = class_find(k);
if (e != NULL) {
int same = e->nslots == count;
if (same)
for (i = 0; i < count; i++)
if (e->slots[i] != list[i] || !slot_type_eq(&e->types[i], &types[i])) {
same = 0;
break;
}
if (same) { free(list); free(types); return; }
free(e->slots);
free(e->types);
free(e->warned);
e->slots = list;
e->types = types;
e->warned =
count > 0 ? (uint32_t *)calloc((size_t)count, sizeof(uint32_t)) : NULL;
if (count > 0 && e->warned == NULL)
trap_oom(NULL, 0, count * (int64_t)sizeof(uint32_t));
e->nslots = count;
e->typed = 0;
for (i = 0; i < count; i++)
if (types[i].kind != ST_ANY) e->typed = 1;
/* Wrapping is not a correctness question — what matters is that the new
* generation differs from the one the live instances carry — but zero is
* reserved for "no definition registered", so it is stepped over. */
e->gen = e->gen + 1u;
if (e->gen == 0u) e->gen = 1u;
return;
}
class_add(k, list, types, count);
}
/* update-instance-for-redefined-class's dispatcher, as the last reload that
* installed a class or one of its methods left it; NULL until then. Set by a
* thunk and not found by name, because the name is a Flan symbol this file
* cannot spell and the body behind it moves with every method added. */
static void *migrate_fn;
void flan_dyn_class_hook(void *fn) { migrate_fn = fn; }
extern int (*flan_dyn_migrate_hook)(void *fn, uint64_t instance,
uint64_t added, uint64_t discarded);
/* Allocation and the map operations are further down, under their own
* headings; the hook's arguments are built with them. */
flan_dyn flan_dyn_vec_new(void);
flan_dyn flan_dyn_map_new(void);
void flan_dyn_push(flan_dyn v, flan_dyn x, const uint8_t *loc, int64_t loclen);
void flan_dyn_map_set(flan_dyn m, flan_dyn k, flan_dyn v);
/* The user hook, run on an instance the name-matching has just brought up to
* date. [inst], [added] and [gone] are rooted by the caller.
*
* Two things are kept for the length of the call, because the call is
* arbitrary Flan and may allocate as much as it likes:
*
* - the name-matched entries, rooted, so that taking the restart puts the
* instance back exactly as name-matching left it, whatever the method did
* to it before it signalled. That is the restart's whole meaning, and it
* is SBCL's choice of what a failed update leaves (std-class.lisp, the
* nlx-protect around the call) moved one step: SBCL restores the obsolete
* instance and retries at the next access, where here the name-matched
* one is kept and nothing is retried.
* - the temporaries ring, saved and put back. A migration starts inside
* [get] or [put], whose caller may be holding an object only the ring
* keeps alive — the result of the call beside it in the same expression.
* A method that allocates more than the ring holds would push it out and
* let the next collection free it, so the ring is rooted for the call and
* restored after it, and the caller sees the ring it left. */
static void class_hook(flan_obj *o, flan_dyn inst, flan_dyn added,
flan_dyn gone, int64_t n) {
flan_dyn *snap = NULL;
flan_obj *ring_was[RING];
flan_dyn ring_rooted[RING];
unsigned ring_at_was = ring_at, k;
int64_t j, roots_at = roots_n;
int r;
if (n > 0) {
snap = (flan_dyn *)malloc((size_t)n * 2 * sizeof *snap);
if (snap == NULL) trap_oom(NULL, 0, n * 2 * (int64_t)sizeof *snap);
memcpy(snap, o->u.v.items, (size_t)n * 2 * sizeof *snap);
for (j = 0; j < n; j++) root_add(&snap[j * 2 + 1], NULL);
}
memcpy(ring_was, ring, sizeof ring);
for (k = 0; k < RING; k++) {
ring_rooted[k] = ring[k] == NULL
? dyn_make(BOX_NIL, 0)
: dyn_make(BOX_OBJ, (uint64_t)(uintptr_t)ring[k]);
root_add(&ring_rooted[k], NULL);
}
r = flan_dyn_migrate_hook(migrate_fn, inst, added, gone);
memcpy(ring, ring_was, sizeof ring);
ring_at = ring_at_was;
/* Nothing below allocates on the collector's heap, so the roots into
[snap] and this frame can go before either does. */
roots_n = roots_at;
if (r == 1) {
/* The restart: the entries name-matching left, in a block of their own,
* whatever the method grew or shrank the instance to. */
flan_dyn *back = NULL;
if (n > 0) {
back = (flan_dyn *)malloc((size_t)n * 2 * sizeof *back);
if (back == NULL) trap_oom(NULL, 0, n * 2 * (int64_t)sizeof *back);
memcpy(back, snap, (size_t)n * 2 * sizeof *back);
}
gc_bytes += (n - o->u.v.cap) * 2 * (int64_t)sizeof(flan_dyn);
free(o->u.v.items);
o->u.v.items = back;
o->u.v.cap = n;
o->len = n;
}
free(snap);
if (r == 2) {
kw_entry *c = o->u.v.klass;
fflush(stdout);
fprintf(stderr,
"dyn migrate: update-instance-for-redefined-class, migrating an "
"instance of %.*s, was left for a restart established outside "
"it. A migration runs inside get, put or set, and cannot be "
"left for one of their callers; the instance is kept as its "
"slots matched by name. Take migrate-by-name, or handle the "
"condition inside the method\n",
(int)c->len, (const char *)(c + 1));
flan_trap((const uint8_t *)"DynMigrate", 10);
}
}
/* An entry at the end of a map, with no lookup first: for a map whose keys
* are known to be distinct already. A lookup compares keys with [dyn_equal],
* which migrates any stale instance it meets and runs that instance's hook —
* and a migration building its own hook's arguments must not start another
* one, or the instance it is migrating is migrated again inside itself. */
static void map_append(flan_obj *m, flan_dyn k, flan_dyn v) {
if (m->len == m->u.v.cap) {
int64_t cap = m->u.v.cap ? m->u.v.cap * 2 : 8;
flan_dyn *items =
(flan_dyn *)realloc(m->u.v.items, (size_t)cap * 2 * sizeof *items);
if (items == NULL) trap_oom(NULL, 0, cap * 2 * (int64_t)sizeof *items);
gc_bytes += (cap - m->u.v.cap) * 2 * (int64_t)sizeof *items;
m->u.v.items = items;
m->u.v.cap = cap;
}
m->u.v.items[m->len * 2] = k;
m->u.v.items[m->len * 2 + 1] = v;
m->len++;
}
/* Which of [o]'s entries is the slot [s], or -1. The interned identity
* compare, never [dyn_equal]: see [map_append]. [flan_dyn_tag] and not a
* bare [dyn_box]: a float is not boxed at all, so its payload bits can read
* as any box tag, and reading a non-keyword's payload as a [kw_entry *] is a
* wild pointer. A raw [put] can have left a float — or anything else — as a
* key. */
static int64_t entry_of(flan_obj *o, kw_entry *s) {
int64_t i;
for (i = 0; i < o->len; i++) {
flan_dyn key = o->u.v.items[i * 2];
if (flan_dyn_tag(key) == FLAN_DYN_TAG_KEYWORD && dyn_kw(key) == s)
return i;
}
return -1;
}
/* The migration. [o] is left holding exactly the class's current slots, in /* The migration. [o] is left holding exactly the class's current slots, in
* the class's order, with the values it already had for the ones it still * the class's order, with the values it already had for the ones it still
* has and nil for the ones it has just gained — which is precisely the * has — which is the property CLHS 4.3.6 guarantees, matched by name, with
* property CLHS 4.3.6 guarantees, matched by name, with the instance's * the instance's identity preserved because none of this allocates a new
* identity preserved because none of this allocates a new object. * object. A slot it has just gained holds its type's zero value, Flan's
* zero-is-initialisation — false, 0, 0.0, the empty string — or nil where
* the type admits nil or has no zero: a dyn slot, an (Option T), a class.
* *
* Rebuilt into a fresh block rather than compacted in place, and the order is * Rebuilt into a fresh block rather than compacted in place, and the order is
* the class's rather than the instance's, so that a migrated instance is * the class's rather than the instance's, so that a migrated instance is
@ -1658,61 +1978,183 @@ void flan_dyn_class_def(flan_dyn name, const uint8_t *slots, int64_t n) {
* and count in insertion order and would have. One malloc per instance per * and count in insertion order and would have. One malloc per instance per
* redefinition is the price, and a migration happens once. * redefinition is the price, and a migration happens once.
* *
* Nothing here allocates on the collector's heap, so no collection can run * In three steps, and the order is what keeps it sound.
* part-way through and see an object whose [len] and [items] disagree.
* *
* Nor can it free a block something above it is walking. The block it frees * First, everything that allocates on the collector's heap: the hook's
* is [o]'s, and every caller syncs [o] before it starts walking [o] — so a * arguments, and an empty string for a gained string slot. A collection may
* re-entry through a nested [dyn_equal], including a map used as a key of * run here, while [o] still holds its old entries whole. Nothing in this
* itself, finds [o] already current and returns at the generation compare. * step compares a key with [dyn_equal] — see [map_append] — so nothing in it
* The key scan here uses the interned identity compare and calls * can migrate another instance and run a hook inside this migration.
* [dyn_equal] not at all, so it cannot re-enter from inside. */ *
static void class_sync(flan_obj *o) { * Second, the name-matching, which allocates nothing on the collector's
class_entry *e; * heap, calls nothing that can migrate, and finishes by stamping [o]
* current. [e] is read only up to here: a hook may build an instance of a
* class the registry has not seen, and adding it moves [classes].
*
* Third, the hook, on an instance that is already current, so a method that
* reads or writes it finds it migrated and does not start a second
* migration. A method may touch anything, including a map something above
* this frame is walking; the migration of [o] itself is finished before it
* runs. */
/* Kept out of line: inlined into [class_sync], its frame and saved
* registers were paid on every [get] and [put] of every map, current or not
* — measured at about a tenth of an untyped [put]'s instructions. */
__attribute__((noinline))
static void class_migrate(flan_obj *o, class_entry *e) {
flan_dyn *fresh = NULL; flan_dyn *fresh = NULL;
int64_t i, j; int64_t i, j, n;
if (o->kind != OBJ_MAP || o->u.v.klass == NULL) return; /* Rooted by address for as long as they may be needed: each is a
e = class_find(o->u.v.klass); * collector object held nowhere else. */
if (e == NULL || e->gen == o->gen) return; flan_dyn inst, added, gone, empty;
if (e->nslots > 0) { int64_t roots_at = roots_n;
fresh = (flan_dyn *)malloc((size_t)e->nslots * 2 * sizeof *fresh); int hook, need_empty = 0;
if (fresh == NULL) trap_oom(NULL, 0, e->nslots * 2 * (int64_t)sizeof *fresh); n = e->nslots;
/* CLHS 4.3.6: the method runs on every instance a redefinition reaches,
* whether or not the slot names moved — a changed type is a change a
* method may want to convert for. */
hook = migrate_fn != NULL && flan_dyn_migrate_hook != NULL;
for (j = 0; j < n; j++)
if (e->types[j].kind == ST_TEXT && !e->types[j].opt
&& entry_of(o, e->slots[j]) < 0)
need_empty = 1;
inst = dyn_make(BOX_OBJ, (uint64_t)(uintptr_t)o);
added = gone = empty = dyn_make(BOX_NIL, 0);
if (hook || need_empty) root_add(&inst, NULL);
if (need_empty) {
empty = flan_dyn_from_bytes((const uint8_t *)"", 0);
root_add(&empty, NULL);
} }
for (j = 0; j < e->nslots; j++) { if (hook) {
flan_dyn v = dyn_make(BOX_NIL, 0); added = flan_dyn_vec_new();
root_add(&added, NULL);
gone = flan_dyn_map_new();
root_add(&gone, NULL);
for (j = 0; j < n; j++)
if (entry_of(o, e->slots[j]) < 0)
flan_dyn_push(added,
dyn_make(BOX_KW, (uint64_t)(uintptr_t)e->slots[j]),
NULL, 0);
/* Every key the class no longer declares, a raw [put]'s included:
* CLHS's discarded slots and their property list, as one map. [o]'s
* keys are distinct, so these are, and they are appended as they are. */
for (i = 0; i < o->len; i++) { for (i = 0; i < o->len; i++) {
flan_dyn key = o->u.v.items[i * 2]; flan_dyn key = o->u.v.items[i * 2];
/* [flan_dyn_tag] and not a bare [dyn_box]: a float is not boxed at int kept = 0;
all, so its payload bits can read as any box tag, and reading a if (flan_dyn_tag(key) == FLAN_DYN_TAG_KEYWORD)
non-keyword's payload as a [kw_entry *] is a wild pointer. A raw for (j = 0; j < n; j++)
[put] can have left a float — or anything else — in here. */ if (dyn_kw(key) == e->slots[j]) { kept = 1; break; }
if (flan_dyn_tag(key) == FLAN_DYN_TAG_KEYWORD if (!kept) map_append(dyn_obj(gone), key, o->u.v.items[i * 2 + 1]);
&& dyn_kw(key) == e->slots[j]) {
v = o->u.v.items[i * 2 + 1];
break;
} }
} }
if (n > 0) {
fresh = (flan_dyn *)malloc((size_t)n * 2 * sizeof *fresh);
if (fresh == NULL) trap_oom(NULL, 0, n * 2 * (int64_t)sizeof *fresh);
}
for (j = 0; j < n; j++) {
const slot_type *t = &e->types[j];
flan_dyn v;
i = entry_of(o, e->slots[j]);
if (i >= 0) {
v = o->u.v.items[i * 2 + 1];
/* A kept value that the slot's new type does not admit is kept
anyway: throwing it away would be the data loss a redefinition
exists to avoid, and there is nothing to convert it to. What it
gets is a warning, once per slot per redefinition, and the next
write to the slot is checked like any other. */
if (!slot_fits(t, v) && e->warned[j] != e->gen) {
char sv[SAY_MAX], st[128];
kw_entry *c = o->u.v.klass, *sl = e->slots[j];
e->warned[j] = e->gen;
say(sv, SAY_MAX, v);
slot_type_text(t, st, sizeof st);
fflush(stdout);
fprintf(stderr,
"warning: %.*s was redefined, and its slot :%.*s is now "
"declared %s. An instance holds %s there, which is %s; it "
"keeps that value, and the next write to :%.*s is checked\n",
(int)c->len, (const char *)(c + 1),
(int)sl->len, (const char *)(sl + 1), st, sv,
tag_of(v), (int)sl->len, (const char *)(sl + 1));
}
}
else if (t->opt) v = dyn_make(BOX_NIL, 0);
else
switch (t->kind) {
case ST_BOOL: v = flan_dyn_from_bool(0); break;
case ST_INT: v = flan_dyn_from_i64(0); break;
case ST_FLOAT: v = flan_dyn_from_f64(0.0); break;
case ST_TEXT: v = empty; break;
default: v = dyn_make(BOX_NIL, 0); break;
}
fresh[j * 2] = dyn_make(BOX_KW, (uint64_t)(uintptr_t)e->slots[j]); fresh[j * 2] = dyn_make(BOX_KW, (uint64_t)(uintptr_t)e->slots[j]);
fresh[j * 2 + 1] = v; fresh[j * 2 + 1] = v;
} }
/* Charged the way [map_set]'s growth is, in both directions: a class that /* Charged the way [map_set]'s growth is, in both directions: a class that
* lost slots gives the bytes back, or the trigger drifts up by whatever * lost slots gives the bytes back, or the trigger drifts up by whatever
* every migration in the program ever released. */ * every migration in the program ever released. */
gc_bytes += (e->nslots - o->u.v.cap) * 2 * (int64_t)sizeof(flan_dyn); gc_bytes += (n - o->u.v.cap) * 2 * (int64_t)sizeof(flan_dyn);
free(o->u.v.items); free(o->u.v.items);
o->u.v.items = fresh; o->u.v.items = fresh;
o->u.v.cap = e->nslots; o->u.v.cap = n;
o->len = e->nslots; o->len = n;
o->gen = e->gen; o->gen = e->gen;
e = NULL;
if (hook) class_hook(o, inst, added, gone, n);
roots_n = roots_at;
}
/* Every read or write of an instance comes through here first: the class's
* entry, with [o] migrated to it if it was stale, or NULL for a map with no
* class. The common case — current — is a lookup and a compare, and the
* migration is a call of its own so that it stays out of the way. The entry
* is looked up again after one, because a hook may have moved the table. */
static class_entry *class_sync(flan_obj *o) {
class_entry *e;
if (o->kind != OBJ_MAP || o->u.v.klass == NULL) return NULL;
e = class_find(o->u.v.klass);
if (e == NULL || e->gen == o->gen) return e;
class_migrate(o, e);
return class_find(o->u.v.klass);
} }
/* The same map with a shape tag on it: what a (defclass ...) constructor /* The same map with a shape tag on it: what a (defclass ...) constructor
* calls. [k] is a keyword and anything else traps by name — the compiler * calls. [k] is a keyword and anything else traps by name — the compiler
* hands it the class's own name and nothing else can reach this. */ * hands it the class's own name and nothing else can reach this.
flan_dyn flan_dyn_map_new_class(flan_dyn k) { *
* [spec] is the class's definition, [class_spec]'s string, and it registers
* the class the first time any instance of it is built. That is what makes
* a slot's type checked in a program that is never reloaded — the registry
* used to be filled only by a reload. A class already registered keeps
* what it has, and has to: a constructor compiled before a redefinition may
* still be on some stack, and letting its definition win would put the
* class back the way it was. Redefining is [flan_dyn_class_def]'s alone. */
/* A constructor call's site: [pending] from the caller, moved to [building]
* by the constructor's first act, so a call through a function value — which
* sets nothing — finds none rather than an earlier call's. Nothing between
* the caller setting it and the constructor taking it can construct: the
* caller evaluated every argument first. */
static const uint8_t *site_pending, *site_building;
static int64_t site_pending_len, site_building_len;
void flan_dyn_ctor_site(const uint8_t *loc, int64_t loclen) {
site_pending = loc;
site_pending_len = loclen;
}
flan_dyn flan_dyn_map_new_class(flan_dyn k, const uint8_t *spec, int64_t n) {
flan_obj *o; flan_obj *o;
site_building = site_pending;
site_building_len = site_pending_len;
site_pending = NULL;
site_pending_len = 0;
if (flan_dyn_tag(k) != FLAN_DYN_TAG_KEYWORD) if (flan_dyn_tag(k) != FLAN_DYN_TAG_KEYWORD)
trap1(NULL, 0, TYPE_TRAP, "class instance", "a class tag is a keyword", k); trap1(NULL, 0, TYPE_TRAP, "class instance", "a class tag is a keyword", k);
if (class_find(dyn_kw(k)) == NULL) {
kw_entry **list;
slot_type *types;
int64_t count = class_spec(spec, n, &list, &types);
class_add(dyn_kw(k), list, types, count);
}
o = gc_alloc(OBJ_MAP, 0); o = gc_alloc(OBJ_MAP, 0);
o->len = 0; o->len = 0;
o->u.v.items = NULL; o->u.v.items = NULL;
@ -2081,6 +2523,14 @@ flan_dyn flan_dyn_sub(flan_dyn a, flan_dyn b, const uint8_t *loc,
int64_t loclen) { int64_t loclen) {
return arith(loc, loclen, "-", a, b); return arith(loc, loclen, "-", a, b);
} }
/* (- x): an int wraps, as (- 0 x) does, and a float flips its sign, so the
* negation of 0.0 is -0.0 and not the 0.0 a subtraction from zero gives. */
flan_dyn flan_dyn_neg(flan_dyn a, const uint8_t *loc, int64_t loclen) {
if (!is_num(a)) trap1(loc, loclen, TYPE_TRAP, "-", "it takes a number", a);
if (flan_dyn_tag(a) == FLAN_DYN_TAG_INT)
return flan_dyn_from_i64((int64_t)(0 - (uint64_t)dyn_int_value(a)));
return flan_dyn_from_f64(-dyn_num_value(a));
}
flan_dyn flan_dyn_mul(flan_dyn a, flan_dyn b, const uint8_t *loc, flan_dyn flan_dyn_mul(flan_dyn a, flan_dyn b, const uint8_t *loc,
int64_t loclen) { int64_t loclen) {
return arith(loc, loclen, "*", a, b); return arith(loc, loclen, "*", a, b);
@ -2587,8 +3037,160 @@ flan_dyn flan_dyn_map_contains(flan_dyn m, flan_dyn k) {
return flan_dyn_from_bool(map_find(o, k) >= 0); return flan_dyn_from_bool(map_find(o, k) >= 0);
} }
/* A class slot's type, checked at the store — SBCL's place for it
* (src/pcl/slots.lisp, [set-slot-value]'s typecheck before the write),
* because the store is where the wrong value is. -1 when [k] is not a slot
* the class declares. */
static int64_t class_slot(class_entry *e, flan_dyn k) {
int64_t j;
if (e == NULL || flan_dyn_tag(k) != FLAN_DYN_TAG_KEYWORD) return -1;
for (j = 0; j < e->nslots; j++)
if (e->slots[j] == dyn_kw(k)) return j;
return -1;
}
/* The three stores that reach a declared slot, for the sentence a refusal
* prints: the call as it would have been written. */
enum { BY_PUT, BY_SET, BY_NEW };
static _Noreturn void trap_slot_type(const uint8_t *loc, int64_t loclen,
int by, flan_obj *o, class_entry *e,
int64_t j, flan_dyn m, flan_dyn v) {
char sm[SAY_MAX], sv[SAY_MAX], st[128];
const slot_type *t = &e->types[j];
kw_entry *sl = e->slots[j], *c = o->u.v.klass;
int sn = (int)sl->len, cn = (int)c->len;
const char *ss = (const char *)(sl + 1), *cs = (const char *)(c + 1);
say(sm, SAY_MAX, m);
say(sv, SAY_MAX, v);
slot_type_text(t, st, sizeof st);
fflush(stdout);
/* A constructor's refusal is placed at the call that was wrong, when the
call said where it was, and names the slot's declaration after it. */
trap_where(by == BY_NEW && site_building != NULL ? site_building : loc,
by == BY_NEW && site_building != NULL ? site_building_len : loclen);
fprintf(stderr, "dyn %s: the slot :%.*s of %.*s is declared %s, and ",
by == BY_PUT ? "put" : by == BY_SET ? "set" : "construct", sn, ss,
cn, cs, st);
/* A number of the right kind that does not fit is not news about its tag. */
if ((t->kind == ST_INT && flan_dyn_tag(v) == FLAN_DYN_TAG_INT)
|| (t->kind == ST_FLOAT
&& (flan_dyn_tag(v) == FLAN_DYN_TAG_INT
|| flan_dyn_tag(v) == FLAN_DYN_TAG_FLOAT)))
fprintf(stderr, "%s is not a value it holds exactly — ", sv);
else if (t->kind == ST_CLASS && flan_dyn_tag(v) == FLAN_DYN_TAG_MAP)
fprintf(stderr, "this is not an instance of it — ");
else
fprintf(stderr, "this is %s — ", tag_of(v));
if (by == BY_PUT)
fprintf(stderr, "(put %s :%.*s %s)\n", sm, sn, ss, sv);
else if (by == BY_SET)
fprintf(stderr, "(set (get %s :%.*s) %s)\n", sm, sn, ss, sv);
else if (site_building != NULL && loc != NULL)
fprintf(stderr, "(%.*s ...) with :%.*s %s; the slot is declared at %.*s\n",
cn, cs, sn, ss, sv, (int)loclen, (const char *)loc);
else
fprintf(stderr, "(%.*s ...) with :%.*s %s\n", cn, cs, sn, ss, sv);
flan_trap((const uint8_t *)"DynType", 7);
}
/* The value a store into [o] under [k] actually stores: [v], or the float an
* int widens to in a float slot. A class with no typed slot answers at its
* flag, and a map with no class before that. */
static flan_dyn check_slot(const uint8_t *loc, int64_t loclen, int by,
flan_obj *o, class_entry *e, flan_dyn m,
flan_dyn k, flan_dyn v) {
int64_t j;
flan_dyn out;
if (e == NULL || !e->typed) return v;
j = class_slot(e, k);
if (j < 0) return v;
if (!slot_admit(&e->types[j], v, &out))
trap_slot_type(loc, loclen, by, o, e, j, m, v);
return out;
}
static inline void map_store(flan_obj *o, flan_dyn k, flan_dyn v);
/* A constructor's stores: [flan_dyn_map_set]'s, with the refusal worded for
* the constructor call it happened inside rather than for a [put] nobody
* wrote, and placed at the slot's declaration. */
void flan_dyn_slot_init(flan_dyn m, flan_dyn k, flan_dyn v,
const uint8_t *loc, int64_t loclen) {
flan_obj *o = want_map("construct", m, k);
class_entry *e = o->u.v.klass == NULL ? NULL : class_find(o->u.v.klass);
map_store(o, k, check_slot(loc, loclen, BY_NEW, o, e, m, k, v));
}
/* (set (get inst :slot) v). Three refusals, each its own sentence, because
* they are three different mistakes: the value is not a class instance at
* all (a map's entries are written with [put], which is where inserting a
* key is real); the key is not a slot the class declares; the value does not
* fit the slot's type. The first two are why this is not [put]: a declared
* slot always exists, so writing one is a store and never an insertion. */
void flan_dyn_slot_set(flan_dyn m, flan_dyn k, flan_dyn v,
const uint8_t *loc, int64_t loclen) {
flan_obj *o;
class_entry *e;
int64_t j;
flan_dyn out;
if (!is_map(m) || dyn_obj(m)->u.v.klass == NULL) {
char sm[SAY_MAX];
say(sm, SAY_MAX, m);
fflush(stdout);
trap_where(loc, loclen);
fprintf(stderr,
"dyn set: (get m k) is a place only on a class instance, and "
"this is %s%s — %s. A map's entries are written with put\n",
is_map(m) ? "a map with no class" : "a ",
is_map(m) ? "" : tag_of(m), sm);
flan_trap((const uint8_t *)"DynType", 7);
}
o = dyn_obj(m);
e = class_sync(o);
j = class_slot(e, k);
if (j < 0) {
char sk[SAY_MAX];
kw_entry *c = o->u.v.klass;
int64_t i;
say(sk, SAY_MAX, k);
fflush(stdout);
trap_where(loc, loclen);
fprintf(stderr, "dyn set: %.*s has no slot %s. Its slots are",
(int)c->len, (const char *)(c + 1), sk);
if (e == NULL || e->nslots == 0) fprintf(stderr, " none");
else
for (i = 0; i < e->nslots; i++)
fprintf(stderr, " :%.*s", (int)e->slots[i]->len,
(const char *)(e->slots[i] + 1));
fprintf(stderr, "; a key the class does not declare is added with put, "
"not set\n");
flan_trap((const uint8_t *)"DynType", 7);
}
if (!slot_admit(&e->types[j], v, &out))
trap_slot_type(loc, loclen, BY_SET, o, e, j, m, v);
map_store(o, k, out);
}
void flan_dyn_map_put(flan_dyn m, flan_dyn k, flan_dyn v, const uint8_t *loc,
int64_t loclen) {
flan_obj *o;
class_entry *e;
if (!is_map(m)) trap2(NULL, 0, TYPE_TRAP, "put", "only a map answers it", m, k);
o = dyn_obj(m);
e = class_sync(o);
/* A map with no class, and a class with no typed slot, stop at the test. */
if (e != NULL && e->typed) v = check_slot(loc, loclen, BY_PUT, o, e, m, k, v);
map_store(o, k, v);
}
/* The same with no site: a map literal's stores, and test/dyn_ops.c. */
void flan_dyn_map_set(flan_dyn m, flan_dyn k, flan_dyn v) { void flan_dyn_map_set(flan_dyn m, flan_dyn k, flan_dyn v) {
flan_obj *o = want_map("put", m, k); flan_dyn_map_put(m, k, v, NULL, 0);
}
/* The store under all three, with the instance already brought up to date. */
static inline void map_store(flan_obj *o, flan_dyn k, flan_dyn v) {
int64_t i = map_find(o, k); int64_t i = map_find(o, k);
if (i >= 0) { if (i >= 0) {
o->u.v.items[i * 2 + 1] = v; o->u.v.items[i * 2 + 1] = v;

View File

@ -105,15 +105,34 @@ flan_dyn flan_dyn_map_new(void);
* *
* The tag is not traced and does not have to be: an interned keyword entry is * The tag is not traced and does not have to be: an interned keyword entry is
* immortal and is not a collector object. */ * immortal and is not a collector object. */
flan_dyn flan_dyn_map_new_class(flan_dyn k); flan_dyn flan_dyn_map_new_class(flan_dyn k, const uint8_t *spec, int64_t n);
/* A constructor's store into a slot, checked against the slot's declared
* type — [flan_dyn_map_set] with a refusal worded for the constructor. */
void flan_dyn_slot_init(flan_dyn m, flan_dyn k, flan_dyn v,
const uint8_t *loc, int64_t loclen);
/* Where the constructor about to be called was called from. Set by the
* caller immediately before the call and taken by the constructor's
* [flan_dyn_map_new_class], so a refusal in its stores names the call. */
void flan_dyn_ctor_site(const uint8_t *loc, int64_t loclen);
/* (set (get inst :slot) v): [m] must be a class instance and [k] a slot its
* class declares, and [v] must fit the slot's type; each is a trap with its
* own sentence, at [loc]. A declared slot always exists, so this stores and
* never inserts. */
void flan_dyn_slot_set(flan_dyn m, flan_dyn k, flan_dyn v,
const uint8_t *loc, int64_t loclen);
/* The class's name as a keyword, or nil for anything that is not an instance /* The class's name as a keyword, or nil for anything that is not an instance
* — an ordinary map included. Never traps. */ * — an ordinary map included. Never traps. */
flan_dyn flan_dyn_class_of(flan_dyn v); flan_dyn flan_dyn_class_of(flan_dyn v);
/* A class definition, registered or re-registered: [name] is the class's name /* A class definition, registered or re-registered: [name] is the class's name
* as a keyword and [slots]/[n] is its slot names packed into one string, * as a keyword and [slots]/[n] is its slots packed into one string, a line
* newline between and no leading colons. The compiler emits one call per * each, the slot's name and then — after a space, for a typed slot — its
* type's name: "x i64\ny" is an i64 slot :x and a slot :y of any value.
* [flan_dyn_map_new_class] takes the same string. The compiler emits one call per
* (defclass ...) into the thunk a reload runs, so a definition that changed * (defclass ...) into the thunk a reload runs, so a definition that changed
* lands here before anything touches an instance. * lands here before anything touches an instance.
* *
@ -122,9 +141,10 @@ flan_dyn flan_dyn_class_of(flan_dyn v);
* migrates nothing. When it does move, every instance built against an * migrates nothing. When it does move, every instance built against an
* earlier definition migrates lazily at its next [get], [put], [has-key?], * earlier definition migrates lazily at its next [get], [put], [has-key?],
* [len] or equality comparison: slots the class still has keep their values * [len] or equality comparison: slots the class still has keep their values
* matched by name, slots it has gained appear as nil, and keys it no longer * matched by name, a slot it has gained holds its type's zero value — nil
* for an untyped, an (Option T) or a class-typed slot — and keys it no longer
* declares are dropped. The instance's identity is preserved throughout; * declares are dropped. The instance's identity is preserved throughout;
* this is CLHS 4.3.6 without the user hook. * this is CLHS 4.3.6, and [flan_dyn_class_hook] is its user hook.
* *
* The drop is unconditional, which is the honest cost of a class instance * The drop is unconditional, which is the honest cost of a class instance
* being an open map: a key written by a raw [put] that the class never * being an open map: a key written by a raw [put] that the class never
@ -132,6 +152,13 @@ flan_dyn flan_dyn_class_of(flan_dyn v);
* class's intention and does not enforce it. */ * class's intention and does not enforce it. */
void flan_dyn_class_def(flan_dyn name, const uint8_t *slots, int64_t n); void flan_dyn_class_def(flan_dyn name, const uint8_t *slots, int64_t n);
/* The body update-instance-for-redefined-class dispatches through, as a
* reload last saw it. Each migration after this calls it, through
* flan_rt.c's [flan_dyn_migrate_hook], with the instance already matched by
* name. A reload that installs a class or a method of that generic calls
* this again, so the body is never older than the last one installed. */
void flan_dyn_class_hook(void *fn);
/* [sizeof(flan_obj)], for the one test that asserts it. The generation a /* [sizeof(flan_obj)], for the one test that asserts it. The generation a
* class instance carries was fitted into the padding between [mark] and * class instance carries was fitted into the padding between [mark] and
* [len] precisely so that this number did not move; a field that pushed it * [len] precisely so that this number did not move; a field that pushed it
@ -168,6 +195,7 @@ flan_dyn flan_dyn_sub(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen
flan_dyn flan_dyn_mul(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); flan_dyn flan_dyn_mul(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen);
flan_dyn flan_dyn_div(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); flan_dyn flan_dyn_div(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen);
flan_dyn flan_dyn_rem(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen); flan_dyn flan_dyn_rem(flan_dyn a, flan_dyn b, const uint8_t *loc, int64_t loclen);
flan_dyn flan_dyn_neg(flan_dyn a, const uint8_t *loc, int64_t loclen);
/* Answer a bool dyn. Numbers compare as numbers and text compares bytewise; /* Answer a bool dyn. Numbers compare as numbers and text compares bytewise;
* a mixture of the two, or anything else, traps. */ * a mixture of the two, or anything else, traps. */
@ -202,6 +230,10 @@ void flan_dyn_push(flan_dyn v, flan_dyn x, const uint8_t *loc, int64_t loclen);
* key in place, so a key occurs once and insertion order is print order. */ * key in place, so a key occurs once and insertion order is print order. */
flan_dyn flan_dyn_map_get(flan_dyn m, flan_dyn k); flan_dyn flan_dyn_map_get(flan_dyn m, flan_dyn k);
void flan_dyn_map_set(flan_dyn m, flan_dyn k, flan_dyn v); void flan_dyn_map_set(flan_dyn m, flan_dyn k, flan_dyn v);
/* [put]'s: [flan_dyn_map_set], with the site a typed class slot's refusal
* prints. */
void flan_dyn_map_put(flan_dyn m, flan_dyn k, flan_dyn v,
const uint8_t *loc, int64_t loclen);
flan_dyn flan_dyn_map_contains(flan_dyn m, flan_dyn k); flan_dyn flan_dyn_map_contains(flan_dyn m, flan_dyn k);
/* Structural, and per type it renders what typed [print] renders. Never /* Structural, and per type it renders what typed [print] renders. Never

View File

@ -647,6 +647,26 @@ void (*flan_break_hook)(const uint8_t *name, int64_t namelen, void *condition,
* site printed just above carries the detail. */ * site printed just above carries the detail. */
void (*flan_trap_hook)(const uint8_t *name, int64_t namelen); void (*flan_trap_hook)(const uint8_t *name, int64_t namelen);
/* The call a class migration makes to update-instance-for-redefined-class:
* [fn] is the method dispatcher's current body, and the three words are the
* instance, the vec of slots it gained and the map of the slots it lost to
* the values they held — dyn words, as [uint64_t] here because this file
* does not include flan_dyn.h.
*
* Here and not in flan_dyn.c because it is set by the agent, and the agent
* must link against a program with no collector in it; and not called from
* here because what makes it a hook is the restart it runs under, which is
* the agent's business — the floor a break inside it reads is the agent's.
* NULL, and no hook runs, outside a dev session: a class is redefined only
* by a reload, and a reload only arrives through the agent.
*
* The answer is 0 when the method returned, 1 when the restart the call
* established was taken, and 2 when some other transfer came back through
* it — one aimed at a restart below the call, which a C frame cannot carry
* on. */
int (*flan_dyn_migrate_hook)(void *fn, uint64_t instance, uint64_t added,
uint64_t discarded);
static _Noreturn void rt_trap(const uint8_t *name, int64_t namelen) { static _Noreturn void rt_trap(const uint8_t *name, int64_t namelen) {
if (flan_trap_hook != NULL) flan_trap_hook(name, namelen); if (flan_trap_hook != NULL) flan_trap_hook(name, namelen);
rt_die(); rt_die();

2
test/daemon-x86.out Normal file
View File

@ -0,0 +1,2 @@
flan dev: built dev-hook.flan in 444ms
flan dev: /home/joe/Development/flan/.claude/worktrees/agent-a6ffe579d55d1892b/test/programs/dev-hook.flan ready on /tmp/claude-1000/rv-3672919.sock (17ms, one process)

View File

@ -985,6 +985,8 @@ static void refuse(const char *what) {
if (strcmp(what, "add") == 0) (void)FDYN_add(flan_dyn_from_i64(3), t); if (strcmp(what, "add") == 0) (void)FDYN_add(flan_dyn_from_i64(3), t);
else if (strcmp(what, "sub") == 0) else if (strcmp(what, "sub") == 0)
(void)FDYN_sub(flan_dyn_nil(), flan_dyn_from_i64(1)); (void)FDYN_sub(flan_dyn_nil(), flan_dyn_from_i64(1));
else if (strcmp(what, "neg") == 0)
(void)flan_dyn_neg(t, NULL, 0);
else if (strcmp(what, "mul") == 0) else if (strcmp(what, "mul") == 0)
(void)FDYN_mul(flan_dyn_from_bool(1), flan_dyn_from_i64(2)); (void)FDYN_mul(flan_dyn_from_bool(1), flan_dyn_from_i64(2));
else if (strcmp(what, "div") == 0) else if (strcmp(what, "div") == 0)
@ -1061,7 +1063,8 @@ static void define(const char *name, const char *slots) {
* Built through the same entry point a constructor uses, so it is stamped * Built through the same entry point a constructor uses, so it is stamped
* exactly as compiled code would stamp it. */ * exactly as compiled code would stamp it. */
static flan_dyn a_point(int64_t x, int64_t y) { static flan_dyn a_point(int64_t x, int64_t y) {
flan_dyn p = flan_dyn_map_new_class(flan_dyn_kw((const uint8_t *)"point", 5)); flan_dyn p = flan_dyn_map_new_class(flan_dyn_kw((const uint8_t *)"point", 5),
(const uint8_t *)"x\ny", 3);
flan_dyn_map_set(p, flan_dyn_kw((const uint8_t *)"x", 1), flan_dyn_map_set(p, flan_dyn_kw((const uint8_t *)"x", 1),
flan_dyn_from_i64(x)); flan_dyn_from_i64(x));
flan_dyn_map_set(p, flan_dyn_kw((const uint8_t *)"y", 1), flan_dyn_map_set(p, flan_dyn_kw((const uint8_t *)"y", 1),
@ -1176,7 +1179,8 @@ static void classes(void) {
define("point", "x\ny\nn"); define("point", "x\ny\nn");
/* Built by hand rather than through [a_point], because it is the instance /* Built by hand rather than through [a_point], because it is the instance
the *new* constructor would build: three slots, stamped current. */ the *new* constructor would build: three slots, stamped current. */
q = flan_dyn_map_new_class(flan_dyn_kw((const uint8_t *)"point", 5)); q = flan_dyn_map_new_class(flan_dyn_kw((const uint8_t *)"point", 5),
(const uint8_t *)"x\ny\nn", 5);
flan_dyn_map_set(q, flan_dyn_kw((const uint8_t *)"x", 1), flan_dyn_map_set(q, flan_dyn_kw((const uint8_t *)"x", 1),
flan_dyn_from_i64(1)); flan_dyn_from_i64(1));
flan_dyn_map_set(q, flan_dyn_kw((const uint8_t *)"y", 1), flan_dyn_map_set(q, flan_dyn_kw((const uint8_t *)"y", 1),
@ -1236,6 +1240,111 @@ static void classes(void) {
printf(failures == 0 ? "classes ok\n" : "classes failed\n"); printf(failures == 0 ? "classes ok\n" : "classes failed\n");
} }
/* ── update-instance-for-redefined-class, re-entered ─────────────────
*
* The hook is Flan in a program; here it is C, installed where the agent
* installs its caller, which is the same call from flan_dyn.c's side. Three
* stale instances, the first holding the other two as keys of raw [put]s, so
* building the first one's discarded map is where a lookup would compare
* them — and migrate them, and run their hooks, inside the first one's
* migration. The hook itself touches the first instance and builds
* instances of classes the registry has not seen, which grows it and moves
* it under any migration still holding an entry. Each instance's hook runs
* once, and the discarded map holds both instance keys. Clean under
* memcheck is the other half of the claim, and is what @valgrind's run of
* this mode says. */
extern int (*flan_dyn_migrate_hook)(void *fn, uint64_t instance, uint64_t added,
uint64_t discarded);
void flan_dyn_class_hook(void *fn);
static flan_dyn hk_p, hk_q, hk_r;
static int hk_runs_p, hk_runs_q, hk_runs_r, hk_fresh, hk_per_call = 20;
static int64_t hk_gone_len = -1;
static int hk_call(void *fn, uint64_t instance, uint64_t added,
uint64_t discarded) {
char name[16];
int i;
(void)fn;
(void)added;
if (instance == hk_p) {
hk_runs_p++;
hk_gone_len = flan_dyn_need_i64(flan_dyn_len(discarded));
}
if (instance == hk_q) hk_runs_q++;
if (instance == hk_r) hk_runs_r++;
(void)slot(hk_p, "x");
for (i = 0; i < hk_per_call; i++) {
snprintf(name, sizeof name, "fresh%d", hk_fresh++);
(void)flan_dyn_map_new_class(
flan_dyn_kw((const uint8_t *)name, (int64_t)strlen(name)),
(const uint8_t *)"a", 1);
}
return 0;
}
static void hook_reentry(void) {
flan_dyn_root_push(&hk_p);
flan_dyn_root_push(&hk_q);
flan_dyn_root_push(&hk_r);
define("pt", "x\ny");
hk_p = flan_dyn_map_new_class(flan_dyn_kw((const uint8_t *)"pt", 2),
(const uint8_t *)"x\ny", 3);
hk_q = flan_dyn_map_new_class(flan_dyn_kw((const uint8_t *)"pt", 2),
(const uint8_t *)"x\ny", 3);
hk_r = flan_dyn_map_new_class(flan_dyn_kw((const uint8_t *)"pt", 2),
(const uint8_t *)"x\ny", 3);
flan_dyn_map_set(hk_p, flan_dyn_kw((const uint8_t *)"x", 1),
flan_dyn_from_i64(1));
/* Distinct, or they are one key: instances compare by their slots. */
flan_dyn_map_set(hk_q, flan_dyn_kw((const uint8_t *)"x", 1),
flan_dyn_from_i64(2));
flan_dyn_map_set(hk_r, flan_dyn_kw((const uint8_t *)"x", 1),
flan_dyn_from_i64(3));
flan_dyn_map_set(hk_p, hk_q, flan_dyn_from_i64(2));
flan_dyn_map_set(hk_p, hk_r, flan_dyn_from_i64(3));
flan_dyn_migrate_hook = hk_call;
flan_dyn_class_hook((void *)hk_call);
define("pt", "x\ny\nz");
check(flan_dyn_need_i64(slot(hk_p, "x")) == 1, "a kept slot after a re-entered hook");
check(hk_runs_p == 1, "the first instance's hook ran once");
check(hk_runs_q == 0 && hk_runs_r == 0,
"building the first instance's arguments migrated no other instance");
check(hk_gone_len == 2, "the discarded map holds both instance keys");
(void)slot(hk_q, "x");
(void)slot(hk_r, "x");
(void)slot(hk_q, "x");
check(hk_runs_q == 1 && hk_runs_r == 1,
"each other instance runs its hook once, at its own first touch");
check(hk_runs_p == 1, "and the first instance's did not run again");
/* A migration started by a store rather than a read, with a hook that
grows the registry past its capacity each time: the store goes on to
check the value against the class's entry, and it has to be the entry
the registry holds after the hook, not the one it held before. 200 and
then 300 new classes are each enough to move the table whatever its
capacity was. :x is an f64 slot now, so the int stored must arrive as a
float, which only the entry's types can say. */
hk_per_call = 200;
define("pt", "x f64\ny\nz\nw");
flan_dyn_map_put(hk_p, flan_dyn_kw((const uint8_t *)"x", 1),
flan_dyn_from_i64(5), NULL, 0);
check(hk_runs_p == 2, "a put migrates and runs the hook");
check(flan_dyn_tag(slot(hk_p, "x")) == FLAN_DYN_TAG_FLOAT,
"a put after a hook that moved the registry widens by the new entry");
hk_per_call = 300;
define("pt", "x f64\ny\nz\nw\nv");
flan_dyn_slot_set(hk_p, flan_dyn_kw((const uint8_t *)"w", 1),
flan_dyn_from_i64(7), NULL, 0);
check(hk_runs_p == 3, "a set migrates and runs the hook");
check(flan_dyn_need_i64(slot(hk_p, "w")) == 7,
"a set after a hook that moved the registry finds its slot");
flan_dyn_migrate_hook = NULL;
flan_dyn_class_hook(NULL);
flan_dyn_root_pop(3);
printf(failures == 0 ? "hook ok\n" : "hook failed\n");
}
int main(int argc, char **argv) { int main(int argc, char **argv) {
flan_rt_init(argc, argv); flan_rt_init(argc, argv);
if (argc < 2) { if (argc < 2) {
@ -1253,6 +1362,10 @@ int main(int argc, char **argv) {
if (strcmp(argv[1], "unrooted") == 0) { unrooted(); return 0; } if (strcmp(argv[1], "unrooted") == 0) { unrooted(); return 0; }
if (strcmp(argv[1], "park") == 0) { park(); return 0; } if (strcmp(argv[1], "park") == 0) { park(); return 0; }
if (strcmp(argv[1], "desc") == 0) { desc(); return 0; } if (strcmp(argv[1], "desc") == 0) { desc(); return 0; }
if (strcmp(argv[1], "hook") == 0) {
hook_reentry();
return failures == 0 ? 0 : 1;
}
if (strcmp(argv[1], "classes") == 0) { if (strcmp(argv[1], "classes") == 0) {
classes(); classes();
return failures == 0 ? 0 : 1; return failures == 0 ? 0 : 1;

View File

@ -1,6 +1,6 @@
;;;; An array literal with nothing outside it saying what its elements are ;;;; An array literal with nothing outside it saying what its elements are
;;;; takes that from its first element: [(f32 1.0) 2.5] is a [2 f32], and the ;;;; takes that from the elements that are not literals: [(f32 1.0) 2.5] is a
;;;; 2.5 is an f32 literal rather than an f64 refused for not being one. ;;;; [2 f32], and the 2.5 is an f32 literal rather than an f64.
(defn sum3 [a [3 f32]] f32 (+ (at a 0) (at a 1) (at a 2))) (defn sum3 [a [3 f32]] f32 (+ (at a 0) (at a 1) (at a 2)))
(defn main [] i32 (defn main [] i32

View File

@ -0,0 +1,59 @@
;;;; An array literal with nothing outside it naming a type: elements that agree
;;;; are a typed array, numbers meeting at the wider and a literal taking the
;;;; others' type, and elements that do not are a dyn vector.
(defstruct P [x i32 y i32])
(defn mixed [] i32
(let [x (i32 4)
a [(f32 1.0) 2.5 3.25]
b [(i64 1) 2 3]
c [(u8 1) 300]
d [x 2.5]
e [1 18446744073709551615]
f [10 "Hi"]
g [nil 1]
h [None (Some 3)]
i [(P 1 2) {.x 3 .y 4}]
j [[1 2] [3 4]]
k [1 2.5]
m [x (i64 5)]
dd [:a "b" 3]]
(println (length a))
(println (+ (at c 1) (i32 (at c 0))))
(println (at d 1))
(println (at e 1))
(println f)
(println g)
(println (length f))
(println (match (at h 1) None 0 (Some v) v))
(println (.y (at i 1)))
(println (at (at j 1) 0))
(println (at k 0))
(println (+ (at m 0) (i64 9000000000)))
(println dd))
0)
;; (the T e) gives any expression its type.
(defn the-forms [] i32
(let [a (the u8 200)
b (the i64 5000000000)
c (the f32 2.5)
d (the [3 f32] [1 2 3.5])
e (the [f32] [1 2.5])
f (the (Option i32) None)
g (the (Option i32) nil)
h (the dyn 3)
n (the i64 (+ (the i32 1) 2))
v (the (Vec i32) (vec-new))]
(println (+ a (u8 55)))
(println b)
(println (* c (f32 2.0)))
(println (+ (at d 0) (at d 2)))
(println (length e))
(println (match f None 0 (Some x) x))
(println (match g None 7 (Some x) x))
(println h)
(println n)
(println (length v)))
0)
(defn main [] i32 (mixed) (the-forms))

View File

@ -0,0 +1,21 @@
;;;; A class redefined under its instances, with update-instance-for-
;;;; redefined-class written from the session to carry a lost slot's value
;;;; into a gained one. dev-classes.flan is the name-matching half; this is
;;;; the half a method adds, and the method that signals.
;;;;
;;;; The instances are pushed by the editor for dev-classes.flan's reason: a
;;;; compiled caller of the constructor would pin its slot count.
(import agent "vendor:agent")
(defclass point [x y])
(defstruct Refused [why i32])
(defonce instances dyn)
(defn main [] i32
(agent/start "/tmp/flan-dev-hook-fallback.sock")
(set instances (vec-new dyn))
(dotimes [i 4000]
(agent/wait 5))
0)

View File

@ -0,0 +1,8 @@
;;;; A slot type a package wrote names the package's own class, not an
;;;; importer's class of the same bare name.
(import g "pkgs/geo")
(defclass pt [z])
(defn main [] i32
(println (g/mk))
(println (pt 1))
0)

View File

@ -0,0 +1,55 @@
;;;; Typed class slots, and set on a slot.
;;;;
;;;; A slot vector reads as a defn's parameter vector: [pause bool] is a slot
;;;; of type bool, and a name followed by another name is a slot with no type,
;;;; which holds any dyn value. The type is checked when a value is stored --
;;;; by the constructor, by put and by set -- and not when one is read: an
;;;; instance is a dyn map whatever its slots say.
;;;;
;;;; set writes a declared slot. A class declares its slots, so one always
;;;; exists and (get s :pause) is a place, where a plain map's absent key is
;;;; not. dyn-slot-trap.flan has the refusals.
(defclass state [pause bool step i32 speed f64 name string tag])
;; A class is a slot type, and (Option T) admits nil beside a T.
(defclass node [owner state next (Option node) weight (Option f32)])
(defn twelve [] i64 12)
(defn main [] i32
(let [s (state false 3 1.5 "sand" :x)]
(println s)
(set (get s :pause) true)
(println (get s :pause))
;; An i32 slot takes any int in i32's range; a dyn caller passes the
;; value as it is, with nothing converted first.
(set (get s :step) -7)
(println (get s :step))
(set (get s :tag) [1 2])
(println (get s :tag))
;; put reaches the same check for a declared slot, and still inserts a
;; key the class does not declare -- an instance is an open map to put.
(put s :speed 2.5)
(put s :scratch 9)
(println (get s :speed))
(println (get s :scratch))
(println (length s))
;; A typed caller boxes into the dyn parameter as any call does.
(set (get s :step) (twelve))
(println (get s :step))
;; An int into a float slot widens, as it does into a typed f64
;; parameter, when the float holds it exactly.
(put s :speed 3)
(println (+ (get s :speed) 0.5))
;; Exact is a round trip, not a range: 2^54 is an f64 exactly.
(put s :speed 18014398509481984)
(println (= (get s :speed) 18014398509481984.0))
(let [n (node s nil nil)]
(set (get n :next) (node s nil 2))
(println (get (get n :next) :weight))
(set (get n :weight) nil)
;; 2^30 is an f32 exactly, though it is past f32's 24-bit significand.
(set (get n :weight) 1073741824)
(println (class-of (get n :owner)))))
0)

View File

@ -0,0 +1,21 @@
;;;; The refusals of a typed class slot, one per run because each ends the
;;;; process. The argument chooses which. The line numbers are asserted by
;;;; the test, so an edit above them moves them.
(defclass state [pause bool step i32 tag])
(defclass node [owner state])
(defn as-dyn [d dyn] dyn d)
(defn main [args [string]] i32
(let [which (if (> (length args) 1) (i32 (bytes->i64 (bytes-view (at args 1)))) 0)
s (state false 3 nil)]
(println "before")
(cond
(= which 0) (println (state 1 2 3))
(= which 1) (put s :pause 1)
(= which 2) (set (get s :step) 5000000000)
(= which 3) (set (get s :paws) true)
(= which 4) (set (get (as-dyn {:pause 1}) :pause) true)
(= which 5) (println (node (node s)))
:else (println (state nil 1 2))))
0)

View File

@ -60,6 +60,10 @@
(print " ") (print " ")
(println (if ok "ok" "WRONG"))) (println (if ok "ok" "WRONG")))
(defn ne-f64 [a f64 b f64] bool (!= a b))
(defn ne-f32 [a f32 b f32] bool (!= a b))
(defn ne-dyn [a dyn b dyn] bool (!= a b))
(defn main [] i32 (defn main [] i32
;; The integers, each printed as the exact decimal the expected output pins. ;; The integers, each printed as the exact decimal the expected output pins.
(println i8-max) (println i8-max)
@ -115,17 +119,26 @@
;; the absence is recorded rather than merely unmentioned: a float's least ;; the absence is recorded rather than merely unmentioned: a float's least
;; value is the negation of its greatest, and there is nothing to derive. ;; value is the negation of its greatest, and there is nothing to derive.
(say "f32's least value negates its greatest" (say "f32's least value negates its greatest"
(< (- (f32 0.0) f32-max) (- (f32 0.0) f32-min-positive))) (< (- f32-max) (- f32-min-positive)))
(say "f64's least value negates its greatest" (say "f64's least value negates its greatest"
(< (- 0.0 f64-max) (- 0.0 f64-min-positive))) (< (- f64-max) (- f64-min-positive)))
;; The infinities and NaNs, which no literal writes. Each infinity is the ;; The infinities and NaNs, which no literal writes. Each infinity is the
;; overflow of its type's greatest value, negated it is below the least ;; overflow of its type's greatest value, negated it is below the least
;; finite one, and a NaN is the one value not equal to itself. ;; finite one, and a NaN is the one value not equal to itself.
(say "f64-inf" (= f64-inf (* f64-max 2.0))) (say "f64-inf" (= f64-inf (* f64-max 2.0)))
(say "f32-inf" (= f32-inf (* f32-max (f32 2.0)))) (say "f32-inf" (= f32-inf (* f32-max (f32 2.0))))
(say "f64-inf negated" (< (- 0.0 f64-inf) (- 0.0 f64-max))) (say "f64-inf negated" (< (- f64-inf) (- f64-max)))
(say "f32-inf negated" (< (- (f32 0.0) f32-inf) (- (f32 0.0) f32-max))) (say "f32-inf negated" (< (- f32-inf) (- f32-max)))
(say "f64-nan" (not (= f64-nan f64-nan))) (say "f64-nan" (not (= f64-nan f64-nan)))
(say "f32-nan" (not (= f32-nan f32-nan))) (say "f32-nan" (not (= f32-nan f32-nan)))
;; != is the one unordered comparison: a NaN is unequal to everything,
;; itself included, and the dyn side agrees. The operands arrive as
;; parameters so that no constant folder answers in the backend's place.
(say "f64-nan != itself" (ne-f64 f64-nan f64-nan))
(say "f32-nan != itself" (ne-f32 f32-nan f32-nan))
(say "!= over ordinary floats"
(and (ne-f64 1.0 2.0) (not (ne-f64 1.5 1.5))
(ne-f32 (f32 1.0) (f32 2.0)) (not (ne-f32 (f32 1.5) (f32 1.5)))))
(say "a dyn NaN != itself" (ne-dyn f64-nan f64-nan))
0) 0)

View File

@ -0,0 +1,18 @@
;;;; A literal arm takes its type from the arm that is not a literal, in an
;;;; if, a cond and a match alike, as a literal operand of + does.
(defn g [c bool n i64] i64 (let [x (if c 4000000 n)] x))
(defn h [k i32 n i64] i64
(let [x (cond (= k 0) 5000000000 (= k 1) 7 :else n)] x))
(defn m [o (Option i64)] i64
(let [x (match o None 3 (Some v) v)] x))
(defn f32s [c bool y f32] f32 (let [x (if c 2.5 y)] x))
(defn main [] i32
(println (g true (i64 3)))
(println (g false (i64 9000000000)))
(println (h 0 (i64 1)))
(println (h 1 (i64 1)))
(println (h 2 (i64 9000000000)))
(println (m None))
(println (m (Some (i64 9000000000))))
(println (f32s true (f32 1.0)))
0)

View File

@ -0,0 +1,54 @@
;;;; (max-value T) and (min-value T): a numeric type's limits, named by the type, at
;;;; a concrete type and inside a generic whose bound admits numbers.
;; A selection sort, descending, whose running best starts at the least value
;; of the element type, so any element beats it.
(defn sort-desc [s [$t]] ()
{:where (numeric? $t)}
(dotimes [i (length s)]
(let [best (min-value $t)
at-best i]
(dotimes [j (- (length s) i)]
(let [k (+ i j)]
(when (> (at s k) best)
(set best (at s k))
(set at-best k))))
(swap s i at-best))))
(defn largest [s [$t]] $t
{:where (numeric? $t)}
(let [best (min-value t)]
(dotimes [i (length s)]
(when (> (at s i) best) (set best (at s i))))
best))
(defn show-i32 [s [i32]] ()
(dotimes [i (length s)] (print (at s i)) (print " "))
(println ""))
(defn show-f64 [s [f64]] ()
(dotimes [i (length s)] (print (at s i)) (print " "))
(println ""))
(defn main [] i32
(println (max-value u8))
(println (min-value u8))
(println (max-value i8))
(println (min-value i8))
(println (max-value i32))
(println (min-value i64))
(println (max-value u64))
(println (= (max-value f32) f32-max))
(println (= (min-value f64) (- f64-max)))
(println (= (max-value i16) i16-max))
(let [a [(i32 3) -7 12 0 -2147483648 5]
b [2.5 -1.0 1e300 -1e308]
c [(u8 4) 0 200 9]]
(sort-desc (slice a))
(show-i32 (slice a))
(sort-desc (slice b))
(show-f64 (slice b))
(println (largest (slice c)))
(println (let [d [(i64 -5) -9]] (largest (slice d))))
(println (let [e [(f32 -1.0) -3.0]] (= (largest (slice e)) (f32 -1.0)))))
0)

25
test/programs/negate.flan Normal file
View File

@ -0,0 +1,25 @@
;;;; (- x) negates: an integer wraps, a float flips its sign — the negation of
;;;; 0.0 is -0.0, which 1/x tells apart — and a dyn does either by its tag.
(defn negi [x i32] i32 (- x))
(defn negf [x f64] f64 (- x))
(defn negf32 [x f32] f32 (- x))
(defn negu [x u8] u8 (- x))
(defn negd [x dyn] dyn (- x))
(defn negg [x $t] $t {:where (numeric? $t)} (- x))
(defn main [] i32
(println (negi 3))
(println (negi -7))
(println (negf 2.5))
(println (/ 1.0 (negf 0.0)))
(println (negf32 (f32 1.5)))
(println (negu (u8 1)))
(println (negd 4))
(println (negd 2.5))
(println (/ 1.0 (negd 0.0)))
(println (negg (i64 9000000000)))
(println (negg 0.5))
(let [a (- 5) b (i64 (- 3))]
(println (+ a (i32 b))))
(println (- f64-inf))
(println (< (- f64-inf) (- f64-max)))
0)

View File

@ -0,0 +1,5 @@
;;;; A package whose slot types name its own classes, imported by
;;;; dyn-class-pkg.flan, which declares a class of the same bare name.
(defclass pt [x f64 y f64])
(defclass seg [a pt b (Option pt) tag])
(defn mk [] dyn (seg (pt 1 2) nil :t))

View File

@ -0,0 +1,13 @@
;;;; A program's function named as a prelude function takes the name over for
;;;; the calls in its own file, and the prelude's own calls keep the prelude's:
;;;; ceil-f32 is written over the prelude's floor-f32, and still answers 3.
(defn abs-f32 [v f32] f32 (if (< v 0.0) (- v) (+ v (f32 100.0))))
(defn floor-f32 [x f32] f32 (f32 999.0))
(defn abs [x i32] i32 (* x 10))
(defn main [] i32
(println (abs-f32 (f32 -2.5)))
(println (abs-f32 (f32 2.5)))
(println (floor-f32 (f32 2.3)))
(println (ceil-f32 (f32 2.3)))
(println (abs -3))
0)

View File

@ -563,13 +563,50 @@ let () =
fpu_out; fpu_out;
outputs ~x86:true "a pointer and a union filled, x86" outputs ~x86:true "a pointer and a union filled, x86"
"programs/fill-ptr-union.flan" fpu_out; "programs/fill-ptr-union.flan" fpu_out;
(* An array literal takes its element type from its first element when (* A literal element takes its type from the other elements when nothing
nothing outside it names one. *) outside the array names one. *)
let first_out = "3\n6.75\n9000000002\n255\n" in let first_out = "3\n6.75\n9000000002\n255\n" in
outputs "an array literal's first element types the rest" outputs "an array literal's first element types the rest"
"programs/array-first-element.flan" first_out; "programs/array-first-element.flan" first_out;
outputs ~x86:true "an array literal's first element types the rest, x86" outputs ~x86:true "an array literal's first element types the rest, x86"
"programs/array-first-element.flan" first_out; "programs/array-first-element.flan" first_out;
(* An array literal whose elements agree is typed and one whose elements
mix is a dyn vector; (the T e) gives any expression its type. *)
let mixed_out =
"3\n301\n2.5\n18446744073709551615\n[ 10 \"Hi\"]\n[ nil 1]\n2\n3\n4\n\
3\n1\n9000000004\n[ :a \"b\" 3]\n\
255\n5000000000\n5\n4.5\n2\n0\n7\n3\n3\n0\n" in
outputs "mixed array literals and the" "programs/array-mixed.flan" mixed_out;
outputs ~x86:true "mixed array literals and the, x86"
"programs/array-mixed.flan" mixed_out;
(* A program's function named as a prelude function takes the name over
for its own file; the prelude's own calls keep the prelude's. *)
let sp_out = "2.5\n102.5\n999\n3\n-30\n" in
outputs "a prelude function shadowed" "programs/shadow-prelude.flan" sp_out;
outputs ~x86:true "a prelude function shadowed, x86"
"programs/shadow-prelude.flan" sp_out;
(* (max-value T) and (min-value T), concrete and inside a generic. *)
let maxof_out =
"255\n0\n127\n-128\n2147483647\n-9223372036854775808\n\
18446744073709551615\ntrue\ntrue\ntrue\n\
12 5 3 0 -7 -2147483648 \n1e+300 2.5 -1 -1e+308 \n200\n-5\ntrue\n" in
outputs "max-value and min-value" "programs/max-value.flan" maxof_out;
outputs ~opt:"-O0" "max-value and min-value, -O0" "programs/max-value.flan" maxof_out;
outputs ~x86:true "max-value and min-value, x86" "programs/max-value.flan" maxof_out;
(* (- x) negates, on every numeric type, a type variable and a dyn. *)
let neg_out =
"-3\n7\n-2.5\n-inf\n-1.5\n255\n-4\n-2.5\n-inf\n-9000000000\n\
-0.5\n-8\n-inf\ntrue\n" in
outputs "unary minus" "programs/negate.flan" neg_out;
outputs ~opt:"-O0" "unary minus, -O0" "programs/negate.flan" neg_out;
outputs ~x86:true "unary minus, x86" "programs/negate.flan" neg_out;
(* A literal arm takes the other arm's type. *)
let arm_out =
"4000000\n9000000000\n5000000000\n7\n9000000000\n3\n9000000000\n2.5\n" in
outputs "a literal arm takes the other arm's type"
"programs/literal-arm.flan" arm_out;
outputs ~x86:true "a literal arm takes the other arm's type, x86"
"programs/literal-arm.flan" arm_out;
(* into. The count of pulls is the assertion a unit test cannot make: one (* into. The count of pulls is the assertion a unit test cannot make: one
pass, one call per element per stage it reaches, and no intermediate pass, one call per element per stage it reaches, and no intermediate
collection anywhere. The two show lines either side of it are the same collection anywhere. The two show lines either side of it are the same
@ -5080,6 +5117,57 @@ level "1"
index_site (); index_site ();
index_site ~x86:true (); index_site ~x86:true ();
(* Typed class slots and set on a slot: the stores that fit, then one
run per refusal. The constructor, put and set each check a declared
slot's type, set refuses a slot the class does not declare and a
value that is not an instance, and put still inserts an undeclared
key. On both backends, because every one of these is a runtime call
whose arguments the two emit separately. *)
let slots_out =
"#state{ :pause false :step 3 :speed 1.5 :name \"sand\" :tag :x}\n\
true\n-7\n[ 1 2]\n2.5\n9\n6\n12\n3.5\ntrue\n2\n:state\n"
in
outputs "dyn: typed class slots" "programs/dyn-class-slots.flan" slots_out;
outputs ~x86:true "dyn: typed class slots, --x86"
"programs/dyn-class-slots.flan" slots_out;
outputs "dyn: a package's slot type names its own class"
"programs/dyn-class-pkg.flan"
"#g/seg{ :a #g/pt{ :x 1 :y 2} :b nil :tag :t}\n#pt{ :z 1}\n";
let slot_trap ?x86 () =
let exe = compile ?x86 "programs/dyn-slot-trap.flan" in
List.iter
(fun (arg, want) ->
let code, text = run exe (Some arg) in
if code <> 134 || not (contains text want) then begin
incr failures;
Printf.printf
"FAIL dyn: a class slot's refusal%s\n got: %S \
(exit %d)\n wanted: %S (exit 134)\n"
(match x86 with Some true -> ", --x86" | _ -> "")
text code want
end)
[ ("0", "dyn-slot-trap.flan:14:28: dyn construct: the slot :pause of \
state is declared bool, and this is int — (state ...) with \
:pause 1; the slot is declared at programs/dyn-slot-trap.flan:4:18");
("1", "dyn-slot-trap.flan:15:19: dyn put: the slot :pause of state \
is declared bool, and this is int");
("2", "dyn-slot-trap.flan:16:19: dyn set: the slot :step of state \
is declared i32, and 5000000000 is not a value it holds \
exactly");
("3", "dyn-slot-trap.flan:17:19: dyn set: state has no slot :paws. \
Its slots are :pause :step :tag; a key the class does not \
declare is added with put, not set");
("4", "dyn-slot-trap.flan:18:19: dyn set: (get m k) is a place only \
on a class instance, and this is a map with no class");
("5", "dyn-slot-trap.flan:19:28: dyn construct: the slot :owner of \
node is declared state, and this is not an instance of it");
("6", "dyn-slot-trap.flan:20:22: dyn construct: the slot :pause of \
state is declared bool, and this is nil") ];
(try Sys.remove exe with Sys_error _ -> ())
in
slot_trap ();
slot_trap ~x86:true ();
(* A numeric cast opening a dyn box — TODO.org, "A numeric cast opens a (* A numeric cast opening a dyn box — TODO.org, "A numeric cast opens a
dyn box". programs/dyn-cast.flan is one program because the three dyn box". programs/dyn-cast.flan is one program because the three
behaviours are one story told in order: the same-kind casts print, the behaviours are one story told in order: the same-kind casts print, the
@ -5684,7 +5772,9 @@ level "1"
f32's least value negates its greatest ok\n\ f32's least value negates its greatest ok\n\
f64's least value negates its greatest ok\n\ f64's least value negates its greatest ok\n\
f64-inf ok\nf32-inf ok\nf64-inf negated ok\nf32-inf negated ok\n\ f64-inf ok\nf32-inf ok\nf64-inf negated ok\nf32-inf negated ok\n\
f64-nan ok\nf32-nan ok\n" f64-nan ok\nf32-nan ok\n\
f64-nan != itself ok\nf32-nan != itself ok\n\
!= over ordinary floats ok\na dyn NaN != itself ok\n"
in in
outputs "type limits" "programs/limits.flan" limits_out; outputs "type limits" "programs/limits.flan" limits_out;
outputs ~opt:"-O0" "type limits, -O0" "programs/limits.flan" limits_out; outputs ~opt:"-O0" "type limits, -O0" "programs/limits.flan" limits_out;
@ -6716,6 +6806,28 @@ level "1"
cli_case "--debug and an explicit -O are refused together" cli_case "--debug and an explicit -O are refused together"
"build ../calc-me.flan --debug -O2 -o /dev/null" ~code:2 "build ../calc-me.flan --debug -O2 -o /dev/null" ~code:2
~says:[ "--debug"; "-O2"; "Drop one of the two" ]; ~says:[ "--debug"; "-O2"; "Drop one of the two" ];
(* The name a shadowed prelude function is moved to is nobody's to read. *)
(let code, text = cli "check programs/shadow-prelude.flan" in
if code <> 0 || contains text "prelude~"
|| not (contains text "defn floor-f32")
then begin
incr failures;
Printf.printf
"FAIL check's listing leaves out the renamed prelude function\n\
\ got: %S (exit %d)\n" text code
end);
(* A file with no main is refused by name before the link, which would
otherwise report an undefined reference from crt1.o. *)
let nomain = Filename.concat scratch "no-main.flan" in
Out_channel.with_open_bin nomain (fun oc ->
output_string oc "(defn f [] i32 0)\n");
cli_case "build of a file with no main names main"
(Printf.sprintf "build %s -o /dev/null" (Filename.quote nomain)) ~code:1
~says:[ "has no main"; "(defn main [] i32" ];
cli_case "run of a file with no main names main"
(Printf.sprintf "run %s" (Filename.quote nomain)) ~code:1
~says:[ "has no main"; "(defn main [] i32" ];
Sys.remove nomain;
(* Every row above that went through the pool has been forked; nothing (* Every row above that went through the pool has been forked; nothing
after this point may look at [failures] until every one of them has after this point may look at [failures] until every one of them has

View File

@ -8073,6 +8073,168 @@ let () =
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ()) List.iter (fun f -> try Sys.remove f with Sys_error _ -> ())
[ lsock2; lout2 ]; [ lsock2; lout2 ];
(* ── update-instance-for-redefined-class, written from the session ──
The method is written and installed while the program runs, then the
class is redefined, and each instance runs the method at its first
touch after that. On both backends, because the method is Flan code
the C runtime calls from inside [get], and that call is the one piece
of this each backend's calling convention has to agree with.
Three claims, in order: a method carries a lost slot's value into a
gained one; a method that signals stops the program with
[migrate-by-name] on offer, and taking it leaves the instance as
name-matching made it, the method's own write included; and a kept
value that no longer fits its slot's new type stays, with a warning.
stdout and stderr go to one file, which is where the warning is read
from. *)
let hook_block ~llvm =
let what = if llvm then "llvm: " else "" in
let hsock = tmp (if llvm then "hook-llvm.sock" else "hook.sock")
and hout = tmp (if llvm then "hook-llvm.out" else "hook.out") in
(try Sys.remove hsock with Sys_error _ -> ());
let hfd =
Unix.openfile hout [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600
in
let argv =
Array.append
[| flan; "dev"; "programs/dev-hook.flan"; "-s"; hsock |]
(if llvm then [| "--llvm" |] else [||])
in
let hpid = Unix.create_process flan argv Unix.stdin hfd hfd in
Unix.close hfd;
let output () = In_channel.with_open_bin hout In_channel.input_all in
if not (listening ~pid:hpid hsock) then begin
fail "%sthe hook daemon %s (%S)" what !listen_why (output ());
(try Unix.kill hpid Sys.sigkill with Unix.Unix_error _ -> ())
end
else begin
let c = connect hsock in
let said r = Option.value ~default:"" (Wire.string_field r "message") in
let value r = Option.value ~default:"" (Wire.string_field r "value") in
let file = " :file \"programs/dev-hook.flan\")" in
let ask code =
request c (Printf.sprintf "(:op \"eval-expr\" :code %S%s" code file)
in
let redefine code =
request c (Printf.sprintf "(:op \"eval\" :code %S%s" code file)
in
let holds claim code =
let r = ask code in
if status r <> "ok" then fail "%s%s: %s" what claim (said r)
else if value r <> "1" then
fail "%s%s answered %S (%s)" what claim (value r) code
in
let defined claim code =
let r = redefine code in
if status r <> "ok" then (fail "%s%s: %s" what claim (said r); false)
else true
in
let stopped r =
match Wire.field r "stopped" with
| Some { Form.v = Form.Sym "t"; _ } -> true
| _ -> false
in
let started () =
status (ask "(do (push instances (point 3 4)) 1)") = "ok"
in
if not (await started) then
fail "%sthe hook daemon never reached a frame boundary" what
else begin
holds "a second instance" "(do (push instances (point 5 6)) 1)";
(* ── A method that carries a value across ── *)
if defined "a method of the migration generic, from the session"
"(defmethod update-instance-for-redefined-class point \
[p added discarded] \
(set (get p :radius) (get discarded :y)) nil)"
&& defined "a class redefined under a method"
"(defclass point [x radius])"
then begin
holds "the method moved the lost slot's value into the new one"
"(if (= (get (at instances 0) :radius) 4) 1 0)";
holds "a kept slot is untouched by the method"
"(if (= (get (at instances 0) :x) 3) 1 0)";
holds "the lost slot is gone"
"(if (= (length (at instances 0)) 2) 1 0)";
holds "each instance runs the method at its own first touch"
"(if (= (get (at instances 1) :radius) 6) 1 0)"
end;
(* ── A method that signals ── *)
if defined "a method that signals"
"(defmethod update-instance-for-redefined-class point \
[p added discarded] \
(set (get p :x) 99) (error (Refused {.why 1})) nil)"
&& defined "a class redefined under a method that signals"
"(defclass point [x radius z])"
then begin
let r = ask "(get (at instances 0) :x)" in
if status r <> "error" then
fail "%sa migration whose method signals answered %s" what
(status r);
let r = request c "(:op \"break\")" in
let names =
match Wire.field r "restarts" with
| Some { Form.v = Form.List l; _ } ->
List.filter_map
(fun (n : Form.t) ->
match n.Form.v with Form.Str x -> Some x | _ -> None)
l
| _ -> []
in
(match names with
| "migrate-by-name" :: _ -> ()
| _ ->
fail "%sthe restarts at a signalling method: %s" what
(String.concat ", " names));
let r = request c "(:op \"restart\" :name \"migrate-by-name\")" in
if status r <> "ok" then
fail "%smigrate-by-name was refused: %s" what (said r);
if not
(await (fun () -> not (stopped (request c "(:op \"describe\")"))))
then fail "%sthe program did not run again after migrate-by-name" what
else begin
holds "migrate-by-name undoes the method's write"
"(if (= (get (at instances 0) :x) 3) 1 0)";
holds "and keeps what name-matching kept"
"(if (= (get (at instances 0) :radius) 4) 1 0)";
holds "and has the new definition's slots"
"(if (= (length (at instances 0)) 3) 1 0)"
end
end;
(* ── A type that no longer fits ── *)
if defined "a method that does nothing"
"(defmethod update-instance-for-redefined-class point \
[p added discarded] nil)"
&& defined "a slot's type changed to one its value does not fit"
"(defclass point [x string radius z n i32 note string])"
then begin
holds "a value that no longer fits is kept"
"(if (= (get (at instances 0) :x) 3) 1 0)";
(* A typed slot gained by the redefinition starts at its type's
zero value, as a typed binding does, and not at nil. *)
holds "a gained i32 slot is 0"
"(if (= (get (at instances 0) :n) 0) 1 0)";
holds "a gained string slot is empty"
"(if (= (get (at instances 0) :note) \"\") 1 0)";
let warned () =
contains_sub (output ())
"warning: point was redefined, and its slot :x is now \
declared string"
in
if not (await warned) then
fail "%sno warning for a kept value that does not fit: %S" what
(output ())
end
end;
(try Unix.close c with Unix.Unix_error _ -> ());
(try Unix.kill hpid Sys.sigkill with Unix.Unix_error _ -> ());
(try ignore (Unix.waitpid [] hpid) with Unix.Unix_error _ -> ())
end;
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ())
[ hsock; hout ]
in
hook_block ~llvm:false;
hook_block ~llvm:true;
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ()) List.iter (fun f -> try Sys.remove f with Sys_error _ -> ())
[ sock; out; bsock; bout ]; [ sock; out; bsock; bout ];
Test_support.report ~label:"dev" () Test_support.report ~label:"dev" ()

View File

@ -154,6 +154,14 @@ let () =
if code <> 0 || out <> "classes ok\n" then if code <> 0 || out <> "classes ok\n" then
fail "redefining a class\n got: %S (exit %d, err %S)" out code err; fail "redefining a class\n got: %S (exit %d, err %S)" out code err;
(* A migration's hook re-entered from the building of its own
arguments, and a hook that grows the class registry under it — see
dyn_ops.c's [hook_reentry]. *)
let code, out, err = run "hook" in
if code <> 0 || out <> "hook ok\n" then
fail "a re-entered migration hook\n got: %S (exit %d, err %S)"
out code err;
let code, out, _ = run "nested" in let code, out, _ = run "nested" in
if code <> 0 || out <> "chain of 64 intact: yes\n" then if code <> 0 || out <> "chain of 64 intact: yes\n" then
fail "a chain of nested vecs\n got: %S (exit %d)" out code; fail "a chain of nested vecs\n got: %S (exit %d)" out code;
@ -205,6 +213,7 @@ let () =
let refusals = let refusals =
[ ("add", "dyn +: int and text"); [ ("add", "dyn +: int and text");
("sub", "dyn -: nil and int"); ("sub", "dyn -: nil and int");
("neg", "dyn -: text, and it takes a number");
("mul", "dyn *: bool and int"); ("mul", "dyn *: bool and int");
("div", "dyn /: vec and int"); ("div", "dyn /: vec and int");
("rem", "dyn %: int and nil"); ("rem", "dyn %: int and nil");

View File

@ -1245,7 +1245,7 @@ let () =
accepts "return type types the literal" "(defn f [] u8 0)"; accepts "return type types the literal" "(defn f [] u8 0)";
accepts "return type types None" "(defn f [] (Option f64) None)"; accepts "return type types None" "(defn f [] (Option f64) None)";
rejects_check "bare None has no type" "(defconst x None)" rejects_check "bare None has no type" "(defconst x None)"
~needle:"what None is an Option of"; ~needle:"(the (Option i32) None)";
accepts "param types the literal" accepts "param types the literal"
"(defn g [x u8] ()) (defn f [] () (g 3))"; "(defn g [x u8] ()) (defn f [] () (g 3))";
rejects_check "wrong argument type" rejects_check "wrong argument type"
@ -1337,14 +1337,18 @@ let () =
infers "min at four" "(min 4 1 3 2)" "i32"; infers "min at four" "(min 4 1 3 2)" "i32";
infers "max at four" "(max 4 1 3 2)" "i32"; infers "max at four" "(max 4 1 3 2)" "i32";
(* And the two counts below the floor. Zero would have to mean an identity (* And the two counts below the floor. Zero would have to mean an identity
element and one a unary operator this language does not have; both are a element, and one is refused for every operator but -, whose one-operand
typo far more often than an intent, so both are refused by name. *) form negates. *)
rejects_check "a sum with no terms" rejects_check "a sum with no terms"
"(defn f [] i32 (+))" ~needle:"+ takes two arguments or more, given 0"; "(defn f [] i32 (+))" ~needle:"+ takes two arguments or more, given 0";
rejects_check "a product with no factors" rejects_check "a product with no factors"
"(defn f [] i32 (*))" ~needle:"* takes two arguments or more, given 0"; "(defn f [] i32 (*))" ~needle:"* takes two arguments or more, given 0";
rejects_check "there is no unary minus" infers "a negated literal" "(- 1)" "i32";
"(defn f [] i32 (- 1))" ~needle:"there is no unary minus"; infers "a negated literal takes its type from the site" "(i64 (- 1))" "i64";
rejects_check "unary minus over a string names the operand"
"(defn f [s string] () (println (- s)))" ~needle:"- takes numbers";
rejects_check "unary minus at an unsigned literal is out of range"
"(defn f [] u8 (- 1))" ~needle:"does not fit in u8";
rejects_check "there is no reciprocal" rejects_check "there is no reciprocal"
"(defn f [] f64 (/ 2.0))" ~needle:"there is no reciprocal"; "(defn f [] f64 (/ 2.0))" ~needle:"there is no reciprocal";
rejects_check "one operand is not a bitwise and" rejects_check "one operand is not a bitwise and"
@ -2711,14 +2715,38 @@ let () =
rejects_check "a constructor takes one argument per slot" rejects_check "a constructor takes one argument per slot"
"(defclass point [x y])\n(defn main [] i32 (let [p (point 1)] 0))" "(defclass point [x y])\n(defn main [] i32 (let [p (point 1)] 0))"
~needle:"point"; ~needle:"point";
(* A slot vector holds names and nothing else. [(defclass point [x i64])] (* A slot vector is a defn's parameter vector: a name followed by a type
is therefore two slots, one of them unfortunately named — the parser is a typed slot, a name followed by another name is an untyped one. The
cannot tell a type's name from a slot's and does not have to, since a type is what a stored dyn value is checked against, so it is one a dyn
slot has no type to write. What it can tell is a form that is not a name value can be checked as, and nothing else. *)
at all. *) accepts "typed slots, and untyped ones beside them"
rejects_check "a slot is a name, not a type expression" "(defclass state [pause bool step bool n i32 tag])\n\
(defn main [] i32 (let [s (state false true 3 :x)] (if (= (get s :n) 3) 0 1)))";
accepts "a class and an Option as slot types"
"(defclass point [x f64])\n\
(defclass node [at point next (Option node) w (Option i32)])\n\
(defn main [] i32 (let [n (node (point 1) nil nil)] 0))";
rejects_check "an Option of dyn is not a slot type"
"(defclass point [x (Option dyn)])\n(defn main [] i32 0)"
~needle:"the slot x of point is declared (Option dyn)";
rejects_check "a slot's type is one a dyn value can be checked as"
"(defclass point [x (Ptr i64)])\n(defn main [] i32 0)" "(defclass point [x (Ptr i64)])\n(defn main [] i32 0)"
~needle:"a class slot is a name"; ~needle:"the slot x of point is declared (Ptr i64)";
rejects_check "a capitalised name in a slot vector is an unknown type"
"(defclass point [x Widget])\n(defn main [] i32 0)"
~needle:"unknown type Widget";
rejects_check "a slot's type is resolved like any other"
"(defclass point [x f65])\n(defn main [] i32 0)"
~needle:"did you mean f64";
(* The slot is a place: its class declares it, so it always exists. *)
accepts "set writes a class slot"
"(defclass state [pause bool])\n\
(defn main [] i32 (let [s (state false)] (set (get s :pause) true) \
(if (get s :pause) 0 1)))";
rejects_check "a class slot has no address"
"(defclass state [pause bool])\n\
(defn main [] i32 (let [s (state false)] (addr (get s :pause)) 0))"
~needle:"addr takes the address of a place";
rejects_check "a class does not name a slot twice" rejects_check "a class does not name a slot twice"
"(defclass point [x x])\n(defn main [] i32 0)" "(defclass point [x x])\n(defn main [] i32 0)"
~needle:"names the slot x twice"; ~needle:"names the slot x twice";
@ -3001,7 +3029,7 @@ let () =
~needle:"needs to know the type it is filling"; ~needle:"needs to know the type it is filling";
rejects_check "a dead-beef in a position with no expected type" rejects_check "a dead-beef in a position with no expected type"
"(defn f [] () (print (dead-beef)))" "(defn f [] () (print (dead-beef)))"
~needle:"needs to know the type it is filling"; ~needle:"(the [4 u32] (dead-beef))";
(* The byte is a u8 and the ordinary literal rule applies to it — there is (* The byte is a u8 and the ordinary literal rule applies to it — there is
no range check of this builtin's own, and there does not need to be. *) no range check of this builtin's own, and there does not need to be. *)
rejects_check "a fill byte out of range" rejects_check "a fill byte out of range"
@ -3810,7 +3838,10 @@ let () =
into a lookup and no place form for one. Refused with that reason rather into a lookup and no place form for one. Refused with that reason rather
than as a milestone that will never arrive. *) than as a milestone that will never arrive. *)
rejects_check "a map entry as a place" rejects_check "a map entry as a place"
"(defn f [] () (set (get m 1) 2))" "(defn f [m (Map i64 i64)] () (set (get m 1) 2))"
~needle:"entries are written with put";
rejects_check "get with three arguments is not a place"
"(defn f [m dyn] () (set (get m 1 2) 2))"
~needle:"a map is written with (put m k v)"; ~needle:"a map is written with (put m k v)";
(* ── restart-case and invoke-restart, §3 to §6 ─────────────────── *) (* ── restart-case and invoke-restart, §3 to §6 ─────────────────── *)
@ -5406,6 +5437,24 @@ let () =
| exception Loc.Error _ -> false); | exception Loc.Error _ -> false);
check "a program that shadows nothing is warned at not at all" check "a program that shadows nothing is warned at not at all"
(Check.shadowed_builtins (program "(defn f [] i32 1)") = []); (Check.shadowed_builtins (program "(defn f [] i32 1)") = []);
(* A prelude function's name is taken over the same way, for the calls in
the defining file. *)
let prelude_src = "(defn abs-f32 [v f32] f32 v)" in
(match
snd (Check.shadow_prelude (Parse.program (Prelude.forms ()))
(program prelude_src))
with
| [ d ] ->
check "a defn of a prelude function's name warns once"
(d.Loc.kind = "check/shadows-prelude"
&& d.Loc.dmsg
= "abs-f32 shadows the prelude's abs-f32 — every call in this file \
now reaches your definition")
| _ -> check "a defn of a prelude function's name warns exactly once" false);
accepts "a defn of a prelude function's name is not defined twice"
prelude_src;
rejects_check "a struct of a prelude type's name is still defined twice"
"(defstruct Form [x i32])" ~needle:"Form is defined twice";
(* An operator is a builtin like any other and shadows like any other. (* An operator is a builtin like any other and shadows like any other.
Pinned in both halves because it is the case most likely to be thought Pinned in both halves because it is the case most likely to be thought
of as special and quietly excepted later: the warning is the same of as special and quietly excepted later: the warning is the same
@ -6205,8 +6254,61 @@ let () =
"(defconst a u64 18446744073709551615) (defonce b u64 0xFFFFFFFFFFFFFFFF) \ "(defconst a u64 18446744073709551615) (defonce b u64 0xFFFFFFFFFFFFFFFF) \
(defn f [x u64] u64 (+ x 9223372036854775808)) \ (defn f [x u64] u64 (+ x 9223372036854775808)) \
(defn g [] f64 (f64 (u64 12345678901234567890)))"; (defn g [] f64 (f64 (u64 12345678901234567890)))";
accepts "a negative decimal is still a u64 bit pattern" (* A negative literal fits no unsigned type, wherever the type comes from;
"(defconst a u64 -1)"; the cast the refusal names is how to write the bit pattern. *)
List.iter
(fun (what, src, needle) -> rejects_check what src ~needle)
[ ("a negative literal at a u64 constant", "(defconst a u64 -1)",
"-1 does not fit in u64, which holds no negative number — write \
(u64 -1) for the u64 with the same bits, 18446744073709551615");
("a negative literal at a u32 global", "(defonce g u32 -5)",
"write (u32 -5) for the u32 with the same bits, 4294967291");
("a negative literal as a u64 return", "(defn f [] u64 -1)",
"-1 does not fit in u64");
("a negative literal as a u32 argument",
"(defn t [x u32] u32 x) (defn f [] u32 (t -2))", "-2 does not fit in u32");
("a negative literal in a u8 field",
"(defstruct S [a u8]) (defn f [] S (S -3))", "write (u8 -3)");
("a negative literal given a u64 by the",
"(defn f [] i32 (let [a (the u64 -1)] 0))", "-1 does not fit in u64");
("a negative literal beside a u64-only literal",
"(defn f [] i32 (let [a [-1 18446744073709551615]] 0))",
"write (u64 -1) for the u64");
("a negative literal beside a u64 element",
"(defn f [x u64] i32 (let [a [x -1]] 0))", "write (u64 -1) for the u64") ];
accepts "the casts those refusals name compile"
"(defconst a u64 (u64 -1)) (defonce g u32 (u32 -5)) \
(defstruct S [a u8]) (defn f [x u64] S \
(let [a [(u64 -1) 18446744073709551615] b [x (u64 -1)] \
c (the u64 (u64 -1))] \
(S (u8 -3))))";
(* The literal that does not fit is the one blamed, not one that does. *)
rejects_check "a negative literal among u64 elements is the one blamed"
"(defn f [] () (println [(u64 2) 1 -1]))" ~needle:"-1 does not fit in u64";
rejects_check "a negative literal after a u64 element is the one blamed"
"(defn f [] () (println [1 (u64 2) -1]))" ~needle:"-1 does not fit in u64";
(* In a generic body the cast would break the other instantiations. *)
(match
checked
"(defn add1 [x $t] $t {:where (numeric? $t)} (+ x -1)) \
(defn main [] i32 (add1 3) (add1 (u64 5)) 0)"
with
| _ -> check "a negative literal at a u64 instantiation is refused" false
| exception Loc.Error d ->
check "the generic's refusal names a fix for every type and the call"
(contains d.Loc.dmsg "as in (- x 1) in place of (+ x -1)"
&& not (contains d.Loc.dmsg "(u64 -1)")
&& List.exists
(fun (n : Loc.note) ->
contains n.Loc.nmsg "add1 is instantiated at $t = u64 here")
d.Loc.notes));
accepts "the generic's fix compiles at both types"
"(defn add1 [x $t] $t {:where (numeric? $t)} (- x 1)) \
(defn main [] i32 (add1 3) (add1 (u64 5)) 0)";
accepts "a doubly negated literal is positive at an unsigned type"
"(defn f [] u8 (- (- 1)))";
rejects_check "a folded constant's conversion is still its type"
"(defconst a u8 (i32 5))" ~needle:"expected u8, found i32";
rejects_check "a wide decimal with nothing to say u64" rejects_check "a wide decimal with nothing to say u64"
~needle:"18446744073709551615 does not fit in i32, the type an integer \ ~needle:"18446744073709551615 does not fit in i32, the type an integer \
literal takes when nothing says otherwise — write (u64 \ literal takes when nothing says otherwise — write (u64 \
@ -6510,17 +6612,106 @@ let () =
parse_rejects "the $ refusal names the bare spelling" parse_rejects "the $ refusal names the bare spelling"
"(defn $foo [x i32] i32 x)" ~needle:"Name it foo"; "(defn $foo [x i32] i32 x)" ~needle:"Name it foo";
(* ── An array literal's first element types the rest ───────────── *) (* ── An array literal with nothing outside it naming a type ────── *)
accepts "an f32 array literal from its first element" infers "a literal takes the other elements' type" "[(f32 1.0) 2.5]" "[2 f32]";
"(defn main [] i32 (let [a [(f32 1.0) 2.5]] (i32 (length a))))"; infers "numbers meet at the wider" "[(u8 1) 256]" "[2 i32]";
(match checked "(defn main [] i32 (let [a [(u8 1) 256]] 0))" with infers "an int and a float literal meet at f64" "[1 2.5]" "[2 f64]";
| _ -> check "an element that does not fit the first element's type" false infers "a wide literal makes the array u64" "[1 18446744073709551615]" "[2 u64]";
infers "None takes the other element's Option" "[None (Some 1)]" "[2 (Option i32)]";
infers "a number and a string are a dyn vector" "[10 \"Hi\"]" "dyn";
infers "nil beside a number is a dyn vector" "[nil 1]" "dyn";
infers "two dyns are a typed array of dyn" "[nil nil]" "[2 dyn]";
infers "the names the element type of a mixed literal" "(the [dyn] [1 2.5])" "[2 dyn]";
infers "the with a slice type gives the literal's array type"
"(the [f32] [1 2.5])" "[2 f32]";
(match checked "(defstruct P [x i32]) (defn main [] i32 (let [a [(P 1) 2]] 0))" with
| _ -> check "a struct beside a number is refused" false
| exception Loc.Error d -> | exception Loc.Error d ->
check "the refusal says the first element set the type" check "elements that cannot become a dyn are refused against the first"
(List.exists (contains d.Loc.dmsg "expected P, found the integer literal 2"
&& List.exists
(fun (n : Loc.note) -> (fun (n : Loc.note) ->
contains n.Loc.nmsg "this array's first element is u8") contains n.Loc.nmsg "this array's first element is P")
d.Loc.notes)); d.Loc.notes));
(match checked "(defn g [x $t] i32 (let [a [x 1]] 0))" with
| _ -> check "a type variable beside a literal is refused" false
| exception Loc.Error d ->
check "a type variable beside a literal names the bound and spells $t"
(contains d.Loc.dmsg "{:where (numeric? $t)}"
&& List.exists
(fun (n : Loc.note) ->
contains n.Loc.nmsg "this array's first element is $t")
d.Loc.notes));
rejects_check "numbers with no common type are refused and the fix named"
"(defn f [x i32 y f32] i32 (let [a [x y]] 0))"
~needle:"elements are i32 and f32, and neither holds every value of the \
other — convert one, as in (f32 x)";
accepts "the conversion that refusal names compiles"
"(defn f [x i32 y f32] i32 (let [a [(f32 x) y]] 0))";
rejects_check "two integer types with no common type are refused"
"(defn f [x i64 y u64] i32 (let [a [x y]] 0))" ~needle:"as in (i64 y)";
accepts "the integer conversion that refusal names compiles"
"(defn f [x i64 y u64] i32 (let [a [x (i64 y)]] 0))";
rejects_check "every element needing a type names the first's refusal"
"(defn main [] i32 (let [a [None None]] 0))"
~needle:"what None is an Option of";
(* ── (max-value T) and (min-value T) ──────────────────────────────── *)
infers "max-value carries its type" "(max-value u16)" "u16";
infers "min-value at a float" "(min-value f32)" "f32";
(match checked "(defn f [x $t] $t (max-value $t))" with
| _ -> check "max-value at an unbounded type variable is refused" false
| exception Loc.Error d ->
check "max-value at an unbounded type variable names the bound and only it"
(contains d.Loc.dmsg "write {:where (numeric? $t)}"
&& not (contains d.Loc.dmsg "Fn")));
infers "two literal if arms meet at the wider" "(if true 1 2.5)" "f64";
infers "two integer if arms stay i32" "(if true 1 2)" "i32";
infers "two literal match arms meet at the wider"
"(match (Some 1) (Some v) 1 None 2.5)" "f64";
accepts "max-value at a type variable the bound admits"
"(defn f [x $t] $t {:where (integer? $t)} (max-value t))";
rejects_check "max-value at a type that is not a number names the bound"
"(defn f [] string (max-value string))"
~needle:"max-value takes a numeric? type, and string is not one";
rejects_check "max-value of a value says it takes a type"
"(defn f [x i32] i32 (max-value x))" ~needle:"max-value takes a type";
accepts "max-of of a slice is the prelude's reduction"
"(defn f [xs [i32]] (Option i32) (max-of xs))";
rejects_check "max-of of a type names max-value"
"(defn f [] u8 (max-of u8))" ~needle:"the largest value of a type is (max-value u8)";
(* ── (the T e) ─────────────────────────────────────────────────── *)
infers "the gives a literal its type" "(the u8 200)" "u8";
infers "the widens as an annotation does" "(the i64 (the i32 1))" "i64";
rejects_check "the does not narrow"
"(defn f [x i64] i32 (the i32 x))" ~needle:"expected i32, found i64";
rejects_check "the refuses a dyn and names the cast"
"(defn f [x dyn] i32 (the i32 x))" ~needle:"write (i32 x) to convert it";
accepts "the cast that refusal names compiles" "(defn f [x dyn] i32 (i32 x))";
rejects_check "the refuses a dyn at a type a dyn does not become"
"(defn f [x dyn] string (the string x))"
~needle:"dyn — string does not cross into a written type yet";
rejects_check "the refuses a dyn at bool, which a dyn becomes where passed"
"(defn f [x dyn] bool (the bool x))"
~needle:"a dyn becomes a bool where a bool is passed";
accepts "the bool a dyn becomes where it is returned" "(defn f [x dyn] bool x)";
accepts "the at an Option takes nil" "(defn f [] (Option i32) (the (Option i32) nil))";
parse_rejects "the takes a type and a value" "(defn f [] i32 (the i32))"
~needle:"the is (the TYPE value)";
(* The refusals of a form with no type of its own name the as a way out, and
the spellings they name compile. *)
rejects_check "an empty array literal names the"
"(defn main [] i32 (let [a []] 0))" ~needle:"(the [0 i32] [])";
accepts "the empty array that refusal names compiles"
"(defn main [] i32 (let [a (the [0 i32] [])] (length a)))";
accepts "the None that refusal names compiles"
"(defn main [] i32 (let [a (the (Option i32) None)] 0))";
accepts "the zeroed that refusal names compiles"
"(defn main [] i32 (let [a (the [4 i32] (zeroed))] (at a 0)))";
accepts "the fills that refusal names compile"
"(defn main [] i32 (let [a (the [4 u32] (filled 0xFF)) \
b (the [4 u32] (dead-beef))] 0))";
(* ── A wide literal's follow-ups ──────────────────────────────── *) (* ── A wide literal's follow-ups ──────────────────────────────── *)
parse_rejects "a wide enum member is refused for its range" parse_rejects "a wide enum member is refused for its range"
@ -6541,10 +6732,7 @@ let () =
"(defmacro idm [x] x) \ "(defmacro idm [x] x) \
(defn f [] u64 (idm 18446744073709551615))"; (defn f [] u64 (idm 18446744073709551615))";
rejects_check "a wide element after a narrow first names the u64 array" accepts "a u64 array with a cast first element"
"(defn main [] i32 (let [a [1 18446744073709551615]] 0))"
~needle:"write the first element as (u64 1) for an array of u64";
accepts "the u64 array that refusal names compiles"
"(defn main [] i32 (let [a [(u64 1) 18446744073709551615]] 0))"; "(defn main [] i32 (let [a [(u64 1) 18446744073709551615]] 0))";
(* ── Suggestions that compile ─────────────────────────────────── *) (* ── Suggestions that compile ─────────────────────────────────── *)

View File

@ -343,7 +343,7 @@ let dyn_sweep () =
the old block, or a [len] that outlived the block it described, the old block, or a [len] that outlived the block it described,
is a use-after-free here and nothing anywhere else. *) is a use-after-free here and nothing anywhere else. *)
[ "ops"; "gc"; "unrooted"; "desc"; "nested"; "sharing"; "park"; [ "ops"; "gc"; "unrooted"; "desc"; "nested"; "sharing"; "park";
"classes" ]; "classes"; "hook" ];
(try Sys.remove exe with Sys_error _ -> ()) (try Sys.remove exe with Sys_error _ -> ())
(* A third sweep, over a handful of the same programs built [--dev]. (* A third sweep, over a handful of the same programs built [--dev].

View File

@ -1591,6 +1591,39 @@ let () =
(String.concat " " c.Session.fns) (String.concat " " c.Session.fns)
| exception Loc.Error { Loc.dmsg = m; _ } -> | exception Loc.Error { Loc.dmsg = m; _ } ->
fail "a class and its caller evaluated together: %s" m); fail "a class and its caller evaluated together: %s" m);
(* A method of update-instance-for-redefined-class, from the session. The
generic is written by [Classes.expand], not by the program, so this is
the case where the declaration being extended is nowhere in the
session's own list — and the method still has to install the generic's
dispatch, and the module has to hand the runtime the body it just
installed, or migrations go on calling the old one. *)
(let t, _ = Session.create ~file:"programs/dev-class.flan" () in
match
Session.eval t
"(defmethod update-instance-for-redefined-class point \
[p added discarded] nil)"
with
| c ->
if not (List.mem Classes.migrate_generic c.Session.fns) then
fail "a migration method installed %s" (String.concat " " c.Session.fns);
if not (has c.Session.ir "call void @flan_dyn_class_hook") then
fail "a migration method did not re-register the hook"
| exception Loc.Error { Loc.dmsg = m; _ } ->
fail "a migration method was refused: %s" m);
(* A slot's type changed and nothing else. Every constructor parameter is
dyn whatever the slot says, so the signature is the one it was and a
compiled caller is no reason to refuse — the type is checked where a
value is stored, at run time. What has to reach the program is the new
definition, and the registration carries it with the type after the
name, which is what makes the runtime see a change and migrate. *)
(let t, _ = Session.create ~file:"programs/dev-class.flan" () in
ignore (Session.eval t "(defn origin [] dyn (point 0 0))");
match Session.eval t "(defclass point [x i64 y])" with
| c ->
if not (has c.Session.ir "c\"x i64\\0Ay\"") then
fail "a slot's new type did not reach the registration"
| exception Loc.Error { Loc.dmsg = m; _ } ->
fail "a slot's type changed under a compiled caller was refused: %s" m);
(* ── What a slot is shown as ────────────────────────────────────────── (* ── What a slot is shown as ──────────────────────────────────────────
[strip_rebind] takes only a trailing ~N — [~] is the reader's delimiter [strip_rebind] takes only a trailing ~N — [~] is the reader's delimiter

View File

@ -337,6 +337,8 @@ extern int64_t flan_break_site_len;
* shadow-stack frame's shape does: the struct is declared in one file. */ * shadow-stack frame's shape does: the struct is declared in one file. */
extern void *flan_restart_push_c(const uint8_t *name, int64_t namelen); extern void *flan_restart_push_c(const uint8_t *name, int64_t namelen);
extern void flan_restart_pop_c(void *frame); extern void flan_restart_pop_c(void *frame);
extern int (*flan_dyn_migrate_hook)(void *fn, uint64_t instance,
uint64_t added, uint64_t discarded);
/* -- How far down a transfer can actually land ----------------------- */ /* -- How far down a transfer can actually land ----------------------- */
@ -425,6 +427,44 @@ extern void flan_dev_frames_restore(void *head) __attribute__((weak));
extern int64_t flan_dyn_root_mark(void) __attribute__((weak)); extern int64_t flan_dyn_root_mark(void) __attribute__((weak));
extern void flan_dyn_root_restore(int64_t n) __attribute__((weak)); extern void flan_dyn_root_restore(int64_t n) __attribute__((weak));
/* -- update-instance-for-redefined-class ------------------------------ */
/* A class migration calls the method from inside [get], [put] or [set] —
* a C frame with no transfer channel of its own — so the call is made the
* way a thunk's is: behind a floor, with a channel of its own and a restart
* of its own above the floor. A break inside the method then offers
* [migrate-by-name] and nothing below the call, which it could not reach.
* Taking it leaves the instance as name-matching made it; flan_dyn.c's
* [class_hook] puts that back.
*
* [eval_boundary] is cleared for the call: an evaluation in progress
* underneath is below this floor, and offering to abandon it would be a
* choice nothing can carry out. Everything saved is restored, so a
* migration inside a thunk inside a break nests like the rest. */
static const uint8_t migrate_name[] = "migrate-by-name";
typedef uint64_t (*migrate_fn_t)(uint64_t, uint64_t, uint64_t, void *);
static int migrate_call(void *fn, uint64_t instance, uint64_t added,
uint64_t discarded) {
int32_t outer = restart_floor;
int32_t oframe = frame_floor;
void *obound = eval_boundary;
void *xfer = NULL;
void *mine;
restart_floor = flan_restart_count();
frame_floor = flan_dev_frame_count();
eval_boundary = NULL;
mine = flan_restart_push_c(migrate_name, sizeof migrate_name - 1);
((migrate_fn_t)fn)(instance, added, discarded, &xfer);
flan_restart_pop_c(mine);
eval_boundary = obound;
restart_floor = outer;
frame_floor = oframe;
if (xfer == NULL) return 0;
return (mine != NULL && xfer == mine) ? 1 : 2;
}
/* The three of them, dropped between two runs of [main]. The counterpart of /* The three of them, dropped between two runs of [main]. The counterpart of
* flan_rt.c's [flan_condition_stacks_reset] and flan_dev.c's * flan_rt.c's [flan_condition_stacks_reset] and flan_dev.c's
* [flan_dev_frames_reset], called from the same one place and for the same * [flan_dev_frames_reset], called from the same one place and for the same
@ -904,6 +944,9 @@ static void break_loop_at(const uint8_t *name, int64_t namelen, void *condition,
: !s->resumable ? " (cannot be taken from this trap)" : !s->resumable ? " (cannot be taken from this trap)"
: i == s->boundary : i == s->boundary
? " (stop running the expression; the program carries on)" ? " (stop running the expression; the program carries on)"
: strcmp(s->names + s->off[i], (const char *)migrate_name) == 0
&& s->reachable[i]
? " (keep the instance as its slots matched by name)"
: s->reachable[i] ? "" : s->reachable[i] ? ""
: " (below this break; cannot be taken)"); : " (below this break; cannot be taken)");
if (s->total > s->n) if (s->total > s->n)
@ -2200,6 +2243,7 @@ static int32_t start_on(const char *path) {
* to do, and stopping forever is worse than the abort it replaces. */ * to do, and stopping forever is worse than the abort it replaces. */
flan_break_hook = break_loop; flan_break_hook = break_loop;
flan_trap_hook = trap_stop; flan_trap_hook = trap_stop;
flan_dyn_migrate_hook = migrate_call;
return 0; return 0;
failed: failed:

View File

@ -1,5 +1,5 @@
;; A class is a named dyn map with a shape tag. Its slots are names and ;; A class is a named dyn map with a shape tag. A slot with no type holds
;; carry no types, and its constructor is the class's own name, positional. ;; any value, and its constructor is the class's own name, positional.
(defclass point [x y]) (defclass point [x y])
(defclass circle [r]) (defclass circle [r])

View File

@ -710,10 +710,17 @@ unit carries nothing for a dyn word to hold, and boxing it is refused — and
<h3>Classes and generic functions</h3> <h3>Classes and generic functions</h3>
<p>A class is a named dyn map with a shape tag. <code>defclass</code> names its <p>A class is a named dyn map with a shape tag. <code>defclass</code> names its
slots, which carry no types; the constructor is the class's own name and is slots, and a slot may be followed by a type, the way a parameter is:
positional; and <code>class-of</code> answers the tag, or <code>nil</code> for <code>[x y]</code> is two slots that hold any value, and <code>[pause bool]</code>
anything that is not an instance. The slots are map keys, so nothing was added to is one that holds only a bool. The type is checked whenever a value is stored,
read or write one.</p> and a slot may be <code>bool</code>, an integer type, <code>f32</code>,
<code>f64</code>, <code>string</code>, a class, or <code>(Option T)</code> of one of
those, which also admits <code>nil</code>. The constructor is the class's own name
and is positional, and <code>class-of</code> answers the tag, or <code>nil</code>
for anything that is not an instance. The slots are map keys: <code>get</code>
reads one, and <code>set</code> writes one, as in
<code>(set (get s :pause) true)</code>. <code>put</code> writes one too, and is
also how a key the class does not declare is added.</p>
<p>Dispatch comes in the two styles and they are one mechanism. <p>Dispatch comes in the two styles and they are one mechanism.
<code>defgeneric</code> dispatches on the class of the first argument, which is <code>defgeneric</code> 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 once, for every method; a method has no return slot; and every parameter of both is
<code>dyn</code>, written or not.</p> <code>dyn</code>, written or not.</p>
<pre><code>;; A class is a named dyn map with a shape tag. Its slots are names and <pre><code>;; A class is a named dyn map with a shape tag. A slot with no type holds
;; carry no types, and its constructor is the class's own name, positional. ;; any value, and its constructor is the class's own name, positional.
(defclass point [x y]) (defclass point [x y])
(defclass circle [r]) (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 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 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 migrates at its next touch. Slots matched by name keep their values, a gained slot
appears as <code>nil</code>, a dropped one goes, the object is the same object, and starts at its type's zero value — <code>false</code>, <code>0</code>,
<code>0.0</code> or <code>""</code>, and <code>nil</code> for a slot with no type,
an <code>(Option T)</code> or a class — a dropped one goes, the object is the same object, and
<code>class-of</code> still answers the same tag, so every method still reaches it. <code>class-of</code> 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.</p> That is CLHS 4.3.6's protocol. Its user hook is
<code>update-instance-for-redefined-class</code>: 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 <code>migrate-by-name</code>
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.</p>
<p><kbd>C-c C-x</kbd> rebuilds, relaunches and reconnects, and is the way out while the <p><kbd>C-c C-x</kbd> 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 above is true. It costs the program's state, which is why it is a key you press rather than