Class: Ibex::CLI

Inherits:
Object
  • Object
show all
Includes:
CLICounterexampleOptions, CLIGenerationArtifacts, CLIGenerationErrorMessages, CLIOutputs
Defined in:
lib/ibex/cli.rb,
sig/ibex/cli.rbs

Overview

Command-line pipeline coordinator. rubocop:disable Metrics/ClassLength -- inline type contracts add lines without adding runtime responsibilities.

Constant Summary collapse

SUBCOMMAND_HANDLERS =

Returns:

  • (Hash[String, [ Symbol, Symbol ]])
{
  "check" => %i[CLIAmbiguity run_check_command],
  "diagnose" => %i[CLIDiagnostics run_diagnose_command],
  "coverage" => %i[CLICoverage run_coverage_command],
  "debug" => %i[CLIDebug run_debug_command],
  "doc" => %i[CLIDocumentation run_documentation_command],
  "errors" => %i[CLIErrorMessages run_error_messages_command],
  "explain" => %i[CLIExplain run_explain_command],
  "fmt" => %i[CLIFormatting run_format_command],
  "test" => %i[CLIGrammarTests run_grammar_tests_command],
  "lsp" => %i[CLILSP run_lsp_command],
  "migrate-check" => %i[CLIRaccMigration run_migrate_check_command],
  "migrate-harness" => %i[CLIRaccMigration run_migrate_harness_command],
  "samples" => %i[CLISamples run_samples_command],
  "validate-ir" => %i[CLIIRTools run_validate_ir_command],
  "compare" => %i[CLIIRTools run_compare_command],
  "migrate-ir" => %i[CLIIRTools run_migrate_ir_command]
}.freeze

Constants included from CLIOutputs

Ibex::CLIOutputs::WARNING_MESSAGES

Constants included from CLICounterexampleOptions

Ibex::CLICounterexampleOptions::DEFAULTS

Class Method Summary collapse

Instance Method Summary collapse

Methods included from CLIOutputs

#atomic_write_ir, #conflict_count, #conflict_messages, #default_output_path, #format_grammar_warning, #handle_grammar_warnings, #ir_file_mode, #report_conflicts, #report_status, #suggest_ielr, #warning_categories, #write_report

Methods included from CLIGenerationArtifacts

#add_generation_manifest, #artifact_label, #begin_artifact_generation, #ensure_generation_inputs_stable!, #finish_artifact_generation, #generation_artifacts, #generation_inputs_stable?, #generation_manifest_options, #manifest_output_path, #record_generation_input, #register_artifact, #verify_artifact_generation

Methods included from CLIGenerationErrorMessages

#add_error_messages_generation_option, #configured_error_messages, #validate_messages_options

Methods included from CLICounterexampleOptions

#add_counterexample_options, #positive_counterexample_limit

Constructor Details

#initialize(stdout:, stderr:, stdin: $stdin, watch_clock: nil, watch_sleeper: nil, watch_iteration_hook: nil) ⇒ CLI

Returns a new instance of CLI.

RBS:

  • (?stdin: _CLIInput, stdout: _CLIOutput, stderr: _CLIOutput, ?watch_clock: (^() -> Float)?, ?watch_sleeper: (^(Float) -> void)?, ?watch_iteration_hook: (^(Symbol, Integer, Array[String]) -> (Integer | Symbol | nil))?) -> void

Parameters:

  • stdout: (_CLIOutput)
  • stderr: (_CLIOutput)
  • stdin: (_CLIInput) (defaults to: $stdin)
  • watch_clock: (^() -> Float, nil) (defaults to: nil)
  • watch_sleeper: (^(Float) -> void, nil) (defaults to: nil)
  • watch_iteration_hook: (^(Symbol, Integer, Array[String]) -> (Integer | Symbol | nil), nil) (defaults to: nil)


153
154
155
156
157
158
159
160
161
162
# File 'lib/ibex/cli.rb', line 153

def initialize(stdout:, stderr:, stdin: $stdin, watch_clock: nil, watch_sleeper: nil, watch_iteration_hook: nil)
  @stdin = stdin
  @stdout = stdout
  @stderr = stderr
  @watch_clock = watch_clock || -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
  @watch_sleeper = watch_sleeper || ->(seconds) { sleep(seconds) }
  @watch_iteration_hook = watch_iteration_hook || ->(_event, _iteration, _paths) {}
  @options = { emit: "ruby", mode: :default, table: :compact, line_convert: true }
             .merge(CLICounterexampleOptions::DEFAULTS)
end

Class Method Details

.start(arguments, stdin: $stdin, stdout: $stdout, stderr: $stderr, watch_clock: nil, watch_sleeper: nil, watch_iteration_hook: nil) ⇒ Integer

rubocop:disable Layout/LineLength

RBS:

  • (Array[String] arguments, ?stdin: _CLIInput, ?stdout: _CLIOutput, ?stderr: _CLIOutput, ?watch_clock: (^() -> Float)?, ?watch_sleeper: (^(Float) -> void)?, ?watch_iteration_hook: (^(Symbol, Integer, Array[String]) -> (Integer | Symbol | nil))?) -> Integer

