Skip to content
cccskills
FreePublished on GitHub

Diagnostics and Error Reporting

Comprehensive guide to the Chemical compiler's diagnostic system — how errors, warnings, and informational messages are collected, reported, and formatted.

The English summary is being prepared. Showing the original description.

chemicallang/chemical31 stars(GitHub 31 + cccskills 0)

Run score Not tested

See how to install

Files included (1)

  • SKILL.md42.5 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Diagnostics and Error Reporting

The Chemical compiler has a sophisticated diagnostics system that captures errors, warnings, and informational messages during all compilation phases. Diagnostics are collected as Diag values by Diagnoser (and the location-aware ASTDiagnoser) objects.

Architecture

Key Files

FilePurpose
core/diag/Diagnoser.hDiagnoser base class — error_count, diagnostics, has_errors(), print_diagnostics()
core/diag/Diagnostic.hDiag class, DiagTag, DiagRelatedInfo, TextReplacement
core/diag/Diagnostic.cppmake_diag/add_diag, print_diagnostics, color, to_string, Diag::format, Diag::ansi
core/diag/DiagSeverity.hDiagSeverity enum: Error, Warning, Information, Hint + color/to_string
core/diag/Position.hPosition { line, character } (0-based), is_ahead/is_behind/is_equal, representation()
core/diag/Range.hRange { start, end }, representation()
core/diag/Location.hLocation { Range range; std::string path; }
core/source/SourceLocation.hSourceLocation class wrapping an encoded uint64_t; ZERO_LOC
core/source/LocationManager.h/.cppEncodes/decodes source locations, owns file paths, packing/overflow logic
compiler/ASTDiagnoser.hASTDiagnoser — location-aware diagnoser extending Diagnoser (full helper set)
compiler/ASTDiagnoser.cppLocation decoding + dup_sym_error / unsatisfied_type_error / print_debug_location
compiler/ASTDiag.hSmall ASTDiag { std::string message; DiagSeverity severity; } struct (unused legacy helper)
parser/Parser.hBasicParser : public ASTDiagnoser — parser-local error()/warning()/info()/hint() at token position
compiler/ASTProcessorOptions.hignore_errors, stop_on_file_error, verbose — control error behavior
compiler/ASTProcessor.h/.cppParallel pass driver; per-task diagnosers, print_mutex, ConcurrentParsingState::has_errors
compiler/ASTCompiler.cppReports codegen diagnostics under Declare / Compile / ExtDeclare / ExtCompile tags
compiler/Codegen.hCodegen : public ASTDiagnoser — codegen-side diagnostics
compiler/CodegenOptions.hCodegen options that affect error behavior
compiler/SanitizerOptions.hSanitizer-related error handling
preprocess/2c/2cASTVisitor.hToCAstVisitor : ... public ASTDiagnoser — C-translation diagnostics (2cTranslation)
compiler/symres/SymbolResolver.cppSymbolResolver : public ASTDiagnoser — symres diagnostics (SymRes, SymRes:*)
server/diagnostics/DiagnosticUtils.cppadd_diagnostics — compiler Diag → lsp::Diagnostic
compiler/cbi/bindings/ASTDiagnoserCBI.h/.cppCBI binding exposing error to compiler plugins
compiler/cbi/bindings/ParserCBI.h/.cppCBI binding Parsererror_at (parser error at a token)
ast/base/ASTNode.hBase AST node — provides encoded_location() used for ranges

Data Model (core/diag/)

DiagSeverity

DiagSeverity is a plain enum class : int (core/diag/DiagSeverity.h:7-16): Error, Warning, Information, Hint (in that order). Spellings matter — Information, not Info; Hint, not Note. Helpers color(ostream&, DiagSeverity) and to_string(DiagSeverity) return "ERROR", "WARN", "INFO", "HINT".

DiagTag : uint8_t (core/diag/Diagnostic.h:13-18) is LSP-style metadata: Unnecessary = 1, Deprecated = 2. Diag::tags is std::optional<std::vector<DiagTag>>; it is serializable but no compiler pass sets it, and the LSP converter drops it.

Position / Range / Location

  • Position { unsigned int line, character; } (core/diag/Position.h:7-35). Both are 0-based. representation() returns "<line+1>:<character+1>". is_ahead/is_behind/is_equal compare line first, then character (core/diag/Diagnostic.cpp:55-67).
  • Range { Position start, end; } (core/diag/Range.h:7-19). representation() returns a single line:char when start == end, otherwise "line:char - line:char".
  • Location { Range range; std::string path; } (core/diag/Location.h:8-11) — a range plus its absolute document path. This is the fully decoded, human-facing form.

DiagRelatedInfo and TextReplacement (fix-its)

  • DiagRelatedInfo (core/diag/Diagnostic.h:20-26): Position location + std::string message, stored in Diag::relatedInformation for "previous declaration here"-style context. Not populated by any pass and not propagated to LSP — dup_sym_error emits two diagnostics instead.
  • TextReplacement { Range range; std::string new_text; } (Diagnostic.h:28-32): one edit that would fix a diagnostic, collected in Diag::fixits_ (:50-51, "Non-serialized set of fixits"). Defined but never populated, and the LSP converter drops it, so quick fixes are not wired up. See Quick Fix / Replacement Support.

Diag

Diag lives in core/diag/Diagnostic.h (not in ASTDiag.h) and carries a Range plus optional metadata (core/diag/Diagnostic.h:34-130):

class Diag {
public:
    Range range;                                  // start/end Position (line + character), 0-based

