Module: Ibex::IR::Serialize

Defined in:
lib/ibex/ir/serialize.rb,
sig/ibex/ir/serialize.rbs

Overview

Stable JSON serialization for the current pipeline IR. rubocop:disable Metrics/ModuleLength -- explicit fields keep serialization changes auditable. rubocop:disable Lint/SelfAssignment -- RBS inline assertions narrow schema-decoded values in place.

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.dump(value) ⇒ Object

RBS:

  • (Grammar | Automaton | Lexer value) -> String



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/ibex/ir/serialize.rb', line 60

def dump(value)
  normalize = lambda do |entry|
    case entry
    when String
      normalized = entry.dup.force_encoding(Encoding::UTF_8)
      normalized.valid_encoding? ? normalized : normalized.scrub
    when Array
      entry.map { |child| normalize.call(child) }
    when Hash
      entry.to_h { |key, child| [normalize.call(key), normalize.call(child)] }
    else
      entry
    end
  end
  "#{JSON.pretty_generate(normalize.call(value.to_h))}\n"
end

.empty_chunksObject



150
151
152
153
# File 'lib/ibex/ir/serialize.rb', line 150

def empty_chunks
  value = {} #: Hash[String, IR::serialized_value]
  value.freeze
end

.empty_parametersObject



156
157
158
159
# File 'lib/ibex/ir/serialize.rb', line 156

def empty_parameters
  value = [] #: Array[IR::serialized_value]
  value.freeze
end

.empty_printersObject



162
163
164
165
# File 'lib/ibex/ir/serialize.rb', line 162

def empty_printers
  value = [] #: Array[IR::serialized_value]
  value.freeze
end

.empty_recoveryObject



174
175
176
177
# File 'lib/ibex/ir/serialize.rb', line 174

def empty_recovery
  value = { "sync_tokens" => [], "on_error_reduce" => [] } #: Hash[String, IR::serialized_value]
  value.freeze
end

.empty_testsObject



168
169
170
171
# File 'lib/ibex/ir/serialize.rb', line 168

def empty_tests
  value = [] #: Array[IR::serialized_value]
  value.freeze
end

.load(source) ⇒ Object

RBS:

  • (String source) -> (Grammar | Automaton | Lexer)



79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/ibex/ir/serialize.rb', line 79

def load(source)
  data = JSON.parse(source) #: Hash[String, IR::serialized_value]
  type = data.fetch("ibex_ir") #: String
  return load_lexer(data) if type == "lexer"

  validate_version(data)
  return load_grammar(data) if type == "grammar"
  return load_automaton(data) if type == "automaton"

  raise Ibex::Error, "(ir):1:1: unsupported IR type #{type.inspect}"
rescue JSON::ParserError => e
  raise Ibex::Error, "(ir):1:1: invalid JSON: #{e.message}"
end

.load_automaton(data) ⇒ Object



180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/ibex/ir/serialize.rb', line 180

def load_automaton(data)
  data = data #: Hash[String, IR::serialized_value]
  grammar_data = data.fetch("grammar") #: Hash[String, IR::serialized_value]
  grammar = load_grammar(grammar_data)
  states_data = data.fetch("states") #: Array[Hash[String, IR::serialized_value]]
  states = states_data.map { |state| load_state(state, grammar) }
  data.fetch("schema_version") #: Integer
  Automaton.new(
    grammar: grammar, states: states, conflict_summary: symbolize(data.fetch("conflict_summary")),
    algorithm: data.fetch("algorithm"), grammar_digest: data.fetch("grammar_digest"),
    entry_states: data["entry_states"], entry_construction: data.fetch("entry_construction")
  )
end

.load_grammar(data) ⇒ Object

rubocop:disable Metrics/AbcSize -- explicit current Grammar IR field mapping is the loader contract.



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/ibex/ir/serialize.rb', line 108

def load_grammar(data)
  data = data #: Hash[String, IR::serialized_value]
  data.fetch("schema_version") #: Integer
  Grammar.new(
    class_name: data.fetch("class_name"), superclass: data["superclass"], start: data.fetch("start"),
    expect: data.fetch("expect"), options: symbolize(data.fetch("options")), symbols: load_symbols(data),
    mode: (data["mode"] || "default").to_sym, starts: data["starts"], expect_rr: data["expect_rr"],
    parser_parameters: symbolize(data.fetch("params", empty_parameters)),
    value_printers: symbolize(data.fetch("printers", empty_printers)),
    grammar_tests: load_grammar_tests(data.fetch("tests", empty_tests)),
    lexer: data["lexer"] && load_lexer(data.fetch("lexer")),
    recovery: symbolize(data.fetch("recovery", empty_recovery)),
    productions: load_productions(data),
    user_code: data.fetch("user_code"), conversions: data.fetch("conversions"),
    warnings: symbolize(data.fetch("warnings")),
    user_code_chunks: load_user_code_chunks(data.fetch("user_code_chunks", empty_chunks)),
    source_provenance: symbolize(data["source_provenance"]),
    parser_contract: load_parser_contract(data.fetch("parser_contract"))
  )
