import type { Span } from "./lexer.js"; export type Severity = "error" | "warning"; export interface Diagnostic { severity: Severity; code: string; message: string; // the "what" span: Span; detail?: string; // the "why" — plain language fixes?: { title: string; code: string }[]; note?: string; } // Loads source lines for code frames. Sources can be registered lazily. const sourceCache = new Map(); export function registerSource(file: string, source: string): void { if (!sourceCache.has(file)) sourceCache.set(file, source.split("\n")); } export function setSources(files: Map): void { for (const [f, s] of files) registerSource(f, s); } export function renderDiagnostic(d: Diagnostic): string { const lines = sourceCache.get(d.span.file); const out: string[] = []; const sev = d.severity === "error" ? "error" : "warning"; const code = d.severity === "error" ? d.code : d.code.replace("E", "W"); out.push(`${sev}[${code}]: ${d.message}`); if (lines) { const { line, col, endCol } = positionOf(d.span, lines); const width = String(line).length; out.push(` ${" ".repeat(width)}┌─ ${d.span.file}:${line}:${col}`); out.push(` ${" ".repeat(width)}│`); const text = lines[line - 1] ?? ""; out.push(` ${String(line).padStart(width)} │ ${text}`); const caretLen = Math.max(1, endCol - col); const caret = " ".repeat(col - 1) + "^".repeat(Math.min(caretLen, Math.max(1, text.length - col + 2))); out.push(` ${" ".repeat(width)} │ ${caret}`); if (d.span.file) { // underline marker for the token itself in detail const srcEnd = sourceCache.get(d.span.file)?.join("\n"); void srcEnd; } } if (d.detail) out.push("", d.detail); if (d.fixes && d.fixes.length) { out.push(""); out.push(" Possible fixes:"); d.fixes.forEach((f, i) => { out.push(` ${i + 1}. ${f.title}`); for (const line of f.code.split("\n")) out.push(` ${line}`); }); } if (d.note) out.push("", ` Note: ${d.note}`); out.push(""); return out.join("\n"); } function positionOf(span: Span, lines: string[]): { line: number; col: number; endCol: number } { let offset = 0; for (let i = 0; i < lines.length; i++) { const len = lines[i]!.length + 1; if (span.start <= offset + len) { const line = i + 1; const col = span.start - offset + 1; const endCol = span.end - offset + 1; return { line, col, endCol }; } offset += len; } return { line: lines.length, col: 1, endCol: 2 }; } export function summary(diags: Diagnostic[]): string { const errors = diags.filter((d) => d.severity === "error").length; const warnings = diags.filter((d) => d.severity === "warning").length; if (errors === 0 && warnings === 0) return "no problems"; const parts: string[] = []; if (errors) parts.push(`${errors} error${errors > 1 ? "s" : ""}`); if (warnings) parts.push(`${warnings} warning${warnings > 1 ? "s" : ""}`); return parts.join(", "); } export const EXPLAIN: Record = { E1001: `Assignability failure. A value of one type was used where another type is required. The message names both types in plain language. The fix depends on the direction of the mistake: - passing an optional where a guaranteed value is required: use 'if let', 'match', 'or', or '.require("...")'; - passing a 'Result' where a plain value is required: handle it with '?', 'match', 'or', or '.require("...")'; - passing the wrong shape: check the field/method names and types.`, E1007: `Optional value used where a guaranteed value is required. 'String?' and 'String' are different types on purpose: an optional value may be absent (undefined). You must prove presence before using the value. Possible fixes: 1. Check that it exists first: if let name = username { greet(name) } 2. Provide a fallback: greet(username or "Guest") 3. Change the function to accept an optional value: fun greet(name: String?)`, E1009: `Result value used where a plain value is required. A function declared with '!' returns a Result; use '?' to propagate it, 'or' for a fallback, 'match' to handle both cases, or '.require("message")' to crash with a good error if the failure is a bug.`, E4001: `A Result value was dropped without being handled. Every fallible result must be handled: propagate with '?', match on 'Ok'/'Err', fall back with 'or', or unwrap with '.require'.`, E4002: `'?' was used outside a fallible function. The propagation operator is only valid inside a function declared with '!' (return type ending in '!'), or inside a 'try' block at the JavaScript boundary.`, E4003: `'raise' was used outside a fallible function. 'raise' is only valid inside a function declared with '!' (return type ending in '!'), or inside a 'try' block at the JavaScript boundary.`, E5001: `Match is not exhaustive. The compiler proved that some shapes of the matched value are not covered. Either add the missing arms or add a '_' wildcard arm.`, E3001: `Name not found. The identifier does not exist in the current scope. Check the spelling and that the module is imported.`, E6001: `'await' used outside an async function.`, }; export function explain(code: string): string { return EXPLAIN[code] ?? `No explanation available for ${code}.`; }