Beginner

Operators in Carbon

Learn arithmetic, comparison, logical, and assignment operators in Carbon with practical Docker-ready examples

Operators are the building blocks of any expression-based language, and Carbon is no exception. As a successor to C++, Carbon inherits the familiar infix operator syntax that systems programmers expect — +, -, *, /, ==, <, &&, || — but tightens the rules around how these operators behave, especially around implicit conversions, integer overflow, and short-circuit evaluation.

Carbon is a multi-paradigm, statically-typed language with partial type inference. That means every operator has a precise type signature, and the compiler verifies operand types at the point of use. Unlike C++, Carbon refuses to silently mix signed and unsigned arithmetic, and it treats overflow as a defined error rather than undefined behavior. This combination produces predictable, debuggable expressions.

In this tutorial you will write a single Carbon program that exercises the major operator categories: arithmetic, comparison, logical, assignment, and bitwise. You will also see how Carbon’s expression-based if participates in operator-like patterns, and how the Core.PrintStr and Core.Print functions from the nightly toolchain let you visualize each result.

Arithmetic Operators

Carbon supports the standard arithmetic operators on its numeric types (i32, i64, f32, f64, and friends). The operators are infix and use the conventional precedence rules, where *, /, and % bind tighter than + and -.

Create a file named operators_arithmetic.carbon:

import Core library "io";

fn Run() {
  let a: i32 = 17;
  let b: i32 = 5;

  Core.PrintStr("a + b = ");
  Core.Print(a + b);
  Core.PrintStr("\n");

  Core.PrintStr("a - b = ");
  Core.Print(a - b);
  Core.PrintStr("\n");

  Core.PrintStr("a * b = ");
  Core.Print(a * b);
  Core.PrintStr("\n");

  Core.PrintStr("a / b = ");
  Core.Print(a / b);
  Core.PrintStr("\n");

  Core.PrintStr("a % b = ");
  Core.Print(a % b);
  Core.PrintStr("\n");
}

Notice that a / b produces 3, not 3.4. When both operands of / are integers, the division is integer division — the fractional component is discarded. To get a floating-point result, at least one operand must be a floating-point type.

Carbon’s division and modulo follow truncated-toward-zero semantics, matching C++ and Rust. Integer overflow is not undefined behavior in Carbon; the language is designed to make overflow a checked error, though the exact diagnostic behavior is still being finalized in the experimental toolchain.

Comparison and Logical Operators

Comparison operators (==, !=, <, >, <=, >=) produce values of type bool. Carbon’s logical operators are the keyword-style and, or, and not, which short-circuit just like C++’s &&, ||, and ! — the right-hand side of and is not evaluated if the left side is false, and the right-hand side of or is not evaluated if the left side is true.

Carbon does not implicitly convert numeric values to bool. You cannot write if (x) to mean “if x is non-zero” — you must write if (x != 0) explicitly. This is a deliberate departure from C++ that eliminates a common class of bugs.

Create a file named operators_logic.carbon:

import Core library "io";

fn Run() {
  let x: i32 = 10;
  let y: i32 = 20;

  Core.PrintStr("x == y: ");
  if (x == y) { Core.PrintStr("true\n"); } else { Core.PrintStr("false\n"); }

  Core.PrintStr("x != y: ");
  if (x != y) { Core.PrintStr("true\n"); } else { Core.PrintStr("false\n"); }

  Core.PrintStr("x < y: ");
  if (x < y) { Core.PrintStr("true\n"); } else { Core.PrintStr("false\n"); }

  Core.PrintStr("x >= 10 and y <= 20: ");
  if (x >= 10 and y <= 20) { Core.PrintStr("true\n"); } else { Core.PrintStr("false\n"); }

  Core.PrintStr("x > 100 or y == 20: ");
  if (x > 100 or y == 20) { Core.PrintStr("true\n"); } else { Core.PrintStr("false\n"); }

  Core.PrintStr("not (x == y): ");
  if (not (x == y)) { Core.PrintStr("true\n"); } else { Core.PrintStr("false\n"); }
}

Because Carbon’s bool type does not currently print directly through Core.Print, we use a small if/else to surface each boolean result as text. In production code you would compose these expressions inside conditionals or generic predicates instead.

Assignment and Compound Assignment

Carbon distinguishes between mutable and immutable bindings at declaration time. var introduces a mutable variable that can be reassigned; let introduces an immutable binding that can never be reassigned. Compound assignment operators (+=, -=, *=, /=, %=) only work on var bindings, because they mutate the underlying storage.

