FREAKV3 docs freak 0.15.1 (Maverick)

Hangar & packages#

A project is a hangar.toml and the sources it names. This page covers what the compiler reads from that manifest, how one project uses another, and what is still missing.

Hangar is FREAK's package manager, shipping as freak hangar <cmd> and as a standalone hangar binary. As of 0.15 the compiler reads the project manifest too: freak build, freak run and freak test work on a whole project, and a project can import tasks from another one.

A project#

freak init NAME creates one; --template=cli, lib or http chooses the starting layout. Its manifest looks like this:

TOML
[project]
name = "sortie"
version = "0.1.0"
kind = "app"
entry = "src/main.fk"

[modules]
greet = "src/greet.fk"

[exports]
greet = "greet::greet"

[tests]
greeting = "tests/greeting_test.fk"

[dependencies]
TableWhat it is for
[project]kind must be app or lib. An app names its entry file
[modules]The project's other source files, each under a module name
[exports]The tasks another project may import, written module::task
[tests]The programs freak test builds and runs
[dependencies]The projects this one may import from

Inside the project directory, freak build and freak run need no file name. A file imports from one of the project's own modules with use self:::

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

FREAK
use self::greet::{greet}

task main() {
    say greet("sortie")
}

Using another project#

A library lists what it offers under [exports]. A project that wants it names it under [dependencies], and imports the exported task through that name:

TOML
[dependencies]
demo = { path = "../.." }

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

FREAK
use demo::{greet}

task main() {
    say greet("consumer")
}

Verified against the shipping compiler, in the consumer project that freak init greeter --template=lib generates under examples/consumer:

$ freak run
Hello, consumer!

An import is checked, not skipped:

You writeThe compiler answers
use greeter::{hello} with no greeter dependency declaredundeclared direct dependency 'greeter'
use demo::{nothing}missing public export 'nothing'
use demo::*import requires {symbol, symbol as alias}
use std::nonexistentunsupported standard module 'nonexistent'
Note

Only a path dependency was run for this page. The 0.15.0 release notes say build, run and test also consume Git dependencies pinned in hangar.lock.

Note

hangar init writes kind = "app" and entry = "src/main.fk" since 0.15.1. A project made by an older hangar init, without those keys, still installs, but freak build refuses it: "project.kind must be 'app' or 'lib'". Add the two keys to fix it.

What does get linked#

cli_load_std in src/cli/build.fk is how the standard library reaches your program. It concatenates a fixed list ahead of your source:

SourceWhen
std/math.fk, std/string.fk, std/convert.fk, std/algorithm.fk, std/json.fk, std/version.fkAlways
std/runtime.fk, std/http.fkAlways, LLVM backend only
std/math3d.fkThe source text contains use std::math3d
std/zip.fkThe source text contains use std::zip
std/ui/window.fkThe source text contains use std::ui
std/test.fkThe source text contains use std::test
std/json_document.fkThe source text contains use std::json_document
std/http_server.fkThe source text contains use std::http_server

Those six use forms are textual triggers, not imports — the build greps your source for the string and links a hard-coded path. Everything else already in that list is in scope whether you write use or not. Packages are not part of this list: they come in through [dependencies] and use name::{item}, as described above.

Commands#

These are the hangar subcommands.

CommandEffect
hangar init [name]Scaffold hangar.toml, src/main.fk, hangar_modules/
hangar add <name> [constraint]Add a registry dependency (latest version if no constraint)
hangar add <name> --path <dir>Add a local path dependency
hangar add <name> <repo> [version]Add a dependency from a git repo
hangar remove <name>Remove it
hangar installDownload all dependencies into hangar_modules/
hangar install freakInstall or update the FREAK toolchain itself
hangar update [package]Re-resolve and update hangar.lock
hangar outdatedReport packages behind their constraint
hangar versionPrint the project version
hangar version patch|minor|majorBump it in the manifest
hangar audit / hangar audit --fixVerify or repair package integrity
hangar auth login / hangar auth status / hangar auth logoutSign in, see who you are, sign out (hangar login / hangar logout are shortcuts)
hangar publish [--dry-run]Publish to the registry

hangar search from the specification does not exist in this build.

Careful

On Windows, hangar add <name> <git-url> fails in 0.15.1 while cleaning up its temporary clone ("could not safely remove complete owned temporary directory"), because Git's read-only object files can't be deleted. Clone the repository yourself and use hangar add <name> --path <clone> until the fix ships.

Signing in#

