FREAKV3 docs freak 0.15.1 (Maverick)

A guessing game#

Build a complete program, one small step at a time: it picks a number, you guess, it tells you higher or lower.

This chapter does not explain features in order. It builds something, and introduces each feature at the moment the program needs it. You will use pilots, input, conversion from text to numbers, comparison, and loops. The chapters after this one explain each of them thoroughly; here the aim is to see them working together.

The game is a classic. The program picks a secret number from 1 to 100. You type a guess. The program answers "too low" or "too high", and you guess again until you find it. A finished session looks like this:

Guess the number! It is between 1 and 100.
Your guess: 50
Too high.
Your guess: abc
'abc' is not a whole number. Try again.
Your guess: 25
Too low.
Your guess: 37
Too low.
Your guess: 42
You got it in 4 guesses!

Setting up#

Create a project for the game and move into it:

shell
freak init guess
cd guess

Open src/main.fk and delete what is there. Each step below replaces the file's contents. Run each one with freak run and see it work before going on.

Step 1: asking for a guess#

Start with the part that talks to the player: print a greeting, ask for a guess, and repeat it back.

examples/book_guess_input.fk compilesruns
-- Step 1: ask a question and keep the answer.
task main() {
    say "Guess the number!"

    pilot guess = ask("Your guess: ")
    say "You guessed: {guess}"
}
Typed at the prompts
50
Program output
Guess the number!
Your guess: You guessed: 50

There are two new things here.

Illustrative snippet; not independently verified. Use the badged executable examples for tested programs.

FREAK
    pilot guess = ask("Your guess: ")

pilot creates a named value. FREAK calls it a pilot, where other languages say variable. The name is guess, and the value comes from the right of the = sign.

ask prints its prompt, waits for the player to type a line and press Enter, and gives back what was typed. The prompt is not followed by a new line, so the player types on the same line as the question. What comes back is a word, FREAK's type for text. So guess holds a word.

Illustrative snippet; not independently verified. Use the badged executable examples for tested programs.

FREAK
    say "You guessed: {guess}"

Inside a word, a name in braces is replaced by that pilot's value. This is called interpolation, and it is the usual way to mix values into output.

Look at the boxes under the listing. Typed at the prompts is the input the program was given when this book checked it. Program output is what the program itself printed. On your screen you would see Your guess: 50 and then You guessed: 50 on the next line. In the output box they run together, because the 50 and the Enter that followed it were typed by the player. They were never printed by the program, so they are not in its output.

Step 2: from a word to a number#

The player typed 50, but the program has the word "50": two characters, a five and a zero. You cannot compare a word with a number. FREAK will not convert between them unless you ask, so ask.

examples/book_guess_parse.fk compilesruns
-- Step 2: turn the typed text into a number, and notice when it is not one.
task main() {
    pilot text = ask("Your guess: ")

    pilot guess = text.trim().parse_int()
    if parse_status() != 0 {
        parse_clear_status()
        say "'{text}' is not a whole number."
    } else {
        pilot doubled = guess * 2
        say "Twice your guess is {doubled}."
    }
}
Typed at the prompts
fifty
Program output
Your guess: 'fifty' is not a whole number.

Two method calls are chained on the first new line:

Illustrative snippet; not independently verified. Use the badged executable examples for tested programs.

FREAK
    pilot guess = text.trim().parse_int()

A dot after a value calls a method, an operation that belongs to that type of value. text.trim() gives back the word with spaces removed from both ends, in case the player typed 50 . Then .parse_int() reads that word as a whole number. FREAK's type for whole numbers is int, so guess is now an int.

You never wrote a type. FREAK works out that text is a word and guess is an int from the values they are given. The types are still there and still checked. They are inferred.

When the player does not type a number#

The check ran this step with the input fifty, which is not a number, and the program said so. This is the part that deserves attention:

Illustrative snippet; not independently verified. Use the badged executable examples for tested programs.

