Migrating from framec 4.3.x to 4.4.0
This release has five user-visible breaks across two RFCs. None are subtle — each surfaces as a clear compile error (or a typed return-shape change you’ll hit at host-compile time). Walk-throughs below.
Frame is pre-public-beta, so no published program is affected; the cost is your own sources, and every break is mechanical.
| Break | Symptom | Fix scope |
|---|---|---|
async members require @@[async] (RFC-0043) |
E720 on framec compile |
One-line per system (codemod available) |
| sync system can’t compose async system (RFC-0043) | E721 on framec compile |
Per-composition design call |
Rust casing returns Result<T, FrameE703Error> (RFC-0043, D5) |
Rust host build fails on the new return type | Per-call-site ? / match |
Swift casing is async throws (RFC-0043, D2) |
Swift host build fails: call needs try |
Per-call-site try await + catch |
@@:system.state → @@:system.state.name (RFC-0045) |
E608 on framec compile |
One-shot find-and-replace |
1. async members require @@[async] (RFC-0043)
What changed
A system that declares any async member — an async interface method, action,
or operation — must now carry the @@[async] attribute on the line
immediately above @@system. This is a hard cut: there is no warning grace
period. The attribute opts the system into the layered casing/machine
architecture.
Symptom
E720: @@system '<Name>' declares async member(s) but lacks the `@@[async]`
system-header attribute (RFC-0043).
Fix
Add @@[async], or run the codemod over your tree — it inserts the attribute
above each @@system whose body declares an async member, and changes nothing
else:
framec project add-async-attr path/to/source-tree
In the playground / WASM:
import { migrate_async_attr } from "@frame-lang/framec-wasm";
const migrated = migrate_async_attr(originalSource);
Worked example
Before (4.3.x):
@@[target("python_3")]
@@system Fetcher {
interface:
async fetch(key: str): str
machine:
$Idle { fetch(key: str): str { @@:(key) } }
}
After (4.4.0):
@@[target("python_3")]
@@[async]
@@system Fetcher {
interface:
async fetch(key: str): str
machine:
$Idle { fetch(key: str): str { @@:(key) } }
}
The generated dispatch core is unchanged; framec now also emits a public casing
Fetcher over a private _FetcherMachine, and the interface method gains the
single-driver gate.
2. sync system can’t compose an async system (RFC-0043)
What changed
A non-@@[async] system can no longer hold an @@[async] system as a domain
field when both are declared in the same file. An async system’s casing methods
return a Future/Promise/Task; a sync holder can’t await them without itself
becoming async.
Symptom
E721: sync @@system '<Holder>' cannot compose async @@system '<Held>' as
domain field '<f>' (RFC-0043).
Detection tokenizes the field’s type text, so direct (f: Fetcher), nullable
(f: Fetcher?), and container-wrapped (Vec<Fetcher>, Option<Fetcher>,
List<Fetcher>) forms all fire. (Same-file only — an async system arriving via
@@import from another file is not yet resolved.)
Fix
Two options:
- Make the holder async too — add
@@[async]to the holder’s header. Its own interface methods become async and canawaitthe child. - Restructure so the async child is held by an async parent, and the sync work lives in a separate system.
Worked example
Before — sync App holds async Fetcher (now E721):
@@[async]
@@system Fetcher {
interface: async fetch(key: str): str
machine: $Idle { fetch(key: str): str { @@:(key) } }
}
@@system App { // sync — cannot await Fetcher
domain:
f = @@Fetcher()
}
After — App opts into async so it can drive the child:
@@[async]
@@system Fetcher {
interface: async fetch(key: str): str
machine: $Idle { fetch(key: str): str { @@:(key) } }
}
@@[async]
@@system App {
interface:
async run(key: str): str
machine:
$Ready {
run(key: str): str { @@:(await self.f.fetch(key)) }
}
domain:
f = @@Fetcher()
}
3. Rust casing methods return Result<T, FrameE703Error> (D5)
What changed
On Rust, each casing interface method now returns Result<T, FrameE703Error>
instead of T. The Err carries the E703 single-driver violation. This
replaces the previous-design panic! so the gate is recoverable — the
idiomatic Rust contract. FrameE703Error implements std::error::Error +
Display + Debug, so it ?-chains and converts into
Box<dyn std::error::Error> cleanly.
Symptom
Host-side Rust build error — your call site binds a T but now gets a
Result<T, FrameE703Error> (type mismatch / unused-Result warning-as-error).
Fix
?-chain it in a fallible context, or match / .unwrap() at the boundary.
Before (4.3.x):
let html = client.fetch(url).await;
After (4.4.0):
let html = client.fetch(url).await?; // in a -> Result fn
// or, handling it explicitly:
let html = match client.fetch(url).await {
Ok(v) => v,
Err(e) => return Err(e.into()),
};
The
_GateGuardclears the gate via RAIIDropeven if a user handler panics — except underpanic = "abort", where unwinding is disabled (documented limitation).
4. Swift casing methods are async throws (D2)
What changed
On Swift, each casing interface method is now async throws -> T and throws
FrameE703Error on a busy gate. This replaces the previous-design fatalError
so the gate is recoverable via try? / do…catch.
Symptom
Swift host build error: a call to a casing method now requires try because the
function can throw.
Fix
Use try await and handle the error.
Before (4.3.x):
let html = await client.fetch(url)
After (4.4.0):
do {
let html = try await client.fetch(url)
// …
} catch let e as FrameE703Error {
// single-driver violation
}
5. @@:system.state name accessor moves (RFC-0045)
What changed
@@:system.state previously evaluated to the current state’s name as a
string. That spelling is now reserved for a future meaning (a direct
reference to the current compartment), and the name accessor is
@@:system.state.name. Generated output for @@:system.state.name is
byte-identical on all 17 backends to what @@:system.state produced before.
Symptom
E608: @@:system.state is reserved for future use; use @@:system.state.name
for the state name.
It fires in both handler and operation bodies for the bare form. The E604 hint
(bare @@:system) now suggests @@:system.state.name, and E421 (no state
access in static operations) is retargeted to the new spelling.
Fix
A one-shot find-and-replace across your sources:
# macOS / BSD sed
grep -rl '@@:system.state' your_sources/ \
| xargs sed -i '' 's/@@:system\.state\b/@@:system.state.name/g'
Take care not to double-apply (the \b word boundary keeps an
already-migrated @@:system.state.name from gaining a second .name).
Before:
report(): str {
@@:("current state is " + @@:system.state)
}
After:
report(): str {
@@:("current state is " + @@:system.state.name)
}
Verifying the migration
framec --versionreports4.4.0.framec <file>on a 4.3.x async source without@@[async]errors with E720; after the codemod it compiles clean.- A 4.3.x source using bare
@@:system.stateerrors with E608; after the replace it compiles clean and emits byte-identical code. - Rust / Swift host projects build once call sites adopt the recoverable
Result/tryshapes. - Sources that never used async or
@@:system.stategenerate byte-identical output to 4.3.x — no action needed.
References
- RFC-0043 —
@@[async], casing/machine, the E703 gate - RFC-0044 — kernel context-stack cleanup on exception
- RFC-0045 — reserve
@@:system.state, relocate to.name - Language reference § Async
- CHANGELOG
[4.4.0]