ADR-0016: Error Management — Diagnostic Levels, Pipeline Interruption, and Exit Codes
Date: 2026-04-01 Status: Accepted
Context
The compiler uses a typed pipeline (ADR-0009) where each step fires in sequence,
threading a CompilationContext that accumulates Diagnostic entries. Two
categories of problem can arise during compilation:
- Syntax errors — the parse tree is broken. Continuing downstream steps would build a model from garbage and produce misleading secondary errors.
- Semantic errors — the model is structurally sound but violates domain rules (consistency, completeness). The model can still be exported; the errors are informational for the user.
Prior to this ADR:
- HaltAndCatchFire checkpoints were placed after both parsing and semantic
checking, aborting the pipeline on any accumulated error.
- Compiler.compile() returned void, so non-fatal errors accumulated silently and
the CLI always exited 0 on a successful run — even when errors had been diagnosed.
- A WARNING level existed in Diagnostic.Level but was never emitted by any
production step, creating an undefined level with no behavioral contract.
Decision
Two diagnostic levels: ERROR and FATAL
WARNING is removed. The two remaining levels have explicit, distinct contracts:
| Level | Pipeline effect | Exit code effect |
|---|---|---|
ERROR |
None — accumulates in CompilationContext |
Exit 1 at end of compilation |
FATAL |
Aborts pipeline at the next fire() boundary |
Exit 1 (via CompilationException) |
ERROR is the default level for diagnosed problems. It signals "something is wrong
and the user must see it", but allows the pipeline to continue and produce output.
FATAL means "continuing is meaningless or dangerous". It is promoted from ERROR
only by HaltAndCatchFire, and only in contexts where downstream steps cannot
function correctly on the resulting intermediate value.
HaltAndCatchFire is placed exclusively after parsing
HaltAndCatchFire converts all accumulated ERROR diagnostics into a single
FATAL, which causes the next Transformation.fire() call to throw
CompilationException and skip remaining steps.
This checkpoint is placed after parsing only:
FileSource → CharStreamProvider → Lexer → Parser → HaltAndCatchFire → ActionListProvider → ...
Rationale: a broken parse tree makes subsequent model-construction steps produce incorrect or misleading results. Aborting early prevents cascading phantom errors.
Semantic checkers (ConsistencyChecker, CompletenessChecker) run without a
following HaltAndCatchFire. Their errors accumulate and the pipeline continues to
export. A structurally broken model (invalid parse) is fundamentally different from a
semantically incomplete one (missing conclusion): the latter is still a valid Java
object that can be serialised and inspected.
compile() returns boolean hasErrors
Compiler.compile() is changed from void to boolean, returning true when at
least one ERROR or FATAL diagnostic was reported. This allows the CLI entry
points to inspect the outcome without access to CompilationContext.
ChainCompiler returns ctx.hasErrors() after the pipeline completes. If the
pipeline throws (e.g. due to a FATAL), the exception propagates naturally and the
return value is never reached — the CLI catch blocks handle that path.
CLI exit codes
| Situation | Exit code |
|---|---|
| Compilation succeeded, no errors | 0 |
| Compilation succeeded, errors diagnosed | 1 |
| Pipeline aborted (syntax errors or explicit FATAL) | 1 |
| Unexpected exception (system error) | 42 |
Exit code 42 is reserved for exceptions that escape all CompilationException and
UnsupportedOperationException handlers — i.e., bugs or environmental failures
outside the compiler's control.
Rationale
- Syntax vs. semantic distinction. Syntax errors invalidate the parse tree; no useful work can follow. Semantic errors are domain-level feedback; export is still meaningful and the user benefits from seeing the output alongside the diagnostics.
- Removing WARNING. A level with no behavioral contract is worse than no level: it creates ambiguity about whether a warning affects the exit code, whether it interrupts the pipeline, and whether it should be printed. With only two levels, the contract is unambiguous.
- boolean return over exception. Throwing a post-completion exception to signal "succeeded with errors" would cause the CLI to print a redundant error message on top of already-printed diagnostics. A boolean return keeps the separation clean.
Consequences
Diagnostic.Levelhas two values:ERRORandFATAL. Any future addition of a new level (e.g.,INFO) must define its pipeline and exit-code contract in a new ADR before being added.CompilationContext.warn()is removed. Steps that previously emitted warnings should either promote toerror()or omit the diagnostic entirely.- The pipeline placement section of ADR-0015 is superseded by this ADR (see note there).
- All CLI commands must check the boolean return of
compile()and maptruetoEXIT_JPIPE_ERROR.