Class: Ibex::Codegen::RBS

Inherits:
Object
  • Object
show all
Defined in:
lib/ibex/codegen/rbs.rb,
sig/ibex/codegen/rbs.rbs

Overview

Generates an RBS declaration for the public surface of a generated parser. rubocop:disable Metrics/ClassLength -- one generator owns the complete parser signature.

Instance Method Summary collapse

Constructor Details

#initialize(automaton, superclass: nil, omit_action_call: nil) ⇒ RBS

Returns a new instance of RBS.

RBS:

  • (IR::Automaton automaton, ?superclass: String?, ?omit_action_call: bool?) -> void

Parameters:

  • automaton (IR::Automaton)
  • superclass: (String, nil) (defaults to: nil)
  • omit_action_call: (Boolean, nil) (defaults to: nil)


18
19
20
21
22
23
24
# File 'lib/ibex/codegen/rbs.rb', line 18

def initialize(automaton, superclass: nil, omit_action_call: nil)
  @automaton = automaton
  @grammar = automaton.grammar
  @superclass = superclass || @grammar.superclass || "Ibex::Runtime::Parser"
  @omit_action_call = omit_action_call.nil? ? @grammar.options[:omit_action_call] : omit_action_call
  @generated_action_abi = GeneratedActionABI::Cache.new
end

Instance Method Details

#action_method?(production) ⇒ Boolean

RBS:

  • (IR::Production production) -> bool

Parameters:

Returns:

  • (Boolean)


335
336
337
# File 'lib/ibex/codegen/rbs.rb', line 335

def action_method?(production)
  !!(production.node || production.action || !@omit_action_call)
end

#append_action_signature(lines, production) ⇒ void

This method returns an undefined value.

RBS:

  • (Array[String] lines, IR::Production production) -> void

Parameters:



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/ibex/codegen/rbs.rb', line 285

def append_action_signature(lines, production)
  parameters = production.rhs.map { |symbol_id| semantic_type(symbol_id) }.join(", ")
  result = production.node ? "AST::#{production.node.fetch(:name)}" : semantic_type(production.lhs)
  if @generated_action_abi.positional_values?(production)
    lines << "  private def _ibex_action_#{production.id}: (#{parameters}) -> #{result}"
    return
  end
  if @generated_action_abi.values_only?(production)
    lines << "  private def _ibex_action_#{production.id}: ([#{parameters}]) -> #{result}"
    return
  end

  locations = Array.new(production.rhs.length, "untyped").join(", ")
  lookahead = composed_action?(production) ? ", untyped" : ""
  lines << "  private def _ibex_action_#{production.id}: " \
           "([#{parameters}], Array[untyped], [#{locations}], Array[untyped], " \
           "Ibex::Runtime::LocationSpan?#{lookahead}) -> #{result}"
end

#append_actions(lines) ⇒ void

This method returns an undefined value.

RBS:

  • (Array[String] lines) -> void

Parameters:

  • lines (Array[String])


273
274
275
276
277
278
279
280
281
282
# File 'lib/ibex/codegen/rbs.rb', line 273

def append_actions(lines)
  actions = @grammar.productions.select { |production| action_method?(production) }
  return if actions.empty?

  lines << ""
  actions.each do |production|
    append_action_signature(lines, production)
    append_composed_fragment_signatures(lines, production) if composed_action?(production)
  end
end

#append_ast_contract(lines) ⇒ void

This method returns an undefined value.

RBS:

  • (Array[String] lines) -> void

Parameters:

  • lines (Array[String])


46
47
48
49
50
51
52
53
54
55
# File 'lib/ibex/codegen/rbs.rb', line 46

def append_ast_contract(lines)
  definitions = ast_node_definitions
  return if definitions.empty?

  lines << "  module AST"
  definitions.each_value { |node| append_ast_node_contract(lines, node) }
  append_visitor_contract(lines, definitions)
  append_listener_contract(lines, definitions)
  lines.push("  end", "")
end

#append_ast_node_contract(lines, node) ⇒ void

This method returns an undefined value.

RBS:

  • (Array[String] lines, IR::node_annotation node) -> void

Parameters:

  • lines (Array[String])
  • node (IR::node_annotation)


146
147
148
149
150
151
152
153
154
155
156
# File 'lib/ibex/codegen/rbs.rb', line 146

