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_MESSAGES =

RBS:

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

Returns:

  • (Hash[Symbol, ^(IR::grammar_warning) -> String])
{
  undeclared_terminal: ->(warning) { "undeclared terminal #{warning[:symbol]}" },
  unused_terminal: ->(warning) { "unused terminal #{warning[:symbol]}" },
  unused_precedence: ->(warning) { "unused precedence #{warning[:symbol]}" },
  unreachable_terminal: ->(warning) { "declared terminal #{warning[:symbol]} is unreachable" },
  unreachable_nonterminal: ->(warning) { "unreachable nonterminal #{warning[:symbol]}" },
  duplicate_production: lambda do |warning|
    "duplicate production #{warning[:production]} (first defined as #{warning[:original]})"
  end,
  implicit_empty: ->(_warning) { "implicit empty alternative; write %empty to document intent" },
  empty_language: ->(warning) { "start symbol #{warning[:symbol]} derives no terminal sentence" },
  lexer_redos: lambda do |warning|
    subject = warning[:symbol] ? " for #{warning[:symbol]}" : ""
    "lexer pattern#{subject} may exhibit excessive backtracking"
  end
}.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)


147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/ibex/cli/outputs.rb', line 147

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)


116
117
118
# File 'lib/ibex/cli/outputs.rb', line 116

def conflict_count(count, kind)
  "#{count} #{kind} conflict#{'s' unless count == 1}"
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])


77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/ibex/cli/outputs.rb', line 77

def conflict_messages(summary, input_path)
  messages = [] #: Array[String]
  unless summary[:expectation_met]
    messages << "#{input_path}:1:1: #{summary[:sr]} shift/reduce conflicts; expected #{summary[:expected_sr]}"
  end
  if summary.key?(:rr_expectation_met)
    unless summary[:rr_expectation_met]
      messages << "#{input_path}:1:1: #{summary[:rr]} reduce/reduce conflicts; expected #{summary[:expected_rr]}"
    end
  elsif summary[:rr].positive?
    messages << "#{input_path}:1:1: #{summary[:rr]} reduce/reduce conflicts"
  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)


133
134
135
136
137
138
139
# File 'lib/ibex/cli/outputs.rb', line 133

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) ⇒ String

RBS:

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

Parameters:

  • warning (IR::grammar_warning)
  • input_path (String)

Returns:

  • (String)


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

def format_grammar_warning(warning, input_path)
  location = warning[:loc]
  rendered = if location
               "#{location[:file] || input_path}:#{location[:line] || 1}:#{location[:column] || 1}"
             else
               "#{input_path}:1:1"
             end
  formatter = WARNING_MESSAGES.fetch(warning[:type]) { ->(item) { item[:type].to_s.tr("_", " ") } }
  "#{rendered}: warning: #{formatter.call(warning)}"
end

#handle_grammar_warnings(grammar, input_path) ⇒ void

This method returns an undefined value.

RBS:

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

Parameters:



41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/ibex/cli/outputs.rb', line 41

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

  messages = grammar.warnings.map { |warning| format_grammar_warning(warning, input_path) }
  if categories.include?(:error)
    promoted = messages.map { |message| message.sub(": warning:", ": warning treated as error:") }
    raise Ibex::Error, promoted.join("\n")
  end

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

#ir_file_mode(path) ⇒ Integer

RBS:

  • (String path) -> Integer

Parameters:

  • path (String)

Returns:

  • (Integer)


165
166
167
168
169
# File 'lib/ibex/cli/outputs.rb', line 165

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:



67
68
69
70
71
72
73
74
# File 'lib/ibex/cli/outputs.rb', line 67

def report_conflicts(automaton, input_path)
  messages = conflict_messages(automaton.conflict_summary, input_path)
  if @options[:warnings]&.include?(:error) && messages.any?
    raise Ibex::Error, messages.map { |message| "#{message}; conflict treated as error" }.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)


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

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

#suggest_ielr(automaton, input_path) ⇒ void

This method returns an undefined value.

RBS:

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

Parameters:



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

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

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

  ielr = LALR::Builder.new(
    automaton.grammar, algorithm: :ielr, entry_isolation: @options[:entry_isolation] == true
  ).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?

  @stderr.puts("#{input_path}:1:1: note: --algorithm=ielr avoids #{avoided.join(' and ')}; " \
               "consider --algorithm=ielr")
end

#warning_categories(value) ⇒ Array[Symbol]

RBS:

  • (String value) -> Array[Symbol]

Parameters:

  • value (String)

Returns:

  • (Array[Symbol])


29
30
31
32
33
34
35
36
37
38
# File 'lib/ibex/cli/outputs.rb', line 29

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:



121
122
123
124
125
126
127
128
129
130
# File 'lib/ibex/cli/outputs.rb', line 121

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