    std::optional<DiagSeverity> severity;         // omitted = client-interpreted
    std::optional<std::string> path_url;          // file path (or documentation URL)
    std::string message;                          // primary text

    std::vector<TextReplacement> fixits_;         // non-serialized fix-its
    std::optional<std::vector<DiagTag>> tags;     // Unnecessary / Deprecated
    std::optional<std::vector<DiagRelatedInfo>> relatedInformation;

    // Append to `message`:
    Diag& operator<<(const char*);
    Diag& operator<<(const chem::string_view&);
    Diag& operator<<(const std::string_view&);
    Diag& operator<<(std::string&);
    Diag& operator<<(char);

    void format(std::ostream& os, const chem::string_view& path, const chem::string_view& tag) const;
    std::ostream& ansi(std::ostream& os, const chem::string_view& path,
                       const chem::string_view& tag = "Diagnostic") const;

    static bool has_errors(std::vector<Diag>& diags);   // scans for DiagSeverity::Error
};

The operator<< overloads append to message only — they do not set severity, tags, or fix-its. Diag::has_errors(std::vector<Diag>&) (core/diag/Diagnostic.h:121-128) is a static scan used when you have a raw vector rather than a Diagnoser.

The Diagnoser Base Class

Diagnoser (core/diag/Diagnoser.h:12-99) is the lowest-level collector. It has no LocationManager and works in terms of Position/Range/explicit file paths. Everything else (ASTDiagnoser, SymbolResolver, Codegen, ToCAstVisitor, BasicParser) derives from it.

class Diagnoser {
public:
    unsigned int error_count = 0;              // incremented on each Error-severity diag
    std::vector<Diag> diagnostics;             // insertion order, never sorted

    static Diag make_diag(const chem::string_view& message, const chem::string_view& file_path,
                          const Position& start, const Position& end, DiagSeverity severity);
    void add_diag(Diag diag);                  // increments error_count if severity == Error
    bool has_errors();                         // error_count > 0
    Diag& empty_diagnostic(const chem::string_view& file_path, const Position& start,
                           const Position& end, DiagSeverity severity);   // append to this
    void diagnostic(const chem::string_view& message, const chem::string_view& file_path,
                    const Position& start, const Position& end, DiagSeverity severity);
    void reset_diagnostics();                  // error_count = 0; diagnostics.clear()

    static void print_diagnostics(std::vector<Diag>& diagnostics, const chem::string_view& path,
                                  const chem::string_view& tag);
    static void print_diagnostics(std::vector<Diag>& diagnostics, const chem::string_view& tag);
    void print_diagnostics(const chem::string_view& path, const chem::string_view& tag);
    void print_diagnostics(const chem::string_view& tag);
};

Key behaviors, verified in core/diag/Diagnostic.cpp:

  • make_diag (Diagnostic.cpp:10-20) builds a Diag with {range, severity, filePath.str(), message.str()}.
  • add_diag (Diagnostic.cpp:22-27) increments error_count only when diag.severity == DiagSeverity::Error; a missing optional does not count.
  • empty_diagnostic (Diagnoser.h:47-53) also increments error_count for Error and returns a reference to the just-appended vector element so callers can << into it.
  • There is no max_severity field, no has_warnings(), and no error_count() method — error_count is a public field.
  • No sorting / dedup. print_diagnostics emits diagnostics in insertion order.
  • reset_diagnostics() clears both the vector and the count. On ASTDiagnoser this is surfaced as reset_errors() (compiler/ASTDiagnoser.h:237-239); the serial SymbolResolver uses reset_errors() after each file/pass.
  • [[deprecated]] void diagnostic(message, severity) (Diagnoser.h:65-66) — the two-argument form without a location; implemented in Diagnostic.cpp:29-31 with empty path and Position{0,0}.

ASTDiagnoser

The ASTDiagnoser class is the primary location-aware collector. It extends the plain Diagnoser (which owns the diagnostics and error count) and adds SourceLocation decoding via a LocationManager:

class ASTDiagnoser : public Diagnoser {
public:
    LocationManager& loc_man;                    // decodes SourceLocation → path + Position range
    ASTDiagnoser(LocationManager& loc_man);

    void location_diagnostic(const chem::string_view& message, SourceLocation loc, DiagSeverity severity);
    Diag& empty_diagnostic(SourceLocation loc, DiagSeverity severity);  // empty Diag to append to
    Diag& error(SourceLocation); Diag& warning(SourceLocation); Diag& warn(SourceLocation); // warn == warning
    Diag& info(SourceLocation);  Diag& hint(SourceLocation);

    // message overloads: error(msg, loc), warn(msg, loc), info(msg, loc);
    // templated node overloads use node->encoded_location().
    void dup_sym_error(const chem::string_view& name, ASTNode* previous, ASTNode* new_node);
    void unsatisfied_type_error(Value* value, BaseType* type);
    // inherited: unsigned int error_count; std::vector<Diag> diagnostics;
    //            bool has_errors(); reset_diagnostics() (exposed as reset_errors())
};

There is no note() method, no suggest(), no undefined_symbol(), no max_severity field, no has_warnings(), and no error_count() method — error_count is a public field on Diagnoser. Diagnostics with Hint severity are used for debug locations.

Full ASTDiagnoser convenience API

