Skip to content

CLI Reference

The sfn binary is the primary interface to the Sailfin toolchain. It compiles, runs, and tests Sailfin programs. The binary is also available as sailfin; both names refer to the same executable.

Terminal window
sfn <command> [options] [arguments]

The current version can always be confirmed with sfn --version.


Compile and execute a Sailfin source file in a single step. The file is compiled to a temporary binary and executed immediately. No output file is produced.

Usage:

Terminal window
sfn run <file.sfn>

Examples:

Terminal window
sfn run hello.sfn
sfn run examples/basics/hello-world.sfn
sfn run src/main.sfn

Behavior:

  • Compiles the source file from scratch on each invocation.
  • Effect annotations are checked at compile time; missing effects produce effects.missing diagnostics with fix-it hints.
  • The program runs in the same terminal session; stdout and stderr are connected to the calling terminal.
  • Exit code of sfn run reflects the exit code of the program.

Discover and run Sailfin test files. Test files follow the *_test.sfn naming convention. Without a path argument, sfn test discovers all *_test.sfn files under the current directory.

Usage:

Terminal window
sfn test [path]

Examples:

Terminal window
sfn test # run all *_test.sfn files found
sfn test compiler/tests/unit/ # run tests in a specific directory
sfn test compiler/tests/unit/arrays_test.sfn # run a single test file

Note: --filter is not yet supported. To narrow test scope, pass a specific file or directory path.

Test file conventions:

  • Test files must be named *_test.sfn.
  • Each test is declared with the test keyword:
test "addition is commutative" {
assert add(2, 3) == add(3, 2);
}
  • Test results are reported with pass/fail counts and source locations for failures.
  • A non-zero exit code is returned if any test fails.

Format Sailfin source files. One style, no configuration — like gofmt for Sailfin.

The formatter operates on the token stream (not the AST), so it works on any syntactically valid file including partial or experimental code. It preserves all comments and produces deterministic, idempotent output.

Usage:

Terminal window
sfn fmt <file.sfn> # print formatted output to stdout
sfn fmt --write <path...> # format files in place
sfn fmt --check <path...> # exit 1 if any file would change (CI mode)

Options:

FlagDescription
--writeOverwrite files in place with formatted output
--checkCheck if files are formatted; exit 1 if any differ (no modifications)

--check and --write are mutually exclusive. Without either flag, formatted output is printed to stdout.

Paths can be individual .sfn files or directories. When given a directory, sfn fmt recursively discovers all .sfn files (up to 10 levels deep).

Examples:

Terminal window
# Format a single file and see the result
sfn fmt src/main.sfn
# Format all compiler sources in place
sfn fmt --write compiler/src/
# CI check — fails if any file is unformatted
sfn fmt --check compiler/src/ runtime/

Formatting rules:

  • Indentation: 4 spaces per level (no tabs)
  • Spacing: Spaces around operators and after keywords; no space before :, ;, or ? (optional suffix); no space after unary ! and -
  • Braces: K&R style — opening brace on the same line as the declaration
  • Blank lines: Exactly one between top-level declarations; suppress at block boundaries
  • Imports: Sorted by path (standard library first, then relative), specifiers sorted alphabetically within each import
  • Inline blocks: Single-statement blocks stay on one line when they fit (e.g., if x { return true; }); multi-statement blocks and struct/enum declarations always expand
  • Comments: Preserved exactly as written; leading and trailing comments kept attached to their tokens
  • Wrapping: Import specifier lists and struct literals wrap at 80 characters

Known limitations:

LimitationDescription
No expression wrappingLong expressions stay on one line; the formatter does not break at operator boundaries
No --diff modeCannot show inline diff between original and formatted; use diff externally
Bulk OOM on many filessfn fmt --write <large-directory> may OOM when processing hundreds of files in a single invocation; use a per-file loop as workaround (see below)
No semantic formattingCannot reorder match arms, inline function bodies, or restructure code
Comment-blank-line collapseBlank lines between section-divider comments and declarations may be normalized away

Workaround for bulk formatting:

Terminal window
# Per-file loop avoids OOM on large codebases
find compiler/src runtime -name '*.sfn' | while read f; do
sfn fmt --write "$f"
done

Run the compiler’s analysis passes — parse, typecheck, and effect-check — without emitting .sfn-asm, LLVM IR, or invoking clang. Used as a fast inner-loop “does this file still look sane?” gate. Returns in seconds rather than the minutes a full build takes.

Usage:

