Diagnostics
Achronyme's diagnostic system: codes, severity, suggestions, and rustc-style rendering.
Overview
A unified diagnostic system spans the parser, IR lowering, ProveIR compiler, Circom frontend, and CLI. Diagnostics are first-class values — every frontend produces a Vec<Diagnostic> rather than throwing exceptions.
Crate: diagnostics (zero workspace dependencies). Used by every other crate. File: diagnostics/src/lib.rs.
Core types
pub struct Diagnostic {
pub severity: Severity,
pub message: String,
pub span: Option<SpanRange>,
pub labels: Vec<Label>,
pub suggestions: Vec<Suggestion>,
pub code: Option<String>,
}
pub enum Severity { Error, Warning, Note }
pub struct Label {
pub span: SpanRange,
pub message: String,
pub style: LabelStyle, // Primary | Secondary
}
pub struct Suggestion {
pub message: String,
pub replacement: String,
pub span: SpanRange,
pub applicability: Applicability, // MachineApplicable | MaybeIncorrect | HasPlaceholders
}
pub struct Span {
pub byte_start: usize,
pub byte_end: usize,
pub line_start: usize,
pub col_start: usize,
pub line_end: usize,
pub col_end: usize,
}
pub struct SpanRange { pub start: Span, pub end: Span }
Builder pattern: Diagnostic::error("…").with_code("E212").with_span(...).with_suggestion(...).
Error code ranges
| Range | Domain | Crate |
|---|---|---|
| E001–E099 | Parser (Achronyme) | achronyme-parser |
| E100–E102 | Parser (Circom) | circom |
| W101–W103 | Warnings (Circom) | circom |
| E200–E211 | Circom lowering | circom |
| E300–E306 | Circom constraint analysis | circom |
| E400–E499 | IR lowering / ProveIR | ir, ir-forge |
| E500–E599 | Constraint backend (R1CS / Plonkish) | zkc |
| W001–W002 | Achronyme warnings (unused, shadow) | ir, resolve |
Codes are stable across releases — a code that appears in user-facing output is documented and tested.
Selected diagnostics
E212 — function body not inlineable with runtime signal arguments
The Circom frontend hits this when a function body has signal-dependent control flow that can’t fold to CircuitExpr. Resolution: lift to Artik bytecode via circom::lowering::artik_lift. Currently the trigger for the witness-VM path.
W103 — DoubleSignalAssignment
A signal <--’d twice. Allowed (because of if-else branch coverage where each branch sets the signal exactly once on its path), but flagged. Was hard error E101 pre-beta.20.
E300 — UnderConstrained
A witness input flows to no assertion. Surfaced by the taint IR pass.
”Did you mean?” suggestions
The shared diagnostics crate carries Suggestion payloads in diagnostics/src/diagnostic.rs. Fuzzy “did you mean?” lookup lives in the producing crates, for example akronc/src/suggest.rs, ir-forge/src/suggest.rs, and circom/src/lowering/suggest.rs, where each caller compares the unresolved name against its own valid namespace.
Severity policy
- Error — compilation halts at the end of the current pass; downstream passes do not run.
- Warning — compilation continues. CLI exits non-zero only with
--strict. - Note — informational, paired with errors/warnings as supporting labels.
Renderer
File: diagnostics/src/render.rs.
pub struct DiagnosticRenderer<'a> { /* source, lines, color */ }
impl<'a> DiagnosticRenderer<'a> {
pub fn new(source: &'a str, color_mode: ColorMode) -> Self;
pub fn render(&self, diag: &Diagnostic) -> String;
pub fn render_to(&self, out: &mut String, diag: &Diagnostic);
}
pub enum ColorMode { Always, Never, Auto }
Output mimics rustc:
- File path + line/col anchor at the top
- Source code excerpt with carets pointing at the span
- Inline labels for primary + secondary spans
- Suggestion blocks with
+/-lines forMachineApplicablefixes
CLI integration
--error-format=human(default) — colored ANSI rendering--error-format=json— line-delimited JSON for editor integrations--error-format=short— single-line summaries--no-color— disables ANSI even in tty mode
The LSP (ach-lsp) consumes diagnostics via --error-format=json and surfaces them through LSP textDocument/publishDiagnostics.
Error recovery
The parser uses synchronization on ; and } boundaries to keep producing Stmt::Error / Expr::Error placeholders, so a single pass yields multiple diagnostics. See achronyme-parser/src/parser/core.rs.
Adding a new diagnostic
- Pick a code in the right range (or extend the range if introducing a new domain).
- Construct via the builder:
Diagnostic::error("circuit cannot use mut bindings") .with_code("E412") .with_suggestion(stmt.span, "let", "replace `mut` with `let`") - Emit through whatever the local error channel is (
Vec<Diagnostic>orResult<_, Diagnostic>). - Add a diagnostic-focused test in the producing crate’s test suite.
- If user-facing, add a row to the diagnostics index in
/docs/language/diagnostics/(achronyme-web).
Source files
| Component | File |
|---|---|
Diagnostic type | diagnostics/src/lib.rs |
Span / SpanRange | diagnostics/src/span.rs |
| Renderer | diagnostics/src/render.rs |
Suggestion payload | diagnostics/src/diagnostic.rs |
| Parser diagnostics | achronyme-parser/src/parser/core.rs |
| Circom diagnostics | circom/src/lib.rs, circom/src/analysis/, circom/src/lowering/error.rs |
| IR diagnostics | ir-core/src/error.rs, ir-forge/src/error.rs, ir/src/eval/error.rs |
See Pipeline Overview for where each diagnostic phase fires. See Extension Guide for how to register a new code in the matching pass.