# > | = ~ @ ?

Rust Compatibility

Futuruna compiles to Rust. You write value semantics; the compiler handles ownership.

Ownership Inference

Futuruna is to Rust as Kotlin is to Java. You never write &T, lifetimes, or .clone().

What the compiler infers

PatternFuturunaGenerated Rust
Read-only parameterdata: List(String)data: &Vec<String>
Borrowed stringprefix: Stringprefix: &String
Single use= x = make_thing()let x = make_thing(); (move)
Multiple use= x = val; f(x); g(x)let x = val.clone(); f(&x); g(&x)
In-place mutationxs: inout List(Int)xs: &mut Vec<i64>

Escape analysis

The compiler performs whole-program escape analysis:

  • Single-use variables move (zero copies)
  • Multi-use variables clone once, then borrow
  • Read-only parameters auto-borrow as &T

The inout keyword

For in-place mutation without ownership transfer:

> sort_vec(xs: inout List(Int)) -> () {
    @ rust { xs.sort(); }
}

= data = [5, 3, 1, 4, 2]
sort_vec(data)
@ print(show(data))    -- [1, 2, 3, 4, 5]

Compiles to fn sort_vec(xs: &mut Vec<i64>). The caller's binding is mutated directly. Save this example as sort.runa and execute it with runa run sort.runa. The interpreter refuses embedded Rust with a runtime error at the @ rust block, so this example requires native execution.

Rust Escape Hatch

When Futuruna's abstractions don't cover a case, embed raw Rust:

@ rust {
    fn fast_sort(x: &mut [f64]) {
        x.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
    }
}

The block is inserted verbatim into the generated Rust. Handles nested braces, strings, and comments. Only compiled code runs it: in the interpreter, calling a function defined in an @ rust block is a runtime error at the call.

Using Rust Crates

Declare a dependency

@ depend "serde" "1"
@ depend "tokio" "1"

Adds the crate to the generated Cargo.toml.

Import Rust items

@ use std::collections::HashMap
@ use std::io::*
@ use serde::{Serialize, Deserialize}

Emits a Rust use statement in the generated code. For Futuruna modules, use @ import.

Build Modes

CommandWhat it does
runa file.runaInterpret directly (no Rust compilation)
runa run file.runaCompile to a native binary and execute it
runa emit file.runaPrint the generated Rust source
runa build file.runaTranspile to Rust and compile to native binary
runa lib file.runaEmit as a Rust library (no fn main, exported names get pub)
runa verify file.runaTranslate invariants to SMT-LIB2 and invoke Z3
runa hashes file.runaShow content-addressed hashes for all definitions
runa registry file.runaGenerate <file>.registry.json next to the source file

Build output

Native run and build artifacts use a dependency-complete compiler cache. The cache records the root source, every transitive plain, qualified, and hash import, its resolved import edges and manifest-resolution contexts, prelude mode, and exact Futuruna compiler. An unchanged graph reuses the validated binary before type checking or Rust code generation. Any source, import-resolution, manifest, prelude-mode, or compiler change causes a miss. Builds on a miss use separate, freshly claimed directories for generated source and executables, including when projects share a filename such as src/main.runa. Cache overrides and cache disabling apply to all native executable reuse.

Caches and build directories live below one per-user cache root: FUTURUNA_COMPILER_CACHE_DIR when set, otherwise $XDG_CACHE_HOME/futuruna, ~/Library/Caches/futuruna on macOS, or ~/.cache/futuruna. The compiler restricts that directory to its owner (mode 0700) and refuses to use it when it is a symbolic link or belongs to another user. Each build or validation run claims a fresh builds/build-<id>/ directory below it; nothing is written to the current directory except the requested output, and no .cargo/config.toml from the source tree applies to generated Cargo projects.

Successful builds remove their workspace after copying or running the executable. Failed compilations retain the generated source at the path in the diagnostic. Programs with Cargo dependencies get their own target directory in the workspace, which prevents concurrent source or target replacement, including when CARGO_TARGET_DIR is set. An unchanged validated source graph still uses the native artifact cache; a cache miss starts a fresh Cargo build.

