ibex 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +43 -0
  3. data/CHANGELOG.md +30 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +194 -0
  6. data/Rakefile +25 -0
  7. data/Steepfile +10 -0
  8. data/docs/architecture.md +99 -0
  9. data/docs/compat-notes.md +37 -0
  10. data/docs/grammar-reference.md +127 -0
  11. data/docs/lexer-coverage.md +14 -0
  12. data/docs/phase10-extensions.md +27 -0
  13. data/docs/racc-migration.md +47 -0
  14. data/exe/ibex +6 -0
  15. data/gemfiles/Gemfile +7 -0
  16. data/gemfiles/Gemfile.lock +98 -0
  17. data/lib/ibex/analysis/sets.rb +194 -0
  18. data/lib/ibex/analysis.rb +8 -0
  19. data/lib/ibex/cli/counterexample_options.rb +38 -0
  20. data/lib/ibex/cli/outputs.rb +88 -0
  21. data/lib/ibex/cli.rb +333 -0
  22. data/lib/ibex/codegen/dot.rb +52 -0
  23. data/lib/ibex/codegen/html.rb +110 -0
  24. data/lib/ibex/codegen/rbs.rb +54 -0
  25. data/lib/ibex/codegen/report.rb +149 -0
  26. data/lib/ibex/codegen/ruby.rb +235 -0
  27. data/lib/ibex/codegen/symbol_labels.rb +18 -0
  28. data/lib/ibex/error.rb +7 -0
  29. data/lib/ibex/frontend/action_scanner.rb +266 -0
  30. data/lib/ibex/frontend/ast.rb +146 -0
  31. data/lib/ibex/frontend/bootstrap_parser.rb +163 -0
  32. data/lib/ibex/frontend/dsl.rb +196 -0
  33. data/lib/ibex/frontend/generated_parser.rb +434 -0
  34. data/lib/ibex/frontend/generated_parser_base.rb +273 -0
  35. data/lib/ibex/frontend/grammar.y +156 -0
  36. data/lib/ibex/frontend/lexer.rb +165 -0
  37. data/lib/ibex/frontend/parser/declarations.rb +163 -0
  38. data/lib/ibex/frontend/parser/rules.rb +154 -0
  39. data/lib/ibex/frontend/parser.rb +23 -0
  40. data/lib/ibex/frontend/regenerator.rb +41 -0
  41. data/lib/ibex/frontend/source_cursor.rb +94 -0
  42. data/lib/ibex/frontend/token_adapter/declaration_state.rb +248 -0
  43. data/lib/ibex/frontend/token_adapter/delimiter_tracker.rb +37 -0
  44. data/lib/ibex/frontend/token_adapter/rule_state.rb +162 -0
  45. data/lib/ibex/frontend/token_adapter.rb +108 -0
  46. data/lib/ibex/frontend.rb +28 -0
  47. data/lib/ibex/ir/automaton_ir.rb +109 -0
  48. data/lib/ibex/ir/grammar_ir.rb +192 -0
  49. data/lib/ibex/ir/serialize.rb +154 -0
  50. data/lib/ibex/ir.rb +53 -0
  51. data/lib/ibex/lalr/builder.rb +270 -0
  52. data/lib/ibex/lalr/conflict.rb +120 -0
  53. data/lib/ibex/lalr/conflict_search.rb +332 -0
  54. data/lib/ibex/lalr/conflict_search_limits.rb +39 -0
  55. data/lib/ibex/lalr/counterexample.rb +138 -0
  56. data/lib/ibex/lalr/default_reductions.rb +67 -0
  57. data/lib/ibex/lalr.rb +27 -0
  58. data/lib/ibex/normalize/declarations.rb +60 -0
  59. data/lib/ibex/normalize/diagnostics.rb +99 -0
  60. data/lib/ibex/normalize/expander.rb +218 -0
  61. data/lib/ibex/normalize/expression.rb +54 -0
  62. data/lib/ibex/normalize.rb +166 -0
  63. data/lib/ibex/runtime/parser.rb +360 -0
  64. data/lib/ibex/runtime.rb +8 -0
  65. data/lib/ibex/tables.rb +125 -0
  66. data/lib/ibex/version.rb +6 -0
  67. data/lib/ibex.rb +23 -0
  68. data/sig/ibex/analysis/sets.rbs +72 -0
  69. data/sig/ibex/analysis.rbs +6 -0
  70. data/sig/ibex/cli/counterexample_options.rbs +16 -0
  71. data/sig/ibex/cli/outputs.rbs +31 -0
  72. data/sig/ibex/cli.rbs +108 -0
  73. data/sig/ibex/codegen/dot.rbs +19 -0
  74. data/sig/ibex/codegen/html.rbs +35 -0
  75. data/sig/ibex/codegen/rbs.rbs +31 -0
  76. data/sig/ibex/codegen/report.rbs +39 -0
  77. data/sig/ibex/codegen/ruby.rbs +92 -0
  78. data/sig/ibex/codegen/symbol_labels.rbs +11 -0
  79. data/sig/ibex/error.rbs +7 -0
  80. data/sig/ibex/frontend/action_scanner.rbs +74 -0
  81. data/sig/ibex/frontend/ast.rbs +223 -0
  82. data/sig/ibex/frontend/bootstrap_parser.rbs +80 -0
  83. data/sig/ibex/frontend/dsl.rbs +124 -0
  84. data/sig/ibex/frontend/generated_parser.rbs +163 -0
  85. data/sig/ibex/frontend/generated_parser_base.rbs +114 -0
  86. data/sig/ibex/frontend/lexer.rbs +61 -0
  87. data/sig/ibex/frontend/parser/declarations.rbs +56 -0
  88. data/sig/ibex/frontend/parser/rules.rbs +48 -0
  89. data/sig/ibex/frontend/parser.rbs +16 -0
  90. data/sig/ibex/frontend/regenerator.rbs +16 -0
  91. data/sig/ibex/frontend/source_cursor.rbs +73 -0
  92. data/sig/ibex/frontend/token_adapter/declaration_state.rbs +97 -0
  93. data/sig/ibex/frontend/token_adapter/delimiter_tracker.rbs +25 -0
  94. data/sig/ibex/frontend/token_adapter/rule_state.rbs +77 -0
  95. data/sig/ibex/frontend/token_adapter.rbs +67 -0
  96. data/sig/ibex/frontend.rbs +15 -0
  97. data/sig/ibex/ir/automaton_ir.rbs +77 -0
  98. data/sig/ibex/ir/grammar_ir.rbs +143 -0
  99. data/sig/ibex/ir/serialize.rbs +50 -0
  100. data/sig/ibex/ir.rbs +43 -0
  101. data/sig/ibex/lalr/builder.rbs +85 -0
  102. data/sig/ibex/lalr/conflict.rbs +47 -0
  103. data/sig/ibex/lalr/conflict_search.rbs +131 -0
  104. data/sig/ibex/lalr/conflict_search_limits.rbs +29 -0
  105. data/sig/ibex/lalr/counterexample.rbs +53 -0
  106. data/sig/ibex/lalr/default_reductions.rbs +20 -0
  107. data/sig/ibex/lalr.rbs +23 -0
  108. data/sig/ibex/normalize/declarations.rbs +20 -0
  109. data/sig/ibex/normalize/diagnostics.rbs +32 -0
  110. data/sig/ibex/normalize/expander.rbs +66 -0
  111. data/sig/ibex/normalize/expression.rbs +21 -0
  112. data/sig/ibex/normalize.rbs +96 -0
  113. data/sig/ibex/runtime/parser.rbs +167 -0
  114. data/sig/ibex/runtime.rbs +6 -0
  115. data/sig/ibex/tables.rbs +50 -0
  116. data/sig/ibex/version.rbs +6 -0
  117. data/sig/ibex.rbs +6 -0
  118. metadata +161 -0
