Console I/O
Source-defined print and println overloads available to Prismio programs.
Last verified
Console output has two public functions: print and println. They are ordinary exact-type overloads implemented in the shipped std/io.psm source module, and one call may carry several values. There is no prelude — import std.io is required, like any other module. That is deliberate: a program that names no I/O carries none, which is what lets a target with no stdout link at all.
print("value: ")
println(42)
println(3.5)
println(true)
println('A')The functions write to the descriptor directly — stdout for print/println, stderr for eprint/eprintln — with no buffering in between. Newline variants append a line ending; non-newline variants leave subsequent output on the current line.
String output
print("progress: ")
println("done")print and println accept String. Their parameters borrow, so printing does not consume the caller's owned string.
Several values in one call
print and println accept more than one value. A space goes between them, as in Python:
import std.io
fn main() -> Int {
print("Total: ", 5)
println()
println("x", 1, true, 'c', 2.5)
return 0
}Total: 5
x 1 true c 2.5
println() with no arguments writes the line break on its own, which is how the
first line above ends. eprintln() does the same on stderr.
Choosing the separator
separator(...) as the last argument replaces the space:
import std.io
fn main() -> Int {
println(1, 2, 3, separator(", "))
println("a", "b", separator(""))
let bar = " | "
println("x", "y", "z", separator(bar))
return 0
}1, 2, 3
ab
x | y | z
It takes a String literal or a name. The separator is written once per gap, so a
call in that position -- separator(strFromInt(n)) -- is refused rather than run
repeatedly; bind it to a let first and pass the name.
What this is, and is not
There is no variadic function here and no new overload. print(a, b, c) is
rewritten by the compiler into the calls you would have written yourself:
print(a)
print(" ")
print(b)
print(" ")
print(c)Each value goes through the exact-type overload it always had, so a type with no
print overload has none here either, and the diagnostic points at that value.
The trailing println keeps its name rather than becoming print plus a
newline, so a line still costs one write.
Two consequences worth knowing:
- Your own declaration wins. A program that declares
fn print(a: String, b: String)is calling that, not being rewritten around it. The same is true ofseparator: declare a function with that name and the marker turns itself off. - The pieces are separate writes.
println("a", "b")reaches the descriptor as three writes rather than one, so another thread printing at the same time can land between them.
Standard error
eprint and eprintln write to stderr instead of stdout.
import std.io
fn main() -> Int {
eprintln("scanning 3 files")
println("result: 42")
return 0
}They exist so that a program whose stdout carries a format can still say
something to the person watching without corrupting it. Anything a caller may
pipe into another tool -- a manifest, JSON, a column of numbers -- is a format in
this sense, and a progress line printed alongside it is no longer parseable
output. The compiler's own host-routing banner is on stderr for exactly this
reason: while it printed to stdout it prefixed aif --manifest and broke that
format's one guarantee, that its first line is aif-manifest 1.
eprint and eprintln accept every type their stdout twins do, and take several
values in one call in the same way. A status line counts things — the compiler
uses one to report how many standard-library modules it rebuilt beside a
project-local toolchain:
import std.io
fn main() -> Int {
eprint("rebuilt ")
eprint(14)
eprintln(" modules")
return 0
}The width-specific overloads (I8 through U64, Float, Bool, Char) are on
stderr too. They have to be: eprintln("count: ", n) splits into an eprint of
the text and an eprintln of the value, so a type missing from this set would
turn a working stdout line into a diagnostic the moment it moved to stderr.
Exact overloads
The same two names accept the five supported console value types. Overload selection uses the argument's exact type; it is not a variadic formatter or a runtime Any conversion.
| Value type | No newline | With newline |
|---|---|---|
String | print | println |
Int, I8, I16, I64, Isize | print | println |
U8, U16, U32, U64, Usize | print | println |
Float | print | println |
Bool | print | println |
Char | print | println |
import std.io
fn main() -> Int {
let code: U8 = 255
let bytes: U64 = 4294967296
println(code)
println(bytes)
return 0
}These are separate exact overloads, not implicit integer promotion. Arithmetic and assignment still require compatible widths, while output preserves the complete signed or unsigned value without a narrowing cast.
Char is byte-sized in 0.1, so its overload is not a complete Unicode scalar-output API. String source/runtime encoding details should be handled through the documented string/FFI contract.
Formatting values
There is no string interpolation or generic format surface. Pass the parts to one call, or make several:
println("items: ", list_len(items), separator(""))For application-specific rich formatting, write typed helper functions or use a carefully declared foreign formatting wrapper. Avoid C variadic APIs unless a stable adapter fixes the signature because source-level FFI variadics are not documented.
The old chunked names such as println_int, print_bool and print_char are not public Prismio functions, and as of 0.1 they no longer exist as runtime symbols either — a program that declared one by hand will fail to link naming it. Use the overload set directly.
What "printed" guarantees
print writes the whole string, or stops because the descriptor refused it.
That is worth stating because the underlying write does not promise it. It returns how many bytes it took, and fewer than asked for is an ordinary outcome rather than an error — most visibly on a descriptor someone set O_NONBLOCK on, which is a property of the open file description and so is inherited across fork/exec and shared by every duplicate of it. std/io.psm resumes from where a short write stopped, and reissues when a write was interrupted by a signal before it moved any bytes.
A descriptor that refuses outright ends the attempt rather than spinning on it. These are write_all semantics, not polling: a non-blocking descriptor that has no room right now is not waited on, because blocking inside print on a descriptor its owner deliberately made non-blocking is not the caller's intent.
A broken pipe is a signal, not a return value. Nothing in the runtime ignores SIGPIPE, so a program whose reader has gone away dies of the signal, exactly as cat does in the same position. This is what a filter should do, and no error handling in print would run instead.
Errors and buffering
There is no buffering to select or flush: output reaches the descriptor on each call. One consequence is worth relying on — stdout and stderr cannot reorder relative to each other, so a status line on stderr always lands where it was written relative to the surrounding stdout.
The 0.1 print functions do not expose a structured recoverable I/O result or a writer handle. A caller that must know whether every byte arrived cannot learn it from print.
Float is the one value type still formatted in C, because %g has no source-level formatter yet; every other overload formats in Prismio.
std/io.psm is currently a minimal output module. Formatted output, input streams, files, error objects, and pluggable writers are not part of the 0.1 standard library.
For input or file access, define a local C-compatible wrapper and state its ownership/error contracts. The planned filesystem and broader I/O modules remain Coming Soon.