# Sailfin Language Reference for LLMs > Version: 0.8.0-alpha.4 | Updated: 2026-07-10 > Source: https://github.com/SailfinIO/sailfin | Site: https://sailfin.dev Sailfin is a systems language with compile-time capability enforcement via effect types. The compiler is self-hosted (written in Sailfin), targets LLVM, and produces native single-binary executables. Licensed under GPLv2 with a runtime library exception. ## Core Differentiator: Effect Types Every function declares what capabilities it uses. Effect annotations are enforced at compile time: undeclared effect usage fails `make compile`, `sfn build`, `sfn run`, `sfn test`, and `sfn check` by default. The `SAILFIN_EFFECT_ENFORCE` env var lets capsule authors opt into `=warning` (telemetry-only mode) while migrating, or `=off` (build-path bypass; `sfn check` still validates per proposal §4.7). Cross-module call-graph propagation is enforced: if A imports B and calls it, A must declare every effect B declares (diagnostic code `E0402`). Aliased imports (`import { foo as bar }`) resolve under the local name. `Member`-callee resolution (`mod.fn()`) is a follow-up (Phase E2 in `docs/proposals/0008-effect-validation.md`). Capsule manifests are a compile-time contract: every function's declared effects must be a subset of the capsule's `[capabilities] required = [...]` surface (diagnostic code `E0403`). A function declaring `![net]` inside a capsule that lists only `["io"]` fails the build. Empty surface (no `[capabilities]` section, or standalone .sfn outside any capsule) skips the check. Undefined function calls are a compile error (diagnostic code `E0420`). A call whose callee is a bare identifier must resolve to an in-scope binding (parameter, local, top-level `fn`/`extern fn`, or import name), a builtin (`print`, `console`, `assert`, `spawn`, `sleep`, `serve`, `channel`, `send`, `receive`, `await`, the six `atomic_*` intrinsics), an implicit prelude/`runtime/sfn/**` global, or an imported free function — otherwise typecheck fails with `error[E0420]: undefined function \`name\``. Member/method callees (`obj.method()`), undefined variable references, and argument type checks are out of scope for this rule. fn read_config(path: string) -> string ![io] { return fs.readFile(path); } fn fetch_and_save(url: string, path: string) ![io, net] { let data = http.get(url).body; fs.writeFile(path, data); } Canonical effects: io, net, model, clock, gpu, rand - io -- file system, printing, process execution - net -- HTTP, WebSocket, TCP, serve - model -- capability gate for AI backend calls; required on any fn that invokes the `sfn/ai` library capsule (post-1.0) - clock -- sleep, timers, monotonic time - gpu -- tensor dispatch, GPU compute (planned) - rand -- random number generation (planned) A function with no effect list performs no side effects. ## Syntax Quick Reference ### Variables let name: string = "Sailfin"; let mut count: int = 0; count += 1; Type annotations use `:`. Type inference is supported (annotation optional). ### Functions fn add(x: int, y: int) -> int { return x + y; } fn greet(name: string = "world") -> string ![io] { print("Hello, {{name}}!"); return "Hello, {{name}}!"; } Parameters use `:` for type annotations. Return types use `->`. Default parameter values are supported. Effect lists come after the return type. ### Structs struct User { id: int; name: string; fn greet(self) -> string { return "Hello, {{self.name}}!"; } } let user: User = User { id: 1, name: "Alice" }; print(user.greet()); Structs support methods (with `self`), generic type parameters, and `implements` clauses for interface conformance. ### Interfaces interface Greeter { fn greet(self) -> string; } struct Bot implements Greeter { name: string; fn greet(self) -> string { return "Beep boop, I'm {{self.name}}"; } } ### Enums (Algebraic Data Types) enum Shape { Circle { radius: float }, Rectangle { width: float, height: float }, } fn area(shape: Shape) -> float { match shape { Shape.Circle { radius } => return 3.14 * radius * radius, Shape.Rectangle { width, height } => return width * height, } } ### Pattern Matching match value { 0 => print("zero"), 1 => print("one"), n if n > 100 => print("large"), _ => print("other"), } Supports literals, wildcards (`_`), guards (`if expr`), and enum destructuring. ### Control Flow if condition { // ... } else if other { // ... } else { // ... } for item in collection { // ... } loop { if done { break; } } There is NO `while` keyword. The parser rejects `while` at statement position (E0411) with a fix-it; write `loop { if !cond { break; } ... }` instead. ### Error Handling try { let data = riskyOperation(); } catch (e) { print("Error: {{e}}"); } Typed errors via the prelude Result enum and the postfix ? operator. Construct with the qualified form Result.Ok { value: x } / Result.Err { error: e }; ? unwraps the Ok value or early-returns the Err arm (pure control flow, no exception runtime). The prelude ships a default Error struct { message: string }. fn safe_divide(a: int, b: int) -> Result { if b == 0 { return Result.Err { error: Error { message: "division by zero" } }; } return Result.Ok { value: a / b }; } fn divide_and_sum(a: int, b: int, c: int, d: int) -> Result { let first = safe_divide(a, b)?; // unwrap, or early-return the Err let second = safe_divide(c, d)?; return Result.Ok { value: first + second }; } fn describe(r: Result) -> string { match r { Result.Ok { value } => return "ok", Result.Err { error } => return "error: " + error.message } } Note: ? is only legal inside a function returning Result, and the operand's error type E must match the enclosing function's E exactly (no From coercion yet). The type-suffix ? (nullable, e.g. Config?) is a distinct, unrelated use of the glyph. ### Generics struct List { items: T[]; fn add(self, item: T) -> void { self.items.append(item); } fn get(self, index: int) -> T | null { if index < 0 || index >= self.items.length { return null; } return self.items[index]; } } Generic type parameters are parsed and captured. Generic enum payloads (e.g. `Result`) monomorphise per instantiation at construction and `match` sites. Constraint syntax (``) is parsed but not enforced; full generic-struct method monomorphization is partial. ### Imports and Modules import { fs } from "fs"; import { http } from "http"; import { parse_program } from "./parser/mod"; Standard library capsules use bare names ("fs", "http", "json", etc.). Relative paths use "./" or "../" syntax. ### Lambdas and Closures let double = fn(x: int) -> int { return x * 2; }; let base = 10; let add_base = fn(x: int) -> int { return x + base; }; // captures `base` Lambdas capture enclosing variables (closures shipped end-to-end, including multi-variable capture). Higher-order dispatch through `fn(int) -> int` typed values works. ### Tests test "addition works" { assert add(2, 3) == 5; assert add(-1, 1) == 0; } Tests are first-class declarations and can declare effects: test "file operations" ![io] { fs.writeFile("test.txt", "hello"); assert fs.readFile("test.txt") == "hello"; } ### Decorators @logExecution fn process(data: string) -> string ![io] { return transform(data); } ### String Interpolation let name = "world"; print("Hello, {{name}}!"); Uses `{{ }}` syntax (migration to `${ }` is planned pre-1.0). ### Extern Functions (FFI) extern fn malloc(size: usize) -> * u8; extern fn free(ptr: * u8) -> void; extern fn memcpy(dst: * u8, src: * u8, n: usize) -> * u8; Declares external C symbols for linking. Signatures must use the C-ABI accept-list — fixed-width ints (`i8`..`u64`, `usize`, `isize`), `bool`, `f32`/`f64`, raw pointers (`* u8`, `* const T`, `* OpaqueStruct`), and `void` (return only); violations are diagnosed (E0801–E0805). Raw pointer types are written with a space after `*` (`* u8`). A defined or extern fn can be passed to a C callee as a code pointer via `my_fn as * u8`; unsupported function-reference forms are rejected at typecheck (E0808/E0809). ## Standard Library Capsules | Capsule | Import | Status | Effects | Description | |-----------|-------------|---------------|---------------|------------------------------------------| | sfn/strings | "strings" | Shipped | None | Trim, case, split/join, find/replace | | sfn/json | "json" | Shipped | None | JSON parse, serialize, pretty-print | | sfn/crypto| "crypto" | Shipped | None | SHA-256, base64, Ed25519 verify, HMAC-SHA-256 | | sfn/math | "math" | Shipped | None | abs, min, max, clamp, pow, floor, ceil | | sfn/path | "path" | Shipped | None | join, dirname, basename, ext, normalize | | sfn/toml | "toml" | Shipped | None | TOML parsing and serialization | | sfn/fs | "fs" | Shipped | io | File r/w/append, exists, mkdir, perms, symlink, mkdtemp | | sfn/os | "os" | Shipped | io | Env vars, home dir, exec, exit | | sfn/log | "log" | Shipped | io, clock | Structured leveled logging | | sfn/time | "time" | Shipped | clock | Sleep, monotonic timing, elapsed | | sfn/http | "http" | Partial | net, io | HTTP GET/POST client; server stubbed | | sfn/cli | "cli" | Shipped | io | CLI arg parsing, subcommands, styling | | sfn/test | "test" | Partial | None / io | Assertions (`expect_*` pure tier, `assert_*` io tier), snapshots | | sfn/sync | "sync" | Stubbed | io | channel/parallel/spawn API — pending concurrency runtime; DO NOT USE yet | | sfn/net | "net" | Stubbed | net, io | TCP/UDP sockets — pending runtime intrinsics; DO NOT USE yet | | sfn/tensor| "tensor" | Shipped (CPU) | gpu (planned) | Tensor ops, matmul, transpose | | sfn/layers| "layers" | Shipped (CPU) | gpu (planned) | Linear layers, ReLU, sequential models | | sfn/nn | "nn" | Shipped (CPU) | gpu (planned) | Activations, normalization, argmax, one_hot | | sfn/losses| "losses" | Shipped | None | MSE, MAE, Huber, hinge loss functions | ## Numeric Types Sailfin has two canonical numeric types: `int` (64-bit signed integer, i64) and `float` (64-bit IEEE-754 float, f64). Unsuffixed integer literals default to `int`; decimal or exponent literals default to `float`. Use `int` for IDs, indices, counts, ports, status codes, and byte offsets; use `float` for measurements, ratios, scores, and balances. The legacy `number` keyword is a deprecated alias for `float`, kept only for source compatibility during the migration; new code should use `int` or `float` explicitly. The compiler source, runtime prelude, standard-library capsules, and reference examples completed their migration off `number` in Slice E.3 (May 2026); the alias itself is scheduled for removal in Slice E.4. Fixed-width FFI integer types (`i8`, `i16`, `i32`, `i64`, `u8`, `u16`, `u32`, `u64`, `usize`) and float types (`f32`, `f64`) are also recognized for extern-fn signatures and low-level interop. Bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`) are shipped and operate on `int`. Mixing `int` and `float` operands in arithmetic, bindings, call arguments, or struct-field writes is a compile error — disambiguate with an explicit cast: `x as float`, `n as int`. The `as` cast lowers through a typed matrix (`sitofp`, `fptosi`, `sext`, `trunc`, ...). `as bool` is rejected; write `x != 0` instead. ## What Works End-to-End Today These features parse, type-check, emit to .sfn-asm IR, and lower to LLVM: - Functions (including async fn, extern fn, generics, defaults, decorators) - Structs with methods, fields, generic params, implements clauses - Enums with variant payloads and pattern matching (generic payloads monomorphise per instantiation) - Interfaces with method signatures - Control flow (if/else, for, loop, match, break, continue — no `while`) - try/catch/finally exception handling - Result + the postfix ? error-propagation operator (typed, exception-free) - int (i64) / float (f64) numeric types with strict no-implicit-mixing rules - Bitwise and shift operators (&, |, ^, <<, >>) on int - Explicit numeric `as` casts (typed lowering matrix) - Atomic intrinsics: atomic_load/store/add/sub/cas/fence (seq_cst) - thread_local let mut globals (top-level only) - String interpolation (via runtime) - Lambdas and closures with capture (multi-variable capture included) - Test declarations (plus -k/--tag filtering, lifecycle hooks, snapshots) - Import/export across modules - Effect annotations -- parsed AND enforced at compile time (build fails on undeclared effects by default; configurable via SAILFIN_EFFECT_ENFORCE) - Colon type annotations (x: Type) for params, variables, fields - Arrow return types (-> Type) for functions - Code formatting via sfn fmt (token-stream, idempotent, CI-enforced) ## What Is Parsed but Not Fully Implemented DO NOT use these in generated code unless explicitly asked — they parse (and some typecheck) but do not yet compile to working programs: - Concurrency constructs: `routine { }`, `await`, `channel()`, `spawn` -- parsed with partial typecheck diagnostics (E0813–E0815); LLVM lowering and the scheduler runtime are pending. `async fn` is structural only. - Member-callee cross-module effect propagation (`mod.fetch()`) -- direct `Identifier` callee resolution ships in Phase E1; member callees and decorator-implied transitives are deferred to Phase E2 - Generic constraints () -- parsed as text, not enforced - Affine / Linear ownership, &T / &mut T borrows -- parsed, transparent (no enforcement) - PII / Secret taint types -- parsed as nominal types, no enforcement - Union types -- parse but match destructuring is unstable ## What Is Not Yet in the Parser - ${ } string interpolation (currently uses {{ }}; migration planned pre-1.0) - |> pipeline operator (planned post-1.0) - while loops (intentional — use loop { } with break) ## AI Constructs (Moved to Library) Earlier drafts had `model`, `prompt`, `tool`, and `pipeline` block keywords. These were removed from the language; richer AI functionality ships as the post-1.0 `sfn/ai` library capsule. The `![model]` effect stays in the language as the capability gate — any function that reaches an AI backend must declare it. Don't emit `model { ... }` / `prompt system { ... }` / `tool ... { ... }` / `pipeline ... { ... }` forms — they are not valid Sailfin. ## Compiler Architecture The compiler is self-hosted (written in Sailfin, compiles itself): Lexer (lexer.sfn) -> Parser (parser/*.sfn) -> AST (ast.sfn) -> Type Checker (typecheck.sfn) -> Native Emitter (emit_native.sfn) -> .sfn-asm intermediate representation -> LLVM Lowering (llvm/lowering/*.sfn) -> LLVM IR -> native binary Build commands: make compile -- build compiler from released seed (self-hosting) make test -- run unit + integration + e2e test suites make check -- full validation including seedcheck make install -- install to ~/.local/bin ## Tooling ### sfn fmt — Code Formatter Built-in formatter with one canonical style, no configuration. Like gofmt. sfn fmt file.sfn # print formatted output to stdout sfn fmt --write src/ # format files in place sfn fmt --check src/ # CI mode: exit 1 if any file would change Formatting rules: - 4-space indentation, K&R braces (opening brace on same line) - Spaces around operators and after keywords - No space before : ; ? (optional suffix) or after unary ! - - Imports sorted by path, specifiers sorted alphabetically - Blank lines: exactly 1 between top-level declarations - Inline blocks: single-statement blocks stay on one line if they fit - Comments preserved exactly as written Known limitations: - No expression wrapping (long lines stay as-is) - No --diff mode - Bulk directory formatting may OOM on very large codebases (use per-file loop) - No semantic restructuring (match arm reordering, function inlining) ## Package Manifest (capsule.toml) [capsule] name = "my-app" version = "1.0.0" [dependencies] "json" = "^1.0.0" "http" = "^1.0.0" [capabilities] required = ["io", "net"] Packages are called "capsules" and published to pkg.sfn.dev by default. Override the registry with `sfn config set registry ` (persists to ~/.sfn/config.toml) or set SFN_REGISTRY for a one-shot override. ## Idiomatic Patterns ### Pure function (no effects) fn fibonacci(n: int) -> int { if n <= 1 { return n; } return fibonacci(n - 1) + fibonacci(n - 2); } ### Effectful service function fn fetch_user(id: int) -> string ![io, net] { let response = http.get("https://api.example.com/users/{{id}}"); if response.status != 200 { print.err("Failed to fetch user {{id}}"); return ""; } return response.body; } Output builtins: `print(value)` writes to stdout, `print.err(value)` to stderr. `print.info` / `print.warn` / `print.error` are deprecated legacy variants — do not emit them in new code; use the `sfn/log` capsule for leveled logging. ### Struct with interface interface Serializable { fn to_json(self) -> string; } struct Config implements Serializable { host: string; port: int; fn to_json(self) -> string { return "{\"host\": \"{{self.host}}\", \"port\": {{self.port}}}"; } } ### Match with enum destructuring (prelude Result — do NOT redefine Result) fn handle(result: Result) ![io] { match result { Result.Ok { value } => print("Success: {{value}}"), Result.Err { error } => print.err("Error: {{error.message}}"), } } ### File I/O with effects import { fs } from "fs"; fn read_config(path: string) -> string ![io] { if !fs.exists(path) { return "{}"; } return fs.readFile(path); } ### Test with effect declaration test "config reader returns default for missing file" ![io] { let config = read_config("nonexistent.toml"); assert config == "{}"; } ## Design Principles 1. Effect types are the core differentiator -- every function declares its capabilities; the compiler verifies them. 2. Boring syntax wins -- TypeScript/Rust/Python conventions unless there's a compelling semantic reason to diverge. 3. Libraries over keywords -- only add keywords for constructs that can't be expressed as library functions. 4. Don't ship unfinished safety claims -- features are either enforced end-to-end or explicitly deferred. No "parsed but unenforced" marketing. 5. Fix the foundation first -- int types, Result, generic constraints, and effect enforcement are prerequisites for everything else. ## Key Differences from Similar Languages vs. TypeScript: Sailfin compiles to native code via LLVM. Effect types are checked at compile time, not documented by convention. vs. Rust: Sailfin trades borrow checking for effect tracking. No lifetime annotations. Capability safety instead of memory safety (ownership is parsed but deferred to post-1.0). vs. Go: Sailfin adds compile-time capability enforcement and algebraic data types with pattern matching. Same single-binary ergonomics. vs. Zig: Sailfin is more opinionated. Effect types control capabilities instead of memory layout. Both target LLVM. ## File Extension Sailfin source files use the `.sfn` extension. ## Further Reading - Language spec: https://sailfin.dev/docs/reference/spec/ (source: site/src/content/docs/docs/reference/spec/) - Feature status: docs/status.md - Roadmap: https://sailfin.dev/roadmap - Examples: examples/ directory (45 programs; the index marks design-stage features) - Contributing: CONTRIBUTING.md