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:

  1. Make the holder async too — add @@[async] to the holder’s header. Its own interface methods become async and can await the child.
  2. 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 _GateGuard clears the gate via RAII Drop even if a user handler panics — except under panic = "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

  1. framec --version reports 4.4.0.
  2. framec <file> on a 4.3.x async source without @@[async] errors with E720; after the codemod it compiles clean.
  3. A 4.3.x source using bare @@:system.state errors with E608; after the replace it compiles clean and emits byte-identical code.
  4. Rust / Swift host projects build once call sites adopt the recoverable Result / try shapes.
  5. Sources that never used async or @@:system.state generate byte-identical output to 4.3.x — no action needed.

References