Hello world
This page teaches the Silk “shape of a program”: explicit imports, a normal main, and a workflow that makes it cheap to
iterate.
Assumption: you have a silk binary available on your PATH.
The smallest program#
Create a file named hello.slk:
import { println } from "std/io";
fn main () -> int {
println("hello from silk");
return 0;
}
Why this looks the way it does#
- Imports are explicit. If you want
println, you import it. This keeps dependencies obvious and makes refactors safer. mainis a normal function. Executables use a conventional entrypoint. There isn’t a special “program block” that behaves differently from the rest of the language.- The return type is explicit.
-> intis the process exit code on hosted platforms. A successful run returns0.
Iteration loop: check → test → build#
Most Silk workflows are intentionally simple:
silk check hello.slk
silk test hello.slk
silk build hello.slk -o build/hello
silk checkanswers: “does this module set parse and type-check?”silk testdiscovers and runstest "name" { ... }blocks and emits TAP output.silk buildcompiles and produces an artifact. When you omit--kind, you’re building an executable.
The important term is module set: each command operates on a set of .slk files compiled together. Even in small
programs, thinking in module sets scales well to larger codebases.
Next: CLI and toolchain
A slightly richer example#
This adds a helper function and shows how “real” Silk code stays ordinary:
import { println } from "std/io";
fn greet (name: string) -> void {
println("hello {s}", name);
}
fn main () -> int {
greet("silk");
return 0;
}
A practical hello: reading arguments#
On hosted targets, Silk can also accept a conventional (argc, argv) entrypoint shape. The standard library includes a
small std::args helper so you can treat raw argv pointers as string views.
import args from "std/args";
import { println } from "std/io";
fn main (argc: int, argv: u64) -> int {
let a = args::Args.init(argc, argv);
if a.count() < 2 {
println("usage: hello <name>");
return 2;
}
println("hello {s}", a.get(1));
return 0;
}
This example is intentionally small, but it demonstrates the “systems” posture of Silk: when a boundary is low-level (process arguments are ultimately raw pointers), the language and stdlib make that boundary explicit rather than hiding it behind magic.
Hello, standard library#
The smallest program imports a single symbol. Real programs quickly pull in modules: filesystems, networking, crypto, and
more. Each tab below is a self-contained “hello, but practical” sketch you can paste into its own .slk file.
Filesystem#
Create a directory, write a file, read it back, clean up:
import fs from "std/fs";
import { println } from "std/io";
fn main () -> int {
// 493 == 0o755 on POSIX.
if fs::mkdir_all("tmp", 493) != None {
println("mkdir failed");
return 1;
}
let path: string = "tmp/hello_silk.txt";
fs::unlink(path); // ignore errors; we just want the file gone
// 420 == 0o644 on POSIX.
match (fs::write_file_string(path, "hello from silk\\n", 420)) {
Ok(_) => {},
Err(_) => {
println("write failed");
fs::unlink(path);
return 2;
},
}
match (fs::read_file_string(path)) {
Ok(s) => {
println("read: {s}", s.as_string());
fs::unlink(path);
return 0;
},
Err(_) => {
println("read failed");
fs::unlink(path);
return 3;
},
}
}
Reference: std::fs
Networking#
A tiny single-request HTTP server on loopback (blocking I/O):
import http from "std/http";
import net from "std/net";
import { println } from "std/io";
fn main () -> int {
let addr = net::SocketAddrV4.loopback(8080);
let mut listener = match net::TcpListener.listen(addr, 16) {
Ok(v) => v,
Err(_) => {
println("listen failed");
return 1;
},
};
println("listening on 127.0.0.1:8080 (try: curl http://127.0.0.1:8080/)");
let stream = match listener.accept() {
Ok(v) => v,
Err(_) => {
listener.close();
println("accept failed");
return 2;
},
};
let mut conn = http::Connection.from_stream(stream);
let req = match conn.read_request() {
Ok(v) => v,
Err(_) => {
conn.close();
listener.close();
return 3;
},
};
println("got {s} {s}", req.method(), req.target());
let w_err: http::Error? = conn.write_response(200, "OK", "hello from silk http\\n");
conn.close();
listener.close();
if w_err != None { return 4; }
return 0;
}
Reference: std::net, std::http
Cryptography#
Hash bytes, compare safely, and wipe buffers when you’re done:
import arrays from "std/arrays";
import buffer from "std/buffer";
import crypto from "std/crypto";
import hash from "std/crypto/hash";
import { print, println } from "std/io";
import mem from "std/runtime/mem";
fn main () -> int {
if crypto::init() != None {
println("crypto init failed");
return 1;
}
let msg: string = "hello from silk";
let msg_ptr: u64 = mem::string_ptr(msg);
let msg_len: i64 = mem::string_len(msg);
let mut out = match buffer::BufferU8.init(32) {
Ok(v) => v,
Err(_) => return 2,
};
let hash_err: crypto::CryptoError? = hash::blake2b(
mut out,
32,
arrays::ByteSlice{ ptr: msg_ptr, len: msg_len }
);
if hash_err != None {
out.drop();
return 3;
}
print("blake2b-256(\"{s}\") = ", msg);
var i: i64 = 0;
while i < 32 {
print("{x:02}", out.get(i));
i = i + 1;
}
println("");
let wipe_err: crypto::CryptoFailed? = crypto::memzero(out.as_bytes());
out.drop();
if wipe_err != None { return 4; }
return 0;
}
Reference: std::crypto
GGML#
std::ggml brings the ggml tensor library into the standard library surface. On the hosted baseline, the toolchain
auto-links ggml when the module is included in your module set.
import ggml from "std/ggml";
import { println } from "std/io";
fn main () -> int {
println("hello from std::ggml");
return 0;
}
Reference: std::ggml
Graphics#
std::graphics provides raw, pinned bindings to common APIs (OpenGL, OpenGL ES, Vulkan). These are FFI bindings — context
creation and safety live above this layer — and hosted builds must link the appropriate loader library.
import opengl from "std/graphics/opengl";
import { println } from "std/io";
fn main () -> int {
println("hello from std::graphics::opengl");
return 0;
}
Build (hosted linux/x86_64 baseline):
silk build hello_gl.slk -o build/hello_gl --needed libGL.so.1
Reference: std::graphics
Concurrency: async and task#
Silk’s concurrency keywords are explicit: task spawns work (returns a Task(T)), and async produces a Promise(T).
Task + yield#
import { println } from "std/io";
task fn add (a: int, b: int) -> int { return a + b; }
async fn main () -> int {
let h = add(1, 2); // h: Task(int)
task {
let v: int = yield h;
println("1 + 2 = {d}", v);
return 0;
}
}
Promise + await#
import { println } from "std/io";
async fn answer () -> int { return 42; }
async fn main () -> int {
let p = answer(); // p: Promise(int)
let v: int = await p; // unwrap the promise
println("answer = {d}", v);
return 0;
}
Reference: Concurrency, Tutorial 5
Formal Silk: #invariant, #variant, #monovariant#
Formal Silk is Silk’s compile-time verification surface (Z3-backed). When you use directives like #invariant or
#monovariant, silk check / silk build prove the obligations at compile time and fail the build if they can’t be
proven.
Loop specs#
Use #variant to prove termination, and #monovariant to prove a measure moves in one direction (non-decreasing or
non-increasing):
fn main () -> int {
let limit: int = 3;
#const original_limit = limit;
let mut i: int = 0;
#invariant i >= 0;
#invariant i <= original_limit;
#variant original_limit - i;
#monovariant i;
while i < limit {
i += 1;
}
return 0;
}
Function contracts#
Contracts attach to a function and introduce proof obligations:
#require x >= 0;
#assure result == x + 1;
fn inc (x: int) -> int {
return x + 1;
}
fn main () -> int {
return inc(2) - 3;
}
Reference: Formal Silk guide, Formal Silk reference, while loop, Syntax tour
WebAssembly and WASI#
Silk can target wasm32-wasi and run under standard WASI runtimes (including Node’s built-in WASI support).
main.slk#
import io from "std/io";
fn main () -> int {
io::println("hello from silk wasm wasi");
return 7;
}
Build:
silk build main.slk --target wasm32-wasi -o out.wasm
run.js#
const fs = require("node:fs");
const { WASI } = require("node:wasi");
async function main() {
const wasmPath = process.argv[2];
const wasi = new WASI({
version: "preview1",
args: [wasmPath],
env: {},
preopens: {},
});
const bytes = fs.readFileSync(wasmPath);
const { instance } = await WebAssembly.instantiate(bytes, wasi.getImportObject());
try {
wasi.start(instance);
} catch {
// Some Node versions throw on proc_exit; the exit code is still available.
}
const exitSym = Object.getOwnPropertySymbols(wasi).find(
(s) => s.toString() === "Symbol(kExitCode)"
);
const code =
exitSym && typeof wasi[exitSym] === "number" ? wasi[exitSym] : process.exitCode ?? 0;
process.exit(code);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
Run:
node --no-warnings run.js out.wasm
echo $?
Expected:
- stdout contains
hello from silk wasm wasi - exit code is
7
Reference: How-To: Run wasm32-wasi Output in Node.js
Where to go next#
- Language tour
- Modules & packages
- Standard library
- Tutorials: Filesystem, Concurrency
- WASI + Node
- Formal Silk
- Reference: packages/imports/exports (
Packages, imports, exportsin the sidebar under “Language”)
Source repository · Edit this page · View Markdown