/** * Compile failures, reported in coordinates of the script the user edits. * * Failures arrive from three places, each with its own idea of position: * numbl's parser (a `position` offset), numbl's lowerer (`UnsupportedConstruct` * / `JitTypeError`, with a `span`), and this project's planner and WGSL * emitter (`UnsupportedOnGpu`, carrying the numbl span it was given). All of * them are offsets into the script, so they need only be turned into a line * and column for display. */ /** A compile failure located in the script source. */ export class ScriptCompileError extends Error { /** Offset into the script, when the failure has a position. */ readonly start?: number; readonly end?: number; constructor( message: string, opts: { start?: number; end?: number; cause?: unknown } = {}, ) { super(message, { cause: opts.cause }); this.name = 'ScriptCompileError'; this.start = opts.start; this.end = opts.end; } } /** Raised for a construct the WGSL backend cannot express. Mirrors numbl's * own decline discipline: fail at compile time with a source span, never * silently produce something that computes the wrong thing. */ export class UnsupportedOnGpu extends Error { readonly span?: unknown; constructor(message: string, span?: unknown) { super(message); this.name = 'UnsupportedOnGpu'; this.span = span; } } /** Extract whatever position information an error carries. */ function positionOf(e: unknown): { start?: number; end?: number } { const span = (e as { span?: { start?: unknown; end?: unknown } }).span; if (span && typeof span.start === 'number') { return { start: span.start, end: typeof span.end === 'number' ? span.end : undefined, }; } // numbl's parser SyntaxError reports a bare offset. const position = (e as { position?: unknown }).position; if (typeof position === 'number') return { start: position }; return {}; } /** Normalize any thrown value into a located `ScriptCompileError`. */ export function asCompileError(e: unknown): ScriptCompileError { if (e instanceof ScriptCompileError) return e; const { start, end } = positionOf(e); const raw = e instanceof Error ? e.message : String(e); // numbl's parse errors read as bare token complaints out of context. const message = (e as Error)?.name === 'SyntaxError' ? `MATLAB syntax error: ${raw}` : raw; return new ScriptCompileError(message, { start, end, cause: e }); } /** Run `fn`, locating any compile failure in the script. */ export function inScript(fn: () => T): T { try { return fn(); } catch (e) { throw asCompileError(e); } } /** Render a failure for display: message and 1-based line/column. */ export function formatFailure(e: unknown, source: string): string { const message = e instanceof Error ? e.message : String(e); if (!(e instanceof ScriptCompileError)) return message; if (e.start !== undefined && e.start <= source.length) { const before = source.slice(0, e.start); const line = before.split('\n').length; const column = e.start - before.lastIndexOf('\n'); return `${message} (line ${line}, column ${column})`; } return message; }