flan/lib/classes.ml
Joseph Ferano ac7608f94b A method's parameter names are bound in parallel, not in sequence
A let binds in sequence, so binding a method's names pairwise from the
generic's reads a name it has just bound. A generic [a b] with a method
[b a] -- a swap, which is what renaming parameters most often is -- was
handed its first argument twice and could not reach its second at all;
[b c] is the same bug one step shorter. Every argument is now copied
into a temp in the unspellable ~ namespace first and every method name
bound from a temp, uniformly rather than only for the pairs that
collide, because a rule that fires on the tangled case alone is one
nobody exercises. Both shapes are in dyn-class.flan, where the values
are what is wrong rather than the types, and across all three rows.

With it, two things the descriptor fix left behind. descriptors_asm
wrote the descriptors into .rodata and a descriptor holds the address of
its own offset table, so every one of them was a relocation in a
read-only section -- a DT_TEXTREL, which ld warns about in a PIE and
refuses in a shared object, and which was warning in the new daemon
case's own output. They go in .data.rel.ro now, in both the executable
and the reload module; readelf -d on a reload module from each backend
shows no TEXTREL. And FIX.org: the stale held line for item 6, the
fourth read site of the shape tag (say_render, not just print), the
warning that a class's qualifier is the importer's alias so a
hand-written :a/point is coupled to one import's name, and the gap
flagged for the next sweep -- marking through a descriptor an x86 reload
module emitted is still unexercised.
2026-09-20 15:36:27 +07:00

352 lines
15 KiB
OCaml