All declared in compiler/ASTDiagnoser.h. The SourceLocation/node forms return a Diag& you append to; the (message, ...) forms add immediately.

  • Empty-diag appenders (return Diag&): error(SourceLocation) :81, warning/warn(SourceLocation) :65,73, info(SourceLocation) :57, hint(SourceLocation) :89, error<NodeT>(NodeT*) :116, warn<NodeT>(NodeT*) :106, info<NodeT>(NodeT*) :96.
  • Message forms (return void): error/warn/info(message, SourceLocation) :140,136,132; error/warn/info<NodeT>(message, NodeT*) :165,158,151; diagnostic(message, SourceLocation, DiagSeverity) :43.
  • Two-location / two-node forms emit two diagnostics with the same message (:172-215) — the compiler's stand-in for LSP relatedInformation.
  • Generic plumbing: location_diagnostic(msg, SourceLocation, DiagSeverity) :38, empty_diagnostic(SourceLocation, DiagSeverity) :51, diagnostic<NodeT>(msg, NodeT*, DiagSeverity) :144.
  • Special helpers: print_debug_location(SourceLocation) :126 (prints a Hint DEBUG_LOC line), dup_sym_error(name, previous, new_node) :220, unsatisfied_type_error(Value*, BaseType*) :232, reset_errors() :237.

The node overloads are SFINAE-constrained with requires requires(NodeT n) { n.encoded_location(); }, and the two-node overloads require it on both.

How locations and ranges are computed

  • location_diagnostic (ASTDiagnoser.cpp:10-14) calls loc_man.getLocationPos(location) → {fileId, start, end}, then loc_man.getPathForFileId(pos.fileId), then Diagnoser::diagnostic(message, filePath, start, end, severity). empty_diagnostic (:16-20) does the same but returns the empty Diag&. AST nodes expose an encoded range via ASTNode::encoded_location() (produced by the parser from tokens), so a diagnostic's Range is the node/token span.
  • print_debug_location (:22-25) builds a Hint diagnostic and calls Diag::format(std::cout, ..., "DEBUG_LOC") using diag.path_url.value() — it prints, not collects.
  • dup_sym_error (:27-30): error(new_node) << "duplicate symbol being declared, symbol '…' already exists" + warn(previous) << "symbol has a conflict".
  • unsatisfied_type_error (:32-39): "value with type 'X' does not satisfy type 'Y'", or "value does not satisfy type 'Y'" when untyped. (The TypeVerify.cpp:234 free function unsatisfied_type_err is what the type checker actually uses; this member is a second copy.)

Parser ranges and parser-local diagnostic helpers

BasicParser : public ASTDiagnoser (parser/Parser.h:50) carries file_id + the current token. loc(start, end) (Parser.cpp:39-41) calls addLocation(file_id, start.line, start.character, end.line, end.character); loc_single(pos, length) (:53-55) uses pos.line for both ends with pos.character + length. loc(token) spans token->position .. token->position + token->value.size(), and get_file_path() (Parser.cpp:106+) resolves file_id through the LocationManager. On top of that, BasicParser adds a token-position layer over ASTDiagnoser (parser/Parser.h:249-349): make_diag, empty_diagnostic(start[, end], severity), diagnostic(severity), the empty appenders error()/warning()/info()/hint(), the message forms error(msg)/warning(msg)/info(msg)/hint(msg), unexpected_error(msg) (appends ", got '<token>'"), error(msg, Position), and error(SourceLocation) (forwards to ASTDiagnoser). That is why parser code reads parser.error("expected a ')' after …") or parser.error() << "expected … " << id (parser/statements/Typealias.cpp:15-81, parser/values/StructValue.cpp:13-21, parser/structures/Struct.cpp:251), and why Parsererror_at (compiler/cbi/bindings/ParserCBI.cpp:43-45) forwards to parser->error(*view, token->position).

ASTDiag.h

ASTDiag.h only declares a small value type — there are no predefined message helpers here:

// compiler/ASTDiag.h
struct ASTDiag {
    std::string message;
    DiagSeverity severity;
};

It is a leftover/legacy helper: no production code constructs an ASTDiag (grep finds references only in the header). All diagnostics are created through ASTDiagnoser (error(...), warn(...), info(...), hint(...)) or the Diagnoser base API.

Severity Levels

Error       → increments Diagnoser::error_count; compilation should stop
Warning     → printed, does not fail compilation
Information → informational message
Hint        → additional context (also used by print_debug_location)

The enum is DiagSeverity, so the spellings are DiagSeverity::Error, Warning, Information (not Info), and Hint (not Note).

Source Location System

SourceLocation

SourceLocation (core/source/SourceLocation.h) is a thin wrapper around a 64-bit encoded value:

class SourceLocation {
public:
    uint64_t encoded;

    bool isValid() const;    // encoded != 0
    bool isInvalid() const;  // encoded == 0
};

inline constexpr uint64_t ZERO_LOC = 0;   // fileId 0 is reserved as "unknown / invalid"

⚠️ ZERO_LOC vs encodeFile. The comment claims fileId 0 is invalid, but encodeFile (LocationManager.cpp:6-16) gives id 0 to the first file. A valid location whose fields are all 0 therefore encodes to exactly ZERO_LOC; do not rely on isValid() to mean "unknown".

Encoded Location Layout

LocationManager::addLocation(fileId, lineStart, charStart, lineEnd, charEnd) packs the range into 63 bits; bit 63 is the overflow indicator (when set, the low 63 bits index LocationManager::locations). The bit widths are declared on LocationManager (FILE_ID_BITS, LINE_START_BITS, CHAR_START_BITS, LINE_END_OFFSET_BITS, CHAR_END_BITS):