Terminal window
sfn check # check every .sfn file under the current directory
sfn check compiler/src # check a specific directory
sfn check compiler/src/main.sfn # check a single file
sfn check src lib # check multiple paths in one invocation
sfn check --quiet compiler/src # suppress diagnostic output; exit code only

Options:

FlagDescription
--quiet, -qSuppress diagnostic output; only the summary and exit code are produced
-h, --helpPrint usage and exit

What gets checked:

  1. Parseparse_program() builds the AST (parser recovers from most errors; a future enhancement will surface parse-stage diagnostics).
  2. Type checktypecheck_diagnostics() reports duplicate symbols (E0001), missing interface members (E0301), interface type-argument mismatches (E0302), and scope violations.
  3. Effect checkvalidate_effects() reports routines that call effectful APIs (print.*, fs.*, http.*, sleep, @logExecution, …) without declaring the matching ![...] effect.

All three passes run regardless of earlier failures — you see every diagnostic in one pass rather than fix-one / rerun cycles.

Output format:

Diagnostics are printed to stderr, one error[CODE]: header per finding with source context and a caret. Effect violations list the triggering calls and a suggested fix. A summary line is printed to stdout:

checked 120 files: ok
checked 120 files: 3 errors

Error codes introduced by sfn check:

CodeMeaning
E0400Function calls effectful APIs without declaring ![...]
E0401Decorator (@trace, @logExecution) requires ![io] but function doesn’t declare it

Typecheck codes (E0001, E0301, E0302) are shared with the regular compilation pipeline.

Exit codes:

CodeMeaning
0No diagnostics
1One or more diagnostics were produced (or no .sfn files were found)
2Usage error (bad flag or path not found)

Why: A full make compile takes many minutes; sfn check gives you the parse/typecheck/effect verdict in seconds. Use it during editing, wire it into pre-commit hooks, or run it from CI as an early-gate before the full build.


Compile a Sailfin source file without running it. Writes an executable to the output path (default: the source filename without .sfn in the current directory).

Usage:

Terminal window
sfn build <file.sfn> [-o <output>]

Options:

FlagDescription
-o <output>Write the compiled binary to output instead of the default path

Examples:

Terminal window
sfn build src/main.sfn
sfn build src/main.sfn -o build/myapp

Subcommand: emit native

The native compiler also supports emitting .sfn-asm intermediate representation for inspection:

Terminal window
sfn emit native compiler/src/version.sfn

This is primarily used by the self-hosting build scripts.


Display the compiler version string and exit with code 0.

Usage:

Terminal window
sfn --version

Example output:

sfn 0.5.1

The version string follows semantic versioning: <major>.<minor>.<patch> for stable releases, <major>.<minor>.<patch>-alpha.<n> for pre-releases. Local dev builds include a git hash suffix: <version>+dev.<hash> (e.g. sfn 0.5.1+dev.abc1234).

The version is read from compiler/capsule.toml at runtime. For installed binaries where that file is not available, a baked-in fallback is used.


Sailfin ships with package-management commands that target a default public registry at pkg.sfn.dev. All registry-touching commands (sfn add, sfn publish) resolve the registry URL through the same three-tier precedence — see sfn config below for how to redirect the toolchain at a private registry.

Scaffold a new Sailfin capsule in the current directory. Writes a capsule.toml manifest (with the capsule name inferred from the directory) and a starter src/main.sfn.

Usage:

Terminal window
sfn init

Fails with a non-zero exit if capsule.toml already exists — sfn init never overwrites existing manifests.


Add a capsule dependency to the current project. The manifest (capsule.toml) is updated, the package is pre-fetched into ~/.sfn/cache/capsules/<scope>/<name>/<version>/, and capsule.lock records the resolved version and SHA-256 integrity hash.

Flags:

  • --dev — add to [dev-dependencies] instead of [dependencies].
  • --update — ignore the existing lockfile entry and resolve the latest version from the registry.

Usage:

Terminal window
sfn add http # stdlib capsule (resolved to sfn/http)
sfn add --dev test # dev-only dependency
sfn add acme/router # third-party scoped capsule
sfn add --update acme/router # force a fresh lookup

If a capsule.lock entry already exists and --update is not passed, the locked version is used without contacting the registry.


Package a capsule (current directory by default, or the provided path) into the SFNPKG format and upload it to the configured registry.

Usage:

Terminal window
sfn publish # package and upload the capsule at cwd
sfn publish path/to/capsule # or publish a capsule elsewhere on disk

