Class: Ibex::Frontend::Formatter

Inherits:
Object
  • Object
show all
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 =

Returns:

  • (Array[Symbol])
%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 =

Signature:

  • Array[Symbol]

Returns:

  • (Array[Symbol])
%i[LEFT RIGHT NONASSOC].freeze
CALLABLES =

Signature:

  • Array[Symbol]

Returns:

  • (Array[Symbol])
%i[LHS PARAMETERIZED_REFERENCE SEPARATED_LIST SEPARATED_NONEMPTY_LIST].freeze
SUFFIXES =

Signature:

  • Array[Symbol]

Returns:

  • (Array[String])
%w[? * +].freeze
CLOSERS =

Signature:

  • Array[String]

Returns:

  • (Array[String])
%w[) , ;].freeze
ADJACENT_AFTER =

Signature:

  • Array[String]

Returns:

  • (Array[String])
["::", "("].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(mode: :default) ⇒ Formatter

Returns a new instance of Formatter.

RBS:

  • (?mode: Symbol) -> void

Parameters:

  • mode: (Symbol) (defaults to: :default)


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

RBS:

  • (String source, ?file: String, ?mode: Symbol) -> String

Parameters:

  • source (String)
  • file: (String) (defaults to: "(grammar)")
  • mode: (Symbol) (defaults to: :default)

Returns:

  • (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

RBS:

  • (external_token? previous_external, external_token? current_external, Symbol? previous_role, Symbol? current_role) -> bool

Parameters:

  • previous_external (external_token, nil)
  • current_external (external_token, nil)
  • previous_role (Symbol, nil)
  • current_role (Symbol, nil)

Returns:

  • (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.

RBS:

  • (external_token external, Symbol? role, Hash[Symbol, untyped] state) -> void

Parameters:

  • external (external_token)
  • role (Symbol, nil)
  • state (Hash[Symbol, untyped])


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.

RBS:

  • (external_token external, Symbol? role, Hash[Symbol, untyped] state) -> void

Parameters:

  • external (external_token)
  • role (Symbol, nil)
  • state (Hash[Symbol, untyped])


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.

RBS:

  • (external_token external, Symbol? role, Hash[Symbol, untyped] state) -> void

Parameters:

  • external (external_token)
  • role (Symbol, nil)
  • state (Hash[Symbol, untyped])


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.

RBS:

  • (external_token external, Symbol? role, Hash[Symbol, untyped] state) -> void

Parameters:

  • external (external_token)
  • role (Symbol, nil)
  • state (Hash[Symbol, untyped])


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.

RBS:

  • (Array[formatter_element] elements) -> void

Parameters:

  • elements (Array[formatter_element])


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.

RBS:

  • (Array[formatter_element] elements) -> void

Parameters:

  • elements (Array[formatter_element])


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.

RBS:

  • (Array[[untyped, untyped]] pending, untyped left, untyped right) -> void

Parameters:

  • pending (Array[[ untyped, untyped ]])
  • left (Object)
  • right (Object)


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]

RBS:

  • (SourceDocument document) -> Array[formatter_element]

Parameters:

Returns:

  • (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

RBS:

  • (Segment segment) -> bool

Parameters:

Returns:

  • (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

RBS:

  • (formatter_element previous, formatter_element current, Array[Segment] gap) -> String

Parameters:

  • previous (formatter_element)
  • current (formatter_element)
  • gap (Array[Segment])

Returns:

  • (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

RBS:

  • (Hash[untyped, untyped] left, untyped right) -> bool

Parameters:

  • left (Hash[untyped, untyped])
  • right (Object)

Returns:

  • (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

RBS:

  • (untyped left, untyped right) -> bool

Parameters:

  • left (Object)
  • right (Object)

Returns:

  • (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

RBS:

  • (Segment previous, Segment current) -> bool

Parameters:

Returns:

  • (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?

RBS:

  • (external_token external, Hash[Symbol, untyped] state) -> Symbol?

Parameters:

  • external (external_token)
  • state (Hash[Symbol, untyped])

Returns:

  • (Symbol, nil)


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]

RBS:

  • (Array[Token] tokens) -> Hash[Integer, external_token]

Parameters:

Returns:

  • (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

RBS:

  • (String source, ?file: String) -> String

Parameters:

  • source (String)
  • file: (String) (defaults to: "(grammar)")

Returns:

  • (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.message}"
end

#indentation(element) ⇒ Integer

RBS:

  • (formatter_element element) -> Integer

Parameters:

  • element (formatter_element)

Returns:

  • (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

RBS:

  • (formatter_element current, Array[Segment] gap) -> String

Parameters:

  • current (formatter_element)
  • gap (Array[Segment])

Returns:

  • (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

RBS:

  • (Symbol? previous_role, Symbol? current_role, formatter_element previous, formatter_element current) -> bool

Parameters:

  • previous_role (Symbol, nil)
  • current_role (Symbol, nil)
  • previous (formatter_element)
  • current (formatter_element)

Returns:

  • (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

RBS:

  • (Array[Segment] gap) -> bool

Parameters:

Returns:

  • (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

RBS:

  • (external_token external, Hash[Symbol, untyped] state) -> Symbol

Parameters:

  • external (external_token)
  • state (Hash[Symbol, untyped])

Returns:

  • (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

RBS:

  • (SourceDocument document) -> String

Parameters:

Returns:

  • (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

RBS:

  • (Symbol kind, Array[Segment] gap, Integer indentation) -> String

Parameters:

  • kind (Symbol)
  • gap (Array[Segment])
  • indentation (Integer)

Returns:

  • (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?

RBS:

  • (external_token external, Hash[Symbol, untyped] state) -> Symbol?

Parameters:

  • external (external_token)
  • state (Hash[Symbol, untyped])

Returns:

  • (Symbol, nil)


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

RBS:

  • (untyped left, untyped right) -> bool

Parameters:

  • left (Object)
  • right (Object)

Returns:

  • (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?

RBS:

  • (Hash[Symbol, untyped] state) -> Symbol?

Parameters:

  • state (Hash[Symbol, untyped])

Returns:

  • (Symbol, nil)


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]

RBS:

  • (Hash[untyped, untyped] value) -> Array[untyped]

Parameters:

  • value (Hash[untyped, untyped])

Returns:

  • (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]

RBS:

  • (Struct[untyped] value) -> Array[Symbol]

Parameters:

  • value (Struct[untyped])

Returns:

  • (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

RBS:

  • (formatter_element? previous, formatter_element current) -> String

Parameters:

  • previous (formatter_element, nil)
  • current (formatter_element)

Returns:

  • (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

RBS:

  • (formatter_element previous, formatter_element current) -> Symbol

Parameters:

  • previous (formatter_element)
  • current (formatter_element)

Returns:

  • (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

RBS:

  • (String text) -> bool

Parameters:

  • text (String)

Returns:

  • (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?

RBS:

  • (external_token external, Hash[Symbol, untyped] state) -> Symbol?

Parameters:

  • external (external_token)
  • state (Hash[Symbol, untyped])

Returns:

  • (Symbol, nil)


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

RBS:

  • (formatter_element previous, Array[Segment] gap) -> String

Parameters:

  • previous (formatter_element)
  • gap (Array[Segment])

Returns:

  • (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