Class: Ibex::Codegen::RBS
- Inherits:
-
Object
- Object
- Ibex::Codegen::RBS
- 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
- #action_method?(production) ⇒ Boolean
- #append_action_signature(lines, production) ⇒ void
- #append_actions(lines) ⇒ void
- #append_ast_contract(lines) ⇒ void
- #append_ast_node_contract(lines, node) ⇒ void
- #append_composed_fragment_signatures(lines, production) ⇒ void
- #append_contract(lines) ⇒ void
- #append_entry_contract(lines) ⇒ void
- #append_lexer_contract(lines) ⇒ void
- #append_listener_contract(lines, definitions) ⇒ void
- #append_parameter_contract(lines) ⇒ void
- #append_repetition_contracts(lines, fields) ⇒ void
- #append_syntax_contract(lines) ⇒ void
- #append_syntax_node_contract(lines, definition) ⇒ void
- #append_value_printer_signatures(lines) ⇒ void
- #append_visitor_contract(lines, definitions) ⇒ void
- #ast_method_name(name) ⇒ String
- #ast_node_definitions ⇒ Hash[String, IR::node_annotation]
- #ast_node_field_types(name, length) ⇒ Array[String]
- #class_parts ⇒ [ Array[String], String ]
- #composed_action?(production) ⇒ Boolean
- #composition_slot_type(production, plan, slot, step_index) ⇒ String
- #generate ⇒ String
- #grammar_digest_comment ⇒ String
- #inferred_ast_type(symbol_id) ⇒ String?
-
#initialize(automaton, superclass: nil, omit_action_call: nil) ⇒ RBS
constructor
A new instance of RBS.
- #merge_syntax_fields(left, right) ⇒ Hash[String, untyped]
- #semantic_type(symbol_id) ⇒ String
- #syntax_field_type(node_name, index) ⇒ String
- #syntax_node_definitions ⇒ Hash[String, Hash[Symbol, untyped]]
Constructor Details
#initialize(automaton, superclass: nil, omit_action_call: nil) ⇒ RBS
Returns a new instance of RBS.
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.[:omit_action_call] : omit_action_call @generated_action_abi = GeneratedActionABI::Cache.new end |
Instance Method Details
#action_method?(production) ⇒ 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.
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.
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.
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.
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.
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.
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.[:cst] == true lines << " CST_METADATA: Hash[Symbol, untyped]" if @grammar.[:cst] == true lines << " CST_KIND_MODEL: Ibex::Runtime::CST::Kind" if @grammar.[:cst] == true append_entry_contract(lines) append_parameter_contract(lines) end |
#append_entry_contract(lines) ⇒ void
This method returns an undefined value.
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.
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.[: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.
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.
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.
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.
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.[: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.
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.
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.
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
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_definitions ⇒ 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]
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 ]
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
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
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 |
#generate ⇒ 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_comment ⇒ 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?
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]
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
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
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_definitions ⇒ 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 |