Bits 53-62: file id            (10 bits)
Bits 35-52: line start         (18 bits)
Bits 23-34: char start         (12 bits)
Bits 12-22: line-end offset    (11 bits, stored as lineEnd - lineStart)
Bits  0-11: char end           (12 bits)
Bit  63:    overflow indicator — when set, the low 63 bits index LocationManager::locations

The shifts are derived, not hand-written (LocationManager.h:78-80): FILE_ID_SHIFT_BITS = 18+12+11+12 = 53, LINE_START_SHIFT_BITS = 35, CHAR_START_SHIFT_BITS = 23. Storing the line end as an offset is what keeps the range in 63 bits: lineEnd is decoded as lineStart + offset (LocationManager.cpp:107). The encoding is an in-memory uint64_t and is never written to disk or across processes, so there are no explicit endianness concerns; shifts/masks are host-standard. There is also no explicit static_assert that the bit widths sum to ≤ 63 — they do, by hand-keeping.

Decode with getLocation(SourceLocation) (returns LocationData { fileId, lineStart, charStart, lineEnd, charEnd }) or getLocationPos(SourceLocation) (returns { fileId, Position start, Position end }). getLineStartFast() skips the full decode when only the line is needed.

⚠️ addLocation filter bug. The range check (LocationManager.cpp:49-55) uses (lineEnd - lineEnd) <= MAX_LINE_END_OFFSET, always true, so it never rejects an oversized span. The #ifdef DEBUG guard (:57-59) does throw, but release builds silently truncate. Fix the predicate to (lineEnd - lineStart) <= MAX_LINE_END_OFFSET.

LocationManager

class LocationManager {
    std::vector<LocationData> locations;              // overflow table for ranges too large to pack
    tsl::ordered_map<std::string, bool> file_paths;   // file id → path (insertion order)
    std::mutex file_mutex;                            // guards encodeFile/encodeExistingFile
    std::mutex location_mutex;                        // guards the overflow table only

    unsigned int encodeFile(const std::string& filePath);      // new or existing id
    int encodeExistingFile(const std::string& filePath);       // id, or -1 if never encoded
    std::string_view getPathForFileId(unsigned int fileId);    // path for id

    uint64_t addLocation(uint32_t fileId, uint32_t lineStart, uint32_t charStart,
                         uint32_t lineEnd, uint32_t charEnd);
    LocationData getLocation(uint64_t data) const;
    LocationData getLocation(SourceLocation loc) const;
    LocationPosData getLocationPos(SourceLocation loc) const;
    unsigned int getFileId(SourceLocation loc) const;

    uint32_t getLineStartFast(SourceLocation data);
    std::string formatLocation(SourceLocation location);
};

Ownership. A single LocationManager is owned by the enclosing build/processor and referenced by every diagnoser/parser in that scope:

  • ASTProcessor holds LocationManager& loc_man (compiler/ASTProcessor.h:91) and passes it to every ASTDiagnoser and SymbolResolver.
  • GlobalInterpretScope/InterpretScope carry loc_man (ast/base/GlobalInterpretScope.cpp:18).
  • The LSP server owns one session-wide LocationManager loc_man (server/WorkspaceManager.h), shared by all files, and every server analyzer decodes through it.
  • encodeFile deduplicates: encoding the same path twice returns the same id (LocationManager.cpp:6-16). getPathForFileId(id) is O(1) random access by iterator arithmetic on the tsl::ordered_map — valid only while the map is alive and the id was actually issued.

Multi-file positions. A SourceLocation encodes the file id, so a single ASTDiagnoser/Diag can point at any file in the build. During a pass, location_diagnostic resolves the correct path per-diagnostic, so one diagnoser can emit errors for several files (e.g. duplicate symbols across modules). Related cross-file ranges are what LSP analyzers resolve via loc_man.getLocationPos + getPathForFileId.

Encode/decode round-trip:

uint64_t loc = loc_man.addLocation(fileId, lineStart, charStart, lineEnd, charEnd);
SourceLocation sloc(loc);
auto data = loc_man.getLocation(sloc);        // LocationData
auto pos  = loc_man.getLocationPos(sloc);     // LocationPosData { fileId, Position start, Position end }

formatLocation (LocationManager.cpp:28-41) is a debug/console helper used by LabBuildCompiler/ASTProcessor error paths; its output is <path><line>:<char>: — 0-based, with no separator between path and line and a trailing colon. It is not used for user-facing output (Diag::ansi is).

Diagnostic Collection Flow

Per-Phase Collection

Each phase returns its diagnostics as a std::vector<Diag> inside a result struct; there is no ASTDiagnoser::merge. The phase-local visitor/diagnoser owns the diagnostics and moves them out when the phase finishes:

// compiler/symres/LinkSignatureAPI.h
struct SymResSignatureResult {
    std::vector<std::pair<TypealiasStatement*, std::vector<TypeLoc>>> inline_instantiations;
    bool has_errors;
    std::vector<Diag> diagnostics;
};

// compiler/symres/SymResLinkBodyAPI.h
struct SymResLinkBodyResult {
    bool has_errors;
    std::vector<Diag> diagnostics;
};

// compiler/ASTProcessor.cpp — type verification per file
struct TypeVerifyFileResult {                 // ASTProcessor.cpp:754-757
    bool has_errors = false;
    std::vector<Diag> diagnostics;
};

Reporting Flow

Results are printed (and discarded) through the static Diagnoser::print_diagnostics, passing a phase tag:

Diagnoser::print_diagnostics(result.diagnostics, chem::string_view(abs_path), "SymRes:link_sig");

Serial phases can use SymbolResolver directly, since it extends ASTDiagnoser (and therefore Diagnoser): resolver->has_errors() / resolver->reset_errors().

Parallel Safety

This is the critical part of the diagnostics design. Files and passes run in parallel, but Diagnoser/Diag are not thread-safe. The pattern is:

  1. One diagnoser per task. Each parallel task makes its own ASTDiagnoser(processor->loc_man) or per-file visitor, so no two threads touch the same error_count/diagnostics. type_verify_file_task (ASTProcessor.cpp:759-773) runs type_verify, then result.has_errors = diagnoser.has_errors(); result.diagnostics = std::move(diagnoser.diagnostics);. link_sig_file_task, gen_inst_file_task, link_body_task etc. have the same shape and return {bool, std::vector<Diag>}.
  2. Move, don't copy. Diagnostics are std::moved out of the local diagnoser into the result, so the task-local object is left empty and ownership transfers to the joining thread.
  3. Print under a mutex. After future.get(), the main thread takes std::lock_guard<std::mutex> guard(print_mutex) (compiler/ASTProcessor.h:122) before Diagnoser::print_diagnostics(...) — e.g. ASTProcessor.cpp:504-506,538-540,586-588,607-609 (symres), :793-797 (type verify), and :1144 (codegen module path).
  4. Aggregate a bool, not a counter. ConcurrentParsingState (ASTProcessor.h:56-75) holds an std::atomic_bool has_errors (set_has_errors/get_has_errors) plus an outstanding-task counter and join promise. ASTProcessor::empty_diags/print_results (ASTProcessor.cpp:71-86) print lexer/parser results.
  5. Serial phases (top-level declare, after-link-signature) reuse one resolver and call reset_errors() between files, reusing error_count rather than accumulating it (ASTProcessor.cpp:482-486, :565-569).

LocationManager is itself thread-safe for the two operations that can race: encodeFile / encodeExistingFile take file_mutex (LocationManager.cpp:7,19), and the overflow-table insert in addLocation takes location_mutex (LocationManager.cpp:69). The common packed case is lock-free.

Diagnostic Flow Through the Pipeline

Each pass tags its output with the phase string passed to print_diagnostics, which becomes the [<tag>] prefix (e.g. [TypeCheck] error: …). Pipeline order and real call sites:

PhaseTagCall site
Lexer"Lexer"ASTProcessor.cpp:78, LabBuildCompiler.cpp:2413,2520
Parser"Parser"ASTProcessor.cpp:79, LabBuildCompiler.cpp:2414,2521
Top-level declare"SymRes:declare"ASTProcessor.cpp:228
Link signature"SymRes:link_sig"ASTProcessor.cpp:246,505
Generic instantiation"SymRes:gen_inst"ASTProcessor.cpp:269,539,587
After link signature"SymRes:after_link_sig"ASTProcessor.cpp:351
Link body"SymRes:link" / "SymRes:link_seq"ASTProcessor.cpp:367,384,608
Type verify"TypeCheck"ASTProcessor.cpp:796
Codegen (LLVM)"Declare"/"Compile"/"ExtDeclare"/"ExtCompile"ASTCompiler.cpp:24,44,64,84,112,133
2c translation"2cTranslation"ASTProcessor.cpp:1563,1585,1607
.mod file"Lexer" / "Parser"server/mod_file/Importer.cpp; LabBuildCompiler.cpp:2413-2414
LSP publish(no tag; LSP Diagnostic)server/diagnostics/DiagnosticUtils.cpp:5

Error handling controls

ASTProcessorOptions (compiler/ASTProcessorOptions.h) governs how far a build proceeds:

  • ignore_errors = false (line 42) — when true, passes do not abort after reporting; diagnostics are still collected.
  • stop_on_file_error = true (line 58) — when true, the pass aborts after the current phase once any file has reported errors, "printing less errors to console". The parallel loops check if(options->stop_on_file_error) return 1; after draining every future (join_all(futures)) — a pending task may never be abandoned, because tasks reference the caller's stack objects (ASTProcessor, allocators); see lang/docs/lab-build-crash-investigation.md.
  • verbose (line 32) — only affects progress/[lab]/benchmark prints; it does not filter or change diagnostics. -v does not suppress hints either (the "omit hints unless verbose" idea is a future improvement, not current behavior).

Build system surfacing and exit codes

  • Parse failure inside a lab file: LabBuildCompiler.cpp:2767-2772 checks state.get_has_errors() and returns nullptr (with a verbose-only [lab] message).
  • Codegen failure: LabBuildCompiler.cpp:1192-1195 checks gen.has_errors(), prints couldn't perform job due to errors during code generation to stderr, and returns 1.
  • .mod diagnostics are printed in LabBuildCompiler.cpp:2413-2414 / :2520-2521.
  • The CLI returns a non-zero code on missing input (core/main/CompilerMain.cpp:597-599) and from the job-loop failures (the many return 1/return 2 sites in LabBuildCompiler.cpp). There is no single central "exit code = error_count"; errors surface as returned 1/2 from LabBuildCompiler run functions, and type/symres errors abort before codegen.

Error Messages

Format

Diag::ansi() (core/diag/Diagnostic.cpp:105-124) renders a single-line, ANSI-coloured diagnostic (line/column are 1-based; path is the file path or, if present, Diag::path_url):

[<tag>] error: <message> at <path>:<line>:<column>

