RFC-0022.1: @@import semantics on package-named target languages
- Status: Superseded by RFC-0024
- Author: Mark Truluck mark.truluck@cogiton.com
- Created: 2026-05-16
- Companion to: RFC-0022
Superseded: This RFC reduced
@@importto a dependency-declaration-only directive on Java/C#/Go. RFC-0024 removes@@importfrom the language entirely; cross-file dependencies are now expressed using the target host language’s native import syntax as Oceans Model pass-through on every backend. The Java/C#/Go behavior this RFC documented is generalized to all 17 targets.
Summary
On three target backends — Java, C#, and Go — @@import is a Frame-level
dependency declaration only. It is parsed, recorded in the
importer’s dependency set, and validated under --import-mode strict, but
emits nothing to the generated target file. Native package, namespace,
and import declarations are the user’s responsibility, written via Frame’s
Oceans Model pass-through. The other fourteen
backends are unchanged: @@import lowers to a native import line
mechanically, as specified in RFC-0022.
Motivation
RFC-0022 specified @@import "./other.<ext>" as a uniform
cross-file directive that lowers to a native import on every backend. On the
fourteen targets that locate symbols by file path — Python, GDScript, Rust,
JavaScript, TypeScript, Dart, Ruby, Lua, PHP, C, C++, Kotlin, Swift, Erlang
— the translation is mechanical: framec rewrites @@import "./counter.fpy"
to from .counter import …, const Counter = preload("res://counter.gd"),
use crate::counter::Counter;, and so on.
Three targets locate symbols by fully-qualified name, not file path:
- Java:
import com.example.counter.Counter; - C#:
using Example.Counter; - Go:
import "github.com/example/counter"
In each case the host language requires more information than a file path provides — it needs the package, namespace, or module path that the imported file belongs to. That information lives inside the imported file as a syntactic declaration the host compiler requires anyway:
package com.example.counter; // mandatory at top of Counter.java
namespace Example.Counter { ... } // mandatory wrapping the class
package counter // mandatory at top of counter.go
framec has no need to translate those keywords. They already exist in the
host language, the user already knows them, and Frame already has a
mechanism for emitting native host-language text directly: the
Oceans Model. Text outside @@system blocks
in a Frame source file is passed through to the generated output verbatim,
in position. A user writing .fjava / .fcs / .fgo source can — and
should — write the native package / namespace / import / using
lines exactly as they would in hand-written Java / C# / Go.
What the current implementation does wrong is emit a placeholder marker:
// @@import "./counter.fjava"
This implies framec is attempting a translation. It isn’t — and on these three backends, it cannot, without inventing a Frame-level vocabulary that duplicates what the host language already provides. The placeholder is misleading.
This RFC clarifies the contract:
@@importon Java / C# / Go is a dependency declaration at the Frame level. It tells framec “this importer depends on systems in that file” for strict-mode validation.- It emits nothing to the generated target file.
- The native package and import lines are written by the user via Oceans Model pass-through.
The contract
The key words MUST, MUST NOT, SHOULD, SHOULD NOT, MAY are to be interpreted as in RFC 2119.
Behavior per backend
| Targets | @@import codegen |
Native package / import via Oceans Model |
|---|---|---|
| Python, GDScript, Rust, JavaScript, TypeScript, Dart, Ruby, Lua, PHP, C, C++, Kotlin, Swift, Erlang | Native import emitted per RFC-0022. | Permitted but not required. |
| Java, C#, Go | Nothing emitted. Dependency recorded for --import-mode strict. |
Required for cross-file composition. |
Validation
Under --import-mode strict (defined in RFC-0022), @@import
"./other.<ext>" on Java / C# / Go MUST:
- Read the imported file.
- Verify the file exists.
- Verify it declares at least one non-
private@@system. - Verify any
@@SystemName()references in the importer resolve to a system declared somewhere in the importer’s transitive import set.
Strict mode MUST NOT attempt to verify that the user’s Oceans Model
pass-through text contains a matching native import / using line. The
host compiler is the source of truth for host-symbol resolution; framec
does not parse non-Frame text.
Under --import-mode lax (default), @@import on these backends is
parsed and the dependency is recorded but no validation runs.
User responsibility on Java / C# / Go
The user MUST write the native package and import declarations as
Oceans Model pass-through text outside @@system blocks. framec emits
these declarations verbatim, in position.
Backends that need package declarations but support @@import codegen
Kotlin shares Java’s package model and is in scope for some readers’
mental models of this RFC. Kotlin is out of scope: RFC-0022 already
specifies a working @@import translation for Kotlin
(import com.example.other.Other), and the package convention can be
expressed via Oceans Model on the importer’s side. If Kotlin support
turns out to need similar clarification, a follow-up companion may
extend this RFC.
Examples
Java — two-file composition
counter.fjava:
@@[target("java")]
package com.example.counter;
@@system Counter {
interface:
bump()
get(): int
machine:
$Active {
bump() { self.n = self.n + 1 }
get(): int { @@:(self.n) }
}
domain:
n: int = 0
}
Generated Counter.java:
package com.example.counter;
public class Counter {
// ...
}
app.fjava:
@@[target("java")]
@@import "./counter.fjava"
package com.example.app;
import com.example.counter.Counter;
@@system App {
interface:
run()
machine:
$Active {
run() { self.c.bump() }
}
domain:
c = @@Counter()
}
Generated App.java:
package com.example.app;
import com.example.counter.Counter;
public class App {
// ...
}
The @@import directive is consumed by framec (for --import-mode
strict validation) and emits nothing. The package and import lines
on the next two source lines pass through to the output unchanged.
C# — namespace via Oceans Model
counter.fcs:
@@[target("csharp")]
namespace Example.Counter
{
@@system Counter { /* ... */ }
}
app.fcs:
@@[target("csharp")]
@@import "./counter.fcs"
using Example.Counter;
namespace Example.App
{
@@system App {
domain: c = @@Counter()
/* ... */
}
}
The namespace Example.Counter { ... } wrapping the @@system is
straight Oceans Model — framec sees Frame inside, native outside, and
emits the file in that order.
Go — module path
counter.fgo:
@@[target("go")]
package counter
@@system Counter { /* ... */ }
app.fgo:
@@[target("go")]
@@import "./counter.fgo"
package app
import "github.com/example/counter"
@@system App {
domain: c = @@Counter()
/* ... */
}
Inside App’s handler bodies, references to the imported system use Go’s
package-qualified form (counter.NewCounter()), written by the user in
Frame source. framec does not rewrite identifiers inside handler bodies;
it emits them verbatim per the Oceans Model.
Alternatives
A. Frame-level @@[package(...)] attribute
Introduce a per-file Frame attribute naming the host-language package; framec generates the package declaration and cross-file import lines.
Rejected. Duplicates information the user can already express via the
Oceans Model. The attribute name also privileges one host language’s
vocabulary — package matches Java and Go, namespace matches C#,
module matches neither — and the choice is a coin flip. Cleanest is
no translation: users write the host’s own keyword in its own native
form.
B. Drop @@import entirely on Java / C# / Go
Define @@import as an error (or a no-op) on these backends; rely on
Oceans Model for everything, including dependency tracking.
Rejected. Loses Frame-level dependency tracking. Strict mode is the
existing affordance for cross-file validation; keeping @@import as a
dependency declaration preserves that capability without committing
framec to translation it can’t do cleanly.
C. Parse the user’s Oceans Model text to extract package info
Have framec scan the pass-through text for package / namespace /
import / using declarations and use those to construct cross-file
imports and validate dependencies.
Rejected. The Oceans Model’s value is that framec does not parse non-Frame text. Asking framec to start parsing Java / C# / Go syntax inside the pass-through breaks the model and introduces three host- language parsers framec doesn’t otherwise need.
D. Emit a comment marker (status quo)
Continue emitting // @@import "..." in the generated file.
Rejected. Misleads readers into thinking framec is attempting a translation. The directive is consumed at the Frame level; the generated file shouldn’t carry residue of it.
Migration
- Codegen: Remove the
// @@import "..."comment marker from Java, C#, and Go output.@@importbecomes silent on these backends — the dependency is recorded in framec’s internal import set; the generated file shows no trace. - Source: Files that today rely on flat-default-package layout (the matrix harness’s single-directory fixtures, for example) continue to work unchanged. Their generated output had no package declaration before; it has none after.
- Real cross-file composition: Files that need real package layout
add the native
package/namespace/import/usinglines directly to their Frame source, as Oceans Model pass-through. This is the same one-line-per-file users would write in hand-authored Java / C# / Go.
No codemod is specified.
Drawbacks
- User burden. Users of these three backends write more native syntax in their Frame source than users of the other fourteen. That asymmetry follows the host languages: Java / C# / Go require more syntactic ceremony than Python or GDScript, regardless of Frame. Frame neither hides nor adds to it.
- No symbol-level cross-file validation in strict mode. Strict
mode verifies the file exists and exports
@@systemnames that resolve, but it cannot verify that the user’s hand-writtenimportline names the right symbols. The host compiler catches that. - Stricter coupling between import directive and Oceans Model text.
If the user writes
@@import "./counter.fjava"but forgets theimport com.example.counter.Counter;line, the Frame side is silent and the Java compiler complains. The error message points to the right file but not the missing-pair relationship. Acceptable — this matches the experience of forgetting animportline in any Java project.
Unresolved questions
- Should strict mode warn when an
@@importis present but no corresponding nativeimportline appears in the file’s Oceans Model text? Doing so would require parsing the pass-through text, which contradicts the Oceans Model. Probably no — leave the host compiler to catch it. Confirm before shipping. - Erlang’s position. Erlang’s runtime model is “all modules in one
OTP application,” and RFC-0022 specified
@@importas a no-op on Erlang for that reason. Erlang’s situation is closer to Java’s (named modules, not file paths) than Python’s. The current Erlang behavior is acceptable but worth re-stating alongside this RFC to avoid future drift.
References
- RFC-0022 — cross-file
@@importdirective (parent). - Frame language reference — module-scope directives.
- Glossary — Oceans Model, system, attribute.
CHANGELOG.md— once shipped, the release notes record the version.