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)


21
22
23
24
25
26
27
# File 'lib/ibex/codegen/rbs.rb', line 21

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)


343
344
345
# File 'lib/ibex/codegen/rbs.rb', line 343

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:



293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
# File 'lib/ibex/codegen/rbs.rb', line 293

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])


281
282
283
284
285
286
287
288
289
290
# File 'lib/ibex/codegen/rbs.rb', line 281

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])


49
50
51
52
53
54
55
56
57
58
# File 'lib/ibex/codegen/rbs.rb', line 49

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)


154
155
156
157
158
159
160
161
162
163
164
# File 'lib/ibex/codegen/rbs.rb', line 154

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:



313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/ibex/codegen/rbs.rb', line 313

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])


206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/ibex/codegen/rbs.rb', line 206

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])


224
225
226
227
228
229
# File 'lib/ibex/codegen/rbs.rb', line 224

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])


247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/ibex/codegen/rbs.rb', line 247

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])


186
187
188
189
190
191
192
193
194
195
196
# File 'lib/ibex/codegen/rbs.rb', line 186

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])


232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/ibex/codegen/rbs.rb', line 232

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, CSTMetadata::field_slot] fields) -> void

Parameters:

  • lines (Array[String])
  • fields (Hash[String, CSTMetadata::field_slot])


130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/ibex/codegen/rbs.rb', line 130

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|
    next unless slot.is_a?(Hash)

    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
  return unless slot.is_a?(Hash)

  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])


61
62
63
64
65
66
67
68
# File 'lib/ibex/codegen/rbs.rb', line 61

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, syntax_definition definition) -> void

Parameters:

  • lines (Array[String])
  • definition (syntax_definition)


102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/ibex/codegen/rbs.rb', line 102

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])


271
272
273
274
275
276
277
278
# File 'lib/ibex/codegen/rbs.rb', line 271

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])


176
177
178
179
180
181
182
183
# File 'lib/ibex/codegen/rbs.rb', line 176

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)


199
200
201
202
203
# File 'lib/ibex/codegen/rbs.rb', line 199

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])


149
150
151
# File 'lib/ibex/codegen/rbs.rb', line 149

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])


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

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 ])


371
372
373
374
# File 'lib/ibex/codegen/rbs.rb', line 371

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)


348
349
350
# File 'lib/ibex/codegen/rbs.rb', line 348

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, IR::action_composition_plan plan, Integer slot, Integer step_index) -> String

Parameters:

  • production (IR::Production)
  • plan (IR::action_composition_plan)
  • slot (Integer)
  • step_index (Integer)

Returns:

  • (String)


332
333
334
335
336
337
338
339
340
# File 'lib/ibex/codegen/rbs.rb', line 332

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)


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

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)


377
378
379
# File 'lib/ibex/codegen/rbs.rb', line 377

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)


359
360
361
362
363
364
365
366
367
368
# File 'lib/ibex/codegen/rbs.rb', line 359

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, CSTMetadata::field_slot]

RBS:

  • (Hash[String, CSTMetadata::field_slot] left, Hash[String, CSTMetadata::field_slot] right) -> Hash[String, CSTMetadata::field_slot]

Parameters:

  • left (Hash[String, CSTMetadata::field_slot])
  • right (Hash[String, CSTMetadata::field_slot])

Returns:

  • (Hash[String, CSTMetadata::field_slot])


90
91
92
93
94
95
96
97
98
99
# File 'lib/ibex/codegen/rbs.rb', line 90

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)


353
354
355
356
# File 'lib/ibex/codegen/rbs.rb', line 353

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)


118
119
120
121
122
123
124
125
126
127
# File 'lib/ibex/codegen/rbs.rb', line 118

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, syntax_definition]

RBS:

  • () -> Hash[String, syntax_definition]

Returns:

  • (Hash[String, syntax_definition])


71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/ibex/codegen/rbs.rb', line 71

def syntax_node_definitions
   = CSTMetadata.new(@grammar).build
  slots = .fetch(:slots)
  definitions = {} #: Hash[String, syntax_definition]
  @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)
    definition = { name: name, kind: slot.fetch(:node_kind), fields: fields }.freeze #: syntax_definition
    definitions[name] = definition
  end
  definitions
end