Events
Events provide typed publish/subscribe communication owned by a value or type. They are intended for notifications such as GUI actions, component state changes, device notifications, domain events, plugin hooks and diagnostic hooks.
An event is not a channel, completion object, hardware interrupt, timer, semaphore, cancellation object or general waitable synchronization signal. Its default behavior is synchronous fan-out: one publication invokes zero or more subscribed handlers in subscription order.
Purpose
An instance event has state. Subscriber slots, active subscriptions and event lifetime are
therefore part of the owning value's storage model. Sec does not allow an impl
block to silently add hidden per-instance event storage.
impl, but
impl does not secretly enlarge each instance.
Default Policy
The current Sec 0.1 default event policy is:
subscriber capacity: 4
dispatch: synchronous
dispatch order: subscription order
publication: owning impl only
subscription: according to field visibility
storage: inline fixed-capacity storage
allocation: none
thread safety: not implied
ISR publication: not implied
The default capacity of four is deliberate. Reaching capacity does not trigger allocation, growth or replacement of another subscriber.
Declaring an Event
The short form declares the event as part of the owning type:
type Button struct {
ButtonPressed: Event[ButtonPressData]
}
The event is part of Button's layout and lifetime. The owner then defines the
behavior that publishes it:
impl Button {
fn Press(data: ButtonPressData) void {
ButtonPressed.Publish(data)
}
}
When the default policy is sufficient, the event does not need to be repeated in the
impl block.
Publication Rights
Inside the owning impl, the event exposes publication capability. Outside that
owning implementation, publication is forbidden by default even when the event is visible.
button.ButtonPressed.Publish(data) // invalid outside Button's owning impl
A suitable diagnostic is:
event ButtonPressed may only be published by Button
Visibility and publication authority are intentionally separate. External code may be able to observe and subscribe to an event without gaining the right to publish it.
Subscription
Subscribers attach a handler through Subscribe(...):
let result := button.ButtonPressed.Subscribe(OnButtonPressed)
A successful subscription produces a move-only Subscription. Closing that
subscription unregisters the handler, and destroying the subscription has the same effect.
subscription.Close()
The current compiler-known subscription result has two implemented outcome variants:
Subscribed(Subscription)
Full
The handler and every capture it retains must remain valid for at least as long as the subscription can remain active.
Capacity
Event[T] uses the default capacity of four subscriptions. A different fixed
capacity can be selected explicitly:
type Button struct {
ButtonPressed: Event[ButtonPressData, 8]
}
Capacity is compile-time known, must be greater than zero and contributes fixed storage to
the owning type. When an event is full, Subscribe(...) reports Full.
It does not allocate, grow, overwrite or silently remove another subscriber.
Dispatch and Handler Semantics
Default publication is synchronous:
ButtonPressed.Publish(data)
All currently eligible handlers are invoked before Publish(...) returns.
Publication does not implicitly spawn tasks, create threads, enqueue work or allocate.
The current handler model is conceptually:
fn Handler(data: T) void
Handlers return void. Application errors must be handled by the handler or
represented through explicit program state. An unhandled panic follows the ordinary panic
rules; the event model does not introduce a separate exception mechanism.
Payload Ownership
Default synchronous publication borrows the payload for the duration of dispatch. Handlers do not take ownership simply because they receive the event notification.
This matters for move-only values: a single publication to multiple subscribers must not implicitly clone a move-only payload. Any future ownership-transferring event policy would need its own explicit rules.
Reentrancy and Mutation During Dispatch
A handler may call methods on the publisher and may cause nested publication. Nested publication is permitted unless a selected event policy explicitly forbids it.
Subscription changes during dispatch follow these rules:
- a handler may close its own subscription or another subscription it owns;
- a subscription removed before its dispatch turn is not invoked;
- a subscription added during publication does not participate until the next publication.
The runtime must preserve memory safety and subscription-list integrity through reentrant and mutation-heavy dispatch.
Explicit Event Storage
Code that needs explicit storage control may separate storage from the public event member:
type Button struct {
buttonPressedStorage: EventStorage[ButtonPressData, 8]
}
impl Button {
event ButtonPressed using buttonPressedStorage
}
This division is important:
EventStorage
layout, capacity, ownership and storage policy
event member
public name, access policy and publication rights
The short Event[T, Capacity] form is conceptually equivalent to compiler-generated
fixed storage plus an event view. Generated storage remains part of layout and ABI reporting
and must not hide dynamic allocation.
impl event ... using ... { ... } policy
block, but explicitly states that the exact policy-block syntax is still open. This manual
therefore does not present that syntax as final Sec syntax.
Visibility and Interfaces
Normal visibility controls whether external code can observe and subscribe to an event. Visibility does not grant publication rights.
An interface may require an observable event member:
interface PressSource {
event ButtonPressed[ButtonPressData]
}
This requirement grants observation/subscription semantics, not permission to publish. The current rulebook also states that an explicit mechanism for external publication remains to be designed.
Copy, Move and Address Stability
Event storage is not implicitly copyable. Copying an event-owning value must never duplicate active subscriptions.
Moving an event-owning value is valid only when the selected storage representation and all active subscriptions remain valid. Inline storage may therefore create an address-stability requirement after subscriptions exist.
The compiler may reject later moves, require stable storage or use an address-independent slot representation. Whichever strategy is selected must be visible to ownership, lifetime and escape analysis.
Allocation
The default event policy performs no hidden allocation. Its subscriber slots are fixed-capacity inline storage owned by the containing value.
An allocation-capable event backend, if used, must be selected explicitly and obey the normal allocation, arena, failure and target-profile rules. Reaching default capacity must never silently switch to allocation.
Tasks, Threads and Thread Safety
The default event is not implicitly thread-safe. Concurrent publication, subscription or unsubscription requires external synchronization or a future explicitly thread-safe policy.
Events may be used from tasks and threads when the selected policy permits it. Because default dispatch is synchronous, handlers execute in the publishing execution context. They do not automatically run on the execution entity that originally subscribed.
Cross-context delivery should therefore use an explicit channel, dispatcher or adapter rather than relying on hidden event scheduling.
Interrupts
Ordinary event publication is not ISR-safe. An ISR must not invoke arbitrary event handlers, allocate, block or acquire ordinary mutexes simply because an event exists.
ISR code should instead use an ISR-safe channel, bounded notification mechanism, atomic state or deferred work that later publishes the software event in an ordinary execution context.
Events Compared with Other Primitives
Events and channels
Event[T]
One publication fans out borrowed data to zero or more handlers.
Default dispatch is synchronous.
Channel[T]
One committed message transfers ownership to one receiver.
Delivery may be buffered and asynchronous.
Events and completion
Completion is one-shot terminal readiness. An event may publish repeatedly. Task, thread, process, DMA and I/O completion are therefore not ordinary events, although adapters may expose notifications as events.
Events and interrupts
A hardware interrupt is an asynchronous control transfer to an ISR. An event is a software publish/subscribe object. The two are intentionally different abstractions.
Compiler Model
The current compiler recognizes these event-related identities:
Event[T]
Event[T, Capacity]
EventStorage[T, Capacity]
Subscription
EventSubscribeResult
Semantic analysis must reason about at least:
- payload type and fixed capacity;
- storage backend and owner type;
- publication rights and visibility;
- subscription ownership and handler lifetime;
- captures and address stability;
- dispatch order, reentrancy and mutation during dispatch;
- thread-safety and allocation policy;
- copy/move restrictions and target support.
The current rulebook also specifies that Semantic IR preserve event storage creation, exposure, subscription, unsubscription, publication, dispatch and subscription lifecycle rather than immediately erasing those semantics into arbitrary calls.
Diagnostics
Representative diagnostics from the current model include:
event capacity must be greater than zero
event ButtonPressed has reached its subscriber capacity of 4
event ButtonPressed may only be published by Button
subscription handler captures value local that does not outlive the subscription
event-owning value button cannot be moved while subscriptions are active
event publication is not thread-safe under the selected policy
event publication is not permitted in an interrupt routine
copying Button would duplicate event storage and subscriptions
Diagnostics should distinguish parse, semantic, ownership, concurrency and target-profile failures rather than collapsing all event misuse into one generic message.
Implementation Status
Current compiler support includes:
- short-form
Event[T]andEvent[T, Capacity]fields; - explicit
EventStorage[T, Capacity]fields; impl event ... using ...exposure;- interface event requirements;
- compiler-known
Event,EventStorage,SubscriptionandEventSubscribeResultidentities; - default capacity four and positive-capacity validation;
- event ownership metadata on struct types;
- event interface conformance checks;
Publish(payload)payload checking and owner-only publication diagnostics;Subscribe(handler)checking forfn(T) void;- compiler-known
Subscription.Close().
Not yet implemented according to the current rulebook:
- runtime subscriber storage and dispatch;
- active subscription tracking;
- subscription-specific capture lifetime analysis;
- address-stability enforcement after subscription;
- thread-safe and ISR-safe event policies;
- policy blocks after
impl event ... using ...; - Semantic IR lowering for event operations.
Best Practices
- Use events for synchronous notification fan-out, not for ownership-transfer queues.
- Keep event storage in the owning type so layout and lifetime remain explicit.
- Publish from the owning implementation unless a future explicit capability says otherwise.
- Keep subscriptions alive for exactly as long as their handlers should remain registered.
- Choose an explicit capacity when four subscriptions are not enough.
- Do not rely on hidden allocation when capacity is exhausted.
- Remember that default dispatch runs handlers synchronously in the publisher's execution context.
- Do not assume default events are thread-safe or ISR-safe.
- Use channels or explicit dispatchers when delivery must cross execution contexts.
- Be cautious when moving an event-owning value after active subscriptions exist.
Summary
Event[T]provides typed publish/subscribe notification.- Instance event storage belongs to the owning type.
- The default subscription capacity is four.
- Default dispatch is synchronous and follows subscription order.
- Default publication is restricted to the owning
impl. - Visibility may grant subscription without granting publication.
- A successful subscription owns a move-only
Subscription. - Closing or destroying a subscription unregisters its handler.
- Default dispatch borrows the payload; it does not clone move-only values for subscribers.
- Default events perform no hidden allocation.
- Default events are not implicitly thread-safe or ISR-safe.
- Events and channels serve different purposes: fan-out notification versus one-receiver ownership transfer.
- The current compiler already understands much of the event source model, but runtime dispatch and several safety analyses remain incomplete.
- This chapter is provisional because the underlying Events rulebook is expected to be revised.