An editor, cargo and gdb

Writing Rust is a loop of four steps: change a file, build it, read what the compiler says, change it again. The compiler is the most useful tool in this course, because it refuses more than most compilers and explains why, and the loop is short enough that the explanation arrives while the code is still in your head.

This lesson sets up that loop: rustc on one file, cargo check inside a project, then GDB for the bugs that survive the compiler, and finally the tools this container does not have, so nothing later in the course promises a command that is missing here.

Edit, build, read, fix

The program below adds a number to a string literal. rustc stops at the type, names the exact trait that is missing and points at the expression, and it does not write a binary: a failed build leaves the old executable in place, which is how a stale program gets run by mistake.

The fix is a number instead of a string, and the second program is the same code after one character of thought. This is the whole loop, and it is worth reading the message instead of skimming it: the first line has the error code, the arrow points at the position, and the note names the trait.

fn main() { let total = 1 + "2"; println!("{}", total); }

fn main() { let total: i32 = 1 + 2; println!("total: {total}"); }

rustc --edition 2024 broken.rs
rustc --edition 2024 fixed.rs
./fixed

error[E0277]: cannot add `&str` to `{integer}`
--> broken.rs:2:19
|
2 | let total = 1 + "2";
| ^ no implementation for `{integer} + &str`
|
= help: the trait `Add<&str>` is not implemented for `{integer}`
= help: the following other types implement trait `Add<Rhs>`:
`&f128` implements `Add<f128>`
`&f128` implements `Add`
`&f16` implements `Add<f16>`
`&f16` implements `Add`
`&f32` implements `Add<f32>`
`&f32` implements `Add`
`&f64` implements `Add<f64>`
`&f64` implements `Add`
and 56 others

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0277`.
total: 3

E0277 is the error for a trait that is not implemented for the types involved, and rustc --explain E0277 prints a page about it. When a build fails, the previous binary is still there, so run the new one by name after a successful build rather than trusting the path.

cargo check

Inside a project the question is not does it build but does it type-check, and cargo can answer that without producing a binary. cargo check runs the compiler front end only, which is faster than a build and enough while the code is still moving.

The project here is the one cargo new makes: a manifest and a source file. The warning below comes from the compiler, and cargo repeats the count at the end. A warning is not a failure, and the binary is still produced; the lesson on warnings and errors turns warnings into errors on purpose.

fn main() { let unused = 5; println!("checker"); }

cd checker
cargo check

warning: unused variable: `unused`
--> src/main.rs:2:9
|
2 | let unused = 5;
| ^^^^^^ help: if this is intentional, prefix it with an underscore: `_unused`
|
= note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default

warning: `checker` (bin "checker") generated 1 warning (run `cargo fix --bin "checker" -p checker` to apply 1 suggestion)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s

The path in the message is relative to the package, src/main.rs, not to the directory you ran the command from. An editor with rust-analyzer runs the same check in the background and underlines the result before you save, which is why the loop feels shorter there than in a terminal.

A breakpoint in gdb

A Rust function keeps its name in the debug information, but the name gdb sees is the qualified one: sum_to::sum_to, module first and function second. Breaking on the bare name fails with Function "sum_to" not defined, which is the first surprise of debugging Rust.

Build with -g to keep that information. The session below stops at the first line of the function, prints the parameter, steps twice and prints the accumulator before letting the program finish. set print address off keeps the output readable, and the process number at the end changes on every run.

fn sum_to(n: u32) -> u32 { let mut total = 0; for i in 0..n { total += i; } total } fn main() { println!("sum: {}", sum_to(5)); }

rustc --edition 2024 -g sum_to.rs
gdb -q -batch -iex 'set auto-load python-scripts off' -ex 'set disable-randomization off' -ex 'set print address off' -ex 'break sum_to::sum_to' -ex run -ex 'print n' -ex next -ex next -ex 'print total' -ex continue ./sum_to

Breakpoint 1: file sum_to.rs, line 2.

Breakpoint 1, sum_to::sum_to (n=5) at sum_to.rs:2
2 let mut total = 0;
$1 = 5
3 for i in 0..n {
4 total += i;
$2 = 0
sum: 10
[Inferior 1 (process 1494) exited normally]

The two -iex options silence the gdb features this container cannot use: the Python auto-load scripts the Rust debug section asks for, and address space randomisation, which needs a permission a container usually does not have. Neither option changes what the debugger shows about the program.

A panic, and where it came from

Some mistakes cannot be caught by the compiler: the index below is parsed from a string at run time, so the bounds check happens when the program runs. An out-of-bounds index panics, which prints a message to standard error, unwinds the stack and exits with status 101.

The number in brackets after thread 'main' is the thread id of that run, and it changes. The rest of the message is stable: the file and position, what went wrong, and the lengths involved. The backtrace is one environment variable away.

fn main() { let values = [1, 2, 3]; let index: usize = "5".parse().unwrap(); println!("{}", values[index]); }

rustc --edition 2024 -g panic.rs
./panic
echo "status: $?"


thread 'main' (1453) panicked at panic.rs:4:20:
index out of bounds: the len is 3 but the index is 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
status: 101

GDB catches the moment before the bounds check fires, which is more useful than the message when the index came from somewhere far away. The breakpoint is on the line, and print index shows the value that was about to be used.

gdb -q -batch -iex 'set auto-load python-scripts off' -ex 'set disable-randomization off' -ex 'set print address off' -ex 'break panic.rs:4' -ex run -ex 'print index' -ex bt ./panic

Breakpoint 1: file panic.rs, line 4.

Breakpoint 1, panic::main () at panic.rs:4
4 println!("{}", values[index]);
$1 = 5
#0 panic::main () at panic.rs:4

The lesson on panics comes back to this: a panic is for a bug, not for an error the caller can handle, and the difference is the subject of the lessons on Result.

What this toolchain does not have

rustc does not care about formatting. The program below is correct, compiles and prints the right answer, and it is unpleasant to read; formatting is a separate tool named rustfmt. The linter is another separate tool, clippy.

This container has neither, because they come from rustup or from extra distribution packages, and the course never assumes them. If you have rustup, rustup component add rustfmt clippy installs both, and cargo fmt and cargo clippy then work as the rest of the Rust world expects.

fn main(){let a=1;let b=2;println!("{}",a+b);}

rustc --edition 2024 unformatted.rs
./unformatted
rustfmt --version
cargo clippy --version

3
bash: line 1: rustfmt: command not found
status: 127
error: no such command: `clippy`

help: view all installed commands with `cargo --list`
help: find a package to install `clippy` with `cargo search cargo-clippy`
status: 101

The last two commands failed here, and the page says so instead of pretending. A course that shows a formatted file and a clean clippy run from a machine without those tools would be showing output it never had.

Exercises

  1. Change the index in panic.rs to 2, rebuild and run it. What does the program print now, and what is the exit status?
  2. Run gdb -q -batch -ex 'break sum_to' ./sum_to and read the message. Then use the qualified name and explain the difference.
  3. Put the body of unformatted.rs through the loop by hand, one statement per line with four spaces of indentation. Does the output change?

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does cargo check do?

    Answer

    It type-checks the crate without producing an executable

    check runs the compiler front end and skips code generation, so it is faster than build.

  2. Why does break sum_to fail in gdb while break sum_to::sum_to works?

    Answer

    The debug information stores the module-qualified name

    A Rust function is recorded with its module path, and the bare name matches nothing.

  3. What is a backtrace?

    Answer

    The list of the functions that called the current one

    A backtrace walks the call stack from the point where the program stopped.

  4. What does rustc -g add to the output?

    Answer

    Debug information with file, line and variable names

    Without -g a debugger sees the code but not the names and lines it needs to be useful.

  5. What does this program print? fn sum_to(n: u32) -> u32 { let mut total = 0; for i in 0..n { total += i; } total } fn main() { println!("sum: {}", sum_to(5)); }

    Answer

    sum: 10

    The range 0..5 stops before 5, so the sum is 0 + 1 + 2 + 3 + 4.

  6. What is the first line of the compiler output for this program? fn main() { let total = 1 + "2"; println!("{}", total); }

    Answer

    error[E0277]: cannot add &str to {integer}

    The Add trait is not implemented for an integer and a string slice, and the code names the trait error.

  7. What does this program print? fn main() { let mut total = 0; for n in 1..=4 { total += n; } println!("total: {total}"); }

    Answer

    total: 10

    The inclusive range adds 1, 2, 3 and 4.

  8. When a rustc build fails, what happens to the old binary in the directory?

    Answer

    It stays where it was, unchanged

    A failed build writes no new binary and does not remove the previous one, which is why the path can be stale.

  9. What does this program print? fn main() { let total: i32 = 1 + 2; println!("total: {total}"); }

    Answer

    total: 3

    Two integers are added, and the named placeholder prints the sum.

  10. What is the exit status of the program that panics on an out-of-bounds index?

    Answer

    101

    A panic that unwinds leaves status 101, which the shell prints with $?.

The code on this page was built with rustc 1.96.1, cargo 1.96.1 and GDB 16.3 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev