Status
Accepted
Context
Slug aims to keep the runtime small while favouring explicit composition through modules and structured concurrency.
Terminal input is a common requirement for CLI tools, REPLs, and scripts. Traditional approaches expose blocking primitives such as:
readLine()
scanf()
input()
However, Slug already provides:
- channels: ADR-025
select: ADR-025, ADR-026- structured concurrency: ADR-015
- recursion instead of loops
Introducing a blocking input primitive would create a special case in the runtime that does not compose naturally with these primitives.
Instead, terminal input should behave like any other asynchronous event source and integrate with the existing concurrency model.
Additionally, Slug programs should behave naturally in Unix pipelines, supporting input from:
- interactive terminals
- pipes (
|) - file redirection (
<)
Decision
Terminal input will be exposed through a new module:
slug.io.stdin
stdin.readLines()
stdin.readLines() -> @chan(@struct(Full)|@struct(Empty))
Returns a shared singleton channel representing the process standard input stream.
Behaviour
- Each line read from
stdinis sent to the channel as aFull{value: "..."}struct value. -
Line endings are normalized:
\nis stripped\r\nbecomes\nthen stripped
- Empty lines are preserved as
"". - If input ends without a trailing newline, the final partial line is still emitted as a
Full{}value beforeEmpty{}is sent. -
When the input stream ends:
- a single
Empty{}value is sent - no further values are emitted.
- a single
Example stream:
Full{value: "a"}
Full{value: "b"}
Full{value: "c"}
Empty{}
Singleton Semantics
stdin.readLines() always returns the same underlying channel.
The runtime owns a single reader task that consumes the OS stdin stream and publishes values onto this channel.
Example:
val a = readLines()
val b = readLines()
Both a and b reference the same channel.
Programs should therefore treat stdin as a single-consumer stream.
If multiple modules require access to input, one module should own the stream and redistribute values explicitly.
Pipe Compatibility
The stdin stream may originate from:
- an interactive terminal
- a Unix pipe
- redirected file input
Example:
printf "a\nb\nc\n" | slug app.slug
Produces the stream:
Full{value: "a"}
Full{value: "b"}
Full{value: "c"}
Empty{}
This behaviour ensures Slug programs compose naturally with standard shell pipelines.
Example Usage
Basic line processing:
var {*} = import(
"slug.io.stdin",
"slug.channel",
)
val loop = fn(in) {
select {
recv in
} /> match {
Full{value} => {
println(">", value)
recur(in)
}
Empty => println("done")
}
}
loop(readLines())
Timeout interaction using existing channel functionality:
var {*} = import(
"slug.std",
"slug.io.stdin",
"slug.channel",
)
val loop = fn(in) {
defer onerror(e) match { Error{ type: "TimeoutError", data: {ms} } => println("Timeout after ms") }
in
/> recv(1000)
/> match {
msg @ Full{} => { println(">", msg); recur(in) }
_ => println("done")
}
}
loop(readLines())
This design allows terminal input to compose naturally with select.
Consequences
Positive
- Maintains a consistent concurrency model by exposing terminal input as a channel
- Allows
stdinto compose naturally withselect, timeouts, and other channel operations - Avoids introducing blocking IO primitives into the language runtime
- Provides excellent Unix compatibility with pipes (
|) and redirection (<) - Keeps the runtime implementation small and straightforward
- Aligns with Slug’s event-stream model for external IO sources
- Enables future IO sources (sockets, processes, signals) to follow the same pattern
Negative
- Multiple consumers compete for values because
stdinis a shared singleton channel - Programs must coordinate ownership if multiple modules require access to input
- Input cannot be rewound once consumed, mirroring standard OS stream semantics
Neutral
- Interactive programs typically process input via recursion rather than loops
- Channel consumers must handle the
Full{}/Empty{}value pattern explicitly stdin.readLines()differs fromfs.readLines(path)by returning a channel rather than a list