hangar auth login runs a device flow: it prints a short one-time code, opens the registry in your browser, and waits while you approve it there. What it saves is a session tied to your account, stored in your user config directory with owner-only permissions. hangar auth status says who you are signed in as ("Not signed in to https://hangar.freaklang.dev/api/v1. Run: hangar auth login" when you aren't), and hangar auth logout revokes the session.

Publishing is tied to an account: the first publish of a name claims it, and nobody else can ship that name afterwards. hangar publish --dry-run shows what would be uploaded. The registry defaults to https://hangar.freaklang.dev/api/v1; set HANGAR_REGISTRY to use another.

Note

Tokens saved by 0.15.0's hangar login don't record which registry they belong to, so Hangar only sends them to the default registry. Sign in again to use another one.

Registry dependencies#

TOML
[dependencies]
cockpit = "^0.1.0"

hangar install resolves this against the registry, pins it in hangar.lock by archive SHA-256, and caches the archive so later builds work offline. A package with dashes in its name is imported with underscores: cockpit-extras becomes use cockpit_extras::{...}.

Caret ranges on 0.x versions follow Cargo and npm since 0.15.1: ^0.1.0 means >=0.1.0, <0.2.0, and ^0.0.3 means exactly 0.0.3. A constraint nothing satisfies is refused with "no published version of NAME satisfies CONSTRAINT". If you want newer minor versions of a 0.x package, write >=0.1.0 instead, knowing that it also accepts 1.0.0 and every later major version.

hangar.toml#

freak init writes the manifest shown at the top of this page. hangar init writes a smaller one, in dotted-key form:

TOML
project.entry = "src/main.fk"
project.kind = "app"
project.name = "demo"
project.version = "0.1.0"
Careful

The specification's §6.2 shows a different manifest — [unit] rather than [project], plus edition, a [build] table with mode/target/voice, and [profile.*] sections. None of that is written or read by the shipping tools. They use the tables listed under A project.

Version constraints are resolved by std/version.fk, which is real, working semver: ver_satisfies("1.4.2", "^1.4") is true. You can call that machinery directly — see Standard library.

hangar.lock records resolved versions.

Single files#

A file built on its own, outside any project, is still one translation unit in one flat global namespace. There a "library" is just source you concatenate ahead of your program:

shell
cat lib/text.fk src/app.fk > build/combined.fk
freak build build/combined.fk

Order matters only for shape declarations: a shape must be registered before a constructor for it is parsed, so put library shapes first. Tasks can be in any order — every task is indexed before any body is checked.

This is exactly how the compiler builds itself. The eight V3 sources and the CLI sources are cat-ed into one translation unit; the file split is a maintenance convention, not a module system. See Getting started.

The concatenated pattern#

examples/module_pattern.fk compilesruns
-- Reusing code across files without a project.
--
-- A file built on its own is one translation unit, so outside a project
-- a "library" is just source you concatenate ahead of your program:
--
--     cat lib/text.fk src/app.fk > build/combined.fk
--     freak build build/combined.fk
--
-- This file is what that produces. Everything lands in one flat global
-- namespace, so a library must prefix its names to avoid collisions --
-- with your code, and with the std/ tasks the build already links.

-- ===================================================================
-- lib/text.fk  -- the "package"
-- ===================================================================

shape TextStats {
    words: int
    longest: word
}

task text_is_blank(c: word) -> bool {
    give back c == " " or c == "\n" or c == "\t" or c == "\r"
}

task text_count_words(source: word) -> int {
    pilot mut total: int = 0
    pilot mut in_word: bool = false
    pilot mut i: int = 0

    repeat source.length() times {
        if text_is_blank(source.char_at(i)) {
            in_word = false
        } else {
            if not in_word { total += 1 }
            in_word = true
        }
        i += 1
    }
    give back total
}

task text_longest_word(source: word) -> word {
    pilot mut best: word = ""
    pilot mut current: word = ""
    pilot mut i: int = 0

    repeat source.length() times {
        pilot c: word = source.char_at(i)
        if text_is_blank(c) {
            if current.length() > best.length() { best = current }
            current = ""
        } else {
            current = current + c
        }
        i += 1
    }
    if current.length() > best.length() { best = current }
    give back best
}

task text_analyse(source: word) -> TextStats {
    give back TextStats {
        words: text_count_words(source),
        longest: text_longest_word(source)
    }
}

-- ===================================================================
-- src/app.fk  -- your program
-- ===================================================================

task main() -> void {
    pilot report: TextStats = text_analyse("the beta will not negotiate with humanity")

    say word_from_int(report.words)
    say report.longest

    -- No `use` line is needed, or possible, here: outside a project a
    -- `use` must name a standard module.
}
Program output
7
negotiate

Conventions that make it survivable#

A single file shares one flat namespace with every linked std/ file and every library you concatenate. Two tasks of your own with one name are an error, and a task named like a std/ task quietly replaces it, so:

  • Prefix everything a library exports. text_count_words, not count_words. The standard library already does this — string_*, std_*, ver_*, json_*, array_*.
  • **Watch for std/ collisions.** Defining your own array_find or string_trim replaces the standard one for your whole program. A compiler builtin such as ask cannot be redefined at all.
  • Remember reserved words are case-insensitive. A library cannot export anything named Result, Max, Route or Check in any casing. See reserved words.
  • Keep a build script. Outside a project, a one-line cat in a .sh/.bat is your build system. Regenerate the combined file every time.

A Makefile-shaped example#

shell
#!/bin/sh
set -e
mkdir -p build
cat lib/text.fk lib/report.fk src/app.fk > build/combined.fk
freak build build/combined.fk
mv build/combined.exe build/app.exe

Line numbers in diagnostics refer to the combined file. The compiler maintains a source map for the std prefix it adds itself, but it cannot know about your cat, so keep the concatenation order stable if you want stable offsets.

What is still missing#

The specification's module system (§6 and §17) goes further than the manifest does. launch visibility does not parse, there is no launch(package), and a glob import is refused. [exports] is the only visibility there is: it limits what another project can import. Inside one project, any module can import any task of another with use self::.

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>.