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
@@fsmaction 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
@@fsmsemantics 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
@@systemto opt into the new statement surface (gradually, where existing native passthrough is awkward) without each construct re-specifying the grammar.
2. Design commitments
-
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@@systemsource compiles unchanged. -
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. -
Whitespace-insensitive. Indentation is not significant. Statement separation is by
;or by whitespace placed where the previous statement is complete. -
No new keywords beyond
if,else. Logical, comparison, and arithmetic operators reuse standard symbol tokens. Thewhenkeyword (RFC-0042 §3.5.4.1) is reserved for transition-target guards and is not a statement-level keyword. -
No control flow beyond
if/else. Nowhile, nofor, noloop. 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);@@fsmrejects 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 = expror the concise@@:(expr).@@:<probe>— read-only context probes per the host construct (@@:cursor,@@:fc,@@:matched, etc. in@@fsm;@@:event,@@:params.x,@@:data.key,@@:system.statein@@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:
- Inside string literals. Standard string lexing applies;
//and/*inside"..."are literal characters. - 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
ifandelse ifconditions must have typebool. 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_strper 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
@@systemtoday is treated entirely as native code (with the seven Frame statements as the exception). When@@systemadopts 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
@@fsmaction body is entirely Frame per RFC-0042. Native code inside an@@fsmaction 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.
whileandforare not in v0.1. Adding them requires reconsidering analyzability (especially in@@fsm). - Pattern matching. A future
match/caseconstruct may be added;whenis 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. @@systemadoption. Opting@@systeminto 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;@@systemopt-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.