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
| Pattern | Futuruna | Generated Rust |
|---|---|---|
| Read-only parameter | data: List(String) | data: &Vec<String> |
| Borrowed string | prefix: String | prefix: &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 mutation | xs: 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
| Command | What it does |
|---|---|
runa file.runa | Interpret directly (no Rust compilation) |
runa run file.runa | Compile to a native binary and execute it |
runa emit file.runa | Print the generated Rust source |
runa build file.runa | Transpile to Rust and compile to native binary |
runa lib file.runa | Emit as a Rust library (no fn main, exported names get pub) |
runa verify file.runa | Translate invariants to SMT-LIB2 and invoke Z3 |
runa hashes file.runa | Show content-addressed hashes for all definitions |
runa registry file.runa | Generate <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 mainis emitted String,List,Option,Result, and exported ADTs use the documented type mapping below- every
Stringparameter of an exported function or rule is&strin the Rust signature, whatever the body does with it, so consumers pass&ownedor a string literal;inoutparameters stay&mut String - other read-only non-copy parameters may be borrowed in Rust signatures, for
example
Packetas&PacketandList(Int)as&Vec<i64> - Rust consumers can compile
runa liboutput that references external crates through@ depend, explicit@ usedeclarations, external-crate stdlib builtins, or raw@ rustblocks, as long as the consuming Cargo project provides the matching dependencies
Stable package layouts:
-
Include the generated file as a Rust module inside an ordinary Cargo package:
runa lib library.runa > src/futuruna_lib.rsmod futuruna_lib; fn main() { let value = futuruna_lib::exported_function(); } -
Place the generated file at
src/lib.rsinside 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.runasnapshots the public/private emitted shape./scripts/rust-interop-canary.shemitstests/canary/interop/rust_consumer_lib.runawithruna lib, compiles it withrustc, and runs an ordinary Rust consumer that calls exported structs, enums, borrowed params, lists,Option, andResult- the same script emits
tests/canary/interop/rust_consumer_external_crate_lib.runawithruna lib, compiles it inside an ordinary Cargo consumer withCARGO_NET_OFFLINE=true, and verifies@ depend,@ use, a raw Rust helper usingregex, and theregex_find_allbuiltin - the same canary also places generated output at
src/lib.rsin 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
regexdependency and verifies the generated source andruna libstderr 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 libconsumers - 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
| Futuruna | Rust | Copy? |
|---|---|---|
Int | i64 | Yes |
Float | f64 | Yes |
Bool | bool | Yes |
Char | char | Yes |
() | () | Yes |
String | String | No (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 -> b | impl 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.