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@@codegenalone 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:
- 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. - The single key is auto-inferred. The framepiler enables
frame_eventautomatically 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. - The flag is at the wrong granularity.
@@codegen { ... }declares at module scope, butFrameEvent/FrameContext/Compartmentclasses are emitted per system. A file with two systems getsOuterFrameEventandInnerFrameEvent, with independent codegen decisions. The module-scoped flag forces a uniform answer where the actual codegen is per-system. - The flag is already a no-op in practice.
generate_frame_ event_classinframec/src/frame_c/compiler/codegen/runtime.rsreturnsSome(class)for every backend except Rust, unconditionally. There is no branch on the parsed@@codegenvalue. Settingframe_event: offdoes not suppress anything; settingframe_event: ondoes not enable anything new. - Zero fixtures use it. A sweep of
framec-test-envshows 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
sedone-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### @@codegensection, 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### @@codegenentry.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
@@codegenbe deleted too? The hard-cut at E824 fires before segmentation, so thePragmaKind::Codegenenum variant and the segmenter’s block-end finder for@@codegenare now dead code. Deleting them would be cleaner but isn’t urgent. Match the pattern from RFC-0024’s@@importdead-code path — defer to a follow-up cleanup.
References
- RFC-0013 — annotation syntax + hard-cut precedent.
- RFC-0024 —
@@importremoval; 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.rs—generate_frame_event_class(the function that always returnedSome(class), demonstrating the flag was a no-op).CHANGELOG.md— release notes for 4.2.0.