Standard Library
The Sailfin standard library is divided into the prelude (always in scope, no import required) and named modules imported on demand. This page documents every available function, struct, and module with full signatures, behavior notes, and examples.
Prelude
Section titled “Prelude”The prelude (runtime/prelude.sfn) is automatically available in every Sailfin program. No import statement is needed.
Output
Section titled “Output”print(value: any) ![io]
Section titled “print(value: any) ![io]”Write a value to stdout followed by a newline. Accepts any type; non-string values are converted to their debug representation.
fn greet(name: string) ![io] { print("Hello, {{name}}!");}print.err(value: any) ![io]
Section titled “print.err(value: any) ![io]”Write a value to stderr followed by a newline. Use for diagnostics, errors, and warnings that should not mix with normal stdout output.
fn validate(path: string) -> boolean ![io] { if path.length == 0 { print.err("validation error: path must not be empty"); return false; } return true;}Deprecated output functions
Section titled “Deprecated output functions”The following functions are still recognized by the runtime for backward compatibility. Prefer print() and print.err() in new code.
| Deprecated | Preferred replacement |
|---|---|
print.info(message) | print(message) |
print.warn(message) | print.err(message) |
print.error(message) | print.err(message) |
print.debug(message) | print(message) |
String Utilities
Section titled “String Utilities”These functions operate on Sailfin strings using Unicode grapheme clusters as the unit of indexing. “Index” always means a grapheme-cluster index, not a byte offset or code-unit index.
substring(text: string, start: int, end: int) -> string
Section titled “substring(text: string, start: int, end: int) -> string”Extract the substring from grapheme index start (inclusive) to end (exclusive). Both bounds are clamped to [0, text.length]. Returns "" when the resulting range is empty or inverted.
let s = "Hello, world!";let hello = substring(s, 0, 5); // "Hello"let world = substring(s, 7, 12); // "world"let empty = substring(s, 5, 5); // ""let clamped = substring(s, 0, 999); // "Hello, world!" (end clamped)Notes:
- Does not panic on out-of-range bounds — bounds are silently clamped.
- Safe to call on empty strings; always returns
"". - Works correctly with multi-byte Unicode characters because it indexes by grapheme cluster.
find_char(text: string, character: string, start: int = 0) -> int
Section titled “find_char(text: string, character: string, start: int = 0) -> int”Find the first occurrence of a single grapheme character in text, beginning the search at grapheme index start. Returns the index of the first match, or -1 if not found.
The start parameter defaults to 0. Negative values are treated as 0. A start beyond the end of the string returns -1 immediately.
Escape sequences are recognized when passed as two-character strings: "\\n" matches a literal newline, "\\r" matches a carriage return, and "\\t" matches a tab.
let path = "/usr/local/bin";let last_slash = find_char(path, "/", 1); // 4
let csv = "name,age,city";let first_comma = find_char(csv, ","); // 4
let line = "hello\nworld";let newline = find_char(line, "\\n"); // 5
let missing = find_char("abc", "z"); // -1grapheme_count(text: string) -> int
Section titled “grapheme_count(text: string) -> int”Return the number of Unicode grapheme clusters in text. For ASCII strings this equals the byte length. For strings containing multi-byte characters, emoji, or combined sequences this may differ from .length.
let ascii = "hello";grapheme_count(ascii); // 5
let emoji = "hi 👋";grapheme_count(emoji); // 4 (h, i, space, 👋)
let empty = "";grapheme_count(empty); // 0Note: Use grapheme_count when you need the number of visible characters a user would perceive. Use .length only when operating on raw bytes or code units.
grapheme_at(text: string, index: int) -> string
Section titled “grapheme_at(text: string, index: int) -> string”Return the grapheme cluster at the given index. Returns "" for an out-of-range index (negative or beyond the end of the string). Never panics.
let s = "café";grapheme_at(s, 0); // "c"grapheme_at(s, 3); // "é" (single grapheme, may be multiple bytes)grapheme_at(s, 99); // ""char_code(character: string) -> int
Section titled “char_code(character: string) -> int”Return the Unicode code point of the first grapheme cluster in character. Returns -1 for an empty string or an invalid input.
char_code("A"); // 65char_code("a"); // 97char_code("€"); // 8364char_code(""); // -1Note: Only the first grapheme cluster of the argument is examined. Pass a single character for predictable results.
strings_equal(left: string, right: string) -> boolean
Section titled “strings_equal(left: string, right: string) -> boolean”Return true if two strings have the same length and each grapheme cluster matches at every position. This performs a grapheme-by-grapheme comparison using char_code internally.
strings_equal("hello", "hello"); // truestrings_equal("hello", "Hello"); // falseNote: The == operator compares strings by value in most contexts; strings_equal is available as an explicit alternative.
clamp(value: float, minimum: float, maximum: float) -> float
Section titled “clamp(value: float, minimum: float, maximum: float) -> float”Return value clamped to the range [minimum, maximum]. Works with both integers and floating-point numbers.
clamp(5, 0, 10); // 5clamp(-3, 0, 10); // 0clamp(15, 0, 10); // 10Struct and Debug Utilities
Section titled “Struct and Debug Utilities”struct_field(name: string, value: any) -> StructField
Section titled “struct_field(name: string, value: any) -> StructField”Construct a StructField record with a name and a value. Used as a building block for struct_repr.
let field = struct_field("age", 30);struct_repr(name: string, fields: StructField[]) -> string
Section titled “struct_repr(name: string, fields: StructField[]) -> string”Produce a human-readable debug representation of a struct. The output format is Name(field1=value1, field2=value2, ...).
struct Point { x: float; y: float;}
fn point_repr(p: Point) -> string { return struct_repr("Point", [ struct_field("x", p.x), struct_field("y", p.y), ]);}
// point_repr(Point { x: 3, y: 7 }) => "Point(x=3, y=7)"to_debug_string(value: any) -> string
Section titled “to_debug_string(value: any) -> string”Convert any value to its debug string representation. Used internally by struct_repr and string interpolation.
to_debug_string(42); // "42"to_debug_string(true); // "true"to_debug_string(null); // "null"to_debug_string("hello"); // "hello"Type Checking Utilities
Section titled “Type Checking Utilities”These functions are used internally by the compiler-generated type guards. They are available in user code but are most commonly used via the check_type wrapper.
check_type(value: any, descriptor: string) -> boolean
Section titled “check_type(value: any, descriptor: string) -> boolean”Return true if value conforms to the type described by the descriptor string. Descriptor syntax mirrors Sailfin type notation: "string", "int", "float", "boolean", "string[]", "string | null", etc. The legacy "number" descriptor is accepted as an alias for "float" to preserve compatibility with the deprecated number type alias.
check_type("hello", "string"); // truecheck_type(42, "int"); // truecheck_type(3.14, "float"); // truecheck_type(null, "string?"); // true (? => union with void/null)check_type([1, 2], "int[]"); // truecheck_type("x", "string | int"); // trueRuntime Utilities
Section titled “Runtime Utilities”match_exhaustive_failed(value: any) -> never
Section titled “match_exhaustive_failed(value: any) -> never”Runtime backstop invoked by compiler-generated code when a match expression fails to cover all cases at runtime. Raises a ValueError with a message including the unmatched value. This function is never called when all match arms are genuinely exhaustive.
You should not call this function directly. It appears in generated code and in documentation so that stack traces referencing it can be understood.
Concurrency and Async Utilities
Section titled “Concurrency and Async Utilities”Status:
routine,await, andparallelare shipped language constructs, and theChannel<T>type from thesyncmodule works today (see the examples below). Structured-concurrency supervision — scope semantics, cancellation, andspawn— is on the roadmap.
monotonic_millis() -> int ![clock]
Section titled “monotonic_millis() -> int ![clock]”Return the current value of a monotonic clock in milliseconds. Useful for measuring elapsed time. The absolute value is not meaningful; only differences between two calls are useful.
fn timed_operation() ![io, clock] { let start = monotonic_millis(); do_work(); let elapsed = monotonic_millis() - start; print("elapsed: {{elapsed}} ms");}channel<T>() -> Channel<T> and channel<T>(capacity: int) -> Channel<T>
Section titled “channel<T>() -> Channel<T> and channel<T>(capacity: int) -> Channel<T>”Create an unbuffered or bounded channel for passing values between concurrent
tasks. capacity = 0 (the no-argument form) produces an unbuffered
(synchronous) channel. Imported from the sync module.
import { Channel, channel } from "sync";
async fn main() ![io] { let messages: Channel<int> = channel();
routine { messages.send(42); }
let result: int = await messages.receive(); print("Received: {{result}}");}Channel<T>.send(value: T) and Channel<T>.receive() -> T
Section titled “Channel<T>.send(value: T) and Channel<T>.receive() -> T”Send a value into the channel, or await the next value out. send returns
immediately on a buffered channel (blocks when the buffer is full); receive
yields via await until a value is available.
import { Channel, channel } from "sync";import { sleep } from "time";
fn main() ![clock, io] { let buffer: Channel<int> = channel(10); // bounded buffer
routine { for i in 1..20 { print("Producing {{i}}"); buffer.send(i); sleep(500); } }
routine { loop { let item = await buffer.receive(); print("Consumed {{item}}"); sleep(1000); } }}Coming in 1.0:
Channel<T>.close()and explicit cancellation, plus a structuredscope { ... }supervisor for grouping routines. See the roadmap.
parallel [ ... ] — language construct
Section titled “parallel [ ... ] — language construct”Run an array literal of zero-argument lambdas concurrently and collect their
return values. parallel is a keyword, not a stdlib function — the
operand is an array literal of fn() -> T { ... } expressions.
fn computeTask1() -> int { return 21; }fn computeTask2() -> int { return 21; }
fn main() ![io] { let results = parallel [ fn() -> int { return computeTask1(); }, fn() -> int { return computeTask2(); }, ];
print("Results: {{results}}");}Array Utilities
Section titled “Array Utilities”array_map(items: any[], mapper: (any) -> any) -> any[]
Section titled “array_map(items: any[], mapper: (any) -> any) -> any[]”Apply mapper to each element and return a new array of results.
let numbers = [1, 2, 3, 4];let doubled = array_map(numbers, fn(n: int) -> int { return n * 2; });// [2, 4, 6, 8]array_filter(items: any[], predicate: (any) -> boolean) -> any[]
Section titled “array_filter(items: any[], predicate: (any) -> boolean) -> any[]”Return a new array containing only the elements for which predicate returns true.
let numbers = [1, 2, 3, 4, 5];let evens = array_filter(numbers, fn(n: int) -> boolean { return n % 2 == 0; });// [2, 4]array_reduce(items: any[], initial: any, reducer: (any, any) -> any) -> any
Section titled “array_reduce(items: any[], initial: any, reducer: (any, any) -> any) -> any”Fold items into a single value using reducer, starting from initial.
let numbers = [1, 2, 3, 4, 5];let sum = array_reduce(numbers, 0, fn(acc: int, n: int) -> int { return acc + n; });// 15Enum Utilities
Section titled “Enum Utilities”These functions are generated by the compiler for enum types and are available for use in custom enum-handling code. They are primarily an implementation detail of the compiler’s enum lowering.
enum_type(name: string) -> EnumType
Section titled “enum_type(name: string) -> EnumType”Create a new EnumType descriptor with no variants defined yet.
enum_define_variant(enum_type: EnumType, variant_name: string, field_names: string[]) -> EnumType
Section titled “enum_define_variant(enum_type: EnumType, variant_name: string, field_names: string[]) -> EnumType”Return a new EnumType with the named variant added.
enum_field(name: string, value: any) -> EnumField
Section titled “enum_field(name: string, value: any) -> EnumField”Construct a single EnumField.
enum_instantiate(enum_type: EnumType, variant_name: string, provided: EnumField[]) -> EnumInstance
Section titled “enum_instantiate(enum_type: EnumType, variant_name: string, provided: EnumField[]) -> EnumInstance”Create an EnumInstance for the named variant, filling in null for any fields not in provided.
enum_get_field(instance: EnumInstance, name: string) -> any
Section titled “enum_get_field(instance: EnumInstance, name: string) -> any”Look up a field value by name on an EnumInstance. Returns null if not found.
Prelude Structs
Section titled “Prelude Structs”These structs are defined in the prelude and may appear in user-facing APIs or diagnostics.
StructField
Section titled “StructField”struct StructField { name: string; value: any;}EnumField
Section titled “EnumField”struct EnumField { name: string; value: any;}EnumVariantDefinition
Section titled “EnumVariantDefinition”struct EnumVariantDefinition { name: string; field_names: string[];}EnumType
Section titled “EnumType”struct EnumType { name: string; variants: EnumVariantDefinition[];}EnumInstance
Section titled “EnumInstance”struct EnumInstance { type: EnumType; variant: string; fields: EnumField[];}TypeDescriptor
Section titled “TypeDescriptor”struct TypeDescriptor { kind: string; name: string?; items: TypeDescriptor[];}Used internally by check_type and parse_type_descriptor.
fs module
Section titled “fs module”The fs module provides filesystem access. All operations require the ![io] effect. The module is bound from runtime.fs in the prelude — no import statement is needed.
fs.readFile(path: string) -> string ![io]
Section titled “fs.readFile(path: string) -> string ![io]”Read the entire contents of the file at path and return them as a string. Raises a runtime error if the file does not exist or cannot be read.
fn load_config(path: string) -> string ![io] { return fs.readFile(path);}Notes:
- The returned string includes all bytes decoded as UTF-8.
- No line-ending normalization is performed.
- For large files, the entire content is loaded into memory.
fs.writeFile(path: string, content: string) ![io]
Section titled “fs.writeFile(path: string, content: string) ![io]”Write content to the file at path, creating the file if it does not exist and overwriting it completely if it does. Parent directories must already exist.
fn save_result(path: string, data: string) ![io] { fs.writeFile(path, data);}fs.appendFile(path: string, content: string) ![io]
Section titled “fs.appendFile(path: string, content: string) ![io]”Append content to the file at path. If the file does not exist it is created. Existing content is preserved; the new content is added at the end.
fn log_to_file(path: string, message: string) ![io] { fs.appendFile(path, message + "\n");}fs.exists(path: string) -> boolean ![io]
Section titled “fs.exists(path: string) -> boolean ![io]”Return true if a file or directory exists at path, false otherwise. Does not distinguish between files and directories.
fn ensure_config(path: string) ![io] { if !fs.exists(path) { fs.writeFile(path, "{}"); }}fs.writeLines(path: string, lines: string[]) ![io]
Section titled “fs.writeLines(path: string, lines: string[]) ![io]”Write an array of strings to path, one per line, overwriting any existing file. Each element is written with a trailing newline.
fn write_report(path: string, lines: string[]) ![io] { fs.writeLines(path, lines);}fs.set_perms(path: string, mode: int) -> boolean ![io]
Section titled “fs.set_perms(path: string, mode: int) -> boolean ![io]”Set POSIX permission bits on path — the chmod(2) wrapper. mode is masked to the lower 12 bits (perm + sticky/setuid/setgid). Returns true on success, false on any error (missing file, permission denied, etc.). POSIX-only; Windows returns false.
fn make_executable(path: string) ![io] { // 0o755 = rwxr-xr-x (octal literals are pending; decimal for now) fs.set_perms(path, 493);}Note: octal literals (0o755) are pending parser support. Until they land, pass the decimal equivalent.
fs.get_perms(path: string) -> int ![io]
Section titled “fs.get_perms(path: string) -> int ![io]”Return the lower 12 bits of st_mode for path — the stat -c '%a' equivalent. Returns -1 on any error (missing file, permission denied, etc.).
fn is_world_readable(path: string) -> boolean ![io] { let mode = fs.get_perms(path); if mode == -1 { return false; } // 4 = 0o004 = world-readable bit return (mode & 4) != 0;}fs.mkdtemp(prefix: string) -> string ![io]
Section titled “fs.mkdtemp(prefix: string) -> string ![io]”Create a unique directory under $TMPDIR (or /tmp if unset) with mode 0700, using mkdtemp(3) so the kernel guarantees uniqueness. Returns the absolute path, or an empty string on failure.
If prefix contains a /, it is treated as a path-prefixed template (the caller picks the parent dir); otherwise the result lives under the system temp dir.
fn scratch_for_run() -> string ![io] { return fs.mkdtemp("sfn-build-");}POSIX-only; Windows returns an empty string.
fs.is_executable(path: string) -> boolean ![io]
Section titled “fs.is_executable(path: string) -> boolean ![io]”Return true iff the current process can exec the path — the access(path, X_OK) equivalent. Permission errors and missing files both collapse to false. POSIX-only; Windows returns false.
fn find_in_path(name: string) -> string ![io] { let candidate = "/usr/local/bin/" + name; if fs.is_executable(candidate) { return candidate; } return "";}fs.symlink(target: string, link: string) -> boolean ![io]
Section titled “fs.symlink(target: string, link: string) -> boolean ![io]”Create a symbolic link at link pointing at target — the symlink(2) wrapper. Per POSIX, the target need not exist; dangling links are intentionally allowed. Returns true on success, false if link already exists or any other error occurs. POSIX-only; Windows returns false.
fn pin_current_release(target: string, link: string) ![io] { fs.symlink(target, link);}Filesystem — Planned
Section titled “Filesystem — Planned”The following filesystem helpers are planned for a future release and are not available today:
fs.readLines(path: string) -> string[] ![io]— read file as an array of linesfs.move(src: string, dst: string) ![io]— rename or move a filefs.copy(src: string, dst: string) ![io]— copy a filefs.walk(path: string) -> string[] ![io]— recursive directory walk
http module
Section titled “http module”The http module provides outbound HTTP client functionality. All operations require the ![net] effect. The module is bound from runtime.http in the prelude.
http.get(url: string) -> Response ![net]
Section titled “http.get(url: string) -> Response ![net]”Perform an HTTP GET request to url and return a Response. Blocks until the response is received or the request fails.
fn fetch_json(url: string) -> string ![net] { let response = http.get(url); return response.body;}http.post(url: string, body: string) -> Response ![net]
Section titled “http.post(url: string, body: string) -> Response ![net]”Perform an HTTP POST request to url with the given string body. Returns a Response. The Content-Type is not set automatically; include it in a custom header when required (see planned headers API below).
fn submit(url: string, payload: string) -> string ![net] { let response = http.post(url, payload); return response.body;}Response type
Section titled “Response type”The Response object returned by http.get and http.post has the following fields:
| Field | Type | Description |
|---|---|---|
body | string | Response body as a UTF-8 string |
status | number | HTTP status code (e.g. 200, 404) |
Note: The full response shape (headers, streaming body, redirect policy, timeouts) is planned. The current
Responseexposesbodyandstatusonly.
HTTP — Planned
Section titled “HTTP — Planned”The following HTTP features are planned for a future release:
- Request headers and custom
Content-Type - Authentication helpers (Bearer, Basic)
- Timeout and retry configuration
http.put,http.delete,http.patch- Streaming response bodies
websocket.connectfor WebSocket support
sfn/log capsule
Section titled “sfn/log capsule”The sfn/log capsule provides structured log output with severity levels. Unlike print, log functions include a level prefix and route warnings and errors to stderr automatically.
Import the capsule before use:
import { log } from "sfn/log";All log functions require the ![io] effect.
log.info(message: string) ![io]
Section titled “log.info(message: string) ![io]”Write an informational message to stdout with an [INFO] prefix. Use for routine operational messages.
fn start_server(port: int) ![io] { log.info("Server starting on port {{port}}");}log.warn(message: string) ![io]
Section titled “log.warn(message: string) ![io]”Write a warning message to stderr with a [WARN] prefix. Use when something unexpected occurred but execution can continue.
fn load_optional(path: string) -> string ![io] { if !fs.exists(path) { log.warn("optional config not found: {{path}}"); return ""; } return fs.readFile(path);}log.error(message: string) ![io]
Section titled “log.error(message: string) ![io]”Write an error message to stderr with an [ERROR] prefix. Use for failures that require attention.
fn connect(host: string) ![io, net] { let response = http.get("http://{{host}}/health"); if response.status != 200 { log.error("health check failed: status {{response.status}}"); }}log.debug(message: string) ![io]
Section titled “log.debug(message: string) ![io]”Write a debug message to stdout with a [DEBUG] prefix. Use for verbose diagnostic output that is typically suppressed in production. Whether debug output appears may be controlled by runtime log-level configuration in a future release.
fn parse_token(raw: string) -> string ![io] { log.debug("parsing token: {{raw}}"); // ... return raw;}Collections — Planned
Section titled “Collections — Planned”Coming in 1.0: Generic containers (
Map<K, V>,Set<T>, and an explicit growableVec<T>) depend on generic type constraints landing first. See the roadmap for sequencing.Today, array literals (
[1, 2, 3]) withT[]types,.length,.push(item), and the prelude array utilities (array_map,array_filter,array_reduce) are the shipped collection surface.
Arrays (available today)
Section titled “Arrays (available today)”let numbers: int[] = [1, 2, 3];numbers.push(4);let n = numbers.length; // 4let first = numbers[0]; // 1Planned Vec<T>
Section titled “Planned Vec<T>”// Planned API — not yet implementedVec<T>.new() -> Vec<T>Vec<T>.push(item: T) -> voidVec<T>.pop() -> T?Vec<T>.get(index: int) -> T?Vec<T>.len() -> intVec<T>.is_empty() -> booleanVec<T>.contains(item: T) -> booleanVec<T>.remove(index: int) -> TVec<T>.slice(start: int, end: int) -> Vec<T>Planned Map<K, V>
Section titled “Planned Map<K, V>”// Planned API — not yet implementedMap<K, V>.new() -> Map<K, V>Map<K, V>.set(key: K, value: V) -> voidMap<K, V>.get(key: K) -> V?Map<K, V>.has(key: K) -> booleanMap<K, V>.delete(key: K) -> voidMap<K, V>.keys() -> K[]Map<K, V>.values() -> V[]Map<K, V>.len() -> intPlanned Set<T>
Section titled “Planned Set<T>”// Planned API — not yet implementedSet<T>.new() -> Set<T>Set<T>.add(item: T) -> voidSet<T>.has(item: T) -> booleanSet<T>.delete(item: T) -> voidSet<T>.size() -> intPlanned modules
Section titled “Planned modules”The modules below are on the roadmap and are documented here to give a preview of the planned API. None are available in the current release.
rand module — Planned (![rand] effect)
Section titled “rand module — Planned (![rand] effect)”Coming in 1.0: Random-number helpers as a standard module. The
randeffect token is parsed today but norand.*APIs ship yet. See the roadmap.
// Planned — not yet implementedrand.int(min: int, max: int) -> int ![rand]rand.float() -> float ![rand] // uniform in [0.0, 1.0)rand.bool() -> boolean ![rand]rand.shuffle<T>(items: T[]) -> T[] ![rand]rand.choice<T>(items: T[]) -> T? ![rand]time module (![clock] effect)
Section titled “time module (![clock] effect)”sleep(milliseconds: int) ![clock]
Section titled “sleep(milliseconds: int) ![clock]”Imported from the time module. Suspend the current execution context for at
least milliseconds milliseconds. Requires the clock effect.
import { sleep } from "time";
fn wait_a_bit() ![clock] { sleep(500); // pause for 500 ms}Coming in 1.0: A structured date/time API on top of the existing
clockeffect.sleep(fromtime) and themonotonic_millisprelude function are available today; richer wall-clock access is planned. See the roadmap.
// Planned — not yet implementedclock.now() -> Timestamp ![clock]clock.utc_now() -> Timestamp ![clock]Timestamp.unix_millis() -> intTimestamp.format(pattern: string) -> stringprocess module — Planned (![io] effect)
Section titled “process module — Planned (![io] effect)”Coming in 1.0: Subprocess execution and process lifecycle helpers. See the roadmap.
// Planned — not yet implementedprocess.exec(command: string) -> ProcessResult ![io]process.exit(code: int) -> never ![io]
struct ProcessResult { stdout: string; stderr: string; exit_code: int;}sync module
Section titled “sync module”The sync module exposes Channel<T> and the channel() constructor. See
the Concurrency and Async Utilities
section above for the full API and examples.
sfn/ai capsule — Planned (![model] effect)
Section titled “sfn/ai capsule — Planned (![model] effect)”Post-1.0: The
sfn/aicapsule will provide model invocation, typed output schemas, tool dispatch, and provider adapters as library functions. Allsfn/aifunctions carry![model]in their signatures, so any caller must declare![model]. Themodel,prompt,tool, andpipelineblock keywords have been removed from the language. See the roadmap.
// Planned sfn/ai API — not yet implementedimport { call_model } from "sfn/ai";
fn classify(text: string) -> string ![model] { return call_model("classifier", text);}The standard library is actively expanding as the runtime migrates from C to Sailfin. See Runtime ABI for migration status.