Parameters:

  • arguments (Array[String])
  • stdin: (_CLIInput) (defaults to: $stdin)
  • stdout: (_CLIOutput) (defaults to: $stdout)
  • stderr: (_CLIOutput) (defaults to: $stderr)
  • watch_clock: (^() -> Float, nil) (defaults to: nil)
  • watch_sleeper: (^(Float) -> void, nil) (defaults to: nil)
  • watch_iteration_hook: (^(Symbol, Integer, Array[String]) -> (Integer | Symbol | nil), nil) (defaults to: nil)

Returns:

  • (Integer)


144
145
146
147
148
149
150
# File 'lib/ibex/cli.rb', line 144

def self.start(arguments, stdin: $stdin, stdout: $stdout, stderr: $stderr, watch_clock: nil, watch_sleeper: nil,
               watch_iteration_hook: nil)
  new(
    stdin: stdin, stdout: stdout, stderr: stderr, watch_clock: watch_clock,
    watch_sleeper: watch_sleeper, watch_iteration_hook: watch_iteration_hook
  ).run(arguments)
end

Instance Method Details

#action_source_output_path(output_path) ⇒ String

RBS:

  • (String output_path) -> String

Parameters:

  • output_path (String)

Returns:

  • (String)


721
722
723
724
725
726
727
# File 'lib/ibex/cli.rb', line 721

def action_source_output_path(output_path)
  configured_path = @options[:action_source]
  path = configured_path == true ? default_output_path(output_path, ".actions.rb") : configured_path
  raise Ibex::Error, "(cli):1:1: action source output path is required" unless path.is_a?(String) && !path.empty?

  path
end

#action_source_source(automaton) ⇒ String

RBS:

  • (IR::Automaton automaton) -> String

Parameters:

Returns:

  • (String)


730
731
732
733
# File 'lib/ibex/cli.rb', line 730

def action_source_source(automaton)
  require_relative "codegen/action_source"
  Codegen::ActionSource.new(automaton, omit_action_call: @options[:omit_actions]).generate
end

#activate_cli_feature(feature) ⇒ void

This method returns an undefined value.

RBS:

  • (Symbol feature) -> void

Parameters:

  • feature (Symbol)


203
204
205
206
# File 'lib/ibex/cli.rb', line 203

def activate_cli_feature(feature)
  extension = Ibex.const_get(feature)
  extend extension unless singleton_class.ancestors.include?(extension)
end

#add_compatibility_options(options) ⇒ void

This method returns an undefined value.

RBS:

  • (OptionParser options) -> void

Parameters:

  • options (OptionParser)


316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
# File 'lib/ibex/cli.rb', line 316

def add_compatibility_options(options)
  options.on("-F", "--frozen", "emit frozen string literals") { @options[:frozen] = true }
  options.on("--line-convert-all", "convert all source lines") do
    @options[:line_convert] = true
    @options[:line_convert_all] = true
  end
  options.on("-l", "--no-line-convert", "use generated-file action lines") do
    @options[:line_convert] = false
    @options[:line_convert_all] = false
  end
  options.on("-a", "--no-omit-actions", "generate implicit action methods") { @options[:omit_actions] = false }
  options.on("--superclass=CLASS", "override parser superclass") { |value| @options[:superclass] = value }
  options.on("--check", "verify generated parser content without rewriting") { @options[:verify_output] = true }
  options.on("-C", "--check-only", "check grammar and exit") { @options[:check_only] = true }
  options.on("-S", "--output-status", "show pipeline status") { @options[:status] = true }
  options.on("-P", "accept the compatibility profiling flag") { @options[:profile] = true }
  options.on("-D FLAGS", "accept internal compatibility flags") { |value| @options[:debug_flags] = value }
end

#add_information_options(options) ⇒ void

This method returns an undefined value.

RBS:

  • (OptionParser options) -> void

Parameters:

  • options (OptionParser)


336
337
338
339
340
341
# File 'lib/ibex/cli.rb', line 336

def add_information_options(options)
  options.on("--version", "show version") { @options[:version] = true }
  options.on("--runtime-version", "show runtime version") { @options[:runtime_version] = true }
  options.on("--copyright", "show copyright") { @options[:copyright] = true }
  options.on("--help", "show help") { @options[:help] = true }
end

#add_output_options(options) ⇒ void

This method returns an undefined value.

RBS:

  • (OptionParser options) -> void

Parameters:

  • options (OptionParser)


279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'lib/ibex/cli.rb', line 279

def add_output_options(options)
  options.on("-o", "--output-file=FILE", "generated parser path") { |value| @options[:output] = value }
  options.on("-E", "--embedded", "embed the Pure Ruby runtime") { @options[:embedded] = true }
  options.on("-t", "--debug", "generate a debug-capable parser") { @options[:debug] = true }
  options.on("-g", "obsolete alias for --debug") { @options[:debug] = true }
  options.on("-v", "--verbose", "write an automaton report") { @options[:verbose] = true }
  add_counterexample_options(options)
  add_signature_output_options(options)
  options.on("--manifest[=FILE]", "write a generation manifest (defaults beside parser)") do |value|
    @options[:manifest] = value || true
  end
  options.on("--dot=FILE", "write Graphviz DOT") { |value| @options[:dot] = value }
  options.on("--mermaid=FILE", "write a Mermaid flowchart") { |value| @options[:mermaid] = value }
  options.on("--html=FILE", "write a self-contained HTML report") { |value| @options[:html] = value }
  options.on("--railroad=FILE", "write a self-contained SVG railroad diagram") do |value|
    @options[:railroad] = value
  end
  options.on("-O", "--log-file=FILE", "automaton report path") do |value|
    @options[:verbose] = true
    @options[:log_file] = value
  end
  options.on("-e", "--executable [RUBY]", "add a shebang") do |value|
    @options[:executable] = value || "/usr/bin/env ruby"
  end
