Module: Ibex::ErrorMessages
- Defined in:
- lib/ibex/error_messages.rb,
lib/ibex/error_messages/parser.rb,
lib/ibex/error_messages/update.rb,
lib/ibex/error_messages/renderer.rb,
lib/ibex/error_messages/parser_v2.rb,
lib/ibex/error_messages/sentence_search.rb
Overview
Parses, validates, and deterministically updates example-keyed syntax error messages.
Defined Under Namespace
Modules: ParserV2 Classes: Document, Entry, Parser, SentenceSearch, Update
Constant Summary collapse
- HEADER_V1 =
"# ibex-messages v1"- HEADER =
"# ibex-messages v2"
Class Method Summary collapse
- .add_uncovered_entries(entries, covered, witnesses, uncovered, next_id) ⇒ Object
- .append_entry(lines, automaton, entry) ⇒ Object
- .append_expected_tokens(lines, automaton, entry) ⇒ Object
- .encode_line(line) ⇒ Object
- .entry_heading(entry) ⇒ Object
- .entry_sort_key(entry) ⇒ Object
- .error_action?(action) ⇒ Boolean
- .error_states(automaton) ⇒ Object
- .expected_tokens(automaton, state) ⇒ Object
- .fail_at(file, line, column, message) ⇒ Object
- .format_error_id(number) ⇒ Object
- .legacy_records_for(document, automaton, file:) ⇒ Object
- .load(path) ⇒ Object
- .messages_for(document, automaton, file:) ⇒ Object
- .migrate_legacy_entries(document, witnesses, classifications, next_id) ⇒ Object
- .next_error_id(document) ⇒ Object
- .ordinary_terminals(automaton) ⇒ Object
- .parse(source, file:) ⇒ Object
- .records_for(document, automaton, file:) ⇒ Object
- .render(automaton, existing: nil, max_tokens: SentenceSearch::DEFAULT_MAX_TOKENS, max_configurations: SentenceSearch::DEFAULT_MAX_CONFIGURATIONS) ⇒ Object
- .render_entries(automaton, entries) ⇒ Object
- .update(automaton, existing: nil, max_tokens: SentenceSearch::DEFAULT_MAX_TOKENS, max_configurations: SentenceSearch::DEFAULT_MAX_CONFIGURATIONS) ⇒ Object
- .update_existing_entries(document, search, witnesses, classifications, next_id) ⇒ Object
- .update_sentence_entry(entry, search, entries, covered, classifications, next_id) ⇒ Object
- .with_error_id(entry, next_id) ⇒ Object
Class Method Details
.add_uncovered_entries(entries, covered, witnesses, uncovered, next_id) ⇒ Object
44 45 46 47 48 49 50 51 52 53 54 55 56 |
# File 'lib/ibex/error_messages/update.rb', line 44 def add_uncovered_entries(entries, covered, witnesses, uncovered, next_id) witnesses.each do |state, witness| next if covered[state] error_id = format_error_id(next_id) next_id += 1 entries << Entry.new( state: state, status: :active, message: nil, line: 1, sentence: witness.tokens, error_id: error_id, entry: witness.entry ) uncovered << "#{error_id} state #{state}: #{witness.tokens.join(' ')}" end end |
.append_entry(lines, automaton, entry) ⇒ Object
18 19 20 21 22 23 24 25 26 27 |
# File 'lib/ibex/error_messages/renderer.rb', line 18 def append_entry(lines, automaton, entry) lines << entry_heading(entry) lines << "## #{entry.error_id}" lines << "# entry: #{entry.entry}" if entry.entry lines << "# state: #{entry.state}" if entry.state append_expected_tokens(lines, automaton, entry) lines << "# Add one or more `| ` lines to customize this error." unless entry. entry.&.split("\n", -1)&.each { |line| lines << "| #{encode_line(line)}" } lines.push("end", "") end |
.append_expected_tokens(lines, automaton, entry) ⇒ Object
38 39 40 41 42 43 |
# File 'lib/ibex/error_messages/renderer.rb', line 38 def append_expected_tokens(lines, automaton, entry) return unless entry.status == :active && entry.state expected = expected_tokens(automaton, automaton.states.fetch(entry.state)) lines << "# expected: #{expected.empty? ? '(none)' : expected.join(', ')}" end |
.encode_line(line) ⇒ Object
46 47 48 |
# File 'lib/ibex/error_messages/renderer.rb', line 46 def encode_line(line) line.gsub("\\", "\\\\").gsub("\t", "\\t").gsub("\r", "\\r") end |
.entry_heading(entry) ⇒ Object
30 31 32 33 34 35 |
# File 'lib/ibex/error_messages/renderer.rb', line 30 def entry_heading(entry) return "legacy-state: #{entry.state}" unless entry.sentence prefix = entry.status == :active ? "sentence" : "unreachable" "#{prefix}: #{entry.sentence.join(' ')}" end |
.entry_sort_key(entry) ⇒ Object
156 157 158 159 |
# File 'lib/ibex/error_messages/update.rb', line 156 def entry_sort_key(entry) rank = { active: 0, unreachable: 1, removed: 2 }.fetch(entry.status) [rank, entry.state || (1 << 62), entry.error_id || ""] end |
.error_action?(action) ⇒ Boolean
149 150 151 |
# File 'lib/ibex/error_messages.rb', line 149 def error_action?(action) action.nil? || action[:type].to_sym == :error end |
.error_states(automaton) ⇒ Object
83 84 85 86 87 88 |
# File 'lib/ibex/error_messages.rb', line 83 def error_states(automaton) terminals = ordinary_terminals(automaton) automaton.states.select do |state| terminals.any? { |terminal| error_action?(state.actions[terminal.id] || state.default_action) } end.sort_by(&:id) end |
.expected_tokens(automaton, state) ⇒ Object
51 52 53 54 55 56 |
# File 'lib/ibex/error_messages/renderer.rb', line 51 def expected_tokens(automaton, state) ordinary_terminals(automaton).filter_map do |terminal| action = state.actions[terminal.id] || state.default_action terminal.display_name || terminal.name unless error_action?(action) end end |
.fail_at(file, line, column, message) ⇒ Object
154 155 156 |
# File 'lib/ibex/error_messages.rb', line 154 def fail_at(file, line, column, ) raise Ibex::Error, "#{file}:#{line}:#{column}: #{}" end |
.format_error_id(number) ⇒ Object
151 152 153 |
# File 'lib/ibex/error_messages/update.rb', line 151 def format_error_id(number) format("E%04d", number) end |
.legacy_records_for(document, automaton, file:) ⇒ Object
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/ibex/error_messages.rb', line 127 def legacy_records_for(document, automaton, file:) valid = error_states(automaton).to_h { |state| [state.id, true] } records = {} #: Hash[Integer, { id: String, message: String }] document.entries.each do |entry| next if entry.status == :removed || !entry. state = entry.state || raise(Ibex::Error, "missing legacy error state") unless valid[state] fail_at(file, entry.line, 1, "unknown error state #{state} for current automaton; run `ibex errors --update`") end records[state] = { id: format_error_id(state + 1), message: entry. } end records.sort.to_h.freeze end |
.load(path) ⇒ Object
77 78 79 |
# File 'lib/ibex/error_messages.rb', line 77 def load(path) parse(File.binread(path), file: path) end |
.messages_for(document, automaton, file:) ⇒ Object
92 93 94 |
# File 'lib/ibex/error_messages.rb', line 92 def (document, automaton, file:) records_for(document, automaton, file: file).transform_values { |record| record.fetch(:message) }.freeze end |
.migrate_legacy_entries(document, witnesses, classifications, next_id) ⇒ Object
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'lib/ibex/error_messages/update.rb', line 107 def migrate_legacy_entries(document, witnesses, classifications, next_id) by_state = document.entries.to_h { |entry| [entry.state, entry] } entries = [] #: Array[Entry] covered = {} #: Hash[Integer, bool] witnesses.each do |state, witness| previous = by_state.delete(state) error_id = format_error_id(next_id) next_id += 1 entries << Entry.new( state: state, status: :active, message: previous&., line: previous&.line || 1, sentence: witness.tokens, error_id: error_id, entry: witness.entry ) covered[state] = true classifications[:uncovered] << "#{error_id} state #{state}: #{witness.tokens.join(' ')}" unless previous end by_state.values.sort_by { |entry| entry.state || -1 }.each do |entry| error_id = format_error_id(next_id) next_id += 1 entries << Entry.new( state: entry.state, status: :removed, message: entry., line: entry.line, error_id: error_id ) classifications[:unreachable] << "#{error_id}: legacy state #{entry.state}" end [entries, covered, next_id] end |
.next_error_id(document) ⇒ Object
142 143 144 145 146 147 148 |
# File 'lib/ibex/error_messages/update.rb', line 142 def next_error_id(document) maximum = document.entries.filter_map do |entry| match = entry.error_id&.match(/\AE([0-9]{4,})\z/) match && Integer(match[1] || "0", 10) end.max (maximum || 0) + 1 end |
.ordinary_terminals(automaton) ⇒ Object
144 145 146 |
# File 'lib/ibex/error_messages.rb', line 144 def ordinary_terminals(automaton) automaton.grammar.terminals.reject { |terminal| terminal.name == "error" } end |
.parse(source, file:) ⇒ Object
71 72 73 |
# File 'lib/ibex/error_messages.rb', line 71 def parse(source, file:) Parser.new(source, file: file).parse end |
.records_for(document, automaton, file:) ⇒ Object
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/ibex/error_messages.rb', line 99 def records_for(document, automaton, file:) return legacy_records_for(document, automaton, file: file) if document.version == 1 search = SentenceSearch.new(automaton) records = {} #: Hash[Integer, { id: String, message: String }] document.entries.each do |entry| next unless entry.status == :active && entry. sentence = entry.sentence || raise(Ibex::Error, "missing active error sentence") state = search.state_for(sentence, entry: entry.entry) unless state fail_at(file, entry.line, 1, "error sentence no longer reaches a syntax error; run `ibex errors --update`") end if records[state] fail_at(file, entry.line, 1, "multiple messages reach error state #{state}; run `ibex errors --update`") end error_id = entry.error_id || raise(Ibex::Error, "missing error id") records[state] = { id: error_id, message: entry. } end records.sort.to_h.freeze end |
.render(automaton, existing: nil, max_tokens: SentenceSearch::DEFAULT_MAX_TOKENS, max_configurations: SentenceSearch::DEFAULT_MAX_CONFIGURATIONS) ⇒ Object
9 10 11 12 13 14 |
# File 'lib/ibex/error_messages/update.rb', line 9 def render(automaton, existing: nil, max_tokens: SentenceSearch::DEFAULT_MAX_TOKENS, max_configurations: SentenceSearch::DEFAULT_MAX_CONFIGURATIONS) update( automaton, existing: existing, max_tokens: max_tokens, max_configurations: max_configurations ).source end |
.render_entries(automaton, entries) ⇒ Object
6 7 8 9 10 11 12 13 14 15 |
# File 'lib/ibex/error_messages/renderer.rb', line 6 def render_entries(automaton, entries) lines = [ HEADER, "# Generated by `ibex errors --update`; sentences and IDs are stable review keys.", "# Edit only `| ` message lines.", "" ] entries.each { |entry| append_entry(lines, automaton, entry) } "#{lines.join("\n").rstrip}\n" end |
.update(automaton, existing: nil, max_tokens: SentenceSearch::DEFAULT_MAX_TOKENS, max_configurations: SentenceSearch::DEFAULT_MAX_CONFIGURATIONS) ⇒ Object
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
# File 'lib/ibex/error_messages/update.rb', line 19 def update(automaton, existing: nil, max_tokens: SentenceSearch::DEFAULT_MAX_TOKENS, max_configurations: SentenceSearch::DEFAULT_MAX_CONFIGURATIONS) document = existing || Document.new(entries: []) search = SentenceSearch.new( automaton, max_tokens: max_tokens, max_configurations: max_configurations ) witnesses = search.all classifications = { uncovered: [], unreachable: [], moved: [] } #: Hash[Symbol, Array[String]] next_id = next_error_id(document) entries, covered, next_id = update_existing_entries( document, search, witnesses, classifications, next_id ) add_uncovered_entries(entries, covered, witnesses, classifications[:uncovered], next_id) entries.sort_by! { |entry| entry_sort_key(entry) } Update.new( source: render_entries(automaton, entries), uncovered: classifications[:uncovered], unreachable: classifications[:unreachable], moved: classifications[:moved] ) end |
.update_existing_entries(document, search, witnesses, classifications, next_id) ⇒ Object
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
# File 'lib/ibex/error_messages/update.rb', line 61 def update_existing_entries(document, search, witnesses, classifications, next_id) return migrate_legacy_entries(document, witnesses, classifications, next_id) if document.version == 1 entries = [] #: Array[Entry] covered = {} #: Hash[Integer, bool] document.entries.each do |entry| if entry.sentence next_id = update_sentence_entry( entry, search, entries, covered, classifications, next_id ) else entries << with_error_id(entry, next_id) next_id += 1 unless entry.error_id end end [entries, covered, next_id] end |
.update_sentence_entry(entry, search, entries, covered, classifications, next_id) ⇒ Object
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/ibex/error_messages/update.rb', line 81 def update_sentence_entry(entry, search, entries, covered, classifications, next_id) error_id = entry.error_id || format_error_id(next_id) next_id += 1 unless entry.error_id sentence = entry.sentence || raise(Ibex::Error, "missing error sentence") state = search.state_for(sentence, entry: entry.entry) unless state entries << Entry.new( state: entry.state, status: :unreachable, message: entry., line: entry.line, sentence: sentence, error_id: error_id, entry: entry.entry ) classifications[:unreachable] << "#{error_id}: #{sentence.join(' ')}" return next_id end classifications[:moved] << "#{error_id}: state #{entry.state} -> #{state}" if entry.state && entry.state != state covered[state] = true entries << Entry.new( state: state, status: :active, message: entry., line: entry.line, sentence: sentence, error_id: error_id, entry: entry.entry ) next_id end |
.with_error_id(entry, next_id) ⇒ Object
134 135 136 137 138 139 |
# File 'lib/ibex/error_messages/update.rb', line 134 def with_error_id(entry, next_id) Entry.new( state: entry.state, status: entry.status, message: entry., line: entry.line, sentence: entry.sentence, error_id: entry.error_id || format_error_id(next_id), entry: entry.entry ) end |