# > | = ~ @ ?

Futuruna Basics

Core language syntax: literals, types, operators, control flow, and closures.

New to the language? The guided tutorial builds a small rule-driven program, runs a concrete scenario, attaches typed metadata, and audits an actual same-rule contradiction.

Literals

Numbers

42          -- Int (i64)
3.14        -- Float (f64)
-7          -- negative Int
1_000_000   -- Int; `_` groups digits
0.000_25    -- Float

A number literal is a run of decimal digits with at most one ., which must be followed by a digit. A _ may stand between two digits to group them and does not change the value. The following spellings would silently mean something other than their conventional reading, so they are errors:

  • a date shape: four digits and two groups of one or two digits joined by the same - or / (2024-01-31, 2024/1/31), or the reverse order (31-01-2024, 31/12/2024). There is no date literal; use a string such as "2024-01-31" for date text, or an explicitly defined date record with the validation your model requires;
  • a comma directly followed by exactly three digits after a number of one to three digits (1,000, [1,500, 2]), which reads as a thousands separator but would separate items. Write 1_000, or put a space after the comma;
  • a second decimal point (1.000.000);
  • a leading zero before another digit (007, 00.5); write 7 or 0.5;
  • a _ that does not stand between two digits (1__000, 1000_).

Digits in @ import module paths and content hashes are part of the name, as in @ import ./kapitel-04.

% is a binary remainder operator, not a percentage suffix. Represent a rate as 25 percent or 0.25 as a fraction, and keep that unit consistent in the model. A trailing % continues onto the next line because it needs a right operand.

Int is a signed 64-bit integer. Addition, subtraction, multiplication, division, remainder, and negation fail if the result cannot be represented. Integer division or remainder by zero also fails. These operations do not wrap or substitute zero in either interpreted or compiled execution. Division truncates toward zero; a nonzero remainder has the dividend's sign.

Float division or remainder by zero (5.0 / 0.0, x % 0.0, also with an Int operand) is an error too, never inf, NaN or 0.0.

Ordinary execution stops with an error. Typed calculations report the invalid value as a case diagnostic; see calculation errors.

Booleans

True        -- capitalized
False

Strings

"hello"                     -- basic string
"line\nnewline"             -- escape sequences: \n \t \\ \"

"""
multi-line string
preserves newlines
"""

"""Result: {{x + 5}}"""    -- interpolation with {{ expr }}

Interpolation desugars to "Result: " + show(x + 5). An ordinary string such as "Hello {name}" keeps its braces literally. Use """Hello {{name}}""" for interpolation. Single quotes enclose one character, not a string.

Ordinary strings recognize \n, \t, \r, \\, and \". Character literals recognize \n, \t, \r, \\, and \'. Unknown escapes are errors at the backslash. Write "a\\qb" to include a literal backslash before q. The literal portions of triple-quoted strings preserve backslashes as written; expressions inside {{ ... }} use normal Futuruna string and character syntax.

When either operand of + is a string, the other value is converted to text: "10" + 5 produces "105". This is concatenation, not numeric addition. Parse numeric input explicitly before doing arithmetic.

Characters

'a'         -- single character (Char type, compiles to Rust char)

Lists

[1, 2, 3]               -- list literal
[]                       -- empty list

Use concat(left, right) to concatenate lists. The + operator does not concatenate lists.

Compare a list with [] using == or !=, in either operand order. The empty list takes its element type from the other operand, including for string lists and nested lists.

List operations are ordinary function calls: length(items) and map(items, |item| item * 2). A list does not expose .length, .len() or .map(...) members.

Unit

()                       -- unit value and unit type

Layout and Multiline Syntax

A newline normally ends a statement. It is a continuation instead when the grammar makes continuation unambiguous:

  • inside parentheses (...) or brackets [...];
  • after an incomplete token such as =, ->, ,, or an operator; or
  • before a continuation token such as |>, ., under, else, and, or a binary operator that cannot begin an expression: +, *, /, %, ^, ==, !=, <, <=, >=, &&.

Use a newline or ; between adjacent expressions. For example, = x = 5 5 is an error; write = x = 5; 5 if you intend two statements. Explicit rune boundaries and compact forms such as { @ print("hello") 5 } remain valid. Unrecognized characters in code are errors, rather than ignored text. Strings, character literals, triple-quoted templates and ---- quotation blocks must have their closing delimiters. Legal quotations and strings may still contain punctuation such as §, curly quotation marks and dashes.

This lets compact and multiline forms mean the same thing:

= compact = calculate(case, [1, 2, 3]) |> cap(100)

