Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

cmetal

A self-contained learning environment for advanced C, taught by fixing broken code.

C is taught everywhere, but almost always up to the point where a program compiles. The hard part of the language starts after that: undefined behavior, aliasing, the lifetime of memory, const discipline, error handling that survives a real call chain. Books explain these; almost nothing lets you practice them with immediate feedback. That is the gap cmetal fills — the way rustlings did for Rust, but for the parts of C that actually hurt.

Each exercise is a .c file with a real bug or a TODO. You open it, fix it, and save. cmetal recompiles, runs the binary — and the tests and sanitizers where the exercise calls for them — and tells you, in seconds, whether you got it right.

  Exercise: 02_memory/memory2

  ✗ AddressSanitizer: heap-use-after-free
      free(buf);
      return buf[0];   // <- read after free

  Press [h] for a hint.

Fix it, save, and the same screen turns green — and the official solution unlocks in my_solutions/ so you can compare it with yours.

The loop

  1. Run cmetal init, then cmetal from the workspace it creates (a git clone of the repository works the same way). Your working copies live in my_exercises/; the originals in exercises/ stay pristine.
  2. Open the exercise's .c file in your editor. Fix the bug.
  3. Save. cmetal recompiles and re-verifies automatically — watch mode is the core experience.
  4. Stuck? Press h for progressive hints.
  5. Green? The solution unlocks. On to the next.

What it is for

  • Deliberate practice on hard C. 62 exercises across 20 topics, from pointer decay to hash tables, garbage collection and bytecode, each built around a bug you will actually meet in production code. See the curriculum.
  • Real toolchains, not a sandbox. gcc and clang, AddressSanitizer and UBSan — the tools you use at work. Choosing a compiler.
  • A curriculum you can extend. Adding an exercise is writing a broken program, its fix, and a few hints; one script gatekeeps quality. See Contributing an exercise.

What it is not

The goal is not to teach every corner of C, but the C that appears in production systems. cmetal is not a C course from zero, not a C++ tutor, and not an IDE or build system. It assumes you know C syntax and want to get good at the parts that bite. It is a sharp tool for one job: deliberate practice on advanced C. Where that line moves next is tracked in the roadmap.

The CLI is written in Rust for a fast experience on Linux and macOS; the exercises are pure C11. (On Windows, use WSL: native toolchains lack the sanitizers the exercises rely on.)

Install

The cmetal binary carries the whole course: install it, run cmetal init, and you have a private workspace with every exercise — no git, no clone. (Contributors still work from the repository; see Contributing.) Pick whichever route gets you the binary.

Prerequisites

  • gcc and/or clang with C11 support — the exercises are compiled with your system toolchain. cmetal uses gcc by default; start it with --compiler clang to work with clang instead (see Choosing a compiler).
  • git and a Rust toolchain — only to contribute, or to install via Cargo / build from source; learning needs neither.

Option 1 — Homebrew (macOS, Linux)

No Rust required.

brew install cdelmonte-zg/tap/cmetal

Option 2 — Cargo (crates.io)

With a Rust toolchain installed:

cargo install cmetal

Option 3 — prebuilt binary

No Rust required. Download the archive for your platform from the latest release, then:

tar -xzf cmetal-<version>-<target>.tar.gz
sudo mv cmetal-<version>-<target>/cmetal /usr/local/bin/

macOS: the binaries are not code-signed yet. If Gatekeeper blocks the first run, clear the quarantine flag: xattr -d com.apple.quarantine $(which cmetal).

Option 4 — build from source

git clone https://github.com/cdelmonte-zg/cmetal.git
cd cmetal
cargo install --path .

Get the exercises

The binary embeds the full curriculum: however you installed it, the shortest path is a self-contained workspace —

cmetal init my-cmetal-course   # or just `cmetal init`
cd my-cmetal-course
cmetal

Cloning the repository works exactly the same way and remains the route for contributing exercises or following unreleased changes:

git clone https://github.com/cdelmonte-zg/cmetal.git
cd cmetal
cmetal

Either way, on first run cmetal copies the exercises into my_exercises/ — that is where you work. Head to the Quickstart.

Upgrade

New exercises ship with the binary. Upgrade it, matching how you installed it:

brew upgrade cmetal              # Homebrew
cargo install cmetal             # crates.io (picks up the newest release)
cargo install --path . --force   # built from source

If you installed a prebuilt binary, download the new archive from the latest release and replace /usr/local/bin/cmetal the same way you installed it.

Then bring your workspace up to date with the curriculum embedded in the new binary:

cd my-cmetal-course
cmetal update

update never overwrites work you have edited: untouched working copies are refreshed, edited ones are kept and reported — compare with cmetal diff <name> or take the new version with cmetal reset <name>. (In a git checkout, update with git pull instead.)

Uninstall

Remove the binary, matching how you installed it:

brew uninstall cmetal            # Homebrew
sudo rm /usr/local/bin/cmetal    # prebuilt binary
cargo uninstall cmetal           # crates.io or built from source

Everything else — your progress, my_exercises/, revealed solutions — lives inside your workspace (or clone). Delete that directory and no trace is left.

Quickstart

Five minutes, one exercise, start to green.

Start

Create a workspace (once) and run cmetal from inside it:

cmetal init my-cmetal-course
cd my-cmetal-course
cmetal

(A cloned repository works exactly the same way — just run cmetal from inside the clone.)

The first run copies the pristine exercises from exercises/ into my_exercises/ — that is where you work; your edits never touch the pristine copies — and drops you on the first unsolved exercise in watch mode.

Fix your first exercise

cmetal shows you the current exercise, its status, and where its file lives:

  Exercise: 00_intro/intro1
  File:     my_exercises/00_intro/intro1.c

  ✗ Compiled and ran, but the program signalled failure.

  Commands: [n]ext [p]rev [h]int [l]ist [r]e-run [q]uit

Open that .c file under my_exercises/ in your editor — not the one in exercises/, which stays untouched. Read the comment at the top: it tells you what the exercise teaches. Find the bug or the TODO, fix it, and save.

cmetal notices the save, recompiles, and re-verifies automatically. When it passes, the screen turns green and the official solution appears in my_solutions/ so you can compare approaches.

Stuck?

Press h. Hints are progressive — the first is the gentlest nudge, and each h reveals one more, up to the near-answer. Nothing is spoiled unless you ask for it. See Hints and solutions.

Move around

KeyAction
nNext exercise
pPrevious exercise
hShow the next hint
lList all exercises
rRe-run the current one
qQuit

Progress is saved to .cmetal-state.txt and persists across sessions — quit any time and cmetal picks up where you left off.

Coming back later

cmetal                    # resume where you left off
cmetal list               # see every exercise and your progress
cmetal solution pointers2 # re-open a solution you've already earned
cmetal reset              # wipe progress, restore pristine exercises

The full command surface is in The CLI.

Next

Watch mode: the core loop

Running cmetal with no arguments starts watch mode — the experience the tool is built around. It picks up at your first unsolved exercise and stays there, watching the file, until you solve it.

What it does

  1. It shows the current exercise: its name, the path to its .c file under my_exercises/, and its current status.
  2. It watches my_exercises/ for changes.
  3. Every time you save the exercise file, cmetal recompiles it, runs it, and re-verifies — no keypress needed. The screen updates in place with the new result (a compile error, a failed assertion, a sanitizer report, or a green pass).
  4. When the exercise passes, its solution unlocks in my_solutions/ and you move on with n.

The unit of feedback is a file save. You never leave your editor to "run" anything; saving is running.

Reading a result

A result is one of a few shapes:

  • Compile error — the compiler's own output, verbatim. The most common early result; the message is the lesson.
  • Runtime failure — the program compiled and ran, but signalled failure (a non-zero exit, or a failed test assertion when the exercise has tests).
  • Sanitizer report — for exercises with sanitizers enabled, AddressSanitizer or UBSan caught something at runtime (a use-after-free, an overflow, a leak). This is the whole point of the UB Lab exercises.
  • Pass — green. The solution unlocks.

What exactly is compiled and run — and with which flags — is spelled out in How verification works.

Keys

KeyAction
nMove to the next exercise
pMove to the previous exercise
hReveal the next hint (they accumulate)
lList every exercise with its status
rRe-run the current exercise now
qQuit (progress is saved)

