Module 4 Exercises — The Modern C Subset

Back to the Course 2 syllabus. Read first: Module 4 lessons (MC 1–38 and Seacord 2–5, 9 remain available as optional deep-dives).

Work in the labs repo’s c/ tree — c/host/src/ex-1-N/ for anything that runs, c/mcu/src/ex-1-N/ for anything read as Cortex-M disassembly — and record everything in m4/notes.md. Everything here runs on the Mac: host-side experiments compile with clang under ASan/UBSan, and every STM32-flavored question is answered by cross-compiling with clang --target=thumbv7em-none-eabihf (or arm-none-eabi-gcc if installed) and reading the disassembly. The Jetson enters only Exercises 4.1 and 4.3, as the third ABI, over SSH. Predicted cells are filled in before compiling; observed cells at the machine.

Exercises

Exercise 4.1 — Same file, three compilers. Write one small source file that deliberately uses the Module 4 feature set: a // comment, a mid-block declaration, a for-scope counter, a designated initializer, a compound literal, a static inline helper, a variadic macro, _Static_assert, an anonymous union, and a _Generic selection. Keep it freestanding-safe (no <stdio.h>) so the same file compiles on every tier. Compile it under -std=c90, -std=c99, -std=c11, -std=c17, and -std=gnu17, each with the Module 0 warning set, on all three compilers: host clang, clang --target=thumbv7em-none-eabihf (plus arm-none-eabi-gcc if installed), and the Jetson’s gcc over SSH. Complete the acceptance table (✓ / warning / error) per compiler:

Feature c90 c99 c11 c17 gnu17
// comment
Mid-block declaration
Designated initializer
Compound literal
static inline
Variadic macro
_Static_assert
Anonymous union
_Generic

Then print (host and Jetson) or _Static_assert (Cortex-M) the values of __STDC_VERSION__, __STDC_HOSTED__, and whether __STDC_NO_THREADS__, __STDC_NO_ATOMICS__, and __STDC_NO_VLA__ are defined, per compiler and dialect. In notes.md: which mode the Course 3 labs-repo toolchain files actually pin, why gnu17 rather than strict c17 is the practical choice for code that includes CMSIS or Linux headers (MC 2), and which of the three compilers disagreed with the other two about anything.

Exercise 4.2 — Modernize a K&R-era module. Write (or resurrect) a small module in deliberately 1989-vintage style — a command parser with a state machine is a good subject: all declarations at the top, plain int/long/char everywhere, memset for clearing state, function-like macros for small helpers, positional struct initializers, a switch with a default: and unmarked fall-through, nested ifs for error cleanup, and several returns. Then port it to the Module 4 idiom: <stdint.h>/<stdbool.h> types, declarations at first use, {0} and designated initializers, static inline replacing the macros, _Static_assert guarding every layout or width assumption the old code made silently, switch on an enum that is -Wswitch-enum-clean with every intended fall-through marked, and a single-exit goto cleanup chain replacing the nested error handling. Build both under the full warning set plus -Wswitch-enum -Wimplicit-fallthrough, then diff the -O2 disassembly of both versions on host clang and on the Cortex-M cross-compile. Deliverable: the before/after source pair, the warning counts before and after, and a notes.md verdict on the codegen diff — the modernization should be free (identical or better code), and the note says where and why, including what the goto chain compiled to.

Exercise 4.3 — Widths and promotions across three ABIs. The same C integer expressions mean subtly different things on the Mac (LP64), the Jetson (LP64, unsigned char), and the STM32 (ILP32, unsigned char). First fill in the sizes table by prediction, then verify — on the host and the Jetson by running, on the cross side with _Static_assert(sizeof(...) == ...) probes that either compile or don’t:

Type Apple Silicon (LP64) Jetson aarch64 (LP64) Cortex-M4 (ILP32)
int
long
size_t
uintptr_t
long long
char signed?
alignof(max_align_t)

Then predict the value and type of a battery of promotion traps from MC 6–10 and Seacord 3 — uint8_t * uint8_t overflowing into int, uint16_t shifted left 20, int8_t right-shifted, unsigned wraparound, a signed/unsigned comparison, a 32×32→64 product with and without the cast-before-multiply, (char)0xFF > 0 on each ABI, and a narrowing uint32_t → uint8_t with and without the range check of lessons §4.4 — and verify on the host and the Jetson with <inttypes.h> PRI macros. Record every case where your prediction missed and which promotion or conversion rule explains it; record which cases -Wconversion and UBSan each reported.

