Say that refusing a signature change is a stopgap

plan.org's hot reload section now says a signature-changing redefinition should
make a new internal function version with its own trampoline: new code resolves
the name to it, existing callers and stored Fn values keep the old one safely,
and the session warns at every tracked caller site still on the old signature.
session.ml refuses the change outright, with a reason that reads like the final
answer.

None of the three parts exists - no function versions, no trampolines, and no
record of which source location called what - so the refusal stays, because the
alternative to refusing is not the new design, it is a silent argument
mismatch. What changes here is only that the code and NEXT.md now say which one
it is, so the next person reads it as the stopgap it is.
This commit is contained in:
Joseph Ferano 2026-09-11 12:26:28 +07:00
parent 55d7c2fae8
commit b34d0d7d59
2 changed files with 24 additions and 2 deletions

15
NEXT.md
View File

@ -607,12 +607,25 @@ Two things the session knows that no single evaluation could:
| Change | What it would have broken |
|---|---|
| a function's signature | a cell is a bare `ptr`; every call site compiled before the change still passes the old arguments through it |
| a function's signature | a cell is a bare `ptr`; every call site compiled before the change still passes the old arguments through it **and this is now a stopgap**, see below |
| a global's type | the storage exists and has a shape — reuse reads at the wrong offsets, replacement discards the state the reload exists to preserve |
| a struct's fields | the values the process is holding have the old layout |
| a `defconst`'s value, **when the checker consumed it** | it is in the *shape* of the program — `(defconst rows (/ h c))` decides `grid`'s type before anything else resolves — so no store can reach it |
| a `defenum` member | `:space` is erased to an `i32` literal in the caller, so it is folded there too |
**The signature row is the one the plan has moved past.** plan.org now says a
signature-changing redefinition should make a new internal function version
with its own trampoline: newly compiled code resolves the name to it, while
existing callers and stored `Fn` values keep the old version and stay safe,
and the session warns at every tracked caller site still targeting the old
signature — recompiling one either retargets it or gives an ordinary type
error. Open decision #6 records it the same way. None of the three parts
exists: there are no function versions, no trampolines (a cell holds a body
address today), and no record of which source locations called what. So the
refusal stays, because the alternative to refusing is not the new design, it
is a silent argument mismatch. It is a stopgap and the message should not be
read as the final answer.
A `defvar`'s *initial value* is deliberately **not** in that table. Its
storage holds live state the program moved past long ago, and refusing to
change the initialiser would be refusing "edit the code, keep the sand". Same

View File

@ -130,7 +130,16 @@ let compatible ~loc (old_ : Tast.program) (new_ : Tast.program) =
&& Types.equal f.Tast.ret g.Tast.ret
in
(* A cell holds a bare pointer. Every call site compiled before this
change still passes the old arguments through it. *)
change still passes the old arguments through it.
This refusal is correct for what is built and is *not* the design
plan.org now describes: a signature change should make a new
internal function version with its own trampoline, leave existing
callers and stored [Fn] values safely on the old one, and warn at
each tracked stale caller site. That needs versions, trampolines
and caller tracking, none of which exist so this stays a refusal
until they do, rather than becoming a silent mismatch. See
plan.org, Hot reload, and open decision #6. *)
if not same then
fail loc
"%s changes signature, from (Fn [%s] %s) to (Fn [%s] %s); \