Tsonic GitHub

Diagnostics System

How errors and warnings are created, collected, and reported.

Overview

The diagnostics system provides structured error reporting across all compiler phases. Instead of throwing exceptions, each phase returns Result<T, Diagnostic[]>.

Diagnostic Structure

type Diagnostic = {
  readonly code: DiagnosticCode; // "TSN1001"
  readonly severity: DiagnosticSeverity; // "error" | "warning" | "info"
  readonly message: string; // Description
  readonly location?: SourceLocation; // File, line, column
  readonly hint?: string; // Suggested fix
  readonly relatedLocations?: readonly SourceLocation[];
};

type SourceLocation = {
  readonly file: string;
  readonly line: number;
  readonly column: number;
  readonly length: number;
};

Error Code Ranges

Range Category
TSN1xxx Module resolution and imports
TSN2xxx Type system
TSN3xxx C# keywords and identifiers
TSN4xxx .NET interop
TSN5xxx NativeAOT and runtime
TSN6xxx Internal compiler errors
TSN7xxx Language semantics and validation
TSN9xxx Metadata and bindings loading

TSN7xxx Detailed Codes

Code Description
TSN7403 Anonymous object literal requires type annotation
TSN7405 Object literal contains ineligible pattern
TSN7406 Mapped type not supported (Partial, Required, etc.)
TSN7407 Conditional type not supported (Extract, Exclude)
TSN7408 Mixed variadic tuples not supported
TSN7409 infer keyword not supported
TSN7410 Intersection types not supported (A & B)
TSN7413 Dictionary key type restriction (string/number only)
TSN7414 Type cannot be represented in compiler subset
TSN7420 ref/out/In are parameter modifiers, not types

Creating Diagnostics

Use the factory function:

import { createDiagnostic } from "./types/diagnostic.js";

const diagnostic = createDiagnostic(
  "TSN1001",
  "error",
  "Local import must have .js or .ts extension",
  { file: "src/main.ts", line: 5, column: 20, length: 15 },
  'Add an extension (recommended): "./utils.js"'
);

Collecting Diagnostics

Use the DiagnosticsCollector to aggregate diagnostics:

import {
  createDiagnosticsCollector,
  addDiagnostic,
  mergeDiagnostics,
} from "./types/diagnostic.js";

// Create collector
const collector = createDiagnosticsCollector();

// Add diagnostic (returns new collector - immutable)
const updated = addDiagnostic(collector, diagnostic);

// Check for errors
if (updated.hasErrors) {
  return { ok: false, error: updated.diagnostics };
}

// Merge multiple collectors
const merged = mergeDiagnostics(collector1, collector2);

Error Flow Through Pipeline

Each phase propagates errors up the chain:

const compile = (entryPoint: string): Result<BuildOutput, Diagnostic[]> => {
  // Phase 1: Create program
  const programResult = createProgram(entryPoint);
  if (!programResult.ok) {
    return programResult; // Propagate errors
  }

  // Phase 2: Resolve modules
  const resolverResult = resolveModules(programResult.value);
  if (!resolverResult.ok) {
    return resolverResult; // Propagate errors
  }

  // Phase 3: Validate
  const validationResult = validateModules(resolverResult.value);
  if (!validationResult.ok) {
    return validationResult; // Propagate errors
  }

  // ... continue pipeline
};

Phase-Specific Errors

Frontend Errors (TSN1xxx, TSN2xxx, TSN7xxx)

// Import validation
const validateImport = (specifier: string): Result<void, Diagnostic> => {
  if (!specifier.endsWith(".js") && !specifier.endsWith(".ts")) {
    return {
      ok: false,
      error: createDiagnostic(
        "TSN1001",
        "error",
        "Local import must have .js or .ts extension",
        location,
        `Change "${specifier}" to "${specifier}.js" (recommended)`
      ),
    };
  }
  return { ok: true, value: undefined };
};

Emitter Errors (TSN5xxx)

// Emission error
const emitExpression = (expr: IrExpression): Result<string, Diagnostic> => {
  if (expr.kind === "unsupported") {
    return {
      ok: false,
      error: createDiagnostic(
        "TSN5001",
        "error",
        `Cannot emit expression: ${expr.reason}`,
        expr.location
      ),
    };
  }
  // ...
};

Backend Errors (TSN6xxx)

// Build error
const runDotnetPublish = (cwd: string): Result<string, Diagnostic> => {
  const result = spawnSync("dotnet", ["publish"], { cwd });
  if (result.status !== 0) {
    return {
      ok: false,
      error: createDiagnostic(
        "TSN6001",
        "error",
        `dotnet publish failed: ${result.stderr}`,
        undefined,
        "Check that .NET SDK is installed"
      ),
    };
  }
  return { ok: true, value: result.stdout };
};

Formatting Diagnostics

The CLI formats diagnostics for console output:

const formatDiagnostic = (diagnostic: Diagnostic): string => {
  const parts: string[] = [];

  if (diagnostic.location) {
    parts.push(
      `${diagnostic.location.file}:${diagnostic.location.line}:${diagnostic.location.column}`
    );
  }

  parts.push(`${diagnostic.severity} ${diagnostic.code}:`);
  parts.push(diagnostic.message);

  if (diagnostic.hint) {
    parts.push(`Hint: ${diagnostic.hint}`);
  }

  return parts.join(" ");
};

Example output:

src/main.ts:5:20 error TSN1001: Local import must have .js or .ts extension
  Hint: Change "./utils" to "./utils.js" (recommended)

Some errors span multiple files. Use relatedLocations:

const createCircularDepError = (cycle: string[]): Diagnostic => ({
  code: "TSN1002",
  severity: "error",
  message: `Circular dependency: ${cycle.join(" -> ")}`,
  location: { file: cycle[0], line: 1, column: 0, length: 0 },
  relatedLocations: cycle.slice(1).map((file) => ({
    file,
    line: 1,
    column: 0,
    length: 0,
  })),
});

Error vs Warning

Errors stop compilation:

  • Missing imports
  • Type mismatches
  • Unsupported features
  • Build failures

Warnings allow compilation to continue:

  • Unused imports
  • Deprecated features
  • Non-critical issues
// Error - stops compilation
const error = createDiagnostic("TSN1001", "error", "Missing file extension");

// Warning - compilation continues
const warning = createDiagnostic(
  "TSN7001",
  "warning",
  "Import 'User' is declared but never used"
);

Best Practices

  1. Specific error codes: Each error type has a unique code
  2. Clear messages: Explain what's wrong
  3. Helpful hints: Suggest how to fix
  4. Accurate locations: Point to exact position
  5. Related info: Include all relevant locations
  6. Fail early: Return errors as soon as detected
  7. Collect all: In IDE mode, collect all errors before returning