Rho Rho language and toolchain

Ports

Rho has no heap, no globals and no file I/O. That is a deliberate constraint
rather than a missing feature: it is what makes a program predictable, and it is
what makes the parts that do need a host visible instead of implicit.

A port is how a program asks for one of those parts.

Declaring what a program needs

A program declares the functions the host must supply:

extern fn read_all(path: string) string

and calls them as it calls any other function. Nothing about the call site says
"this is I/O": the declaration is the whole of the dependency, and it is in the
module, where a reader of the module sees it.

Asking for a port

A project asks the backend for the implementation by name:

# module.yml
build:
  language: go
  targets:
    - id: go
      ports:
        - fsstore

The backend generates the implementation into the project, and the generated
file is ordinary code in the project: readable, and removable if a person wants
to see what it does.

The ports today

PortWhat it suppliesUsed by
fsstoreA module store on the filesystem: put, get, search, listthe registry
logemitWriting a log record, with the formatter's own shapeany project that logs
docemitThe documentation generator, as a page or a sitea project that serves documentation
gatewayA serial link, UDP delivery, a monotonic clock and a wall clock, shared state, tx-id correlation and the telemetry outboxan edge gateway that reads a node and streams what it reads
telemetryPolling a gateway outbox, writing a batch through the sql port, latest and time-range queriesan aggregator that keeps a series for other applications

A worked example

A component declares a library; a project asks for a port; the backend supplies
the implementation. Nothing in the project is generated from the component's
sources, and nothing in the component is generated for the project.

// modules/components/rho-log/src/log.rho
module rho.log

extern fn emit(level: u32, subject: string, message: string) void

fn info(subject: string, message: string) void {
    emit(1, subject, message);
}
# the project's module.yml
build:
  language: go
  targets:
    - id: go
      ports:
        - logemit

The project calls info(...). The logemit port supplies emit, writing the
record in the shape the formatter produces, so a device writing to a serial
console and a server writing to stdout cannot drift apart.

Why a port and not a library

A library would have to be compiled into every project that uses it, and a
library that writes files and takes locks is a library every project then
carries. A port supplies the implementation at generation time, from the
backend, so:

  • the project stays pure Rho, and every dependency it has is declared in its
    manifest;
  • the implementation is one per backend, so two projects do not drift apart;
  • the boundary is visible: what came from the host is what a port named, and
    nothing else did.

Note

A port is a promise the backend makes, checked when the port is generated rather
than at the call. A project that declares a port no backend supplies fails at
the call site rather than at the manifest, so the list of ports a project's
targets declare in module.yml is the honest record of what a project needs.

A service that also does something else

An HTTP service answers requests, but an edge gateway also reads a link while it
answers, and a program has no threads of its own. A server-kind module declares
fn background() void, and the generated service runs it on its own goroutine,
after init() and beside the listener:

fn init() void {
    print("gateway: starting\n")
}

fn background() void {
    if !gateway_serial_start() {
        return
    }
    while true {
        let frame: string = gateway_serial_read(100)
        if strlen(frame) > 0 {
            ingest_frame(frame)
        }
    }
}

The state background() writes and a handler reads is what the gateway port
is for: a value a program cannot declare as a global is one the host holds, and
the port's mutex is what makes it safe to share. The frame grammar itself stays
in Rho — the port supplies the link, not the protocol.

gateway_now_ms is a monotonic duration since the gateway started; it is not a
calendar. gateway_wall_time_ms is the host's Unix time, which is what a
service records beside a node's own uptime timestamp. A generated service's
streaming responses are not bounded by the server's read or write deadline, so a
file-following event stream stays open for the action it reports.