Engine events — script waits for engine to broadcast GitHub issue

vibecode
{"vibecode": {
    "doc": "engine_events",
    "role": "brainstorm — letting the engine itself broadcast events into running Caspian code, with the script explicitly yielding control via %engine.wait or %engine.wait_loop. Cooperative model (script knows where it could be paused), not preemptive (no surprise interrupts mid-statement). Not for V1.",
    "status": "idea — not in active design, not in V1.0",
    "key_concepts": ["engine_as_a_broadcaster",
        "cooperative_yield_via_percent_engine_wait",
        "single_shot_vs_internal_loop_forms",
        "avoids_interrupt_driven_complexity",
        "explicit_yield_points_preserve_simplicity_promise"]
}}

A possible future direction: the engine itself can broadcast events into running Caspian code. Use cases that motivate it — timers, signal handling, GC notifications, resource-pressure warnings, async I/O readiness without explicit polling.

The natural-but-bad way to do this is interrupt-driven — the engine preempts the running line, dispatches the event, then resumes the script. Same model as Unix signals. Powerful but evil: any non-atomic operation in user code can be interrupted between substeps, reentrancy gets ugly, and Caspian's "single-threaded, you can reason about it" simplicity goes out the window.

This document sketches a cooperative alternative: the script explicitly yields control to the engine at a known point. No surprises mid-statement; the script chooses when it's OK to be paused.


%engine.wait — single-shot GitHub issue

The script blocks until the engine broadcasts one event, then resumes.

%engine.wait do($event_name, $content)
    # handler body — fires once, then control returns to whatever's
    # below this construct
end

The closure receives the event name and the event's content. After it returns, the script continues from below. To handle the next event, the script calls %engine.wait again.

Composable — the script controls the loop, can interleave other work, can decide when to stop waiting.


%engine.wait_loop — internal loop GitHub issue

The script blocks indefinitely, firing the closure once per event as they arrive. Doesn't return on its own — control stays inside the loop.

%engine.wait_loop do($event_name, $content)
    # handler body — fires for every event the engine broadcasts,
    # in arrival order
end

Convenient for "this is my main loop" patterns where the script's whole job is reacting to engine-driven events.


Why cooperative, not preemptive GitHub issue

Interrupt-driven engine events would break Caspian's settled invariant that single-threaded synchronous code runs predictably. A signal arriving mid-@x = @x + 1 could split that into "load @x → INTERRUPT → store @x", and a handler running during the interrupt could observe inconsistent state.

The cooperative shape (%engine.wait and %engine.wait_loop) gives a clean yield point. The script knows EXACTLY where it could be paused — only at calls to these constructs. Outside of them, the synchronous, surprise-free promise still holds.

This matches well-trodden cooperative event-handling patterns in other languages:


Relationship to the existing event system GitHub issue

Caspian's event-broadcasting system already lets any object broadcast and any object listen. The engine could just BE another broadcaster — %utils.broadcast calls fired from inside the engine's internals.

Under that framing, %engine.wait could be sugar for:

  1. Register a closure on %engine for any event.
  2. Yield control to the engine until one of those events fires.
  3. Run the closure.
  4. Unregister and return.

%engine.wait_loop would be similar but without the unregister-and-return step.

Whether to spec these as primitive constructs or as derived sugar over the registration mechanism is one of the open points.


Open points GitHub issue


Why this isn't in V1 GitHub issue

V1's promise is a synchronous, single-threaded script model with no surprises. Adding any form of engine-broadcast events — even cooperative ones — expands the mental model. The cooperative version preserves the surprise-free promise IF the script never calls %engine.wait, but the moment it does, the script is in a different programming model and needs to think about event ordering, reentrancy, and exit conditions.

Worth doing eventually for the use cases (timers, signals, GC hooks) — but the design needs to settle before commitment, and V1 has enough to ship without it.

© 2026 Puck.uno