# Events ## Overview Events communicate readiness between asynchronous Choreo paths. A future is a producer-local completion handle; an event is the published synchronization point that one or more consumers can wait on. ```choreo shared event full; load = tma.load.async input => tile; trigger full after load; ``` Native operation completion can be bound to an event with `trigger event after future`. ## Declaration and Initial State Events use ordinary storage and array syntax: ```choreo shared event ready_for_compute; shared event full[2], empty[2] = ready; ``` Generation zero is pending by default. `= ready` marks generation zero ready, which is useful for initially empty pipeline slots. The backend chooses the hardware synchronization primitive from the event's producer, consumers, scope, and participation. Source code does not select an mbarrier, named barrier, or transaction count. For a one-slot CTA release handoff initialized with `= ready`, with statically known, disjoint, warp-aligned waiter and trigger sets, the CUDA backend may lower the event directly to a named barrier and prime it automatically. Events with pending initial state, dynamic, overlapping, sub-warp, multi-slot, TMA-completion, or cluster-scope participation keep the general mbarrier lowering. This choice is an implementation detail and does not change the source semantics. ## Immediate Trigger `trigger` without dependencies publishes readiness at that point: ```choreo trigger empty; trigger empty0, empty1; ``` This form is useful for releasing an empty buffer after its consumer is done or for explicit initialization. It does not represent completion of an earlier async operation unless that operation has already been waited on. ## Trigger After Future Use a dependency when readiness comes from an async operation: ```choreo load = tma.load.async input => tile; trigger full after load; ``` Several futures can be joined into one event: ```choreo q_load = tma.load.async q => q_s; k_load = tma.load.async k => k_s; trigger qk_inputs_full after q_load, k_load; ``` `after` is a binding clause, not an implicit wait. The statement does not block the producer path, and the compiler must not synthesize a wait. It is currently supported for global-to-shared TMA futures, whose hardware mbarrier completion can directly publish the event. Operations without native event binding use an explicit wait and a direct trigger. WGMMA is one example: ```choreo qk = mma.row.row.async scores, q_s, k_s; wait qk; trigger qk_done; ``` Writing `wait qk; trigger qk_done after qk;` is rejected because the future was already consumed by the explicit wait. Inline event operands on DMA/TMA and special MMA wait-trigger statements are not part of the language. ## Waiting `wait` blocks the current path until the selected event generation is ready: ```choreo wait full; wait lhs_full, rhs_full; ``` Events are generation-based. A completed wait advances that path to the next generation. Repeated producer/consumer handoffs therefore reuse the event without a source-level reset operation. ## Cyclic Event Generations Use `.at(order)` when an event array is a cyclic pipeline ring: ```choreo slot = order % 2; wait empty.at(order); load = tma.load.async input.at(order) => tile[slot]; trigger full.at(order++) after load; ``` The consumer uses the same logical order: ```choreo slot = order % 2; wait full.at(order); // Consume tile[slot]. trigger empty.at(order++); ``` For `event[N]`, the compiler derives the physical event slot as `order % N` and its barrier phase as `(order / N) % 2`. The argument may be a monotonically increasing logical order or an equivalent order maintained modulo `2 * N`. For example, `(order + 1) & 3` is sufficient for `event[2]`. Reducing the argument modulo `N` is not sufficient because it discards the phase. A cyclic event ring is currently one-dimensional. Pre- and post-increment follow C/C++ value semantics. In particular, `event.at(order++)` selects the current generation and advances `order` once after evaluating the event operand. Slot and phase lowering reuse that one evaluated value. `event[index]` remains ordinary physical array indexing. Its index is not interpreted as a logical order, so it is not an alias for `event.at(order)`. Shared data arrays remain ordinary arrays. Their physical slot is therefore written explicitly, as in `tile[slot]`; event generation and data indexing do not share a special source-level type. ## Multiple Consumers Publish a future once, then use an event when multiple paths need the result: ```choreo trigger tile_full after load; inthreads.async (consumer_a) { wait tile_full; } inthreads.async (consumer_b) { wait tile_full; } ``` The compiler derives event participation from the active scopes. Do not publish the same operation future separately for each consumer. For independent generations or independently released buffers, use an event array rather than making unrelated consumers race on one generation. ## Producer-Consumer Example ```choreo shared f16[128, 128] tile[2]; shared event full[2], empty[2] = ready; parallel g by 2 : group-4 { inthreads.async (g == 0) { foreach {order} in [tile_count] { slot = order % 2; wait empty.at(order); load = tma.load.async input.at(order) => tile[slot]; trigger full.at(order) after load; } } inthreads.async (g == 1) { foreach {order} in [tile_count] { slot = order % 2; wait full.at(order); // Compute with tile[slot]. trigger empty.at(order); } } } ``` This is the complete pipeline vocabulary: async operations produce futures, events publish readiness, and `.at(order)` selects cyclic generations.