Tutorial 1: Your first program#
> Build and run a FREAK program from nothing, then grow it into something with a shape and a method. About 15 minutes.
This assumes you have a working freak on your PATH. If not, do Getting started first — you need to build the compiler from source, because V3 is self-hosted.
Step 1 — Say something#
Create greet.fk:
task main() -> void {
say "Valkyries, launch"
}Run it:
freak run greet.fkTwo things are already worth noticing. say needs no import — it is a compiler builtin, always in scope. And main is optional: statements written at the top level of a file run in order, so this would work too:
say "Valkyries, launch"Use main anyway. It gives you somewhere to put local bindings.
Step 2 — Bindings#
pilot introduces a binding. Add a couple:
task main() -> void {
pilot callsign: word = "Valkyrie-1"
pilot sorties: int = 12
say callsign
say word_from_int(sorties)
}The annotation is optional — pilot sorties = 12 infers int. Note word_from_int: say accepts int directly, but the moment you want to glue a number onto a word you must convert it, because + will not mix types.
There are five types worth knowing at this stage: int, num (float), word (string), bool, and void. Full list in Bindings & types.
**Do not name anything Pilot.** V3 matches keywords case-insensitively, so Pilot, PILOT and pilot are the same token, and none can be an identifier. The same trap catches Result, Max, Check, Route, Move, Copy, Some, Ok, Err and Done. Pick a synonym — this tutorial uses Aviator.
Step 3 — Interpolation#
Instead of converting and concatenating, put the value in the string:
task main() -> void {
pilot callsign: word = "Valkyrie-1"
pilot sorties: int = 12
say "{callsign} has flown {sorties} sorties"
}{path} substitutes a binding. A path is an identifier plus optional .field hops — callsign, unit.name, self.frame.thrust.
Interpolation substitutes paths, never calls. "{rank()}" prints those characters literally rather than calling anything. Compute into a binding first — this is the single most common surprise in V3.
Step 4 — A task of your own#
task declares a function; give back returns from it.
task rank_for(sorties: int) -> word {
if sorties >= 50 { give back "veteran" }
if sorties >= 10 { give back "regular" }
give back "rookie"
}
task main() -> void {
say rank_for(12)
}There is no implicit return. A block-bodied task that returns a value needs give back on every path — a bare expression at the end is evaluated and discarded. The arrow shorthand => expr from the specification does not parse in V3.
Order does not matter: every task is indexed before any body is checked, so main can call a task declared below it.
Step 5 — A shape#
shape is V3's record type, and the only user-defined type there is.
shape Aviator {
name: word
sorties: int
}
task main() -> void {
pilot takeru: Aviator = Aviator { name: "Shirogane Takeru", sorties: 12 }
say takeru.name
say word_from_int(takeru.sorties)
}Every field must be supplied by name at construction. Fields are readable and assignable, and they chain: unit.frame.thrust.
Step 6 — Methods#
impl attaches tasks to a shape. Whether the first parameter is self decides how you call it.
impl Aviator {
-- No self: an associated method, called Aviator::recruit(...)
task recruit(name: word) -> Aviator {
give back Aviator { name: name, sorties: 0 }
}
-- With self: an instance method, called value.rank()
task rank(self) -> word {
if self.sorties >= 50 { give back "veteran" }
give back "rookie"
}
}Write self bare, never self: Aviator — the compiler fills in the owning shape.
Step 7 — Loops#
Three forms, no for each:
repeat 12 times { takeru.sorties += 1 } -- counted
repeat until done { work() } -- condition, tested first
training arc until ready max 8 sessions { } -- bounded, cannot spin foreverTo reassign a binding you must declare it pilot mut. Without --strict-borrow the compiler will not enforce that, but write it anyway — it documents intent and the checker will demand it once you turn the flag on.
The finished program#
Everything above, assembled:
-- Tutorial 1, finished program.
--
-- A greeter that takes a name, decides how formal to be, and reports.
-- Everything here is core V3: bindings, a shape, a method, a task,
-- interpolation, a conditional and a counted loop.
shape Aviator {
name: word
sorties: int
}
impl Aviator {
-- An associated method: called as Aviator::recruit(...).
task recruit(name: word) -> Aviator {
give back Aviator { name: name, sorties: 0 }
}
-- An instance method: `self` is the receiver.
task rank(self) -> word {
if self.sorties >= 50 { give back "veteran" }
if self.sorties >= 10 { give back "regular" }
give back "rookie"
}
task greeting(self) -> word {
-- Interpolation substitutes PATHS only, never calls. Writing
-- "{self.rank()}" would print those characters literally, so the
-- call result goes into a binding first.
pilot rank: word = self.rank()
give back "{self.name} - {self.sorties} sorties, rank {rank}"
}
}
task main() -> void {
pilot mut takeru: Aviator = Aviator::recruit("Shirogane Takeru")
-- Fly a few missions.
repeat 12 times {
takeru.sorties += 1
}
say takeru.greeting()
pilot rank: word = takeru.rank()
say "logged as: {rank}"
if takeru.sorties > 10 {
say "cleared for the next operation"
} else {
say "needs more flight hours"
}
}
Shirogane Takeru - 12 sorties, rank regular
logged as: regular
cleared for the next operation
Where to go next#
- Tutorial 2 — a real command-line tool with files and colour
- Tutorial 3 — lists, and modelling data properly
- Control flow — the loop and branch forms in detail
- Not in V3 — what to write instead of the features the specification promises