Module: Ibex::CLIOutputs

Included in:
CLI
Defined in:
lib/ibex/cli/outputs.rb,
sig/ibex/cli/outputs.rbs

Overview

Files, warnings, and progress emitted by CLI pipeline stages.

Constant Summary collapse

WARNING_MESSAGE_IDS =

RBS:

  • private def register_artifact: (Symbol, String, String, ?mode: Integer?, ?status: bool) -> Artifact
    private def configuration_value: (String) -> Object?

Returns:

  • (Hash[Symbol, String])
{
  undeclared_terminal: "warning.undeclared_terminal",
  unused_terminal: "warning.unused_terminal",
  unused_precedence: "warning.unused_precedence",
  unreachable_terminal: "warning.unreachable_terminal",
  unreachable_nonterminal: "warning.unreachable_nonterminal",
  duplicate_production: "warning.duplicate_production",
  implicit_empty: "warning.implicit_empty",
  empty_language: "warning.empty_language",
  lexer_redos: "warning.lexer_redos"
}.freeze

Instance Method Summary collapse

Instance Method Details

#atomic_write_ir(path, source) ⇒ void

This method returns an undefined value.

RBS:

  • (String path, String source) -> void

Parameters:

  • path (String)
  • source (String)


174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/ibex/cli/outputs.rb', line 174

def atomic_write_ir(path, source)
  require "tempfile"

  target_path = File.symlink?(path) ? File.realpath(path) : path
  directory = File.dirname(File.expand_path(target_path))
  basename = File.basename(target_path)
  Tempfile.create([".#{basename}.", ".tmp"], directory) do |temporary|
    temporary.binmode
    temporary.write(source)
    temporary.flush
    temporary.fsync
    temporary.chmod(ir_file_mode(target_path))
    temporary.close
    File.rename(temporary.path, target_path)
  end
end

#conflict_count(count, kind) ⇒ String

RBS:

  • (Integer count, String kind) -> String

Parameters:

  • count (Integer)
  • kind (String)

Returns:

  • (String)


142
143
144
145
# File 'lib/ibex/cli/outputs.rb', line 142

def conflict_count(count, kind)
  plural = @language == "en" && count != 1 ? "s" : ""
  Messages.translate("conflict.count", language: @language, count: count, kind: kind, plural: plural)
end

#conflict_messages(summary, input_path) ⇒ Array[String]

RBS:

  • (IR::conflict_summary summary, String input_path) -> Array[String]

Parameters:

  • summary (IR::conflict_summary)
  • input_path (String)

Returns:

  • (Array[String])


86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/ibex/cli/outputs.rb', line 86

def conflict_messages(summary, input_path)
  messages = [] #: Array[String]
  unless summary[:expectation_met]
    detail = Messages.translate(
      "conflict.shift_reduce_expected",
      language: @language,
      actual: summary[:sr],
      expected: summary[:expected_sr]
    )
    messages << "#{input_path}:1:1: #{detail}"
  end
  if summary.key?(:rr_expectation_met)
    unless summary[:rr_expectation_met]
      detail = Messages.translate(
        "conflict.reduce_reduce_expected",
        language: @language,
        actual: summary[:rr],
        expected: summary[:expected_rr]
      )
      messages << "#{input_path}:1:1: #{detail}"
    end
  elsif summary[:rr].positive?
    detail = Messages.translate("conflict.reduce_reduce", language: @language, actual: summary[:rr])
    messages << "#{input_path}:1:1: #{detail}"
  end
  messages
end

#default_output_path(input_path, extension) ⇒ String

RBS:

  • (String input_path, String extension) -> String

Parameters:

  • input_path (String)
  • extension (String)

Returns:

  • (String)


160
161
162
163
164
165
166
# File 'lib/ibex/cli/outputs.rb', line 160

def default_output_path(input_path, extension)
  directory = File.dirname(input_path)
  basename = File.basename(input_path)
  replaced = basename.sub(/\.[^.]+\z/, extension)
  replaced = "#{basename}#{extension}" if replaced == basename
  directory == "." ? replaced : File.join(directory, replaced)
end

#format_grammar_warning(warning, input_path, promoted: false) ⇒ String

RBS:

  • (IR::grammar_warning warning, String input_path, ?promoted: bool) -> String

Parameters:

  • warning (IR::grammar_warning)
  • input_path (String)
  • promoted: (Boolean) (defaults to: false)

Returns:

  • (String)


52
53
54
55
56
57
58
59
60
61
62
# File 'lib/ibex/cli/outputs.rb', line 52

def format_grammar_warning(warning, input_path, promoted: false)
  location = warning[:loc]
  rendered = if location
               "#{location[:file] || input_path}:#{location[:line] || 1}:#{location[:column] || 1}"
             else
               "#{input_path}:1:1"
             end
  label_id = promoted ? "label.warning_as_error" : "label.warning"
  label = Messages.translate(label_id, language: @language)
  "#{rendered}: #{label}: #{grammar_warning_message(warning)}"
end

#grammar_warning_message(warning) ⇒ String

RBS:

  • (IR::grammar_warning warning) -> String

Parameters:

  • warning (IR::grammar_warning)

Returns:

  • (String)


65
66
67
68
69
70
# File 'lib/ibex/cli/outputs.rb', line 65