Exercise 4.4 — The expression safari. Write a battery of one-line expressions covering lessons §5: an unsequenced modification (i = i++ + 1), a read-and-write in one expression (a[i] = i++), two side-effecting calls in one argument list, two volatile reads in one expression, each of the five precedence misreads in the §5.3 table, a ?: whose branches have different types, a sizeof whose operand has a side effect, and sizeof applied to an array parameter inside a function. For each, predict the diagnostic (-Wsequence-point, -Wparentheses, -Wconversion, none) and the observed result at -O0 and -O2, then verify on the host and, for the codegen cases, in the Cortex-M disassembly:

Expression Standard’s verdict (UB / unspecified / well-defined) Warning predicted Warning observed Result -O0 Result -O2
i = i++ + 1
a[i] = i++
f(g(), h())
x = REG_A + REG_B (both volatile)
a < b < c
x & MASK == MASK
x << 2 + 1
cond ? -1 : 1U
sizeof(i++)
sizeof arr / sizeof arr[0] on a parameter

Deliverable: the table, plus a notes.md paragraph on which rows changed behavior between -O0 and -O2 and why that is the definition of undefined behavior rather than a compiler bug (Module 7 picks this thread up).

Exercise 4.5 — Designated initializers and the dangling literal. Build a CMSIS-style configuration struct (model it on GPIO_InitTypeDef) and a sparse lookup table using [index] = designators. Verify in lldb that unnamed members and gaps really are zero-filled (MC 12, 14), and check whether padding bytes are — compare {0} against memset on a struct with a uint8_t followed by a uint32_t. Then demonstrate the MC 13 pitfall deliberately: a function that returns (or stores) a pointer to a block-scope compound literal, and a second function that consumes one correctly during the call. Predict what -Wall -Wextra catches at compile time and what only AddressSanitizer catches at runtime; verify both. Deliverable: the two-column “caught by warnings / caught by ASan” table with observed compiler output, and the padding observation.

Exercise 4.6 — Compile-time layout contracts. Invent a small “datasheet” for a fake peripheral: a register block with fixed offsets, one register with an anonymous-union bit view. Write the C struct overlay for it and pin the contract down with _Static_assert + offsetof for every register, a width assert for every field type, and _Alignas(32) on a DMA-style buffer whose alignment you then confirm via _Alignof, the linker map, and an lldb address check (MC 25–27). Break one assertion deliberately (reorder two members) and record the exact compile error — the point of the exercise is that the build fails, not the bench session. Cross-compile the same file to confirm the contract holds identically on the Cortex-M, and record whether the bit-field view’s layout matched your prediction on both compilers (it is implementation-defined, and the note says what that means for Module 8).

Exercise 4.7 — The preprocessor, seen and disciplined. Four parts. (a) Run clang -E on a translation unit that includes one CMSIS device header (from the Course 3 firmware tree) and count lines of your code vs. lines of vendor code in the .i output. (b) Write a configuration header that #errors on a missing or out-of-range CONFIG_SAMPLE_RATE_HZ, then misspell the macro in one #if and show that only -Wundef reports it. (c) Build the fault-table X-macro from lessons §13.5 — enum, name table, and a switch that dispatches on it — and add a _Static_assert that catches a table that drifted. (d) Write the classic bad_abs(x) macro and its static inline replacement, pass counter++ through both, and predict-then-observe the double evaluation; then wrap a two-statement macro without do { … } while (0) and show the if that breaks it. Deliverable: the four observations in notes.md, plus the one-paragraph rule for when a macro is allowed in the working subset.

Exercise 4.8 — A type-safe clamp and a restrict kernel. Part one: implement clamp_i16, clamp_i32, and clamp_f32 as static inline functions and dispatch them through one clamp(x, lo, hi) generic selection (MC 30). Pass an int16_t * 2 argument and record which association _Generic selected and why (integer promotion); pass an unhandled type and record the diagnostic. Part two: take a naive C dot product (or the Q15 kernel from Module 0 §1) and produce the four-way codegen comparison, with and without restrict-qualified pointers, at clang -O2 on the host and on the Cortex-M cross-compile (MC 18):

Host, no restrict Host, restrict M4, no restrict M4, restrict
Inner-loop instruction count
Vectorized?
Loads per iteration

Deliverable: the table plus the API-contract paragraph — when restrict is an honest promise the caller can actually keep, what happens when it lies (UB, not a wrong answer you can test for), and why the CMSIS-DSP kernels Course 3 calls use it where they do. Close notes.md with one paragraph on where _Generic earns its keep in firmware and where it’s over-engineering (MC 30’s caveat).