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.
