RFC-0024: Remove @@import — host-language imports via Oceans Model
- Status: Accepted
- Author: Mark Truluck mark.truluck@cogiton.com
- Created: 2026-05-16
- Supersedes: RFC-0022, RFC-0022.1
Summary
Remove the @@import directive from the Frame language. Cross-file
dependencies are expressed entirely in the target host language’s
native syntax — from .counter import Counter for Python,
use crate::counter::Counter; for Rust,
const Counter = preload("res://counter.gd") for GDScript, and so
on — written by the user as
Oceans Model pass-through. framec
emits state-machine classes; the host language’s import system
resolves the names.
Motivation
RFC-0022 introduced @@import for three reasons:
- Auto-emit native imports on the fourteen file-path-resolving backends.
- Validate cross-file references at framec-compile time under
--import-mode strict. - Resolve
@@SystemName()references in handler bodies — give framec confidence that a name like@@Counter()corresponds to a real@@systemdeclaration somewhere in the dependency set.
RFC-0022.1 reduced (1) on three backends (Java, C#, Go) because their native import vocabulary requires information framec doesn’t carry (host packaging). Users on those backends already write native imports via Oceans Model pass-through.
Walking through the remaining responsibilities of @@import:
- (1) Auto-emit native imports. Convenience: saves the user
typing one native line per imported file. Users already know how
to write
from .other import Xin their target language; framec adds no value by translating from a Frame-specific spelling. - (2) Validation. Duplicates work the host compiler already
does.
javac,rustc, the Python runtime — every host catches missing imports. Strict mode just shifts the error one pipeline-step earlier. - (3) Resolve
@@SystemName(). This was the load-bearing reason. But on close inspection it isn’t: framec lowers@@Counter()using the literal name from source. The factory call shape is target-uniform per backend —Counter._create(),Counter::new(),Counter.new(),new Counter()— and only needs the identifier “Counter.” WhetherCounteris defined in the same file or imported from another is irrelevant to codegen. The host language’s import system resolves the name at host-compile time; if the user forgot to import, the host compiler errors.
Three responsibilities, three honest answers: convenience, duplicate work, target-uniform lowering that doesn’t need the directive at all. None survive scrutiny.
This RFC removes @@import and --import-mode entirely. Frame
becomes simpler; the language surface shrinks; cross-file
composition becomes whatever the host language already does.
This is the same trajectory taken for Java’s runtime imports (RFC-0022.1 + the FQ-types migration): when the host language has a working mechanism, framec gets out of the way.
The contract
The key words MUST, MUST NOT, SHOULD, SHOULD NOT, MAY are to be interpreted as in RFC 2119.
Syntax
The @@import directive is removed from the language. A Frame
source file containing @@import at module scope is a parse error
(E823). The compiler SHOULD include a migration pointer in the
error message: “Replace with the target language’s native import
syntax outside any @@system block. See RFC-0024.”
The --import-mode CLI flag is removed. The strict-mode validators
E821 (unreadable import) and E822 (no system in imported file) are
removed.
Cross-file @@SystemName() lowering
@@SystemName(args) in a handler body or domain initializer lowers
to the target’s factory call pattern using only the literal
name SystemName from the source. framec MUST NOT verify
that SystemName corresponds to a declaration anywhere in the
project. If the host language’s import system fails to resolve the
name at host-compile time, that’s the host’s error to report.
This is unchanged from current behavior for in-file
@@SystemName() — framec already lowers using just the name. The
RFC formalizes that the lowering doesn’t require declaration
knowledge.
User responsibility
The user writes native imports in their target language’s idiom,
outside any @@system block. framec emits them verbatim via
Oceans Model pass-through.
Examples
Python — two-file composition
counter.fpy:
@@[target("python_3")]
@@system Counter {
interface:
bump()
get(): int
machine:
$Active {
bump() { self.n = self.n + 1 }
get(): int { @@:(self.n) }
}
domain:
n: int = 0
}
app.fpy:
@@[target("python_3")]
from .counter import Counter
@@system App {
interface:
run()
machine:
$Active {
run() { self.c.bump() }
}
domain:
c = @@Counter()
}
Generated app.py:
from .counter import Counter
class App:
# ...
@classmethod
def _create(cls):
c = cls()
c.c = Counter._create()
# ...
@@Counter() lowers to Counter._create() — framec uses the
literal name. The user’s from .counter import Counter line lands
verbatim via Oceans Model.
Rust — same shape
app.frs:
@@[target("rust")]
use crate::counter::Counter;
@@system App {
domain:
c: Counter = @@Counter()
// ...
}
Generated app.rs carries the use line and emits
Counter::new() at the call site.
Java — same shape (post RFC-0022.1, now without @@import)
app.fjava:
@@[target("java")]
package com.example.app;
import com.example.counter.Counter;
@@system App {
domain:
c: Counter = @@Counter()
// ...
}
This was already the required style on Java/C#/Go per RFC-0022.1;
this RFC removes the optional @@import declaration that those
backends never honored anyway.
GDScript — preload
app.fgd:
@@[target("gdscript")]
const Counter = preload("res://counter.gd")
@@system App {
domain:
c = @@Counter()
}
framec emits Counter.new() at the call site; the preload
binding from the user prolog makes Counter resolvable.
Alternatives
A. Status quo (RFC-0022 + RFC-0022.1)
Keep @@import with current semantics: native-emit on 14
backends, dependency-declaration-only on Java/C#/Go.
Rejected. Three reasons. (1) The directive adds language
surface without doing work the host can’t already do. (2) The
14-vs-3 split is a smell — different backends behave differently
on the same syntax, surfacing as user confusion. (3) The
trajectory across recent RFCs (RFC-0022.1, Java FQ types, Oceans
Model glossary) is “framec stops translating things the host
language already handles”; keeping @@import is the inconsistent
exception.
B. Soft deprecation (warning, hard-cut later)
Emit a deprecation warning W707 when @@import is used; remove
the directive 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).
Soft-deprecating delays the cleanup without changing the eventual
outcome.
C. Keep @@import as pure validation, no codegen on any backend
Make @@import a “dependency-declaration only” on every backend
(extending RFC-0022.1’s Java/C#/Go behavior universally). Removes
the 14-vs-3 split but keeps the directive.
Rejected. This is just RFC-0022.1 generalized. It preserves the validation-time error (one step earlier than host-compile) but costs language surface. The host compiler already catches missing imports; the early-detection benefit is small. Removing entirely is cleaner.
Migration
Pre-1.0 hard cut. After framec 4.2.0 (or whatever version ships
this RFC), @@import parses to E823.
Codemod for the 14 file-path backends. A mechanical sweep
replaces each @@import "./other.f<ext>" line with the target’s
native import:
| Target | Replacement |
|---|---|
| Python | from .<filename> import <Systems...> |
| GDScript | const <System> = preload("res://<filename>.gd") per system |
| Rust | use crate::<filename>::{<Systems...>}; |
| JS / TS | import { <Systems...> } from "./<filename>"; |
| Dart | import '<filename>.dart'; |
| C / C++ | #include "<filename>.h" |
| PHP | require_once __DIR__ . '/<filename>.php'; |
| Ruby | require_relative '<filename>' |
| Lua | local <Cap> = require '<filename>' |
| Erlang | (no replacement — single OTP application) |
| Kotlin | import com.example.<filename>.<Systems...> (package conv.) |
| Swift | (same module; typically no import needed) |
The list of <Systems...> is peek-able by reading the imported
file’s @@system declarations — the same peek RFC-0022 already
performs. A codemod ships alongside the RFC release.
Java / C# / Go fixtures need no migration. They already use
native imports via Oceans Model (per RFC-0022.1); the @@import
line on those files was already a no-op and is just deleted.
Documentation. Per-language guides remove the “RFC-0022
@@import works on this backend” sections; cross-file composition
guidance moves entirely to native idiom (already done for
Java/C#/Go).
Drawbacks
- Loses early validation. Strict-mode cross-file checks are gone. The host compiler catches the same errors, just one pipeline step later. Real cost: an extra ~10-30 seconds of host compile before the error surfaces, in cases where the user forgot an import. Judged acceptable.
- Codemod work. Every fixture using
@@importneeds a mechanical rewrite to the native form. The matrix corpus has a small number of multi-file fixtures (Wave 7 persist × multi- system, RFC-0022 example fixtures); the sweep is bounded. - RFC turnover. RFC-0022 and RFC-0022.1 become Superseded just weeks after shipping. The cost is documentation churn. The benefit is that the language is honestly smaller.
- Loss of project-wide dependency view. With
@@import, framec had a complete view of cross-file dependencies in one parse. Future tooling (project linters, dependency graph visualizers) loses that handle. Counter-argument: the host language’s import system has the same information; tools should read host imports, not invent a Frame-specific layer.
Unresolved questions
- Release pacing. Ship in 4.2.0 alongside other Oceans-Model consolidations? Or wait for a dedicated cleanup release?
- E823 error message wording. Should the parse error name RFC-0024 directly, name the host-syntax equivalent for the current target, or both?
- Codemod scope. Run only over
tests/common/positive/, or also overframe-arcadeand any other framec-using repositories? The codemod itself is target-uniform; the question is which trees to apply it to.
References
- RFC-0022 — cross-file
@@importdirective (superseded). - RFC-0022.1 —
@@importsemantics on package-named target languages (superseded). - RFC-0013 — annotation syntax + hard-cut precedent.
- Frame language reference — module-scope directives.
- Glossary — Oceans Model, system.
CHANGELOG.md— once shipped, the release notes record the version.