Chapter 1 · Part I

A Text-Analysis CLI

A small tool that counts lines, words, and word frequency in a text file. Nothing about it is hard. That is exactly why it is the right place to start: with no algorithmic difficulty to hide behind, every discipline in this book has to show up on its own merits.

Call it nexwc — a distant cousin of Unix's wc, with word frequency added. Given a file, it reports how many lines and words it contains, and its most common words. A programmer could write a version of this in twenty minutes without thinking about contracts at all. The point of walking through it carefully is to notice what changes when you do think about them — the design gets stated before the code, boundaries gets real checks instead of an assumption, and testing is simplified because the library underneath is separated from the command line wrapped around it.

Problem

nexwc takes a file path and an optional flag:

nexwc <file>              -- line/word counts, top 5 words
nexwc <file> --top <n>     -- top n words instead of top 5

Everything about its behavior at the boundary has to be decided before any code: what happens when the file does not exist, what happens when --top is given a non-positive number or no number at all, and what the process's exit code means to whatever invoked it. A CLI's real interface isn't its function signatures. It's the shape of its arguments, the text of its errors, and the number it hands back to the shell. Decide that first, and the implementation underneath has very little room to be wrong in a way that matters.

The first function we write for this program is for handling abnormal conditions it may face while on duty:

function fail(message: String, code: Integer) do
  print("error: " + message)
  exit(code)
end

Every error path in nexwc goes through fail, and nowhere else. That is worth noticing before anything else in this chapter: the message and the exit are printed and enforced from exactly one place, not scattered through the program wherever an error might occur. fail is named once, does one thing, and every caller trusts it to actually stop execution rather than falling through — which is exactly the kind of promise a contract is good at stating, even here, where the promise is about control flow rather than a value.

Design as Contract

The counting logic has nothing to do with files, flags, or exit codes, so it does not live anywhere near them. Word_Stats is a small, pure class.

class Word_Stats
create
  make(text: String) do
    this.text := text
  end
feature
  text: String

  line_count(): Integer
  do
    if text = "" then
      result := 0
    else
      result := text.split("\n").length
    end
  ensure
    non_negative: result >= 0
  end

  word_count(): Integer
  do
    result := words().length
  ensure
    non_negative: result >= 0
  end
  ...
end

These postconditions look almost too obvious to write — of course a count is non-negative. That is the point of writing them anyway. A postcondition earns its keep the moment the implementation changes and stops being obviously correct; non_negative costs one line today and stands guard against every future edit to line_count's body, including ones nobody has thought of yet. The interesting contract in this file is top_words, the one method with real work to do:

-- The n most frequent words, most frequent first; ties broken
-- alphabetically so the output is deterministic.
top_words(n: Integer): Array[Count_Entry]
require
  positive: n > 0
do
  ...
ensure
  bounded: result.length <= n
end

require positive: n > 0 is the contract's half of a promise that the CLI layer has to keep: whatever calls this method has already made sure n is a positive count. That single line is why --top's validation belongs on the command-line side of this program rather than inside the library — the library states what it needs at its boundary and trusts the caller to have gotten there, rather than re-checking a condition its caller already checked. ensure bounded closes the other end: whatever the frequency table looks like, you never get back more entries than you asked for, even from a two-word input.

Build

Source

The complete, current code for this project is at examples/contracts_at_work/01_text_stats on GitHub.

Word splitting uses a compiled regular expression rather than hand-rolled character scanning — this is what a CLI tool's arithmetic actually looks like once the interesting part is a library method away:

intern text/Regex

words(): Array[String] do
  let rx: Regex := create Regex.compile_with_flags("[a-z']+", "i")
  result := rx.find_all(text)
end

Frequency counting builds a Map[String, Integer], converts it to an array of a small result type, and sorts by count with a case-insensitive alphabetical tiebreaker, using an inline comparison function passed straight to sort:

let sorted: Array[Count_Entry] := entries.sort(fn(a: Count_Entry, b: Count_Entry): Integer do
  if a.count /= b.count then
    result := b.count - a.count
  elseif a.word < b.word then
    result := -1
  elseif a.word > b.word then
    result := 1
  else
    result := 0
  end
end)