r is useful when a result depends on something outside the file — you changed a compiler with --compiler, or you just want to force a fresh run.

Progress persists

Your position and which exercises you've solved are written to .cmetal-state.txt in your workspace. Quit whenever; the next cmetal resumes exactly where you were. To wipe it and start clean, use cmetal reset.

When you don't want the loop

Watch mode is for working through the curriculum. For one-off actions — running a single exercise, listing progress, re-opening a solution — the subcommands do the job without entering the loop.

Hints and solutions

cmetal is built so you can get exactly as much help as you want, and no more. Two mechanisms — progressive hints and earned solutions — sit on a spectrum from "gentle nudge" to "here is the answer", and you control where on it you land.

Progressive hints

Every exercise ships a ladder of hints, ordered from the gentlest nudge to almost the full answer. In watch mode, press h to reveal the first. Press h again for the next. Hints accumulate on screen — you never lose an earlier one — and you decide how far down the ladder to go.

From the CLI you can ask for a specific depth without entering watch mode:

cmetal hint pointers1              # first hint
cmetal hint pointers1 --level 2    # first two hints

The idea is deliberate: a good first hint reframes the problem ("sizeof on a pointer gives the pointer size, not the array size") rather than handing you the fix. Reach for the next one only when the current one hasn't unstuck you.

Earned solutions

When an exercise passes, its official solution unlocks in my_solutions/. This is the moment to compare: your fix against the reference, side by side. There is often more than one correct answer, and seeing a second one is where a lot of the learning happens.

Two design choices keep this honest:

  • Solutions unlock only after you solve the exercise. You can't peek your way past a problem — the file simply isn't there until you've earned it.
  • Solutions are stored obfuscated on disk (.c.enc files), so casually browsing the repository on GitHub or in your editor never spoils an answer. cmetal decodes the one you earned into my_solutions/ when you pass.

To re-open a solution you've already earned:

cmetal solution pointers2

Starting over

To redo a single exercise — pristine file back, marked pending again, everything else kept:

cmetal reset pointers1

For a clean slate — pristine exercises, no revealed solutions, progress reset — use it without a name:

cmetal reset

That restores my_exercises/ from the pristine exercises/ and clears .cmetal-state.txt. Your workspace (or checkout) is otherwise untouched.

Choosing a compiler

cmetal compiles the exercises with your real system toolchain — gcc or clang — not a bundled or emulated one. Which compiler you use is a genuine part of the lesson: the two disagree, on purpose, about some of these bugs.

Picking one

By default cmetal uses gcc. Pick the other at startup:

cmetal --compiler clang

The choice applies to every exercise for that session and is shown on the welcome screen and in the watch header, so you always know which compiler's diagnostics you're reading. The single-exercise subcommands take it too:

cmetal run bitwise2 --compiler clang
cmetal verify --compiler clang

Why the compiler matters

Advanced C is partly a conversation with a specific compiler. gcc and clang issue different warnings, phrase the same error differently, and — critically for a learning tool — some bugs are only diagnosed by one of them. An exercise whose whole point is a gcc-specific -W... warning would compile cleanly under clang and pass without teaching anything.

Compiler-restricted exercises

To avoid that hollow pass, an exercise can declare which compilers can actually detect its bug, via compilers = [...] in info.toml. When you run cmetal with a compiler that isn't in the list, that exercise is skipped rather than shown as solved — it appears as "requires gcc" (or clang) in cmetal list.

This keeps the invariant honest: an exercise only counts as passed on a compiler that could have failed it. The mechanism is described from the author's side in Anatomy of an exercise and How verification works.

Which should you use?

Either. If you're learning C for a codebase that standardises on one of them, match it. Otherwise, work through once with gcc and once with clang — reading how each describes the same class of bug is itself worth doing. Both must have C11 support (any reasonably recent version does).

The CLI, subcommand by subcommand

Running cmetal with no arguments starts watch mode. The subcommands below do one thing and exit — handy for scripting, for jumping to a specific exercise, or for checking progress without entering the loop.

