RFC-0050 — Frame Statement Syntax

  • Status: Draft
  • Author: Mark Truluck
  • Scope: Defines Frame’s statement-level surface — assignment, call, conditional, and return-value statements — plus the expression grammar they use, the lvalue rules, and Frame-level comments. Applies to any Frame construct that admits statement-level code in its bodies.
  • Companion: RFC-0042 (@@fsm — the first construct to require this surface).
  • Precursor for: RFC-0042 implementation. RFC-0042’s @@fsm action bodies use the statement surface defined here.

1. Purpose

Frame today recognizes seven statement constructs inside @@system handler bodies; everything else is native code that passes through unchanged (the Oceans Model). This works for @@system because handler bodies are short and most logic happens through transitions, state variables, and the @@: context vocabulary.

@@fsm (RFC-0042) introduces action bodies that need more than seven constructs: writers need to make decisions inside actions, work with stage captures, and assign to multiple domain fields without dropping to native code mid-action. RFC-0042 §3.6 sketched a statement grammar to fill this gap.

This RFC lifts that sketch out of the @@fsm RFC and turns it into Frame’s canonical statement-level surface, available to any current or future construct that admits statement-level code. Doing so:

  • Keeps RFC-0042 focused on @@fsm semantics rather than language-wide grammar.
  • Establishes a single normative reference for if/else, expressions, comments-as-Frame-syntax, and statement separation across all constructs.
  • Allows @@system to opt into the new statement surface (gradually, where existing native passthrough is awkward) without each construct re-specifying the grammar.

2. Design commitments

  1. Strictly additive. This RFC adds new Frame-level parse paths. It does not remove or alter the existing seven Frame statements (-> $State, => $^, push$, pop$, $.varName = expr, @@:return = expr, @@:(expr)), nor change which constructs accept which subsets. Existing @@system source compiles unchanged.

  2. Statement-level only. RFC-0050 specifies statements and the expressions that appear inside them. It does not touch top-level declarations, section headers, or anything outside { ... } statement blocks.

  3. Whitespace-insensitive. Indentation is not significant. Statement separation is by ; or by whitespace placed where the previous statement is complete.

  4. No new keywords beyond if, else. Logical, comparison, and arithmetic operators reuse standard symbol tokens. The when keyword (RFC-0042 §3.5.4.1) is reserved for transition-target guards and is not a statement-level keyword.

  5. No control flow beyond if/else. No while, no for, no loop. Iteration in Frame remains a state-machine concern (transitions, self-loops in @@fsm, machine-level recursion in @@system). Adding loops would re-introduce questions about analyzability that Frame’s state-machine model deliberately resolves at the structural level.

3. Grammar

3.1 Block structure

Statements appear inside {} blocks. A block may be empty ({} is a no-op). Indentation is not significant.

block       ::= "{" statement_list? "}"
statement_list ::= statement (separator statement)* separator?
separator   ::= ";" | whitespace

A ; is always permitted and never required. A trailing ; before a } is permitted. Whitespace (including newlines) is sufficient as a separator when the previous statement is syntactically complete.

The following three forms are equivalent:

{ self.x = 1; self.y = 2 }
{ self.x = 1 self.y = 2 }
{
    self.x = 1
    self.y = 2
}

3.2 Statements

statement     ::= assignment
                | call
                | if_stmt
                | return_value
                | frame_statement       (* the existing 7 — admitted where the host construct permits *)

assignment    ::= lvalue "=" expression
lvalue        ::= ("self" ".")? identifier
                | "@@:return"
                | "$." identifier       (* state variables — only where the host construct has them *)

call          ::= identifier "(" args? ")"
args          ::= expression ("," expression)*

if_stmt       ::= "if" condition block
                  ("else" "if" condition block)*
                  ("else" block)?
condition     ::= expression            (* must have type bool *)

return_value  ::= "@@:(" expression ")"
                | "@@:return" "=" expression

The frame_statement non-terminal is each host construct’s existing statement vocabulary (e.g., -> $State in @@system machines; bare-expression @@:return assignment at a match tail in @@fsm). RFC-0050 does not redefine these; it specifies the additional statement forms each construct gains by adopting this surface.

Calls in this RFC are non-method, top-level identifier applications: declared actions, declared Frame built-ins, and host-language functions called via native passthrough. Method-style dispatch (obj.method(args)) is not added by this RFC; @@:self.method(args) and other @@:-prefixed reentrant calls remain governed by their respective specs.

3.3 Expressions

expression  ::= logical_or
logical_or  ::= logical_and ("||" logical_and)*
logical_and ::= equality ("&&" equality)*
equality    ::= relational (("==" | "!=") relational)*
relational  ::= additive (("<" | "<=" | ">" | ">=") additive)*
additive    ::= multiplicative (("+" | "-") multiplicative)*
multiplicative ::= unary (("*" | "/" | "%") unary)*
unary       ::= ("!" | "-") unary
              | primary
primary     ::= literal
              | reference
              | call
              | "(" expression ")"
              | stage_ref               (* `@@fsm` only; per RFC-0042 §3.5.2 *)
              | probe                   (* `@@:` family per host construct *)

reference   ::= "self" "." identifier
              | "$." identifier         (* host-permitting *)
              | identifier              (* only in constructor-parameter scope *)

literal     ::= bool_literal | int_literal | string_literal
bool_literal ::= "true" | "false"
int_literal  ::= [-+]?[0-9]+
string_literal ::= "\"" (any character with standard escapes) "\""

Operator precedence binds tightest at the top of the grammar (unary, multiplicative) and loosest at the bottom (logical_or). Parentheses override precedence.

3.4 Variable scope

  • self.<name> — refers to a host-construct domain field. Valid wherever the host construct exposes domain access.
  • $.<name> — state-variable access. Valid only in constructs that have state variables (e.g., @@system); @@fsm rejects this form (RFC-0042 §2 commitment 5).
  • Bare <name> — refers to a constructor parameter, and only inside a domain-field initializer expression. Bare names in any other statement position are an error (E703).
  • @@:return — refers to the current handler’s or fsm’s return slot. Read with @@:return; write with @@:return = expr or the concise @@:(expr).
  • @@:<probe> — read-only context probes per the host construct (@@:cursor, @@:fc, @@:matched, etc. in @@fsm; @@:event, @@:params.x, @@:data.key, @@:system.state in @@system).

3.5 Comments

line_comment  ::= "//" (any character except newline)*
block_comment ::= "/*" (any character)* "*/"

Line comments extend to end of line. Block comments may span multiple lines but do not nest. Both are lexically equivalent to whitespace.

Comments may appear anywhere whitespace is permitted by the grammar — between statements, between expression operands, inside parameter lists, between top-level declarations. Two exceptions:

  1. Inside string literals. Standard string lexing applies; // and /* inside "..." are literal characters.
  2. Inside multi-character tokens such as @@:, ->, :=, ==, !=, &&, ||. A comment between the characters of such a token is malformed.

For @@fsm, a third exception applies (per RFC-0042 §3.6): comments are not interpreted inside /.../ regex delimiters. That exception is @@fsm-specific and not part of this RFC’s generic comment rule.

4. Semantics

4.1 Evaluation order

Within a block, statements execute top-to-bottom. Within an expression, operators evaluate per the precedence grammar in §3.3, with left-to-right associativity for binary operators of equal precedence. Frame does not specify side-effect ordering for arguments to a call beyond left-to-right; pure expressions are the expected norm.

4.2 Type checking

  • if and else if conditions must have type bool. A non-bool condition is E706.
  • Assignment RHS must be type-compatible with the lvalue’s declared type. Mismatches are E706.
  • Comparison operators (==, !=, <, <=, >, >=) require operands of compatible types per Frame’s type system. <-family operators require numeric or string-ordered types.
  • Logical operators (&&, ||, !) require bool operands and produce bool.
  • Arithmetic operators (+, -, *, /, %) require numeric operands and produce numeric. + is not overloaded for string concatenation; use the host construct’s string built-ins (e.g., to_str per RFC-0042 §7.2 — and a future string-concat built-in TBD).

4.3 Short-circuit evaluation

&& short-circuits left-to-right: if the LHS is false, the RHS is not evaluated. || short-circuits similarly: if the LHS is true, the RHS is not evaluated. This matters when RHS expressions include calls that could be side-effecting (though calls in pure-expression contexts like when predicates already forbid side effects per RFC-0042 §3.5.4.1).

4.4 Native-code coexistence

A construct that adopts RFC-0050 keeps its native-passthrough behavior for code outside {} Frame statement blocks. The framepiler does not attempt to parse native code as Frame statements; the boundary between Frame and native code is the block-brace pair.

Specifically:

  • A handler body in @@system today is treated entirely as native code (with the seven Frame statements as the exception). When @@system adopts RFC-0050 (a future RFC; not specified here), only code inside Frame-recognized statement positions is parsed as Frame; the rest remains native passthrough.
  • An @@fsm action body is entirely Frame per RFC-0042. Native code inside an @@fsm action body is not supported in v0.1 — writers who need native interop call out via declared actions (RFC-0042 §3.7).

4.5 Frame-level vs target-level operators

The Frame operators specified in §3.3 are emitted in each target backend’s natural equivalent. For example, && becomes && in C-family languages, and in Python, andalso in Erlang. Frame does not expose target-specific operator quirks; codegen handles the mapping.

5. Errors and diagnostics

Code Meaning
E703 Read of undeclared name (extends the existing definition to cover RFC-0050 statement positions)
E704 Write to undeclared name (same extension)
E706 Type mismatch in assignment, comparison, or condition expression
E760 Statement block delimiter mismatch (missing }, etc.)
E761 Malformed expression — missing operand, unmatched paren, etc.
E762 if condition not of type bool

E703, E704, E706 are pre-existing codes; this RFC clarifies that they apply uniformly across the new statement positions. E760–E762 are new and reserved by this RFC.

6. Validation tests

The full test suite lives in framec/tests/stmt_syntax/. Each test follows the same structured format as RFC-0042’s FSM-TEST-NNN entries.

Smoke