end

#add_pipeline_options(options) ⇒ void

This method returns an undefined value.

RBS:

  • (OptionParser options) -> void

Parameters:

  • options (OptionParser)


245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/ibex/cli.rb', line 245

def add_pipeline_options(options)
  options.on("--emit=FORMAT", "ast, sets, lexer-ir, grammar-ir, automaton-ir, or ruby") do |value|
    @options[:emit] = value
  end
  options.on("--from=FORMAT", %w[grammar-ir automaton-ir], "resume from IR JSON") do |value|
    @options[:from] = value
  end
  options.on("--mode=MODE", %w[default extended], "grammar mode") { |value| @options[:mode] = value.to_sym }
  options.on("--table=FORMAT", %w[plain compact], "parser table format") do |value|
    @options[:table] = value.to_sym
  end
  options.on(
    "--cst-trivia=POLICY",
    %w[leading balanced drop attach],
    "leading, balanced, drop, or attach (alias for leading)"
  ) do |value|
    @options[:cst_trivia] = value.to_sym
  end
  options.on("--algorithm=NAME", %w[slr lalr ielr lr1], "parser construction algorithm") do |value|
    @options[:algorithm] = value.to_sym
  end
  options.on("--suggest-ielr", "check whether IELR removes unexpected LALR conflicts") do
    @options[:suggest_ielr] = true
  end
  options.on("--entry-isolation", "build independent state sets for each start symbol") do
    @options[:entry_isolation] = true
  end
  options.on("--warnings=CATEGORIES", "all, error, all,error, or none") do |value|
    @options[:warnings] = warning_categories(value)
  end
  options.on("--watch", "regenerate file outputs when grammar sources change") { @options[:watch] = true }
end

#add_signature_output_options(options) ⇒ void

This method returns an undefined value.

RBS:

  • (OptionParser options) -> void

Parameters:

  • options (OptionParser)


306
307
308
309
310
311
312
313
# File 'lib/ibex/cli.rb', line 306

def add_signature_output_options(options)
  options.on("--rbs[=FILE]", "write an RBS signature (defaults beside parser)") do |value|
    @options[:rbs] = value || true
  end
  options.on("--action-source[=FILE]", "write static-check-only action Ruby (defaults beside parser)") do |value|
    @options[:action_source] = value || true
  end
end

#build_automaton(grammar, input_path) ⇒ IR::Automaton

RBS:

  • (IR::Grammar grammar, String input_path) -> IR::Automaton

Parameters:

Returns:



744
745
746
747
748
749
750
751
752
753
754
755
# File 'lib/ibex/cli.rb', line 744

def build_automaton(grammar, input_path)
  algorithm = @options[:algorithm] || :lalr
  report_status("building #{algorithm} automaton")
  automaton = LALR::Builder.new(
    grammar, algorithm: algorithm, entry_isolation: @options[:entry_isolation] == true
  ).build
  report_conflicts(automaton, input_path)
  suggest_ielr(automaton, input_path)
  write_report(automaton, input_path) if @options[:verbose] && !@options[:verify_output]
  write_visualizations(automaton) unless @options[:verify_output]
  automaton
end

#canonical_target_path(path, seen = {}) ⇒ String

RBS:

  • (String path, ?Hash[String, bool] seen) -> String

Parameters:

  • path (String)
  • seen (Hash[String, bool]) (defaults to: {})

Returns:

  • (String)


463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
# File 'lib/ibex/cli.rb', line 463

def canonical_target_path(path, seen = {})
  suffix = [] #: Array[String]
  cursor = File.expand_path(path)
  until File.exist?(cursor) || File.symlink?(cursor)
    parent = File.dirname(cursor)
    return path if parent == cursor

    suffix.unshift(File.basename(cursor))
    cursor = parent
  end
  if File.symlink?(cursor)
    raise Errno::ELOOP, cursor if seen[cursor]

    seen[cursor] = true
    target = File.expand_path(File.readlink(cursor), File.dirname(cursor))
    return File.join(canonical_target_path(target, seen), *suffix)
  end

  File.join(File.realpath(cursor), *suffix)
end

#dispatch_automaton(automaton, path) ⇒ Integer

RBS:

  • (IR::Automaton automaton, String path) -> Integer

Parameters:

Returns:

  • (Integer)


568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
# File 'lib/ibex/cli.rb', line 568