Wherever a subcommand takes <name>, it can be omitted to mean the current exercise — run, hint, solution and diff all accept it.

cmetal init [dir]

Create a self-contained workspace from the curriculum embedded in the binary — no git clone needed. With no argument it creates ./cmetal-workspace. The target must be a new or empty directory: init refuses to touch a directory that already has content.

cmetal init my-cmetal-course
cd my-cmetal-course
cmetal

cmetal run <name>

Compile, run, and verify a single exercise, print the result, and exit.

cmetal run pointers1
cmetal run bitwise2 --compiler clang

Useful when you want to check one exercise without the watch loop taking over.

cmetal hint <name> [--level N]

Print the exercise's hints. With no --level, prints the first; --level N prints the first N. Hints go from gentlest to near-answer — see Hints and solutions.

cmetal hint pointers1
cmetal hint pointers1 --level 3

cmetal solution <name>

Reveal the official solution for an exercise you've already solved. Solutions are stored obfuscated and unlock only once earned, so this works only after the exercise passes.

cmetal solution pointers2

cmetal diff <name>

Show how your working copy differs from the pristine exercise, as a unified diff. Useful after cmetal update reports that an exercise changed upstream while you had edits.

cmetal diff pointers1

cmetal list

List every exercise with its status — solved, pending, or skipped (e.g. "requires gcc" for a compiler-restricted exercise under the wrong compiler). This is the map of where you are.

cmetal list

cmetal verify

Run the full verification pipeline across all exercises and report. This is the same check CI runs; use it to confirm a clean toolchain or after changing compilers.

cmetal verify
cmetal verify --compiler clang

cmetal update

Bring an init-created workspace up to date with the curriculum embedded in the binary (upgrade the binary first: that is how new exercises arrive). Your work is safe: working copies you edited are never overwritten — if their exercise changed upstream, update says so and points you at cmetal diff/cmetal reset — while copies you never touched are refreshed automatically. Interrupted updates are recovered on the next run. In a git checkout, use git pull instead.

cmetal update

cmetal reset [name]

With a name: restore that one exercise's working copy to the pristine version and mark it pending again — other progress is kept. Without: clear ALL progress and restore every pristine exercise into my_exercises/. Either way, reset asks for confirmation before discarding work you have edited (--force skips the prompt). Nothing outside the cmetal workspace is touched.

cmetal reset pointers1   # redo one exercise
cmetal reset             # start over completely

--compiler <gcc|clang>

A global flag, accepted by watch mode and the single-exercise subcommands, that selects the toolchain for the session. Defaults to gcc. See Choosing a compiler.

Interactive keys (watch mode)

Inside watch mode, these keys drive the loop:

KeyAction
nNext exercise
pPrevious exercise
hShow hint (additive)
lList all exercises
rRe-run current exercise
qQuit

Contributing to cmetal

Thanks for your interest in contributing!

Note: learners never edit exercises/ — cmetal copies it into the gitignored my_exercises/ workspace on first run. exercises/ must only ever contain the broken, unsolved versions.

Adding an exercise

  1. Create the exercise file in exercises/<topic_dir>/<name>.c
  2. Create the matching solution in solutions/<topic_dir>/<name>.c, then run python3 scripts/solutions_codec.py pack — solutions are stored obfuscated as .c.enc so learners aren't spoiled by accident (cmetal reveals them in my_solutions/ once an exercise passes). To edit existing solutions, run ... unpack first, edit, re-pack.
  3. Add an entry in info.toml with progressive hints

Naming

  • Topic directories: NN_topic (e.g. 01_pointers, 06_strings)
  • Exercise files: <topic>N.c (e.g. pointers1.c, pointers2.c)

Exercise structure

// Short description of what this exercise teaches.

#include <stdio.h>

// TODO: Fix/implement something
void function_with_bug(void) { ... }

#ifndef TEST
int main(void) {
    // Interactive demo that shows the bug
}
#else
#include "cmetal_test.h"

TEST(test_name) { ASSERT_EQ(...); }

int main(void) {
    RUN_TEST(test_name);
    TEST_REPORT();
}
#endif

info.toml entry