def append_ast_node_contract(lines, node)
  name = node.fetch(:name)
  fields = node.fetch(:fields)
  types = ast_node_field_types(name, fields.length)
  lines << "    class #{name} < Data"
  fields.each_with_index { |field, index| lines << "      attr_reader #{field}: #{types.fetch(index)}" }
  keywords = fields.each_with_index.map { |field, index| "#{field}: #{types.fetch(index)}" }
  lines << "      def self.new: (#{keywords.join(', ')}) -> instance"
  lines << "      def initialize: (#{keywords.join(', ')}) -> void"
  lines << "    end"
end

#append_composed_fragment_signatures(lines, production) ⇒ void

This method returns an undefined value.

RBS:

  • (Array[String] lines, IR::Production production) -> void

Parameters:



305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
# File 'lib/ibex/codegen/rbs.rb', line 305

def append_composed_fragment_signatures(lines, production)
  plan = production.action&.composition&.dig(:plan)
  raise Ibex::Error, "missing action composition plan" unless plan

  plan.fetch(:steps).each_with_index do |step, index|
    next unless step[:code]

    inputs = step.fetch(:inputs).map do |slot|
      composition_slot_type(production, plan, slot, index)
    end
    locations = Array.new(inputs.length, "untyped").join(", ")
    result = step[:result_type] || "untyped"
    lines << "  private def _ibex_inline_fragment_#{production.id}_#{index}: " \
             "([#{inputs.join(', ')}], Array[untyped], [#{locations}], Array[untyped], " \
             "Ibex::Runtime::LocationSpan?) -> #{result}"
  end
end

#append_contract(lines) ⇒ void

This method returns an undefined value.

RBS:

  • (Array[String] lines) -> void

Parameters:

  • lines (Array[String])


198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/ibex/codegen/rbs.rb', line 198

def append_contract(lines)
  lines.push("  PARSER_TABLE_FORMAT_VERSION: Integer",
             "  GRAMMAR_DIGEST: String", "  STATE_COUNT: Integer", "  PRODUCTION_COUNT: Integer",
             "  TOKEN_IDS: Hash[untyped, Integer]", "  TOKEN_NAMES: Hash[Integer, String]",
             "  ACTIONS: untyped", "  GOTOS: untyped", "  DEFAULT_ACTIONS: Array[untyped]",
             "  EAGER_REDUCTIONS: Hash[Integer, [:reduce, Integer]]",
             "  PRODUCTIONS: Array[Hash[Symbol, untyped]]",
             "  ERROR_MESSAGES: Hash[Integer, String | { id: String, message: String }]",
             "  PARSER_TABLES: Hash[Symbol, untyped]", "  DEBUG_ENABLED: bool", "",
             "  def self.parser_tables: () -> Hash[Symbol, untyped]")
  lines << "  SYMBOL_NAMES: Hash[Integer, String]" if @grammar.options[:cst] == true
  lines << "  CST_METADATA: Hash[Symbol, untyped]" if @grammar.options[:cst] == true
  lines << "  CST_KIND_MODEL: Ibex::Runtime::CST::Kind" if @grammar.options[:cst] == true
  append_entry_contract(lines)
  append_parameter_contract(lines)
end

#append_entry_contract(lines) ⇒ void

This method returns an undefined value.

RBS:

  • (Array[String] lines) -> void

Parameters:

  • lines (Array[String])


216
217
218
219
220
221
# File 'lib/ibex/codegen/rbs.rb', line 216

def append_entry_contract(lines)
  return unless @grammar.starts.length > 1

  lines << "  ENTRY_STATES: Hash[Symbol, Integer]"
  @grammar.starts.each { |name| lines << "  def parse_#{name}: () -> untyped" }
end

#append_lexer_contract(lines) ⇒ void

This method returns an undefined value.

RBS:

  • (Array[String] lines) -> void

Parameters:

  • lines (Array[String])


239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/ibex/codegen/rbs.rb', line 239

def append_lexer_contract(lines)
  lexer = @grammar.lexer
  return unless lexer

  lines.push(
    "  LEXER_STATES: Array[String]",
    "  LEXER_RULES_BY_STATE: Hash[Symbol, Array[Hash[Symbol, untyped]]]",
    "",
    "  def lex: (String | IO | Fiber source, ?file: String) -> self",
    "  def parse: (String | IO | Fiber source, ?file: String) -> untyped",
    "  def lexer_state: () -> Symbol",
    "  def lexer_state=: (Symbol | String state) -> Symbol",
    "  def next_token: () -> [untyped, untyped, Hash[Symbol, untyped]]"
  )
  if @grammar.options[:cst] == true
    lines << "  def parse_with_syntax: (String | IO | Fiber source, ?file: String) -> " \
             "Ibex::Runtime::CST::ParseResult"
  end
  lexer.rules.each do |rule|
    lines << "  private def _ibex_lexer_action_#{rule.id}: (String lexeme) -> untyped" if rule.action
  end
