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 (@@:selfportable 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:
- Visibility to tooling. A guard buried in native
ifis 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; nativeifdemotes them to implementation. - Clause selection. One event, several reactions chosen by condition, is
statechart-idiomatic (and Erlang-idiomatic). Today it must be hand-rolled
as an
if/elseladder inside a single handler. - 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-
ifconditions, 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’sexpand_self_in_bodymachinery, 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 nativeif/whenin 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;whenreads 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
guardstatement (early-exit in handler bodies): in scope here, separate RFC, or rejected as native-ifterritory?
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.