Summary

Explore a static modifier on states that guarantees a single shared compartment instance across all transitions to that state. Currently every -> $State creates a fresh compartment, reinitializing state variables. A static state would reuse the same compartment, preserving state variables across transitions.

Motivation

Several patterns want “return to a state and find it how I left it”:

  1. Dashboard state — user navigates away and back; dashboard should remember scroll position, filter settings, loaded data. Currently requires domain variables or push/pop.

  2. Configuration state — settings accumulated over time shouldn’t reset when the system transitions through other states and returns.

  3. Connection pool — a state that holds open connections shouldn’t reinitialize them on every entry.

Today the workarounds are:

  • Domain variables — move everything out of state scope into domain. Loses the encapsulation benefit of state variables.
  • Push/pop — save the compartment on the stack. Works but is a stack discipline (LIFO), not random-access. Can’t “go back to $Dashboard” from anywhere without explicit push management.

Design Space

What “static” means for a state

A static state has exactly one compartment instance per system lifetime. It’s created once (at system construction or on first entry) and reused on every subsequent -> $StaticState.

@@system App {
    machine:
        static $Dashboard {
            $.scroll_pos: int = 0
            $.filters: list = []
            // These survive across transitions away and back
        }
        $Detail {
            view(id: int) { ... }
            back() { -> $Dashboard }  // reuses existing compartment
        }
}

Enter handler semantics

Question: Does $> fire on every entry, or only on first entry?

Option A: Always fire. $> fires every time the system transitions to the static state. This is consistent with the current model where $> always fires. The difference is that state variables aren’t reinitialized — they retain their values from the last exit.

Option B: Fire only on first entry. $> fires once when the compartment is created. Subsequent entries skip it. This matches the intuition of “static initialization” but breaks the enter/exit lifecycle contract.

Option C: Two handlers. $> fires on every entry (for refresh logic). A new $>> fires only on first creation (for initialization). This is the most expressive but adds complexity.

Recommendation: Option A. Always fire $>. State variables persist, but the enter handler still runs — it can read the persisted state and act accordingly (e.g., refresh data if stale, update UI). This is the least surprising behavior and doesn’t break the lifecycle contract.

Enter args semantics

Question: What happens to enter args on a static state?

-> (data) $Dashboard passes enter args. On a fresh compartment, these populate enter_args and the $> handler reads them. On a REUSED compartment, what happens?

Option A: Replace. The caller’s enter args replace the compartment’s enter_args. The $> handler sees the new values. This matches normal transition semantics and is consistent with RFC-0008’s replace-not-merge design for pop transitions.

Option B: Ignore. Enter args are silently dropped on reentry to a static state. The $> handler sees the original enter args from first creation.

Option C: Error. Passing enter args to a static state is a compile error (E6xx). The caller must communicate via domain variables or interface methods.

Recommendation: Option A. Replace semantics. The caller explicitly chose to pass args — honor them. The static state’s $> handler should handle both “first entry with initial data” and “reentry with updated data” cases.

State args semantics

Question: Can a static state have state params ($Dashboard(config: dict))?

State params ($State(x: int)) populate state_args on the compartment. For static states, the compartment is reused. Should state_args be re-populated on each transition?

Option A: Populate once. State args are set on first creation only. Subsequent -> $Dashboard(new_config) is an error — the compartment already has its state_args.

Option B: Replace. State args are replaced on every entry, same as enter args.

Option C: No state params on static states. Compile error if a static state declares params. State identity is fixed; parameterization implies fresh instances.

Recommendation: Option C. Static states should not have state params. The purpose of static is “one instance, persistent identity.” Parameterized states are the opposite — “fresh instance per transition, configured by params.” If you need to pass configuration to a static state, use enter args (which replace on each entry) or domain variables.

Exit handler semantics

<$ fires on every exit, same as today. No change needed — the compartment persists but the exit handler still runs for cleanup/bookkeeping.

Compartment lifecycle

Creation: The static state’s compartment is created:

  • Eagerly at system construction (alongside the start state’s compartment), OR
  • Lazily on first transition to the state.

