Merge master
This commit is contained in:
commit
5a78cdd202
100
TODO.org
100
TODO.org
@ -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.
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
24
lib/ast.ml
24
lib/ast.ml
@ -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,
|
||||||
|
|||||||
16
lib/build.ml
16
lib/build.ml
@ -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.
|
||||||
|
|||||||
978
lib/check.ml
978
lib/check.ml
File diff suppressed because it is too large
Load Diff
120
lib/classes.ml
120
lib/classes.ml
@ -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. *)
|
||||||
|
|||||||
37
lib/dev.ml
37
lib/dev.ml
@ -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 —
|
||||||
|
|||||||
13
lib/emit.ml
13
lib/emit.ml
@ -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)
|
||||||
|
|||||||
67
lib/load.ml
67
lib/load.ml
@ -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
|
||||||
|
|||||||
38
lib/parse.ml
38
lib/parse.ml
@ -489,6 +489,15 @@ and form f mk (head : Form.t) (args : Form.t list) : Ast.expr =
|
|||||||
array of integers — the wrong reading, and a silent one. Read here, the
|
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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
22
lib/x86.ml
22
lib/x86.ml
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
2
test/daemon-x86.out
Normal 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)
|
||||||
117
test/dyn_ops.c
117
test/dyn_ops.c
@ -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;
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
59
test/programs/array-mixed.flan
Normal file
59
test/programs/array-mixed.flan
Normal 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))
|
||||||
21
test/programs/dev-hook.flan
Normal file
21
test/programs/dev-hook.flan
Normal 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)
|
||||||
8
test/programs/dyn-class-pkg.flan
Normal file
8
test/programs/dyn-class-pkg.flan
Normal 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)
|
||||||
55
test/programs/dyn-class-slots.flan
Normal file
55
test/programs/dyn-class-slots.flan
Normal 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)
|
||||||
21
test/programs/dyn-slot-trap.flan
Normal file
21
test/programs/dyn-slot-trap.flan
Normal 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)
|
||||||
@ -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)
|
||||||
|
|||||||
18
test/programs/literal-arm.flan
Normal file
18
test/programs/literal-arm.flan
Normal 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)
|
||||||
54
test/programs/max-value.flan
Normal file
54
test/programs/max-value.flan
Normal 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
25
test/programs/negate.flan
Normal 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)
|
||||||
5
test/programs/pkgs/geo/geo.flan
Normal file
5
test/programs/pkgs/geo/geo.flan
Normal 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))
|
||||||
13
test/programs/shadow-prelude.flan
Normal file
13
test/programs/shadow-prelude.flan
Normal 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)
|
||||||
@ -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
|
||||||
|
|||||||
162
test/test_dev.ml
162
test/test_dev.ml
@ -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" ()
|
||||||
|
|||||||
@ -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");
|
||||||
|
|||||||
@ -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 ─────────────────────────────────── *)
|
||||||
|
|||||||
@ -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].
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
44
vendor/agent/flan_agent.c
vendored
44
vendor/agent/flan_agent.c
vendored
@ -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:
|
||||||
|
|||||||
@ -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])
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user