Skip to content
Courses

Reading Unfamiliar Code

Programming FoundationsLesson 8 of 890 minutesLessonBuilds on: C Memory LabLab next: Code Investigation Lab

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:

  1. The entry point: top-level Python code or C main.
  2. External input: arguments, files, or standard input.
  3. Output and effects: printed text, written files, and exit codes.
  4. Functions called from the entry point.
  5. Conditions and loops that decide what happens.
  6. 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.

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.

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.

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;
}

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 / fprintf

The 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.

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.

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:

Terminal window
gcc -Wall -Wextra -Wpedantic -O0 -g -o token-report token-report.c
./token-report a37z
printf '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.

  • 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.

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.

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.

Knowledge check

Question 1 of 2

To isolate the upper length boundary, which pair is most useful?