[[exercises]]
name = "pointers3"
dir = "01_pointers"
test = true          # compile with -DTEST and run tests
sanitizers = false   # compile with ASan/UBSan
# flags = ["-O2"]    # optional extra compiler flags for this exercise
# compilers = ["gcc"]  # optional: restrict to compilers where the bug is
                       # detectable (default: all). Restricted exercises are
                       # skipped when cmetal runs with another --compiler.
hints = [
    "First hint: the gentlest nudge",
    "Second hint: more specific",
    "Third hint: almost the answer",
]

Requirements for solutions

  • Must compile with -Wall -Wextra -Werror -pedantic -std=c11
  • Must pass with both gcc and clang
  • Must pass with -fsanitize=address,undefined when sanitizers are enabled
  • Stick to C11 standard -- no POSIX-specific features

Language vs platform

Exercises may rely on mainstream ABI facts — 8-bit bytes, 32-bit int, 8-byte double alignment — but must not present them as C guarantees. When an exercise depends on such an assumption, make it explicit in the code with a _Static_assert (see ub3, structs1, bitwise1, strings3) and phrase comments so that what the C standard guarantees and what the target ABI provides stay distinguishable.

Fallible allocations

If an exercise's contract includes allocation failure ("returns -1 and leaves the object untouched"), route its allocations through CMETAL_MALLOC / CMETAL_REALLOC from include/cmetal_alloc.h instead of calling malloc/realloc directly. In normal builds they are plain malloc/realloc; in TEST builds a test can arm the next allocation to fail with cmetal_fail_next_alloc() and assert the failure branch deterministically (see memory2, structs2, function_pointers2).

Stand-alone exercises

Exercises must stand alone: solving one never requires code or concepts built in another, and every exercise — including the implementation track's — must be useful to someone who will never build an interpreter (see the editorial rule in VISION.md).

The exercise invariant

Every exercise must fail verification as shipped, and every solution must pass it. An exercise that is already green teaches nothing; CI enforces this on every push and PR:

python3 scripts/check_exercises.py

This replicates the exact cmetal verification pipeline (base flags plus the per-exercise test, sanitizers and flags settings from info.toml). Run it whenever you add or change an exercise — and before pushing, so you don't accidentally publish exercises in their solved state. You can have git run it automatically on every push:

git config core.hooksPath scripts/hooks

Running tests locally

cargo test                # Rust unit + integration tests
cargo clippy -- -D warnings
python3 scripts/check_exercises.py   # C exercise/solution invariant

Code style

  • C: follow the existing style, 4-space indent
  • Rust: cargo fmt + cargo clippy

The curriculum

cmetal ships 62 exercises across 20 topics, ordered roughly from warm-up to the parts of C that bite in code review. Each is a real bug — the kind found in production C — never a fill-in-the-blanks template.

#TopicExercisesWhat you will learn
00Intro1Getting started, basic program structure
01Pointers2Decay, arithmetic, pointer-size pitfalls
02Memory3malloc/free, realloc, leaks, double-free
03Undefined Behavior4Signed overflow, sequence points, integer promotion, stack lifetimes
04Preprocessor1Stringify, token pasting, macro pitfalls
05UB Lab6Hands-on UB experiments with sanitizer feedback
06Strings3Safe concatenation, tokenizing, parsing
07Structs3Layout/padding, opaque types, linked lists
08Function Pointers3Callbacks, generic sort, dispatch tables
09Const Correctness3const parameters, pointer-to-const, immutable API
10Error Handling3Return codes, error propagation, error context
11Bitwise3Bit counting, packing/unpacking, bit tricks
12Encodings3Endianness, varints, bit packing — bytes on the wire
13Tagged Unions4Tag discipline, exhaustive dispatch, ownership, header-first polymorphism
14Hash Tables5FNV-1a, probing, tombstones, rehash on growth, interning
15Arenas3Bump allocation, alignment, chained growth, escape discipline
16Garbage Collection3Mark-sweep: reachability, cycles, sweep discipline, finalization
17NaN Boxing3IEEE-754 bit layout, legal punning, mask discipline, payload packing
18Bytecode Dispatch3Defensive stream decoding, refusable stacks, jump-table discipline
19Capstone3A binary format end to end: writing, validating, owning

