C-c C-c on a generic installs its copies, and a refusal about one says where it came from
A generic defn produces no Tast.fn, so the editor was told nothing had been installed and nothing had gone wrong. eval now expands a redefined generic name to its copies, and picks up any copy the running process was never built with - which is how a redefined caller reaching a generic at a new element type gets that copy built and loaded. C-x C-e is the path that could really go stale, and did: it checks against the live environment, so an expression naming a generic at an unused type generated a copy that existed in no program and the thunk called a symbol nothing defined. Marked and spliced. There was no cache to invalidate. program_with_env builds a fresh env every evaluation, so the instantiation cache cannot survive one; the test pins that rather than inventing machinery for it. A signature change reaches the session as a refusal about put!-i32, a name the source does not contain. It now says which generic it is a copy of, at which types, and that every copy changed together.
This commit is contained in:
parent
7f86f32699
commit
b438a71031
48
lib/check.ml
48
lib/check.ml
@ -5540,6 +5540,54 @@ let program (decls : Ast.decl list) : Tast.program =
|
|||||||
let program_all (decls : Ast.decl list) : Tast.program =
|
let program_all (decls : Ast.decl list) : Tast.program =
|
||||||
fst (build_program ~keep_going:true decls)
|
fst (build_program ~keep_going:true decls)
|
||||||
|
|
||||||
|
(* ── What a session needs to know about instantiations ──────────────────
|
||||||
|
A generic [defn] never reaches [Tast.fns] — only its copies do — so the
|
||||||
|
editor's [C-c C-c], which installs the bodies named by the form it was
|
||||||
|
sent, would install nothing at all for a generic. These are what
|
||||||
|
[Session.eval] expands the name with. They are here rather than there
|
||||||
|
because [env]'s tables are the only record that a symbol was ever generic:
|
||||||
|
past this module an instantiation is an ordinary function and nothing knows
|
||||||
|
it was written once. *)
|
||||||
|
|
||||||
|
(* Is this name a generic definition rather than an ordinary one? *)
|
||||||
|
let is_generic env n = Hashtbl.mem env.gsigs n
|
||||||
|
|
||||||
|
(* Every copy of [gname] this check produced, by symbol. Transitivity needs no
|
||||||
|
walk: a whole-program check has already generated every copy every call
|
||||||
|
site asked for, including the ones a generic pulled in by calling another
|
||||||
|
generic at its own variable. *)
|
||||||
|
let instantiations env gname =
|
||||||
|
match Hashtbl.find_opt env.insts gname with
|
||||||
|
| None -> []
|
||||||
|
| Some l -> List.rev_map (fun (_, _, sym) -> sym) !l
|
||||||
|
|
||||||
|
(* The generic a symbol came from, and the types it was asked for — [None] for
|
||||||
|
an ordinary function. What a refusal about [sort!-i32] needs in order to
|
||||||
|
say which line the programmer should look at, since [sort!-i32] appears
|
||||||
|
nowhere in the source. *)
|
||||||
|
let instantiation_origin env sym =
|
||||||
|
Hashtbl.fold
|
||||||
|
(fun gname l acc ->
|
||||||
|
match acc with
|
||||||
|
| Some _ -> acc
|
||||||
|
| None ->
|
||||||
|
(match List.find_opt (fun (_, _, s) -> String.equal s sym) !l with
|
||||||
|
| Some (ps, _, _) -> Some (gname, ps)
|
||||||
|
| None -> None))
|
||||||
|
env.insts None
|
||||||
|
|
||||||
|
(* Checking one expression against a live session can *generate* a copy: the
|
||||||
|
first [C-x C-e] of [(id 3)] instantiates [id] at [i32] and the copy is in
|
||||||
|
[env.instances] and in no program anywhere. Without these two the module
|
||||||
|
that gets built calls a symbol it never defined. A mark before and the
|
||||||
|
difference after is the whole protocol. *)
|
||||||
|
let instance_mark env = List.length env.instances
|
||||||
|
|
||||||
|
let instances_since env mark =
|
||||||
|
let fresh = List.length env.instances - mark in
|
||||||
|
List.rev
|
||||||
|
(List.filteri (fun i _ -> i < fresh) env.instances)
|
||||||
|
|
||||||
(* One expression, checked against a program that is already running. The
|
(* One expression, checked against a program that is already running. The
|
||||||
frame is empty — a REPL expression has no parameters and no enclosing
|
frame is empty — a REPL expression has no parameters and no enclosing
|
||||||
function — so the slots it needs are whatever its own [let]s allocate. *)
|
function — so the slots it needs are whatever its own [let]s allocate. *)
|
||||||
|
|||||||
102
lib/session.ml
102
lib/session.ml
@ -128,7 +128,8 @@ let known t n =
|
|||||||
(* Everything here is a change that would load cleanly and then be wrong. The
|
(* Everything here is a change that would load cleanly and then be wrong. The
|
||||||
house rule (NEXT.md, Watch for) says recognise it and refuse with the
|
house rule (NEXT.md, Watch for) says recognise it and refuse with the
|
||||||
reason, so each one names what it would have broken. *)
|
reason, so each one names what it would have broken. *)
|
||||||
let compatible ~loc (old_ : Tast.program) (new_ : Tast.program) =
|
let compatible ?(origin = fun _ -> None) ~loc (old_ : Tast.program)
|
||||||
|
(new_ : Tast.program) =
|
||||||
let find_fn p n =
|
let find_fn p n =
|
||||||
List.find_opt (fun (f : Tast.fn) -> String.equal f.Tast.name n) p.Tast.fns
|
List.find_opt (fun (f : Tast.fn) -> String.equal f.Tast.name n) p.Tast.fns
|
||||||
in
|
in
|
||||||
@ -154,15 +155,46 @@ let compatible ~loc (old_ : Tast.program) (new_ : Tast.program) =
|
|||||||
until they do, rather than becoming a silent mismatch. See
|
until they do, rather than becoming a silent mismatch. See
|
||||||
plan.org, Hot reload, and open decision #6. *)
|
plan.org, Hot reload, and open decision #6. *)
|
||||||
if not same then
|
if not same then
|
||||||
|
(* ── When the name is not one the programmer wrote ──────────────
|
||||||
|
A generic's instantiations are named [sort!-i32], [sort!-f32]
|
||||||
|
and so on, and the mangling carries only the *type variables*
|
||||||
|
— so editing the generic's other parameters changes every copy's
|
||||||
|
signature at once, under the same names. The refusal then
|
||||||
|
arrives about [sort!-i32], which appears nowhere in the file
|
||||||
|
being edited, for a reason invisible at the edited line.
|
||||||
|
|
||||||
|
So the refusal says where the name came from: which generic, at
|
||||||
|
which types, and that every copy changed together. The
|
||||||
|
programmer's next move is a restart either way — the point is
|
||||||
|
that they can tell *why* without going looking for a function
|
||||||
|
that does not exist in the source.
|
||||||
|
|
||||||
|
Note what does *not* come through here: adding or removing a
|
||||||
|
[where] clause changes no signature at all. It changes which
|
||||||
|
call sites are legal, and those refusals land at the call sites,
|
||||||
|
in the checker, before this is ever reached. *)
|
||||||
|
let what, note =
|
||||||
|
match origin f.Tast.name with
|
||||||
|
| None -> f.Tast.name, ""
|
||||||
|
| Some (gname, tys) ->
|
||||||
|
( Printf.sprintf "%s, the copy of the generic %s at %s"
|
||||||
|
f.Tast.name gname
|
||||||
|
(String.concat ", " (List.map Types.to_string tys)),
|
||||||
|
Printf.sprintf
|
||||||
|
" Editing %s changed every copy of it at once, so this \
|
||||||
|
refusal is about a function the source does not name."
|
||||||
|
gname )
|
||||||
|
in
|
||||||
fail loc
|
fail loc
|
||||||
"%s changes signature, from (Fn [%s] %s) to (Fn [%s] %s); \
|
"%s changes signature, from (Fn [%s] %s) to (Fn [%s] %s); \
|
||||||
the calls already compiled into the running program pass the old \
|
the calls already compiled into the running program pass the old \
|
||||||
one. Restart to change it."
|
one.%s Restart to change it."
|
||||||
f.Tast.name
|
what
|
||||||
(String.concat " " (List.map Types.to_string g.Tast.params))
|
(String.concat " " (List.map Types.to_string g.Tast.params))
|
||||||
(Types.to_string g.Tast.ret)
|
(Types.to_string g.Tast.ret)
|
||||||
(String.concat " " (List.map Types.to_string f.Tast.params))
|
(String.concat " " (List.map Types.to_string f.Tast.params))
|
||||||
(Types.to_string f.Tast.ret))
|
(Types.to_string f.Tast.ret)
|
||||||
|
note)
|
||||||
new_.Tast.fns;
|
new_.Tast.fns;
|
||||||
List.iter
|
List.iter
|
||||||
(fun (g : Tast.global) ->
|
(fun (g : Tast.global) ->
|
||||||
@ -354,9 +386,29 @@ let eval ?(origin = "<eval>") ?pause t src : change =
|
|||||||
(* Nothing above this line has changed the session. A [Loc.Error] from here
|
(* Nothing above this line has changed the session. A [Loc.Error] from here
|
||||||
leaves it exactly as it was. *)
|
leaves it exactly as it was. *)
|
||||||
let program, env = Check.program_with_env decls in
|
let program, env = Check.program_with_env decls in
|
||||||
compatible ~loc t.program program;
|
compatible ~origin:(Check.instantiation_origin env) ~loc t.program program;
|
||||||
compatible_enums ~loc t.decls decls;
|
compatible_enums ~loc t.decls decls;
|
||||||
let fns =
|
(* ── The bodies to install ────────────────────────────────────────────
|
||||||
|
The names the form declared that have a body in the checked program —
|
||||||
|
and, for a generic, the bodies its *copies* have, because a generic
|
||||||
|
[defn] never reaches [Tast.fns] at all. Without the second clause
|
||||||
|
[C-c C-c] on a generic reports [installs=false, fns=[]]: it installs
|
||||||
|
nothing and says nothing went wrong, which is the feature being unusable
|
||||||
|
in the loop the project exists for.
|
||||||
|
|
||||||
|
Transitivity is free. The check above was a whole-program check, so
|
||||||
|
[env.insts] already holds every copy every call site asked for, including
|
||||||
|
the ones a redefined generic pulled in by calling another generic at its
|
||||||
|
own variable.
|
||||||
|
|
||||||
|
The third clause is the one that makes a redefinition reach a type the
|
||||||
|
process was never built with. Redefining a *caller* so that it uses a
|
||||||
|
generic at a new element type generates a brand-new symbol the host has
|
||||||
|
never had — it is not [known t] and no name in [names] mentions it — so
|
||||||
|
it has to be found by being an instantiation that the running process
|
||||||
|
lacks. [Emit.redefinition] then writes it as a new by-name cell, which is
|
||||||
|
the same path a [defn] the process was never built with already takes. *)
|
||||||
|
let declared_fns =
|
||||||
List.filter
|
List.filter
|
||||||
(fun n ->
|
(fun n ->
|
||||||
List.exists
|
List.exists
|
||||||
@ -364,6 +416,26 @@ let eval ?(origin = "<eval>") ?pause t src : change =
|
|||||||
program.Tast.fns)
|
program.Tast.fns)
|
||||||
names
|
names
|
||||||
in
|
in
|
||||||
|
let from_generics =
|
||||||
|
List.concat_map
|
||||||
|
(fun n ->
|
||||||
|
if Check.is_generic env n then Check.instantiations env n else [])
|
||||||
|
names
|
||||||
|
in
|
||||||
|
let new_instances =
|
||||||
|
List.filter_map
|
||||||
|
(fun (f : Tast.fn) ->
|
||||||
|
if known t f.Tast.name then None
|
||||||
|
else
|
||||||
|
match Check.instantiation_origin env f.Tast.name with
|
||||||
|
| Some _ -> Some f.Tast.name
|
||||||
|
| None -> None)
|
||||||
|
program.Tast.fns
|
||||||
|
in
|
||||||
|
let fns =
|
||||||
|
List.sort_uniq String.compare
|
||||||
|
(declared_fns @ from_generics @ new_instances)
|
||||||
|
in
|
||||||
(* A constant that changed and can be published: known to the host, not
|
(* A constant that changed and can be published: known to the host, not
|
||||||
consumed by the checker. The module stores its new value at the frame
|
consumed by the checker. The module stores its new value at the frame
|
||||||
boundary, exactly as it stores a new function body. *)
|
boundary, exactly as it stores a new function body. *)
|
||||||
@ -1010,7 +1082,15 @@ let eval_expr ?(origin = "<eval>") ?(pause = false) t src : change =
|
|||||||
Ast.loc = parsed.Ast.loc }
|
Ast.loc = parsed.Ast.loc }
|
||||||
else parsed
|
else parsed
|
||||||
in
|
in
|
||||||
|
(* Checking against the live environment can *generate* code: the first
|
||||||
|
[C-x C-e] of a call to a generic at a type nothing has used yet
|
||||||
|
instantiates it here, and the copy lands in [t.env] and in no program
|
||||||
|
anywhere. Marked before and collected after, and spliced into the module
|
||||||
|
below — without this the thunk calls a symbol the module never defines
|
||||||
|
and the host has no cell for. *)
|
||||||
|
let mark = Check.instance_mark t.env in
|
||||||
let checked, base, bnames = Check.expression t.env parsed in
|
let checked, base, bnames = Check.expression t.env parsed in
|
||||||
|
let fresh = Check.instances_since t.env mark in
|
||||||
(* The thunk's frame starts at whatever [Check.expression] needed and grows
|
(* The thunk's frame starts at whatever [Check.expression] needed and grows
|
||||||
as the walk finds slices in it, so the slots the renderer asks for are
|
as the walk finds slices in it, so the slots the renderer asks for are
|
||||||
appended past [base] and collected here to size the frame below. *)
|
appended past [base] and collected here to size the frame below. *)
|
||||||
@ -1047,15 +1127,21 @@ let eval_expr ?(origin = "<eval>") ?(pause = false) t src : change =
|
|||||||
for every expression ever typed. *)
|
for every expression ever typed. *)
|
||||||
let program =
|
let program =
|
||||||
{ t.program with
|
{ t.program with
|
||||||
Tast.fns = t.program.Tast.fns @ [ thunk ];
|
Tast.fns = t.program.Tast.fns @ fresh @ [ thunk ];
|
||||||
externs = t.program.Tast.externs @ externs }
|
externs = t.program.Tast.externs @ externs }
|
||||||
in
|
in
|
||||||
|
(* The copies stay in the session's program, unlike the thunk: the thunk is
|
||||||
|
not a declaration and there is nothing to keep, but a copy that has been
|
||||||
|
built and loaded *is* part of the running process from here on, and
|
||||||
|
forgetting it would generate a second one under the same name at the next
|
||||||
|
evaluation. *)
|
||||||
|
t.program <- { t.program with Tast.fns = t.program.Tast.fns @ fresh };
|
||||||
let ir =
|
let ir =
|
||||||
(* The thunk gets debug info on the same flag as everything else. It is a
|
(* The thunk gets debug info on the same flag as everything else. It is a
|
||||||
function nobody sets a breakpoint on by name, but it is a frame on the
|
function nobody sets a breakpoint on by name, but it is a frame on the
|
||||||
stack when the expression signals, and a frame the debugger cannot name
|
stack when the expression signals, and a frame the debugger cannot name
|
||||||
is the thing the conditions buffer is trying to stop showing. *)
|
is the thing the conditions buffer is trying to stop showing. *)
|
||||||
Emit.redefinition ~dev:true ~debug:t.debug ~known:(known t) ~call:name
|
Emit.redefinition ~dev:true ~debug:t.debug ~known:(known t) ~call:name
|
||||||
program ~fns:[ name ]
|
program ~fns:(List.map (fun (f : Tast.fn) -> f.Tast.name) fresh @ [ name ])
|
||||||
in
|
in
|
||||||
{ ir; names = []; fns = []; installs = true }
|
{ ir; names = []; fns = []; installs = true }
|
||||||
|
|||||||
39
test/programs/reload-generic.flan
Normal file
39
test/programs/reload-generic.flan
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
;;;; The session's fixture for generics in the dev loop.
|
||||||
|
;;;;
|
||||||
|
;;;; A generic [defn] never reaches [Tast.fns] — only its copies do — so every
|
||||||
|
;;;; question the editor asks about one has to be answered by expanding the
|
||||||
|
;;;; name. This file is the smallest program that makes each of those
|
||||||
|
;;;; questions concrete: one generic used at two element types, one generic
|
||||||
|
;;;; that calls another so that instantiation has to be transitive, and one
|
||||||
|
;;;; call site whose element type is *not* used anywhere else, so that a
|
||||||
|
;;;; redefinition can reach a copy the process was never built with.
|
||||||
|
|
||||||
|
(defvar counter i64)
|
||||||
|
|
||||||
|
(defn put! [xs [$t] i i32 v $t] ()
|
||||||
|
{:where (copyable? $t)}
|
||||||
|
(set (at xs i) v))
|
||||||
|
|
||||||
|
;;; Calls [put!] at its own variable, so the copy of [put!] is generated when
|
||||||
|
;;; [hold!] is instantiated and not before.
|
||||||
|
(defn hold! [xs [$t] v $t] ()
|
||||||
|
{:where (copyable? $t)}
|
||||||
|
(put! xs 0 v))
|
||||||
|
|
||||||
|
(defn pick [xs [$t]] $t
|
||||||
|
{:where (ordered? $t)}
|
||||||
|
(let [m (at xs 0)]
|
||||||
|
(dotimes [i (len xs)]
|
||||||
|
(set m (min m (at xs i))))
|
||||||
|
m))
|
||||||
|
|
||||||
|
(defn step [] ()
|
||||||
|
(let [ns [5 3 9 1]
|
||||||
|
fs [2.5 0.5 1.5]]
|
||||||
|
(hold! (slice ns 0 4) 7)
|
||||||
|
(hold! (slice fs 0 3) 0.25)
|
||||||
|
(set counter (+ counter (i64 (pick (slice ns 0 4)))))))
|
||||||
|
|
||||||
|
(defn main [] ()
|
||||||
|
(step)
|
||||||
|
(println counter))
|
||||||
@ -265,6 +265,147 @@ let () =
|
|||||||
if has str.Session.ir "@flan_reload_transient" then
|
if has str.Session.ir "@flan_reload_transient" then
|
||||||
fail "an expression holding a string claimed to be unloadable";
|
fail "an expression holding a string claimed to be unloadable";
|
||||||
|
|
||||||
|
(* ── Generics in the dev loop ─────────────────────────────────────────
|
||||||
|
A generic [defn] produces no [Tast.fn] of its own — only its copies do —
|
||||||
|
so every one of these is a question the editor asks that the ordinary
|
||||||
|
name-to-body path cannot answer. *)
|
||||||
|
let gen () = fst (Session.create ~file:"programs/reload-generic.flan" ()) in
|
||||||
|
|
||||||
|
(* 1. [C-c C-c] on a generic used to report [installs=false, fns=[]]: it
|
||||||
|
installed nothing and did not say anything had gone wrong. Both copies
|
||||||
|
have to be named, and the copy of [put!] that [hold!] pulls in has to be
|
||||||
|
there too, which is transitivity. *)
|
||||||
|
(match Session.eval (gen ()) "(defn hold! [xs [$t] v $t] () {:where (copyable? $t)} (put! xs 0 v) (put! xs 0 v))" with
|
||||||
|
| c ->
|
||||||
|
if not c.Session.installs then
|
||||||
|
fail "redefining a generic installed nothing";
|
||||||
|
List.iter
|
||||||
|
(fun want ->
|
||||||
|
if not (List.mem want c.Session.fns) then
|
||||||
|
fail "redefining a generic did not install %s; it installed %s"
|
||||||
|
want (String.concat " " c.Session.fns))
|
||||||
|
[ "hold!-i32"; "hold!-f64" ];
|
||||||
|
(* And only its own copies: [put!] did not change, and its copies are
|
||||||
|
reached through their cells, so reinstalling them would be work with
|
||||||
|
no effect. *)
|
||||||
|
if List.mem "put!-i32" c.Session.fns then
|
||||||
|
fail "redefining a generic reinstalled an unchanged generic's copies"
|
||||||
|
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||||
|
fail "redefining a generic: %s" m);
|
||||||
|
|
||||||
|
(* The callee side of the same rule: redefining [put!] reinstalls the copies
|
||||||
|
of [put!], which exist only because [hold!] asked for them — the
|
||||||
|
instantiation that generated them was transitive, and finding them again
|
||||||
|
is one table lookup rather than a walk, because a whole-program check has
|
||||||
|
already regenerated all of them. *)
|
||||||
|
(match Session.eval (gen ()) "(defn put! [xs [$t] i i32 v $t] () {:where (copyable? $t)} (set (at xs i) v))" with
|
||||||
|
| c ->
|
||||||
|
List.iter
|
||||||
|
(fun want ->
|
||||||
|
if not (List.mem want c.Session.fns) then
|
||||||
|
fail "redefining a called generic did not install %s; it \
|
||||||
|
installed %s" want (String.concat " " c.Session.fns))
|
||||||
|
[ "put!-i32"; "put!-f64" ]
|
||||||
|
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||||
|
fail "redefining a generic: %s" m);
|
||||||
|
|
||||||
|
(* 2. Staleness, and the answer is that there is none to have. The
|
||||||
|
instantiation cache lives in the [Check.env] that [Check.program_with_env]
|
||||||
|
builds *fresh* on every evaluation, so a redefined generic's copies are
|
||||||
|
regenerated from the new body and there is no cached copy of the old one
|
||||||
|
anywhere to invalidate. Pinned here because the alternative — a cache that
|
||||||
|
survived between evaluations — would make [C-c C-c] appear to succeed
|
||||||
|
while the program kept running the old body, which is the quiet version
|
||||||
|
of failure (1). *)
|
||||||
|
(let t = gen () in
|
||||||
|
let c =
|
||||||
|
Session.eval t
|
||||||
|
"(defn pick [xs [$t]] $t {:where (ordered? $t)} (let [m (at xs 0)] \
|
||||||
|
(dotimes [i (len xs)] (set m (max m (at xs i)))) m))"
|
||||||
|
in
|
||||||
|
if not (List.mem "pick-i32" c.Session.fns) then
|
||||||
|
fail "redefining a generic did not reinstall pick-i32";
|
||||||
|
(* The new body is the one that got emitted, not a cached copy of the old:
|
||||||
|
[max] lowers to a [>] where [min] lowered to a [<]. *)
|
||||||
|
if not (has c.Session.ir "icmp sgt") then
|
||||||
|
fail "the reinstalled copy carried the old body";
|
||||||
|
(* And again, to show the second evaluation is not served from a cache the
|
||||||
|
first one left behind. *)
|
||||||
|
let c2 = Session.eval t "(defn pick [xs [$t]] $t {:where (ordered? $t)} (at xs 0))" in
|
||||||
|
if not (List.mem "pick-i32" c2.Session.fns) then
|
||||||
|
fail "a second redefinition of a generic installed nothing");
|
||||||
|
|
||||||
|
(* 3. A redefinition that needs a copy the process was never built with. The
|
||||||
|
fixture never calls [pick] at f64, so [pick-f64] exists in no program
|
||||||
|
anywhere; redefining the *caller* to ask for it has to build and install
|
||||||
|
it. Nothing in the form names [pick-f64] — it is found by being an
|
||||||
|
instantiation the host lacks. *)
|
||||||
|
(match
|
||||||
|
Session.eval (gen ())
|
||||||
|
"(defn step [] () (let [ns [5 3 9 1] fs [2.5 0.5 1.5]] \
|
||||||
|
(set counter (+ counter (i64 (pick (slice ns 0 4)))) ) \
|
||||||
|
(set counter (+ counter (i64 (pick (slice fs 0 3)))))))"
|
||||||
|
with
|
||||||
|
| c ->
|
||||||
|
if not (List.mem "pick-f64" c.Session.fns) then
|
||||||
|
fail "a redefinition needing a new instantiation did not install \
|
||||||
|
pick-f64; it installed %s" (String.concat " " c.Session.fns)
|
||||||
|
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||||
|
fail "a redefinition needing a new instantiation: %s" m);
|
||||||
|
|
||||||
|
(* 4. A signature change on a generic is refused, and the refusal is about a
|
||||||
|
name the source does not contain: the mangling carries only the type
|
||||||
|
variables, so every copy changes signature at once and under the same
|
||||||
|
name. It has to say where that name came from. *)
|
||||||
|
(* The change has to be one the *checker* accepts, which is the narrow case
|
||||||
|
and worth saying why. A generic whose arity or variable positions move is
|
||||||
|
refused at its call sites, in the checker, with the call site's own
|
||||||
|
location — a better error than this one and the reason this path is
|
||||||
|
reached less often than it looks. What reaches here is a change every
|
||||||
|
call site still accepts and every *copy* does not: widening the index
|
||||||
|
from i32 to i64 leaves [(put! xs 0 v)] checking, because the literal
|
||||||
|
adapts, and changes [put!-i32]'s signature underneath every compiled
|
||||||
|
caller. *)
|
||||||
|
(match
|
||||||
|
Session.eval (gen ())
|
||||||
|
"(defn put! [xs [$t] i i64 v $t] () {:where (copyable? $t)} \
|
||||||
|
(set (at xs (i32 i)) v))"
|
||||||
|
with
|
||||||
|
| _ -> fail "a generic's changed parameter type was accepted"
|
||||||
|
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||||
|
if not (has m "changes signature") then
|
||||||
|
fail "a generic's changed parameter type: %S" m;
|
||||||
|
if not (has m "the copy of the generic put!") then
|
||||||
|
fail "the refusal did not say the name came from put!: %S" m;
|
||||||
|
if not (has m "every copy of it at once") then
|
||||||
|
fail "the refusal did not say every copy changed together: %S" m);
|
||||||
|
|
||||||
|
(* And what is *not* refused, which the notes expected to be: adding a
|
||||||
|
[where] clause changes no signature at all. What it changes is which call
|
||||||
|
sites are legal, and an illegal one is a checker refusal at the call site
|
||||||
|
long before the session is asked anything. *)
|
||||||
|
(match
|
||||||
|
Session.eval (gen ())
|
||||||
|
"(defn pick [xs [$t]] $t {:where [(ordered? $t) (copyable? $t)]} (at xs 0))"
|
||||||
|
with
|
||||||
|
| c ->
|
||||||
|
if not (List.mem "pick-i32" c.Session.fns) then
|
||||||
|
fail "adding a where predicate did not reinstall the copies"
|
||||||
|
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||||
|
fail "adding a where predicate was refused: %s" m);
|
||||||
|
|
||||||
|
(* [C-x C-e] checks against the *live* environment rather than re-checking
|
||||||
|
the program, so an expression that instantiates a generic at a type
|
||||||
|
nothing has used generates a copy that exists in no program. The module
|
||||||
|
has to carry it, or the thunk calls a symbol nothing defines. *)
|
||||||
|
(let t = gen () in
|
||||||
|
match Session.eval_expr t "(println (pick (slice [1.5 0.5] 0 2)))" with
|
||||||
|
| e ->
|
||||||
|
if not (has e.Session.ir "pick-f64") then
|
||||||
|
fail "an expression that instantiated a generic did not carry the copy"
|
||||||
|
| exception Loc.Error { Loc.dmsg = m; _ } ->
|
||||||
|
fail "an expression that instantiates a generic: %s" m);
|
||||||
|
|
||||||
if !failures = 0 then print_endline "session: all tests passed"
|
if !failures = 0 then print_endline "session: all tests passed"
|
||||||
else begin
|
else begin
|
||||||
Printf.printf "\n%d failure(s)\n" !failures;
|
Printf.printf "\n%d failure(s)\n" !failures;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user