def grammar_warning_message(warning)
  id = WARNING_MESSAGE_IDS.fetch(warning[:type])
  id = "warning.lexer_redos_symbol" if warning[:type] == :lexer_redos && warning[:symbol]
  values = warning.to_h.transform_values(&:to_s) #: Hash[Symbol, String]
  Messages.translate(id, language: @language, **values)
end

#handle_grammar_warnings(grammar, input_path) ⇒ void

This method returns an undefined value.

RBS:

  • (IR::Grammar grammar, String input_path) -> void

Parameters:



37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/ibex/cli/outputs.rb', line 37

def handle_grammar_warnings(grammar, input_path)
  categories = @options[:warnings]
  return if categories.nil? || categories.include?(:none) || grammar.warnings.empty?

  if categories.include?(:error)
    promoted = grammar.warnings.map do |warning|
      format_grammar_warning(warning, input_path, promoted: true)
    end
    raise Ibex::Error, promoted.join("\n")
  end

  grammar.warnings.each { |warning| @stderr.puts(format_grammar_warning(warning, input_path)) }
end

#ir_file_mode(path) ⇒ Integer

RBS:

  • (String path) -> Integer

Parameters:

  • path (String)

Returns:

  • (Integer)


192
193
194
195
196
# File 'lib/ibex/cli/outputs.rb', line 192

def ir_file_mode(path)
  return File.stat(path).mode & 0o777 if File.exist?(path)

  0o666 & ~File.umask
end

#report_conflicts(automaton, input_path) ⇒ void

This method returns an undefined value.

RBS:

  • (IR::Automaton automaton, String input_path) -> void

Parameters:



73
74
75
76
77
78
79
80
81
82
83
# File 'lib/ibex/cli/outputs.rb', line 73

def report_conflicts(automaton, input_path)
  messages = conflict_messages(automaton.conflict_summary, input_path)
  if @options[:warnings]&.include?(:error) && messages.any?
    promoted = messages.map do |message|
      Messages.translate("conflict.treated_as_error", language: @language, detail: message)
    end
    raise Ibex::Error, promoted.join("\n")
  end

  messages.each { |message| @stderr.puts(message) }
end

#report_status(message) ⇒ void

This method returns an undefined value.

RBS:

  • (String message) -> void

Parameters:

  • message (String)


169
170
171
# File 'lib/ibex/cli/outputs.rb', line 169

def report_status(message)
  @stderr.puts("ibex: #{message}") if @options[:status]
end

#suggest_ielr(automaton, input_path) ⇒ void

This method returns an undefined value.

rubocop:disable Metrics/AbcSize -- the bounded comparison reports each conflict class independently.

RBS:

  • (IR::Automaton automaton, String input_path) -> void

Parameters:



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/ibex/cli/outputs.rb', line 116

def suggest_ielr(automaton, input_path)
  return unless @options[:suggest_ielr]
  return unless automaton.algorithm == "lalr1"
  return unless configuration_value("parser.algorithm") == :lalr

  summary = automaton.conflict_summary
  return if summary[:expectation_met] && summary.fetch(:rr_expectation_met, summary[:rr].zero?)

  ielr = LALR::Builder.new(
    Configuration::AnalysisGrammar.for_algorithm(automaton.grammar, :ielr),
    algorithm: :ielr, entry_isolation: configuration_value("parser.entries") == :isolated
  ).build
  removed_sr = [summary[:sr] - ielr.conflict_summary[:sr], 0].max
  removed_rr = [summary[:rr] - ielr.conflict_summary[:rr], 0].max
  avoided = [] #: Array[String]
  avoided << conflict_count(removed_sr, "shift/reduce") if removed_sr.positive?
  avoided << conflict_count(removed_rr, "reduce/reduce") if removed_rr.positive?
  return if avoided.empty?

  conjunction = @language == "ja" ? "" : " and "
  detail = Messages.translate("note.ielr", language: @language, avoided: avoided.join(conjunction))
  @stderr.puts("#{input_path}:1:1: #{detail}")
end

#warning_categories(value) ⇒ Array[Symbol]

RBS:

  • (String value) -> Array[Symbol]

Parameters:

  • value (String)

Returns:

  • (Array[Symbol])


25
26
27
28
29
30
31
32
33
34
# File 'lib/ibex/cli/outputs.rb', line 25

def warning_categories(value)
  categories = value.split(",").map(&:strip).reject(&:empty?).map(&:to_sym)
  unknown = categories - %i[all error none]
  raise OptionParser::InvalidArgument, "unknown warning category #{unknown.first}" if unknown.any?
  if categories.empty? || (categories.include?(:none) && categories.length > 1)
    raise OptionParser::InvalidArgument, "warning category none cannot be combined"
  end

  categories
end

#write_report(automaton, input_path) ⇒ void

This method returns an undefined value.

RBS:

  • (IR::Automaton automaton, String input_path) -> void

Parameters:



148
149
150
151
152
153
154
155
156
157
# File 'lib/ibex/cli/outputs.rb', line 148

def write_report(automaton, input_path)
  require_relative "../codegen/report"
  path = @options[:log_file] || default_output_path(input_path, ".output")
  report = Codegen::Report.render(
    automaton,
    max_tokens: @options[:counterexample_max_tokens],
    max_configurations: @options[:counterexample_max_configurations]
  )
  register_artifact(:report, path, report, status: true)
end