Authentication: the command reads your bearer token from the SFN_TOKEN environment variable first, then falls back to ~/.sfn/credentials (written by sfn login). If neither is set, publishing fails with a clear error.

Payload: the capsule’s capsule.toml and every src/**/*.sfn file are concatenated into a SFNPKG/1 bundle, SHA-256 digested, base64-encoded, and POSTed as JSON to <registry>/api/publish. Non-2xx HTTP responses are surfaced with the server error message.


Store a registry bearer token at ~/.sfn/credentials (mode 600, inside ~/.sfn/ which is created at mode 700). This is the token sfn publish uses when SFN_TOKEN is not set.

Usage:

Terminal window
sfn login # prompts and reads one line from stdin
sfn login <token> # or pass the token as an argument
echo "<token>" | sfn login # or pipe it in

Rejects empty tokens. Overwrites any existing credentials file.


sfn config <get|set|unset|list> [key] [value]

Section titled “sfn config <get|set|unset|list> [key] [value]”

Persist per-user toolchain settings to ~/.sfn/config.toml (mode 600). The only supported key today is registry, which controls where sfn add and sfn publish look for capsules.

Subcommands:

FormDescription
sfn config listPrint every resolved setting.
sfn config get <key>Print the resolved value for a single key.
sfn config set <key> <value>Persist <value> for <key>.
sfn config unset <key>Remove the key from ~/.sfn/config.toml, reverting to the default.

Usage:

Terminal window
sfn config set registry https://registry.acme.internal # point at a private registry
sfn config get registry # https://registry.acme.internal
sfn config list # registry = https://registry.acme.internal
sfn config unset registry # back to the default

Resolution order for the registry URL (highest priority first):

  1. SFN_REGISTRY environment variable — a one-shot override useful for CI.
  2. [registry] url in ~/.sfn/config.toml — persisted by sfn config set.
  3. Compiled-in default: https://pkg.sfn.dev.

The URL must start with http:// or https:// and may not contain whitespace, quotes, or shell metacharacters. Invalid values are rejected at sfn config set and silently ignored (with a warning to stderr) when coming from the environment or a hand-edited config file.

Enterprise example: host a mirror behind your firewall and opt everyone in by adding a single line to your shell profile:

Terminal window
export SFN_REGISTRY=https://registry.acme.internal

Or put it in ~/.sfn/config.toml once per workstation with sfn config set registry ... — no shell changes required.


The repository Makefile provides higher-level build orchestration for the self-hosting workflow. These targets are for working on the Sailfin compiler itself; end users generally only need sfn run and sfn test.