end

#append_listener_contract(lines, definitions) ⇒ void

This method returns an undefined value.

RBS:

  • (Array[String] lines, Hash[String, IR::node_annotation] definitions) -> void

Parameters:

  • lines (Array[String])
  • definitions (Hash[String, IR::node_annotation])


178
179
180
181
182
183
184
185
186
187
188
# File 'lib/ibex/codegen/rbs.rb', line 178

def append_listener_contract(lines, definitions)
  lines.push("    class Listener", "      def walk: (untyped node) -> untyped",
             "      def enter: (untyped node) -> void", "      def exit: (untyped node) -> void")
  definitions.each_value do |node|
    name = node.fetch(:name)
    method = ast_method_name(name)
    lines << "      def enter_#{method}: (#{name} node) -> void"
    lines << "      def exit_#{method}: (#{name} node) -> void"
  end
  lines.push("      def listener_children: (untyped node) -> Array[untyped]", "    end")
end

#append_parameter_contract(lines) ⇒ void

This method returns an undefined value.

RBS:

  • (Array[String] lines) -> void

Parameters:

  • lines (Array[String])


224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/ibex/codegen/rbs.rb', line 224

def append_parameter_contract(lines)
  parameters = @grammar.parser_parameters
  return if parameters.empty?

  parameters.each do |parameter|
    lines << "  @#{parameter[:name]}: #{parameter[:semantic_type] || 'untyped'}"
  end
  keywords = parameters.map do |parameter|
    "#{parameter[:name]}: #{parameter[:semantic_type] || 'untyped'}"
  end
  lines << ""
  lines << "  def initialize: (#{keywords.join(', ')}, **untyped) -> void"
end

#append_repetition_contracts(lines, fields) ⇒ void

This method returns an undefined value.

RBS:

  • (Array[String] lines, Hash[String, untyped] fields) -> void

Parameters:

  • lines (Array[String])
  • fields (Hash[String, untyped])


126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/ibex/codegen/rbs.rb', line 126

def append_repetition_contracts(lines, fields)
  repeated = fields.select { |_field, slot| slot.is_a?(Hash) && slot[:extraction] }
  element = "Enumerator[Ibex::Runtime::CST::SyntaxNode | Ibex::Runtime::CST::SyntaxToken, void]"
  repeated.each do |field, slot|
    lines << "      def each_#{field}_element: () -> #{element}"
    lines << "      def each_#{field}_separator: () -> #{element}" if slot.fetch(:extraction) == :separated_list
  end
  return unless repeated.one?

  _field, slot = repeated.first
  lines << "      def each_element: () -> #{element}"
  lines << "      def each_separator: () -> #{element}" if slot.fetch(:extraction) == :separated_list
end

#append_syntax_contract(lines) ⇒ void

This method returns an undefined value.

RBS:

  • (Array[String] lines) -> void

Parameters:

  • lines (Array[String])


58
59
60
61
62
63
64
65
# File 'lib/ibex/codegen/rbs.rb', line 58

def append_syntax_contract(lines)
  definitions = syntax_node_definitions
  return if definitions.empty? || @grammar.options[:cst] != true

  lines << "  module Syntax"
  definitions.each_value { |definition| append_syntax_node_contract(lines, definition) }
  lines.push("  end", "")
end

#append_syntax_node_contract(lines, definition) ⇒ void

This method returns an undefined value.

RBS:

  • (Array[String] lines, Hash[Symbol, untyped] definition) -> void

Parameters:

  • lines (Array[String])
  • definition (Hash[Symbol, untyped])


98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/ibex/codegen/rbs.rb', line 98

def append_syntax_node_contract(lines, definition)
  name = definition.fetch(:name)
  fields = definition.fetch(:fields)
  lines.push(
    "    class #{name} < Ibex::Runtime::CST::TypedNode",
    "      KIND: Integer",
    "      def self.cast: (Ibex::Runtime::CST::SyntaxNode node) -> #{name}?"
  )
  fields.each_with_index do |(field, _slot), index|
    lines << "      def #{field}: () -> #{syntax_field_type(name, index)}"
  end
  append_repetition_contracts(lines, fields)
  lines << "    end"
end

#append_value_printer_signatures(lines) ⇒ void

This method returns an undefined value.

RBS:

  • (Array[String] lines) -> void

