Renderingen-US

Capabilities and frames

The seam between Faber and every way an operating system can implement I/O.

call is Faber's low-level capability call primitive — the boundary between Faber code and the outside world. It opens a typed conversation (channel) with a host resource identified by a route string, then exchanges structured frames (frame) over directional half-streams. The host kernel dispatches each route to a pluggable provider crate, which implements the actual I/O — filesystem, networking, console, time, randomness, or anything else the OS can do.

call is a keyword, not a function. It opens an opaque conversation with a route named by an ascii literal and optional opener data:

# Simple materialized call: open, send opener, drain response
const string content ← call 'solum:lege' ("config.toml") ↦ string

# Typed conversation handle for streaming interaction
const channel s ← call 'processus:curre' ("ls", ["-la"])

The route string follows a prefix:verb pattern. The host kernel matches on the prefix only — the provider owns all verbs under that prefix:

solum:lege   ─┐
solum:modum  ─┼─►  prefix "solum"  ──►  only provider crate
solum:vincula─┘

call is not a foreign function interface. It does not call C functions, load dynamic libraries, or embed inline assembly. It is a structured message passing boundary: Faber sends typed frames and receives typed frames, without knowing whether the provider is implemented in Rust, runs in-process, delegates to a system call, or forwards to a remote host.

Frame types#

Five compiler-owned types form the frame system:

TypeRoleKey surface
channelConversation handle — an in-flight bidirectional exchangeCreated by call; drained via ↦ T or split into views
frame<T>Frame envelope — one structured message in a conversationFields: id, call, status, data, created_ms, from, trace
statusLifecycle marker enumrequest, item, byte, bulk, done, error, cancel
send<T>Outbound half-stream — send frames to the providerda(T), fini() → status
recv<T>Inbound half-stream — receive frames from the provideraccipe(), cursor(), exhauri(), fini()

Using directional views#

# Open a conversation, get directional views
const channel s ← call 'solum:scribe' ("output.txt")
const send<string> out ← s.meus<string>()
const recv<string> input ← s.tuus<string>()

# Send content frames
out.da("line one")
out.da("line two")
out.fini()

# Read response frames
for from input.cursor() const frame {
    print frame.data
}
const status inbound ← input.fini()

Simple materialization#

For the common case — open, send opener, drain all response frames into one value — channel ↦ T collapses the conversation:

# Read a file: open + drain into string
const string body ← call 'solum:lege' ("config.toml") ↦ string

# Parse JSON from an HTTP response
const json data ← call 'http:peti' ("https://api.example.com/data") ↦ json

Materialization uses a type-directed collector: ↦ string concatenates all inbound frames, ↦ json parses the concatenated payload, ↦ list<T> collects frames into a list.

Host providers#

Effect families are implemented as separate provider crates under faberlang/hosts/crates. Each provider owns all verbs under its prefix:

ProviderPrefixI/O domain
onlyonly:*Filesystem: read, write, metadata, directory operations
processusprocessus:*Process execution: spawn, pipe, exit codes
consolumconsolum:*Console I/O: stdin, stdout, stderr
tempustempus:*Time: now, sleep, timers
aleatoraleator:*Randomness: entropy, distributions
httphttp:*HTTP client (Tier D, when landed)

Providers are separate crates with their own dependencies — only does not pull in HTTP, http does not pull in filesystem code. Each provider exports a register() function that the generated host manifest calls at startup.

Layer stack#

Faber source:     call 'solum:lege' (path) ↦ string
Compiler:         channel open + generic attach (no provider crate names)
Runtime:          HostDispatch + conversation protocol (faber-runtime)
Kernel:           route(frame) → provider for prefix
Provider:         only provider reads file, returns content

The compiler emits generic dispatch — it never embeds provider crate names into generated code. The runtime provides HostDispatch and the conversation protocol. The kernel (from hosts/crates/host-kernel) routes frames to the correct provider based on prefix. The provider (from hosts/crates/* providers) performs the actual I/O.

This means generated Faber code is provider-neutral. The same compiled binary can be linked against different provider implementations — a real filesystem provider for production, a mock provider for testing — by changing the compile manifest.

Compile manifest#

Which providers to link is controlled by the generated compile manifest and the faber.toml [dispatch] table:

[target.rust]
host = "native"

[dispatch]
providers = ["solum", "processus", "consolum", "tempus", "aleator"]

[dispatch.providers.http]
enabled = true

During authoring, missing providers produce a runtime E_NO_ROUTE error. In strict mode (future), every call prefix in the program must appear in the compile manifest, and the compiler validates that the provider's capability manifest covers the routes used.

Architecture#

The host platform is split across three repositories in the faberlang organisation:

RepositoryRole
hosts/crates/host-kernelThin router — owns Frame, Conversation, terminal lifecycle, prefix dispatch, structured errors (E_NO_ROUTE), capability manifest aggregation
hosts/crates/host-nativeNative attach — workers, register_providers startup hook, generated host_register.rs integration
hosts/crates/* providersProvider implementations — Cargo workspace with per-family crates (only, processus, etc.)

Each provider crate owns its own native dependencies. The http provider pulls in hyper and tokio only when HTTP is enabled. The only provider uses standard file APIs with no additional network dependencies.

Same route, any host. Because call dispatches on route strings and providers are pluggable, the same Faber source can target a native binary (host-native-rs), a WASM runtime (host-kernel as a Frame/Wasm adapter), or a TypeScript Node.js process (host-providers-ts) without changing a single line of Faber code.

Norma wrappers#

Most Faber code does not call call directly. The Norma standard library wraps common call routes in typed functions:

# Norma wraps call in typed, reviewed functions
fn read(string via) → string {
    return call 'solum:lege' (via) ↦ string
}

fn write(string via, string content) → void {
    const void _ ← call 'solum:scribe' (via, content) ↦ void
}

fn curre(string command, list<string> args) → string {
    return call 'processus:curre' (command, args) ↦ string
}

These wrapper functions provide type safety, documentation, and error handling without hiding the fact that I/O crosses the call boundary. The Norma wrappers are open source and live under norma/src/.

References#

  1. radix/docs/design/frame-stream-types.md — full spec for channel, frame, status, send, recv
  2. radix/docs/design/host-provider-gateway.md — thin router architecture, provider contracts, compile manifest
  3. faberlang/hosts/crates/host-kernel/ — kernel router implementation
  4. faberlang/hosts/crates/host-native/ — native attach and registration
  5. faberlang/hosts/crates/ — provider crates (only, processus, consolum, tempus, aleator, http)
  6. radix/corpus/ad/ — channel exempla files