@@ -0,0 +1,47 @@
1
+ # Migrating from racc
2
+
3
+ Ibex targets grammar-file compatibility, generated parser public API compatibility, and the main racc CLI options. It does not
4
+ copy racc's internal table arrays, internal method names, native runtime, or generated source layout.
5
+
6
+ ## Typical migration
7
+
8
+ 1. Run `ibex -o parser.rb grammar.y` in place of `racc -o parser.rb grammar.y`.
9
+ 2. Change the generated-file runtime dependency from deployment packaging only; application calls to `do_parse`, `yyparse`,
10
+ `next_token`, `on_error`, `token_to_str`, `yyerror`, `yyerrok`, and `yyaccept` remain the same.
11
+ 3. Use `-E` if the generated parser must be a single file with no installed Ibex gem.
12
+ 4. Keep the default `--mode=racc` until intentionally adopting EBNF or names. Extended grammars can make that choice locally by
13
+ placing `pragma extended` immediately after their class header instead of requiring `--mode=extended` at each invocation.
14
+
15
+ ## CLI mapping
16
+
17
+ | Option | Ibex behavior |
18
+ |---|---|
19
+ | `-o`, `--output-file` | Select generated parser path |
20
+ | `-t`, `--debug`; `-g` | Generate a debug-capable parser; `-g` is accepted as an obsolete alias |
21
+ | `-v`, `-O` | Write the independent state report and optional path |
22
+ | `-e [RUBY]` | Add a shebang and executable permission |
23
+ | `-E`, `--embedded` | Embed the Pure Ruby runtime |
24
+ | `-F`, `--frozen` | Accepted; Ibex always emits frozen-string magic comments |
25
+ | `--rbs[=FILE]` | Ibex extension; emit a generated parser signature |
26
+ | `--warnings=all,error` | Ibex extension; display or promote structured grammar diagnostics |
27
+ | `--line-convert-all`, `-l` | Map header/inner/footer too, or disable all source mapping |
28
+ | `-a` | Generate methods for implicit actions |
29
+ | `--superclass` | Override the grammar superclass |
30
+ | `-C`, `-S` | Check only; show pipeline status |
31
+ | `-P`, `-D` | Accepted no-ops because they expose generator internals |
32
+ | `--version`, `--runtime-version`, `--copyright`, `--help` | Informational output |
33
+
34
+ Ibex defaults to `<input>.rb`; racc 1.8.1 was observed to default to `<input>.tab.rb`. Use `-o` for portable scripts.
35
+ By default, semantic actions and `inner` methods report grammar-file lines while `header` and `footer` retain generated-file
36
+ lines. `--line-convert-all` maps every user-code section; `-l` maps none. The mapping is retained through IR JSON resumption.
37
+
38
+ ## Known differences
39
+
40
+ - Generated source and internal table representations are intentionally different.
41
+ - `.output` report formatting is independent and contains additional resolved-conflict and witness data.
42
+ - An undeclared invalid token was observed to enter racc's `error` production without `on_error`. Ibex intentionally reports the
43
+ unknown lookahead through `on_error` first, then recovers if the callback returns. Declared invalid tokens match in the
44
+ black-box recovery probe.
45
+ - `require "racc/parser"` replacement and previously generated racc parser table compatibility are out of scope.
46
+
47
+ See [compatibility observations](compat-notes.md) for the tested version and probe set.
data/exe/ibex ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "ibex/cli"
5
+
6
+ exit Ibex::CLI.start(ARGV)
data/gemfiles/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rbs", "4.0.3", require: false
6
+ gem "rbs-inline", "0.14.0", require: false
7
+ gem "steep", "2.0.0", require: false
@@ -0,0 +1,98 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ ast (2.4.3)
5
+ concurrent-ruby (1.3.8)
6
+ csv (3.3.5)
7
+ ffi (1.17.4)
8
+ ffi (1.17.4-arm64-darwin)
9
+ fileutils (1.8.0)
10
+ json (2.21.1)
11
+ language_server-protocol (3.17.0.6)
12
+ listen (3.10.0)
13
+ logger
14
+ rb-fsevent (~> 0.10, >= 0.10.3)
15
+ rb-inotify (~> 0.9, >= 0.9.10)
16
+ logger (1.7.0)
17
+ parser (3.3.12.0)
18
+ ast (~> 2.4.1)
19
+ racc
20
+ prism (1.9.0)
21
+ racc (1.8.1)
22
+ rainbow (3.1.1)
23
+ rb-fsevent (0.11.2)
24
+ rb-inotify (0.11.1)
25
+ ffi (~> 1.0)
26
+ rbs (4.0.3)
27
+ logger
28
+ prism (>= 1.6.0)
29
+ tsort
30
+ rbs-inline (0.14.0)
31
+ prism (>= 0.29)
32
+ rbs (~> 4.0)
33
+ securerandom (0.4.1)
34
+ steep (2.0.0)
35
+ concurrent-ruby (>= 1.1.10)
36
+ csv (>= 3.0.9)
37
+ fileutils (>= 1.1.0)
38
+ json (>= 2.1.0)
39
+ language_server-protocol (>= 3.17.0.4, < 4.0)
40
+ listen (~> 3.0)
41
+ logger (>= 1.3.0)
42
+ parser (>= 3.2)
43
+ prism (>= 0.25.0)
44
+ rainbow (>= 2.2.2, < 4.0)
45
+ rbs (~> 4.0)
46
+ securerandom (>= 0.1)
47
+ strscan (>= 1.0.0)
48
+ terminal-table (>= 2, < 5)
49
+ uri (>= 0.12.0)
50
+ strscan (3.1.8)
51
+ terminal-table (4.0.0)
52
+ unicode-display_width (>= 1.1.1, < 4)
53
+ tsort (0.2.0)
54
+ unicode-display_width (3.2.0)
55
+ unicode-emoji (~> 4.1)
56
+ unicode-emoji (4.2.0)
57
+ uri (1.1.1)
58
+
59
+ PLATFORMS
60
+ arm64-darwin-24
61
+ ruby
62
+
63
+ DEPENDENCIES
64
+ rbs (= 4.0.3)
65
+ rbs-inline (= 0.14.0)
66
+ steep (= 2.0.0)
67
+
68
+ CHECKSUMS
69
+ ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
70
+ bundler (4.0.13) sha256=19f08be7f27022cf0b89f27da0b044ae075e8270a9ef44ad248a932614e1ca3b
71
+ concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1
72
+ csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f
73
+ ffi (1.17.4) sha256=bcd1642e06f0d16fc9e09ac6d49c3a7298b9789bcb58127302f934e437d60acf
74
+ ffi (1.17.4-arm64-darwin) sha256=19071aaf1419251b0a46852abf960e77330a3b334d13a4ab51d58b31a937001b
75
+ fileutils (1.8.0) sha256=8c6b1df54e2540bdb2f39258f08af78853aa70bad52b4d394bbc6424593c6e02
76
+ json (2.21.1) sha256=13a43df75d95641443f5702dff350f237164a9d811ff0f2c2800d4d980220583
77
+ language_server-protocol (3.17.0.6) sha256=5ef2c0c138f8267e1bc631d3328347d354f96724b0af22f2c79516120443b7f0
78
+ listen (3.10.0) sha256=c6e182db62143aeccc2e1960033bebe7445309c7272061979bb098d03760c9d2
79
+ logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
80
+ parser (3.3.12.0) sha256=21a6d7f755d5a24dfbdc6e6b772e4e879a52e7631a88bc5a3a134606052c9828
81
+ prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
82
+ racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
83
+ rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
84
+ rb-fsevent (0.11.2) sha256=43900b972e7301d6570f64b850a5aa67833ee7d87b458ee92805d56b7318aefe
85
+ rb-inotify (0.11.1) sha256=a0a700441239b0ff18eb65e3866236cd78613d6b9f78fea1f9ac47a85e47be6e
86
+ rbs (4.0.3) sha256=5a7bf70e2628549d9a1f44eae447b2cfe55968a9c60cfff52693a4bdcc020e14
87
+ rbs-inline (0.14.0) sha256=eaf47b46690d63acddab1f6804c9cc205a4bc78e637cfc421a0b1239874ef51c
88
+ securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
89
+ steep (2.0.0) sha256=6eb0ecc09637bbb54f0a5f2cf63daea6d3208ccace64b4f1107d976333605c30
90
+ strscan (3.1.8) sha256=aae2db611a225559f21ffbb71765c9a4e60fd262534a9ea84f4f11c7f32f679e
91
+ terminal-table (4.0.0) sha256=f504793203f8251b2ea7c7068333053f0beeea26093ec9962e62ea79f94301d2
92
+ tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
93
+ unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
94
+ unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
95
+ uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6
96
+
97
+ BUNDLED WITH
98
+ 4.0.13
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ibex
4
+ module Analysis
5
+ # Computes nullable, FIRST, and FOLLOW sets over Grammar IR using integer bitsets.
6
+ class Sets
7
+ attr_reader :nullable_bits #: Integer
8
+ attr_reader :first_bits #: Array[Integer]
9
+ attr_reader :follow_bits #: Array[Integer]
10
+
11
+ # @rbs @grammar: IR::Grammar
12
+
13
+ # @rbs (IR::Grammar grammar) -> void
14
+ def initialize(grammar)
15
+ @grammar = grammar
16
+ @nullable_bits = 0
17
+ @first_bits = Array.new(grammar.symbols.length, 0)
18
+ @follow_bits = Array.new(grammar.symbols.length, 0)
19
+ grammar.terminals.each { |terminal| @first_bits[terminal.id] = bit(terminal.id) }
20
+ compute_nullable
21
+ compute_first
22
+ compute_follow
23
+ end
24
+
25
+ # @rbs (Integer | String | Symbol | IR::GrammarSymbol symbol) -> bool
26
+ def nullable?(symbol)
27
+ id = symbol_id(symbol)
28
+ @nullable_bits.anybits?(bit(id))
29
+ end
30
+
31
+ # @rbs (Integer | String | Symbol | IR::GrammarSymbol symbol) -> Array[String]
32
+ def first(symbol)
33
+ terminal_names(@first_bits.fetch(symbol_id(symbol)))
34
+ end
35
+
36
+ # @rbs (Integer | String | Symbol | IR::GrammarSymbol symbol) -> Array[String]
37
+ def follow(symbol)
38
+ definition = definition_for(symbol)
39
+ raise Ibex::Error, "(analysis):1:1: FOLLOW is only defined for nonterminals" unless definition.nonterminal?
40
+
41
+ terminal_names(@follow_bits.fetch(definition.id))
42
+ end
43
+
44
+ # @rbs (Array[Integer] symbol_ids) -> Integer
45
+ def first_of_sequence(symbol_ids)
46
+ bits = 0
47
+ symbol_ids.each do |id|
48
+ bits |= @first_bits.fetch(id)
49
+ return bits unless nullable_id?(id)
50
+ end
51
+ bits
52
+ end
53
+
54
+ # @rbs (Array[Integer] symbol_ids) -> bool
55
+ def sequence_nullable?(symbol_ids)
56
+ symbol_ids.all? { |id| nullable_id?(id) }
57
+ end
58
+
59
+ private
60
+
61
+ # @rbs () -> void
62
+ def compute_nullable
63
+ dependencies, remaining, queue = nullable_worklist
64
+
65
+ until queue.empty?
66
+ id = queue.shift
67
+ next if nullable_id?(id)
68
+
69
+ @nullable_bits |= bit(id)
70
+ dependencies[id].each do |production|
71
+ remaining[production.id] -= 1
72
+ queue << production.lhs if remaining[production.id].zero?
73
+ end
74
+ end
75
+ end
76
+
77
+ # @rbs () -> [Array[Array[IR::Production]], Array[Integer], Array[Integer]]
78
+ def nullable_worklist
79
+ dependencies = Array.new(@grammar.symbols.length) { [] }
80
+ remaining = Array.new(@grammar.productions.length, 0)
81
+ queue = [] #: Array[Integer]
82
+ @grammar.productions.each do |production|
83
+ next if production.rhs.any? { |id| required_symbol_by_id(id).terminal? }
84
+
85
+ remaining[production.id] = production.rhs.length
86
+ production.rhs.each { |id| dependencies[id] << production }
87
+ queue << production.lhs if production.rhs.empty?
88
+ end
89
+ [dependencies, remaining, queue]
90
+ end
91
+
92
+ # @rbs () -> void
93
+ def compute_first
94
+ dependencies = Array.new(@grammar.symbols.length) { [] }
95
+ @grammar.productions.each do |production|
96
+ production.rhs.each do |id|
97
+ dependencies[id] << production.lhs
98
+ break unless nullable_id?(id)
99
+ end
100
+ end
101
+
102
+ propagate_bits(@first_bits, dependencies, @grammar.terminals.map(&:id))
103
+ end
104
+
105
+ # @rbs () -> void
106
+ def compute_follow
107
+ start_definition = @grammar.symbol(@grammar.start)
108
+ raise Ibex::Error, "(analysis):1:1: unknown start symbol #{@grammar.start}" unless start_definition
109
+
110
+ start_id = start_definition.id
111
+ @follow_bits[start_id] |= bit(0)
112
+ dependencies = Array.new(@grammar.symbols.length) { [] }
113
+ @grammar.productions.each { |production| initialize_follow(production, dependencies) }
114
+ seeds = @grammar.nonterminals.filter_map { |symbol| symbol.id unless @follow_bits[symbol.id].zero? }
115
+ propagate_bits(@follow_bits, dependencies, seeds)
116
+ end
117
+
118
+ # @rbs (IR::Production production, Array[Array[Integer]] dependencies) -> void
119
+ def initialize_follow(production, dependencies)
120
+ trailer = 0
121
+ suffix_nullable = true
122
+ production.rhs.reverse_each do |id|
123
+ definition = required_symbol_by_id(id)
124
+ if definition.nonterminal?
125
+ @follow_bits[id] |= trailer
126
+ dependencies[production.lhs] << id if suffix_nullable
127
+ trailer = @first_bits[id] | (nullable_id?(id) ? trailer : 0)
128
+ suffix_nullable &&= nullable_id?(id)
129
+ else
130
+ trailer = @first_bits[id]
131
+ suffix_nullable = false
132
+ end
133
+ end
134
+ end
135
+
136
+ # @rbs (Array[Integer] sets, Array[Array[Integer]] dependencies, Array[Integer] seeds) -> void
137
+ def propagate_bits(sets, dependencies, seeds)
138
+ queue = seeds.dup
139
+ queued = Array.new(@grammar.symbols.length, false)
140
+ queue.each { |id| queued[id] = true }
141
+
142
+ until queue.empty?
143
+ source = queue.shift
144
+ queued[source] = false
145
+ dependencies[source].each do |target|
146
+ combined = sets[target] | sets[source]
147
+ next if combined == sets[target]
148
+
149
+ sets[target] = combined
150
+ next if queued[target]
151
+
152
+ queued[target] = true
153
+ queue << target
154
+ end
155
+ end
156
+ end
157
+
158
+ # @rbs (Integer id) -> bool
159
+ def nullable_id?(id)
160
+ @nullable_bits.anybits?(bit(id))
161
+ end
162
+
163
+ # @rbs (Integer bits) -> Array[String]
164
+ def terminal_names(bits)
165
+ @grammar.terminals.filter_map { |terminal| terminal.name if bits.anybits?(bit(terminal.id)) }
166
+ end
167
+
168
+ # @rbs (Integer | String | Symbol | IR::GrammarSymbol symbol) -> Integer
169
+ def symbol_id(symbol)
170
+ definition_for(symbol).id
171
+ end
172
+
173
+ # @rbs (Integer | String | Symbol | IR::GrammarSymbol symbol) -> IR::GrammarSymbol
174
+ def definition_for(symbol)
175
+ return symbol if symbol.is_a?(IR::GrammarSymbol)
176
+
177
+ definition = symbol.is_a?(Integer) ? @grammar.symbol_by_id(symbol) : @grammar.symbol(symbol.to_s)
178
+ return definition if definition
179
+
180
+ raise Ibex::Error, "(analysis):1:1: unknown symbol #{symbol}"
181
+ end
182
+
183
+ # @rbs (Integer id) -> IR::GrammarSymbol
184
+ def required_symbol_by_id(id)
185
+ @grammar.symbol_by_id(id) || raise(Ibex::Error, "(analysis):1:1: unknown symbol id #{id}")
186
+ end
187
+
188
+ # @rbs (Integer id) -> Integer
189
+ def bit(id)
190
+ 1 << id
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "analysis/sets"
4
+
5
+ module Ibex
6
+ module Analysis
7
+ end
8
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ibex
4
+ # Counterexample report limits and their command-line validation.
5
+ module CLICounterexampleOptions
6
+ DEFAULTS = {
7
+ counterexample_max_tokens: LALR::Counterexample::DEFAULT_MAX_TOKENS,
8
+ counterexample_max_configurations: LALR::Counterexample::DEFAULT_MAX_CONFIGURATIONS
9
+ }.freeze #: Hash[Symbol, Integer]
10
+
11
+ private
12
+
13
+ # @rbs (OptionParser options) -> void
14
+ def add_counterexample_options(options)
15
+ options.on("--counterexamples", "include conflict counterexamples in a report") { @options[:verbose] = true }
16
+ options.on("--counterexample-max-tokens=N", Integer, "maximum counterexample search token budget") do |value|
17
+ @options[:counterexample_max_tokens] = positive_counterexample_limit(value, "--counterexample-max-tokens")
18
+ end
19
+ options.on(
20
+ "--counterexample-max-configurations=N", Integer, "maximum counterexample search configuration budget"
21
+ ) do |value|
22
+ @options[:counterexample_max_configurations] = positive_counterexample_limit(
23
+ value, "--counterexample-max-configurations"
24
+ )
25
+ end
26
+ end
27
+
28
+ # @rbs (Integer value, String option) -> Integer
29
+ def positive_counterexample_limit(value, option)
30
+ if value.positive?
31
+ @options[:verbose] = true
32
+ return value
33
+ end
34
+
35
+ raise Ibex::Error, "(cli):1:1: #{option} must be positive"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ibex
4
+ # Files, warnings, and progress emitted by CLI pipeline stages.
5
+ module CLIOutputs
6
+ WARNING_MESSAGES = {
7
+ undeclared_terminal: ->(warning) { "undeclared terminal #{warning[:symbol]}" },
8
+ unused_terminal: ->(warning) { "unused terminal #{warning[:symbol]}" },
9
+ unreachable_nonterminal: ->(warning) { "unreachable nonterminal #{warning[:symbol]}" },
10
+ duplicate_production: lambda do |warning|
11
+ "duplicate production #{warning[:production]} (first defined as #{warning[:original]})"
12
+ end,
13
+ empty_language: ->(warning) { "start symbol #{warning[:symbol]} derives no terminal sentence" }
14
+ }.freeze #: Hash[Symbol, ^(IR::grammar_warning) -> String]
15
+
16
+ private
17
+
18
+ # @rbs (String value) -> Array[Symbol]
19
+ def warning_categories(value)
20
+ categories = value.split(",").map(&:strip).reject(&:empty?).map(&:to_sym)
21
+ unknown = categories - %i[all error none]
22
+ raise OptionParser::InvalidArgument, "unknown warning category #{unknown.first}" if unknown.any?
23
+ if categories.empty? || (categories.include?(:none) && categories.length > 1)
24
+ raise OptionParser::InvalidArgument, "warning category none cannot be combined"
25
+ end
26
+
27
+ categories
28
+ end
29
+
30
+ # @rbs (IR::Grammar grammar, String input_path) -> void
31
+ def handle_grammar_warnings(grammar, input_path)
32
+ categories = @options[:warnings]
33
+ return if categories.nil? || categories.include?(:none) || grammar.warnings.empty?
34
+
35
+ messages = grammar.warnings.map { |warning| format_grammar_warning(warning, input_path) }
36
+ if categories.include?(:error)
37
+ promoted = messages.map { |message| message.sub(": warning:", ": warning treated as error:") }
38
+ raise Ibex::Error, promoted.join("\n")
39
+ end
40
+
41
+ messages.each { |message| @stderr.puts(message) }
42
+ end
43
+
44
+ # @rbs (IR::grammar_warning warning, String input_path) -> String
45
+ def format_grammar_warning(warning, input_path)
46
+ location = warning[:loc]
47
+ rendered = if location
48
+ "#{location[:file] || input_path}:#{location[:line] || 1}:#{location[:column] || 1}"
49
+ else
50
+ "#{input_path}:1:1"
51
+ end
52
+ formatter = WARNING_MESSAGES.fetch(warning[:type]) { ->(item) { item[:type].to_s.tr("_", " ") } }
53
+ "#{rendered}: warning: #{formatter.call(warning)}"
54
+ end
55
+
56
+ # @rbs (IR::Automaton automaton, String input_path) -> void
57
+ def report_conflicts(automaton, input_path)
58
+ summary = automaton.conflict_summary
59
+ unless summary[:expectation_met]
60
+ @stderr.puts("#{input_path}:1:1: #{summary[:sr]} shift/reduce conflicts; expected #{summary[:expected_sr]}")
61
+ end
62
+ @stderr.puts("#{input_path}:1:1: #{summary[:rr]} reduce/reduce conflicts") if summary[:rr].positive?
63
+ end
64
+
65
+ # @rbs (IR::Automaton automaton, String input_path) -> void
66
+ def write_report(automaton, input_path)
67
+ path = @options[:log_file] || default_output_path(input_path, ".output")
68
+ report = Codegen::Report.render(
69
+ automaton,
70
+ max_tokens: @options[:counterexample_max_tokens],
71
+ max_configurations: @options[:counterexample_max_configurations]
72
+ )
73
+ File.write(path, report)
74
+ report_status("wrote #{path}")
75
+ end
76
+
77
+ # @rbs (String input_path, String extension) -> String
78
+ def default_output_path(input_path, extension)
79
+ replaced = input_path.sub(/\.[^.]+\z/, extension)
80
+ replaced == input_path ? "#{input_path}#{extension}" : replaced
81
+ end
82
+
83
+ # @rbs (String message) -> void
84
+ def report_status(message)
85
+ @stderr.puts("ibex: #{message}") if @options[:status]
86
+ end
87
+ end
88
+ end