Class: Ibex::Frontend::Formatter
- Inherits:
-
Object
- Object
- Ibex::Frontend::Formatter
- Defined in:
- lib/ibex/frontend/formatter.rb,
sig/ibex/frontend/formatter.rbs
Overview
Deterministically formats grammar trivia and rejects any semantic change. rubocop:disable Metrics/ClassLength -- token layout policy stays together so adjacency rules remain auditable.
Constant Summary collapse
- DECLARATION_STARTS =
%i[ PRAGMA INCLUDE IMPORT TOKEN PRECHIGH PRECLOW OPTIONS EXPECT EXPECT_RR START RECOVER ON_ERROR_REDUCE TEST LEXER CONVERT DISPLAY TYPE PARAM PRINTER RULE ].freeze
- ASSOCIATIONS =
%i[LEFT RIGHT NONASSOC].freeze
- CALLABLES =
%i[LHS PARAMETERIZED_REFERENCE SEPARATED_LIST SEPARATED_NONEMPTY_LIST].freeze
- SUFFIXES =
%w[? * +].freeze
- CLOSERS =
%w[) , ;].freeze
- ADJACENT_AFTER =
["::", "("].freeze
Class Method Summary collapse
Instance Method Summary collapse
- #adjacent_tokens?(previous_external, current_external, previous_role, current_role) ⇒ Boolean
- #advance_conversion_state(external, role, state) ⇒ void
- #advance_precedence_state(external, role, state) ⇒ void
- #advance_role_state(external, role, state) ⇒ void
- #advance_rule_state(external, role, state) ⇒ void
- #annotate_comment_indentation(elements) ⇒ void
- #annotate_roles(elements) ⇒ void
- #append_semantic_children(pending, left, right) ⇒ void
- #build_elements(document) ⇒ Array[formatter_element]
- #comment?(segment) ⇒ Boolean
- #comment_separator(previous, current, gap) ⇒ String
- #comparable_semantic_hashes?(left, right) ⇒ Boolean
- #comparable_semantic_values?(left, right) ⇒ Boolean
- #direct_opaque_pair?(previous, current) ⇒ Boolean
- #document_token_role(external, state) ⇒ Symbol?
- #external_types(tokens) ⇒ Hash[Integer, external_token]
- #format(source, file: "(grammar)") ⇒ String
- #indentation(element) ⇒ Integer
-
#initialize(mode: :default) ⇒ Formatter
constructor
A new instance of Formatter.
- #leading_separator(current, gap) ⇒ String
- #line_boundary?(previous_role, current_role, previous, current) ⇒ Boolean
- #newline_in?(gap) ⇒ Boolean
- #precedence_role(external, state) ⇒ Symbol
- #render(document) ⇒ String
- #render_separator(kind, gap, indentation) ⇒ String
- #rule_token_role(external, state) ⇒ Symbol?
- #same_semantic_projection?(left, right) ⇒ Boolean
- #section_end_role(state) ⇒ Symbol?
- #semantic_keys(value) ⇒ Array[untyped]
- #semantic_members(value) ⇒ Array[Symbol]
- #separator(previous, current) ⇒ String
- #separator_kind(previous, current) ⇒ Symbol
- #text_ends_with_newline?(text) ⇒ Boolean
- #token_role(external, state) ⇒ Symbol?
- #trailing_separator(previous, gap) ⇒ String
Constructor Details
#initialize(mode: :default) ⇒ Formatter
Returns a new instance of Formatter.
32 33 34 35 36 |
# File 'lib/ibex/frontend/formatter.rb', line 32 def initialize(mode: :default) raise ArgumentError, "mode must be :default or :extended" unless %i[default extended].include?(mode) @mode = mode end |
Class Method Details
.format(source, file: "(grammar)", mode: :default) ⇒ String
27 28 29 |
# File 'lib/ibex/frontend/formatter.rb', line 27 def self.format(source, file: "(grammar)", mode: :default) new(mode: mode).format(source, file: file) end |
Instance Method Details
#adjacent_tokens?(previous_external, current_external, previous_role, current_role) ⇒ Boolean
298 299 300 301 302 303 304 305 |
# File 'lib/ibex/frontend/formatter.rb', line 298 def adjacent_tokens?(previous_external, current_external, previous_role, current_role) return true if current_role == :named_reference || previous_role == :named_reference return true if current_external == "::" return true if CLOSERS.include?(current_external) || SUFFIXES.include?(current_external) return true if ADJACENT_AFTER.include?(previous_external) current_external == "(" && CALLABLES.include?(previous_external) end |
#advance_conversion_state(external, role, state) ⇒ void
This method returns an undefined value.
202 203 204 205 206 207 208 209 210 211 212 213 |
# File 'lib/ibex/frontend/formatter.rb', line 202 def advance_conversion_state(external, role, state) if external == :CONVERT state[:conversion] = true state[:conversion_name] = true elsif role == :conversion_end state[:conversion] = false elsif state.fetch(:conversion) && role == :conversion_entry state[:conversion_name] = false elsif state.fetch(:conversion) && !state.fetch(:conversion_name) state[:conversion_name] = true end end |
#advance_precedence_state(external, role, state) ⇒ void
This method returns an undefined value.
193 194 195 196 197 198 199 |
# File 'lib/ibex/frontend/formatter.rb', line 193 def advance_precedence_state(external, role, state) if role == :declaration && %i[PRECHIGH PRECLOW].include?(external) state[:precedence_closer] = external == :PRECHIGH ? :PRECLOW : :PRECHIGH elsif role == :precedence_end state[:precedence_closer] = nil end end |
#advance_role_state(external, role, state) ⇒ void
This method returns an undefined value.
186 187 188 189 190 |
# File 'lib/ibex/frontend/formatter.rb', line 186 def advance_role_state(external, role, state) advance_precedence_state(external, role, state) advance_conversion_state(external, role, state) advance_rule_state(external, role, state) end |
#advance_rule_state(external, role, state) ⇒ void
This method returns an undefined value.
216 217 218 219 220 221 222 |
# File 'lib/ibex/frontend/formatter.rb', line 216 def advance_rule_state(external, role, state) state[:section] = :rules if external == :RULE state[:rule_colon] = true if external == :LHS state[:rule_colon] = false if role == :rule_colon state[:depth] += 1 if external == "(" state[:depth] -= 1 if external == ")" end |
#annotate_comment_indentation(elements) ⇒ void
This method returns an undefined value.
121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/ibex/frontend/formatter.rb', line 121 def annotate_comment_indentation(elements) following_role = nil #: Symbol? elements.reverse_each do |element| segment = element.fetch(:segment) if comment?(segment) element[:role] = :rule_comment if following_role == :rule_start element[:role] = :alternative_comment if following_role == :alternative elsif element.fetch(:external) following_role = element.fetch(:role) end end end |
#annotate_roles(elements) ⇒ void
This method returns an undefined value.
105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/ibex/frontend/formatter.rb', line 105 def annotate_roles(elements) state = { section: :declarations, precedence_closer: nil, conversion: false, conversion_name: true, depth: 0, rule_colon: false } #: Hash[Symbol, untyped] elements.each do |element| external = element.fetch(:external) next unless external element[:role] = token_role(external, state) advance_role_state(external, element.fetch(:role), state) end annotate_comment_indentation(elements) end |
#append_semantic_children(pending, left, right) ⇒ void
This method returns an undefined value.
384 385 386 387 388 389 390 391 392 393 |
# File 'lib/ibex/frontend/formatter.rb', line 384 def append_semantic_children(pending, left, right) case left when Struct semantic_members(left).reverse_each { |member| pending << [left[member], right[member]] } when Array left.each_index.reverse_each { |index| pending << [left[index], right[index]] } when Hash semantic_keys(left).reverse_each { |key| pending << [left[key], right[key]] } end end |
#build_elements(document) ⇒ Array[formatter_element]
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/ibex/frontend/formatter.rb', line 71 def build_elements(document) external_types = external_types(document.tokens) pending = [] #: Array[Segment] elements = document.cst.each_with_object([]) do |segment, result| if %i[whitespace newline].include?(segment.kind) pending << segment next end result << { segment: segment, external: segment.token_index && external_types[segment.token_index], role: nil, gap: pending } pending = [] end annotate_roles(elements) elements end |
#comment?(segment) ⇒ Boolean
262 263 264 |
# File 'lib/ibex/frontend/formatter.rb', line 262 def comment?(segment) %i[line_comment block_comment].include?(segment.kind) end |
#comment_separator(previous, current, gap) ⇒ String
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 |
# File 'lib/ibex/frontend/formatter.rb', line 267 def comment_separator(previous, current, gap) previous_segment = previous.fetch(:segment) current_segment = current.fetch(:segment) if text_ends_with_newline?(previous_segment.text) newlines = gap.select { |segment| segment.kind == :newline }.map(&:text) return newlines.join + (" " * indentation(current)) end if previous_segment.kind == :line_comment || newline_in?(gap) return render_separator(:line, gap, indentation(current)) end return " " if current_segment.kind == :line_comment || previous_segment.kind == :block_comment || current_segment.kind == :block_comment render_separator(:space, gap, indentation(current)) end |
#comparable_semantic_hashes?(left, right) ⇒ Boolean
375 376 377 378 379 380 381 |
# File 'lib/ibex/frontend/formatter.rb', line 375 def comparable_semantic_hashes?(left, right) return false unless right.is_a?(Hash) left_keys = semantic_keys(left) right_keys = semantic_keys(right) left_keys.length == right_keys.length && left_keys.all? { |key| right_keys.include?(key) } end |
#comparable_semantic_values?(left, right) ⇒ Boolean
366 367 368 369 370 371 372 |
# File 'lib/ibex/frontend/formatter.rb', line 366 def comparable_semantic_values?(left, right) return left.instance_of?(right.class) && left.members == right.members if left.is_a?(Struct) return right.is_a?(Array) && left.length == right.length if left.is_a?(Array) return comparable_semantic_hashes?(left, right) if left.is_a?(Hash) left == right end |
#direct_opaque_pair?(previous, current) ⇒ Boolean
256 257 258 259 |
# File 'lib/ibex/frontend/formatter.rb', line 256 def direct_opaque_pair?(previous, current) (previous.kind == :user_code_marker && current.kind == :user_code_body) || (previous.kind == :user_code_body && current.kind == :user_code_marker) end |
#document_token_role(external, state) ⇒ Symbol?
143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/ibex/frontend/formatter.rb', line 143 def document_token_role(external, state) return :user_code if external == :USER_CODE return precedence_role(external, state) if %i[PRECHIGH PRECLOW].include?(external) return :precedence_level if ASSOCIATIONS.include?(external) && state[:precedence_closer] return section_end_role(state) if external == :END return :conversion_entry if state.fetch(:conversion) && state.fetch(:conversion_name) return :rule_keyword if external == :RULE return :declaration if DECLARATION_STARTS.include?(external) nil end |
#external_types(tokens) ⇒ Hash[Integer, external_token]
93 94 95 96 97 98 99 100 101 102 |
# File 'lib/ibex/frontend/formatter.rb', line 93 def external_types(tokens) adapter = TokenAdapter.new(tokens, extended: @mode == :extended) types = {} #: Hash[Integer, external_token] index = 0 while (classified = adapter.next_token) types[index] = classified.fetch(0) index += 1 end types end |
#format(source, file: "(grammar)") ⇒ String
39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
# File 'lib/ibex/frontend/formatter.rb', line 39 def format(source, file: "(grammar)") document = Parser.new(source, file: file, mode: @mode).parse_source_document formatted = render(document) reparsed = Parser.new(formatted, file: file, mode: @mode).parse_source_document unless same_semantic_projection?(document.ast, reparsed.ast) raise Ibex::Error, "#{file}: formatting would change grammar semantics" end formatted rescue Ibex::Error raise rescue StandardError => e raise Ibex::Error, "#{file}: formatting failed: #{e.}" end |
#indentation(element) ⇒ Integer
321 322 323 324 325 326 327 |
# File 'lib/ibex/frontend/formatter.rb', line 321 def indentation(element) case element.fetch(:role) when :rule_start, :precedence_level, :conversion_entry, :rule_comment then 2 when :alternative, :alternative_comment then 4 else 0 end end |
#leading_separator(current, gap) ⇒ String
240 241 242 243 244 |
# File 'lib/ibex/frontend/formatter.rb', line 240 def leading_separator(current, gap) return "" unless newline_in?(gap) render_separator(:line, gap, indentation(current)) end |
#line_boundary?(previous_role, current_role, previous, current) ⇒ Boolean
309 310 311 312 313 314 315 316 317 318 |
# File 'lib/ibex/frontend/formatter.rb', line 309 def line_boundary?(previous_role, current_role, previous, current) return false if current_role == :rule_start && previous.fetch(:external) == :INLINE return true if %i[declaration rule_keyword precedence_level precedence_end conversion_entry conversion_end rule_start alternative grammar_end user_code].include?(current_role) return true if previous_role == :rule_keyword return false unless current.fetch(:segment).kind == :user_code_marker previous.fetch(:segment).kind != :user_code_body end |
#newline_in?(gap) ⇒ Boolean
340 341 342 |
# File 'lib/ibex/frontend/formatter.rb', line 340 def newline_in?(gap) gap.any? { |segment| segment.kind == :newline } end |
#precedence_role(external, state) ⇒ Symbol
179 180 181 182 183 |
# File 'lib/ibex/frontend/formatter.rb', line 179 def precedence_role(external, state) return :precedence_end if state[:precedence_closer] == external :declaration end |
#render(document) ⇒ String
57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/ibex/frontend/formatter.rb', line 57 def render(document) @default_newline = document.source.match(/\r\n|\n/)&.[](0) || "\n" elements = build_elements(document) output = +"" previous = nil #: formatter_element? elements.each do |current| output << separator(previous, current) output << current.fetch(:segment).text previous = current end output end |
#render_separator(kind, gap, indentation) ⇒ String
330 331 332 333 334 335 336 337 |
# File 'lib/ibex/frontend/formatter.rb', line 330 def render_separator(kind, gap, indentation) return "" if kind == :none return " " if kind == :space newlines = gap.select { |segment| segment.kind == :newline }.map(&:text) newlines = [@default_newline] if newlines.empty? newlines.join + (" " * indentation) end |
#rule_token_role(external, state) ⇒ Symbol?
164 165 166 167 168 169 170 171 172 173 174 175 176 |
# File 'lib/ibex/frontend/formatter.rb', line 164 def rule_token_role(external, state) return :rule_start if external == :INLINE && state.fetch(:section) == :rules return :rule_start if external == :LHS return :alternative if external == "|" && state.fetch(:section) == :rules && state.fetch(:depth).zero? if external == ":" return :rule_colon if state.fetch(:rule_colon) && state.fetch(:depth).zero? return :named_reference end nil end |
#same_semantic_projection?(left, right) ⇒ Boolean
350 351 352 353 354 355 356 357 358 359 360 361 362 363 |
# File 'lib/ibex/frontend/formatter.rb', line 350 def same_semantic_projection?(left, right) pending = [[left, right]] #: Array[[untyped, untyped]] until pending.empty? pair = pending.pop next unless pair left_value, right_value = pair next if left_value.equal?(right_value) return false unless comparable_semantic_values?(left_value, right_value) append_semantic_children(pending, left_value, right_value) end true end |
#section_end_role(state) ⇒ Symbol?
156 157 158 159 160 161 |
# File 'lib/ibex/frontend/formatter.rb', line 156 def section_end_role(state) return :conversion_end if state.fetch(:conversion) return :grammar_end if state.fetch(:section) == :rules nil end |
#semantic_keys(value) ⇒ Array[untyped]
401 402 403 |
# File 'lib/ibex/frontend/formatter.rb', line 401 def semantic_keys(value) value.keys.reject { |key| %i[loc span].include?(key) } end |
#semantic_members(value) ⇒ Array[Symbol]
396 397 398 |
# File 'lib/ibex/frontend/formatter.rb', line 396 def semantic_members(value) value.members.reject { |member| %i[loc span].include?(member) } end |
#separator(previous, current) ⇒ String
225 226 227 228 229 230 231 232 233 234 235 236 237 |
# File 'lib/ibex/frontend/formatter.rb', line 225 def separator(previous, current) gap = current.fetch(:gap) return leading_separator(current, gap) unless previous previous_segment = previous.fetch(:segment) current_segment = current.fetch(:segment) return "" if direct_opaque_pair?(previous_segment, current_segment) return trailing_separator(previous, gap) if current_segment.kind == :eof return comment_separator(previous, current, gap) if comment?(previous_segment) || comment?(current_segment) kind = separator_kind(previous, current) render_separator(kind, gap, indentation(current)) end |
#separator_kind(previous, current) ⇒ Symbol
284 285 286 287 288 289 290 291 292 293 294 |
# File 'lib/ibex/frontend/formatter.rb', line 284 def separator_kind(previous, current) previous_external = previous.fetch(:external) current_external = current.fetch(:external) previous_role = previous.fetch(:role) current_role = current.fetch(:role) return :line if line_boundary?(previous_role, current_role, previous, current) return :none if adjacent_tokens?(previous_external, current_external, previous_role, current_role) :space end |
#text_ends_with_newline?(text) ⇒ Boolean
345 346 347 |
# File 'lib/ibex/frontend/formatter.rb', line 345 def text_ends_with_newline?(text) text.end_with?("\n") end |
#token_role(external, state) ⇒ Symbol?
135 136 137 138 139 140 |
# File 'lib/ibex/frontend/formatter.rb', line 135 def token_role(external, state) document_role = document_token_role(external, state) return document_role if document_role rule_token_role(external, state) end |
#trailing_separator(previous, gap) ⇒ String
247 248 249 250 251 252 253 |
# File 'lib/ibex/frontend/formatter.rb', line 247 def trailing_separator(previous, gap) segment = previous.fetch(:segment) return "" if segment.kind == :user_code_body return render_separator(:line, gap, 0) if previous.fetch(:role) == :grammar_end render_separator(gap.any? { |item| item.kind == :newline } ? :line : :none, gap, 0) end |