RFC-0032: Remove @@codegen { ... } — auto-inference is the path

  • Status: Accepted
  • Author: Mark Truluck mark.truluck@cogiton.com
  • Created: 2026-05-18
  • Extends: RFC-0013 — annotation syntax (normalized @@persist@@[persist] and @@target@@[target(...)]; left @@codegen alone as a block construct).

Summary

Remove the @@codegen { ... } directive from the Frame language. Its single config knob (frame_event: on | off) is redundant with the framepiler’s auto-inference and was, in any case, declared at the wrong granularity for multi-system files. The directive is hard-cut to E824 at module scope. The framepiler emits the per-system <SystemName>FrameEvent / <SystemName>FrameContext classes whenever a feature requires them — exactly as it already did when @@codegen was absent. No user-facing replacement is provided.

Motivation

RFC-0013 normalized Frame’s pre-1.0 mix of directive forms onto the bracketed @@[...] attribute syntax — @@persist@@[persist], @@target python_3@@[target("python_3")]. It explicitly left @@codegen { ... } alone, on the rationale that a block of key: value, ... config was structurally different from an attribute. That rationale ages poorly:

  1. The block has exactly one key. Per frame_language.md, @@codegen { ... } accepts a single configuration: frame_event: on | off. There has never been a second key, and none is planned.
  2. The single key is auto-inferred. The framepiler enables frame_event automatically whenever a feature that requires it appears: enter/exit parameters, event forwarding, @@:return, or interface return values. The user-facing flag is a manual override of a decision the compiler already makes correctly.
  3. The flag is at the wrong granularity. @@codegen { ... } declares at module scope, but FrameEvent / FrameContext / Compartment classes are emitted per system. A file with two systems gets OuterFrameEvent and InnerFrameEvent, with independent codegen decisions. The module-scoped flag forces a uniform answer where the actual codegen is per-system.
  4. The flag is already a no-op in practice. generate_frame_ event_class in framec/src/frame_c/compiler/codegen/runtime.rs returns Some(class) for every backend except Rust, unconditionally. There is no branch on the parsed @@codegen value. Setting frame_event: off does not suppress anything; setting frame_event: on does not enable anything new.
  5. Zero fixtures use it. A sweep of framec-test-env shows zero .f<ext> fixtures contain @@codegen. Removing it does not require a corpus migration. The only mentions are in documentation, which is fixable.

The cleanest answer is: delete the directive entirely. Auto- inference is the path. Match the trajectory established by RFC-0024 (remove @@import) — when the framepiler can correctly infer or the host language already handles a concern, remove the Frame-side directive.

The contract

The key words MUST, MUST NOT, SHOULD, SHOULD NOT, MAY are to be interpreted as in RFC 2119.

Syntax

The @@codegen { ... } directive is removed from the language. A Frame source file containing @@codegen at module scope is a parse error (E824). The compiler MUST emit a migration pointer in the error message: “Delete the directive — the framepiler auto-enables frame_event whenever a feature that requires it appears.” See RFC-0032.

Per-system codegen unchanged

The framepiler MUST continue to emit <SystemName>FrameEvent, <SystemName>FrameContext, <SystemName>Compartment classes whenever the per-system feature surface requires them. This is the existing behavior — RFC-0032 changes only the directive’s syntactic surface, not the codegen output.

User responsibility

The user SHOULD NOT attempt to suppress or force-enable frame_event emission. The framepiler’s auto-inference is the contract. If a per-system override ever becomes necessary, a future RFC may introduce a system-scoped attribute (e.g. @@[codegen(frame_event: on)] @@system Foo { ... }) that lives at the correct granularity. This RFC does not propose such an attribute — none is needed today.

Examples

Before (4.1.x and earlier)

@@[target("python_3")]
@@codegen { frame_event: on }

@@system Counter {
    interface:
        bump()
    machine:
        $Active {
            bump() { self.n = self.n + 1 }
        }
    domain:
        n: int = 0
}

After (4.2.0+)

@@[target("python_3")]

@@system Counter {
    interface:
        bump()
    machine:
        $Active {
            bump() { self.n = self.n + 1 }
        }
    domain:
        n: int = 0
}

The generated code is byte-identical: CounterFrameEvent, CounterFrameContext, and CounterCompartment classes are emitted whether or not the directive was present. The directive was already a no-op.

Multi-system case — why module scope was wrong

@@[target("python_3")]

@@[main]
@@system Outer {
    interface:
        run(): int
    machine:
        $A { run(): int { @@:(42) } }
}

@@system Inner {
    interface:
        hop(): int
    machine:
        $X { hop(): int { @@:(7) } }
}