Create a file named operators_assign.carbon:

import Core library "io";

fn Run() {
  var counter: i32 = 0;

  counter += 10;
  Core.PrintStr("After += 10: ");
  Core.Print(counter);
  Core.PrintStr("\n");

  counter -= 3;
  Core.PrintStr("After -= 3:  ");
  Core.Print(counter);
  Core.PrintStr("\n");

  counter *= 4;
  Core.PrintStr("After *= 4:  ");
  Core.Print(counter);
  Core.PrintStr("\n");

  counter /= 2;
  Core.PrintStr("After /= 2:  ");
  Core.Print(counter);
  Core.PrintStr("\n");

  counter %= 7;
  Core.PrintStr("After %= 7:  ");
  Core.Print(counter);
  Core.PrintStr("\n");
}

Attempting let counter: i32 = 0; counter += 10; would be a compile error: Carbon’s checker proves that let bindings cannot mutate, and the compound assignment operator requires a mutable target.

Precedence and Grouping

Carbon’s operator precedence is intentionally less aggressive than C++’s. The Carbon designers found that the C precedence table is one of the most-memorized and least-internalized tables in programming, so Carbon requires explicit parentheses when mixing operators from different precedence “categories” (e.g., bitwise with arithmetic, or comparison with logical). Within a single category, precedence behaves as expected.

Create a file named operators_precedence.carbon:

import Core library "io";

fn Run() {
  // Standard precedence: * binds tighter than +
  let r1: i32 = 2 + 3 * 4;
  Core.PrintStr("2 + 3 * 4 = ");
  Core.Print(r1);
  Core.PrintStr("\n");

  // Parentheses change evaluation order
  let r2: i32 = (2 + 3) * 4;
  Core.PrintStr("(2 + 3) * 4 = ");
  Core.Print(r2);
  Core.PrintStr("\n");

  // Mixing comparison with arithmetic
  let total: i32 = 5 * 6 - 2;
  Core.PrintStr("5 * 6 - 2 == 28: ");
  if (total == 28) { Core.PrintStr("true\n"); } else { Core.PrintStr("false\n"); }
}

When in doubt, add parentheses. Carbon’s compiler will reject ambiguous mixed-precedence expressions rather than silently picking a default — a deliberate choice to keep code readable for the next person who reads it.

Running with Docker

Carbon’s nightly toolchain runs on Linux. Use the same Ubuntu container approach as the Hello World tutorial. Replace hello.carbon with the file you want to compile.

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

# Compile and run operators_arithmetic.carbon
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=ops.o operators_arithmetic.carbon && ./carbon_toolchain-\${VERSION}/bin/carbon link --output=ops ops.o && ./ops"

To run the other files in this tutorial, substitute the corresponding .carbon source name in the compile step. The toolchain download is cached for the lifetime of a single docker run, so you may want to combine multiple compile/link/execute steps into a single bash -c invocation when iterating locally.

Expected Output

Running operators_arithmetic.carbon produces:

a + b = 22
a - b = 12
a * b = 85
a / b = 3
a % b = 2

Running operators_logic.carbon produces:

x == y: false
x != y: true
x < y: true
x >= 10 and y <= 20: true
x > 100 or y == 20: true
not (x == y): true

Running operators_assign.carbon produces:

After += 10: 10
After -= 3:  7
After *= 4:  28
After /= 2:  14
After %= 7:  0

Running operators_precedence.carbon produces:

2 + 3 * 4 = 14
(2 + 3) * 4 = 20
5 * 6 - 2 == 28: true

Key Concepts

  • Familiar infix syntax — Carbon’s + - * / % and comparison operators read the same as C++, easing migration for systems programmers.
  • Integer division truncates — When both operands are integers, / discards the fractional part. Use a floating-point type for real division.
  • Keyword logical operators — Carbon uses and, or, and not rather than &&, ||, and !. They short-circuit just like the C++ symbols.
  • No implicit bool conversion — Numeric values do not auto-coerce to bool. You must compare explicitly, which eliminates a common class of C++ bugs.
  • var vs let — Compound assignment (+=, etc.) requires a var binding. Immutable let bindings cannot be the target of any assignment operator.
  • Defined overflow — Integer overflow is a checked error in Carbon, not undefined behavior. The exact diagnostic remains under design.
  • Stricter precedence rules — Carbon requires parentheses when mixing operators from different precedence categories, refusing to guess at programmer intent.
  • Experimental status — The operator syntax shown here reflects the nightly toolchain as of 2026.02.07; details may shift before the 0.1 release.

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