Skip to main content
CFDLAcademy
All chapters

Part 2 · The core language · Chapter 16 of 28

Diagnostics as a discipline

The core track can skip this chapter, though it is the deep dive most worth not skipping. You have been reading refusals since chapter 2. This chapter teaches you to read them the way an experienced practitioner does: as a structured message from a specific stage, carrying a specific promise, and pointing at a specific class of fix. The compiler's error output is not an obstacle course before the results appear. It is half the product: a model that moves money is validated by what it refuses as much as by what it computes.

Anatomy of a refusal

Every diagnostic carries the same parts:

  • A code, such as E2103.
  • A severity.
  • A message stating the rule.
  • The file and span of the offending text — line and column, start to end.
  • Where it helps, a hint naming the likely fix.

The command line prints them in that order: the severity and code with the message, then the file, line and column, then the hint when there is one:

error[E2103_SCHEDULE_OUT_OF_BOUNDS]: Stream 'debt.principal' schedule is outside model timeline (timeline: 2026-01-01 to 2026-12-01).
  --> fixtures/invalid/bad_schedule_out_of_bounds/model.cfdl:7:12

A hint follows on its own line as = hint:.

Two properties are designed, not accidental. Codes are stable. Before 1.0 a code may be renamed, widened or deleted, and a deleted code's number returns to the pool; from 1.0 a code is never reused or renamed (docs/08 §8). A team's internal notes ("we see E1201 whenever someone splits the debt file") stay true within a release, and tooling can key on codes.

Messages name the rule, not just the symptom. "Must be a dotted qualified name (e.g. cre.lease.rent)" teaches the grammar inside the error. Sometimes a message quotes your own model's vocabulary back at you: the schedule's dates, the phase's name. The compiler spends the resolution work it has already done to make the refusal about your model rather than about syntax.

The taxonomy is a map of the pipeline

Codes are banded by the stage that owns them. The bands are the previous chapter read back as an index:

  • E0xxx — lexical and parse. The text itself is malformed. Fix the writing. These errors never require understanding your model, only the grammar.
  • E1xxx — resolution. A name problem: duplicates, unresolved references, import cycles, paths outside the root. Fix the relationships: something is declared twice, or referenced but not declared, or declared where it cannot be seen.
  • E2xxx — validation. The model is well-formed and self-consistent but breaks a semantic rule: a schedule off the grid, a missing required clause, a malformed day rule. These errors teach the most, because each one states a modeling rule precisely.
  • Pack bands. A model using an industry pack gets that pack's validations — a lease missing its base rent, an occupancy outside [0,1] — in the pack's own vocabulary. Same anatomy, domain rules.
  • Evaluation-time refusals. What cannot be known until numbers exist surfaces from the engine with the same structure. The previous chapter explained why these cannot surface earlier. Much that looks evaluation-time is caught at compile: a prev read with no value in the first period is E1129 (docs/08 §7.3).

The band tells you which kind of thinking the fix needs, before you read the message. E0 says look at the text. E1 says look at the names. E2 says look at the claims. That pre-classification is the difference between opening the right file first and searching all of them.

Cascades, and trusting the first error

When one mistake produces several messages, the first message is the cause; treat the rest as consequences. The compiler works to suppress cascades — parse recovery resynchronizes at statement boundaries so one typo does not produce twenty phantom errors — but suppression is best-effort where refusal is guaranteed. The discipline: fix the first error, recompile, reread. Never fix three reported errors in one edit. The second and third were often shadows of the first, and a "fix" applied to a shadow is tomorrow's real error.

The five-minute method

The debugging discipline this course has built piecewise, assembled:

  1. On a compile error, read the code's band, then the span, then the hint. Fix one error, recompile. Done crisply, this step is most of debugging in this language: the structural errors that consume spreadsheet afternoons cannot survive to run time here.
  2. When the model compiles but a number is wrong, descend. Apply chapter 3's four steps: which streams, which periods, which settings, then the claim. The wrongness is always located, and the series tell you where.
  3. When the model compiles but a number is missing, check the existence gates. A guard that is never true, an event that never fired, a schedule whose window is empty, a waterfall step starved by seniority. Missing cash is almost always a condition, not an amount.
  4. When two runs disagree, diff the configurations, then the IRs. Deterministic evaluation means the disagreement has a cause in the inputs. The previous chapter gave you both diffs.
  5. When you and a colleague disagree, exchange the model_hash and the seed. If the hashes match and the numbers differ, the difference is configuration. The conversation is now about a small JSON file, not about trust. Two corollaries sharpen the exchange. The hash is taken over the model without its views, so a colleague who added a slice or a statement still shares your model_hash — different presentations are not different models, and the hash refuses to pretend they are. And the results carry a second fingerprint, ledger_hash, over what came out: the series, the journal, the transitions and the trace — one for the base run, one for each scenario and one for the trials. A differing ledger_hash on a matching model_hash therefore implicates the whole ledger, and compare names the first period and actor that decided differently before any cash moved. Better still, exchange a package: cfdl package writes a manifest of the model, the run configuration, its inputs files and the results with every checksum and hash, and your colleague's cfdl verify checks all of it without running anything.

Three habits have no place in the method. Do not rerun unchanged code hoping for different output; the engine is deterministic. Do not adjust magnitudes to "see what moves"; every claim is legible, so read the claim. Do not delete and retype; the span already names the offending text. These habits come from debugging nondeterministic systems. Here they are worse than wasted: they destroy the audit trail of deliberate claims that is the model's whole value.

Errors as curriculum

A closing reframe for readers who teach or lead teams. Because every refusal states a rule, the set of diagnostics is a syllabus of the language's semantics, and deliberately triggering them is the fastest fluency drill available. The exercises in this course that said "break it on purpose" were building exactly this. A practitioner who has seen E1201's cycle report, E2103's overrun span, and the first-period prev refusal (E1129) reads a real error as a familiar shape, not a novel emergency. Ten minutes of deliberate breakage per construct, once, repays itself on the first real incident.

On your own

  1. Collect five distinct diagnostic codes from models you break on purpose: one lexical, one resolution, one validation, one pack, one evaluation-time. For each, write the one-sentence rule it enforces without looking at the message again. That summary sheet is your team's onboarding document.
  2. Take a model you wrote in Part II and introduce a subtle wrongness that still compiles — a guard condition off by one period. Hand the model to a colleague with only the five-minute method, and time them. Then swap roles. The method is learned by being the person who finds the defect.
  3. The next time a model refuses to compile at the end of a long day, notice the impulse to fight the compiler. Then read the hint, which was written for exactly that moment.