def dispatch_automaton(automaton, path)
  handle_grammar_warnings(automaton.grammar, path)
  return 0 if @options[:check_only]

  write_railroad(automaton.grammar) unless @options[:verify_output]
  return emit_sets(automaton.grammar) if @options[:emit] == "sets"
  return emit_lexer(automaton.grammar) if @options[:emit] == "lexer-ir"
  return emit_grammar(automaton.grammar) if @options[:emit] == "grammar-ir"
  return emit_loaded_automaton(automaton, path) if @options[:emit] == "automaton-ir"

  if @options[:emit] == "ruby"
    prepare_loaded_automaton(automaton, path)
    return generate_ruby(automaton, path)
  end

  raise Ibex::Error, "(cli):1:1: AST cannot be reconstructed from Automaton IR"
end

#dispatch_grammar(grammar, path) ⇒ Integer

RBS:

  • (IR::Grammar grammar, String path) -> Integer

Parameters:

Returns:

  • (Integer)


550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
# File 'lib/ibex/cli.rb', line 550

def dispatch_grammar(grammar, path)
  handle_grammar_warnings(grammar, path)
  if @options[:check_only]
    build_automaton(grammar, path) if @options[:warnings]&.include?(:error)
    return 0
  end

  write_railroad(grammar) unless @options[:verify_output]
  return emit_sets(grammar) if @options[:emit] == "sets"
  return emit_lexer(grammar) if @options[:emit] == "lexer-ir"
  return emit_grammar(grammar) if @options[:emit] == "grammar-ir"
  return emit_automaton(grammar, path) if @options[:emit] == "automaton-ir"
  return emit_ruby(grammar, path) if @options[:emit] == "ruby"

  raise Ibex::Error, "(cli):1:1: emit format #{@options[:emit].inspect} is not available yet"
end

#dispatch_subcommand(arguments) ⇒ Integer?

RBS:

  • (Array[String] arguments) -> Integer?

Parameters:

  • arguments (Array[String])

Returns:

  • (Integer, nil)


193
194
195
196
197
198
199
200
# File 'lib/ibex/cli.rb', line 193

def dispatch_subcommand(arguments)
  definition = SUBCOMMAND_HANDLERS[arguments.first]
  return unless definition

  feature, handler = definition
  activate_cli_feature(feature)
  send(handler, arguments.drop(1))
end

#emit_ast(ast) ⇒ Integer

RBS:

  • (Frontend::AST::Root ast) -> Integer

Parameters:

Returns:

  • (Integer)


605
606
607
608
# File 'lib/ibex/cli.rb', line 605

def emit_ast(ast)
  @stdout.puts(JSON.pretty_generate(ast.to_h))
  0
end

#emit_automaton(grammar, input_path) ⇒ Integer

RBS:

  • (IR::Grammar grammar, String input_path) -> Integer

Parameters:

Returns:

  • (Integer)


642
643
644
645
646
647
# File 'lib/ibex/cli.rb', line 642

def emit_automaton(grammar, input_path)
  automaton = build_automaton(grammar, input_path)
  finish_artifact_generation(@generation_sources)
  @stdout.write(IR::Serialize.dump(automaton))
  0
end

#emit_grammar(grammar) ⇒ Integer

RBS:

  • (IR::Grammar grammar) -> Integer

Parameters:

Returns:

  • (Integer)


611
612
613
614
615
# File 'lib/ibex/cli.rb', line 611

def emit_grammar(grammar)
  finish_artifact_generation(@generation_sources)
  @stdout.write(IR::Serialize.dump(grammar))
  0
end

#emit_lexer(grammar) ⇒ Integer

RBS:

  • (IR::Grammar grammar) -> Integer

Parameters:

Returns:

  • (Integer)


618
619
620
621
622
623
624
625
# File 'lib/ibex/cli.rb', line 618

def emit_lexer(grammar)
  lexer = grammar.lexer
  raise Ibex::Error, "(cli):1:1: grammar does not declare a lexer" unless lexer

  finish_artifact_generation(@generation_sources)
  @stdout.write(IR::Serialize.dump(lexer))
  0
end

#emit_loaded_automaton(automaton, input_path) ⇒ Integer

RBS:

  • (IR::Automaton automaton, String input_path) -> Integer

Parameters:

Returns:

  • (Integer)


736
737
738
739
740
741
# File 'lib/ibex/cli.rb', line 736

def emit_loaded_automaton(automaton, input_path)
  prepare_loaded_automaton(automaton, input_path)
  finish_artifact_generation(@generation_sources)
  @stdout.write(IR::Serialize.dump(automaton))
  0
end

#emit_ruby(grammar, input_path) ⇒ Integer

RBS:

  • (IR::Grammar grammar, String input_path) -> Integer

Parameters:

Returns:

  • (Integer)


650
651
652
653
# File 'lib/ibex/cli.rb', line 650

def emit_ruby(grammar, input_path)
  automaton = build_automaton(grammar, input_path)
  generate_ruby(automaton, input_path)
end

#emit_sets(grammar) ⇒ Integer

RBS:

  • (IR::Grammar grammar) -> Integer

Parameters:

Returns:

  • (Integer)


628
629
630
631
632
633
634
635
636
637
638
639
# File 'lib/ibex/cli.rb', line 628

