The Seven Runes
Every statement in Futuruna begins with a rune — a single character that declares what the statement is.
| Rune | Question | What it does |
|---|---|---|
# | What exists? | Types, effects, traits, impls |
> | What happens? | Functions, actors, modules |
| | What must be true? | Rules, invariants, handlers, scopes |
= | What is? | Bindings, monadic bind |
~ | What flows? | Reactive streams, subjects |
@ | Where do proofs stop? | IO, imports, dependencies, meta |
? | Prove it. | Verification demands |
# -- What exists
Defines the shape of data: types, algebraic effects, traits, and implementations.
Struct (single-variant product type)
# Point(x: Float, y: Float)
# Weather(city: City, temp: Float, condition: Condition, wind_kph: Float)
Construction uses positional arguments:
= p = Point(1.0, 2.0)
= w = Weather(Copenhagen, 22.0, Sunny, 10.0)
Fields are accessed with dot notation: w.temp, w.condition.
Enum (multi-variant algebraic data type)
# Color = Red | Green | Blue
# Shape = Circle(radius: Float) | Rectangle(width: Float, height: Float)
# Option(a) = None | Some(a)
# List(a) = Nil | Cons(head: a, tail: List(a))
ADT with methods
# Color = Red | Green | Blue {
> name(c) -> String {
match c {
| Red -> "red"
| Green -> "green"
| Blue -> "blue"
}
}
}
Methods are standalone functions. The first parameter (without type annotation) receives the ADT type.
Product types with rule members
# TaxCase(person: Person, rates: Rates) {
| taxable_income() -> person.gross_income
| tax_due() -> taxable_income() * rates.percent / 100
> label() -> String { "tax:" + show(tax_due()) }
}
= tax = TaxCase(Person(1000), Rates(25))
= due = tax.tax_due()
= label = tax.label()
When a product type body contains | entries, those entries are rule members
of the product value. This is the RuleScope model: a pure calculation object
whose constructor inputs are visible inside scoped rules. Rule members can call
sibling rule members, ordinary global functions/rules, and use under /
exception with the same priority semantics as top-level rules. Rule member
names do not leak globally. Scoped dispatch matches both name and arity, so a
same-named member with a different parameter count does not hide an ordinary
global rule. Pure zero-argument rule members are memoized for the duration of
one root member evaluation; repeated dependencies therefore retain value
semantics without repeatedly evaluating the same rule cascade.
The same product body may contain ordinary > methods. Methods share the
product instance and can call rule members with tax_due() or self.tax_due().
Fields are also available in product methods, so person.gross_income works in
both | rule members and > methods. A | rule member and > method cannot
use the same member name.
RuleScope is different from | scope Name { ... }: | scope owns reactive
lifecycle work such as subjects, streams, subscriptions, and teardown. A
RuleScope has no mutation or lifecycle ownership.
Effect declaration
# effect Console {
> say(msg: String) -> ()
> ask(prompt: String) -> String
}
Defines abstract operations that callers can intercept via | handle.
Trait declaration
# trait Printable {
> display(self) -> String
}
# trait Greetable {
> greet(self) -> String {
"Hello, " + display(self) -- default implementation
}
}
Impl block
# impl Printable for Color {
> display(self) -> String {
match self {
| Red -> "Red"
| Green -> "Green"
| Blue -> "Blue"
}
}
}
> -- What happens
Defines transformation: functions, actors, and modules.
Function
> add(a: Int, b: Int) -> Int { a + b }
> greet(name: String) -> String {
"Hello, " + name + "!"
}
Parameters can omit type annotations (inferred). Return type after ->.
Function with effects
> process(item: String) -> String with Console, Logger {
say("Processing: " + item)
log("info", "processed " + item)
item
}
The with clause declares which effects the function may perform.
Function with inout (mutable value semantics)
> sort_vec(xs: inout List(Int)) -> () {
@ rust { xs.sort(); }
}
inout parameters are passed as &mut T. The caller's value is mutated in place.
Generic function
> map_list(xs: List(a), f: a -> b) -> List(b) {
match xs {
| Nil -> Nil
| Cons(h, t) -> Cons(f(h), map_list(t, f))
}
}
Lowercase type variables (a, b) become Rust generics.
Actor
> actor counter(state: Int) {
| Increment -> state + 1
| Decrement -> state - 1
| Reset -> 0
}
Actors have a state parameter and message handlers. Each handler returns the new state. Compiles to a tokio task with an mpsc channel.
Module
> module Math {
> square(x: Int) -> Int { x * x }
> cube(x: Int) -> Int { x * x * x }
}
Modules can be nested. Contents are accessed via Math.square(5).
| -- What must be true
Declares rules, invariants, effect handlers, and scopes. The most versatile rune.
Logic rules (Prolog-style)
| taxable(person) -> resident(person), has_income(person)
Default rules with overrides (Catala-style)
| advisory(w) -> "all clear"
| advisory(w) -> "heat warning" under w.temp > 35.0
| exception heatwave advisory(w) -> "danger" under w.temp > 45.0
Rules are evaluated by priority tier: exceptions, guarded defaults, ordinary
clauses, then an unguarded default. Within one tier, source order is
authoritative and the first applicable rule wins. Put the more specific of two
overlapping guards first. under adds a guard condition. exception <label>
places the rule in the exception tier for the same head. The label (here
heatwave) names the exception for readability and debugging; it does not
affect priority.
Named invariants (verification targets)
| name: subject_expr -> predicate_expr
Defines a named predicate that ? can check. The subject expression is the value being tested; the predicate expression must return Bool.
= balance = 1000
= max_supply = 1000000
| balance_bounded: balance -> balance >= 0 && balance <= max_supply
The name before : is the invariant name. The expression between : and -> is the subject (captured by ? name: val). The expression after -> is the predicate.
Effect handlers
= result = | handle Console {
| say(msg) -> { @ print("[console] " + msg); resume(()) }
| ask(prompt) -> { @ print("[console] " + prompt); resume("default") }
} in greet("World")
Intercepts effect operations from the in body. resume(value) continues execution with the given return value.
Scope blocks (lifecycle management)
| scope WeatherStation {
~ readings = subject()
readings <- 42
@ print(show(readings))
}
Scopes group statements with lifecycle management. Subjects, streams, and live subscriptions within a scope are cleaned up when the scope ends. Named scopes are also the explicit owner required for live subscriptions started inside ordinary functions. See docs/stream-lifetimes.md.
Match arms
Inside a match expression, | introduces each arm (see basics.md for match syntax).
= -- What is
Binds a name to a value. Ground truth at a point in time.
Simple binding
= x = 42
= name = "hello"
= result = add(20, 22)
With type annotation
= x: Int = 42
= name: String = "hello"
Top-level initialization order
Unique top-level bindings may refer to bindings declared later, directly or
through functions, rules, and RuleScope members. Futuruna initializes the
required dependencies first in both interpreted and compiled execution.
Declarative @ comptime and @ export annotations remain attached when a
binding moves with its dependencies.
| answer() -> base + 1
= result = answer()
= base = 41
Rebound names keep source-order semantics. A cycle between unique top-level bindings is rejected with the complete initialization path.
Monadic bind (early return)
= value <- parse_int("42")
If the expression returns Ok(v) or Some(v), binds v and continues. If Err(e) or None, returns immediately (early return). Equivalent to Rust's ? operator.
> add_parsed(a_str: String, b_str: String) -> Result(Int, String) {
= a <- parse_int(a_str)
= b <- parse_int(b_str)
Ok(a + b)
}
~ -- What flows
Declares reactive streams and subscribes to them. Values that change over time.
The ~ rune has two forms:
- Binding (
~ name = expr) — creates a stream - Subscription (
~ expr | arms) — consumes a stream with event handling
Stream binding
~ nums = from_list([1, 2, 3, 4, 5])
~ doubled = map(nums, |x| x * 2)
~ big = nums |> filter(|x| x > 3)
Subscription (~ + |)
-- Subscribe to a stream with value handling
~ nums | x -> { @ print(show(x)) }
-- With error handling
~ nums
| x -> { @ print(show(x)) }
| Err(e) -> { @ print("error: " + show(e)) }
-- Full lifecycle (value + error + completion)
~ nums
| x -> { @ print(show(x)) }
| Err(e) -> { @ print("error: " + show(e)) }
| Complete -> { @ print("stream ended") }
-- Pipeline ending in subscription
~ sensor |> filter(valid) |> map(to_celsius)
| t -> { display(t) }
| Err(e) -> { log(e) }
The | arms handle three stream events: values, errors, and completion. This replaces for loops on streams. Use for for lists/ranges; use ~ + | for streams.
See streams.md for the full stream API and subscription reference. For lifetime ownership rules around function-local subscriptions, see docs/stream-lifetimes.md.
Subject creation (push-based streams)
~ clicks = subject() -- empty subject
~ temp = subject(20.0) -- with initial value
~ history = subject(0, 10) -- replay subject (buffer last 10)
Push values into subjects
clicks <- "click1"
clicks <- "click2"
temp <- 25.0
Subject properties
clicks.count -- number of values pushed
temp.latest -- most recent value
@ -- Where proofs stop
The boundary between the verified world and effects. Every @ says: formal reasoning cannot reach here.
Print (IO)
@ print("hello")
@ print("value: " + show(x))
Import (multi-file)
@ import ./utils -- flat import: merge all definitions
@ import Utils from ./utils -- qualified: access via Utils.function()
@ import #a1b2c3 from ./utils -- content-addressed import
Use (Rust items)
@ use std::collections::HashMap
@ use std::io::*
Use @ import for Futuruna modules.
Depend (Cargo dependencies)
@ depend "serde" "1"
@ depend "tokio" "1"
Export (visibility)
@ export
> public_function() -> Int { 42 }
Marks the next definition as public. Without @ export, definitions are private.
Calculate (typed external input)
@ calculate("Danish personal income tax")
| calculate_tax(input: TaxInput) -> TaxResult(annual_tax = annual_tax(input))
Marks one typed rule or function as a discoverable calculation boundary for
runa schema, runa template, and runa call. This annotation does not perform
an effect or change rule semantics. Its optional single string labels the whole
calculation; nested input labels and questions remain field metadata. See
calculations.md.
Comptime (compile-time evaluation)
@ comptime = table = generate_lookup(1000)
The expression is evaluated at compile time and inlined as a constant.
Rust escape hatch
@ rust {
fn fast_sort(x: &mut [f64]) {
x.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
}
}
Inline raw Rust code. Handles nested braces, strings, and comments correctly.
? -- Prove it
Interrogates what other runes declared. Checks invariants defined with |.
How it works
- Define an invariant with
|:| balance_ok: balance -> balance >= 0 && balance <= max_supply - Check it with
?:? balance_ok
The six forms
Without else, failure halts the program. With else, failure is handled and execution continues.
-- Form 1: Bare check (halt on failure)
? balance_ok
-- Form 2: Pass block (halt on failure)
? balance_ok -> {
@ print("Balance verified")
}
-- Form 3: Capture + pass (halt on failure)
? balance_ok: val -> {
@ print("Balance is " + show(val))
}
-- Form 4: Else block (no halt)
? balance_ok else {
@ print("Balance violated!")
}
-- Form 5: Pass + else (no halt)
? balance_ok -> {
@ print("OK")
} else {
@ print("FAIL")
}
-- Form 6: Full form — capture + pass + else (no halt)
? balance_ok: val -> {
@ print("Verified: " + show(val))
} else {
@ print("Violation: " + show(val))
}
The : val capture binds the subject value (the data being checked), not the boolean result.
Verify all invariants
? all -- check all, halt on any failure
? all -> { @ print("All OK") } -- with pass block
? all -> { @ print("OK") } else { @ print("Some failed") } -- with both
Three assurance levels
The same ? line works at three levels of assurance:
runa run— evaluates the predicate with current values at runtimeruna build— emitsdebug_assert!()in the compiled binaryruna verify— translates to SMT-LIB2 and invokes Z3 to prove for all inputs
Verifying rule dispatch
runa verify can translate pure, total, non-recursive | rule groups directly,
including rules inside a product RuleScope. Conditions and exceptions use the
same precedence as execution; there is no need to restate the rule cascade as a
separate > function.
# TaxCase(income: Int) {
| rate_percent() -> 25
| rate_percent() -> 30 under income > 500000
| exception low_income rate_percent() -> 20 under income < 100000
| tax_due() -> income * rate_percent() / 100
}
= high_income_case = TaxCase(income = 600000)
| high_income_tax: high_income_case.tax_due() -> high_income_case.tax_due() == 180000
Plain imports are resolved recursively for verification. An exception declared by an importing file therefore extends the imported rule group and keeps its normal exception priority. Within one priority tier, imported declarations come before declarations in the importing file and the first applicable rule wins, just as it does during execution.
The solver path fails closed with a diagnostic for partial non-Boolean rules,
recursive dispatch, higher-order parameters, effects, and other expressions
outside its current first-order subset. runa verify remains a Preview surface.