Module: Ibex::CLIIRTools

Defined in:
lib/ibex/cli/ir_tools.rb,
sig/ibex/cli/ir_tools.rbs

Overview

CLI validation and structural comparison for versioned IR documents.

Instance Method Summary collapse

Instance Method Details

#compare_grammars(before, after) ⇒ Hash[Symbol, untyped]

RBS:

  • (IR::Grammar before, IR::Grammar after) -> Hash[Symbol, untyped]

Parameters:

Returns:

  • (Hash[Symbol, untyped])


140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/ibex/cli/ir_tools.rb', line 140

def compare_grammars(before, after)
  before_symbols = before.symbols.map(&:name)
  after_symbols = after.symbols.map(&:name)
  before_productions = production_shapes(before)
  after_productions = production_shapes(after)
  {
    kind: "grammar",
    symbols: { added: (after_symbols - before_symbols).sort, removed: (before_symbols - after_symbols).sort },
    productions: {
      added: (after_productions - before_productions).sort,
      removed: (before_productions - after_productions).sort,
      count: numeric_change(before.productions.length, after.productions.length)
    },
    warnings: numeric_change(before.warnings.length, after.warnings.length)
  }
end

#compare_ir(before, after) ⇒ Hash[Symbol, untyped]

RBS:

  • (IR::Grammar | IR::Automaton | IR::Lexer before, IR::Grammar | IR::Automaton | IR::Lexer after) -> Hash[Symbol, untyped]

Parameters:

Returns:

  • (Hash[Symbol, untyped])


105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/ibex/cli/ir_tools.rb', line 105

def compare_ir(before, after)
  return compare_grammars(before, after) if before.is_a?(IR::Grammar) && after.is_a?(IR::Grammar)
  if before.is_a?(IR::Automaton) && after.is_a?(IR::Automaton)
    return {
      kind: "automaton",
      algorithm: { before: before.algorithm, after: after.algorithm },
      states: numeric_change(before.states.length, after.states.length),
      transitions: numeric_change(transition_count(before), transition_count(after)),
      conflicts: %i[sr resolved_sr rr].to_h do |kind|
        [kind, numeric_change(summary_count(before, kind), summary_count(after, kind))]
      end,
      grammar: compare_grammars(before.grammar, after.grammar)
    }
  end
  return compare_lexers(before, after) if before.is_a?(IR::Lexer) && after.is_a?(IR::Lexer)

  raise Ibex::Error, "(cli):1:1: unsupported IR comparison"
end

#compare_lexers(before, after) ⇒ Hash[Symbol, untyped]

RBS:

  • (IR::Lexer before, IR::Lexer after) -> Hash[Symbol, untyped]

Parameters:

Returns:

  • (Hash[Symbol, untyped])


125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/ibex/cli/ir_tools.rb', line 125

def compare_lexers(before, after)
  before_rules = before.rules.map { |rule| [rule.state, rule.kind, rule.token, rule.pattern, rule.options] }
  after_rules = after.rules.map { |rule| [rule.state, rule.kind, rule.token, rule.pattern, rule.options] }
  {
    kind: "lexer",
    states: { added: after.states - before.states, removed: before.states - after.states },
    rules: {
      added: after_rules - before_rules, removed: before_rules - after_rules,
      count: numeric_change(before.rules.length, after.rules.length)
    },
    warnings: numeric_change(before.warnings.length, after.warnings.length)
  }
end

#migrate_ir_options(arguments) ⇒ migrate_ir_options

RBS:

  • (Array[String] arguments) -> migrate_ir_options

Parameters:

  • arguments (Array[String])

Returns:



85
86
87
88
89
90
91
92
93
94
# File 'lib/ibex/cli/ir_tools.rb', line 85

def migrate_ir_options(arguments)
  settings = { paths: [] } #: migrate_ir_options
  parser = OptionParser.new do |options|
    options.banner = "Usage: ibex migrate-ir INPUT --to=VERSION [-o FILE]"
    options.on("--to=VERSION", Integer, "target schema version") { |value| settings[:to] = value }
    options.on("-o FILE", "--output=FILE", "write atomically to FILE") { |value| settings[:output] = value }
  end
  settings[:paths] = parser.parse(arguments)
  settings
end

#numeric_change(before, after) ⇒ Hash[Symbol, Integer]

RBS:

  • (Integer before, Integer after) -> Hash[Symbol, Integer]

Parameters:

  • before (Integer)
  • after (Integer)

Returns:

  • (Hash[Symbol, Integer])


180
181
182
# File 'lib/ibex/cli/ir_tools.rb', line 180

def numeric_change(before, after)
  { before: before, after: after, delta: after - before }
end

#production_shapes(grammar) ⇒ Array[String]

RBS:

  • (IR::Grammar grammar) -> Array[String]

Parameters:

Returns:

  • (Array[String])