STMT-TEST-001 — Empty block compiles. {} parses as a zero-statement block; semantically a no-op.

STMT-TEST-002 — Single assignment. { self.x = 1 } compiles; on execution, self.x becomes 1.

STMT-TEST-003 — Multi-statement, ;-separated. { self.x = 1; self.y = 2 } compiles; both fields update.

STMT-TEST-004 — Multi-statement, whitespace-separated. Same logic without ;; equivalent.

STMT-TEST-005 — Trailing ; before } permitted. { self.x = 1; } compiles.

Conditionals

STMT-TEST-010 — if/else chain. { if cond1 { … } else if cond2 { … } else { … } } compiles; the right block executes.

STMT-TEST-011 — Non-bool condition rejected. if 5 { … } produces E762.

STMT-TEST-012 — Unparenthesized condition with logical ops. if a && b || c { … } parses per precedence; no parens required.

Expressions

STMT-TEST-020 — Precedence: + binds tighter than <. if a + b < c { … } parses as if ((a + b) < c).

STMT-TEST-021 — Precedence: && binds tighter than ||. if a || b && c { … } parses as if (a || (b && c)).

STMT-TEST-022 — Parentheses override precedence. if (a || b) && c { … } parses as the user expects.

STMT-TEST-023 — Short-circuit on &&. if false && side_effect() { … } does not call side_effect.

STMT-TEST-024 — Short-circuit on ||. if true || side_effect() { … } does not call side_effect.

Lvalue and scope

STMT-TEST-030 — self.<name> assignment. { self.x = 1 } compiles where x is a declared domain field.

STMT-TEST-031 — Bare-name in initializer scope. A domain: field’s initializer x: int = initial compiles where initial is a constructor parameter.

STMT-TEST-032 — Bare-name in statement body rejected. { x = 1 } produces E703 when x is not in the (empty) statement-body bare-name scope.

STMT-TEST-033 — @@:return = expr assignment. Compiles; sets the return slot.

STMT-TEST-034 — @@:(expr) concise form. Compiles; equivalent to @@:return = expr.

Comments

STMT-TEST-040 — Line comment. // hello ignored.

STMT-TEST-041 — Block comment. /* hello */ ignored, spans multiple lines if used.

STMT-TEST-042 — Comment between statements. { self.x = 1 // note \n self.y = 2 } compiles.

STMT-TEST-043 — Comment inside a multi-character token rejected. @/* */@:return is malformed.

Type checking

STMT-TEST-050 — Type mismatch in assignment. self.count = "foo" where count: int produces E706.

STMT-TEST-051 — Type mismatch in comparison. if self.count == "foo" { … } produces E706.

STMT-TEST-052 — Logical operator non-bool operand. if self.count && true { … } where count: int produces E706.

7. Interaction with existing Frame constructs

7.1 @@system

@@system does not automatically adopt RFC-0050 statement parsing in v0.1. Existing @@system handler bodies continue to be processed as today (seven Frame statements + native passthrough). A future RFC may opt @@system into RFC-0050 statement parsing for action bodies or specific positions; that is out of scope for this RFC.

This means: an existing @@system codebase compiles unchanged after RFC-0050 ships.

7.2 @@fsm

@@fsm action bodies (RFC-0042 §3.7) admit the full RFC-0050 surface. RFC-0042 §3.6 is deleted (its content lives here); RFC-0042 cross-references this RFC.

7.3 actions: blocks

Actions inside an @@fsm actions: block use RFC-0050 statements. Actions cannot issue transitions, write to other states’ captures, or invoke side-effecting @@: writes (per RFC-0042 §3.7). Those constraints stand; RFC-0050’s syntax is a superset of what’s permitted.

7.4 Native passthrough

Native code (target-language code outside Frame-recognized statement positions) is unchanged. The framepiler does not parse it; it emits it verbatim per the Oceans Model.

8. Open extensions

  • Loops. while and for are not in v0.1. Adding them requires reconsidering analyzability (especially in @@fsm).
  • Pattern matching. A future match/case construct may be added; when is reserved for transition-target guards per RFC-0042 §3.5.4.1 and will not be overloaded.
  • String concatenation operator. + is not overloaded for strings in v0.1. Either a future operator or a Frame built-in will fill this gap.
  • Method calls. obj.method(args) is not added by this RFC. @@:self.method(args) and other @@:-prefixed forms remain governed by their host constructs.
  • @@system adoption. Opting @@system into RFC-0050 statement parsing for some or all body positions is a future RFC.

9. Implementation notes

  • Parser changes: extend the existing Frame parser to recognize the new statement forms inside any block that the host construct flags as “RFC-0050 statement scope.” @@fsm (RFC-0042) is the first such host; @@system opt-in is future work.
  • Validator changes: extend type-checking to cover the new statement positions; emit E703/E704/E706/E762 per the table in §5.
  • Codegen: each backend emits Frame operators per its target-language equivalents. The Python reference backend is straightforward; non-trivial backends (Erlang, C) get bespoke per-operator mapping.
  • Cost: roughly four weeks (parser + validator + codegen-mapping rules + tests) per the RFC-0042 execution plan §3.

End of RFC-0050.