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#
silk init helloInitialized Silk project hello at /home/you/hellosilk 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:
Refusing to overwrite existing project file /home/you/hello/silk.tomlsilk init --lib makes a library package and silk init --workspace a workspace; see Projects and packages.
The generated files#
hello/
.gitignore
silk.toml
src/
main.silksilk.toml is the manifest:
[package]
name = "hello"
version = "0.1.0"
silk-version = "0.1.0"| Key | Meaning |
|---|---|
name | The package name. The executable gets this name too. |
version | Your package's own version. |
silk-version | The 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:
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#
cd hello
silk runCompiling 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:
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:mainnow returns the process exit status.? &mut HostInput:mainasks the runtime for the program's arguments and environment. This is the only requirementmainmay have.HostInput.argument(1)returnsOption<Bytes>. Index 0 is the program's own name. Copying the argument allocates, so the call gets anAllocator.Arguments are raw bytes.
String.fromUtf8turns them into astring, or fails on bytes that are not UTF-8.{name}in the template is filled from thenamefield of&.{ name: text }. The compiler checks that every placeholder has a field.
Arguments after -- go to your program:
silk run -- Ada
echo $?Hello, Ada!
0Without an argument, main returns 2, and silk run exits with the program's status:
silk run
echo $?usage: hello <name>
2silk 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:
silk checkChecking 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.25sWhile 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#
silk buildCompiling 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.84sA 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:
./build/llvm/x86_64-unknown-linux-gnu/debug/hello GraceHello, 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.
| Command | Output |
|---|---|
silk build | build/llvm/<target>/debug/<name>, unoptimized, fast to compile |
silk build --release | build/llvm/<target>/release/<name>, optimized |
silk clean | Deletes 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.