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
- Run
cmetal init, thencmetalfrom the workspace it creates (a git clone of the repository works the same way). Your working copies live inmy_exercises/; the originals inexercises/stay pristine. - Open the exercise's
.cfile in your editor. Fix the bug. - Save. cmetal recompiles and re-verifies automatically — watch mode is the core experience.
- Stuck? Press
hfor progressive hints. - 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 clangto 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
| Key | Action |
|---|---|
n | Next exercise |
p | Previous exercise |
h | Show the next hint |
l | List all exercises |
r | Re-run the current one |
q | Quit |
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 — how save-driven verification actually works.
- The curriculum — the 62 exercises and what each one teaches.
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
- It shows the current exercise: its name, the path to its
.cfile undermy_exercises/, and its current status. - It watches
my_exercises/for changes. - 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).
- When the exercise passes, its solution unlocks in
my_solutions/and you move on withn.
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
| Key | Action |
|---|---|
n | Move to the next exercise |
p | Move to the previous exercise |
h | Reveal the next hint (they accumulate) |
l | List every exercise with its status |
r | Re-run the current exercise now |
q | Quit (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.encfiles), so casually browsing the repository on GitHub or in your editor never spoils an answer. cmetal decodes the one you earned intomy_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:
| Key | Action |
|---|---|
n | Next exercise |
p | Previous exercise |
h | Show hint (additive) |
l | List all exercises |
r | Re-run current exercise |
q | Quit |
Contributing to cmetal
Thanks for your interest in contributing!
Note: learners never edit
exercises/— cmetal copies it into the gitignoredmy_exercises/workspace on first run.exercises/must only ever contain the broken, unsolved versions.
Adding an exercise
- Create the exercise file in
exercises/<topic_dir>/<name>.c - Create the matching solution in
solutions/<topic_dir>/<name>.c, then runpython3 scripts/solutions_codec.py pack— solutions are stored obfuscated as.c.encso learners aren't spoiled by accident (cmetal reveals them inmy_solutions/once an exercise passes). To edit existing solutions, run... unpackfirst, edit, re-pack. - Add an entry in
info.tomlwith 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
gccandclang - Must pass with
-fsanitize=address,undefinedwhen 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.
| # | Topic | Exercises | What you will learn |
|---|---|---|---|
| 00 | Intro | 1 | Getting started, basic program structure |
| 01 | Pointers | 2 | Decay, arithmetic, pointer-size pitfalls |
| 02 | Memory | 3 | malloc/free, realloc, leaks, double-free |
| 03 | Undefined Behavior | 4 | Signed overflow, sequence points, integer promotion, stack lifetimes |
| 04 | Preprocessor | 1 | Stringify, token pasting, macro pitfalls |
| 05 | UB Lab | 6 | Hands-on UB experiments with sanitizer feedback |
| 06 | Strings | 3 | Safe concatenation, tokenizing, parsing |
| 07 | Structs | 3 | Layout/padding, opaque types, linked lists |
| 08 | Function Pointers | 3 | Callbacks, generic sort, dispatch tables |
| 09 | Const Correctness | 3 | const parameters, pointer-to-const, immutable API |
| 10 | Error Handling | 3 | Return codes, error propagation, error context |
| 11 | Bitwise | 3 | Bit counting, packing/unpacking, bit tricks |
| 12 | Encodings | 3 | Endianness, varints, bit packing — bytes on the wire |
| 13 | Tagged Unions | 4 | Tag discipline, exhaustive dispatch, ownership, header-first polymorphism |
| 14 | Hash Tables | 5 | FNV-1a, probing, tombstones, rehash on growth, interning |
| 15 | Arenas | 3 | Bump allocation, alignment, chained growth, escape discipline |
| 16 | Garbage Collection | 3 | Mark-sweep: reachability, cycles, sweep discipline, finalization |
| 17 | NaN Boxing | 3 | IEEE-754 bit layout, legal punning, mask discipline, payload packing |
| 18 | Bytecode Dispatch | 3 | Defensive stream decoding, refusable stacks, jump-table discipline |
| 19 | Capstone | 3 | A 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:
- Compile and run the demo. The
.cfile is compiled with the base flags and executed. A compile error stops here; a non-zero exit is a failure. - 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. - 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-DTESTstage.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
| Piece | Location | Role |
|---|---|---|
| Exercise | exercises/<NN_topic>/<name>.c | The broken code the learner fixes |
| Solution | solutions/<NN_topic>/<name>.c.enc | The reference fix, stored obfuscated |
| Metadata | info.toml | Hints 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.cfile at<dir>/<name>.c. Naming isNN_topicfor directories and<topic>N.cfor 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 singlehint = "..."instead of ahints = [...]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:
- 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.
- 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. - Feedback in seconds. Save the file, see the result. Compile errors, test failures and sanitizer reports are the teaching material.
- No accidental spoilers. Solutions stay opaque until earned, then appear next to your own attempt for comparison.
- Pure C11, real toolchains. gcc and clang, ASan and UBSan — the tools you will use at work. No custom runtime, no framework lock-in.
- 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.