Browse the docs

Your first program.

This page takes you from an empty folder to a program that reads an argument, prints a greeting and sets its exit status. You need silk on your PATH and Clang 22 or newer; see Install.

Create a package#

shell
silk init hello
TEXT
Initialized Silk project hello at /home/you/hello

silk init makes the folder and three files in it. Without a path it uses the current folder. --name <name> gives the package a name other than the folder's. It refuses to touch a folder that already has a silk.toml:

TEXT
Refusing to overwrite existing project file /home/you/hello/silk.toml

silk init --lib makes a library package and silk init --workspace a workspace; see Projects and packages.

The generated files#

TEXT
hello/
  .gitignore
  silk.toml
  src/
    main.silk

silk.toml is the manifest:

toml
[package]
name = "hello"
version = "0.1.0"
silk-version = "0.1.0"
KeyMeaning
nameThe package name. The executable gets this name too.
versionYour package's own version.
silk-versionThe Silk toolchain the package runs with. silk init writes the version of the compiler that made it.

The manifest does not name the root module, because the default is the file silk init made: src/main.silk. The compiler starts reading there and finds other files through imports. source and main in [package] move it; see The manifest in the Projects section.

.gitignore holds one line, /build/. Compiled output goes to build/, and you don't commit it.

src/main.silk is a complete program:

silk
import silk.core.effect { Effect }
import silk.io.logger { LogError }
import silk.io.logger_native { StdoutLogger }

pub effect fn main() -> ()
  ! LogError {
  let mut logger = StdoutLogger.make()

  run Effect.log("Hello, world!\n", &()) |> Effect.provideMut(&mut logger)
}

main is an effect function, because printing is an effect. ! LogError says it can fail with a LogError. Printing goes through a logger: Effect.log describes the message, Effect.provideMut(&mut logger) supplies a logger that writes to standard output, and run performs it. Chapter 1 of the Silk book, Hello, Silk, reads this file line by line.

Run it#

shell
cd hello
silk run
TEXT
Compiling hello (x86_64-unknown-linux-gnu, debug)
  load     143 modules, 1 in package         0.07s
  check    1 module                          0.13s
  codegen  689 functions                     0.14s
  borrow   229 bodies                        0.01s
  link     clang                             0.38s
Finished build/llvm/x86_64-unknown-linux-gnu/debug/hello in 0.77s
Hello, world!

silk run builds the package and starts the program. The first lines are the build's progress: one row per compiler phase, then the path of the program it built. They go to standard error, and the program's own output follows on standard output, so silk run > out.txt captures only Hello, world!. In a terminal the rows are redrawn in place while they run. silk run --quiet keeps only the Finished line. From here on, the output shown after silk run is the program's alone.

Read an argument and choose the exit status#

Replace src/main.silk with this:

silk
import silk.mem.allocator { Allocator, OutOfMemoryError }
import silk.collections.bytes { Bytes }
import silk.core.effect { Effect }
import silk.system.host_input { HostInput, HostInputError }
import silk.io.logger { LogError }
import silk.core.option { Option }
import silk.io.logger_native { StdoutLogger }
import silk.core.result { Result }
import silk.text.string { String, InvalidUtf8 }

pub effect fn main() -> i32
  ! LogError | HostInputError | OutOfMemoryError
  ? &mut HostInput {
  let mut logger = StdoutLogger.make()
  let mut allocator = Allocator.systemAllocatorProvider()

  let first = run HostInput.argument(1) |> Effect.provideMut<Allocator>(&mut allocator)
  let name = match move first {
    Option<Bytes>.Some { value } => move value
    Option<Bytes>.None => {
      run Effect.log("usage: hello <name>\n", &()) |> Effect.provideMut(&mut logger)
      return 2
    }
  }
  let text = match String.fromUtf8(Bytes.asSlice(&name)) {
    Result<string, InvalidUtf8>.Success { value } => value
    Result<string, InvalidUtf8>.Failure { .. } => "stranger"
  }

  run Effect.log("Hello, {name}!\n", &.{ name: text }) |> Effect.provideMut(&mut logger)
  return 0
}

What changed:

  • -> i32: main now returns the process exit status.

  • ? &mut HostInput: main asks the runtime for the program's arguments and environment. This is the only requirement main may have.

  • HostInput.argument(1) returns Option<Bytes>. Index 0 is the program's own name. Copying the argument allocates, so the call gets an Allocator.

  • Arguments are raw bytes. String.fromUtf8 turns them into a string, or fails on bytes that are not UTF-8.

  • {name} in the template is filled from the name field of &.{ name: text }. The compiler checks that every placeholder has a field.

Arguments after -- go to your program:

shell
silk run -- Ada
echo $?
TEXT
Hello, Ada!
0

Without an argument, main returns 2, and silk run exits with the program's status:

shell
silk run
echo $?
TEXT
usage: hello <name>
2

silk run Ada works as well, but -- makes sure an argument such as --release reaches your program instead of silk.

On POSIX hosts the status is the low 8 bits of the i32. A main that returns () exits with 0. A failure that escapes an effect main, such as a LogError here, ends the program with status 1 and a report on standard error. See Program entry and termination.

Check without building#

silk check type-checks the package and builds nothing, so it is the fastest way to see errors:

shell
silk check
TEXT
Checking hello (x86_64-unknown-linux-gnu)
  load     143 modules, 1 in package         0.01s
  check    2 modules                         0.22s
  borrow   253 bodies                        0.01s
Finished hello in 0.25s

While it works, silk check reports each phase on standard error: load reads your module and the standard library modules it imports, check type-checks them, and borrow checks ownership in each function body. The last line says the check finished and how long it took. Errors, when there are any, appear between the phase rows and a closing Failed line.

Build an executable#

shell
silk build
TEXT
Compiling hello (x86_64-unknown-linux-gnu, debug)
  load     143 modules, 1 in package         0.05s
  check    1 module                          0.18s
  codegen  803 functions                     0.17s
  borrow   267 bodies                        0.01s
  link     clang                             0.41s
Finished build/llvm/x86_64-unknown-linux-gnu/debug/hello in 0.84s

A build adds two phases: codegen generates machine code for the functions the program uses, and link runs Clang to produce the executable. The last line names it.

The executable lands at build/llvm/<target>/debug/<name>. It runs on its own, with no silk involved:

shell
./build/llvm/x86_64-unknown-linux-gnu/debug/hello Grace
TEXT
Hello, Grace!

Next to it are hello.symbols, a table that maps machine-level symbol names back to Silk names, and two files the next build reads to skip work: hello.inputs, the list of files this build read, and hello.units, a folder of compiled code. A build that changes nothing reuses them.

CommandOutput
silk buildbuild/llvm/<target>/debug/<name>, unoptimized, fast to compile
silk build --releasebuild/llvm/<target>/release/<name>, optimized
silk cleanDeletes build/

The target is your host's: x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu or aarch64-apple-darwin.

Next#

A tour of Silk shows more of the language in one program. The Silk book teaches it in order, starting from this same silk init.

View the source of this page