end

.load_grammar_tests(tests) ⇒ Object



315
316
317
318
# File 'lib/ibex/ir/serialize.rb', line 315

def load_grammar_tests(tests)
  tests = tests #: Array[IR::serialized_value]
  symbolize(tests).map { |test| test.merge(expectation: test.fetch(:expectation).to_sym) }
end

.load_lexer(data) ⇒ Object



195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/ibex/ir/serialize.rb', line 195

def load_lexer(data)
  data = data #: Hash[String, IR::serialized_value]
  version = data.fetch("schema_version") #: Integer
  unless SUPPORTED_LEXER_SCHEMA_VERSIONS.include?(version)
    expected = SUPPORTED_LEXER_SCHEMA_VERSIONS.join(", ")
    raise Ibex::Error,
          "(ir):1:1: unsupported lexer schema_version #{version.inspect}; " \
          "expected the current lexer format (#{expected})"
  end
  rules_data = data.fetch("rules") #: Array[Hash[String, IR::serialized_value]]
  rules = rules_data.map do |rule|
    LexerRule.new(
      id: rule.fetch("id"), state: rule.fetch("state"), kind: rule.fetch("kind").to_sym,
      token: rule["token"], pattern: rule.fetch("pattern"), pattern_kind: rule.fetch("pattern_kind").to_sym,
      options: rule.fetch("options"), action: rule["action"], location: symbolize(rule.fetch("loc"))
    )
  end
  Lexer.new(
    states: data.fetch("states"), rules: rules, warnings: symbolize(data.fetch("warnings")),
    schema_version: version, source_provenance: symbolize(data["source_provenance"])
  )
end

.load_parser_contract(value) ⇒ Object



321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
# File 'lib/ibex/ir/serialize.rb', line 321

def load_parser_contract(value)
  value = value #: Hash[String, IR::serialized_value]

  entries = ParserContract::DEFINITIONS.keys.to_h do |key|
    entry = value.fetch(key.to_s)
    location = entry["loc"]
    loaded_location = if location
                        Location.new(
                          file: location.fetch("file"), line: location.fetch("line"),
                          column: location.fetch("column")
                        )
                      end
    [
      key,
      ParserContract::Entry.new(
        key, value: entry["value"]&.to_sym, explicit: entry.fetch("explicit"), location: loaded_location
      )
    ]
  end
  ParserContract.new(**entries)
end

.load_production(production) ⇒ Object



258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/ibex/ir/serialize.rb', line 258

def load_production(production)
  production = production #: Hash[String, IR::serialized_value]
  action_data = production["action"]
  action_data = action_data #: Hash[String, IR::serialized_value] if action_data
  action = if action_data
             Action.new(code: action_data.fetch("code"), location: symbolize(action_data["loc"]),
                        named_refs: symbolize(action_data.fetch("named_refs")),
                        context_length: action_data.fetch("context_length"),
                        composition: symbolize(action_data["composition"]))
           end
  Production.new(id: production.fetch("id"), lhs: production.fetch("lhs"), rhs: production.fetch("rhs"),
                 action: action, precedence_override: production["prec_override"],
                 origin: symbolize(production.fetch("origin")), documentation: production["doc"],
                 expansion: symbolize(production["expansion"]), node: symbolize(production["node"]))
end

.load_productions(data) ⇒ Object



144
145
146
147
# File 'lib/ibex/ir/serialize.rb', line 144

def load_productions(data)
  productions_data = data.fetch("productions") #: Array[Hash[String, IR::serialized_value]]
  productions_data.map { |production| load_production(production) }
end

.load_state(state, grammar) ⇒ Object



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/ibex/ir/serialize.rb', line 219

def load_state(state, grammar)
  state = state #: Hash[String, IR::serialized_value]
  items_data = state.fetch("items") #: Array[Hash[String, IR::serialized_value]]
  items = items_data.map do |item|
    lookahead_names = item.fetch("lookaheads") #: Array[String]
    lookaheads = lookahead_names.map do |name|
      symbol = grammar.symbol(name) || raise(Ibex::Error, "(ir):1:1: unknown symbol #{name.inspect}")
      symbol.id
    end
    AutomatonItem.new(production: item.fetch("production"), dot: item.fetch("dot"), lookaheads: lookaheads)
  end
  AutomatonState.new(id: state.fetch("id"), items: items,
                     transitions: symbol_keyed(state.fetch("transitions"), grammar),
                     actions: symbol_keyed(state.fetch("actions"), grammar, actions: true),
                     gotos: symbol_keyed(state.fetch("gotos"), grammar),
                     default_action: normalize_action(state["default_action"]),
                     conflicts: symbolize(state.fetch("conflicts")))
end