runa check uses the same graph validation and caches only successful checks. Its Rust metadata lane additionally fingerprints rustc and retains the generated source in a persistent validation workspace. Set FUTURUNA_COMPILER_CACHE_DIR to choose a cache root, FUTURUNA_DISABLE_COMPILER_CACHE=1 to bypass it, or FUTURUNA_COMPILER_CACHE_TRACE=1 to report cache hits and misses on standard error. FUTURUNA_COMPILER_TIMING=1 reports parse, source-graph, type-check, Rust-generation, and backend-validation timings.

For a shorter edit loop, runa check --frontend file.runa stops after parsing, import-aware type checking, calculation-contract checks, and Futuruna compiler validation diagnostics. It deliberately skips complete Rust generation and rustc or Cargo validation, and reports that reduced assurance in its success message. Use ordinary runa check as the authoritative pre-commit and CI gate.

Successful backend validation is cached by the complete generated Rust hash and Rust toolchain fingerprint. Byte-identical generated Rust is left untouched, so a compiler rebuild or source-only metadata change reruns Futuruna analysis but does not ask rustc to prove an identical backend artifact again. For large generated single-crate programs, creating rustc incremental state costs more than a cold metadata-only validation, so it is disabled by default. Set FUTURUNA_RUSTC_INCREMENTAL_CHECK=1 to opt into persistent rustc incremental state for a workload where repeated small backend changes benefit from it. Programs containing raw @ rust blocks do not use the content-only validation cache because those blocks may read external files or compile-time environment values.

Cache validation includes the exact SHA-256 of the runa executable. That digest is reused across CLI processes only while the executable's canonical path, size, modification time, and platform file identity remain unchanged. Replacing or rewriting the executable recomputes the digest before any compiler or calculation artifact can be accepted; malformed fingerprint cache entries are ignored.

For the precise compatibility boundary around emitted Rust, native build artifacts, runa lib, and WASM package output, see ../artifact-codegen-contracts.md. The short rule is: generated Rust behavior for stable pure/core programs is a contract; exact emitted text, helper names, and private layout are internal unless an artifact expectation fixture or doc explicitly promises them.

Rust-Facing Library Contract

runa lib file.runa emits a Rust source file intended to be compiled into a Rust library or included as a Rust module.

Libraries with runtime bindings expose an explicit initializer, __fut_init(). It returns an owned __FutGlobals context. Exported functions that use those bindings receive a reference to the context before their ordinary arguments. Keep that context to reuse initialized values; each initialization creates an independent instance. Emitting the library does not run ordinary initializers.

For example, an exported answer() that reads values from an imported module is called from Rust as follows:

let model = futuruna_lib::__fut_init();
let answer = futuruna_lib::answer(&model);

The generated signature identifies whether a function needs this context. Module callbacks retain the initialized values they capture when they escape their declaration scope.

WASM packages expose the same instance boundary to JavaScript. After loading the package with its default initializer, call its named __fut_init() export to obtain a context. Pass it before ordinary arguments where the generated TypeScript signature requires __FutGlobals, and call context.free() when finished with the instance.

Stable today:

  • exported Futuruna ADTs become public Rust structs/enums
  • exported struct fields are public
  • exported Futuruna functions become public Rust functions
  • private Futuruna helpers remain private Rust items
  • no binary fn main is emitted
  • String, List, Option, Result, and exported ADTs use the documented type mapping below
  • every String parameter of an exported function or rule is &str in the Rust signature, whatever the body does with it, so consumers pass &owned or a string literal; inout parameters stay &mut String
  • other read-only non-copy parameters may be borrowed in Rust signatures, for example Packet as &Packet and List(Int) as &Vec<i64>
  • Rust consumers can compile runa lib output that references external crates through @ depend, explicit @ use declarations, external-crate stdlib builtins, or raw @ rust blocks, as long as the consuming Cargo project provides the matching dependencies

Stable package layouts:

  1. Include the generated file as a Rust module inside an ordinary Cargo package:

    runa lib library.runa > src/futuruna_lib.rs
    
    mod futuruna_lib;
    
    fn main() {
        let value = futuruna_lib::exported_function();
    }
    
  2. Place the generated file at src/lib.rs inside a user-owned Cargo library crate, then depend on that crate from another Cargo package:

    mkdir -p generated/src
    runa lib library.runa > generated/src/lib.rs
    
    # generated/Cargo.toml
    [package]
    name = "futuruna_generated"
    version = "0.1.0"
    edition = "2021"
    
    [dependencies]
    # Add crates required by @ depend or dependency-backed builtins, for example:
    regex = "1"
    
    # app/Cargo.toml
    [dependencies]
    futuruna_generated = { path = "../generated" }
    
    fn main() {
        let value = futuruna_generated::exported_function();
    }
    