How the topics build

The early topics (Pointers, Memory) establish the mental model that everything else depends on: what a pointer is, when an array decays into one, who owns a heap allocation and when it dies. Get these wrong and the rest of C is guesswork.

The middle topics turn that model against you. Undefined Behavior and the six-exercise UB Lab are where you deliberately trigger real UB — signed overflow, use-after-free, dangling pointers — and watch AddressSanitizer and UBSan catch it. The sanitizer report is the teaching material; the point isn't to avoid UB abstractly but to recognise what it looks like when a tool flags it.

The later foundations topics are about writing C other people can trust: Strings (the functions everyone gets wrong), Structs (layout, padding, opaque types), Function Pointers (callbacks and dispatch), Const Correctness (APIs that document their own immutability), Error Handling (codes that survive a real call chain), and Bitwise (the low-level idioms).

From topic 12 on, the implementation track applies all of it to the C found in interpreters, compilers, and binary formats: bytes on the wire (Encodings), data modelling under manual memory (Tagged Unions), Hash Tables built from scratch, Arenas as a lifetime strategy, a mark-sweep Garbage Collector with AddressSanitizer as the judge, the bit-level value representation of NaN Boxing, defensive Bytecode Dispatch, and a Capstone that serializes, validates and reloads a binary format end to end. The track follows the project-wide editorial rule stated in the roadmap: every exercise must be useful to someone who will never build an interpreter — an arena is a lifetime strategy, a varint decoder is any defensive parser — and no exercise requires code from an earlier chapter.

The UB Lab

The UB Lab (topic 05) is worth calling out. Most exercises ask you to fix a bug. The UB Lab asks you to observe one: you write the code that triggers a specific undefined behavior, run it under sanitizers, and read exactly how the tool reports it. Use-after-free across function boundaries, dangling stack pointers, integer promotion traps — the goal is fluency in what a sanitizer is telling you, which is a skill no book can hand you.

Seeing the list live

cmetal list shows every exercise with its status — solved, pending, or skipped because it requires a specific compiler. It is the authoritative, up-to-date view; this table is the map.

The curriculum is growing — where it's headed (concurrency, alignment and the machine model, long-lasting APIs, a C23 track) is in the roadmap.

How verification works

Every time you save an exercise — or run cmetal run, cmetal verify, or the CI check — the same pipeline decides pass or fail. Knowing the exact stages and flags demystifies the results you see.

The pipeline

For each exercise, in order:

  1. Compile and run the demo. The .c file is compiled with the base flags and executed. A compile error stops here; a non-zero exit is a failure.
  2. Compile and run the tests — only if the exercise sets test = true. The file is recompiled with -DTEST, which switches on the exercise's built-in test block, and run. A failed assertion is a failure.
  3. Compile and run under sanitizers — only if the exercise sets sanitizers = true. The file is recompiled with AddressSanitizer and UBSan and run again. Any sanitizer diagnostic is a failure.

An exercise passes only when every stage that applies to it passes.

The flags

Base flags (stage 1, and stage 2 with -DTEST added):

-Iinclude -Wall -Wextra -Werror -pedantic -std=c11 -g

-Werror matters: for the exercises, a warning is an error. Much of what you learn here is making the compiler stop complaining.

Sanitizer flags (stage 3):

-Iinclude -fsanitize=address,undefined -fno-sanitize-recover=all -g -std=c11

-fno-sanitize-recover=all makes the program abort on the first sanitizer finding rather than printing and continuing — so a caught UB is an unambiguous failure. Note the sanitizer stage drops -Werror: its job is to catch runtime behavior, not warnings.

Any per-exercise flags from info.toml (for example -O2) are appended to every stage — some bugs only surface under optimisation.

Per-exercise settings

Three fields in info.toml tune the pipeline for each exercise:

  • test — run the -DTEST stage.
  • sanitizers — run the AddressSanitizer/UBSan stage.
  • flags — extra compiler flags appended to every stage.
  • compilers — restrict the exercise to compilers that can actually detect its bug (see below).

The exercise invariant