Recommendation: Lazy. Eager creation adds complexity (multiple compartments at construction time) and may create unused compartments. Lazy is simpler and matches the “first entry initializes” mental model.

Storage: The static compartment is stored on the system instance (alongside __compartment and _state_stack). Proposed field: __static_compartments: HashMap<String, Compartment>.

Persistence (@@persist)

Question: How does save/restore handle static compartments?

The static compartments must be serialized alongside __compartment and _state_stack. The restore path must reconstruct them and place them in __static_compartments.

Serialization format:

{
    "_compartment": { ... },
    "_state_stack": [ ... ],
    "_static_compartments": {
        "Dashboard": { "state": "Dashboard", "state_vars": { "scroll_pos": 42 }, ... },
        "Settings": { "state": "Settings", "state_vars": { ... }, ... }
    },
    "domain_var": "value"
}

Restore: Static compartments are deserialized into __static_compartments. If the current state IS a static state, __compartment points to the same object as the corresponding entry in __static_compartments.

Cleanup / GC

Static compartments live for the system’s lifetime. They’re never garbage collected. This is by design — “static” means “always available.” If a static state holds resources (connections, file handles), the system’s destructor or a dedicated cleanup() operation should release them.

Concern: A system with many static states accumulates compartments. Each holds state_vars, enter_args, exit_args. For most use cases this is negligible. For resource-heavy cases, a reset() method on the static state could re-initialize its compartment (effectively making it non-static for one transition).

HSM interaction

Question: Can static states participate in HSM hierarchies?

A static state with a parent: static $Dashboard => $App { ... }. The parent_compartment field on the static compartment points to… what? The parent may have been re-created since the static compartment was last active.

Option A: No HSM for static states. Compile error if a static state declares a parent. This avoids the parent-compartment staleness problem.

Option B: Re-link parent on entry. Each time the system transitions to the static state, update parent_compartment to the current parent’s compartment. This keeps the HSM chain fresh.

Recommendation: Option A for now. HSM + static introduces complexity (stale parent chains, forwarding to wrong compartments). Revisit if a compelling use case emerges.

Push/pop interaction

Question: Can you push$ a static state?

push$ saves a reference to the current compartment on the stack. For static states, the compartment is shared — pushing it saves the same reference that __static_compartments holds. Popping it back restores the same object. This is consistent but may surprise users who expect push/pop to create snapshots.

Recommendation: Allow push/pop. The reference semantics are the same as bare push$ (no transition) on non-static states — the stack entry and current compartment are the same object. Document that static + push$ does NOT create a snapshot.

Syntax

@@system App {
    machine:
        $Init { ... }

        static $Dashboard {
            $.scroll_pos: int = 0
            $>() { ... }
            <$() { ... }
        }

        static $Settings {
            $.theme: str = "light"
        }
}

The static keyword before the state name. Same keyword used for static operations — consistent language surface.

Implementation sketch

Parser

  • Recognize static before $StateName in the machine section.
  • Set StateAst.is_static: bool.

Validator

  • E6xx: static state with state params → error.
  • E6xx: static state with HSM parent → error (for now).

Codegen — system struct

  • Add __static_compartments field (HashMap or per-state fields).

Codegen — transition

  • When target is a static state: look up compartment from __static_compartments instead of creating a new one.
  • If not yet created (lazy): create, store, and use.
  • Replace enter_args if caller provided them.
  • Do NOT reinitialize state variables.

Codegen — persistence

  • Serialize __static_compartments alongside existing fields.
  • Deserialize on restore.

Open questions

  1. Keyword reuse. static already means “no self access” on operations. For states, it means “single instance.” Different semantics, same keyword. Is this confusing?

  2. Start state as static. Can the start state be static? Probably yes — it’s created at construction (eager for start state, lazy for others). No special case needed.

  3. Transition TO static from static. If the current state is static and we transition to another static state, the first one’s compartment stays in __static_compartments. The $< handler fires. On return, the $> handler fires with preserved state variables. This is correct but complex. Document clearly.

  4. Thread safety. If Frame systems are used in multi-threaded contexts, shared compartments need synchronization. Out of scope for this RFC — Frame systems are single-threaded by design.