158
159
160
161
162
163
164
# File 'lib/ibex/cli/ir_tools.rb', line 158

def production_shapes(grammar)
  grammar.productions.map do |production|
    lhs = grammar.symbol_by_id(production.lhs)&.name || production.lhs.to_s
    rhs = production.rhs.map { |id| grammar.symbol_by_id(id)&.name || id.to_s }
    "#{lhs} -> #{rhs.join(' ')}"
  end
end

#run_compare_command(arguments) ⇒ Integer

RBS:

  • (Array[String] arguments) -> Integer

Parameters:

  • arguments (Array[String])

Returns:

  • (Integer)


38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/ibex/cli/ir_tools.rb', line 38

def run_compare_command(arguments)
  raise Ibex::Error, "(cli):1:1: compare requires exactly two IR files" unless arguments.length == 2

  before = IR::Validator.validate(File.read(arguments.fetch(0)))
  after = IR::Validator.validate(File.read(arguments.fetch(1)))
  compatible = (before.is_a?(IR::Grammar) && after.is_a?(IR::Grammar)) ||
               (before.is_a?(IR::Automaton) && after.is_a?(IR::Automaton)) ||
               (before.is_a?(IR::Lexer) && after.is_a?(IR::Lexer))
  raise Ibex::Error, "(cli):1:1: cannot compare different IR kinds" unless compatible

  @stdout.puts(JSON.pretty_generate(compare_ir(before, after)))
  0
end

#run_migrate_ir_command(arguments) ⇒ Integer

RBS:

  • (Array[String] arguments) -> Integer

Parameters:

  • arguments (Array[String])

Returns:

  • (Integer)


53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/ibex/cli/ir_tools.rb', line 53

def run_migrate_ir_command(arguments)
  settings = migrate_ir_options(arguments)
  input = single_ir_path(settings.fetch(:paths), "migrate-ir")
  output = settings[:output]
  if output && same_file_target?(input, output)
    raise Ibex::Error, "(cli):1:1: migrate-ir input and output paths must be distinct"
  end

  value = IR::Validator.validate(File.read(input))
  target = settings[:to] || raise(Ibex::Error, "(cli):1:1: migrate-ir requires --to=VERSION")
  if value.is_a?(IR::Lexer)
    raise Ibex::Error, "(cli):1:1: lexer IR is already at its only supported schema version" unless
      target == value.schema_version

    source = IR::Serialize.dump(value)
    output ? atomic_write_ir(output, source) : @stdout.write(source)
    return 0
  end
  unless IR::SUPPORTED_SCHEMA_VERSIONS.include?(target)
    raise Ibex::Error, "(cli):1:1: unsupported migration target schema_version #{target}"
  end
  if target < value.schema_version
    raise Ibex::Error,
          "(cli):1:1: downgrading schema_version #{value.schema_version} to #{target} is not supported"
  end

  source = IR::Serialize.dump(IR::Migration.to_version(value, to: target))
  output ? atomic_write_ir(output, source) : @stdout.write(source)
  0
end

#run_validate_ir_command(arguments) ⇒ Integer

RBS:

  • (Array[String] arguments) -> Integer

Parameters:

  • arguments (Array[String])

Returns:

  • (Integer)


23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/ibex/cli/ir_tools.rb', line 23

def run_validate_ir_command(arguments)
  path = single_ir_path(arguments, "validate-ir")
  value = IR::Validator.validate(File.read(path))
  kind = if value.is_a?(IR::Grammar)
           "grammar"
         elsif value.is_a?(IR::Lexer)
           "lexer"
         else
           "automaton"
         end
  @stdout.puts("valid #{kind} IR v#{value.schema_version}")
  0
end

#single_ir_path(arguments, command) ⇒ String

RBS:

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

Parameters:

  • arguments (Array[String])
  • command (String)

Returns:

  • (String)


97
98
99
100
101
# File 'lib/ibex/cli/ir_tools.rb', line 97

def single_ir_path(arguments, command)
  return arguments.first if arguments.length == 1

  raise Ibex::Error, "(cli):1:1: #{command} requires exactly one IR file"
end

#summary_count(automaton, kind) ⇒ Integer

RBS:

  • (IR::Automaton automaton, Symbol kind) -> Integer

Parameters:

Returns:

  • (Integer)


172
173
174
175
176
177
# File 'lib/ibex/cli/ir_tools.rb', line 172

def summary_count(automaton, kind)
  value = automaton.conflict_summary.fetch(kind)
  return value if value.is_a?(Integer)

  raise Ibex::Error, "(ir):1:1: conflict count #{kind} must be an integer"
end

#transition_count(automaton) ⇒ Integer

RBS:

  • (IR::Automaton automaton) -> Integer

Parameters:

Returns:

  • (Integer)


167
168
169
# File 'lib/ibex/cli/ir_tools.rb', line 167

def transition_count(automaton)
  automaton.states.sum { |state| state.transitions.length }
end