about summary refs log tree commit diff stats
path: root/js/baba-yaga/src/core/error.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/baba-yaga/src/core/error.js')
-rw-r--r--js/baba-yaga/src/core/error.js294
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
+    };
+  }
+}