Parameters:

  • lines (Array[String])


263
264
265
266
267
268
269
270
# File 'lib/ibex/codegen/rbs.rb', line 263

def append_value_printer_signatures(lines)
  @grammar.value_printers.each do |printer|
    symbol = @grammar.symbol(printer[:symbol]) || raise(Ibex::Error, "missing printer symbol #{printer[:symbol]}")
    lines << ""
    lines << "  private def _ibex_value_printer_#{symbol.id}: " \
             "(#{symbol.semantic_type || 'untyped'} value) -> untyped"
  end
end

#append_visitor_contract(lines, definitions) ⇒ void

This method returns an undefined value.

RBS:

  • (Array[String] lines, Hash[String, IR::node_annotation] definitions) -> void

Parameters:

  • lines (Array[String])
  • definitions (Hash[String, IR::node_annotation])


168
169
170
171
172
173
174
175
# File 'lib/ibex/codegen/rbs.rb', line 168

def append_visitor_contract(lines, definitions)
  lines.push("    class Visitor", "      def visit: (untyped node) -> untyped")
  definitions.each_value do |node|
    name = node.fetch(:name)
    lines << "      def visit_#{ast_method_name(name)}: (#{name} node) -> untyped"
  end
  lines.push("      def visit_children: (untyped node) -> untyped", "    end")
end

#ast_method_name(name) ⇒ String

RBS:

  • (String name) -> String

Parameters:

  • name (String)

Returns:

  • (String)


191
192
193
194
195
# File 'lib/ibex/codegen/rbs.rb', line 191

def ast_method_name(name)
  name.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
      .gsub(/([a-z\d])([A-Z])/, '\1_\2')
      .downcase
end

#ast_node_definitionsHash[String, IR::node_annotation]

RBS:

  • () -> Hash[String, IR::node_annotation]

Returns:

  • (Hash[String, IR::node_annotation])


141
142
143
# File 'lib/ibex/codegen/rbs.rb', line 141

def ast_node_definitions
  @grammar.productions.filter_map(&:node).to_h { |node| [node.fetch(:name), node] }
end

#ast_node_field_types(name, length) ⇒ Array[String]

RBS:

  • (String name, Integer length) -> Array[String]

Parameters:

  • name (String)
  • length (Integer)

Returns:

  • (Array[String])


159
160
161
162
163
164
165
# File 'lib/ibex/codegen/rbs.rb', line 159

def ast_node_field_types(name, length)
  productions = @grammar.productions.select { |production| production.node&.fetch(:name) == name }
  Array.new(length) do |index|
    types = productions.map { |production| semantic_type(production.rhs.fetch(index)) }.uniq
    types.length == 1 ? types.fetch(0) : "(#{types.join(' | ')})"
  end
end

#class_parts[ Array[String], String ]

RBS:

  • () -> [Array[String], String]

Returns:

  • ([ Array[String], String ])


363
364
365
366
# File 'lib/ibex/codegen/rbs.rb', line 363

def class_parts
  parts = @grammar.class_name.split("::")
  [parts.take(parts.length - 1), parts.fetch(-1)]
end

#composed_action?(production) ⇒ Boolean

RBS:

  • (IR::Production production) -> bool

Parameters:

Returns:

  • (Boolean)


340
341
342
# File 'lib/ibex/codegen/rbs.rb', line 340

def composed_action?(production)
  production.action&.composition&.dig(:plan, :version) == 1
end

#composition_slot_type(production, plan, slot, step_index) ⇒ String

RBS:

  • (IR::Production production, Hash[Symbol, untyped] plan, Integer slot, Integer step_index) -> String

Parameters:

  • production (IR::Production)
  • plan (Hash[Symbol, untyped])
  • slot (Integer)
  • step_index (Integer)

Returns:

  • (String)


324
325
326
327
328
329
330
331
332
# File 'lib/ibex/codegen/rbs.rb', line 324

def composition_slot_type(production, plan, slot, step_index)
  physical = plan.fetch(:physical)
  return semantic_type(production.rhs.fetch(slot)) if slot < physical

  prior_index = slot - physical
  raise Ibex::Error, "invalid composed action slot #{slot}" if prior_index >= step_index

  plan.fetch(:steps).fetch(prior_index)[:result_type] || "untyped"
end

#generateString

RBS:

  • () -> String

Returns:

  • (String)


27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/ibex/codegen/rbs.rb', line 27