The rule the whole project is built on: every exercise must fail verification as shipped, and every solution must pass it. An exercise that starts green teaches nothing. This is enforced by scripts/check_exercises.py, which replicates this exact pipeline against both the broken exercises/ and the reference solutions/, and runs in CI on every push and pull request.

Compiler restriction

Some bugs are only diagnosed by one compiler (a gcc-specific warning, say). Such an exercise declares compilers = ["gcc"]. Under a different compiler it is skipped, because its "must fail as shipped" half can't hold there — but note the solution must still pass under every compiler, so that half of the invariant is always checked. This is why a restricted exercise shows as "requires gcc" in cmetal list rather than passing vacuously. See Choosing a compiler.

Anatomy of an exercise

An exercise is three things that live together: a broken C file, its reference solution, and an entry in info.toml that ties them to the verification pipeline. This chapter is the reference for that shape; the step-by-step contributor walkthrough is in Contributing an exercise.

The three pieces

PieceLocationRole
Exerciseexercises/<NN_topic>/<name>.cThe broken code the learner fixes
Solutionsolutions/<NN_topic>/<name>.c.encThe reference fix, stored obfuscated
Metadatainfo.tomlHints and per-exercise pipeline settings

Learners never touch exercises/ — cmetal copies it into the gitignored my_exercises/ workspace on first run. exercises/ therefore only ever holds the broken, unsolved version.

The exercise file

A typical exercise carries an interactive demo and an optional test block behind #ifdef TEST, so the same file serves both the run stage and the -DTEST test stage:

// Short description of what this exercise teaches.

#include <stdio.h>

// TODO: fix or implement something
void function_with_bug(void) { /* ... */ }

#ifndef TEST
int main(void) {
    // Interactive demo that shows the bug
}
#else
#include "cmetal_test.h"

TEST(test_name) { ASSERT_EQ(/* ... */); }

int main(void) {
    RUN_TEST(test_name);
    TEST_REPORT();
}
#endif

The first comment line matters — it's what the learner reads to know what the exercise is about.

The info.toml entry

[[exercises]]
name = "pointers3"
dir = "01_pointers"
test = true            # run the -DTEST stage
sanitizers = false     # run the AddressSanitizer/UBSan stage
# flags = ["-O2"]      # extra compiler flags, appended to every stage
# compilers = ["gcc"]  # restrict to compilers that can detect the bug
hints = [
    "First hint: the gentlest nudge",
    "Second hint: more specific",
    "Third hint: almost the answer",
]
  • name / dir — locate the .c file at <dir>/<name>.c. Naming is NN_topic for directories and <topic>N.c for files (01_pointers/pointers2.c).
  • test, sanitizers, flags, compilers — feed straight into the verification pipeline.
  • hints — the progressive ladder, gentlest first. An exercise may use a single hint = "..." instead of a hints = [...] list.

Solution storage

Solutions are authored in plaintext under solutions/ but committed as .c.enc so browsing the repo never spoils an answer. Two scripts manage the round-trip:

python3 scripts/solutions_codec.py unpack   # decode .c.enc -> .c to edit
python3 scripts/solutions_codec.py pack     # re-encode .c -> .c.enc to commit

The plaintext .c files under solutions/ are gitignored; only the .c.enc form is tracked.

The invariant it all serves

Whatever the pieces say, one rule governs them: the exercise must fail verification as shipped, and the solution must pass it. cmetal verify and scripts/check_exercises.py both enforce it — the latter runs in CI. See How verification works.

Vision

cmetal wants to become the reference hands-on path from "I know C syntax" to "I write C I can defend in a code review" — increasingly through the problems found in language implementations and binary formats.

C is taught everywhere, but almost always up to the point where programs compile. The hard part of the language starts after that: undefined behavior, aliasing, lifetime of memory, const discipline, error handling that survives real call chains. Books explain these topics; almost nothing lets you practice them with immediate feedback. That is the gap cmetal exists to fill — the way rustlings did for Rust, but for the parts of C that actually hurt.

Where we are

