<commit_before>require "./syntax/location.cr"
require "./syntax/ast.cr"
require "./source-highlight.cr"
require "./interpreter/context.cr"
require "colorize"

module Charly
  # The base for all exceptions in charly
  class BaseException < Exception
    def to_s(io)
      meta(io)
      io << "#{@message}".colorize(:red)
    end

    # :nodoc:
    private def meta(io)
    end
  end

  # `IOException` is thrown when a file could not be read
  class IOException < BaseException
  end

  # `LocalException` is the base type for all exceptions that can highlight
  # parts of a file
  class LocalException < BaseException
    property location_start : Location
    property location_end : Location
    property source : String
    property filename : String
    property trace : Array(Trace)

    def initialize(@location_start, @location_end, @message, @trace = [] of Trace)
      path = @location_start.filename

      if path.starts_with? Dir.current
        @filename = File.join(".", path.gsub(Dir.current, ""))
      else
        @filename = path
      end

      # Load the file at the path of @location_start

      if File.exists?(path) && File.readable?(path)
        @source = File.read(path)
      else
        @source = ""
      end
    end

    def self.new(location_start : Location, message : String)
      self.new(location_start, location_start, message)
    end

    def self.new(node : ASTNode, message : String)
      self.new(node.location_start, node.location_end, message)
    end

    def self.new(node : ASTNode, context : Context, message : String)
      self.new(node.location_start, node.location_end, message, context.trace)
    end

    private def meta(io)
      # Print the filename
      io << @filename.colorize(:yellow)
      io << "\n"

      # Print the source highlight
      if (source = @source).is_a? String
        # They both might be set to null, so check first
        loc_start, loc_end = @location_start, @location_end
        if loc_start.is_a?(Location) && loc_end.is_a?(Location)
          highlighter = SourceHighlight.new(loc_start, loc_end)
          highlighter.present(source, io)
          io << "at #{loc_start}\n".colorize(:green)
        end
      end

      # Print the stack trace
      @trace.each do |entry|
        io << "#{entry.colorize(:green)}\n"
      end
    end
  end

  #  A `SyntaxError` describes unexpected chars or tokens in the source string
  class SyntaxError < LocalException
  end

  # A `InvalidNode` describes unexpected nodes in a parse tree
  class RunTimeError < LocalException
  end
end
<commit_msg>Print the node location after the trace for better consistency<commit_after>require "./syntax/location.cr"
require "./syntax/ast.cr"
require "./source-highlight.cr"
require "./interpreter/context.cr"
require "colorize"

module Charly
  # The base for all exceptions in charly
  class BaseException < Exception
    def to_s(io)
      meta(io)
      io << "#{@message}".colorize(:red)
    end

    # :nodoc:
    private def meta(io)
    end
  end

  # `IOException` is thrown when a file could not be read
  class IOException < BaseException
  end

  # `LocalException` is the base type for all exceptions that can highlight
  # parts of a file
  class LocalException < BaseException
    property location_start : Location
    property location_end : Location
    property source : String
    property filename : String
    property trace : Array(Trace)

    def initialize(@location_start, @location_end, @message, @trace = [] of Trace)
      path = @location_start.filename

      if path.starts_with? Dir.current
        @filename = File.join(".", path.gsub(Dir.current, ""))
      else
        @filename = path
      end

      # Load the file at the path of @location_start

      if File.exists?(path) && File.readable?(path)
        @source = File.read(path)
      else
        @source = ""
      end
    end

    def self.new(location_start : Location, message : String)
      self.new(location_start, location_start, message)
    end

    def self.new(node : ASTNode, message : String)
      self.new(node.location_start, node.location_end, message)
    end

    def self.new(node : ASTNode, context : Context, message : String)
      self.new(node.location_start, node.location_end, message, context.trace)
    end

    private def meta(io)
      # Print the filename
      io << @filename.colorize(:yellow)
      io << "\n"

      # Print the source highlight
      loc_start, loc_end = nil, nil
      if (source = @source).is_a? String
        # They both might be set to null, so check first
        loc_start, loc_end = @location_start, @location_end
        if loc_start.is_a?(Location) && loc_end.is_a?(Location)
          highlighter = SourceHighlight.new(loc_start, loc_end)
          highlighter.present(source, io)
        end
      end

      # Print the stack trace
      @trace.each do |entry|
        io << "#{entry.colorize(:green)}\n"
      end
      io << "at #{loc_start}\n".colorize(:green)
    end
  end

  #  A `SyntaxError` describes unexpected chars or tokens in the source string
  class SyntaxError < LocalException
  end

  # A `InvalidNode` describes unexpected nodes in a parse tree
  class RunTimeError < LocalException
  end
end

{'fraction_non_alphanumeric': 0.09613554159527889, 'fraction_numerical': 0.0, 'mean_word_length': 2.7395017793594305, 'pattern_counts': {'":': 0, '<': 33, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3272381', 'n_tokens_mistral': 1565, 'n_tokens_neox': 1526, 'n_words': 479}