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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1cf3b4cceb041622899923a174e36d413adb399c22cdb44f8023f1f199acea73
4
+ data.tar.gz: 62ac1c3098103ac5270a8c684065424fcfc8bb2759332b3414672454ecbb25ca
5
+ SHA512:
6
+ metadata.gz: ec45347b6c9ec5f427261ade9ea4102a2b7b6583bcec47ee024d920fc624018bae9315a7129657977de47241e867ef23809ccef372169cb8e1d6a9ad858e31b3
7
+ data.tar.gz: a163ab0fb84918570c2f62a9299d395653a0896c510e43a7672677803fe73f6010d74305868e1f6e5018b13b30d053c1a4d0852e16a0941f56a123bb883e7422
data/.rubocop.yml ADDED
@@ -0,0 +1,43 @@
1
+ AllCops:
2
+ CacheRootDirectory: tmp/rubocop_cache
3
+ NewCops: enable
4
+ TargetRubyVersion: 3.0
5
+ SuggestExtensions: false
6
+ Exclude:
7
+ - "pkg/**/*"
8
+ - "tmp/**/*"
9
+ - "vendor/**/*"
10
+ - "lib/ibex/frontend/generated_parser.rb"
11
+
12
+ Layout/LineLength:
13
+ Max: 120
14
+
15
+ Layout/LeadingCommentSpace:
16
+ AllowRBSInlineAnnotation: true
17
+
18
+ Style/StringLiterals:
19
+ EnforcedStyle: double_quotes
20
+
21
+ Metrics/AbcSize:
22
+ Max: 36
23
+
24
+ Metrics/ClassLength:
25
+ Max: 200
26
+
27
+ Metrics/CyclomaticComplexity:
28
+ Max: 10
29
+
30
+ Metrics/MethodLength:
31
+ Max: 30
32
+
33
+ Metrics/ModuleLength:
34
+ Max: 150
35
+
36
+ Metrics/ParameterLists:
37
+ Max: 12
38
+
39
+ Metrics/PerceivedComplexity:
40
+ Max: 10
41
+
42
+ Style/Documentation:
43
+ Enabled: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,30 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ - Establish the Pure Ruby gem, Minitest, lint, CI, and CLI foundations.
6
+ - Add a table-driven Pure Ruby LR runtime with pull/push APIs, recovery, and tracing.
7
+ - Tokenize grammar files with positions, opaque Ruby actions, comments, and user-code blocks.
8
+ - Parse compatible and extended grammar syntax into a serializable, location-preserving AST.
9
+ - Normalize grammars into immutable, versioned JSON IR with EBNF expansion and diagnostics.
10
+ - Compute nullable, FIRST, and FOLLOW sets with deterministic integer bitsets.
11
+ - Build deterministic LALR(1) automata, resolve and retain conflicts, and render state reports.
12
+ - Generate plain or compact Ruby parsers with embedded runtime and source-line mapping support.
13
+ - Preserve per-block user-code source maps through IR and implement compatible default, all-code, and disabled line conversion.
14
+ - Complete the compatible CLI surface and add optional racc black-box result comparisons.
15
+ - Add extended EBNF/named-reference integration, source-facing EBNF labels in text/DOT/HTML reports, and resumable IR pipelines.
16
+ - Add selectable SLR, LALR(1), and canonical LR(1) construction strategies.
17
+ - Add a Ruby DSL frontend that converges on the existing Grammar AST and IR pipeline.
18
+ - Add unifying conflict counterexamples with complete competing derivation trees and bounded nonunifying fallback.
19
+ - Add generated parser RBS output, shipped runtime signatures, and strict structured grammar diagnostics.
20
+ - Expand generated signatures and Steep checking to every library source, including concrete frontend/IR/LALR/codegen/CLI domain
21
+ types and the self-hosted parser.
22
+ - Support quoted, interpolated, and multiple Ruby heredocs plus recursively nested grouped EBNF.
23
+ - Define unknown external tokens to invoke `on_error` before yacc-style recovery.
24
+ - Add post-commit shift, reduction, and successful-recovery observation hooks to the runtime.
25
+ - Add complete quickstart, grammar, migration, architecture, and extension documentation with an executable README test.
26
+ - Self-host the grammar frontend from a committed Ibex grammar with deterministic regeneration and bootstrap parity checks.
27
+ - Version generated parser tables and reject missing or unsupported formats before consuming input.
28
+ - Add grammar-local `pragma extended` selection and document the action and named-reference boundary inside EBNF groups.
29
+ - Add fixed-seed pipeline properties, versioned Grammar/Automaton IR fixtures, and a reproducible whole-builder benchmark.
30
+ - Keep the whole-library Steep statistics in the README synchronized and enforce them in CI.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Yudai Takada
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,194 @@
1
+ # Ibex
2
+
3
+ Ibex is a Pure Ruby LR parser generator. It accepts racc-compatible grammar files, generates parsers with the familiar
4
+ `do_parse` / `yyparse` API, and requires no C or Java extension. Its staged Grammar IR and Automaton IR can also drive extended
5
+ EBNF syntax, diagnostics, visualizations, and alternate LR construction algorithms.
6
+
7
+ ## Requirements and installation
8
+
9
+ Ibex supports Ruby 3.0 or later and has no runtime gem dependencies. From a source checkout:
10
+
11
+ ```sh
12
+ bundle install
13
+ bundle exec rake
14
+ ```
15
+
16
+ Build and install the local gem when you want the `ibex` executable on your `PATH`:
17
+
18
+ ```sh
19
+ gem build ibex.gemspec
20
+ gem install ./ibex-0.1.0.gem
21
+ ```
22
+
23
+ ## Three-minute calculator
24
+
25
+ Save this as `calculator.y`:
26
+
27
+ <!-- calculator-grammar:start -->
28
+ ```text
29
+ class Calculator
30
+ token NUM
31
+ preclow
32
+ left '+'
33
+ left '*'
34
+ prechigh
35
+ rule
36
+ expr : expr '+' expr { result = val[0] + val[2] }
37
+ | expr '*' expr { result = val[0] * val[2] }
38
+ | NUM { result = val[0] }
39
+ end
40
+ ---- inner
41
+ def parse_tokens(tokens)
42
+ @tokens = tokens
43
+ do_parse
44
+ end
45
+
46
+ def next_token
47
+ @tokens.shift
48
+ end
49
+ ---- footer
50
+ if $PROGRAM_NAME == __FILE__
51
+ tokens = [[:NUM, 2], ['+', nil], [:NUM, 3], ['*', nil], [:NUM, 4]]
52
+ puts Calculator.new.parse_tokens(tokens)
53
+ end
54
+ ```
55
+ <!-- calculator-grammar:end -->
56
+
57
+ Generate and run it:
58
+
59
+ ```sh
60
+ ibex calculator.y
61
+ ruby calculator.rb
62
+ # 14
63
+ ```
64
+
65
+ From a checkout without installing the gem, use `bundle exec ruby -Ilib exe/ibex calculator.y` and
66
+ `bundle exec ruby -Ilib calculator.rb` instead.
67
+
68
+ Ibex generates compact tables by default. Compatibility-safe default reductions shrink profitable states while retaining
69
+ explicit error cells, including recovery and undeclared-token behavior. `--table=plain` produces inspectable Hash rows, while
70
+ `-E` embeds the runtime into a single dependency-free output file.
71
+
72
+ ## Lexer contract
73
+
74
+ Ibex does not generate a lexer. A pull parser implements `next_token` and returns `[token, value]`; `false` or `nil` marks EOF.
75
+ Bare grammar tokens normally use Ruby symbols (`:NUM`), and quoted grammar tokens use strings (`'+'`). A push source can call
76
+ `yyparse(receiver, method_name)` where the receiver method yields the same pairs.
77
+
78
+ The default `on_error(token_id, value, value_stack)` raises `Ibex::ParseError`. Override it to use yacc-style `error` recovery.
79
+ Semantic actions can call `yyerror`, `yyerrok`, or `yyaccept`, and `expected_tokens` reports valid lookaheads in the current state.
80
+ Parser subclasses can also override `on_shift(token_id, value, state)`,
81
+ `on_reduce(production_id, values, result)`, and `on_error_recover(token_id, value, value_stack)` as no-op-by-default observers.
82
+ Ordinary shifts and the synthetic recovery-token shift use separate hooks; observer return values never replace semantic values.
83
+
84
+ ## Extended mode
85
+
86
+ `--mode=extended` enables optional, repeated, and separated values plus named references. A grammar can make the same choice
87
+ locally with `pragma extended` immediately after its `class` header:
88
+
89
+ ```text
90
+ class ExtendedParser
91
+ pragma extended
92
+ rule
93
+ arguments : separated_list(NUM, ',') { result = val[0] }
94
+ sum : NUM:left '+' NUM:right { result = left + right }
95
+ maybe : NUM?
96
+ many : NUM*
97
+ some : NUM+
98
+ pairs : (KEY VALUE)*
99
+ end
100
+ ```
101
+
102
+ The value conventions are `nil` or a value for `?`, and arrays for `*`, `+`, `separated_list`, and
103
+ `separated_nonempty_list`. Parenthesized sequences and alternatives can be nested; multi-item groups produce an Array value.
104
+ Text, DOT, and HTML automaton reports label lowered helper symbols with these source-level EBNF expressions.
105
+
106
+ ## Pipeline and diagnostics
107
+
108
+ ```sh
109
+ ibex --emit=grammar-ir grammar.y > grammar.json
110
+ ibex --from=grammar-ir --emit=automaton-ir grammar.json > automaton.json
111
+ ibex --from=automaton-ir -o parser.rb automaton.json
112
+ ibex -v --dot=states.dot --html=states.html grammar.y
113
+ ibex --algorithm=lr1 grammar.y
114
+ ibex --counterexamples --counterexample-max-tokens=64 --counterexample-max-configurations=100000 grammar.y
115
+ ibex --rbs -o parser.rb grammar.y
116
+ ibex --warnings=all,error -C grammar.y
117
+ ```
118
+
119
+ Supported construction algorithms are `slr`, `lalr` (default), and canonical `lr1`. Reports retain precedence-resolved
120
+ conflicts and distinguish unifying counterexamples from nonunifying reachability witnesses. Counterexample searches default to
121
+ 32 tokens and 50,000 explored configurations; `--counterexample-max-tokens=N` and
122
+ `--counterexample-max-configurations=N` set positive per-run budgets and request a report. `--rbs` writes a signature beside the
123
+ generated parser; `--rbs=FILE` selects another path. Application methods supplied as opaque `---- inner` code can be declared by
124
+ reopening the generated class in an application RBS file.
125
+
126
+ `--warnings=all` prints unused terminals, unreachable nonterminals, duplicate productions, undeclared terminals, and empty-language
127
+ diagnostics. Add `error` (`--warnings=all,error`, or simply `--warnings=error`) to make any such diagnostic fail the command.
128
+ Action exceptions and `inner` methods point back to their `.y` lines by default. `--line-convert-all` extends that mapping to
129
+ `header` and `footer`; `-l` disables every source-line conversion. User-code chunk locations survive Grammar/Automaton IR JSON
130
+ round trips, so resumed generation has the same backtraces as direct generation.
131
+
132
+ ## Documentation
133
+
134
+ - [Grammar reference](docs/grammar-reference.md)
135
+ - [racc migration guide](docs/racc-migration.md)
136
+ - [Architecture and IR schemas](docs/architecture.md)
137
+ - [Compatibility observations](docs/compat-notes.md)
138
+ - [Phase 10 extensions](docs/phase10-extensions.md)
139
+
140
+ ## Development
141
+
142
+ Run all unit, integration, documentation, and optional local racc black-box tests with `bundle exec rake test`; run style checks
143
+ with `bundle exec rake lint`. The default `bundle exec rake` runs both. Compatibility tests skip automatically when the `racc`
144
+ command is unavailable.
145
+
146
+ Ibex's grammar frontend is self-hosted. Edit `lib/ibex/frontend/grammar.y`, then regenerate and verify the committed parser with:
147
+
148
+ ```sh
149
+ bundle exec rake frontend:generate
150
+ git diff --exit-code -- lib/ibex/frontend/generated_parser.rb
151
+ bundle exec ruby -Itest test/frontend/self_host_test.rb
152
+ ```
153
+
154
+ Normal library and CLI execution use the generated parser. The handwritten `BootstrapParser` is loaded only by this regeneration
155
+ workflow, whose direct dependency graph also works when the generated file is absent. Byte-comparison and AST/error parity tests
156
+ prevent generated-source drift.
157
+
158
+ Fixed-seed property tests exercise SLR, LALR, and LR(1) pipeline invariants, while versioned Grammar and Automaton IR fixtures
159
+ guard byte-stable schema compatibility. Intentional fixture updates are documented in `test/fixtures/ir/README.md`. Run the
160
+ reproducible whole-builder benchmark without a timing pass/fail threshold with:
161
+
162
+ ```sh
163
+ benchmark/pipeline.rb --rules 200 --iterations 5 --seed 12345
164
+ ```
165
+
166
+ Signatures for every Ruby source under `lib/` are generated from rbs-inline annotations and checked with Steep, including the
167
+ self-hosted generated parser. To regenerate the committed signature tree and reproduce its validation locally:
168
+
169
+ ```sh
170
+ BUNDLE_GEMFILE=gemfiles/Gemfile bundle install
171
+ BUNDLE_GEMFILE=gemfiles/Gemfile ruby -e '
172
+ sources = Dir.glob("lib/**/*.rb").sort
173
+ exec("bundle", "exec", "rbs-inline", "--opt-out", "--base=lib", "--output=sig", *sources)
174
+ '
175
+ BUNDLE_GEMFILE=gemfiles/Gemfile bundle exec rbs -r digest -r json -r optparse -I sig validate
176
+ BUNDLE_GEMFILE=gemfiles/Gemfile bundle exec steep check
177
+ BUNDLE_GEMFILE=gemfiles/Gemfile bundle exec steep stats
178
+ BUNDLE_GEMFILE=gemfiles/Gemfile bundle exec ruby tool/type_stats.rb --write
179
+ ```
180
+
181
+ CI performs generation in a clean temporary directory and compares the complete trees, so missing source signatures and stale
182
+ signature files both fail the build.
183
+ <!-- type-stats:start -->
184
+ The current whole-library `steep stats` result is 3,771 typed calls and 449 untyped calls out of 4,220 (89.4% typed).
185
+ The generated signature tree contains 397 explicit `untyped` occurrences across 16 files.
186
+ <!-- type-stats:end -->
187
+
188
+ Those boundaries are concentrated in generated-parser reduction values, heterogeneous JSON decoding/serialization, runtime
189
+ semantic values and parser-table cells, and embedded user Ruby. Token/location records, the complete grammar AST, parser
190
+ classifier state, IR, the public Ruby DSL, bootstrap parser state, analysis, automaton construction, code generators, table
191
+ construction, and CLI options use concrete domain types. The committed self-hosted parser remains in the Steep target; no library
192
+ directory or generated source is excluded.
193
+
194
+ Ibex is available under the [MIT License](LICENSE.txt).
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+ require "rubocop/rake_task"
6
+
7
+ Rake::TestTask.new(:test) do |task|
8
+ task.libs << "test"
9
+ task.pattern = "test/**/*_test.rb"
10
+ task.warning = true
11
+ end
12
+
13
+ RuboCop::RakeTask.new(:lint)
14
+
15
+ namespace :frontend do
16
+ desc "Regenerate the self-hosted grammar parser"
17
+ task :generate do
18
+ require_relative "lib/ibex/frontend/regenerator"
19
+
20
+ output = File.expand_path("lib/ibex/frontend/generated_parser.rb", __dir__)
21
+ File.write(output, Ibex::Frontend::Regenerator.generate)
22
+ end
23
+ end
24
+
25
+ task default: %i[test lint]
data/Steepfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ target :library do
4
+ signature "sig"
5
+ check "lib"
6
+
7
+ library "digest"
8
+ library "json"
9
+ library "optparse"
10
+ end
@@ -0,0 +1,99 @@
1
+ # Architecture and IR schemas
2
+
3
+ Ibex keeps syntax, grammar meaning, automaton construction, and output concerns behind two versioned immutable contracts.
4
+
5
+ ```text
6
+ .y Lexer -> Token adapter -> self-hosted LR Parser ─┐
7
+ Ruby DSL ────────┴─> Grammar AST -> Normalizer -> Grammar IR
8
+ |
9
+ set analysis
10
+ |
11
+ SLR/LALR/LR1 Builder -> Automaton IR
12
+ |
13
+ Ruby/RBS generators / report / DOT / HTML / counterexamples
14
+ ```
15
+
16
+ Frontend changes stop at the Normalizer. Algorithm strategies consume Grammar IR and produce identical Automaton IR shapes.
17
+ Outputs consume Automaton IR and never call builder internals. The CLI only connects stages and supports JSON resumption.
18
+
19
+ The text frontend's canonical syntax is `lib/ibex/frontend/grammar.y`. Ibex generates and commits
20
+ `lib/ibex/frontend/generated_parser.rb`; the public `Frontend::Parser` always delegates to that class. Lexer `Token` objects remain
21
+ the semantic values passed through `TokenAdapter`, preserving their `Location` in AST nodes and diagnostics. The explicitly named
22
+ handwritten `BootstrapParser` is excluded from normal loading and exists only to break the regeneration cycle. See
23
+ [ADR 0015](decisions/0015-self-hosted-grammar-frontend.md) for the update procedure and boundary.
24
+
25
+ The RBS generator emits the generated class namespace, superclass, parser-table constants, and `.parser_tables` contract. The
26
+ gem also ships a one-to-one rbs-inline-generated signature tree under `sig/` for every Ruby source in `lib/`, including the
27
+ self-hosted parser. CI regenerates into an empty temporary directory, compares the complete trees, validates the RBS environment,
28
+ and runs Steep against the entire library. Token/location records, grammar AST nodes, parser classifier and bootstrap state, the
29
+ Ruby DSL, IR records, and automaton actions use concrete domain types. Generated-parser reduction values, dynamic parser-table
30
+ cells, decoded JSON values, and user methods embedded as opaque Ruby source remain `untyped`; applications can reopen the generated
31
+ class in their own RBS files to declare embedded methods.
32
+
33
+ ## Grammar IR v1
34
+
35
+ Top-level fields:
36
+
37
+ | Field | Meaning |
38
+ |---|---|
39
+ | `ibex_ir`, `schema_version` | `"grammar"`, `1` |
40
+ | `class_name`, `superclass` | Generated Ruby class contract |
41
+ | `start`, `expect`, `options` | Start name, unresolved S/R expectation, result/action flags |
42
+ | `symbols` | Interned terminals and nonterminals; `$eof` id 0 and `error` id 1 |
43
+ | `productions` | Numeric LHS/RHS ids, action, precedence override, source origin |
44
+ | `user_code`, `conversions`, `warnings` | Concatenated code, external token expressions, structured diagnostics |
45
+ | `user_code_chunks` | Optional opaque chunks with first-code-line locations for compatible source mapping |
46
+
47
+ Warning records use stable type names (`undeclared_terminal`, `unused_terminal`, `unreachable_nonterminal`,
48
+ `duplicate_production`, and `empty_language`) and retain source locations. The CLI applies display/error policy at the boundary;
49
+ normalization and IR serialization do not discard diagnostics.
50
+
51
+ A symbol has `id`, `name`, `kind`, `reserved`, optional `prec {associativity, level}`, and `loc`. A production has `id`, `lhs`,
52
+ `rhs`, optional `action`, optional `prec_override`, and `origin`. Synthetic EBNF origins include an additive, deterministic
53
+ `expression` label used by text, DOT, and HTML presentation while numeric symbol identities remain unchanged. An action has opaque
54
+ `code`, `loc`, `named_refs [{name,index}]`, and `context_length`; middle-action helpers use the last field to view preceding stack
55
+ values.
56
+
57
+ IR objects and nested collections are frozen. JSON keys have deterministic order, so dump/load/dump is byte-stable. Incompatible
58
+ schema changes require a new version. The additive `user_code_chunks` field is optional so older schema-v1 JSON remains loadable;
59
+ new text-front-end IR retains it to make direct and resumed `--line-convert-all` generation identical.
60
+
61
+ ## Automaton IR v1
62
+
63
+ Top-level fields are `ibex_ir: "automaton"`, `schema_version`, `algorithm`, `grammar_digest`, embedded `grammar`, `states`, and
64
+ `conflict_summary`. Embedding Grammar IR makes automaton JSON sufficient for code generation after `--from=automaton-ir`.
65
+
66
+ Each state contains:
67
+
68
+ - merged items `{production, dot, lookaheads}`;
69
+ - named `transitions`;
70
+ - resolved terminal `actions` and nonterminal `gotos`;
71
+ - an optional reduce `default_action`, selected only when explicit error masks preserve every terminal lookup and reduce the
72
+ total encoded ACTION entries;
73
+ - every conflict, including precedence-resolved conflicts and the resolution reason.
74
+
75
+ `conflict_summary.sr` counts unresolved default-shift conflicts for `expect`; `resolved_sr` counts retained precedence or
76
+ associativity decisions; `rr` counts reduce/reduce cells.
77
+
78
+ ## Runtime table contract
79
+
80
+ Generated subclasses expose `.parser_tables` with a required `format_version`, external `tokens`, display `token_names`, ACTION
81
+ and GOTO tables, per-state default actions, and production `{lhs,length,action}` records. The runtime validates the version before
82
+ reading input and rejects missing or unsupported formats with a regeneration instruction; see
83
+ [ADR 0018](decisions/0018-parser-table-format-version.md). Plain tables are arrays of Hash rows. Compact tables use row
84
+ displacement with offsets, values, and row-ownership checks; both expose equivalent lookups. Default reductions are restricted
85
+ to known token ids, and explicit error masks preserve the pre-optimization result of every declared terminal cell, including
86
+ the synthetic `error` terminal. The deterministic size policy is fixed by
87
+ [ADR 0014](decisions/0014-compatibility-safe-default-reductions.md).
88
+
89
+ The runtime maintains state and value stacks, pulls a lookahead only when required, and applies tagged `shift`, `reduce`,
90
+ `accept`, and `error` actions. Recovery pops to a state that shifts token id 1, suppresses repeated reports for three successful
91
+ shifts, and honors `yyerrok`. No-op `on_shift`, `on_reduce`, and `on_error_recover` extension points observe successfully
92
+ committed events without changing parser results; the recovery hook retains the pre-pop error context and is distinct from an
93
+ ordinary token shift. Their ordering and payload contract is fixed by [ADR 0013](decisions/0013-runtime-observation-hooks.md).
94
+
95
+ ## Clean-room boundary
96
+
97
+ Implementation work uses public racc documentation, CLI black-box behavior, and published LR algorithms only. racc implementation
98
+ sources and generated source are not inputs to the design. Self-authored compatibility grammars execute both outputs in separate
99
+ processes and compare observable results.
@@ -0,0 +1,37 @@
1
+ # Compatibility notes
2
+
3
+ Compatibility observations are recorded here without consulting racc implementation sources or generated source text.
4
+
5
+ ## Observed CLI (2026-07-22)
6
+
7
+ Black-box command: `racc --help`, version 1.8.1.
8
+
9
+ Observed options are `-o/--output-file`, `-t/--debug`, obsolete `-g`, `-v/--verbose`, `-O/--log-file`,
10
+ `-e/--executable`, `-E/--embedded`, `-F/--frozen`, `--line-convert-all`, `-l/--no-line-convert`,
11
+ `-a/--no-omit-actions`, `--superclass`, `-C/--check-only`, `-S/--output-status`, profiling `-P`, internal `-D`,
12
+ `--version`, `--runtime-version`, `--copyright`, and `--help`.
13
+
14
+ Ibex accepts that option set. `-P` and `-D` are accepted compatibility no-ops because they expose generator internals rather
15
+ than parser behavior. Frozen string literals are always emitted, so `-F` is also a no-op. Ibex's default output is `<input>.rb`
16
+ rather than racc's observed `<input>.tab.rb`; `-o` is portable between both tools.
17
+
18
+ ## Behavioral probes
19
+
20
+ Self-authored grammars are compiled independently and the generated files are executed without inspecting them. Current probes
21
+ cover arithmetic precedence, empty rules, string tokens, `convert`, `no_result_var`, inline actions, dangling-else `expect`, and
22
+ a generated 500-production grammar. Precedence-resolved conflicts remain in Automaton IR diagnostics but are not counted in CLI
23
+ warnings or `expect`, matching the observed calculator behavior.
24
+
25
+ An inline action's own `val[0]` was observed as `nil`; its result occupies one value position visible to the final action. The
26
+ grammar reference confirms that `convert` takes a quoted string containing Ruby source: for example `NUM ':number'` emits the
27
+ symbol expression, while `NUM '"number"'` emits a Ruby string token.
28
+
29
+ Source-line probes with raises in self-authored `header`, `inner`, and `footer` blocks show three distinct modes. The default maps
30
+ semantic actions and `inner` code to the grammar file but leaves `header` and `footer` on generated-file lines.
31
+ `--line-convert-all` maps all three user-code sections, while `-l` leaves all of them on generated-file lines.
32
+
33
+ With an explicitly declared but syntactically invalid `BAD` token, error recovery results and `on_error` observations (token
34
+ string, value, and value-stack length) match. An undeclared `:BAD` was observed to enter racc's `error` production without an
35
+ `on_error` callback. Ibex intentionally treats it as an unknown lookahead and calls `on_error` before attempting the same error
36
+ recovery flow. This preserves the unexpected token object and value for logging and application policy instead of silently
37
+ entering recovery.
@@ -0,0 +1,127 @@
1
+ # Grammar reference
2
+
3
+ Ibex's default `racc` mode accepts the compatible grammar described here. `--mode=extended` or an explicit grammar-file
4
+ `pragma extended` adds the marked syntax; extensions are never inferred from a production.
5
+
6
+ ## File structure
7
+
8
+ ```text
9
+ class Namespace::Parser < OptionalSuperclass
10
+ pragma extended # optional; must precede ordinary declarations
11
+ declarations
12
+ rule
13
+ productions
14
+ end
15
+ ---- header
16
+ Ruby copied before the parser class
17
+ ---- inner
18
+ Ruby copied inside the parser class
19
+ ---- footer
20
+ Ruby copied after the parser class
21
+ ```
22
+
23
+ The superclass defaults to `Ibex::Runtime::Parser`. Repeated user-code blocks retain their source order and are concatenated.
24
+ Grammar comments use `#` through end of line or `/* ... */`.
25
+
26
+ ## Declarations
27
+
28
+ - `pragma extended` enables extended syntax for this grammar even when the CLI uses its default or explicit `--mode=racc`.
29
+ It must immediately follow the class header, before every ordinary declaration. Unknown, duplicate, and misplaced pragmas
30
+ are positioned errors. The pragma is consumed by the frontend and is not stored in AST or Grammar IR output.
31
+
32
+ - `token NAME ...` declares terminals for typo diagnostics. It is optional. Uppercase names and quoted strings are terminals;
33
+ lowercase names are nonterminals unless they are `error`.
34
+ - A `prechigh ... preclow` block lists precedence from high to low; `preclow ... prechigh` lists it from low to high. Each level
35
+ begins with `left`, `right`, or `nonassoc` followed by one or more terminals.
36
+ - `options no_result_var` makes an action's final expression its value. `omit_action_call` is enabled by default;
37
+ `no_omit_action_call` disables it.
38
+ - `expect N` suppresses the warning when exactly N unresolved shift/reduce conflicts remain. Conflicts resolved by precedence are
39
+ retained in Automaton IR but are not counted.
40
+ - `start name` overrides the first rule as the start symbol.
41
+ - `convert ... end` changes external token objects. The second column is a quoted string containing Ruby source, not the value
42
+ itself: `NUM ':number'` uses `:number`, while `NUM '"number"'` uses the String `"number"`.
43
+
44
+ ## Productions and actions
45
+
46
+ ```text
47
+ rule
48
+ expression : expression '+' expression { result = val[0] + val[2] }
49
+ | '-' expression = UMINUS
50
+ | NUMBER
51
+ | /* empty */
52
+ ; /* optional */
53
+ end
54
+ ```
55
+
56
+ Alternatives use `|`; a trailing semicolon is optional. `= TOKEN` overrides a production's precedence. The `error` terminal
57
+ enables yacc-style recovery.
58
+
59
+ Actions are opaque Ruby between balanced braces. `val` contains RHS values, `result` begins as `val[0]`, and `_values` is a copy
60
+ of the surrounding value stack. With `no_result_var`, the action's evaluated value is used directly. A middle action becomes an
61
+ empty helper production and consumes one value position in the enclosing RHS.
62
+
63
+ Action and `inner` backtraces use the original grammar filename and line by default. `--line-convert-all` applies the same mapping
64
+ to `header` and `footer`; `-l` keeps all backtraces on generated-file lines.
65
+
66
+ The action scanner handles nested braces, quoted/backtick strings and interpolation, `%q/%Q/%w/%W/%i/%I/%x/%r/%s`, regular
67
+ expressions, comments, character literals, and unquoted, single-quoted, double-quoted, or backtick heredocs. Indented, squiggly,
68
+ interpolated, and multiple heredocs on one opener line are supported. See [lexer coverage](lexer-coverage.md).
69
+
70
+ ## Runtime errors
71
+
72
+ The default `on_error(token_id, value, value_stack)` raises `Ibex::ParseError`. Override it and return to allow an `error`
73
+ production to recover. Unknown external token objects receive a temporary negative internal id, remain printable through
74
+ `token_to_str`, and always invoke `on_error` before recovery is attempted.
75
+
76
+ Three optional observer methods default to no-ops. `on_shift(token_id, value, state)` follows each ordinary input-token shift;
77
+ `on_reduce(production_id, values, result)` follows a completed semantic action and goto; and
78
+ `on_error_recover(token_id, value, value_stack)` follows a successful synthetic `error` shift while retaining the original
79
+ unexpected-token context. Hook return values are ignored and exceptions propagate. See
80
+ [ADR 0013](decisions/0013-runtime-observation-hooks.md) for exact ordering and snapshot semantics.
81
+
82
+ ## Extended EBNF and names
83
+
84
+ Extended mode supports:
85
+
86
+ - `item?`: `nil` or the item value.
87
+ - `item*`: zero or more values as an Array.
88
+ - `item+`: one or more values as an Array.
89
+ - `separated_list(item, separator)`: zero or more item values; separators are omitted.
90
+ - `separated_nonempty_list(item, separator)`: one or more item values.
91
+ - `symbol:name`: binds the corresponding RHS value as a local variable in the final action.
92
+
93
+ Parenthesized groups may contain sequences, alternatives, and nested EBNF, for example `(KEY VALUE)*`, `(A | B)+`, or
94
+ `separated_list((KEY VALUE), ',')`. A one-item group has that item's value; a multi-item group has an Array of its item values;
95
+ an empty group has `nil`. Named references must be unique in an outer alternative and cannot use `result`, `val`, or `_values`;
96
+ references inside a group are rejected because the group is lowered behind one outer value slot. Text, DOT, and HTML reports
97
+ render lowered helper nonterminals as their original EBNF expressions instead of exposing generated helper names.
98
+
99
+ Actions and named references are supported on an outer production alternative, but not inside a parenthesized EBNF group.
100
+ Move the action or binding to a separately named ordinary rule and reference that rule from the group.
101
+
102
+ ## Strict diagnostics
103
+
104
+ Grammar IR retains structured diagnostics for undeclared or unused terminals, unreachable nonterminals, duplicate productions,
105
+ and a start symbol that cannot derive any terminal sentence. They remain silent by default for compatibility. `--warnings=all`
106
+ prints them, `--warnings=all,error` or `--warnings=error` promotes them to command failures, and `--warnings=none` explicitly
107
+ suppresses them.
108
+
109
+ ## Ruby DSL
110
+
111
+ The DSL builds the same AST and IR without evaluating grammar text:
112
+
113
+ ```ruby
114
+ ast = Ibex::Frontend::DSL.grammar(class_name: "Calculator") do |grammar|
115
+ grammar.token(:NUM)
116
+ grammar.precedence { |levels| levels.left("'+'"); levels.left("'*'") }
117
+ grammar.rule(:expr) do |rule|
118
+ rule.alt(:expr, "'+'", :expr, action: " result = val[0] + val[2] ")
119
+ rule.alt(:NUM)
120
+ end
121
+ end
122
+
123
+ grammar_ir = Ibex::Normalizer.new(ast).normalize
124
+ ```
125
+
126
+ The builder also provides `options`, `expect`, `start`, `convert`, `user_code`, `ref(as:)`, `optional`, `star`, `plus`,
127
+ `separated_list`, and `inline`.
@@ -0,0 +1,14 @@
1
+ # Lexer coverage
2
+
3
+ | Construct | Status | Notes |
4
+ |---|---|---|
5
+ | Nested action braces | Supported | Balanced recursively |
6
+ | Single, double, and backtick strings | Supported | Escapes and `#{...}` interpolation are skipped |
7
+ | `%q`, `%Q`, `%w`, `%W`, `%i`, `%I`, `%x`, `%r`, `%s` | Supported | Paired and single-character delimiters |
8
+ | Regular expressions | Supported | Conservative expression-prefix heuristic; character classes and flags supported |
9
+ | `#` comments and `?x` characters | Supported | Ignored for brace balancing |
10
+ | `<<ID`, `<<-ID`, `<<~ID` heredocs | Supported | Terminators obey indentation mode |
11
+ | Single/double/backtick quoted heredocs | Supported | Quoted identifiers may contain any non-quote, non-newline text |
12
+ | Interpolated and multiple heredocs | Supported | Bodies are opaque; multiple openers on one line are consumed in order |
13
+ | Grammar `#` and `/* ... */` comments | Supported | Removed before tokenization |
14
+ | `---- header`, `inner`, `footer` | Supported | Must begin in column one; order and duplicates retained |
@@ -0,0 +1,27 @@
1
+ # Phase 10 extensions
2
+
3
+ ## E3: conflict witnesses
4
+
5
+ `Ibex::LALR::Counterexample` accepts only Automaton IR. It explores parser-stack configurations, forces each pair of competing
6
+ actions at the requested conflict, and then searches for a common suffix that lets both branches accept the same terminal
7
+ sentence. A successful result contains `unifying: true`, the conflict lookahead position, and both complete derivation trees.
8
+
9
+ Because unifying-counterexample search is not guaranteed to terminate for every context-free grammar, the search has explicit
10
+ token and configuration budgets. The defaults are 32 tokens and 50,000 explored configurations. Pass `max_tokens:` and
11
+ `max_configurations:` to `Ibex::LALR::Counterexample.new` or `Ibex::Codegen::Report.render` to adjust them through the Ruby API.
12
+ For generated reports, use the positive-integer CLI options `--counterexample-max-tokens=N` and
13
+ `--counterexample-max-configurations=N`; either option also requests a report. The token budget covers each unifying-search
14
+ candidate and any returned unifying counterexample, including a non-EOF conflict lookahead; the EOF marker does not consume a
15
+ token. If no common sentence is found within those budgets, the result is marked `unifying: false` and contains the deterministic
16
+ shortest reachability witness, which may exceed the unifying-search token budget. This distinction prevents a nonunifying
17
+ diagnostic from being presented as proof of ambiguity.
18
+
19
+ ## E7: Ruby DSL
20
+
21
+ `Ibex::Frontend::DSL.grammar` constructs the existing AST with synthetic locations and joins the normal pipeline at Normalizer.
22
+ Text and DSL inputs therefore share symbol classification, EBNF expansion, validation, algorithms, and outputs.
23
+
24
+ ## E9: construction algorithms
25
+
26
+ The builder first constructs canonical LR(1). `lr1` retains the states, `lalr` merges equal LR(0) cores, and `slr` applies FOLLOW
27
+ sets to completed items in the LR(0) states. All strategies use the same conflict resolver and Automaton IR.