RFC-0047 — Guard syntax for Frame (PLACEHOLDER)

  • Status: Placeholder — survey + design space only; no design decided
  • Author: Mark Truluck mark.truluck@cogiton.com
  • Created: 2026-06-11
  • Builds on: RFC-0013 (@@/@@: syntax), RFC-0046 (@@:self portable self-reference; the verbatim-passthrough contract guards must honor)

The key words MUST, MUST NOT, SHOULD, and MAY are to be interpreted as described in RFC 2119.

Purpose of this placeholder

Reserve the RFC number and capture the prior-art survey + design space for a first-class guard syntax in Frame. Two questions are deliberately left open for the design phase (see Open Questions); one is already decided:

D1 (decided): the guard expression is native code. Frame owns the guard syntax (the brackets/placement and its dispatch semantics); the boolean expression inside is the target language’s, passed through verbatim — exactly the RFC-0046 contract. Portability inside a guard comes from @@:self.field / @@:self.action(), not from a Frame expression language. framec MUST NOT grow an expression compiler for guards.

Terminology note (avoid a collision)

Frame already uses “guard” internally for the self-call transition guard (RFC-0006): the caller-side if _transitioned: return; short-circuit emitted after @@:self.method(). That is a runtime codegen mechanism, not user syntax. This RFC is about user-written guards — boolean conditions that gate transitions and/or select handler clauses. The design phase SHOULD pick naming that keeps the two distinct in docs and code.

Motivation (why syntax, when native if already works)

Today a “guarded transition” is written with native control flow:

tick() {
    if (count > 3) {
        -> $Done
    }
}

This works on every backend and honors passthrough. What it cannot do:

  1. Visibility to tooling. A guard buried in native if is opaque to framec — the Graphviz backend cannot label the edge [count > 3], a future analyzer cannot see that the transition is conditional, and the documentation generator cannot render the statechart faithfully. UML-style guards are model information; native if demotes them to implementation.
  2. Clause selection. One event, several reactions chosen by condition, is statechart-idiomatic (and Erlang-idiomatic). Today it must be hand-rolled as an if/else ladder inside a single handler.
  3. Statechart fidelity. Engineers arriving from UML/SCXML/Stateflow/QP expect event [guard] / action. Frame currently has no spelling for it.

Prior-art survey

Statechart family (transition guards)

System Syntax Semantics Notes
Harel statecharts (1987) e[c]/a transition taken iff trigger e fires AND condition c holds; a is the effect the origin of the bracket notation
UML state machines trigger [guard] / effect guard is a side-effect-free boolean, evaluated at trigger time; false → transition not enabled (event may be consumed by another transition or discarded) also: choice pseudostate with [guard] on outgoing edges and a mandatory-in-practice [else]
SCXML (W3C) <transition event="e" cond="expr" target="s"/> cond is a host-datamodel expression (e.g. ECMAScript) — strong precedent for D1’s native-expression decision document order = priority; first enabled transition wins
Stateflow (MATLAB) e[c]{a} on edges guard in brackets, action in braces; MATLAB/C as expression language guards routinely call functions — side-effect freedom is convention, not enforced
QP/QM (Quantum Leaps) guards are C/C++ code on transition branches generated as if ladders shows the “guards are just native code attached to model edges” end-state
XState (JS) guard: 'isReady' — a named guard resolved from an implementation map model references the guard by name; implementation is host code interesting middle path: Frame analog would be [@@:self.is_ready()] — a named action as guard, visible to tooling AND native

Clause-selection family (one event, many guarded clauses)

Language Syntax Semantics Notes
Erlang f(X) when X > 0 -> …; and receive Msg when G -> … clauses tried top-to-bottom, first whose pattern+guard matches wins guard language is restricted (BIFs only, no user calls, no side effects) — enforceable because Erlang owns the expression language; Frame (per D1) cannot enforce this, only document it
Elixir def f(x) when x > 0 same model, same restricted guard set  
Haskell f x \| x > 0 = … equation guards, top-to-bottom  
Rust match v { P if cond => … } match-arm guards; arm skipped if guard false, later arms still tried guard is arbitrary native expression — closest to what D1 implies
Scala case x if cond => same  
Swift case let x where cond: pattern guards in switch  

Swift’s other guard (statement form)

