Two things, and either alone is useless, so they are one commit. Emit.redefinition compiles one function into its own module against a host that is already running. What it does *not* define is the design: a global is external, so state survives a reload and sand's grid is not reset by editing the code; every other function is a declare, so a redefined settle calls the host's move-grain rather than a frozen copy; there is no main. Build.shared puts that text through llc + ld -shared. ld, not clang, because a shared object is allowed undefined symbols and that is the whole mechanism - and because the driver is 50ms of a 20ms job. Measured here: llc 16ms, ld 3ms, dlopen 0.04ms. Loading a body is not installing it, though. A call bound at link time cannot notice a new one, so a dev build routes every Flan-to-Flan call through a cell - a mutable global holding the address of the function that is current - and a module publishes itself with one store. The cell load is emitted after the arguments, so a redefinition between two calls cannot land inside one. Three details that are not free choices. flan_reload_install is a named function rather than an ELF constructor, because the agent has to choose when the store happens and a constructor would do it during dlopen, mid-frame, on whatever thread called it. A redefinition's own body is hidden, because default visibility in a shared object is interposable and that applies to taking the address too: plain @"flan.bump" inside the module resolves to the host's copy, so the installer would publish the function it was replacing and the reload would silently do nothing. And -rdynamic is what exports the cells at all, so it and cells are one flag: Build.opts.dev, flan build --dev, the first time opts means something semantic rather than an optimisation level. The test is one process, because two runs would prove nothing about a swap, and two .so paths, because dlopen caches by path and would hand back the first handle. Every call in it goes through outer, compiled once into the host and never rebuilt, so a changed answer can only mean its call site followed. v2 recurses through its own cell, which is the interposition case; it would print the old body's text if it did not. helper differs between the fixtures purely as a tripwire for a module that grew its own copy. LLVM cannot fold the indirection - the cell is an external mutable global - and a --dev calc-me keeps 46 indirect calls at -O2. values, machine and sand-headless now run as dev builds in the acceptance table too; the sand hash is the one result that would notice a call reaching the wrong function.
27 lines
1.1 KiB
Plaintext
27 lines
1.1 KiB
Plaintext
;;;; The reload primitive's fixture, v2. Three differences from v1, each one a
|
|
;;;; separate thing being checked:
|
|
;;;;
|
|
;;;; [bump] steps by 10 and adds 1000, so the host's untouched call site in
|
|
;;;; [outer] visibly runs the new body rather than the one it was linked to.
|
|
;;;;
|
|
;;;; [bump] also calls *itself*. Inside a shared object a plain call would be
|
|
;;;; interposed by the host's copy — the module would look self-consistent and
|
|
;;;; silently run the old body — so the self-call goes through the cell like
|
|
;;;; any other. If it did not, the first recursive step would print "v1" and
|
|
;;;; the transcript would say so.
|
|
;;;;
|
|
;;;; [helper] is changed only as a tripwire. The module declares it rather than
|
|
;;;; defining it, so this body is dead text and the call has to land on the
|
|
;;;; host's [* x 2]; with the two bodies identical nothing at run time would
|
|
;;;; notice a module that grew its own copy.
|
|
(defvar counter i64)
|
|
|
|
(defn helper [x i64] i64 (* x 3))
|
|
|
|
(defn bump [] i64
|
|
(print-line "v2")
|
|
(set counter (+ counter 10))
|
|
(if (> counter 100) (+ (helper counter) 1000) (bump)))
|
|
|
|
(defn outer [] i64 (bump))
|