= multiline =
    calculate(
        case,
        [
            1,
            2,
            3,
        ],
    )
    |> cap(100)

Delimited sequences accept a trailing comma. This applies consistently to function and rule parameters, calls, constructors, type fields and arguments, patterns, lists, tuples, closures, proof arguments, effect handlers, and grouped @ use imports. The formatter keeps one item per line when a sequence is already multiline.

Block braces {...} contain statements, so their newlines remain statement boundaries. Explicit list-shaped brace syntax, such as grouped @ use {...} imports, handles its own comma-separated items. Continue a block expression by leaving an incomplete token at the end of the line:

> total(base: Int, adjustment: Int) -> Int {
    base +
    adjustment
}

For algebraic data types, put | after a variant when another variant follows. That makes the continuation explicit without confusing the next variant with a top-level rule:

# Shape =
    Circle(radius: Float) |
    Rectangle(
        width: Float,
        height: Float,
    )

- and || can begin an expression (negation and a parameterless closure), so a line that starts with either one begins a new statement. Such a line is an error when it is indented further than the statement before it, because the indentation reads as a continuation the grammar does not perform. Put the operator at the end of the preceding line, or wrap the whole expression in parentheses. For example, = net = 100 followed by an indented - 30 is an error, while an indented + 30 continues the expression. A line-leading - at the statement's own indentation is ordinary negation, such as a block's final -total.

Types

Primitives

TypeRust equivalentExample
Inti6442
Floatf643.14
StringString"hello"
BoolboolTrue
Charchar'a'
()()()

Composite

TypeRust equivalentExample
List(a)Vec<A>[1, 2, 3]
Option(a)Option<A>Some(42), None
Result(a, e)Result<A, E>Ok(42), Err("fail")
Pair(a, b)(A, B)Pair(1, "x")

Tuple literals may span lines and may end in a trailing comma:

= sources = (
    primary_source,
    amendment_source,
)

Pair construction and field access:

= p = Pair(1, "hello")
@ print(show(p.fst)) -- 1
@ print(show(p.snd)) -- hello

A nonexistent field is an error. Known receiver types are checked before execution; an untyped receiver is checked when the field is accessed. A real field whose value is () remains a valid field.

Function types

Int -> Bool              -- function from Int to Bool
(Int, Int) -> String     -- two-argument function
a -> b                   -- generic function type

Generic type variables

Lowercase single letters are type variables: a, b, c, etc. They become uppercased Rust generics (A, B, C). Uppercase names like T pass through unchanged.

Operators

Arithmetic (precedence low to high)

OpMeaning
+, -addition, subtraction
*, /, %multiplication, division, modulo

Arithmetic uses Int or Float operands. For +, -, *, and /, mixing Int and Float converts the integer to Float and produces a Float. Remainder (%) requires two Int values or two Float values. Outside arithmetic an Int is not a Float: a Float parameter, field, binding, or result needs a Float value such as 25.0 or to_float(n), so Rates(25) for # Rates(percent: Float) is a type error. + also concatenates when either operand is a String; use concat to combine lists.

Comparison

OpMeaning
==, !=equality, inequality
<, >, <=, >=ordering

Primitive equality requires operands of the same type: 5 == 5.0 is a type error; to_float(5) == 5.0 compares two floats. Lists, tuples, and data constructors support structural equality. Ordering compares numbers, two strings, or two characters. Numeric ordering permits Int/Float mixing with the same conversion as arithmetic. Floating-point conversion can lose integer precision, so keep exact integer calculations in Int.

Logical

OpMeaning
&&logical AND
||logical OR
not(x)logical NOT (function)

Logical operators require Bool operands. !x is also Boolean negation; unary -x requires a number. Invalid operand types known to the frontend are reported at the source expression before program effects run.

Special operators

OpMeaningExample
|>pipe-forwardx |> f becomes f(x)
<-send/pushsubject <- value
?.safe callexpr?.field (None propagation)
?:elvisexpr ?: default (unwrap with fallback)

The pipe operator inserts the left side as the first argument:

x |> f           -- f(x)
x |> f(a, b)     -- f(x, a, b)
x |> f |> g      -- g(f(x))

Control Flow

if/else

An if condition must return Bool. Numbers, strings, lists, and () are not converted to Boolean values.

if condition { then_expr }
if condition { then_expr } else { else_expr }
if x > 0 { "positive" } else if x == 0 { "zero" } else { "negative" }

match

match expr {
    | Pattern1 -> body1
    | Pattern2 if guard -> body2
    | _ -> default_body
}

