Reading Unfamiliar Code
Do not begin at line one and hope
Section titled “Do not begin at line one and hope”Reading from line one can help with a short script, but function definitions and branches mean the order on the page may differ from the order of execution. Before getting caught in individual lines, work out where execution begins and which parts it can reach. On a small unfamiliar program, I’d start by locating:
- The entry point: top-level Python code or C
main. - External input: arguments, files, or standard input.
- Output and effects: printed text, written files, and exit codes.
- Functions called from the entry point.
- Conditions and loops that decide what happens.
- State that changes along those paths.
A small sketch can help you connect those pieces, if you want one. Names like parse, validate, and save suggest what a function is meant to do. Check its body and callers before relying on that suggestion.
Trace one value
Section titled “Trace one value”Follow one value through the program. Pick an input, such as a command-line argument or a line from a file, and trace it from where it enters to where it affects the output. Check each transformation:
| Step | Location | Value/type | Evidence |
|---|---|---|---|
| Entry | argument or file | text | input interface |
| Conversion | parser | integer or structured value | conversion call |
| Decision | condition | compared value | branch expression |
| Effect | output | result | print/write/return |
Look for assumptions as you go (a required length, a permitted range, a terminator, a successful allocation, an expected file format). Use those assumptions to choose boundary tests.
Test before editing
Section titled “Test before editing”Before changing the source, test your understanding against the program’s behavior. Start with a normal case, then choose inputs that could expose a mistaken assumption:
- a normal case
- an empty input
- values sitting exactly on each boundary
- values just past each boundary
- the wrong type entirely
- input that’s unexpectedly long
Predict the result before running each case. If it contradicts your prediction, check the input and build, then trace the decision where they differ. Resolve that disagreement before changing the program.
Temporary print statements can show intermediate values. Keep a clean copy of the source and add one observation point at a time so you can tell what each change reveals. The extra code can also affect timing or state, so consider whether it changed the behavior you’re investigating.
The GCC warning-options reference explains which assumptions the compiler can flag during a rebuild. Keep those diagnostics separate from the program behavior you actually observe.
Complete case study: map before running
Section titled “Complete case study: map before running”Create token-report.c, but do not compile it yet:
#include <ctype.h>#include <stdio.h>#include <string.h>
static unsigned int score(const char *text, size_t length) { unsigned int total = 0; for (size_t index = 0; index < length; index++) { unsigned char byte = (unsigned char)text[index]; if (isdigit(byte)) { total += (unsigned int)(byte - '0'); } } return total;}
static int accepted(const char *text) { size_t length = strlen(text); return length >= 4 && length <= 8 && score(text, length) == 10;}
int main(int argc, char **argv) { if (argc != 2) { fprintf(stderr, "usage: %s TOKEN\n", argv[0]); return 2; } unsigned int result = score(argv[1], strlen(argv[1])); printf("length=%zu score=%u accepted=%s\n", strlen(argv[1]), result, accepted(argv[1]) ? "yes" : "no"); return accepted(argv[1]) ? 0 : 1;}Structural inventory and call graph
Section titled “Structural inventory and call graph”The entry point is main, and the external input is one command-line string. The effects are a single output line, a usage diagnostic on standard error, and three possible exit statuses coming back to the shell. The major calls form this graph:
main├── strlen├── score ──► isdigit├── accepted│ ├── strlen│ └── score ──► isdigit└── printf / fprintfThe graph shows that some values are calculated more than once. That gives us work we could remove, but it doesn’t by itself show that the result is wrong.
Trace one candidate
Section titled “Trace one candidate”For a37z, predict this data flow:
| Step | Location | State | Source evidence |
|---|---|---|---|
| Entry | argv[1] |
"a37z" |
argument-count branch permits access |
| Length | strlen |
4 | four bytes before \0 |
| Loop 0 | score |
a, total 0 |
not a digit |
| Loop 1 | score |
3, total 3 |
contribution 3 |
| Loop 2 | score |
7, total 10 |
contribution 7 |
| Loop 3 | score |
z, total 10 |
not a digit |
| Decision | accepted |
true | length 4–8 and score 10 |
| Effect | main |
accepted=yes, status 0 |
output and return expressions |
This trace is a prediction based on the source. Running the program will test its expected output and status; observing the intermediate values would require additional checks.
Build a boundary matrix
Section titled “Build a boundary matrix”| Input | Length class | Score | Predicted status | Purpose |
|---|---|---|---|---|
| no argument | missing | — | 2 | input shape |
37 |
below 4 | 10 | 1 | lower boundary |
a37z |
exactly 4 | 10 | 0 | accepted boundary |
abcdefgh |
exactly 8 | 0 | 1 | correct length, wrong score |
1a2b3c4d |
exactly 8 | 10 | 0 | accepted upper boundary |
1a2b3c4de |
above 8 | 10 | 1 | just outside upper boundary |
Compile and test:
gcc -Wall -Wextra -Wpedantic -O0 -g -o token-report token-report.c./token-report a37zprintf 'status=%s\n' "$?"If the output contradicts the table, find the earliest differing state. Use the trace to explain the discrepancy before deciding what needs to change.
Modification exercise: reduce duplicate work
Section titled “Modification exercise: reduce duplicate work”Change accepted so it receives the already-calculated length and score, rather than calculating both of them over again. Check which calls are now unnecessary, then use the table above to check that output and status still match for those cases.
Then introduce exactly one comparison change, using length < 8 in place of length <= 8. Predict which of your existing tests exposes it, run that test first, and compare the result with your prediction.
Common failures and recovery
Section titled “Common failures and recovery”- You started with tiny syntax details: go back to the entry point, the external inputs, the effects, the major calls, and the state.
- You changed code before preserving a baseline: restore the clean copy, rebuild it, and run the matrix before making a single instrumenting or refactoring change.
- A result seems flatly impossible: confirm the executable was actually rebuilt, check the exact input, and trace it from the first transformation onward.
- A function name shaped your conclusion: cite the conditions or the runtime output establishing the behavior instead. A name alone doesn’t establish what the function does.
Check your result
Section titled “Check your result”Compare your boundary runs before and after changing length <= 8 to length < 8. An eight-character input that passes the score check should distinguish the builds. If it does not, follow the length and score decisions separately and check that you rebuilt.
What this lets us investigate
Section titled “What this lets us investigate”You’ve now got a repeatable source-level investigation process. The Code Investigation Lab asks you to apply it without being handed the command sequence first.
Check your understanding
Section titled “Check your understanding”Knowledge check
Question 1 of 2
Practice complete
You've checked the key ideas. Revisit any explanation you need, then continue when it makes sense.