def emit_sets(grammar)
  sets = Analysis::Sets.new(grammar)
  nonterminals = grammar.nonterminals.sort_by(&:name)
  output = {
    nullable: nonterminals.filter_map { |symbol| symbol.name if sets.nullable?(symbol) },
    first: nonterminals.to_h { |symbol| [symbol.name, sets.first(symbol).sort] },
    follow: nonterminals.to_h { |symbol| [symbol.name, sets.follow(symbol).sort] }
  }
  finish_artifact_generation(@generation_sources)
  @stdout.puts(JSON.pretty_generate(output))
  0
end

#generate_ruby(automaton, input_path) ⇒ Integer

RBS:

  • (IR::Automaton automaton, String input_path) -> Integer

Parameters:

Returns:

  • (Integer)


656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
# File 'lib/ibex/cli.rb', line 656

def generate_ruby(automaton, input_path)
  source = Codegen::Ruby.new(
    automaton, table: @options[:table], embedded: @options.fetch(:embedded, false),
               line_convert: @options.fetch(:line_convert), debug: @options.fetch(:debug, false),
               line_convert_all: @options.fetch(:line_convert_all, false),
               omit_action_call: @options[:omit_actions], superclass: @options[:superclass],
               executable: @options[:executable], cst_trivia: @options.fetch(:cst_trivia, :leading),
               error_messages: configured_error_messages(automaton)
  ).generate
  output_path = @options[:output] || default_output_path(input_path, ".rb")
  action_source = action_source_source(automaton) if @options[:action_source]
  write_action_source(output_path, action_source) if action_source
  register_artifact(:parser, output_path, source, mode: (0o755 if @options[:executable]), status: true)
  write_rbs(automaton, output_path) if @options[:rbs]
  finish_artifact_generation(@generation_sources)
end

#generation_paths(input_path) ⇒ Hash[Symbol, String?]

RBS:

  • (String input_path) -> Hash[Symbol, String?]

Parameters:

  • input_path (String)

Returns:

  • (Hash[Symbol, String?])


485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
# File 'lib/ibex/cli.rb', line 485

def generation_paths(input_path)
  paths = { input: input_path, messages: @options[:messages] } #: Hash[Symbol, String?]
  if @options[:emit] == "ruby" && !@options[:check_only]
    output = @options[:output] || default_output_path(input_path, ".rb")
    paths[:parser] = output
    paths[:rbs] = rbs_output_path(output) if @options[:rbs]
    paths[:action_source] = action_source_output_path(output) if @options[:action_source]
    paths[:manifest] = manifest_output_path_for(output) if @options[:manifest]
  end
  unless @options[:verify_output]
    paths.merge!(dot: @options[:dot], mermaid: @options[:mermaid], html: @options[:html],
                 railroad: @options[:railroad])
    paths[:report] = @options[:log_file] || default_output_path(input_path, ".output") if @options[:verbose]
  end
  paths
end

#informational_result(parser) ⇒ Integer?

RBS:

  • (OptionParser parser) -> Integer?

Parameters:

  • parser (OptionParser)

Returns:

  • (Integer, nil)


344
345
346
347
348
349
350
# File 'lib/ibex/cli.rb', line 344

def informational_result(parser)
  return print_version if @options[:version] || @options[:runtime_version]
  return print_copyright if @options[:copyright]
  return print_help(parser) if @options[:help]

  nil
end

#input_path(remaining) ⇒ String

RBS:

  • (Array[String] remaining) -> String

Parameters:

  • remaining (Array[String])

Returns:

  • (String)


353
354
355
356
357
358
# File 'lib/ibex/cli.rb', line 353

def input_path(remaining)
  path = remaining.first || raise(Ibex::Error, "(cli):1:1: grammar file is required")
  raise Ibex::Error, "(cli):1:1: only one grammar file may be specified" if remaining.length > 1

  path
end

#manifest_output_path_for(output_path) ⇒ String

RBS:

  • (String output_path) -> String

Parameters:

  • output_path (String)

Returns:

  • (String)


794
795
796
797
798
799
# File 'lib/ibex/cli.rb', line 794

def manifest_output_path_for(output_path)
  configured = @options[:manifest]
  return configured if configured.is_a?(String) && !configured.empty?

  default_output_path(output_path, ".ibex.json")
end

#normalize_grammar_path(path) ⇒ IR::Grammar

RBS:

  • (String path) -> IR::Grammar

Parameters:

  • path (String)

Returns:



526
527
528
# File 'lib/ibex/cli.rb', line 526

def normalize_grammar_path(path)
  Normalizer.new(resolve_grammar_path(path), mode: @options[:mode]).normalize
end

#option_parserOptionParser

RBS:

  • () -> OptionParser

Returns:

  • (OptionParser)


215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/ibex/cli.rb', line 215

