FREAKV3 docs freak 0.14.2 (Maverick)

Bindings & types#

> The value model: five types, one binding keyword, and a case-insensitive keyword table that will bite you.

Bindings#

pilot introduces a binding. The type annotation is optional; when present it follows the name after a colon.

examples/variables.fk compilesruns
-- Bindings are declared with `pilot`.
-- `fixed pilot` marks an immutable binding.
-- `pilot mut` opts a binding into reassignment.
task main() -> void {
    pilot answer: int = 42
    pilot ratio: num = 3.5
    pilot callsign: word = "Shirogane"
    pilot ready: bool = true

    fixed pilot MAX_SORTIES: int = 12

    pilot mut score: int = 0
    score = score + 10
    score += 5

    say word_from_int(answer)
    say format_num(ratio)
    say callsign
    say word_from_bool(ready)
    say word_from_int(MAX_SORTIES)
    say word_from_int(score)
}
Program output
42
3.5
Shirogane
true
12
15

Three forms exist:

FormMeaning
pilot x = exprOrdinary binding
pilot mut x = exprExplicitly reassignable
fixed pilot x = exprImmutable binding
Note

mut and fixed only change behaviour under --strict-borrow. Without that flag V3 does no ownership or mutability checking, and all three forms behave identically. Write mut anyway — it documents intent and it is what the checker will demand when you turn it on.

Assignment operators: =, +=, -=, *=, /=, %=.

Type inference#

The annotation can always be omitted. V3 infers from the initialiser.

examples/inference.fk compilesruns
-- The type annotation is optional. V3 infers from the initializer.
-- When you do annotate, the annotation is a single identifier:
-- `int`, `num`, `word`, `bool`, or the name of a shape.
task main() -> void {
    pilot a = 7            -- int
    pilot b = 2.5          -- num
    pilot c = "text"       -- word
    pilot d = false        -- bool

    say word_from_int(a)
    say format_num(b)
    say c
    say word_from_bool(d)
}
Program output
7
2.5
text
false

The type table#

TypeNotes
int64-bit signed integer. The default for whole-number literals
num64-bit float. Any literal containing .
wordUTF-8 string
booltrue / false, and the aliases yes / no / hai / iie
voidAbsence of a value. Only a return type
List<T>Typed growable sequence. T is a scalar or a shape name; no nesting
ByteBufferBuiltin binary read/write cursor, with real methods
a shape nameAny type you declared with shape

There is no uint, tiny, char, big, float, float32 or never; and no maybe<T>, result<T,E>, Map<K,V>, Set<T>, tuple, fixed array or raw pointer.

The type-annotation grammar#

The parser takes one identifier, plus a single special case for List:

FREAK
pilot a: int = 1                       -- fine
pilot b: word = "x"                    -- fine
pilot c: Rect = Rect { w: 1, h: 2 }    -- fine, Rect is a shape
pilot d: List<int> = [1, 2, 3]         -- fine
pilot e: ByteBuffer = ByteBuffer::new()

-- none of these parse:
--   pilot f: maybe<int> = some(1)
--   pilot g: [int; 4] = [1, 2, 3, 4]
--   pilot h: List<List<int>> = []
--   task i<T>(x: T) -> T { give back x }

List<T> is the only generic form in the language, and it is hard-coded in parser_take_type rather than being a general mechanism — the element must be a plain identifier, so List<List<int>> is rejected with "expected '>' after list element type".

The same grammar applies to shape fields, task parameters, task return types and extern signatures.

Note

List<T> and ByteBuffer are recent additions to V3. Much of the rest of the language still assumes the older untyped int handle, so the two surfaces coexist — see Lists & arrays for which calls accept which.

Numeric literals#

A literal with a . is num; otherwise int.

FREAK
pilot i = 42        -- int
pilot f = 42.0      -- num

There is no negative literal. -5 parses as unary minus applied to 5, which is fine inside an expression but not as an initialiser in every position. The idiom used throughout the compiler's own source is a subtraction:

FREAK
pilot below_zero: int = 0 - 5
say word_from_int(-below_zero)