Swift’s guard cond else { …; return } is not a dispatch guard — it is an early-exit assertion: state the happy-path precondition, bail otherwise, with the compiler enforcing that the else block exits scope. Relevant to Frame as possible handler-body prior art (a portable “bail unless” that pairs well with @@:return), but it solves a different problem from UML guards. The design phase SHOULD decide whether it is in scope at all or noted as a separate future RFC.

Where Frame sits today

  • No guard syntax; conditionals are native if + Frame transition statements.
  • The Graphviz backend renders transitions found by the scanner — it cannot see native-if conditions, so all transitions render unconditional. Guard syntax is what would make diagrams faithful.
  • HSM dispatch (=> $^) gives a natural “no clause matched” fallback: UML’s “event not consumed propagates” maps cleanly onto forwarding to the parent.

Design space (for the design phase — NOT decided)

Model A — transition guards (UML-faithful)

Guard attaches to the transition site. Sketches (syntax illustrative only):

tick() {
    -> [count > 3] $Done        // guard on the transition
    -> $Waiting                  // else-ish: next unguarded transition
}

Pros: directly renderable on diagram edges; minimal grammar. Cons: largely sugar for if around ->; semantics of fallthrough between several guarded -> in one body need careful definition (first-match? top-to-bottom?).

Model B — handler-clause selection (Erlang-faithful)

Multiple clauses for the same event in one state; guard selects the clause:

$Vending {
    insert(coin: int) [coin >= price] { -> $Dispensing }
    insert(coin: int)                  { @@:self.refund(coin) }
}

Pros: declarative one-event/many-reactions; the whole reaction (not just the transition) is gated; clause list renders naturally as multiple labeled edges. Cons: bigger grammar/dispatch change (today: one handler per event per state — arcanum, validators, and all 17 dispatch backends assume it).

Cross-cutting semantic questions (either model)

  • First-match order — textual top-to-bottom, first enabled wins (SCXML/Erlang precedent). Anything else (priority annotations) is over-design.
  • No clause enabled — discard the event, or fall through to the HSM parent (=> $^ semantics)? UML says an unconsumed event propagates; Frame’s explicit-forward philosophy may argue for discard unless the user wrote => $^.
  • [else] — supported (UML choice precedent), or is an unguarded clause the else?
  • Side-effect freedom — with native expressions (D1) framec cannot enforce it; the spec SHOULD state it as a normative authoring rule (guards MUST be side-effect-free) with the consequences documented (a guard may be evaluated and its transition still not taken).
  • Evaluation context — guards see event params, @@:self.field, @@:params.x; on which targets does the guard need the same expansion path as handler bodies (it does — reuse RFC-0046’s expand_self_in_body machinery, not a new pass)?
  • Graphviz/tooling — the scanner captures the guard text verbatim as segment metadata so the Graphviz backend can label edges [<text>]; this falls out of D1 (guard text is just a bounded native span).

Implementation ground rules (carried from RFC-0046, binding on the design)

  • The guard recognizer lives in the dogfooded FSM scanners (a context_parser.frs-style arm or a handler-signature parser extension) — no ad-hoc string matching.
  • The guard expression is an FSM-bounded native span: framec stores it verbatim, expands @@:self.* within it via the existing segment pipeline, and emits it inside the target’s native if/when in the generated dispatch. No textual post-passes.

Open questions (the design phase decides)

  • O1 — Scope: Model A (transition guards), Model B (clause selection), or both (B subsumes A in expressive power; A is closer to UML diagrams)? This RFC deliberately surveys both; the choice is the core design decision.
  • O2 — Spelling: UML brackets ([…]) vs a keyword (when …)? Brackets collide with native array/list literals only at positions the grammar controls, so likely safe; when reads better in clause position (Erlang precedent) but adds a quasi-keyword to a language that has none.
  • O3 — No-match behavior: discard vs parent-forward (see above).
  • O4 — Swift-style guard statement (early-exit in handler bodies): in scope here, separate RFC, or rejected as native-if territory?

Non-goals (settled now)

  • No Frame expression language (D1). Guards do not make framec an expression compiler on any target.
  • No runtime guard objects / named-guard registry (XState’s map): Frame’s analog is simply calling an action — [@@:self.is_ready()] — which needs no new mechanism.