The effect vocabulary (ADR-0003) plus the nine trace effects
(docs/observability.md constraint 2) - one @type t() union, in this
one module, that every interpreter function emits from and every
consumer pattern-matches against, including Statifier.Session, which
drives <send>/<cancel>/<invoke>. This module defines the
vocabulary and the trace gate; it never emits an effect itself.
Every effect is {tag, payload_struct}
An effect is always a two-element tuple: a vocabulary tag, and a struct
carrying its named fields. The tag keeps case effect do {:log, log} -> ... reading the way ADR-0003 writes it and makes "is this a trace
effect?" a single match (trace?/1); the struct payload gives named
fields, dialyzer coverage, and room to grow the <send>/<send_delayed>/
<cancel>/<invoke> payloads without changing any arity. There are no
positional tuples with four or five elements anywhere in this
vocabulary.
The vocabulary
Every tag in this vocabulary is produced today: :log, :done,
:budget_exhausted, :invoke, :cancel_invoke, :autoforward,
:datamodel_change, :datamodel_init, :send, :send_delayed,
:cancel, and all nine trace effects.
Trace effects carry indexes and counters, never structs
Every effect in this vocabulary, core and trace alike, carries
macrostep/microstep/round (constraint 4, ADR-0046); trace payloads
additionally carry, wherever one names an entity, a constraint-3
identity - a state index, a t_index, a c_index - never a
%Statifier.Machine.State{}, %Statifier.Machine.Transition{}, or a
compiled content-node struct.
Tooling resolves an identity back to its node through
Statifier.Machine.at/2, transition/2, or content/2
(docs/observability.md:73-74). round is the only one of the three that
advances in a round that runs no microstep at all - a livelocked or
eventless round still stamps a fresh round on everything it emits, which
is what keeps such a trace ordered (ADR-0020).
Trace effects are ordinary list members, never a side channel
A trace effect is just another element of the same [effect] list a
microstep returns, in the same order, delivered the same way. There is no
separate trace stream to keep in sync (docs/observability.md:75-76).
The gate: trace/3
require Statifier.Effect, as: Effect
Effect.trace(machine_state, Effect.Trace.EntrySet, indexes: entry_order)expands to
if machine_state.trace do
[{:trace, Statifier.Effect.Trace.EntrySet.new(machine_state, indexes: entry_order)}]
else
[]
endTwo properties make this the gate the untraced hot path needs:
- Single evaluation. The
machine_stateargument is bound to a variable inside the quoted block before theif, so a call site passing an expression (Effect.trace(begin_microstep(ms), ...)) evaluates it exactly once. Writingunquote(machine_state)twice - once for.trace, once asnew/2's argument - would double-evaluate it; that is the bug this shape exists to avoid. - Laziness.
fieldsis spliced only inside theif'sdobranch. Whenmachine_state.traceisfalse, the gate is one boolean field read and an empty list: no payload struct is built, and thefieldsexpression is never evaluated at all - not even for its side effects.
Every trace payload module defines new/2, taking the machine_state (from
which it stamps macrostep/microstep/round) and a keyword list of its
own fields, so no call site ever repeats the counters and no call site can
forget them.
Summary
Types
The eleven core effects - the ADR-0003 set plus ADR-0019's :budget_exhausted, :cancel_invoke/:autoforward, and the two datamodel effects: :datamodel_change (a write) and :datamodel_init (the starting baseline).
Every effect this interpreter can emit - the single source of truth for the vocabulary.
The nine trace effects - the seven docs/observability.md constraint-2 rows plus InvokePass/FinalizeAutoforward for the two Appendix-D-named phase boundaries <invoke> (this bead) added.
Functions
The trace emission gate. payload_module is one of the
Statifier.Effect.Trace.* modules; fields is the keyword list passed to
its new/2 (the counters are stamped automatically, never repeated at the
call site).
Whether effect is a trace effect - a single match on the :trace tag.
Table-driven over the whole vocabulary at the call site
(test/statifier/effect_test.exs), so a trace point added without a
matching case here fails that test rather than silently miscounting.
Types
@type core() :: {:send, Statifier.Effect.Send.t()} | {:send_delayed, Statifier.Effect.SendDelayed.t()} | {:cancel, Statifier.Effect.Cancel.t()} | {:invoke, Statifier.Effect.Invoke.t()} | {:cancel_invoke, Statifier.Effect.CancelInvoke.t()} | {:autoforward, Statifier.Effect.Autoforward.t()} | {:budget_exhausted, Statifier.Effect.BudgetExhausted.t()} | {:done, Statifier.Effect.Done.t()} | {:log, Statifier.Effect.Log.t()} | {:datamodel_change, Statifier.Effect.DatamodelChange.t()} | {:datamodel_init, Statifier.Effect.DatamodelInit.t()}
The eleven core effects - the ADR-0003 set plus ADR-0019's :budget_exhausted, :cancel_invoke/:autoforward, and the two datamodel effects: :datamodel_change (a write) and :datamodel_init (the starting baseline).
Every effect this interpreter can emit - the single source of truth for the vocabulary.
@type trace() :: {:trace, Statifier.Effect.Trace.EventDequeued.t()} | {:trace, Statifier.Effect.Trace.TransitionsSelected.t()} | {:trace, Statifier.Effect.Trace.ExitSet.t()} | {:trace, Statifier.Effect.Trace.ContentExecuted.t()} | {:trace, Statifier.Effect.Trace.EntrySet.t()} | {:trace, Statifier.Effect.Trace.MacrostepStable.t()} | {:trace, Statifier.Effect.Trace.Done.t()} | {:trace, Statifier.Effect.Trace.InvokePass.t()} | {:trace, Statifier.Effect.Trace.FinalizeAutoforward.t()}
The nine trace effects - the seven docs/observability.md constraint-2 rows plus InvokePass/FinalizeAutoforward for the two Appendix-D-named phase boundaries <invoke> (this bead) added.
Functions
@spec trace( machine_state :: Macro.t(), payload_module :: Macro.t(), fields :: Macro.t() ) :: Macro.t()
The trace emission gate. payload_module is one of the
Statifier.Effect.Trace.* modules; fields is the keyword list passed to
its new/2 (the counters are stamped automatically, never repeated at the
call site).
Expands to a one-element list holding {:trace, payload} when
machine_state.trace is true, or [] with fields never evaluated when
it is false. machine_state is evaluated exactly once regardless of which
branch is taken - see the moduledoc's "Single evaluation" note.
Whether effect is a trace effect - a single match on the :trace tag.
Table-driven over the whole vocabulary at the call site
(test/statifier/effect_test.exs), so a trace point added without a
matching case here fails that test rather than silently miscounting.