RFC-0022.1: @@import semantics on package-named target languages

Superseded: This RFC reduced @@import to a dependency-declaration-only directive on Java/C#/Go. RFC-0024 removes @@import from 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:

  1. @@import on 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.
  2. It emits nothing to the generated target file.
  3. 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:

  1. Read the imported file.
  2. Verify the file exists.
  3. Verify it declares at least one non-private @@system.
  4. 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. @@import becomes 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 / using lines 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 @@system names that resolve, but it cannot verify that the user’s hand-written import line 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 the import 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 an import line in any Java project.

Unresolved questions

  • Should strict mode warn when an @@import is present but no corresponding native import line 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 @@import as 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