// error.js - Rich error handling system for Baba Yaga /** * Enhanced error class with source location, context, and suggestions */ export class BabaError extends Error { constructor(message, location = null, source = '', suggestions = [], type = 'BabaError') { super(message); this.name = type; this.location = location; // { line, column, length? } this.source = source; this.suggestions = suggestions; this.timestamp = new Date().toISOString(); } /** * Format error with source context and helpful information */ formatError() { let formatted = `${this.name}: ${this.message}`; if (this.location && this.source) { const lines = this.source.split('\n'); const lineIndex = this.location.line - 1; if (lineIndex >= 0 && lineIndex < lines.length) { const line = lines[lineIndex]; const column = Math.max(0, this.location.column - 1); const length = this.location.length || 1; // Create pointer to error location const pointer = ' '.repeat(column) + '^'.repeat(Math.min(length, line.length - column)); formatted += `\n --> line ${this.location.line}, column ${this.location.column}`; // Show surrounding context (up to 2 lines before/after) const contextStart = Math.max(0, lineIndex - 2); const contextEnd = Math.min(lines.length, lineIndex + 3); for (let i = contextStart; i < contextEnd; i++) { const lineNum = i + 1; const isErrorLine = i === lineIndex; const prefix = isErrorLine ? ' > ' : ' '; const lineNumStr = lineNum.toString().padStart(3, ' '); formatted += `\n${prefix}${lineNumStr} | ${lines[i]}`; if (isErrorLine) { formatted += `\n | ${pointer}`; } } } } if (this.suggestions.length > 0) { formatted += '\n\nSuggestions:'; for (const suggestion of this.suggestions) { formatted += `\n - ${suggestion}`; } } return formatted; } /** * Convert to JSON for serialization */ toJSON() { return { name: this.name, message: this.message, location: this.location, suggestions: this.suggestions, timestamp: this.timestamp, stack: this.stack }; } } /** * Specific error types for different phases */ export class LexError extends BabaError { constructor(message, location, source, suggestions = []) { super(message, location, source, suggestions, 'LexError'); } } export class ParseError extends BabaError { constructor(message, location, source, suggestions = []) { super(message, location, source, suggestions, 'ParseError'); } } export class RuntimeError extends BabaError { constructor(message, location, source, suggestions = []) { super(message, location, source, suggestions, 'RuntimeError'); } } export class TypeError extends BabaError { constructor(message, location, source, suggestions = []) { super(message, location, source, suggestions, 'TypeError'); } } export class ValidationError extends BabaError { constructor(message, location, source, suggestions = []) { super(message, location, source, suggestions, 'ValidationError'); } } /** * Error helper functions for common scenarios */ export class ErrorHelpers { /** * Create error with token location information */ static fromToken(ErrorClass, message, token, source, suggestions = []) { const location = token ? { line: token.line || 1, column: token.column || 1, length: token.value ? token.value.length : 1 } : null; return new ErrorClass(message, location, source, suggestions); } /** * Create error with AST node location (if available) */ static fromNode(ErrorClass, message, node, source, suggestions = []) { const location = node && node.location ? node.location : null; return new ErrorClass(message, location, source, suggestions); } /** * Generate suggestions for common typos */ static generateSuggestions(input, validOptions, maxDistance = 2) { const suggestions = []; for (const option of validOptions) { const distance = this.levenshteinDistance(input, option); if (distance <= maxDistance) { suggestions.push(`Did you mean "${option}"?`); } } return suggestions.slice(0, 3); // Limit to 3 suggestions } /** * Calculate Levenshtein distance for typo suggestions */ static levenshteinDistance(str1, str2) { const matrix = []; for (let i = 0; i <= str2.length; i++) { matrix[i] = [i]; } for (let j = 0; j <= str1.length; j++) { matrix[0][j] = j; } for (let i = 1; i <= str2.length; i++) { for (let j = 1; j <= str1.length; j++) { if (str2.charAt(i - 1) === str1.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min( matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1 ); } } } return matrix[str2.length][str1.length]; } /** * Common error messages with suggestions */ static unexpectedToken(expected, actual, token, source) { const suggestions = []; if (expected === 'SEMICOLON' && actual === 'EOF') { suggestions.push('Add a semicolon at the end of the statement'); } else if (expected === 'RPAREN' && actual === 'EOF') { suggestions.push('Add a closing parenthesis'); } else if (expected === 'RBRACE' && actual === 'EOF') { suggestions.push('Add a closing brace'); } else if (actual === 'IDENTIFIER' && token.value) { const keywords = ['when', 'is', 'then', 'with', 'rec', 'Ok', 'Err', 'true', 'false']; suggestions.push(...this.generateSuggestions(token.value, keywords)); } return ErrorHelpers.fromToken( ParseError, `Expected ${expected} but got ${actual}`, token, source, suggestions ); } static undefinedVariable(name, source, location = null) { const suggestions = [ `Check if "${name}" is spelled correctly`, 'Make sure the variable is declared before use', 'Check if the variable is in the correct scope' ]; return new RuntimeError( `Undefined variable: ${name}`, location, source, suggestions ); } static undefinedProperty(property, object, source, location = null) { const suggestions = [ `Check if "${property}" is spelled correctly`, 'Use the "keys" function to see available properties', `Make sure "${property}" exists on the object` ]; return new RuntimeError( `Undefined property: ${property}`, location, source, suggestions ); } static typeMismatch(expected, actual, value, source, location = null) { const suggestions = []; if (expected === 'Int' && actual === 'Float') { suggestions.push('Use math.floor() or math.round() to convert to integer'); } else if (expected === 'String' && actual === 'Number') { suggestions.push('Convert to string using string concatenation with ""'); } else if (expected === 'List' && actual === 'String') { suggestions.push('Use str.split() to convert string to list'); } const displayValue = typeof value === 'object' && value !== null && 'value' in value ? value.value : value; return new TypeError( `Expected ${expected} but got ${actual} (value: ${JSON.stringify(displayValue)})`, location, source, suggestions ); } } /** * Error recovery strategies for parsers */ export class ErrorRecovery { /** * Skip tokens until we find a synchronization point */ static synchronize(tokens, position, syncTokens = ['SEMICOLON', 'EOF']) { while (position < tokens.length) { if (syncTokens.includes(tokens[position].type)) { break; } position++; } return position; } /** * Try to recover from missing tokens by inserting them */ static insertMissingToken(tokenType, location) { return { type: tokenType, value: tokenType === 'SEMICOLON' ? ';' : '', line: location.line, column: location.column, synthetic: true // Mark as inserted for recovery }; } }