def option_parser
  OptionParser.new do |options|
    options.banner = "Usage: ibex [options] grammarfile"
    add_pipeline_options(options)
    add_output_options(options)
    add_error_messages_generation_option(options)
    add_compatibility_options(options)
    add_information_options(options)
    options.separator("")
    options.separator("Subcommands:")
    options.separator("    check --ambiguity         search for ambiguity within explicit budgets")
    options.separator("    coverage                  collect, merge, or check runtime coverage")
    options.separator("    debug AUTOMATON [TOKEN]  simulate validated Automaton IR tables")
    options.separator("    diagnose                  collect frontend diagnostics")
    options.separator("    doc                       render grammar documentation")
    options.separator("    errors --list|--update  list or update example-keyed syntax error messages")
    options.separator("    explain                   explain selected parser conflicts")
    options.separator("    fmt                       format grammar source")
    options.separator("    test                      run grammar-declared source examples")
    options.separator("    lsp                       run the language server over stdio")
    options.separator("    migrate-check             statically check a racc grammar migration")
    options.separator("    migrate-harness           generate a differential subprocess harness")
    options.separator("    samples                   generate bounded terminal sentences")
    options.separator("    validate-ir FILE          validate a versioned IR document")
    options.separator("    compare BEFORE AFTER      compare two versioned IR documents")
    options.separator("    migrate-ir INPUT --to=2   migrate a versioned IR document")
  end
end

#portable_target_key(path) ⇒ [ String, String ]

RBS:

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

Parameters:

  • path (String)

Returns:

  • ([ String, String ])


451
452
453
454
455
456
457
458
459
460
# File 'lib/ibex/cli.rb', line 451

def portable_target_key(path)
  canonical = canonical_target_path(path)
  basename = File.basename(canonical)
  folded = if basename.encoding == Encoding::UTF_8 && basename.valid_encoding?
             basename.unicode_normalize(:nfc).downcase(:fold).unicode_normalize(:nfc)
           else
             basename.downcase
           end
  [File.dirname(canonical), folded]
end

#prepare_loaded_automaton(automaton, input_path) ⇒ void

This method returns an undefined value.

RBS:

  • (IR::Automaton automaton, String input_path) -> void

Parameters:



758
759
760
761
762
763
# File 'lib/ibex/cli.rb', line 758

def prepare_loaded_automaton(automaton, input_path)
  report_conflicts(automaton, input_path)
  suggest_ielr(automaton, input_path)
  write_report(automaton, input_path) if @options[:verbose] && !@options[:verify_output]
  write_visualizations(automaton) unless @options[:verify_output]
end

RBS:

  • () -> Integer

Returns:

  • (Integer)


599
600
601
602
# File 'lib/ibex/cli.rb', line 599

def print_copyright
  @stdout.puts("Ibex #{VERSION} Copyright (c) 2026 Yudai Takada")
  0
end

RBS:

  • (OptionParser parser) -> Integer

Parameters:

  • parser (OptionParser)

Returns:

  • (Integer)


593
594
595
596
# File 'lib/ibex/cli.rb', line 593

def print_help(parser)
  @stdout.puts(parser)
  0
end

RBS:

  • () -> Integer

Returns:

  • (Integer)


587
588
589
590
# File 'lib/ibex/cli.rb', line 587

def print_version
  @stdout.puts("ibex #{VERSION}")
  0
end

#process_grammar(path) ⇒ Integer

RBS:

  • (String path) -> Integer

Parameters:

  • path (String)

Returns:

  • (Integer)


503
504
505
506
507
508
509
510
511
512
513
514
515
516
# File 'lib/ibex/cli.rb', line 503

def process_grammar(path)
  return process_ir(path) if @options[:from]

  begin_artifact_generation
  report_status("reading #{path}")
  resolution = resolve_grammar_path(path)
  @generation_inputs = @last_resolver.source_records
  @generation_sources = @generation_inputs.map(&:path)
  validate_generation_paths!(path, source_paths: resolution.files)
  return emit_ast(resolution.root) if @options[:emit] == "ast"

  grammar = Normalizer.new(resolution, mode: @options[:mode]).normalize
  dispatch_grammar(grammar, path)
end

#process_ir(path) ⇒ Integer

RBS:

  • (String path) -> Integer

Parameters:

  • path (String)

Returns:

  • (Integer)


531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
# File 'lib/ibex/cli.rb', line 531

def process_ir(path)
  require_relative "ir/validator"

  begin_artifact_generation
  report_status("reading #{path}")
  source = File.binread(path)
  input = record_generation_input(path, source)
  validate_generation_paths!(path, source_paths: [input.path])
  value = IR::Validator.validate(source)
  expected = @options[:from] == "grammar-ir" ? IR::Grammar : IR::Automaton
  raise Ibex::Error, "#{path}:1:1: expected #{@options[:from]} input" unless value.is_a?(expected)

  return dispatch_grammar(value, path) if value.is_a?(IR::Grammar)
  return dispatch_automaton(value, path) if value.is_a?(IR::Automaton)

  raise Ibex::Error, "#{path}:1:1: expected #{@options[:from]} input"
end

#rbs_output_path(output_path) ⇒ String

RBS:

  • (String output_path) -> String

Parameters:

  • output_path (String)

Returns:

  • (String)


698
699
700
701
702
703
704
# File 'lib/ibex/cli.rb', line 698

def rbs_output_path(output_path)
  configured_path = @options[:rbs]
  path = configured_path == true ? default_output_path(output_path, ".rbs") : configured_path
  raise ArgumentError, "RBS output path is required" unless path.is_a?(String)

  path
end

#rbs_source(automaton) ⇒ String

RBS:

  • (IR::Automaton automaton) -> String

Parameters:

Returns:

  • (String)


