Yep — here’s the concrete picture, with the “no-Proxy” trampoline approach, and what it can/can’t do. ## A concrete hot-reload example (with trampolines) ### `sprite.cell` v1 ```js // sprite.cell var X = key('x') var Y = key('y') def proto = { move: function(dx, dy) { this[X] += dx this[Y] += dy } } var make = function(x, y) { var s = meme(proto) s[X] = x s[Y] = y return s } return { proto: proto, make: make } ``` ### What the runtime stores on first load Internally (not visible to Cell), you keep a per-module record: ```js module = { scope: { X, Y, proto, make }, // bindings created by var/def in module export_current: { proto, make }, // what the module returned export_handle: null // stable thing returned by use() in hot mode } ``` Now, **in hot-reload mode**, `use('sprite')` returns `export_handle` instead of `export_current`. Since you don’t have Proxy/getters, the handle can only be “dynamic” for **functions** (because functions are called). So you generate trampolines for exported functions: ```js // export_handle is a plain object export_handle = stone({ // stable reference (proto is identity-critical and will be patched in place) proto: module.scope.proto, // trampoline (always calls the latest implementation) make: function(...args) { return module.scope.make.apply(null, args) } }) ``` Note what this buys you: * Anyone who cached `var sprite = use('sprite')` keeps the same `sprite` object forever. * Calling `sprite.make(...)` always goes through the trampoline and hits the *current* `module.scope.make`. ### Reload to `sprite.cell` v2 Say v2 changes `move` and `make`: ```js def proto = { move: function(dx, dy) { // new behavior this[X] = this[X] + dx * 2 this[Y] = this[Y] + dy * 2 } } var make = function(x, y) { ... } // changed too return { proto, make } ``` Runtime reload sequence (safe point: between actor turns): 1. Evaluate the new module to produce `new_scope` and `new_export`. 2. Reconcile into the old module record: * **`var` bindings:** rebind * `old.scope.make = new.scope.make` * (and any other `var`s) * **`def` bindings:** keep the binding identity, but if it’s an object you want hot-updatable, **patch in place** * `old.scope.proto.move = new.scope.proto.move` * (and other fields on proto) Now the magic happens: * Existing instances `s` have prototype `old.scope.proto` (stable identity). * You patched `old.scope.proto.move` to point at the new function. * So `s.move(...)` immediately uses the new behavior. * And `sprite.make(...)` goes through the trampoline to `old.scope.make`, which you rebound to the new `make`. That’s “real” hot reload without Proxy. --- ## “Module exports just a function” — yes, and it’s actually the easiest If a module returns a function: ```js // returns a function directly return function(x) { ... } ``` Hot-reload mode can return a **trampoline function**: ```js handle = stone(function(...args) { return module.scope.export_function.apply(this, args) }) ``` On reload, you rebind `module.scope.export_function` to the new function, and all cached references keep working. --- ## “Module exports just a string” — possible, but not hot-swappable by reference (without changing semantics) If the export is a primitive (text/number/logical/null), there’s no call boundary to hang a trampoline on. If you do: ```js return "hello" ``` Then anyone who did: ```js def msg = use('msg') // msg is a text value ``` …is holding the text itself. You can’t “update” that value in place without either: ### Option 1: Accept the limitation (recommended) * Hot reload still reloads the module. * But **previously returned primitive exports don’t change**; callers must call `use()` again to see the new value. This keeps your semantics clean. ### Option 2: Dev-mode wrapping (changes semantics) In hot-reload mode only, return a box/thunk instead: * box: `{ get: function(){...} }` * thunk: `function(){ return current_text }` But then code that expects a text breaks unless it’s written to handle the box/thunk. Usually not worth it unless you explicitly want “dev mode has different types”. **Best convention:** if you want a reloadable “string export”, export a function: ```js var value = "hello" return { get: function() { return value } } ``` Now `get()` is trampoline-able. --- ## About `var` vs `def` on reload You’re very close, just phrase it precisely: * **`var`**: binding is hot-rebindable On reload, `old.scope[name] = new.scope[name]`. * **`def`**: binding identity is stable (const binding) On reload, you do **not** rebind the slot. But: for `def` that points to **mutable objects that must preserve identity** (like prototypes), you *can still patch the object’s fields in place*: * binding stays the same object * the object’s contents update That’s not “setting new defs to old defs”; it’s “keeping old defs, optionally copying new content into them”. If you want to avoid surprises, make one explicit rule: * “def objects may be patched in place during hot reload; def primitives are never replaced.” --- ## One important consequence of “no Proxy / no getters” Your trampoline trick only guarantees hot-reload for: * exported **functions** (via trampolines) * exported **objects whose identity never changes** (like `proto`), because the handle can point at the stable old object It **does not** guarantee hot-reload for exported scalars that you expect to change (because the handle can’t dynamically compute a property value). That’s fine! It just becomes a convention: “export state through functions, export identity anchors as objects.” --- If you keep those rules crisp in the doc, your hot reload story becomes genuinely robust *and* lightweight: most work is “rebind vars” + “patch proto tables” + “trampoline exported functions.” The rest is just conventions that make distributed actor code sane.