.load_symbol_metadata(symbol, field) ⇒ Object

Raises:



286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/ibex/ir/serialize.rb', line 286

def (symbol, field)
  symbol = symbol #: Hash[String, IR::serialized_value]
  value = symbol[field]
  return nil if value.nil?

  position = symbol_source_position(symbol)
  raise Ibex::Error, "#{position}: #{field} must be a String or null" unless value.is_a?(String)
  raise Ibex::Error, "#{position}: #{field} must not be empty" if value.strip.empty?
  raise Ibex::Error, "#{position}: #{field} must be a single line" if value.match?(/[\r\n]/)
  raise Ibex::Error, "#{position}: #{field} must not contain control characters" if
    value.match?(/[[:cntrl:]]/)

  value
end

.load_symbols(data) ⇒ Object



131
132
133
134
135
136
137
138
139
140
141
# File 'lib/ibex/ir/serialize.rb', line 131

def load_symbols(data)
  symbols_data = data.fetch("symbols") #: Array[Hash[String, IR::serialized_value]]
  symbols_data.map do |symbol|
    GrammarSymbol.new(id: symbol.fetch("id"), name: symbol.fetch("name"), kind: symbol.fetch("kind"),
                      reserved: symbol.fetch("reserved"), precedence: symbolize(symbol["prec"]),
                      location: symbolize(symbol["loc"]),
                      display_name: (symbol, "display_name"),
                      semantic_type: (symbol, "semantic_type"),
                      documentation: symbol["doc"])
  end
end

.load_user_code_chunks(chunks) ⇒ Object



275
276
277
278
279
280
281
282
283
# File 'lib/ibex/ir/serialize.rb', line 275

def load_user_code_chunks(chunks)
  chunks = chunks #: Hash[String, Array[Hash[String, IR::serialized_value]]]
  chunks.to_h do |name, values|
    loaded = values.map do |value|
      UserCodeChunk.new(code: value.fetch("code"), location: symbolize(value.fetch("loc")))
    end
    [name, loaded]
  end
end

.normalize_action(value) ⇒ Object



248
249
250
251
252
253
254
255
# File 'lib/ibex/ir/serialize.rb', line 248

def normalize_action(value)
  value = value #: IR::serialized_value
  return nil unless value

  action = symbolize(value)
  action[:type] = action[:type].to_sym
  action
end

.symbol_keyed(values, grammar, actions: false) ⇒ Object



239
240
241
242
243
244
245
# File 'lib/ibex/ir/serialize.rb', line 239

def symbol_keyed(values, grammar, actions: false)
  values = values #: Hash[String, IR::serialized_value]
  values.to_h do |name, value|
    symbol = grammar.symbol(name) || raise(Ibex::Error, "(ir):1:1: unknown symbol #{name.inspect}")
    [symbol.id, actions ? normalize_action(value) : value]
  end
end

.symbol_source_position(symbol) ⇒ Object



302
303
304
305
306
307
308
309
310
311
312
# File 'lib/ibex/ir/serialize.rb', line 302

def symbol_source_position(symbol)
  location = symbol["loc"]
  return "(ir):1:1" unless location.is_a?(Hash)

  file = location["file"]
  line = location["line"]
  column = location["column"]
  return "(ir):1:1" unless file.is_a?(String) && line.is_a?(Integer) && column.is_a?(Integer)

  "#{file}:#{line}:#{column}"
end

.symbolize(value) ⇒ Object

Parsed IR values are recursively heterogeneous until schema validation narrows them.



345
346
347
348
349
350
351
352
# File 'lib/ibex/ir/serialize.rb', line 345

def symbolize(value)
  value = value #: IR::serialized_value
  case value
  when Array then value.map { |item| symbolize(item) }
  when Hash then value.to_h { |key, item| [key.to_sym, symbolize(item)] }
  else value
  end
end

.validate_version(data) ⇒ Object

Raises:



97
98
99
100
101
102
103
104
# File 'lib/ibex/ir/serialize.rb', line 97

def validate_version(data)
  version = data["schema_version"]
  return if SUPPORTED_SCHEMA_VERSIONS.include?(version)

  expected = SUPPORTED_SCHEMA_VERSIONS.join(", ")
  raise Ibex::Error,
        "(ir):1:1: unsupported schema_version #{version.inspect}; expected the current format (#{expected})"
end

Instance Method Details

#self?.dumpString

RBS:

  • (Grammar | Automaton | Lexer value) -> String

Parameters:

Returns:

  • (String)


94
# File 'sig/ibex/ir/serialize.rbs', line 94

def self?.dump: (Grammar | Automaton | Lexer value) -> String

#self?.loadGrammar, ...

RBS:

  • (String source) -> (Grammar | Automaton | Lexer)

Parameters:

  • source (String)

Returns:



97
# File 'sig/ibex/ir/serialize.rbs', line 97

def self?.load: (String source) -> (Grammar | Automaton | Lexer)