severity is optional: if absent, ansi prints no error:/warn: word — just [<tag>] <message> at <path>:<line>:<column>. Warnings render [<tag>] warn: …, info [<tag>] info: …, and hints [<tag>] hint: …. Diag::format() is the non-coloured variant ([<tag>] <message> at <path>:<range>) used by print_debug_location and a few debug sites. There is no multi-line snippet/caret rendering and no = note: continuation lines — related context is meant to be carried in Diag::relatedInformation (currently unused). Range::representation() is only reached by format(), not ansi().

ANSI colors

Colors come from the vendored rang.hpp (core/diag/Diagnostic.cpp) and are emitted by both color() (Diagnostic.cpp:69-84) and Diag::ansi (:105-124): Error red, Warning yellow, Information gray, Hint cyan. print_diagnostics (Diagnostic.cpp:33-53) routes Error to std::cerr and everything else to std::cout, flushing both at the end; the tag is emitted verbatim inside [...].

There is no source-line excerpt and no caret underline. Diagnostics are one line each; Diag has only a Range and print_diagnostics has no file-loading step.

Error limit

There is no per-diagnostic cap. The only "limit" is ASTProcessorOptions::stop_on_file_error, which short-circuits a pass after the first file with errors to reduce console noise; ignore_errors bypasses the abort so more errors accumulate.

Common Error Types

Error PatternSourceReal message / example
Duplicate symbolASTDiagnoser::dup_sym_error (ASTDiagnoser.cpp:27-30), NodeSymbolDeclarer.h:53duplicate symbol being declared, symbol 'x' already exists + symbol has a conflict; or symbol with name 'x' already exists
Symbol not foundLinkSignature.cpp:130,253unresolved variable identifier 'foo' not found
Unsatisfied typeTypeVerify.cpp:234 (unsatisfied_type_err)value with type 'float' does not satisfy type 'int'
AssignmentTypeVerify.cpp:1539,1557Expression is not assignable / cannot assign to a non mutable value
Struct initLinkSignature.cpp:501struct with name 'X' has a constructor, use @direct_init to allow direct initialization
Uninitialized variableTypeVerify.cpp:109,354use of uninitialized variable 'x' before it is initialized (use of)
Destructible indexTypeVerify.cpp:354index operator on a destructible type is not allowed, use \&raw` to take a pointer to the element instead`
Unknown annotationparser/statements/AnnotationMacro.cpp:28unknown annotation found '@foo' (parse error)
Runtime outside functionLinkSignature.cpp:414 (RUNTIME_EVAL_ERR)cannot evaluate at runtime outside function body
Non-exported typeLinkSignature.cpp:144non exported type being used in a public type, please use 'public' or 'protected' to expose it
Unsafe derefSymResLinkBody.cpp:2457de-referencing a pointer in safe context is prohibited (warning)
Malformed sourceparser/structures/Struct.cpp:251expected a closing brace '}' for [<thing>]

Plugin Diagnostics

Compiler plugins report diagnostics through the CBI binding in compiler/cbi/bindings/ASTDiagnoserCBI.h/.cpp — a single no-mangle symbol void ASTDiagnosererror(ASTDiagnoser* diagnoser, chem::string_view* msg, uint64_t loc) that calls diagnoser->error(*msg, loc). Registered in CBI.cpp:464-465,491 as compiler_ASTDiagnosererror under the ASTDiagnoser interface. The Chemical side is lang/libs/compiler/src/ASTDiagnoser.ch: func error(&self, msg : &std::string_view, loc : ubigint). CBI exposes it to plugins as compiler_ASTDiagnosererror; there is no cbi_report_warning/info/note family. Plugins can also report at a token through the parser binding Parsererror_at (compiler/cbi/bindings/ParserCBI.cpp:43-45, registered as compiler_Parsererror_at in CBI.cpp:148), surfaced in lang/libs/compiler/src/Parser.ch:25,75 (func error_at(&self, msg, token)).

Only Error severity crosses the plugin boundary. A plugin cannot currently emit warnings/hints through CBI, and plugin diagnostics carry a raw ubigint encoded location (obtained from an AST node), not a Diag object.

LSP Integration

The LSP server converts compiler diagnostics to lsp::Diagnostic in server/diagnostics/DiagnosticUtils.cpp:5-16 (add_diagnostics): it maps diag.range to an lsp::Range, copies diag.message, and integer-casts diag.severity.value() to lsp::DiagnosticSeverity. Key points (cross-link: .agents/skills/lsp_server/SKILL.md §4.1, §6):

  • Diag::range maps directly to lsp::Range (both 0-based; Diag::ansi adds +1 only for display). Severity is a raw integer cast whose Error/Warning/Information/Hint order matches lsp::DiagnosticSeverity (1..4).
  • The converter assumes diag.severity.has_value() and unwraps it without a check — a Diag with no severity is UB here. All diagnostics from ASTDiagnoser and parser helpers do set severity.
  • relatedInformation, tags, and fixits_ are dropped.
  • Sources fed into add_diagnostics: lexer (LexResult::diags), parser (parser.diagnostics), and SymbolResolver (resolver.diagnostics), accumulated in process_file (server/LspSemanticTokens.cpp:990-997). Type verification does not run in the LSP.
  • Publishing uses lsp::notifications::TextDocument_PublishDiagnostics via publish_diagnostics (LspSemanticTokens.cpp:1266), serialized by publish_diagnostics_mutex with an std::async cancel flag. Build failures are reported separately through the custom chemical/buildStatus notification (WorkspaceManager::report_build_failure, WorkspaceManager.cpp:596) so they do not clobber per-URI diagnostics.
  • Ranges for navigation features (hover, go-to-definition) come from the same LocationManager::getLocationPos decode, so the location layer is shared between diagnostics and the rest of the IDE surface.