The framepiler emits independent per-system class sets: OuterFrameEvent / OuterFrameContext / OuterCompartment / Outer and InnerFrameEvent / InnerFrameContext / InnerCompartment / Inner. Each system’s decision to emit those helpers is taken from that system’s own feature usage. A module-scoped @@codegen { frame_event: on } directive would have forced both, even if only one needed it (and forced it off would have broken whichever system needed it). The granularity mismatch is the structural reason the directive cannot work correctly even if the auto-inference weren’t already perfect.

Alternatives

A. Keep @@codegen { ... } as a block-form directive

Status quo. Justifies it as “reserved for future code-generation config knobs.”

Rejected. Three independent reasons: (1) the directive is at the wrong granularity; (2) the only knob it controls is auto-inferred; (3) no second knob has materialized in the years since the directive was introduced. “Reserved for future use” is a smell in a pre-1.0 language with a hard-cut migration culture.

B. Migrate to @@[codegen(frame_event: on)] attribute form

Wrap the existing single knob in the established attribute grammar. Matches RFC-0013’s normalization trajectory.

Rejected. Preserves both the granularity bug (still module-scoped) and the redundancy with auto-inference. It would be uniform-but-pointless: the user shouldn’t be setting this knob at all. Removing entirely is honest about the directive’s lack of purpose.

C. Migrate to system-scoped @@[codegen(frame_event: on)] @@system Foo

Attach the knob to individual systems at the correct granularity, matching @@[main] / @@[persist] patterns.

Rejected for now. This is the right shape if a per-system override ever becomes necessary. But there is no current use case: auto-inference is reliable, no fixture or user has needed to override it, and adding a per-system knob “just in case” is exactly the YAGNI trap. If the need arises post-1.0, a small RFC can re-introduce the attribute. Until then, no directive.

D. Soft deprecation with W707

Emit a deprecation warning when @@codegen is used; remove in a later release.

Rejected for pre-1.0. Frame is pre-1.0; hard cuts are the established convention (RFC-0013 hard-cut @@persist and @@target; RFC-0015 hard-cut the @@: operation-attribute form; RFC-0024 hard-cut @@import). Soft-deprecating delays the cleanup without changing the eventual outcome.

Migration

Per-source migration. For every Frame source file containing a @@codegen { ... } block, delete the block. The generated code is byte-identical without it. A codemod is unnecessary because:

  • The block is always at module scope, between @@[target(...)] and the first @@system.
  • No fixtures in the test corpus use it (verified 2026-05-18 with grep -r '@@codegen' tests/).
  • A user-side sed one-liner suffices for any external sources:

    perl -i -0pe 's/\@\@codegen\s*\{[^}]*\}\s*//g' your_file.fpy
    

    (Multi-line because the block can span lines.)

Documentation migration. Five framec docs mentioned @@codegen. All updated in the same commit that ships this RFC:

  • docs/frame_language.md — removed the ### @@codegen section, removed the Token Summary table row, removed the directive from the Source File Structure overview, removed the Complete Example block.
  • docs/frame_getting_started.md — removed the “Codegen Options” subsection.
  • docs/frame_quickstart.md — removed the @@codegen { frame_event: on } line from the source-file-structure example.
  • docs/glossary.md — removed the ### @@codegen entry.
  • docs/rfcs/frc-future.md — removed @@codegen { ... } from the module-scope example table.

Drawbacks

  • A possible future need. If a per-system override ever becomes legitimately necessary, this RFC will need a follow-up to re-introduce a system-scoped attribute. Cost: one small RFC. Benefit: the language stays smaller until the need is real.
  • Loss of a documented escape hatch. A user who hits an edge case where auto-inference is wrong loses a stated way to override it. Counter-argument: the existing override didn’t work anyway (the codegen branch was unconditional), so users weren’t actually relying on it.
  • RFC turnover. Another pre-1.0 directive retired. The combined trajectory of RFC-0013 → RFC-0024 → RFC-0032 is “Frame gets smaller by retiring everything redundant with native syntax or compiler inference.” A user reading the RFC log might see churn. Counter-argument: churn shaped by a consistent principle is not churn — it’s convergence on the endpoint.

Unresolved questions

  • Should the segmenter / pragma_scanner code that recognizes @@codegen be deleted too? The hard-cut at E824 fires before segmentation, so the PragmaKind::Codegen enum variant and the segmenter’s block-end finder for @@codegen are now dead code. Deleting them would be cleaner but isn’t urgent. Match the pattern from RFC-0024’s @@import dead-code path — defer to a follow-up cleanup.

References

  • RFC-0013 — annotation syntax + hard-cut precedent.
  • RFC-0024@@import removal; same trajectory of “framec stops doing what’s already handled.”
  • Frame language reference — module-scope directives.
  • framec/src/frame_c/compiler/pipeline/compiler.rs — E824 emission site, alongside E803, E804, E823.
  • framec/src/frame_c/compiler/codegen/runtime.rsgenerate_frame_event_class (the function that always returned Some(class), demonstrating the flag was a no-op).
  • CHANGELOG.md — release notes for 4.2.0.