def generate
  lines = ["# Generated by Ibex #{grammar_digest_comment}", ""]
  modules, class_name = class_parts
  modules.each { |name| lines << "module #{name}" }
  lines << "class #{class_name} < #{@superclass}"
  append_ast_contract(lines)
  append_syntax_contract(lines)
  append_contract(lines)
  append_lexer_contract(lines)
  append_value_printer_signatures(lines)
  append_actions(lines)
  lines << "end"
  modules.reverse_each { lines << "end" }
  "#{lines.join("\n")}\n"
end

#grammar_digest_commentString

RBS:

  • () -> String

Returns:

  • (String)


369
370
371
# File 'lib/ibex/codegen/rbs.rb', line 369

def grammar_digest_comment
  @automaton.grammar_digest.delete_prefix("sha256:").chars.first(12).join
end

#inferred_ast_type(symbol_id) ⇒ String?

RBS:

  • (Integer symbol_id) -> String?

Parameters:

  • symbol_id (Integer)

Returns:

  • (String, nil)


351
352
353
354
355
356
357
358
359
360
# File 'lib/ibex/codegen/rbs.rb', line 351

def inferred_ast_type(symbol_id)
  productions = @grammar.productions.select { |production| production.lhs == symbol_id }
  return if productions.empty? || productions.any? { |production| production.node.nil? }

  names = productions.map { |production| production.node&.fetch(:name) }.compact.uniq
  return if names.empty?

  types = names.map { |name| "AST::#{name}" }
  types.length == 1 ? types.fetch(0) : "(#{types.join(' | ')})"
end

#merge_syntax_fields(left, right) ⇒ Hash[String, untyped]

RBS:

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

Parameters:

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

Returns:

  • (Hash[String, untyped])


86
87
88
89
90
91
92
93
94
95
# File 'lib/ibex/codegen/rbs.rb', line 86

def merge_syntax_fields(left, right)
  left.to_h do |name, left_slot|
    right_slot = right.fetch(name)
    left_index = left_slot.is_a?(Hash) ? left_slot.fetch(:index) : left_slot
    right_index = right_slot.is_a?(Hash) ? right_slot.fetch(:index) : right_slot
    raise Ibex::Error, "inconsistent CST slot for #{name}" unless left_index == right_index

    [name, left_slot == right_slot ? left_slot : left_index]
  end.freeze
end

#semantic_type(symbol_id) ⇒ String

RBS:

  • (Integer symbol_id) -> String

Parameters:

  • symbol_id (Integer)

Returns:

  • (String)


345
346
347
348
# File 'lib/ibex/codegen/rbs.rb', line 345

def semantic_type(symbol_id)
  symbol = @grammar.symbol_by_id(symbol_id) || raise(Ibex::Error, "missing grammar symbol id #{symbol_id}")
  symbol.semantic_type || inferred_ast_type(symbol_id) || "untyped"
end

#syntax_field_type(node_name, index) ⇒ String

RBS:

  • (String node_name, Integer index) -> String

Parameters:

  • node_name (String)
  • index (Integer)

Returns:

  • (String)


114
115
116
117
118
119
120
121
122
123
# File 'lib/ibex/codegen/rbs.rb', line 114

def syntax_field_type(node_name, index)
  symbol_ids = @grammar.productions.filter_map do |production|
    production.rhs.fetch(index) if production.node&.fetch(:name) == node_name
  end
  types = symbol_ids.map do |symbol_id|
    symbol = @grammar.symbols.fetch(symbol_id)
    symbol.terminal? ? "Ibex::Runtime::CST::SyntaxToken" : "Ibex::Runtime::CST::SyntaxNode"
  end.uniq
  types.one? ? types.fetch(0) : "(#{types.join(' | ')})"
end

#syntax_node_definitionsHash[String, Hash[Symbol, untyped]]

RBS:

  • () -> Hash[String, Hash[Symbol, untyped]]

Returns:

  • (Hash[String, Hash[Symbol, untyped]])


68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/ibex/codegen/rbs.rb', line 68

def syntax_node_definitions
   = CSTMetadata.new(@grammar).build
  slots = .fetch(:slots)
  definitions = {} #: Hash[String, Hash[Symbol, untyped]]
  @grammar.productions.each do |production|
    node = production.node
    next unless node

    slot = slots.fetch(production.id)
    name = node.fetch(:name)
    previous = definitions[name]
    fields = previous ? merge_syntax_fields(previous.fetch(:fields), slot.fetch(:fields)) : slot.fetch(:fields)
    definitions[name] = { name: name, kind: slot.fetch(:node_kind), fields: fields }.freeze
  end
  definitions
end