The tiebreak matters more than it looks like it should: without it, two words with equal frequency could print in either order depending on Map iteration order, and a test asserting an exact top-N list would be flaky through no fault of its own. Deterministic output is a design decision, not an accident, and it is cheaper to build in here than to chase down later as an intermittent test failure.

The CLI wrapper, nexwc.nex, is where argument parsing lives — and where the postcondition on parse_top_count mirrors top_words's precondition on the other side of the same promise:

function parse_top_count(args: Array[String]): Integer
do
  result := 5
  let i: Integer := 0
  from
    i := 0
  until
    i = args.length
  do
    if args.get(i) = "--top" then
      if i + 1 >= args.length then
        fail("--top requires a number", 2)
      end
      let n: Integer := args.get(i + 1).to_integer()
      if n <= 0 then
        fail("--top must be a positive integer, got " + args.get(i + 1), 2)
      end
      result := n
    end
    i := i + 1
  end
ensure
  positive: result > 0
end

Notice the shape of this: parse_top_count never returns without either a positive number or having already called fail, which does not return at all. The ensure positive: result > 0 is not defensive filler — it is a machine-checked statement that this function's two exit paths (a good number, or no return) are the only two that exist, which is exactly the guarantee top_words's precondition needs from its caller. This is "parse, don't validate" in miniature: args is parsed into a value that is positive by construction the moment it exists, so nothing downstream ever needs to ask again.

The one thing this project needed that the design above did not anticipate: a bare relative path such as nexwc.nex sample.txt resolves against the nex CLI launcher's own installation directory, not wherever the program was actually invoked from — the launcher changes into $NEX_HOME before starting the JVM. This is documented behavior in the language, not a bug, and the fix is a few lines that read the caller's real working directory back out of an environment variable the launcher sets beforehand:

function resolve_path(proc: Process, raw: String): String do
  if raw.starts_with("/") then
    result := raw
  else
    result := proc.getenv("NEX_USER_DIR") + "/" + raw
  end
end

Every project in this book that touches a file by a relative path — Chapters 2, 5, and 9 among them — uses this exact pattern.

Test

Word_Stats is pure and host-free, so its check suite is eight direct assertions with nothing to set up and nothing to tear down — construct it with a string, ask it questions, compare the answers:

let sample: String := "the quick brown fox\njumps over the lazy dog\nthe fox runs"
let stats: Word_Stats := create Word_Stats.make(sample)

c.check("line_count", "3", "" + stats.line_count())
c.check("word_count", "12", "" + stats.word_count())

let top2: Array[Count_Entry] := stats.top_words(2)
c.check("top_words[0]", "the: 3", top2.get(0).to_string())
c.check("top_words[1]", "fox: 2", top2.get(1).to_string())

nexwc.nex itself has no automated check suite, and that is a deliberate choice rather than an omission. Once the counting logic is fully covered as a library, what is left in the entry-point file is argument parsing, an existence check, and a print loop — thin enough that the cost of scripting a subprocess harness to assert on stdout and exit codes would outweigh what it catches. It was still verified directly, by hand, before being trusted: run against the sample file with no flag, with --top 3, against a missing file, and against a malformed --top value, checking in each case that the output and exit code matched what the original problem promised. In short, often the right way for testing a CLI tool built like this is: exhaustive, automated coverage on the pure library beneath it, and a short, deliberate manual pass on the thin host-facing shell that calls it.

Takeaways

Split a program's logic from the shell that reads arguments and prints output, and testing stops being a chore: the logic gets exhaustive, automated coverage with nothing to set up, and the thin shell left around it is small enough that a short manual pass covers it completely. A postcondition that looks too obvious to write — a count can't be negative — is still worth writing, because it costs one line today and stands guard against every future edit to that method's body, including the ones nobody has thought of yet. And "parse, don't validate" applies as much to a CLI's own arguments as to anything else: once parse_top_count hands back a value, nothing downstream ever needs to ask whether it's positive again.

Chapter 2 asks a harder version of the same question this project answered quietly: what does a contract owe a piece of state that has to survive being written to disk and read back?