Today cmetal ships 62 exercises across 20 topics, in two tiers. The foundations tier (topics 00–11) covers general advanced C, from the intro and the preprocessor through pointers, memory, undefined behavior and the UB Lab, strings, structs, function pointers, const, error handling, and bitwise. The implementation track (topics 12–19) applies it to the C of language implementations and binary formats: encodings, tagged unions, hash tables, arenas, a mark-sweep GC, NaN boxing, bytecode dispatch, and a capstone that serializes, validates and reloads a bytecode chunk — magic, version, constant pool, code stream.

Around the exercises: a watch-mode TUI with progressive hints, sanitizer-backed verification, solutions that unlock only after you solve the exercise, and a self-contained distribution. The binary embeds the curriculum, cmetal init materializes a private workspace with no git clone, cmetal update delivers new exercises without ever overwriting your work, and a single version tag publishes every install route: Homebrew, prebuilt tarballs, and crates.io (cargo install cmetal).

Design principles

These are the non-negotiables that every future change must respect:

  1. Learn by fixing, not by reading. Every exercise is broken code with a real bug — the same bugs found in production C — never a fill-in-the- blanks template.
  2. Every exercise fails as shipped; every solution passes. This invariant is enforced by CI (scripts/check_exercises.py). An exercise that starts green teaches nothing.
  3. Feedback in seconds. Save the file, see the result. Compile errors, test failures and sanitizer reports are the teaching material.
  4. No accidental spoilers. Solutions stay opaque until earned, then appear next to your own attempt for comparison.
  5. Pure C11, real toolchains. gcc and clang, ASan and UBSan — the tools you will use at work. No custom runtime, no framework lock-in.
  6. The learner's clone stays clean. Work happens in gitignored directories; the repository itself is never dirtied by learning.

Where we want to go

The direction — the C of language implementations

cmetal includes a focused implementation track around the C used in interpreters, compilers, and binary formats. That domain covers much of hard C — tagged unions, hash tables, arenas, garbage collectors, bytecode — in pure C11, entirely in userspace, where sanitizers give direct feedback. It also addresses a documented gap: the standard book on the subject advises readers who are not yet comfortable with C to work through an introductory book first and come back.

The general advanced-C curriculum stays as it is: it is the foundations tier the implementation track builds on.

One editorial rule keeps cmetal a C trainer rather than an interpreter tutorial in exercise form: every exercise must be useful to someone who will never build an interpreter. Endian-safe I/O belongs in any protocol; a defensive varint decoder is any parser; an arena is a lifetime strategy; a GC is a graph traversal over owned memory. Exercises stand alone — they never require the artifacts of previous chapters.

Near term — deepen the foundations

The first pass of the implementation track is complete — topics 12 through 19, from endianness to the bytecode capstone, all shipped. The near-term work is on the foundations tier it builds on:

  • Proper arcs for the thin topics. Intro and Preprocessor sit at one exercise each, Pointers at two, and several foundations topics at three. Each should grow into an arc of 3–5.
  • More UB Lab scenarios — use-after-free across functions, double-free, misaligned access.
  • "What the sanitizer is telling you" notes attached to each exercise's hints.

Mid term — the missing chapters of advanced C

  • Concurrency: C11 <threads.h>, atomics, data races caught by ThreadSanitizer.
  • The machine under the language: alignment, padding, restrict, volatile — what the optimizer assumes and why (endianness ✓ started with topic 12).
  • APIs that last: opaque handles, ownership conventions, ABI stability basics.
  • Difficulty ratings and optional topic tracks, so a learner can follow "systems track" or "embedded track" through the same exercise pool.

Long term

  • C23 track as compiler support matures (nullptr, constexpr, checked arithmetic).
  • Community exercise pipeline: contributing a new exercise should be a 30-minute task with the invariant checker as the only gatekeeper.
  • Localized hints — the code stays English, the teaching can speak your language.

Non-goals

  • Teaching C syntax from zero — start with any introductory course, then come here.
  • Teaching compiler theory — parsing algorithms and type systems have their own excellent books; cmetal teaches the C those books assume you already have.
  • C++ — a different language with different lessons.
  • Becoming an IDE, a build system, or a general-purpose C tutor. cmetal is a sharp tool for one job: deliberate practice on hard C.

How to help

Pick a topic above, write one broken program and its fix, and read CONTRIBUTING.md. If scripts/check_exercises.py is green, you have taught the next person something real.