FREAK
    if parse_status() != 0 {
        parse_clear_status()
        say "'{text}' is not a whole number."
    } else {

parse_int always gives back an int. If the word is not a number, it gives back 0, and it records that the parse failed. parse_status() reports that record: 0 means no parse has failed, 1 means a word was not a number, and 2 means it was a number too large to hold. != means "is not equal to".

So the pattern is: parse, then ask at once whether it worked. The record stays set until you clear it, which is what parse_clear_status() is for. If you forget to clear it, the next successful parse will still look like a failure.

Careful

There is a shorter method, to_int(), which also converts a word to an int. It is lenient: it reads as many digits as it finds at the start of the word, so "12abc" becomes 12, and it gives back 0 when there are none. It records nothing, so you cannot tell "0" from "zero". Use parse_int() for anything a person typed.

if runs the first block when its condition is true and the else block otherwise. Chapter 5 covers it properly.

Interpolation takes names#

In the else block, the program doubles the guess before printing it:

Illustrative snippet; not independently verified. Use the badged executable examples for tested programs.

FREAK
        pilot doubled = guess * 2
        say "Twice your guess is {doubled}."

It would be shorter to write {guess * 2} inside the word. That does not work. Braces interpolate a name: a pilot, or as Part II will show, a field of one. They never interpolate a calculation or a call: the compiler refuses {guess * 2} with the message unsupported interpolation expression. Work the value out first, give it a name, and interpolate the name.

Planned

The specification gives conversion a different shape. "42".to_int() would give back a maybe<int>, a value that is either a number or nothing. You would get the number out with check or or else, both of which make you say what happens when there is none. The status record is how V3 manages without that type. Part VII describes the planned design.

Step 3: a secret number#

The game needs a number the player cannot predict. Most languages have a random-number library. V3 does not have one yet, so this program takes its unpredictability from the clock.

examples/book_guess_secret.fk compilesruns
-- Step 3: a secret number between 1 and 100.
-- V3 has no random-number library, so the clock supplies the unpredictability.
task secret_number() -> int {
    pilot micros = time::monotonic_ns() / 1000
    give back micros % 100 + 1
}

task main() {
    pilot secret = secret_number()
    if secret >= 1 and secret <= 100 {
        say "The secret is somewhere from 1 to 100."
    } else {
        say "Something is wrong with the secret: {secret}"
    }
}
Program output
The secret is somewhere from 1 to 100.

time::monotonic_ns() gives back the number of nanoseconds on a clock that only moves forward. The two colons separate a namespace from a name: time is a group of built-in operations to do with clocks, and monotonic_ns is one of them. You will meet process:: shortly.

The value is large, and at the moment a person starts a program its low digits are as good as random. Not every system's clock ticks in single nanoseconds, though, and on one that does not, the last few digits are always zero. So the task divides by 1000 first, which leaves microseconds, and takes its digits from there.

% is the remainder operator. micros % 100 is what is left after dividing by 100, which is always from 0 to 99. Adding 1 moves that to the range 1 to 100.

This is the first task of your own that gives something back. Its first line says so:

Illustrative snippet; not independently verified. Use the badged executable examples for tested programs.

FREAK
task secret_number() -> int {

-> int declares that the task gives back an int, and give back is the statement that does it. main calls the task by writing its name with parentheses and keeps the answer in a pilot.

The listing cannot print the secret, since a different number each run could not be checked against a fixed expected output. It checks that the number is in range instead. and joins two conditions and is true when both are.

Careful

A clock is fine for a game and wrong for anything that must not be guessed. Do not use this technique for passwords, tokens or keys.

Planned

The specification includes a std::random module, with random::rand_int_range(lo, hi) for a whole number in a range and random::secure() for randomness from the operating system. Neither compiler has it yet.

Step 4: comparing the guess#

Now put a secret and a guess side by side. To keep this step easy to check, the secret is fixed at 42 for the moment.

examples/book_guess_compare.fk compilesruns
-- Step 4: compare one guess with the secret.
task main() {
    pilot secret = 42

    pilot text = ask("Your guess: ")
    pilot guess = text.trim().parse_int()

    if guess < secret {
        say "Too low."
    } else if guess > secret {
        say "Too high."
    } else {
        say "You got it!"
    }
}
Typed at the prompts
50
Program output
Your guess: Too high.

< and > compare two numbers. An if can be followed by any number of else if branches and one final else. The conditions are tried from the top, and the first true one wins. If the guess is neither lower nor higher than the secret, only one possibility is left, so the last branch needs no condition.

Both guess and secret are ints. Had you forgotten to parse the guess and tried to compare the word text with secret, the compiler would have refused to build the program. Chapter 3 shows what it says.

Step 5: guessing until it is right#

One guess is not much of a game. The program has to keep asking. That needs a loop, and a way to stop.

Here is the complete program. Read it once, and then go through the notes below.

examples/book_guess.fk compilesruns
-- guess.fk: the finished guessing game.
--
-- Run it with no arguments to play against the clock's choice of secret.
-- Pass a number (`./guess 42`) to fix the secret, which is how this
-- program is tested.

task secret_number() -> int {
    if process::args_count() > 1 {
        pilot chosen = process::arg(1).parse_int()
        if parse_status() == 0 { give back chosen }
        parse_clear_status()            -- not a number: fall back to the clock
    }
    pilot micros = time::monotonic_ns() / 1000
    give back micros % 100 + 1
}

task main() {
    say "Guess the number! It is between 1 and 100."

    pilot secret = secret_number()
    pilot mut attempts = 0
    pilot mut solved = false

    repeat until solved {
        pilot text = ask("Your guess: ")

        -- An empty line means the input ended (or the player just pressed
        -- Enter). Stop instead of asking forever.
        if text.trim() == "" {
            say "No guess given. The number was {secret}."
            break
        }

        pilot guess = text.trim().parse_int()
        if parse_status() != 0 {
            parse_clear_status()
            say "'{text}' is not a whole number. Try again."
            continue
        }

        attempts += 1

        if guess < secret {
            say "Too low."
        } else if guess > secret {
            say "Too high."
        } else {
            solved = true
        }
    }

    if solved {
        if attempts == 1 {
            say "You got it in one guess!"
        } else {
            say "You got it in {attempts} guesses!"
        }
    }
}
Run with arguments
42
Typed at the prompts
50
abc
25
37
42
Program output
Guess the number! It is between 1 and 100.
Your guess: Too high.
Your guess: 'abc' is not a whole number. Try again.
Your guess: Too low.
Your guess: Too low.
Your guess: You got it in 4 guesses!

The loop#

Illustrative snippet; not independently verified. Use the badged executable examples for tested programs.

FREAK
    pilot mut solved = false

    repeat until solved {

repeat until runs its block again and again, and stops when the condition becomes true. The condition here is a pilot, solved, which starts out false. true and false are values of the type bool.

When the player finds the number, the last branch of the comparison sets solved = true. The loop tests the condition before each pass, finds it true, and stops.

mut#

solved and attempts are declared with pilot mut. mut says that the pilot's value will change after it is created. It is a note to the reader: a plain pilot can be changed too, and Chapter 3 shows the form that cannot. secret never changes, so it is a plain pilot.

attempts += 1 is short for attempts = attempts + 1.

continue and break#

Two statements change the course of a loop from inside.

continue abandons the rest of the current pass and goes back to the top of the loop. The program uses it after a guess that is not a number: there is nothing to compare, so it asks again. Because continue comes before attempts += 1, a mistyped guess is not counted.

break leaves the loop at once. The program uses it when the player gives no guess at all. That happens when the player presses Enter on an empty line, and also when the input ends, for instance because the program was fed from a file that ran out. Without this check, a program reading from an exhausted input would ask forever.

After the loop, solved tells the two ways of leaving apart. It is true only if the player actually found the number.

A secret you can choose#

Look again at secret_number:

Illustrative snippet; not independently verified. Use the badged executable examples for tested programs.

FREAK
task secret_number() -> int {
    if process::args_count() > 1 {
        pilot chosen = process::arg(1).parse_int()
        if parse_status() == 0 { give back chosen }
        parse_clear_status()            -- not a number: fall back to the clock
    }
    pilot micros = time::monotonic_ns() / 1000
    give back micros % 100 + 1
}

A program can be started with arguments, extra words after its name on the command line. process::args_count() says how many there are, and process::arg(1) is the first one. Argument 0 is the program's own name, which is why the count has to be greater than 1 for there to be a real argument.

If an argument is given and it is a number, the task gives it back immediately. A give back ends the task on the spot, so the lines after it run only when there was no usable argument.

Notice that the argument is parsed with the same care as the player's guesses. The parse is checked at once, and a failure is cleared. Without those two lines, starting the game with an argument that is not a number would leave the failure recorded, and the player's first perfectly good guess would be reported as not a number.

This is not a cheat mode for its own sake. A program whose behaviour depends on the clock cannot be tested, because you cannot say in advance what it should print. Giving the secret from outside makes the game repeatable. The listing above was checked exactly that way: started with the argument 42, and fed five lines of input.

Playing it#

To play with a chosen secret, pass it after --. Everything after -- goes to your program, not to freak:

shell
freak run -- 42

To play properly, leave the argument off:

shell
freak run

freak build makes a program you can start without freak. In this project it is written next to the source, as src/main:

shell
freak build
./src/main 42

Try the edges. Type a word instead of a number. Type a number with spaces around it. Press Enter on an empty line. The program has an answer for each.

In V4#

At the commit this book is pinned to, V4 cannot build this game, for two reasons. It does not know ask, and reports it the way it reports any name it cannot place:

examples-v4/ask.fk V4 snapshot 7c9a1f3c27rejected
task main() -> int {
    pilot text = ask("Your guess: ")
    give back 0
}
V4 diagnostic
v4-native-contract-error=native type not yet supported: unknown

And it cannot yet hold a word in a pilot, so there would be nowhere to keep a guess:

examples-v4/word_pilot.fk V4 snapshot 7c9a1f3c27rejected
task main() -> int {
    pilot callsign = "Valkyrie-1"
    give back 0
}
V4 diagnostic
v4-native-contract-error=native words currently support only literal say operands

What V4 does have is everything the game decides with: ints, comparison, tasks, and loops.

So here is the game's logic with the conversation removed. The program plays against itself. It guesses the middle of the range, uses the verdict to halve the range, and counts its guesses. With nothing to print the count with, it gives the count back from main, where it becomes the exit code.

examples-v4/guess_logic.fk V4 snapshot 7c9a1f3c27runs
-- The heart of the guessing game, without the talking: find a secret by
-- halving the range, and report the number of guesses as the exit code.
task judge(guess: int, secret: int) -> int {
    if guess < secret { give back 0 - 1 }
    if guess > secret { give back 1 }
    give back 0
}

task main() -> int {
    pilot secret = 42
    pilot low = 1
    pilot high = 100
    pilot attempts = 0
    pilot solved = false

    repeat until solved {
        pilot guess = (low + high) / 2
        pilot verdict = judge(guess, secret)
        attempts += 1

        if verdict < 0 {
            low = guess + 1
        } else if verdict > 0 {
            high = guess - 1
        } else {
            solved = true
        }
    }

    give back attempts
}
Exit code 7

Seven guesses to find 42: it tries 50, 25, 37, 43, 40, 41 and then 42.

Two details differ from the V3 listing. main declares -> int, as Chapter 1 explained. And the pilots that change are declared without mut. At this commit V4 does not accept pilot mut, and lets every pilot be reassigned:

examples-v4/pilot_mut.fk V4 snapshot 7c9a1f3c27rejected
task main() -> int {
    pilot mut fuel = 50
    fuel = 75
    give back fuel
}
V4 diagnostic
2|0@25:44|unsupported local pattern|use a single name, (a, b, ...), or [a, b, ...]

V3 builds this V4 listing as it stands, in its strict mode too, and it gives the same exit code. Both follow the specification's section on pilots: a pilot can be reassigned unless it is declared fixed. The difference left is mut itself, which V3 accepts as an optional word and this V4 commit rejects.

Summary#

You built a complete interactive program. Along the way you used:

  • pilot to name a value, and pilot mut to say that it will change
  • ask to read a line, and say with {name} interpolation to print
  • methods on words: trim() and parse_int(), with parse_status() to learn whether the parse worked
  • the types word, int and bool, which you wrote down only once, as a task's return type
  • if, else if and else to choose
  • repeat until to loop, with continue and break to steer
  • a task that gives back a value, built-in operations in the time:: and process:: namespaces, and command-line arguments to make the program testable

Each of these was introduced quickly. The next three chapters go back over them with care. Chapter 3 starts with pilots and the types of the values they hold.

Badged executable examples were compiled and run with the released freak 0.15.1 (Maverick). 84/84 examples match their reviewed expected output. Illustrative snippets are labelled separately. Regenerate with python tools/refresh.py.

V4 boxes were checked against the development snapshot 7c9a1f3c27 (2026-10-04); 46/46 V4 examples match their reviewed expectation. Regenerate with python tools/verify_v4.py --checkout <Freak-lang>.