Hello, World!
Your First Program
Section titled “Your First Program”Create a file called hello.sfn with the following contents:
fn main() ![io] { print("Hello, World!");}Then run it:
sfn run hello.sfnOutput:
Hello, World!That is the complete program. Let’s walk through every token so nothing is mysterious before you move on.
Breaking down the syntax
Section titled “Breaking down the syntax”fn main() ![io] {fn— declares a function.main— the entry point. Sailfin looks for a function namedmainwhen you run a file.()— the parameter list.maintakes no parameters.![io]— an effect annotation. It declares that this function performs IO operations.iocovers anything that writes to stdout, stderr, or the filesystem. The compiler uses this information to enforce capability boundaries.{— opens the function body.
print("Hello, World!");print— the built-in output function. It writes to stdout with no prefix."Hello, World!"— a string literal.;— statement terminator.
The
![io]annotation is not optional. The compiler will reject any call toprint()in a function that does not declare theioeffect. This is by design: Sailfin requires you to be explicit about what a function does to the world around it.
What Happens When You Run It
Section titled “What Happens When You Run It”When you run sfn run hello.sfn, the toolchain does the following:
- Lexing — the source file is tokenized.
- Parsing — tokens are assembled into an abstract syntax tree (AST).
- Type checking — types are inferred and validated.
- Effect checking — the compiler verifies that every effectful call is
covered by a declared effect. A call to
print()requiresio. - Native emission — the AST is lowered to
.sfn-asmintermediate representation. - LLVM lowering —
.sfn-asmis lowered to LLVM IR, compiled to a native binary, and executed.
This all happens in one command. You do not manage build artifacts for single-file programs.
What Happens If You Forget ![io]
Section titled “What Happens If You Forget ![io]”Omit the effect annotation:
fn main() { print("Hello, World!");}Run it:
sfn run hello.sfnThe compiler emits a diagnostic and exits without running the program:
error[E0210]: call to `print` requires effect `io`, but `main` does not declare it --> hello.sfn:2:5 | 1 | fn main() { | ^ effect `io` missing here — add `![io]` to the function signature 2 | print("Hello, World!"); | ^^^^^^^^^^^^^^^^^^^^^^ `print` is an `io` operation | = help: change `fn main()` to `fn main() ![io]`The fix is exactly what the diagnostic says: add ![io] to the function
signature. The compiler reports the source span, identifies the missing effect,
and provides a fix-it hint. This pattern is consistent throughout the language —
every missing effect produces a diagnostic of this form.
A More Complete Program
Section titled “A More Complete Program”The hello-world program shows the minimum viable structure. Here is a slightly more interesting example that introduces variables, string interpolation, and a function call:
fn greet(name: string) -> string { return "Hello, {{name}}!";}
fn main() ![io] { let name = "World"; print(greet(name));}Run it:
sfn run hello.sfnOutput:
Hello, World!What is new here
Section titled “What is new here”fn greet(name: string) -> string— a function that takes astringparameter and returns astring. Note thatgreetdoes not declare![io]because it does not perform any IO itself — it just produces a value."Hello, {{name}}!"— string interpolation. Any expression inside{{ }}is evaluated and inserted into the string at that position. This works with variables, function calls, and field accesses.let name = "World"— declares an immutable local variable. The type is inferred asstring.print(greet(name))— callsgreetwithname, then passes the result toprint. Theioeffect is declared onmain, which is the function actually callingprint, so the effect check passes.
Effect transitivity:
greetdoes not need![io]because it never calls
Adding a Struct
Section titled “Adding a Struct”Sailfin structs group related data. Fields are declared with name: Type;:
struct Person { name: string; age: number;}
fn greet(person: Person) -> string { return "Hello, {{person.name}}! You are {{person.age}} years old.";}
fn main() ![io] { let alice = Person { name: "Alice", age: 30 }; let bob = Person { name: "Bob", age: 25 };
print(greet(alice)); print(greet(bob));}Output:
Hello, Alice! You are 30 years old.Hello, Bob! You are 25 years old.What is new here
Section titled “What is new here”struct Person { ... }— declares a struct type namedPerson.name: string;— a field namednameof typestring. Field declarations usename: Type;and end with a semicolon.Person { name: "Alice", age: 30 }— struct instantiation. Fields are assigned by name. Order does not matter.person.name— field access inside a string interpolation expression.
Your First Test
Section titled “Your First Test”Sailfin has first-class test support. Add a test block to the same file:
struct Person { name: string; age: number;}
fn greet(person: Person) -> string { return "Hello, {{person.name}}! You are {{person.age}} years old.";}
fn main() ![io] { let alice = Person { name: "Alice", age: 30 }; print(greet(alice));}
test "greet produces correct greeting" { let p = Person { name: "Alice", age: 30 }; assert greet(p) == "Hello, Alice! You are 30 years old.";}
test "greet handles different names" { let p = Person { name: "Bob", age: 25 }; assert greet(p) == "Hello, Bob! You are 25 years old.";}Run the tests:
sfn test hello.sfnOutput:
running 2 tests in hello.sfn
PASS greet produces correct greeting PASS greet handles different names
2 passed, 0 failedWhat is new here
Section titled “What is new here”test "name" { ... }— a test block. The string is the test name displayed in output. Test blocks live in the same file as the code they test, or in a separate*_test.sfnfile.assert expression;— fails the test if the expression evaluates tofalse. The compiler reports the failing assertion with a source span.sfn test hello.sfn— runs all test blocks in the specified file. Runsfn test(no arguments) to run all*_test.sfnfiles in the project.
Note: Test blocks do not need an effect annotation even if they call effectful functions through the code under test. Effect requirements are on function declarations, not on test blocks.
Stderr Output
Section titled “Stderr Output”Use print.err() to write to stderr instead of stdout:
fn main() ![io] { print("This goes to stdout."); print.err("This goes to stderr.");}Both print() and print.err() require the io effect. There is no separate
io vs io.err distinction — both are covered by ![io].
Organizing into a Project
Section titled “Organizing into a Project”Once your program grows beyond a single file, organize it as a capsule (Sailfin’s
package format). Create a capsule.toml in the root of your project:
[capsule]name = "my-app"version = "0.1.0"entry = "src/main.sfn"A typical project layout:
my-app/ capsule.toml src/ main.sfn greet.sfn tests/ greet_test.sfnWith a capsule.toml present, running sfn run from the project root uses the
entry field to find the entry point, and sfn test discovers all *_test.sfn
files recursively.
You can import from other source files within the same capsule using import statements at the top of a file:
import { greet } from "./greet";
fn main() ![io] { print(greet("World"));}Next Steps
Section titled “Next Steps”- Tour of Sailfin — A deeper walkthrough covering control flow, enums, pattern matching, and more
- Language Basics — Variables, types, functions, and control flow in depth
- Effect System — How effect annotations work and why they matter
- Editor Setup — Get syntax highlighting and snippets in your editor