Asked whether a defconst could be redefined, probed it, and got ":status ok" for a change that did nothing at all - the module was built, delivered, installed, and the program went on using the old value. That is the silent-wrongness class the house rule exists to prevent, so it is now four refusals and a fix. A defconst's value is folded into its call sites - into an array length at worst, which is decided before any type resolves - so it lives in the running program's code and not only in its storage. Refused. A defenum member is the same thing: :space is erased to an i32 literal in the caller. Refused, and compared over declarations rather than over Tast.program, which carries no enums at all for exactly that reason. A defvar's initial value is deliberately not refused. 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 Tast.global record as a defconst, opposite answers, told apart by gconst. The value comparison is structural and conservative - anything it does not recognise counts as changed. Comparing emitted text would be wrong, since Emit.const on a string allocates a name off a per-module counter and two different strings in two throwaway modules both come out as @".str.0". Third: a new global's declared initial value was being dropped. flan_dev_global callocs, so (defvar n i64 42) added at run time was silently zero. It now takes the initial value as a blob, copies it on the allocation and ignores it afterwards - the second half being where "a reload must not reset the program's state" lives. In the allocation path rather than a branch at the call site, so it cannot be got wrong at one of them. Fourth: a change with no body to publish and no storage to allocate now answers "nothing to install" instead of shipping an empty module. That is what the defconst probe actually did, and it cost the program a frame's worth of reload it did not need.
104 lines
4.0 KiB
C
104 lines
4.0 KiB
C
/* flan_dev — the part of the host ABI that only a dev build has.
|
|
*
|
|
* A redefinition module reaches the host's functions and globals through
|
|
* symbols the host already exports: a cell for each function, the storage for
|
|
* each global. That covers everything the program was *built* with. It does
|
|
* not cover a name the module introduces — a defn or a defvar typed into the
|
|
* REPL after the process started — because there is no symbol in the host to
|
|
* bind to and ELF cannot grow one.
|
|
*
|
|
* So a name that is new at run time is keyed by string instead. This file is
|
|
* the two lookups that make that work, and deliberately nothing else:
|
|
*
|
|
* flan_dev_cell(name) the cell a new function lives in
|
|
* flan_dev_global(name, size, init) the storage a new global lives in
|
|
*
|
|
* Both are idempotent: the second module to mention a name gets what the first
|
|
* one got. That is the whole point. Two modules that each define their own
|
|
* copy of a new function would each call their own, and redefining it would
|
|
* update one of them.
|
|
*
|
|
* The table never moves. A module holds the address of a cell for as long as
|
|
* it is loaded, so a growable table would leave those addresses pointing into
|
|
* a freed allocation. Fixed capacity and a loud failure instead.
|
|
*
|
|
* Never dlclose a module. A cell holds an address inside that module's text,
|
|
* and unloading it leaves every call site pointing at unmapped memory. There
|
|
* is no unload path here on purpose.
|
|
*/
|
|
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#define FLAN_DEV_MAX 4096
|
|
|
|
typedef struct {
|
|
const char *name; /* strdup'd: the module that passed it may go away */
|
|
void *cell; /* a function's cell, or a global's storage */
|
|
size_t size; /* a global's size; 0 for a function */
|
|
} entry;
|
|
|
|
static entry table[FLAN_DEV_MAX];
|
|
static size_t used;
|
|
|
|
static void die(const char *what, const char *name) {
|
|
fprintf(stderr, "flan_dev: %s: %s\n", what, name);
|
|
fflush(stderr);
|
|
abort();
|
|
}
|
|
|
|
static entry *find(const char *name) {
|
|
for (size_t i = 0; i < used; i++)
|
|
if (strcmp(table[i].name, name) == 0) return &table[i];
|
|
return NULL;
|
|
}
|
|
|
|
static entry *intern(const char *name) {
|
|
if (used == FLAN_DEV_MAX) die("out of dev name slots", name);
|
|
entry *e = &table[used++];
|
|
e->name = strdup(name);
|
|
if (e->name == NULL) die("out of memory", name);
|
|
e->cell = NULL;
|
|
e->size = 0;
|
|
return e;
|
|
}
|
|
|
|
/* The cell a run-time-introduced function is called through. One indirection
|
|
* more than a function the host was built with, whose cell is a symbol the
|
|
* module can name directly — the compiler picks per name, so the common case
|
|
* stays a single load. */
|
|
void **flan_dev_cell(const char *name) {
|
|
entry *e = find(name);
|
|
if (e == NULL) e = intern(name);
|
|
return &e->cell;
|
|
}
|
|
|
|
/* Storage for a run-time-introduced global, allocated once.
|
|
*
|
|
* [init] is its declared initial value, or NULL for all-zero. It is copied on
|
|
* the allocation and ignored on every call after it, which is where "a reload
|
|
* must not reset the program's state" lives: the second module to mention this
|
|
* name is a redefinition, and re-running an initialiser would throw away
|
|
* exactly what the reload exists to preserve. Doing it here rather than by a
|
|
* branch in the caller means the rule cannot be got wrong at one call site.
|
|
*
|
|
* A size mismatch is the layout-drift failure, caught at its first chance: the
|
|
* running process has already laid this memory out, and handing back the old
|
|
* allocation for a differently shaped type means the new body reads fields at
|
|
* the wrong offsets and nothing ever says so. Retyping a var needs a restart. */
|
|
void *flan_dev_global(const char *name, uint64_t size, const void *init) {
|
|
entry *e = find(name);
|
|
if (e == NULL) {
|
|
e = intern(name);
|
|
e->cell = calloc(1, size ? (size_t)size : 1);
|
|
if (e->cell == NULL) die("out of memory", name);
|
|
e->size = (size_t)size;
|
|
if (init != NULL && size > 0) memcpy(e->cell, init, (size_t)size);
|
|
return e->cell;
|
|
}
|
|
if (e->size != (size_t)size) die("size changed; restart to retype", name);
|
|
return e->cell;
|
|
}
|