Inside an object's on_close handler GitHub issue

vibecode
{"vibecode": {"example": "inside_on_close",
    "shows": "drinian_during_a_gc_invoked_on_close_handler_running_against_an_object_whose_last_reference_was_just_dropped",
    "shape": "on_close_handler_is_a_call_stack_frame_pushed_by_the_engine_not_by_user_code; runs_with_strict_rules_per_garbage_collection_md",
    "design_note": "action_on_close_is_a_new_frame_action; engine_enforces_2ms_cap_no_io_no_allocation_no_resurrection_when_this_action_is_active"}}

User code drops the last reference to an object that has an on_close handler. The runtime traces, finds the object unreachable, and invokes the handler synchronously in the calling frame's stack — per garbage-collection.md § on_close. The snapshot is taken inside the handler.

Caspian source:

caspian
class
    on_close do($call)
        @socket.close                            # CAPTURED HERE
    end
end

$conn = %['myapp.com/connection'].new()
$conn = nil

Pause point: line 3, inside the on_close handler, just after the handler frame was pushed by the GC and before @socket.close dispatches. The connection instance is still alive — $call.receiver holds it for the duration of the handler — but $conn (the only user-code reference) has been nilled.

json
{
  "comment": "Drinian mid-on_close. The handler frame at the top was pushed by the engine (not by user code) when GC determined the connection instance was unreachable from any root. The handler runs in the dying object's class's role (here, user), with strict rules enforced.",
  "srcs": {
    "a": {"file": "/home/miko/conn.casp"}
  },
  "roles": {
    "user": {"sees": ["stdout", "stderr"]},
    "stdlib": {},
    "engine": {}
  },
  "objects": {
    "stdout": {"class": "stream", "owner": "engine"},
    "stderr": {"class": "stream", "owner": "engine"}
  },
  "call_stack": [
    {
      "comment": "Frame 0: top-level. $conn was set on line 7, then reassigned to nil on line 8. The local still exists in locals as nil. The class `myapp.com/connection` was defined at top level, but the class registry itself lives in engine-private state, not in this frame.",
      "action": "top_level",
      "role": "user",
      "lexical_parent": null,
      "src": ["a", 8],
      "locals": {
        "conn": null
      }
    },
    {
      "comment": "Frame 1: the on_close handler. Pushed by the engine when GC fired during the line 8 nil-assignment. action: 'on_close' tells the engine to enforce the strict rules (2ms cap, no I/O, no allocation, no resurrection, uncatchable timeout abort). lexical_parent is 0 because the class body and its handler were defined at top level. role is user because the class is user-owned.",
      "action": "on_close",
      "role": "user",
      "lexical_parent": 0,
      "src": ["a", 3],
      "locals": {
        "call": {"hash": {
          "receiver": {
            "instance": "myapp.com/connection",
            "fields": {
              "@socket": "<ref to socket instance>"
            },
            "src": ["a", 7]
          },
          "args": null,
          "opts": null,
          "block": null,
          "super": null
        }, "src": ["a", 2]}
      },
      "gc": {
        "deadline_ms_remaining": 1.74,
        "rules": ["no_allocation", "no_io", "no_resurrection", "no_catch_abort", "no_cleanup_order_dependency"]
      }
    }
  ]
}

What to notice:

Open question:


© 2026 Puck.uno