RFC-0037 — Reserved identifier namespace: the __ prefix
- Status: Accepted (2026-05-22). Enforcement shipped as validator E115 in 4.2.1.
- Author: Mark Truluck
- Created: 2026-05-22
- Resolves:
_scratch/FRAMEC_BUGS.mdIssue #40 (residual edge) - Builds on: RFC-0025 / RFC-0025.1
(typed compartment payload — the
StateContextenum whose synthesized sentinel motivated this rule)
Summary
framec synthesizes identifiers in every backend’s emitted code —
__compartment, __prepareEnter, __kernel, __sys_<name>, the Rust
StateContext::__NoContext sentinel, and others. These names share a
namespace with identifiers derived from the user’s Frame source (state
names → enum variants / methods, params → fields, …). When a user
identifier happens to match a synthesized one, the generated code fails
to compile with an opaque target-language error far from the Frame source.
This RFC reserves the __ (double-underscore) prefix for
framec-synthesized identifiers and forbids it in user-authored Frame
identifiers, so the two namespaces are provably disjoint. Enforcement is
validator E115 (state names), which fires at the Frame layer with a
clear message instead of letting the collision reach the target compiler.
Motivation
FRAMEC_BUGS #40. The Rust {Sys}StateContext enum emits a variant per
state plus a synthesized catch-all sentinel. The sentinel was hardcoded as
Empty, so a user state $Empty produced a second Empty variant:
error[E0428]: the name `Empty` is defined multiple times
The immediate fix renamed the sentinel to __NoContext. But that only
moves the collision: a user state $__NoContext would collide again, and
the same class of bug lurks wherever framec synthesizes a name (the
__compartment field, __sys_<name> system-param storage, the
__prepareEnter method, …). Whack-a-mole renaming of each sentinel is not
a fix; the namespaces must be made disjoint by contract.
The convention already exists implicitly — every framec-synthesized
identifier across all 17 backends is __-prefixed. This RFC makes that
convention normative and enforced.
Specification (normative)
-
Identifiers beginning with
__are reserved for framec. A user-authored Frame state name MUST NOT begin with__. -
framec rejects a violating program at validation time with E115:
E115: state '$__Foo' uses the reserved `__` prefix in system 'R'; `__`-prefixed names are reserved for framec-synthesized identifiers — rename the state -
Conversely, every identifier framec synthesizes into emitted code MUST carry the
__prefix (or be otherwise provably disjoint from user-derived names), so reserving the prefix is sufficient to guarantee disjointness. -
Only the double underscore is reserved. A single leading
_(e.g.$_Idle) is not reserved — backends that dislike it handle it in their own naming layer; it does not collide with framec internals.
Scope
- Enforced now (4.2.1): state names, via E115. State names are the
highest-risk class — they map directly to type/variant/method
identifiers in the typed backends (Rust enum variants, the
StateContextsentinel, dispatch method names). - Not yet enforced: handler/event names, state/enter/exit params, state vars, domain fields, system names. These are lower-risk (most map into per-state scopes or string-keyed maps, not the top-level synthesized namespace) and no collision has been observed. Extending E115 to these classes is a mechanical follow-up if a collision is ever found; the reserved-prefix contract already covers them.
Rationale
- One rule, all backends. The check runs pre-codegen, so it protects every target uniformly rather than each backend re-discovering the collision in its own emitted code.
- Fail fast, fail clear. E115 names the offending state and the rule
at the Frame layer; the alternative is an
E0428/ redeclaration error deep in generated Rust/Java/… that the user must reverse-engineer. - Cheap for users.
__-prefixed state names are vanishingly rare in practice (state names are user-facing PascalCase like$Idle,$Running); the cost of the reservation is effectively zero.
Alternatives considered
- Per-collision sentinel renaming (status quo before this RFC). Rename each synthesized name as collisions are found. Rejected: whack-a-mole; never guarantees disjointness; each new synthesized name reopens the risk.
- Mangle colliding user names in codegen. Silently rewrite
$Emptyto e.g.Empty_. Rejected: surprising (the generated type names no longer match the Frame source), and the mangling itself can collide. - No reservation. Rejected — this is what produced #40.
Migration
None for conforming programs. A program with a $__-prefixed state name
(previously either a hard compile break à la #40 or accidental) now gets a
clear E115 at compile time; rename the state. Frame OS hit exactly this
($Empty) and had applied a $Empty → $Idle workaround; with the #40
sentinel rename that workaround is no longer required, and E115 guards the
residual $__ edge.
Non-goals
- No change to codegen for any conforming program.
- No reservation of the single-underscore prefix.
- No new surface syntax.