707
708
709
710
711
712
# File 'lib/ibex/cli.rb', line 707

def rbs_source(automaton)
  require_relative "codegen/rbs"
  Codegen::RBS.new(
    automaton, superclass: @options[:superclass], omit_action_call: @options[:omit_actions]
  ).generate
end

#resolve_grammar_path(path) ⇒ Frontend::Resolution

RBS:

  • (String path) -> Frontend::Resolution

Parameters:

  • path (String)

Returns:



519
520
521
522
523
# File 'lib/ibex/cli.rb', line 519

def resolve_grammar_path(path)
  loader = Frontend::SourceLoader.new(record_reads: true)
  @last_resolver = Frontend::Resolver.new(path, mode: @options[:mode], loader: loader)
  @last_resolver.resolve
end

#run(arguments) ⇒ Integer

RBS:

  • (Array[String] arguments) -> Integer

Parameters:

  • arguments (Array[String])

Returns:

  • (Integer)


166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/ibex/cli.rb', line 166

def run(arguments)
  subcommand = dispatch_subcommand(arguments)
  return subcommand unless subcommand.nil?

  parser = option_parser
  remaining = parser.parse(arguments)
  validate_watch_information_options
  information = informational_result(parser)
  return information unless information.nil?

  validate_messages_options
  validate_generation_options
  path = input_path(remaining)
  validate_generation_paths!(path)
  if @options[:watch]
    run_watch_feature(path)
  else
    process_grammar(path)
  end
rescue OptionParser::ParseError, Ibex::Error, SystemCallError, SystemStackError => e
  @stderr.puts(e.message)
  1
end

#run_watch_feature(path) ⇒ Integer

RBS:

  • (String path) -> Integer

Parameters:

  • path (String)

Returns:

  • (Integer)


209
210
211
212
# File 'lib/ibex/cli.rb', line 209

def run_watch_feature(path)
  activate_cli_feature(:CLIWatch)
  send(:run_watch, path)
end

#same_file_target?(left, right) ⇒ Boolean

RBS:

  • (String left, String right) -> bool

Parameters:

  • left (String)
  • right (String)

Returns:

  • (Boolean)


437
438
439
440
441
442
443
444
445
446
447
448
# File 'lib/ibex/cli.rb', line 437

def same_file_target?(left, right)
  expanded_left = File.expand_path(left)
  expanded_right = File.expand_path(right)
  return true if expanded_left == expanded_right

  return true if File.exist?(expanded_left) && File.exist?(expanded_right) &&
                 File.identical?(expanded_left, expanded_right)

  portable_target_key(expanded_left) == portable_target_key(expanded_right)
rescue SystemCallError
  expanded_left == expanded_right
end

#validate_action_source_generation_optionsvoid

This method returns an undefined value.

RBS:

  • () -> void



381
382
383
384
385
386
# File 'lib/ibex/cli.rb', line 381

def validate_action_source_generation_options
  return unless @options[:action_source]

  raise Ibex::Error, "(cli):1:1: --action-source requires --emit=ruby" unless @options[:emit] == "ruby"
  raise Ibex::Error, "(cli):1:1: --action-source and --check-only cannot be combined" if @options[:check_only]
end

#validate_generation_optionsvoid

This method returns an undefined value.

RBS:

  • () -> void



361
362
363
364
365
366
367
368
369
370
# File 'lib/ibex/cli.rb', line 361

def validate_generation_options
  if @options[:entry_isolation] && @options[:from] == "automaton-ir"
    raise Ibex::Error, "(cli):1:1: --entry-isolation cannot be combined with --from=automaton-ir"
  end

  validate_watch_generation_options if @options[:watch]
  validate_manifest_generation_options
  validate_action_source_generation_options
  validate_verification_generation_options
end

#validate_generation_paths!(input_path, source_paths: [input_path]) ⇒ void

This method returns an undefined value.

RBS:

  • (String input_path, ?source_paths: Array[String]) -> void

Parameters:

  • input_path (String)
  • source_paths: (Array[String]) (defaults to: [input_path])


415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
# File 'lib/ibex/cli.rb', line 415

def validate_generation_paths!(input_path, source_paths: [input_path])
  outputs = generation_paths(input_path).except(:input, :messages)
  paths = outputs.filter_map do |kind, path|
    [kind, path] if path
  end #: Array[[Symbol, String]]
  source_entries = source_paths.map.with_index do |path, index|
    [index.zero? ? :input : :"include_#{index}", path]
  end
  source_entries << [:messages, @options[:messages]] if @options[:messages]
  paths.concat(source_entries)
  collision = paths.combination(2).find do |pair|
    left = pair.fetch(0)
    right = pair.fetch(1)
    same_file_target?(left.fetch(1), right.fetch(1))
  end
  return unless collision

  labels = collision.map { |kind, path| "#{kind}=#{path}" }
  raise Ibex::Error, "(cli):1:1: paths must be distinct: #{labels.join(', ')}"
end

#validate_manifest_generation_optionsvoid

This method returns an undefined value.

RBS:

  • () -> void



373
374
375
376
377
378
# File 'lib/ibex/cli.rb', line 373

