diff options
Diffstat (limited to 'js/baba-yaga/src/core/error.js')
-rw-r--r-- | js/baba-yaga/src/core/error.js | 294 |
1 files changed, 294 insertions, 0 deletions
diff --git a/js/baba-yaga/src/core/error.js b/js/baba-yaga/src/core/error.js new file mode 100644 index 0000000..6a19cd1 --- /dev/null +++ b/js/baba-yaga/src/core/error.js @@ -0,0 +1,294 @@ +// 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 + }; + } +} |