Skip to content

Commit

Permalink
Merge pull request #304 from yui-knk/better_error_message
Browse files Browse the repository at this point in the history
Better error message
  • Loading branch information
yui-knk authored Dec 23, 2023
2 parents 19a76c7 + bac9652 commit 2ae2485
Show file tree
Hide file tree
Showing 12 changed files with 420 additions and 152 deletions.
11 changes: 9 additions & 2 deletions lib/lrama/grammar/rule_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,15 @@ def numberize_references
else
candidates = rhs.each_with_index.select {|token, i| token.referred_by?(ref_name) }

raise "Referring symbol `#{ref_name}` is duplicated. #{token}" if candidates.size >= 2
raise "Referring symbol `#{ref_name}` is not found. #{token}" unless referring_symbol = candidates.first
if candidates.size >= 2
location = token.location.partial_location(ref.first_column, ref.last_column)
raise location.generate_error_message("Referring symbol `#{ref_name}` is duplicated.")
end

unless referring_symbol = candidates.first
location = token.location.partial_location(ref.first_column, ref.last_column)
raise location.generate_error_message("Referring symbol `#{ref_name}` is not found.")
end

ref.index = referring_symbol[1] + 1
end
Expand Down
4 changes: 3 additions & 1 deletion lib/lrama/lexer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ class Lexer
%rule
)

def initialize(text)
def initialize(path, text)
@path = path
@scanner = StringScanner.new(text)
@head_column = @head = @scanner.pos
@head_line = @line = 1
Expand All @@ -58,6 +59,7 @@ def column

def location
Location.new(
grammar_file_path: @path,
first_line: @head_line, first_column: @head_column,
last_line: line, last_column: column
)
Expand Down
75 changes: 72 additions & 3 deletions lib/lrama/lexer/location.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
module Lrama
class Lexer
class Location
attr_reader :first_line, :first_column, :last_line, :last_column
attr_reader :grammar_file_path, :first_line, :first_column, :last_line, :last_column

def initialize(first_line:, first_column:, last_line:, last_column:)
def initialize(grammar_file_path:, first_line:, first_column:, last_line:, last_column:)
@grammar_file_path = grammar_file_path
@first_line = first_line
@first_column = first_column
@last_line = last_line
Expand All @@ -12,14 +13,82 @@ def initialize(first_line:, first_column:, last_line:, last_column:)

def ==(other)
self.class == other.class &&
self.grammar_file_path == other.grammar_file_path &&
self.first_line == other.first_line &&
self.first_column == other.first_column &&
self.last_line == other.last_line &&
self.last_column == other.last_column
end

def partial_location(left, right)
offset = -first_column
new_first_line = -1
new_first_column = -1
new_last_line = -1
new_last_column = -1

_text.each.with_index do |line, index|
new_offset = offset + line.length + 1

if offset <= left && left <= new_offset
new_first_line = first_line + index
new_first_column = left - offset
end

if offset <= right && right <= new_offset
new_last_line = first_line + index
new_last_column = right - offset
end

offset = new_offset
end

Location.new(
grammar_file_path: grammar_file_path,
first_line: new_first_line, first_column: new_first_column,
last_line: new_last_line, last_column: new_last_column
)
end

def to_s
"(#{first_line},#{first_column})-(#{last_line},#{last_column})"
"#{grammar_file_path} (#{first_line},#{first_column})-(#{last_line},#{last_column})"
end

def generate_error_message(error_message)
<<~ERROR.chomp
#{grammar_file_path}:#{first_line}:#{first_column}: #{error_message}
#{line_with_carrets}
ERROR
end

def line_with_carrets
<<~TEXT
#{text}
#{carrets}
TEXT
end

private

def blanks
(text[0...first_column] or raise "#{first_column} is invalid").gsub(/[^\t]/, ' ')
end

def carrets
blanks + '^' * (last_column - first_column)
end

def text
_text.join("\n")
end

def _text
return @_text if @_text

offset = 0

@_text = File.read(grammar_file_path).split("\n")[(first_line - 1)...last_line]
@_text
end
end
end
Expand Down
40 changes: 9 additions & 31 deletions lib/lrama/parser.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 9 additions & 31 deletions parser.y
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,7 @@ end

def parse
report_duration(:parse) do
@lexer = Lrama::Lexer.new(@text)
@lexer = Lrama::Lexer.new(@path, @text)
@grammar = Lrama::Grammar.new(@rule_counter)
@precedence_number = 0
reset_precs
Expand All @@ -527,34 +527,26 @@ end

def on_error(error_token_id, error_value, value_stack)
if error_value.is_a?(Lrama::Lexer::Token)
line = error_value.first_line
first_column = error_value.first_column
last_column = error_value.last_column
location = error_value.location
value = "'#{error_value.s_value}'"
else
line = @lexer.line
first_column = @lexer.head_column
last_column = @lexer.column
location = @lexer.location
value = error_value.inspect
end
error_message = "parse error on value #{value} (#{token_to_str(error_token_id) || '?'})"
raise_parse_error(error_message, line, first_column, last_column)
raise_parse_error(error_message, location)
end
def on_action_error(error_message, error_value)
if error_value.is_a?(Lrama::Lexer::Token)
line = error_value.first_line
first_column = error_value.first_column
last_column = error_value.last_column
location = error_value.location
else
line = @lexer.line
first_column = @lexer.head_column
last_column = @lexer.column
location = @lexer.location
end
raise_parse_error(error_message, line, first_column, last_column)
raise_parse_error(error_message, location)
end
private
Expand All @@ -574,20 +566,6 @@ def end_c_declaration
@lexer.end_symbol = nil
end
def raise_parse_error(error_message, line, first_column, last_column)
text = @text.split("\n")[line - 1]
raise ParseError, <<~ERROR
#{@path}:#{line}:#{first_column}: #{error_message}
#{text}
#{carrets(text, first_column, last_column)}
ERROR
end
def blanks(text, first_column)
text[0...first_column].gsub(/[^\t]/, ' ')
end
def carrets(text, first_column, last_column)
blanks(text, first_column) + '^' * (last_column - first_column)
def raise_parse_error(error_message, location)
raise ParseError, location.generate_error_message(error_message)
end
13 changes: 12 additions & 1 deletion sig/lrama/lexer/location.rbs
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
module Lrama
class Lexer
class Location
attr_reader grammar_file_path: String
attr_reader first_line: Integer
attr_reader first_column: Integer
attr_reader last_line: Integer
attr_reader last_column: Integer

def initialize: (first_line: Integer, first_column: Integer, last_line: Integer, last_column: Integer) -> void
def initialize: (grammar_file_path: String, first_line: Integer, first_column: Integer, last_line: Integer, last_column: Integer) -> void

def ==: (Location other) -> bool
def partial_location: (Integer, Integer) -> Location
def generate_error_message: (String) -> String
def line_with_carrets: () -> String

private

def blanks: () -> String
def carrets: () -> String
def text: () -> String
def _text: () -> Array[String]
end
end
end
Loading

0 comments on commit 2ae2485

Please sign in to comment.