I/O Operations in Carbon
Reading input and writing output in Carbon using the experimental Core "io" library - PrintStr, Print, PrintChar, and ReadChar with Docker-ready examples
Input and output are where an experimental language shows its true age, and Carbon is refreshingly honest about it. As a multi-paradigm systems language (imperative, object-oriented, and generic) with static, partially-inferred typing, Carbon is designed to eventually offer rich, type-safe I/O. But the pre-0.1 nightly toolchain ships only a tiny, deliberately temporary Core library called "io". Its own source even carries the note: “This library is not part of the design. Either write a matching proposal or remove this.”
That small surface is exactly what makes it a good teaching subject. Every I/O function in the current toolchain maps directly to a familiar C library call:
Core.Print(x: i32)lowers toprintf("%d\n", x)— it prints a 32-bit integer followed by a newline.Core.PrintChar(x: char)lowers toputchar— it prints one character with no newline.Core.PrintStr(msg: str)is written in Carbon itself: it loops over the string and callsPrintCharfor each character, so it adds no trailing newline.Core.ReadChar() -> i32lowers togetchar— it reads one byte from standard input and returns it, orCore.EOF()(which is-1) at end of input.
Notice what is not here: there is no file library, no formatted string interpolation, and no line-reading helper. In this tutorial you will learn how to produce formatted output by composing these primitives, how to read from standard input with ReadChar, and how to perform “file I/O” today the only way Carbon supports it — through shell redirection.
Console Output: PrintStr, Print, and PrintChar
The single most important detail about Carbon output is the newline behavior: Print adds one, PrintStr and PrintChar do not. Once you internalize that, you can assemble any line you want.
Create a file named io_output.carbon:
import Core library "io";
fn Run() {
// PrintStr writes a string with no trailing newline.
Core.PrintStr("=== Carbon I/O ===\n");
// Print writes an i32 followed by a newline (it lowers to printf "%d\n").
Core.PrintStr("First appeared: ");
Core.Print(2022);
// Because PrintStr never adds a newline, you can build a line in pieces
// and compute values inline before finishing it with Print.
let width: i32 = 8;
let height: i32 = 5;
Core.PrintStr("Area: ");
Core.Print(width * height);
// PrintChar writes a single character (again, no newline). Character
// literals use single quotes, including escapes like '\n'.
Core.PrintChar('D');
Core.PrintChar('o');
Core.PrintChar('n');
Core.PrintChar('e');
Core.PrintChar('\n');
}
Here Print(width * height) prints the computed i32 value 40, showing that Print accepts any i32 expression, not just literals. The four PrintChar calls followed by PrintChar('\n') demonstrate that you are responsible for your own line breaks whenever you avoid Print.
Reading a Single Character from Standard Input
Input in the current toolchain is byte-oriented. Core.ReadChar() returns the next byte as an i32, or Core.EOF() when the stream is exhausted. This makes end-of-input handling explicit — a good fit for a systems language.
Create a file named read_char.carbon:
import Core library "io";
fn Run() {
Core.PrintStr("Reading one character from input...\n");
// ReadChar returns the byte value as an i32, or Core.EOF() (-1) if the
// input stream is empty.
var c: i32 = Core.ReadChar();
if (c == Core.EOF()) {
Core.PrintStr("No input was provided.\n");
} else {
Core.PrintStr("Byte value of first character: ");
Core.Print(c);
}
}
Because ReadChar returns an i32, you compare it against Core.EOF() (or the raw value -1) rather than against a character. Given the single-character input A, whose ASCII byte value is 65, this program reports that value directly.
Looping Over All Input Until EOF
Real input processing means reading until the stream ends. The idiom is a while loop guarded by Core.EOF(): read a character, act on it, then read the next one. Here we count characters and newlines — the beginnings of a wc-style tool.
Create a file named count_input.carbon:
import Core library "io";
fn Run() {
var chars: i32 = 0;
var newlines: i32 = 0;
// Prime the loop with the first read, then keep reading until EOF.
var c: i32 = Core.ReadChar();
while (c != Core.EOF()) {
chars += 1;
// 10 is the ASCII code for the newline character '\n'.
if (c == 10) {
newlines += 1;
}
c = Core.ReadChar();
}
Core.PrintStr("Characters read: ");
Core.Print(chars);
Core.PrintStr("Newlines read: ");
Core.Print(newlines);
}
Comparing c against the integer code 10 avoids any character-to-integer conversion, keeping the code firmly in i32 arithmetic that the nightly toolchain handles reliably. Given the two-line input Hello\nCarbon\n (13 bytes, 2 newlines), the counts come out as 13 and 2.
“File I/O” Through Shell Redirection
Carbon’s nightly toolchain has no file library — there is no open, read, or write for files, and no proposal-backed file API yet exists. That is not a gap to paper over with invented functions; it is the honest current state of an experimental language.
The practical consequence is that a Carbon program reads files the Unix way: you redirect a file onto standard input, and redirect standard output into another file. Your program never mentions filenames at all — it just processes the stream from ReadChar and emits results with Print. This example reads digits from its input and reports how many it found and their sum.
Create a file named sum_digits.carbon:
import Core library "io";
fn Run() {
var digits: i32 = 0;
var sum: i32 = 0;
var c: i32 = Core.ReadChar();
while (c != Core.EOF()) {
// ASCII digits '0' through '9' occupy byte codes 48 through 57.
if (c >= 48 and c <= 57) {
sum += c - 48;
digits += 1;
}
c = Core.ReadChar();
}
Core.PrintStr("Digits found: ");
Core.Print(digits);
Core.PrintStr("Sum of digits: ");
Core.Print(sum);
}
To feed this program a “file,” create a plain-text data file that it can read from standard input.
Create a file named numbers.txt:
Carbon 2022
42 rocks
Running ./sum_digits < numbers.txt treats numbers.txt as the program’s input. The digits 2 0 2 2 (from 2022) and 4 2 (from 42) give six digits summing to 12.
Running with Docker
Carbon’s nightly toolchain runs on Linux, so we use an Ubuntu container that downloads the toolchain, compiles the example to an object file, links it into a native executable, and runs it.
| |
To provide standard input, pipe or redirect into the compiled program. For the character-counting example, printf supplies two lines of input:
| |
For file-style I/O, redirect the data file onto standard input with <:
| |
Note: The first run downloads roughly 200 MB for the toolchain, so it takes a few minutes. You can also experiment instantly in the browser at carbon.compiler-explorer.com.
Expected Output
Running io_output:
=== Carbon I/O ===
First appeared: 2022
Area: 40
Done
Running read_char with the input A:
Reading one character from input...
Byte value of first character: 65
Running count_input with the input Hello\nCarbon\n:
Characters read: 13
Newlines read: 2
Running sum_digits < numbers.txt:
Digits found: 6
Sum of digits: 12
Key Concepts
Printadds a newline;PrintStrandPrintChardo not.Core.Print(x: i32)lowers toprintf("%d\n", x), while the string and character functions emit exactly the bytes you give them — you supply your own line breaks.- Output is composed, not formatted. The nightly toolchain has no string interpolation, so you build lines by calling
PrintStr,PrintChar, andPrintin sequence. - Input is byte-oriented through
ReadChar.Core.ReadChar()returns ani32(a byte value) orCore.EOF()(-1), so end-of-input is an explicit comparison, not an exception. - Loop until
Core.EOF(). The standard reading pattern primes with oneReadChar, processes inside awhile (c != Core.EOF())loop, and reads again at the bottom. - Work in
i32for reliability. Comparing against numeric ASCII codes (10for newline,48–57for digits) keeps arithmetic ini32and sidesteps the character-conversion rough edges of the pre-0.1 toolchain. - There is no file library yet. Carbon performs file I/O today only via shell redirection (
< input.txt,> output.txt); the"io"library is explicitly marked as temporary and outside the language design. - The
"io"library is a moving target. Because Carbon is pre-0.1, these functions and their names can change between nightly releases — always check the toolchain version you downloaded.
Comments
Loading comments...
Leave a Comment