Quick Fix / Replacement Support

The data structures exist but the pipeline does not use them yet: TextReplacement/Diag::fixits_ (Diagnostic.h:28-32,51), DiagTag::Unnecessary/Deprecated/Diag::tags (:13-18,54), DiagRelatedInfo/Diag::relatedInformation (:20-26,58). What is used instead: related context is emitted as separate diagnostics (dup_sym_error plus the two-node/two-location overloads), and deprecation is a node flag (set_deprecated via @deprecated — .agents/skills/annotations/SKILL.md), not a DiagTag.

To wire quick fixes end-to-end: populate Diag::fixits_, translate TextReplacement → lsp::TextEdit and tags/relatedInformation in add_diagnostics, and register a textDocument/codeAction handler in LSPMain.cpp (none exists — no codeAction capability is advertised).

Print/Buffer Strategy

Current Approach

Diagnostics are collected during a phase and printed after it, via the static Diagnoser::print_diagnostics:

// After a phase (optionally under print_mutex in parallel phases):
Diagnoser::print_diagnostics(result.diagnostics, chem::string_view(abs_path), "SymRes:link_sig");

// Or print a diagnoser's own collected diagnostics:
diagnoser.print_diagnostics(path, "SymRes");

print_diagnostics sends Error severities to stderr and everything else to stdout.

Future Improvements

  1. Batch and sort diagnostics by file/line/column
  2. Deduplicate repeated errors
  3. Colored output based on severity
  4. JSON output for IDE integration
  5. Compressed output — omit hints unless verbose (-v)
  6. Diagnostic groups — group related errors together
  7. Error recovery — continue compilation to find more errors
  8. Suppression — allow suppressing specific warnings

Current state: (1) is not done — diagnostics is an unordered insertion-order vector and print_diagnostics iterates it directly. (3) is actually done via rang.hpp colors in Diag::ansi. (5) is not done. (7) is partially achieved by ignore_errors + stop_on_file_error. The rest remain unstarted.

Adding a New Diagnostic or Error Message

Checklist and conventions:

  1. Pick the right API. Most passes have an ASTDiagnoser& and an AST node: prefer diagnoser.error(node) << "message " << value; (append) or diagnoser.error("message", node). In the parser use parser.error("...") / parser.error() << .... Use warn only for non-fatal issues. There is no note/suggest.
  2. Attach a real location. Never use the two-arg deprecated diagnostic(message, severity) (defaults to {0,0} + empty path). Use a node (encoded_location()), a SourceLocation, or a parser Position.
  3. Message style. Lowercase, descriptive, condition-first: "unresolved variable identifier 'x' not found", "cannot assign to a non mutable value", "expected a ')' after …". Built by streaming operator<<; include offending names/types in single quotes via representation().
  4. Choose severity deliberately. Error aborts (unless ignore_errors); warnings and below do not, and only Error increments error_count.
  5. Respect threading. Inside a per-file parallel task use the task-local diagnoser; never emit into a shared one from multiple threads. Report via the result struct; the join path prints under print_mutex.
  6. Add a phase tag for a new pass (the string passed to print_diagnostics, e.g. SymRes:*, TypeCheck, Declare/Compile, 2cTranslation).
  7. Test with lang/tests/negative/ (asserts exit code + stderr, see .agents/skills/testing/SKILL.md) or an isolated lang/compiled/temp.ch.
  8. CBI enum sync is not needed for a plain diagnostic, but mirror any new CBI-exposed severity/DiagTag/DiagSeverity in the .ch binding (AGENTS.md enum-sync rule).

Conventions quick list: lowercase messages, single-quoted identifiers, operator<< composition, attach a node/location, lowest correct severity, parallel-safe collection, phase tag.

Gotchas

  • Location validity. isValid() is encoded != 0, but file id 0 is the legitimate first-file id (see ZERO_LOC). ASTDiagnoser does not guard a zero location: it decodes to fileId 0 and calls getPathForFileId(0), which is UB if no file was ever encoded.
  • Dangling file paths. getPathForFileId returns a string_view into LocationManager::file_paths; it dangles if the manager is destroyed or the map mutates. In the LSP a single loc_man lives for the session, and cached locations go stale when a file is re-lexed (LSP skill §9.1).
  • No dedup, no sort. Expect duplicate and out-of-order messages, especially across parallel files.
  • error_count is a field, not a method. It counts only Error severity, including empty diagnostics created via error(loc) even with no appended message.
  • Warnings do not fail the build. There is no Chemical warnings-as-errors / -Werror (the -Werror= flags in CMakeLists.txt:76,91 are for the C++ compiler). A warnings-only pass returns has_errors() == false and continues. Emit Error if it must be fatal.
  • Severity-optional Diag. Hand-built Diags may have severity == std::nullopt; they print with no severity word and go to stdout. The LSP converter unwraps .value() unconditionally, so always set severity for anything reaching the LSP.
  • relatedInformation/tags/fixits_ are dead ends today — setting them changes no output until the consumers are extended.
  • addLocation overflow is unchecked in release (see the filter bug above). Keep line < 262144, char < 4096, span < 2048 lines or the location silently truncates.
  • Interpreter: GlobalInterpretScope derives from ASTDiagnoser (ast/base/GlobalInterpretScope.cpp:18), so InterpretScope reports via global->getASTDiagnoser().warn(location) (ast/base/InterpretScope.h:276) using the same encoding.