TargetDescription
make compileBuild the native compiler binary from a released seed, using the self-hosting pipeline. Skips rebuild if the binary is up to date.
make rebuildForce a rebuild from a released seed regardless of timestamps. Routes through <seed> build -p compiler (the self-hosting driver path); the prior scripts/build.sh orchestrator was retired in Stage E PR7 (#383) and is no longer in-tree.
make installInstall the built compiler binary into $(BINDIR) (default: ~/.local/bin). Requires make compile to have run first.
make checkCompile (if needed), build a sailfin-seedcheck binary, verify it can run hello-world.sfn, then run the full test suite against it. This is the authoritative CI gate.
make testRun the full Sailfin-native test suite (unit + integration + e2e). Requires make compile first.
make test-unitRun unit tests from compiler/tests/unit/*_test.sfn.
make test-integrationRun integration tests from compiler/tests/integration/*_test.sfn.
make test-e2eRun end-to-end tests from compiler/tests/e2e/*_test.sfn.
make packageBuild and package native artifacts into dist/. Used for release artifacts.
make fetch-seedDownload the latest released seed compiler from GitHub Releases into build/seed/. Requires GITHUB_TOKEN.
make cleanRemove dist/ packaged artifacts. Does not remove build intermediates.
make clean-buildRemove build/ artifacts (keeps build/seed/ by default). Pass KEEP_SEED=0 to also remove the downloaded seed.
make clean-allRemove both dist/ and build/ artifacts completely.
make helpPrint a summary of available targets.

<seed> build -p compiler (the path make rebuild now routes through, as of Stage E PR2) runs stage_capsule_imports and compile_capsule_modules in parallel via the resolver’s xargs fan-out (Stage E PR3 / #278). Cold builds measure ~2m27s on a 4-core box with the parallel resolver — matching the historical ~2 min with --jobs 4 that the prior (now retired) bash scripts/build.sh used to provide. Parallelism is controlled by SAILFIN_BUILD_JOBS (default: nproc, clamped to [1, 8]); SAILFIN_BUILD_JOBS=1 keeps the pre-PR3 sequential path available as a regression-bisect escape hatch. The formerly-load-bearing scripts/build.sh was deleted in Stage E PR7 (#383); there is no longer a parallel-build escape hatch outside the driver.

The ~2 GB-per-job divisor reflects the per-module peak RSS documented in docs/build-performance.md → Phase 6 (heaviest module ~1.76 GB after the lowering_core.sfn decomposition). With 2 jobs running the heaviest pair concurrently, peak concurrent memory is ~3.5 GB — fits the macOS 7 GB ceiling with OS + Actions agent overhead. Hosts with more RAM are still capped by nproc first.


These environment variables influence the behavior of sfn and the Makefile build system.

VariableScopeDescription
SAILFIN_RUNTIME_ROOTsfn binaryOverride the directory where sfn looks for the bundled runtime. By default, the runtime is resolved relative to the executable.
SFN_REGISTRYsfn add / sfn publishOverride the package registry base URL for this shell. Takes precedence over ~/.sfn/config.toml. See sfn config.
SFN_TOKENsfn publishBearer token used when uploading a capsule. Takes precedence over ~/.sfn/credentials written by sfn login.
PREFIXMakefileInstallation prefix. Defaults to $HOME/.local. The binary is installed to $(PREFIX)/bin.
GLOBAL_BIN_DIRInstaller scriptOverride the installation bin directory directly (takes precedence over PREFIX).
GITHUB_TOKENmake fetch-seedGitHub personal access token used to download seed releases from the SailfinIO/sailfin repository. Required for make fetch-seed.
BUILD_JOBSMakefileNumber of parallel compile jobs. Default: 1.
BUILD_ARGSMakefileExtra arguments passed through to the build orchestrator script.
SEEDMakefilePath to (or name of) the seed compiler used as the bootstrap. Defaults to sfn (resolved from PATH).
SEED_VERSIONMakefileVersion tag of the seed to fetch. Defaults to latest.
NATIVE_OPTMakefileOptimization level passed to clang when compiling LLVM IR. Defaults to -O2.
CLANGMakefileclang executable to use. Defaults to clang.
KEEP_SEEDmake clean-buildSet to 0 to delete build/seed/ during make clean-build. Defaults to 1 (seed is preserved).

These variables enable verbose runtime diagnostics. They are intended for compiler development and troubleshooting, not end-user programs.

VariableDescription
SAILFIN_TRACE_ARGVPrint the argument vector received by sailfin_cli_main on startup (honored by the Sailfin entry point in compiler/src/cli_main.sfn).
SAILFIN_TRACE_CRASHEnable extended crash diagnostics.
SAILFIN_TRACE_ALLOC_STATSPrint allocation statistics on exit.
SAILFIN_TRACE_LARGE_ARRAY_BACKTRACECapture backtraces for unusually large array allocations.
SAILFIN_DEBUG_DUMP_BUDGETControl debug-dump output budget.

CodeMeaning
0Success — compilation and execution (or tests) completed without errors.
1General failure — compilation error, runtime error, or test failure.
non-zeroAny non-zero exit code indicates failure. The specific value reflects the program’s own exit code when using sfn run.

When sfn test is used, a non-zero exit code means at least one test failed. All test results are printed before the process exits.


ConventionDescription
*.sfnSailfin source files
*_test.sfnTest files — discovered automatically by sfn test and make test-*
capsule.tomlCapsule manifest — declares name, version, dependencies, and required capabilities
workspace.tomlWorkspace manifest — shared policies for multi-capsule projects
*.sfn-asmNative IR intermediate representation produced by sfn emit native
build/native/sailfinDefault output path for the self-hosted compiler binary
build/seed/bin/sailfinDefault path for the downloaded seed compiler
dist/Release packaging output directory

Compile and run a hello-world program:

Terminal window
sfn run examples/basics/hello-world.sfn

Build the compiler from source:

Terminal window
make compile

Run the full test suite:

Terminal window
make test

Install the compiler locally:

Terminal window
make compile
make install
# compiler is now at ~/.local/bin/sfn

Force a fresh rebuild:

Terminal window
make rebuild

Run only unit tests in a specific directory:

Terminal window
build/native/sailfin test compiler/tests/unit/

Run the CI validation gate locally:

Terminal window
make check

Fetch a fresh seed compiler:

Terminal window
GITHUB_TOKEN=<your-token> make fetch-seed

See Capsules and Packages for documentation on capsule.toml and the planned package registry commands.