Events

Provisional chapter: this page documents the current Sec event model so the manual remains complete and navigable. The underlying Events rulebook is an older revision and is expected to be rewritten. This chapter must therefore be reviewed and synchronized again when the new rulebook lands. Where the current rulebook explicitly leaves syntax or policy design open, this page leaves it open too.

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.

Core rule: instance-event storage belongs to the data definition of the owning type. Behavior and publication policy may be exposed through 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.

The current Events rulebook sketches an 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

This status reflects the current older Events rulebook and should be rechecked when that rulebook is replaced.

Current compiler support includes:

  • short-form Event[T] and Event[T, Capacity] fields;
  • explicit EventStorage[T, Capacity] fields;
  • impl event ... using ... exposure;
  • interface event requirements;
  • compiler-known Event, EventStorage, Subscription and EventSubscribeResult identities;
  • 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 for fn(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.