(** The dyn side's classes and generic functions, turned into ordinary
declarations.
[(defclass point [x y])] is a constructor. [(defgeneric area [self] dyn)]
and [(defmulti describe [x] dyn (get x :kind))] are each one function whose
body is a dispatch, and [(defmethod area point [p] ...)] is a branch of
one. Nothing below this pass knows any of the four forms exists: what it
writes is [defn]s, and they are checked, emitted, rooted, redefined and
inspected as any other function is.
**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
its generic, below it, or at a reload an hour later — and the generic's
body is not decidable until every method is in hand. The pass runs at the
top of [Check.build_program], over the flat list an import has already
been folded into, so a reload rebuilds every dispatch from the session's
whole set of declarations and a method added to a running program is the
ordinary redefinition of the one function that dispatches. [Shim.expand]
is the precedent, form for form.
**Why the method bodies are inlined rather than lifted into functions of
their own.** A generic is then exactly one top-level name, which is what
makes adding a method at a reload work: the session installs the bodies
the evaluated form declared, one name each, and a new method has to reach
a call site that was compiled before it existed. One function means one
cell to replace. The cost is that a method is not separately callable and
does not appear as a frame of its own, which is recorded in FIX.org.
**What it does not do.** There is no inheritance, no multi-argument
dispatch, and no :before/:after/:around — see FIX.org, 2026-09-20, for
which of those were deferred and why. A method's dispatch value is a
literal, so the specificity question CLOS answers with a class precedence
list does not arise here: two methods either answer for the same value,
which is refused, or for different ones. *)
let dyn_at loc : Ast.texpr = { Ast.t = Ast.Tname "dyn"; tloc = loc }
let ex loc (e : Ast.expr_kind) : Ast.expr = { Ast.e; loc }
(* The name the dispatch value is bound to inside a generic's body. [~] is a
delimiter in the reader, so no symbol anyone can write is this one and no
method body can shadow it or be shadowed by it. *)
let dispatch_slot = "~dispatch"
(* The condition a generic signals when no method answers. Its struct is in
the prelude; this is the only place that builds one. *)
let no_method = "NoMethod"
(* ── Collecting ────────────────────────────────────────────────────── *)
type generic = {
gkind : [ `Class | `Multi ];
gfn : Ast.fn;
gloc : Loc.t;
(* In source order, which is dispatch order: the first method whose value
matches answers, and since duplicates are refused the order is not
observable except for [:else], which is moved to the end regardless. *)
mutable gms : Ast.methd list;
}
let collect (decls : Ast.decl list) =
let classes : (string, Loc.t) Hashtbl.t = Hashtbl.create 8 in
let generics : (string, generic) Hashtbl.t = Hashtbl.create 8 in
List.iter
(fun (d : Ast.decl) ->
match d.Ast.d with
| Ast.Defclass (n, slots) ->
(* Two slots of one name would write one entry and read one value,
and the constructor would take two arguments for it. The duplicate
parameter that falls out of it is refused by the checker anyway;
this says which declaration it came from. *)
let seen = Hashtbl.create 8 in
List.iter
(fun (s, sloc) ->
if Hashtbl.mem seen s then
Loc.failk "check/duplicate-slot" sloc
"%s names the slot %s twice. A slot is a key in the \
instance's map, so the second would replace the first and \
the constructor would take an argument that goes nowhere"
n s;
Hashtbl.replace seen s ())
slots;
Hashtbl.replace classes n d.Ast.dloc
| Ast.Defgeneric fn ->
Hashtbl.replace generics fn.Ast.name
{ gkind = `Class; gfn = fn; gloc = d.Ast.dloc; gms = [] }
| Ast.Defmulti fn ->
Hashtbl.replace generics fn.Ast.name
{ gkind = `Multi; gfn = fn; gloc = d.Ast.dloc; gms = [] }
| _ -> ())
decls;
(* Methods second, so a method may be written above the generic it extends
— which it has to be able to be, since a reload appends. *)
List.iter
(fun (d : Ast.decl) ->
match d.Ast.d with
| Ast.Defmethod m ->
let g =
match Hashtbl.find_opt generics m.Ast.mgen with
| Some g -> g
| None ->
Loc.failk "check/unknown-generic" d.Ast.dloc
"no defgeneric or defmulti names %s, so there is nothing for \
this method to be a method of. A generic function is \
declared once, with its parameters and its return type, and \
the methods are written against it"
m.Ast.mgen
in
(match m.Ast.mkey with
| Ast.Dclass c when not (Hashtbl.mem classes c) ->
Loc.failk "check/unknown-class" m.Ast.mkloc
"no defclass names %s. A bare name in a method's dispatch slot \
is a class, and stands for the shape tag its instances carry; \
a dispatch value that is not a class is written as itself — a \
keyword, a string, an integer"
c
| _ -> ());
let n = List.length m.Ast.mfn.Ast.params
and want = List.length g.gfn.Ast.params in
if n <> want then
Loc.failk "check/method-arity" d.Ast.dloc
"%s takes %d argument%s and this method of it takes %d. Every \
method of a generic function has the generic's own parameter \
list: one call site reaches all of them, and it can only pass \
one number of arguments"
m.Ast.mgen want (if want = 1 then "" else "s") n;
(* A class's name and its keyword are one dispatch value — a class
stands for the keyword its instances carry, which is the whole of
how the two dispatch styles share a mechanism — so [point] and
[:point] have to be compared as one or the second method is
accepted and is dead code. *)
let norm = function Ast.Dclass c -> Ast.Dkw c | k -> k in
List.iter
(fun (prev : Ast.methd) ->
if norm prev.Ast.mkey = norm m.Ast.mkey then
Loc.failk "check/duplicate-method" d.Ast.dloc
"%s already has a method for %s. Two methods for one \
dispatch value is an ambiguity nothing resolves — there \
is no specificity rule here, because a dispatch value is \
a value and not a type"
m.Ast.mgen (Ast.dispatch_text m.Ast.mkey))
g.gms;
g.gms <- g.gms @ [ m ]
| _ -> ())
decls;
(classes, generics)
(* ── Writing the declarations ──────────────────────────────────────── *)
(* [(defclass point [x y])] becomes
(defn point [x dyn y dyn] dyn #point{:x x :y y})
— the constructor, positional, one argument per slot in the order the
slots were written. The slots are keys in an ordinary dyn map, so [get],
[put] and [has-key?] are how one is read and written and no new operation
is needed for any of it. What the class adds is the tag on the map, which
is what [class-of] answers and what a generic dispatches on.
Named-slot construction — the dyn twin of [(Cursor {.src s})], with an
omitted slot meaning nil — is deferred, and so is refusing an unknown slot
at [(get p :z)]. Both are recorded in FIX.org. *)
let constructor n slots loc : Ast.decl =
let params =
List.map
(fun (s, sloc) -> { Ast.fname = s; fty = dyn_at sloc; floc = sloc })
slots
in
let pairs =
List.map (fun (s, sloc) -> (ex sloc (Ast.Kw s), ex sloc (Ast.Var s))) slots
in
{ Ast.d =
Ast.Defn
{ Ast.name = n; params; praw = None; ret = Some (dyn_at loc);
fwhere = []; fbody = [ ex loc (Ast.MapLit (Some n, pairs)) ];
nloc = loc };
dloc = loc }
(* The dispatch value a method answers for, as an expression to compare
against. A class's name stands for the keyword its instances carry, which
is the whole of how the CLOS half and the Clojure half share one
mechanism: [(defgeneric area [self] dyn)] is [(defmulti area [self] dyn
(class-of self))], and a method written for the class [point] is a method
written for the value [:point]. *)
let key_expr loc (k : Ast.dispatch) : Ast.expr =
match k with
| Ast.Dclass c -> ex loc (Ast.Kw c)
| Ast.Dkw k -> ex loc (Ast.Kw k)
| Ast.Dstr s -> ex loc (Ast.Str s)
| Ast.Dint i -> ex loc (Ast.Int i)
| Ast.Dbool b -> ex loc (Ast.Var (if b then "true" else "false"))
| Ast.Delse -> assert false (* never compared: it is the else arm *)
(* A method's body, with the method's own parameter names bound to the
generic's. The names are the method's to choose — [(defmethod area point [p]
...)] under [(defgeneric area [self] dyn)] — and a method whose names
already agree with the generic's binds nothing, so the common case adds no
[let] at all.
**The rebinding is parallel, and it has to be.** A [let] here binds in
sequence: each binding is in scope for the next one's value. So the
pairwise spelling — [(let [b a a b] ...)] for a generic [[a b]] and a
method [[b a]] — reads the [b] it has just bound and hands the method its
first argument twice, with the second unreachable. That is a swap, and a
swap is exactly what a method renaming its parameters is most likely to be
doing; the non-swap case [[b c]] is the same bug one step shorter, since
[b] reads the binding above it rather than the parameter. Both are what
[rotatef] and Clojure's destructuring do in parallel, and neither language
would read a name it was in the middle of rebinding.
So every argument is copied into a temp first and every method name is
bound from a temp, never from a parameter. [~] is a delimiter in the
reader, so the temps cannot collide with a method's names whatever they
are, and one uniform shape is written rather than only the pairs that
actually collide — a rule that fires only on the tangled case is a rule
nobody exercises. *)
let method_body (g : generic) (m : Ast.methd) : Ast.expr =
let loc = m.Ast.mfn.Ast.nloc in
let pairs = List.combine m.Ast.mfn.Ast.params g.gfn.Ast.params in
if
List.for_all
(fun ((mp : Ast.field), (gp : Ast.field)) ->
String.equal mp.Ast.fname gp.Ast.fname)
pairs
then ex loc (Ast.Do m.Ast.mfn.Ast.fbody)
else begin
let tmp i = Printf.sprintf "~arg%d" i in
let hold =
List.mapi
(fun i ((_ : Ast.field), (gp : Ast.field)) ->
{ Ast.bname = tmp i; bty = None;
bval = ex gp.Ast.floc (Ast.Var gp.Ast.fname);
bloc = gp.Ast.floc })
pairs
in
let rename =
List.mapi
(fun i ((mp : Ast.field), (_ : Ast.field)) ->
{ Ast.bname = mp.Ast.fname; bty = None;
bval = ex mp.Ast.floc (Ast.Var (tmp i));
bloc = mp.Ast.floc })
pairs
in
ex loc (Ast.Let (hold @ rename, m.Ast.mfn.Ast.fbody))
end
(* What a generic answers when no method does. Common Lisp signals here and
so does this: it is a condition with a handler-case around it, not a trap,
because a dispatch that missed is a thing a program can be written to
answer — a default, a log line, a fallback object — and a trap would take
that away. [error] rather than [signal] because there is no value to carry
on with if nothing handles it.
No restart is established at the miss. That is BoundsError's and
ArithError's decision, taken here for their reason: a restart frame is
allocated by the form that offers it, and the ones that matter — a frame
loop's [continue] — are already on the stack and reachable from a handler
without this form pushing anything. *)
let miss loc gname : Ast.expr =
ex loc
(Ast.Signal
(Ast.Serror,
ex loc
(Ast.Struct
(no_method,
[ ("generic", ex loc (Ast.Str gname));
("value", ex loc (Ast.Var dispatch_slot)) ]))))
let dispatcher (g : generic) : Ast.decl =
let loc = g.gloc in
let gname = g.gfn.Ast.name in
(* The value the methods are keyed by. A defgeneric computes it — the shape
tag of the first argument — and a defmulti's own body is it. *)
let value =
match g.gkind with
| `Multi -> ex loc (Ast.Do g.gfn.Ast.fbody)
| `Class ->
(match g.gfn.Ast.params with
| p :: _ ->
ex loc
(Ast.Call (ex loc (Ast.Var "class-of"),
[ ex p.Ast.floc (Ast.Var p.Ast.fname) ]))
| [] ->
Loc.failk "check/method-arity" loc
"%s takes no arguments, and a defgeneric dispatches on the class \
of its first one. Write a defmulti, whose body says what to \
dispatch on"
gname)
in
(* [:else] last whatever order it was written in, because it is the arm
everything else falls through to and not a value to compare against. *)
let fallback, cases =
List.partition (fun (m : Ast.methd) -> m.Ast.mkey = Ast.Delse) g.gms
in
let last =
match fallback with
| m :: _ -> method_body g m
| [] -> miss loc gname
in
let chain =
List.fold_right
(fun (m : Ast.methd) rest ->
let kloc = m.Ast.mkloc in
ex kloc
(Ast.If
(ex kloc
(Ast.Call (ex kloc (Ast.Var "="),
[ ex kloc (Ast.Var dispatch_slot);
key_expr kloc m.Ast.mkey ])),
method_body g m, Some rest)))
cases last
in
{ Ast.d =
Ast.Defn
{ g.gfn with
Ast.fbody =
[ ex loc
(Ast.Let ([ { Ast.bname = dispatch_slot; bty = None;
bval = value; bloc = loc } ],
[ chain ])) ] };
dloc = loc }
(** Every [defclass], [defgeneric], [defmulti] and [defmethod] in the list,
replaced by the [defn]s they stand for. Everything else is untouched and
keeps its position: a constructor is written where its class was and a
dispatch where its generic was, so declaration order — which is emission
order for globals — does not move. *)
let expand (decls : Ast.decl list) : Ast.decl list =
let has =
List.exists
(fun (d : Ast.decl) ->
match d.Ast.d with
| Ast.Defclass _ | Ast.Defgeneric _ | Ast.Defmulti _
| Ast.Defmethod _ -> true
| _ -> false)
decls
in
if not has then decls
else begin
let _classes, generics = collect decls in
List.filter_map
(fun (d : Ast.decl) ->
match d.Ast.d with
| Ast.Defclass (n, slots) -> Some (constructor n slots d.Ast.dloc)
| Ast.Defgeneric fn | Ast.Defmulti fn ->
Some (dispatcher (Hashtbl.find generics fn.Ast.name))
(* Gone: its body is inside its generic's dispatch. *)
| Ast.Defmethod _ -> None
| _ -> Some d)
decls
end