@ depend records the Cargo dependencies needed by generated Rust. In runa lib mode, Futuruna emits source only; the consuming Cargo package owns its Cargo.toml and must provide matching dependency entries.

When runa lib sees required Cargo dependencies, it prints a Cargo.toml-ready dependency list to stderr and writes the same guidance into the generated Rust header:

// Required Cargo dependencies for consumers:
//   regex = "1"
// Add these entries to the consuming Cargo.toml [dependencies].

If a Rust consumer omits one of those entries, Cargo will fail while compiling the generated source. Treat that as a consumer package setup issue: copy the listed entries into the consuming crate's [dependencies].

Blocking evidence:

  • tests/expect/artifact/lib_export_contract.runa snapshots the public/private emitted shape
  • ./scripts/rust-interop-canary.sh emits tests/canary/interop/rust_consumer_lib.runa with runa lib, compiles it with rustc, and runs an ordinary Rust consumer that calls exported structs, enums, borrowed params, lists, Option, and Result
  • the same script emits tests/canary/interop/rust_consumer_external_crate_lib.runa with runa lib, compiles it inside an ordinary Cargo consumer with CARGO_NET_OFFLINE=true, and verifies @ depend, @ use, a raw Rust helper using regex, and the regex_find_all builtin
  • the same canary also places generated output at src/lib.rs in a user-owned Cargo library crate and compiles a separate downstream Cargo package that depends on that generated library by path
  • the same canary intentionally builds a Cargo consumer without the required regex dependency and verifies the generated source and runa lib stderr carried actionable dependency guidance before Cargo reports the missing crate

Outside the stable contract:

  • exact helper names and private generated layout
  • automatic manifest/package generation for runa lib consumers
  • richer FFI patterns beyond ordinary Rust source inclusion and Cargo dependencies supplied by the consumer crate
  • runa from-rust, which has a separate stable FRSS-v0 single-file validation contract rather than being part of the Rust-facing library contract; see ../from-rust-contract.md for its separate validation lane

Type Mapping

FuturunaRustCopy?
Inti64Yes
Floatf64Yes
BoolboolYes
CharcharYes
()()Yes
StringStringNo (Clone)
List(a)Vec<A>No (Clone)
Option(a)Option<A>No (Clone)
Result(a, e)Result<A, E>No (Clone)
Pair(a, b)Pair<A, B> (struct with fst, snd)No (Clone)
a -> bimpl FnMut(A) -> B or impl FnOnce(A) -> B—

Generic type variables (lowercase) are uppercased in Rust output: a becomes A, b becomes B.

Copy vs Clone: The escape analysis uses this distinction. Copy types (Int, Float, Bool, Char, unit) are freely duplicated — no clone needed. Clone types (String, List, structs, Option, Result) are cloned when used more than once and moved when used exactly once. You never write .clone() — the compiler inserts it.

The compiler chooses FnOnce when a closure parameter is used exactly once as a direct call and not inside a nested lambda. Otherwise it uses FnMut. This means closures that move captured non-Copy values (called once) get FnOnce, while closures called in loops or multiple times get FnMut.

Multi-File Projects

Flat import

@ import ./utils

Merges all definitions from utils.runa into the current scope. Transitive dependencies are loaded automatically.

Qualified import

@ import Utils from ./utils

Definitions accessed as Utils.function(). Only @ export-marked definitions are visible.

Content-addressed import

@ import #a1b2c3d4 from ./utils

Import by structural hash. Same hash = same code, regardless of filename. Inspired by Unison.

Actors

Actors compile to a message enum and a handler function behind a shared handle:

> actor counter(state: Int) {
    | Increment -> state + 1
    | Decrement -> state - 1
    | Reset -> 0
}

Generated Rust:

  • A message enum with variants for each handler pattern
  • A _handle(state, message) function that returns the next state
  • A _spawn() helper that returns a clonable handle (__FutActor) whose copies share the state

Messages are handled one at a time, before <- returns; see Actors.