There are also no numeric suffixes: 42u, 3.14f, 42t and 999b are all V4.

Mixing int and num#

Arithmetic that involves a num on either side produces num. Convert explicitly at the boundary:

FREAK
pilot n: int = 7
pilot f: num = n.to_num()
pilot back: int = f.to_int()
ConversionResult
intValue.to_num()num
intValue.to_word()word
numValue.to_int()int
numValue.to_word()word
boolValue.to_word()word
wordValue.to_int()int
wordValue.to_num()num
word_from_int(i)word
word_from_bool(b)word
format_num(f)word
parse_num(w)num
word_to_int(w)int
char_to_word(code)word

Reserved words are case-insensitive#

This is the single most surprising rule in V3, and the one that produces the most confusing errors.

The lexer lowercases every identifier before comparing it against the keyword table. pilot, Pilot, PILOT and PiLoT are all the same keyword token. A keyword can never be an identifier, so none of those can name a shape, task, field or binding.

examples/reserved_words.fk compilesruns
-- V3 matches keywords CASE-INSENSITIVELY. `pilot`, `Pilot` and `PILOT`
-- are all the same keyword token, so none of them can be used as a name.
--
-- This bites most often on: Pilot, Result, Max, Route, Check, Move,
-- Copy, Some, Ok, Err, Got, Use, In, As, Each, Times, Done, Max.
--
-- Pick a synonym instead. These all compile:
shape Aviator {
    tag: word
}

task outcome(value: int) -> word {
    if value > 0 { give back "positive" }
    give back "non-positive"
}

task main() -> void {
    pilot a: Aviator = Aviator { tag: "V-1" }
    pilot verdict: word = outcome(3)
    pilot upper_bound: int = 100

    say a.tag
    say verdict
    say word_from_int(upper_bound)
}
Program output
V-1
positive
100

Naming a shape Pilot produces:

error: expected an identifier for shape name, found 'Pilot'

The full reserved list#

Every word below is reserved in any casing:

pilot   fixed    task     say      shape    impl     doctrine
launch  use      as       in       lend     mut      move
copy    break    continue if       else     when     repeat
times   until    done     for      each     check    result
got     nobody   some     ok       err      sessions max
foreshadow  payoff  route  sadly   deus_ex_machina   isekai
eventually  and    or      not     nakama   tsundere extern

true  false  yes  no  hai  iie

Multi-word keywords, lexed greedily:

give back   or else    trust me    for each    for science
training arc   bringing back   only on   PLUS ULTRA   FINAL FORM

The names that catch people most often are Pilot, Result, Max, Route, Check, Move, Copy, Some, Ok, Err, Got, Use, In, As, Each, Done and Times. Pick a synonym: Aviator, Outcome, upper_bound, verdict.

Careful

Several of these words are reserved but have no grammar at all in V3 — doctrine, use, launch, check, lend, route, foreshadow, isekai, trust me, for each. They lex as keywords and then fail in the parser with "this token cannot start an expression". They still cannot be used as identifiers.

Top-level code#

A file does not need a main. Statements written at the top level run in order, and top-level bindings are visible to every task in the file.

examples/toplevel.fk compilesruns
-- A file does not need a `main`. Top-level statements run in order,
-- and top-level bindings are visible to every task in the file.
pilot squadron: word = "Valkyries"
pilot strength: int = 4

task roster() -> word {
    give back "{squadron} x{strength}"
}

say "booting"
say roster()

strength = 5
say roster()
Program output
booting
Valkyries x4
Valkyries x5
Note

This is a real difference from the bible, which forbids arbitrary executable statements at root scope and requires root bindings to be fixed pilot constants. V3 allows both.

Comments#

-- starts a comment that runs to the end of the line. There is no block comment form.

FREAK
-- a whole-line comment
pilot x = 1    -- a trailing comment

Every FREAK snippet on this site was compiled by freak 0.14.2 (Maverick), built from source with a verified self-host fixed point. 45/45 examples compile; regenerate with python tools/verify.py then python tools/build_docs.py.