Intermediate

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 to printf("%d\n", x) — it prints a 32-bit integer followed by a newline.
  • Core.PrintChar(x: char) lowers to putchar — it prints one character with no newline.
  • Core.PrintStr(msg: str) is written in Carbon itself: it loops over the string and calls PrintChar for each character, so it adds no trailing newline.
  • Core.ReadChar() -> i32 lowers to getchar — it reads one byte from standard input and returns it, or Core.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.

1
2
3
4
5
# Pull the Ubuntu image
docker pull ubuntu:22.04

# Compile and run the console-output example
docker run --rm -v $(pwd):/app -w /app ubuntu:22.04 bash -c "apt-get update -qq && apt-get install -y -qq wget libgcc-11-dev > /dev/null 2>&1 && VERSION=0.0.0-0.nightly.2026.02.07 && wget -q https://github.com/carbon-language/carbon-lang/releases/download/v\${VERSION}/carbon_toolchain-\${VERSION}.tar.gz && tar -xzf carbon_toolchain-\${VERSION}.tar.gz && ./carbon_toolchain-\${VERSION}/bin/carbon compile --output=io_output.o io_output.carbon && ./carbon_toolchain-\${VERSION}/bin/carbon link --output=io_output io_output.o && ./io_output"

To provide standard input, pipe or redirect into the compiled program. For the character-counting example, printf supplies two lines of input:

1
2
# Compile count_input and pipe two lines of text into it
docker run --rm -v $(pwd):/app -w /app ubuntu:22.04 bash -c "apt-get update -qq && apt-get install -y -qq wget libgcc-11-dev > /dev/null 2>&1 && VERSION=0.0.0-0.nightly.2026.02.07 && wget -q https://github.com/carbon-language/carbon-lang/releases/download/v\${VERSION}/carbon_toolchain-\${VERSION}.tar.gz && tar -xzf carbon_toolchain-\${VERSION}.tar.gz && ./carbon_toolchain-\${VERSION}/bin/carbon compile --output=count_input.o count_input.carbon && ./carbon_toolchain-\${VERSION}/bin/carbon link --output=count_input count_input.o && printf 'Hello\nCarbon\n' | ./count_input"

For file-style I/O, redirect the data file onto standard input with <:

1
2
# Compile sum_digits and read numbers.txt from standard input
docker run --rm -v $(pwd):/app -w /app ubuntu:22.04 bash -c "apt-get update -qq && apt-get install -y -qq wget libgcc-11-dev > /dev/null 2>&1 && VERSION=0.0.0-0.nightly.2026.02.07 && wget -q https://github.com/carbon-language/carbon-lang/releases/download/v\${VERSION}/carbon_toolchain-\${VERSION}.tar.gz && tar -xzf carbon_toolchain-\${VERSION}.tar.gz && ./carbon_toolchain-\${VERSION}/bin/carbon compile --output=sum_digits.o sum_digits.carbon && ./carbon_toolchain-\${VERSION}/bin/carbon link --output=sum_digits sum_digits.o && ./sum_digits < numbers.txt"

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

  • Print adds a newline; PrintStr and PrintChar do not. Core.Print(x: i32) lowers to printf("%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, and Print in sequence.
  • Input is byte-oriented through ReadChar. Core.ReadChar() returns an i32 (a byte value) or Core.EOF() (-1), so end-of-input is an explicit comparison, not an exception.
  • Loop until Core.EOF(). The standard reading pattern primes with one ReadChar, processes inside a while (c != Core.EOF()) loop, and reads again at the bottom.
  • Work in i32 for reliability. Comparing against numeric ASCII codes (10 for newline, 4857 for digits) keeps arithmetic in i32 and 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.

Running Today

All examples can be run using Docker:

docker pull ubuntu:22.04
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining