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 PARSER 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_parser_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
- #nested_declaration_role(external, state) ⇒ Symbol?
- #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[Object?]
- #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.
43 44 45 46 47 |
# File 'lib/ibex/frontend/formatter.rb', line 43 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
38 39 40 |
# File 'lib/ibex/frontend/formatter.rb', line 38 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
333 334 335 336 337 338 339 340 |
# File 'lib/ibex/frontend/formatter.rb', line 333 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.
237 238 239 240 241 242 243 244 245 246 247 248 |
# File 'lib/ibex/frontend/formatter.rb', line 237 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_parser_state(external, role, state) ⇒ void
This method returns an undefined value.
216 217 218 219 220 221 222 223 224 225 |
# File 'lib/ibex/frontend/formatter.rb', line 216 def advance_parser_state(external, role, state) if external == :PARSER state[:parser] = true state[:parser_key] = true elsif role == :parser_end state[:parser] = false elsif state.fetch(:parser) state[:parser_key] = !state.fetch(:parser_key) end end |
#advance_precedence_state(external, role, state) ⇒ void
This method returns an undefined value.
228 229 230 231 232 233 234 |
# File 'lib/ibex/frontend/formatter.rb', line 228 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.
208 209 210 211 212 213 |
# File 'lib/ibex/frontend/formatter.rb', line 208 def advance_role_state(external, role, state) advance_precedence_state(external, role, state) advance_conversion_state(external, role, state) advance_parser_state(external, role, state) advance_rule_state(external, role, state) end |
#advance_rule_state(external, role, state) ⇒ void
This method returns an undefined value.
251 252 253 254 255 256 257 |
# File 'lib/ibex/frontend/formatter.rb', line 251 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.
132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/ibex/frontend/formatter.rb', line 132 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.
116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/ibex/frontend/formatter.rb', line 116 def annotate_roles(elements) state = { section: :declarations, precedence_closer: nil, conversion: false, conversion_name: true, parser: false, parser_key: true, depth: 0, rule_colon: false } #: formatter_state 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.
424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 |
# File 'lib/ibex/frontend/formatter.rb', line 424 def append_semantic_children(pending, left, right) case left when Struct return unless right.is_a?(Struct) semantic_members(left).reverse_each { |member| pending << [left[member], right[member]] } when Array return unless right.is_a?(Array) left.each_index.reverse_each { |index| pending << [left[index], right[index]] } when Hash return unless right.is_a?(Hash) semantic_keys(left).reverse_each { |key| pending << [left[key], right[key]] } end end |
#build_elements(document) ⇒ Array[formatter_element]
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/ibex/frontend/formatter.rb', line 82 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
297 298 299 |
# File 'lib/ibex/frontend/formatter.rb', line 297 def comment?(segment) %i[line_comment block_comment].include?(segment.kind) end |
#comment_separator(previous, current, gap) ⇒ String
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 |
# File 'lib/ibex/frontend/formatter.rb', line 302 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
415 416 417 418 419 420 421 |
# File 'lib/ibex/frontend/formatter.rb', line 415 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
402 403 404 405 406 407 408 409 410 411 412 |
# File 'lib/ibex/frontend/formatter.rb', line 402 def comparable_semantic_values?(left, right) if left.is_a?(Struct) return false unless right.is_a?(Struct) && left.instance_of?(right.class) return left.members == right.members end 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
291 292 293 294 |
# File 'lib/ibex/frontend/formatter.rb', line 291 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?
154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/ibex/frontend/formatter.rb', line 154 def document_token_role(external, state) return :user_code if external == :USER_CODE nested_role = nested_declaration_role(external, state) return nested_role if nested_role 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]
104 105 106 107 108 109 110 111 112 113 |
# File 'lib/ibex/frontend/formatter.rb', line 104 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
50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/ibex/frontend/formatter.rb', line 50 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
357 358 359 360 361 362 363 |
# File 'lib/ibex/frontend/formatter.rb', line 357 def indentation(element) case element.fetch(:role) when :rule_start, :precedence_level, :conversion_entry, :parser_setting, :rule_comment then 2 when :alternative, :alternative_comment then 4 else 0 end end |
#leading_separator(current, gap) ⇒ String
275 276 277 278 279 |
# File 'lib/ibex/frontend/formatter.rb', line 275 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
344 345 346 347 348 349 350 351 352 353 354 |
# File 'lib/ibex/frontend/formatter.rb', line 344 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 parser_setting parser_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 |
#nested_declaration_role(external, state) ⇒ Symbol?
167 168 169 170 171 172 173 174 175 |
# File 'lib/ibex/frontend/formatter.rb', line 167 def nested_declaration_role(external, state) return precedence_role(external, state) if %i[PRECHIGH PRECLOW].include?(external) return :precedence_level if ASSOCIATIONS.include?(external) && state[:precedence_closer] return :parser_end if external == :END && state.fetch(:parser) return :parser_setting if state.fetch(:parser) && state.fetch(:parser_key) return section_end_role(state) if external == :END nil end |
#newline_in?(gap) ⇒ Boolean
376 377 378 |
# File 'lib/ibex/frontend/formatter.rb', line 376 def newline_in?(gap) gap.any? { |segment| segment.kind == :newline } end |
#precedence_role(external, state) ⇒ Symbol
201 202 203 204 205 |
# File 'lib/ibex/frontend/formatter.rb', line 201 def precedence_role(external, state) return :precedence_end if state[:precedence_closer] == external :declaration end |
#render(document) ⇒ String
68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/ibex/frontend/formatter.rb', line 68 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
366 367 368 369 370 371 372 373 |
# File 'lib/ibex/frontend/formatter.rb', line 366 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?
186 187 188 189 190 191 192 193 194 195 196 197 198 |
# File 'lib/ibex/frontend/formatter.rb', line 186 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
386 387 388 389 390 391 392 393 394 395 396 397 398 399 |
# File 'lib/ibex/frontend/formatter.rb', line 386 def same_semantic_projection?(left, right) pending = [[left, right]] #: Array[[Object?, Object?]] 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?
178 179 180 181 182 183 |
# File 'lib/ibex/frontend/formatter.rb', line 178 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[Object?]
447 448 449 |
# File 'lib/ibex/frontend/formatter.rb', line 447 def semantic_keys(value) value.keys.reject { |key| %i[loc span].include?(key) } end |
#semantic_members(value) ⇒ Array[Symbol]
442 443 444 |
# File 'lib/ibex/frontend/formatter.rb', line 442 def semantic_members(value) value.members.reject { |member| %i[loc span extended_loc].include?(member) } end |
#separator(previous, current) ⇒ String
260 261 262 263 264 265 266 267 268 269 270 271 272 |
# File 'lib/ibex/frontend/formatter.rb', line 260 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
319 320 321 322 323 324 325 326 327 328 329 |
# File 'lib/ibex/frontend/formatter.rb', line 319 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
381 382 383 |
# File 'lib/ibex/frontend/formatter.rb', line 381 def text_ends_with_newline?(text) text.end_with?("\n") end |
#token_role(external, state) ⇒ Symbol?
146 147 148 149 150 151 |
# File 'lib/ibex/frontend/formatter.rb', line 146 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
282 283 284 285 286 287 288 |
# File 'lib/ibex/frontend/formatter.rb', line 282 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 |