The | before each arm is optional. Patterns can destructure ADTs:

match shape {
    | Circle(r) -> 3.14 * r * r
    | Rectangle(w, h) -> w * h
}

match point {
    | Point(x: xval, y: _) -> xval    -- named field destructuring
}

for loop

for item in collection {
    @ print(show(item))
}

Works with List, Stream, and subjects.

Closures

|x| x * 2                       -- single parameter
|x, y| x + y                    -- multiple parameters
|x: Int, y: Float| x + y        -- with type annotations

Closures capture their enclosing environment.

Built-in Functions (Quick Reference)

For the complete standard library with all ~70 builtins, see stdlib.md.

Display

FunctionSignatureDescription
showa -> StringConvert any value to string

List operations

FunctionSignatureDescription
lengthList(a) -> IntList length
headList(a) -> aFirst element
tailList(a) -> List(a)All but first
push(List(a), a) -> List(a)Append element
concat(List(a), List(a)) -> List(a)Concatenate
reverseList(a) -> List(a)Reverse
map(List(a), a -> b) -> List(b)Map function
filter(List(a), a -> Bool) -> List(a)Filter
foldl(List(a), b, (b, a) -> b) -> bLeft fold
range(Int, Int) -> List(Int)Range [start, end)

Math

FunctionSignatureDescription
absInt -> IntAbsolute value
sqrtFloat -> FloatSquare root
pow(Float, Float) -> FloatExponentiation
roundFloat -> IntRound to nearest
floorFloat -> IntFloor
max_int(Int, Int) -> IntMaximum
min_int(Int, Int) -> IntMinimum
clamp(Int, Int, Int) -> IntClamp to range
to_floatInt -> FloatConvert to float

String

FunctionSignatureDescription
string_lengthString -> IntUnicode scalar length
starts_with(String, String) -> BoolPrefix check

Option/Result

FunctionSignatureDescription
unwrap_or(Option(a), a) -> aUnwrap with default
is_someOption(a) -> BoolCheck if Some
is_noneOption(a) -> BoolCheck if None

Logic

FunctionSignatureDescription
notBool -> BoolLogical NOT
assertBool -> ()Runtime assertion
identitya -> aIdentity function

Comments

-- Line comment

----
Block comment
can span multiple lines
----

-- starts a line comment that runs to the end of the line, so a line of three dashes is a comment. A run of four or more consecutive dashes is one block delimiter, whatever its length: the first run opens a block comment and the next run closes it. Banners such as ----- or ------------ therefore enclose the text between them:

-----
-- Tax law
-----
= x = 5

A delimiter run that opens a block comment and has no closing run is an error.

Danish Source Files

A file whose first declaration is @ sprog da is written in Danish. Comments, blank lines and a leading byte-order mark may precede the declaration, and a comment may follow it on the same line. @sprog da and @ language da are the same declaration. Language codes are case-insensitive: da or dansk selects Danish, en or english selects English. Any other code, and a language declaration after another declaration, is an error. A file without the declaration is English.

The language of a file selects its keywords and the Danish names of builtins. It never renames what the author declares, and it never changes how values behave.

Keywords

DanishEnglishDanishEnglish
skelmatchhvisif
ellerselsemedwith
undtagelseexceptionomfangscope
påonaktøractor
startspawneffekteffect
modulmoduleimporterimport
brugusetræktrait
hvorwhereladlet
gørdosåthen
returnerreturnhåndterhandle
genoptagresumeudførperform
Sandt, sandtTrueFalskt, falsktFalse
ogandelleror

og binds more tightly than eller; both work in Boolean expressions and between rule goals. English keywords, && and || remain available. A keyword may also be used as a parameter or field name (start, under); the name keeps the Danish spelling. indud marks an inout parameter, delt a shared type, and @ eksport / @ afhæng are @ export / @ depend.

Danish names for builtins, types and constructors

A Danish file may call a builtin by its Danish name, and write builtin types and the constructors of Option and Result in Danish:

DanishEnglishDanishEnglish
HeltalIntKommatalFloat
TekstStringBoolskBool
TegnCharListeList
NaturligtNatIntetNone
NogetSomeFejlErr

A Danish name refers to the builtin only when neither the file nor a module it imports with a plain @ importer declares that name. Declared names — functions, rules, bindings, parameters, fields, types and constructors — always mean what the author declared, in every file:

@ sprog da
# Kontrol = Godkendt | Fejl      -- a user constructor named Fejl
# Sag(beløb: Heltal)
> vis(s: Sag) -> Tekst { "sag" } -- a user function named vis
= sag = Sag(beløb = 5)
= beløb = sag.beløb
@ print(vis(sag)) -- sag
@ print("""Beløb: {{beløb}}""") -- Beløb: 5
@ print(show(Fejl)) -- Fejl

vis(sag) calls the user function; interpolation always uses the builtin show.

Here Fejl is the declared Kontrol constructor, and calculation schemas and results publish it as "Fejl". Where no user declaration exists, Fejl(…), Noget(…) and Intet construct the built-in Result and Option values, which are always published and shown under their English names Err, Some and None. A name after . is always a field or member name, never a builtin.

Names cross language boundaries unchanged: an English file that imports a Danish module calls its functions by their declared Danish names (tæl(1)), and a Danish file that imports an English module calls tag(…) or hale(…) as declared there. English files do not know the Danish names; hale([1, 2]) in an English file is an undefined name unless the program declares it.

Diagnostics in a Danish file name builtins, types and keywords the way the source line spells them (fold, not foldl; returner, not return).

DanishEnglish
visshow
skrivprint
vis_heltalshow_int
vis_kommatalshow_float
kvrodsqrt
potenspow
til_kommatalto_float
afrundround
gulvfloor
længdelength
tekst_længdestring_length
opdelsplit
samljoin
indeholdercontains
starter_medstarts_with
ender_medends_with
erstatreplace
til_storeto_upper
til_småto_lower
deltekstsubstring
tegn_vedchar_at
indeks_afindex_of
formater_kommatalformat_float
fortolk_heltalparse_danish_int
fortolk_kommatalparse_danish_float
tekst_tegnstring_chars
hovedhead
haletail
ntenth
vendreverse
tilføjpush
områderange
afbildmap
filtrerfilter
foldfoldl
sortersort
sorter_eftersort_by
nogenany
alleall
flad_afbildflat_map
parzip
numererenumerate
tag_menstake_while
spring_mensdrop_while
sum_listesum_list
unikkedistinct
tæl_eftercount_by
opdel_efterpartition
stykkerchunked
abonnersubscribe
læs_filread_file
skriv_filwrite_file
tilføj_filappend_file
fil_eksistererfile_exists
læs_linjerread_lines
miljø_varenv_var
json_fortolkjson_parse
json_hentjson_get
json_tekstjson_string
json_taljson_number
json_sandjson_bool
json_listejson_array
json_udsendjson_emit
json_objektjson_object
kort_nytmap_new
kort_indsætmap_insert
kort_hentmap_get
kort_hent_ellermap_get_or
kort_indeholdermap_contains
kort_fjernmap_remove
kort_nøglermap_keys
kort_værdiermap_values
kort_postermap_entries
kort_længdemap_len
kort_fletmap_merge
kort_framap_from
sæt_nytset_new
sæt_indsætset_insert
sæt_indeholderset_contains
sæt_fjernset_remove
sæt_længdeset_len
sæt_til_listeset_to_list
sæt_foreningset_union
sæt_fællesset_intersect
sæt_forskelset_diff
sæt_fra_listeset_from_list
fra_listefrom_list
tagtake
springskip
indsamlcollect
tælcount
vinduewindow
sidstelast
kombiner_senestecombine_latest
fletmerge
førstefirst
reducerreduce
start_medstart_with
sammenkædconcat
parvispairwise
spørgask
deltshared
ikkenot
find_allefindall

Numbers, sorting and printed values

fortolk_heltal and fortolk_kommatal (English parse_danish_int and parse_danish_float) read Danish number text and return Result(Int, String) and Result(Float, String). Surrounding whitespace is ignored. The text is an optional sign, an integer part written either as plain digits or with . between groups of three digits, and — for fortolk_kommatal only — an optional , followed by digits:

@ sprog da
fortolk_heltal("1.250.000")     -- Ok(1250000)
fortolk_kommatal("1.234,75")    -- Ok(1234.75)
fortolk_kommatal("1,5")         -- Ok(1.5)
fortolk_heltal("1,5")           -- Err("`1,5` is not a Danish integer")
fortolk_kommatal("1.5")         -- Err("`1.5` is not a Danish decimal number")

parse_int and parse_float read the language-neutral format with a decimal point in every file. sorter orders strings by Unicode code point, and vis prints values exactly as show does (true, None, Some(3), 1.5). Neither depends on the language of the file, so a value sorts and prints the same wherever it is used. Order Danish text with sorter_efter and an explicit key, and format amounts for Danish readers with formater_kommatal and erstat.