# What is a "Poke"? Understanding Urbit Events ## The Big Picture **Poke** is Urbit's term for "send an event to Arvo (the kernel)". Think of it like this: - **Unix**: You write to a file descriptor or call `ioctl()` - **Windows**: You send a message to a window handle - **Urbit**: You **poke** Arvo with an **ovum** (event) ## Event Structure: Ovum An `ovum` (plural: ova) is a single event. It's a simple pair: ``` ovum = [wire card] ``` Where: - **wire**: A path identifying WHERE this event came from/goes to - Example: `/d/term/1` (Dill terminal 1) - Example: `/g/http/0v1a2b3c` (Eyre HTTP request) - Example: `/a/peer/~zod` (Ames network to ~zod) - **card**: The actual action/data, structured as `[tag payload]` - Example: `[%belt %ret]` (keyboard Return key) - Example: `[%born ~]` (HTTP server started) - Example: `[%send ...]` (Network packet) ## The Poke Cycle ``` ┌─────────────┐ │ Runtime │ (Vere, Neovere, Sword) └──────┬──────┘ │ │ 1. Poke with ovum [wire card] │ ▼ ┌─────────────┐ │ Arvo │ (The kernel) └──────┬──────┘ │ │ 2. Returns [effects new_kernel] │ ▼ ┌─────────────┐ │ Runtime │ └──────┬──────┘ │ │ 3. Execute effects │ (print to terminal, send network packet, etc.) │ └─────► Real world I/O ``` ## How Poke Actually Works From the Vere C code (and our OCaml port): ```ocaml (* Get poke function from kernel *) let poke_arm = slot 23 kernel in (* Create gate (function) *) let gate = nock kernel poke_arm in (* Call gate with event *) let result = slam gate event in (* Result is [effects new_kernel] *) match result with | Cell (effects, new_kernel) -> (* Update kernel state *) kernel := new_kernel; (* Execute effects *) execute_effects effects ``` ## Types of Events (Common Cards) ### Terminal (Dill) ``` Wire: /d/term/1 Inputs (to Arvo): [%belt %ret] - Return key [%belt %bac] - Backspace [%belt [%txt "hello"]] - Text input Outputs (from Arvo): [%blit %lin "text"] - Print line [%blit %clr] - Clear screen [%blit %hop 5] - Move cursor ``` ### HTTP (Eyre) ``` Wire: /g/http/0v... Inputs: [%request request-data] - HTTP request Outputs: [%http-response response] - HTTP response ``` ### Network (Ames) ``` Wire: /a/peer/~zod Inputs: [%hear packet] - Received packet Outputs: [%send packet] - Send packet ``` ### Timer (Behn) ``` Wire: /b/wait/... Inputs: [%wake ~] - Timer fired Outputs: [%doze time] - Set next timer ``` ## Boot vs Runtime ### During Boot (Lifecycle) ```ocaml (* Boot uses a special formula, not poke! *) let lifecycle = [2 [0 3] [0 2]] in let kernel = nock event_list lifecycle in (* No effects during boot - just builds kernel *) ``` Boot events are **batch processed** via the lifecycle formula. They build up the kernel state but don't produce effects. ### After Boot (Runtime) ```ocaml (* Runtime uses poke for each event *) let effects = poke kernel event in (* Effects need to be executed! *) ``` Runtime events are **individually poked** and produce effects that must be executed. ## Why Two Different Mechanisms? **Lifecycle** (boot): - Process many events as a batch - Pure state building - No I/O needed - Fast initialization **Poke** (runtime): - Process one event at a time - Produces side effects - Needs I/O execution - Interactive operation ## Our Implementation ### State.poke (lib/state.ml:66) ```ocaml let poke state event = (* Get poke gate from kernel slot 23 *) let formula = slot poke_formula_axis kernel in let gate = nock_on kernel formula in (* Slam gate with event *) let result = slam_on gate event in (* Extract effects and new kernel *) match result with | Cell (effects, new_core) -> state.roc <- new_core; (* Update kernel *) state.eve <- succ eve; (* Increment event number *) effects (* Return effects *) ``` ### What We Need To Do Next 1. ✅ **Poke works** - ~11,000 pokes/second 2. ✅ **Effects returned** - Structure is correct 3. ⏳ **Parse effects** - Extract wire/card from effect list 4. ⏳ **Route effects** - Send to appropriate driver (Dill, Eyre, Ames) 5. ⏳ **Execute effects** - Actually do the I/O 6. ⏳ **Event loop** - Continuous input → poke → effects → output ## Example: Terminal Input Flow ``` 1. User types "hello" and hits Enter 2. Terminal driver creates belt event: ovum = [/d/term/1 [%belt [%txt "hello"]]] [/d/term/1 [%belt %ret]] 3. Poke Arvo: effects = State.poke state ovum 4. Arvo processes input, maybe runs dojo command 5. Arvo returns effects: [ [/d/term/1 [%blit [%lin "result here"]]] [/d/term/1 [%blit [%lin "~zod:dojo> "]]] ] 6. Dill driver executes effects: - Print "result here" to terminal - Print prompt "~zod:dojo> " 7. Wait for next input ``` ## Performance Notes From our benchmarks: - **Poke speed**: ~11,000 pokes/second (0.09ms each) - **Boot time**: ~1s for full solid pill - **Memory**: No limit (OCaml GC manages it) This is plenty fast for interactive use! ## Key Insight **Poke is just a function call into the kernel.** There's no magic. It's: 1. Load kernel from memory 2. Call its poke function (slot 23) 3. Get back effects + new kernel 4. Replace old kernel with new kernel 5. Execute the effects The genius is that the **entire OS state** is in that kernel noun, and poke is a pure function that transforms it. ## References - `vere/pkg/vere/mars.c` - C implementation - `vere/pkg/noun/vortex.c:u3v_poke()` - Poke implementation - `docs/runtime/api.md` - API documentation (check `/docs/runtime/`) - Arvo source: `sys/arvo.hoon` in urbit/urbit repo ## See Also - `BOOT_COMPARISON.md` - Why lifecycle ≠ poke - `EFFECTS.md` - Effects processing architecture - `LOOM.md` - Memory management (or lack thereof!)