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.md Issue #40 (residual edge)
  • Builds on: RFC-0025 / RFC-0025.1 (typed compartment payload — the StateContext enum 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)

  1. Identifiers beginning with __ are reserved for framec. A user-authored Frame state name MUST NOT begin with __.

  2. 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
    
  3. 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.

  4. 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 StateContext sentinel, 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 $Empty to 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.