Debugging Diagnostics

Adding a Diagnostic

// In any AST visitor / Value / ASTNode:
diagnoser.error(node->encoded_location()) << "this feature is not yet implemented";
// Or with a message and a location:
diagnoser.warn(node->encoded_location(), "this operation is deprecated, use 'new_thing' instead");
// Or a message and any node exposing encoded_location():
diagnoser.error("expected an integer", someValue);

Checking, counting, and inspecting

if(diagnoser.has_errors()) { return; }             // skip codegen when error_count > 0
unsigned int err_count = diagnoser.error_count;    // public field, not a method
for(auto& d : diagnoser.diagnostics) {
    d.ansi(std::cerr, chem::string_view("<path>"), "MyTag") << '\n';   // colored, 1-based
    // or: d.format(std::cout, path, "MyTag");                        // plain, range form
}

Isolating a failing file

Copy the failing construct into lang/compiled/temp.ch and compile it directly with Compiler/TCCCompiler (see AGENTS.md "Debugging: Isolating a Single Test Case"). The emitted diagnostic tag tells you which pass produced the message, and --mode/-v control progress output, not the diagnostics themselves.

Performance Considerations

  1. Owned messages: Diag::message is a std::string; file paths are deduplicated in LocationManager::file_paths
  2. Move semantics: Diagnostics are moved (not copied) out of phase-local diagnosers
  3. Per-file abort: ASTProcessorOptions::stop_on_file_error decides whether a module aborts after a phase reports errors
  4. Per-phase collection: Each phase collects independently, parallel-safe
  5. Overflow table: Locations too large to pack are stored in LocationManager::locations, referenced by an index

Additional notes: only oversized locations take location_mutex; packed ones are lock-free (LocationManager.cpp:47-75). encodeFile stores each path once, printing is serialized under print_mutex, and getLineStartFast skips the full 5-field decode.

Cross-Links

  • LSP Server (.agents/skills/lsp_server/SKILL.md) — add_diagnostics, publishing, Diag→range.
  • Parser Internals (.agents/skills/parser_internals/SKILL.md) — BasicParser, loc(), error recovery.
  • Type Verification (.agents/skills/type_verification/SKILL.md) — unsatisfied_type_err, TypeCheck messages.
  • Symbol Resolution (.agents/skills/symres/SKILL.md) — SymbolResolver (an ASTDiagnoser), dup_sym_error.
  • Annotations (.agents/skills/annotations/SKILL.md) — @deprecated, annotation parse errors.
  • Compiler Bindings / CBI (.agents/skills/compiler_bindings/SKILL.md, .agents/skills/cbi_plugin_api/SKILL.md) — ASTDiagnosererror, Parsererror_at, enum-sync.
  • Testing (.agents/skills/testing/SKILL.md) — negative tests and @test dispatch.
  • Build System (.agents/skills/build_system/SKILL.md) — how jobs surface errors and exit codes.

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Comprehensive guide to the Chemical compiler's annotation system (`@name`, `@name(args)`) — how annotations are lexed, parsed, registered in the `AnnotationController`, stored and queried centrally, consumed by the compiler and CBI plugins, and how to add a new one. Load when working on annotations, `AnnotationController`, compiler plugins that define/read annotations, or node attribute flags such as `@extern`, `@test`, `@make`, `@deprecated`, `@no_mangle`, `@inline`, and `@volatile`.

The English summary is being prepared. Showing the original description.

chemicallang/chemical31Updated Oct 26, 2026

The C++ AST node framework of the Chemical compiler — the ASTAny/ASTNode/Value/BaseType hierarchy, node/value/type kind enums and range predicates, the visitor dispatch framework, the arena allocator, TypeBuilder canonicalization, member containers, and cross-reference resolution. Load when adding or modifying AST nodes, writing compiler passes/codegen visitors, debugging kind mismatches or arena/lifetime issues, or extending the compiler API bindings.

The English summary is being prepared. Showing the original description.

chemicallang/chemical31Updated Oct 26, 2026

The Chemical compiler benchmark & release analytics system — how the static GitHub Pages dashboard works, the JSON data model (manifest/daily/release records), the bash collection scripts (bench-*.sh), the GitHub Actions workflows (benchmark-daily/release), and the critical pitfalls (hidden upload dirs, worktree submodules, gh-pages-only data). Load when working on the dashboard, benchmark collection, or analytics workflows.

The English summary is being prepared. Showing the original description.

chemicallang/chemical31Updated Oct 26, 2026

Comprehensive deep-dive into the Chemical build pipeline — how chemical.mod is converted to build.lab, how build scripts are JIT-compiled via TinyCC, how jobs are created and executed, and how ASTProcessor orchestrates the parallel compilation passes.

The English summary is being prepared. Showing the original description.

chemicallang/chemical31Updated Oct 26, 2026

All documentation related to building and running Project or Tests

The English summary is being prepared. Showing the original description.

chemicallang/chemical31Updated Oct 26, 2026

Comprehensive guide to the C translation backend (2c) — how Chemical AST is translated to C code, key patterns, gotchas, and optimization strategies.

The English summary is being prepared. Showing the original description.

chemicallang/chemical31Updated Oct 26, 2026

See all skills by chemicallang

Report a problem with this skill