def validate_manifest_generation_options
  return unless @options[:manifest]

  raise Ibex::Error, "(cli):1:1: --manifest requires --emit=ruby" unless @options[:emit] == "ruby"
  raise Ibex::Error, "(cli):1:1: --manifest and --check-only cannot be combined" if @options[:check_only]
end

#validate_verification_generation_optionsvoid

This method returns an undefined value.

RBS:

  • () -> void



389
390
391
392
393
394
# File 'lib/ibex/cli.rb', line 389

def validate_verification_generation_options
  return unless @options[:verify_output]

  raise Ibex::Error, "(cli):1:1: --check requires --emit=ruby" unless @options[:emit] == "ruby"
  raise Ibex::Error, "(cli):1:1: --check and --check-only cannot be combined" if @options[:check_only]
end

#validate_watch_generation_optionsvoid

This method returns an undefined value.

RBS:

  • () -> void



405
406
407
408
409
410
411
412
# File 'lib/ibex/cli.rb', line 405

def validate_watch_generation_options
  raise Ibex::Error, "(cli):1:1: --watch cannot be combined with --from" if @options[:from]
  raise Ibex::Error, "(cli):1:1: --watch cannot be combined with --check" if @options[:verify_output]
  raise Ibex::Error, "(cli):1:1: --watch cannot be combined with --check-only" if @options[:check_only]
  return if @options[:emit] == "ruby"

  raise Ibex::Error, "(cli):1:1: --watch requires --emit=ruby with file outputs"
end

#validate_watch_information_optionsvoid

This method returns an undefined value.

RBS:

  • () -> void



397
398
399
400
401
402
# File 'lib/ibex/cli.rb', line 397

def validate_watch_information_options
  return unless @options[:watch]
  return unless %i[version runtime_version copyright help].any? { |key| @options[key] }

  raise Ibex::Error, "(cli):1:1: --watch cannot be combined with information options"
end

#verify_file(path, source, label) ⇒ void

This method returns an undefined value.

RBS:

  • (String path, String source, String label) -> void

Parameters:

  • path (String)
  • source (String)
  • label (String)


683
684
685
686
687
688
# File 'lib/ibex/cli.rb', line 683

def verify_file(path, source, label)
  raise Ibex::Error, "#{path}:1:1: generated #{label} is missing" unless File.exist?(path)
  return if File.binread(path) == source

  raise Ibex::Error, "#{path}:1:1: generated #{label} is stale; regenerate it with the same options"
end

#verify_generated_outputs(automaton, output_path, source, action_source) ⇒ Integer

RBS:

  • (IR::Automaton automaton, String output_path, String source, String? action_source) -> Integer

Parameters:

  • automaton (IR::Automaton)
  • output_path (String)
  • source (String)
  • action_source (String, nil)

Returns:

  • (Integer)


674
675
676
677
678
679
680
# File 'lib/ibex/cli.rb', line 674

def verify_generated_outputs(automaton, output_path, source, action_source)
  verify_file(output_path, source, "parser")
  verify_file(rbs_output_path(output_path), rbs_source(automaton), "RBS signature") if @options[:rbs]
  verify_file(action_source_output_path(output_path), action_source, "action source") if action_source
  report_status("verified #{output_path}")
  0
end

#write_action_source(output_path, source) ⇒ void

This method returns an undefined value.

RBS:

  • (String output_path, String source) -> void

Parameters:

  • output_path (String)
  • source (String)


715
716
717
718
# File 'lib/ibex/cli.rb', line 715

def write_action_source(output_path, source)
  path = action_source_output_path(output_path)
  register_artifact(:action_source, path, source, status: true)
end

#write_railroad(grammar) ⇒ void

This method returns an undefined value.

RBS:

  • (IR::Grammar grammar) -> void

Parameters:



785
786
787
788
789
790
791
# File 'lib/ibex/cli.rb', line 785

def write_railroad(grammar)
  path = @options[:railroad]
  return unless path

  require_relative "codegen/railroad"
  register_artifact(:railroad, path, Codegen::Railroad.render(grammar))
end

#write_rbs(automaton, output_path) ⇒ void

This method returns an undefined value.

RBS:

  • (IR::Automaton automaton, String output_path) -> void

Parameters:



691
692
693
694
695
# File 'lib/ibex/cli.rb', line 691

def write_rbs(automaton, output_path)
  path = rbs_output_path(output_path)
  source = rbs_source(automaton)
  register_artifact(:rbs, path, source, status: true)
end

#write_visualizations(automaton) ⇒ void

This method returns an undefined value.

RBS:

  • (IR::Automaton automaton) -> void

Parameters:



766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
# File 'lib/ibex/cli.rb', line 766

def write_visualizations(automaton)
  dot_path = @options[:dot]
  mermaid_path = @options[:mermaid]
  html_path = @options[:html]
  if dot_path
    require_relative "codegen/dot"
    register_artifact(:dot, dot_path, Codegen::Dot.render(automaton))
  end
  if mermaid_path
    require_relative "codegen/mermaid"
    register_artifact(:mermaid, mermaid_path, Codegen::Mermaid.render(automaton))
  end
  return unless html_path

  require_relative "codegen/html"
  register_artifact(:html, html_path, Codegen::HTML.render(automaton))
end