Frame QuickStart
A dense syntax reference. For a tutorial walkthrough see Getting Started. For full semantics see the Language Reference. For runnable examples see the Cookbook.
Where each doc fits
| Doc | Use when… |
|---|---|
| frame_quickstart.md (this) | you need to look up syntax at a glance |
| frame_getting_started.md | learning Frame for the first time |
| frame_language.md | resolving a semantics question, reading error codes, checking per-target behavior |
| frame_cookbook.md | “is there a recipe that does X” |
| frame_runtime.md | debugging generated code, understanding the dispatch model |
File skeleton
<prolog native code> # optional
@@[target("python_3")] # required, exactly once
@@[persist] # optional — see Persistence
@@system Name (params)? : Base?, Base? {
operations: ... # all sections optional, but in this order:
interface: ... # operations → interface → machine → actions → domain
machine: ... # (E113 if out of order)
actions: ...
domain: ...
}
<epilog native code> # optional
Section order (mandatory)
operations → interface → machine → actions → domain
| Section | Public? | State-aware? | Purpose |
|---|---|---|---|
operations |
yes | no (bypasses state machine) | utility methods, test hooks, static functions |
interface |
yes | yes (routes through dispatch) | the system’s events |
machine |
— | yes | states, state vars, handlers |
actions |
no (private) | no (no Frame statements allowed) | private helpers called from handlers |
domain |
— | — | instance fields |
States
$StateName { # first state = start state
$.var1: T = init # state var (initializer REQUIRED)
$.var2: T = init
$>() { ... } # enter handler
$>(arg: T) { ... } # enter with enter_args
<$() { ... } # exit handler
<$(arg: T) { ... } # exit with exit_args
eventName(args): ret { ... } # event handler
}
$Child => $Parent { # HSM: Child inherits from Parent
...
=> $^ # trailing default forward: unhandled → parent
}
- State variables reset on normal transition
-> $State, preserved on-> pop$. - Access state vars with
$.name; access domain vars withself.name/this.name/ per-target.
Interface declarations
interface:
event() # no params, no return
event(a: T, b: U) # typed params (types are opaque strings)
event(): ret # return type, no default
event(): ret = "default" # return type with default value
async event() # async variant
Frame statements (the 7 constructs)
| Form | Effect |
|---|---|
-> $State |
transition |
-> $State(args) |
transition with state args |
-> (enter) $State |
transition with enter args |
(exit) -> $State |
transition with exit args |
(exit) -> (enter) $State(state) |
full-form transition |
-> "label" $State |
labeled transition (for diagrams) |
-> (enter) "label" $State |
label + enter args |
-> => $State |
transition with event forwarding (re-dispatch current event) |
-> pop$ |
transition to popped state |
-> (enter) pop$ |
pop with fresh enter args (decorated pop) |
(exit) -> pop$ |
pop with exit args |
-> => pop$ |
pop with event forwarding |
=> $^ |
forward current event to parent state |
push$ |
save current compartment onto state stack |
pop$ |
pop (rarely used bare — usually -> pop$) |
$.varName = expr |
write state variable |
$.varName |
read state variable |
Every transition generates an implicit return — code after -> is unreachable (E400).
@@ context accessors
All use colon-then-namespace, dot for fields:
| Syntax | Meaning |
|---|---|
@@:(expr) |
set return value (concise) |
@@:return = expr |
set return value (long form) |
@@:return(expr) |
set return value AND exit handler |
@@:return |
read current return value |
@@:params.x |
interface parameter x |
@@:event |
current interface method name |
@@:data.key |
call-scoped data (per-dispatch) |
@@:self.method(args) |
reentrant self-call (method must be in interface: — E601) |
@@:system.state |
current state name (read-only string, no $) |
return expr is native — does NOT set the return value (W415). Use @@:(expr) / @@:return = expr / @@:return(expr).
HSM (Hierarchical State Machines)
$Parent { # can exist without ever being a direct transition target
shared_event() { ... }
}
$Child => $Parent { # Child inherits Parent's handlers via => $^
specific_event() { ... }
specific_event_with_forward() {
log("Child processing")
=> $^ # forward to parent after local work
}
=> $^ # trailing: unhandled events go to parent
}
V4 semantics: unhandled events are IGNORED unless => $^ explicitly forwards. No automatic inheritance.
State stack (pushdown)
push$ # save current compartment (by reference!)
push$ -> $State # usual: save + transition to new compartment
-> pop$ # pop and transition to saved state
(exit_args) -> (enter_args) => pop$ # decorated pop: exit args + fresh enter args + forward event
| Transition type | State variables |
|---|---|
-> $State |
reset to initial values |
-> pop$ |
preserved from the popped compartment |
System parameters (3 groups, in this order in the header)
@@system Name ( $(state_args), $>(enter_args), domain_args ) { ... }
| Sigil | Lands in | Matched by start state |
|---|---|---|
$(name: T) |
compartment.state_args (positional) |
$Start(name: T) { ... } |
$>(name: T) |
compartment.enter_args (positional) |
$Start { $>(name: T) { ... } } |
bare name: T |
constructor arg; used in domain init | domain: name: T = name |
Each param body: name, name: T, or name: T = default.
Instantiation (@@SystemName)
@@Name() # no params
@@Name(42) # one domain param
@@Name("foo", 3) # multiple positional domain params
@@Name(name="foo", size=3) # named domain params
@@Name($(7)) # state arg
@@Name($>("ready")) # enter arg
@@Name($(7), $>("ready"), "alice") # all three: state, enter, domain
@@Name($(x=7), $>(msg="hi"), n="a") # all three, named form
Within one call: don’t mix positional and named. Defaults are substituted at the call site.
Known framec bug:
$(...)at call sites inside handler bodies is not expanded. Use a bare domain param as a workaround until fixed.
Persistence
A persisted system declares three system-level attributes:
@@[persist(str)] # blob type
@@[save(save_state)] # save method name
@@[load(restore_state)] # load method name
@@system Foo {
...
}
| Method | Kind | Behavior |
|---|---|---|
save_state() |
instance | returns the serialized blob |
restore_state(data) |
instance | mutates self from blob |
Bare @@[persist] (no save/load names) is rejected with
E814. The legacy operations: @@[save] foo() form is
rejected with E819 at framec 4.1.0+; the codemod at
scripts/migrate_rfc0015.py rewrites old fixtures.
The names are yours to pick (pickle / unpickle, snapshot
/ restore, etc.). Load is two-step: s = @@Foo() then
s.restore_state(data).
Restore does NOT invoke $>. The state is being restored, not entered.
Quiescent contract — E700. Calling save_state() from inside
a handler is a contract violation. The runtime errors with
E700: system not quiescent (per-backend mechanism: throw, panic,
abort, or push_error). Only call save_state between events.
Nested @@SystemName fields persist recursively. All 17
backends; each child’s state embeds in the parent’s blob.
Python uses pickle. Untrusted-source blobs run arbitrary code
on restore_state. Don’t unpickle data you didn’t write yourself
without validation. JSON migration for Python is in RFC-0012,
deferred pending customer feedback.
Async
- Prefix
asyncon any interface method / action / operation. - If any interface method is
async, the whole dispatch chain becomes async. - Two-phase init:
s = @@System()thenawait s.init()(Swift:initAsync()).
| Target | Async | Notes |
|---|---|---|
| python_3, typescript, javascript, rust, dart, gdscript | yes | standard async/await (gdscript: bare await) |
| kotlin | yes | suspend fun, no await keyword on suspend→suspend calls |
| swift | yes | initAsync() is the async entry point (not init) |
| csharp | yes | async Task<T> |
| java | yes | CompletableFuture<T> on public interface only |
| cpp_23 | yes | FrameTask<T> coroutine; needs -std=c++23 |
| c, go, php, ruby, lua, erlang | no | async is a framec error |
Types
Frame has no type system. Types and initializer expressions are opaque strings passed through verbatim to the target language.
- Write native type names:
int,str,Vec<i32>,std::string, etc. - Portable init literals:
"",0,false,[],{} - Non-portable init (target-specific constructors) must be written as native code, not through Frame’s normalizer.
- Dynamic targets (Python, JS, Ruby, Lua, Erlang, PHP): type is optional.
- Static targets that zero-init (C, C++, Go): init is optional (E605 requires a type though).
Target languages
python_3 typescript javascript rust
c cpp_23 java csharp
go php kotlin swift
ruby erlang lua dart
gdscript graphviz
Set via @@[target("<id>")] (required, exactly once) or CLI -l <lang>. graphviz emits DOT source for state-diagram rendering; pipe through dot -Tsvg.
Visibility
| Element | Default | Override |
|---|---|---|
@@system Foo |
public (public class, export class, pub struct, etc.) |
@@system private Foo |
| interface methods | public | — (always public) |
| operations | public | — (always public) |
| actions, handlers | private | — (always private) |
@@system public Foo is an error (redundant). private is an error on targets without class-level visibility (Python, Ruby, Lua, C, GDScript, Erlang).
const domain fields
domain:
const max_retries: int = 3 # immutable after construction
const threshold: int = threshold # initialized from system param
counter: int = 0 # mutable
Assignment to a const field in a handler body is E615. Per-target rendering: final (Java/Dart/Kotlin), readonly (C#/TS), const (C++), let (Swift); comment-only marker where the target doesn’t enforce immutability.
Common idioms
| Idiom | Shape |
|---|---|
| Kernel-loop chain (recipe 31, 110) | $> reads data, decides, queues next ->; whole chain runs inside one interface call |
| Self-replay (recipe 53) | -> $Start then @@:self.feed(ch) — re-dispatches the byte to the new state |
| Retry via re-entry (recipe 24) | -> $SameState re-runs $> with reset state vars |
| Safety overlay (recipe 49) | HSM parent holds e_stop, fault handlers; all operational children inherit |
| Interlock (recipe 60) | Omit the handler in the state where the capability shouldn’t exist — silent no-op |
| Transient decision state (recipes 5, 31, 110) | State with only a $> handler that branches on data captured at entry |
| Parent-callback (recipes 28, 48, 109) | Child system calls self.parent.method(...) to report results |
| Oracle specialist (recipe 110, Parsers essay) | Coordinator calls specialist’s interface method; specialist runs its own FSM and returns a verdict |
| State-as-gate (recipes 60, 81, 83, 105) | Reach-by-construction: no handler means the capability literally doesn’t exist in that state |
Error codes (the ones you’ll actually hit)
| Code | Meaning | Common cause |
|---|---|---|
| E113 | section order | interface: declared before operations: |
| E116 | duplicate state name | two $Name { ... } blocks |
| E400 | unreachable code | code after -> |
| E401 | Frame statement in action / operation | -> $State inside actions: body |
| E402 | transition to undefined state | typo in -> $Nmae |
| E403 | => $^ in state without parent |
forward from a non-HSM state |
| E405 | parameter arity mismatch | transition to $S(arg: T) without passing arg |
| E410 | duplicate state variable | two $.x: in one state |
| E413 | HSM cycle | $A => $B and $B => $A |
| E601 | @@:self.X() method not in interface |
X is in actions: or operations: |
| E602 | @@:self.X() arg count mismatch |
interface has 2 params, call passes 3 |
| E603 | bare @@:self |
must be @@:self.method(args) |
| E604 | bare @@:system |
must be @@:system.state (or other member) |
| E605 | static target, no type on domain field | add : T in Rust/C/C++/Go |
| E613 | domain field shadows system param | pick different names |
| E615 | assigning to const field |
remove const or drop the assignment |
| W414 | unreachable state | state with no incoming transitions |
| W415 | handler return value lost | you wrote return expr; use @@:(expr) |
| W601 | self-call return not captured | @@:self.X() has a return type but result is discarded |
CLI quick reference
framec source.fpy # compile to target declared via @@[target(...)]
framec source.fpy -l rust # override target
framec source.fpy -l graphviz | dot -Tsvg -o diagram.svg
framec source.fpy -o out.py # write output to file
cargo test # run framec's 370 unit tests
cargo clippy -- -D warnings # lint
cargo fmt --check # format check
60-second starter
@@[target("python_3")]
@@system Turnstile {
interface:
coin()
push(): str = "blocked"
machine:
$Locked {
coin() { -> $Unlocked }
push(): str { @@:("locked — insert coin") }
}
$Unlocked {
coin() { }
push(): str {
@@:("welcome")
-> $Locked
}
}
}
if __name__ == '__main__':
t = @@Turnstile()
print(t.push()) # "locked — insert coin"
t.coin()
print(t.push()) # "welcome"
framec turnstile.fpy > turnstile.py && python3 turnstile.py