ibex 0.1.0 → 0.2.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 (384) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +247 -104
  3. data/docs/architecture.md +322 -28
  4. data/docs/cst-migration.md +107 -0
  5. data/docs/cst.md +213 -0
  6. data/docs/development.md +129 -0
  7. data/docs/editor-setup.md +25 -0
  8. data/docs/error-ux.md +60 -0
  9. data/docs/grammar-reference.md +458 -18
  10. data/docs/lexer-migration.md +51 -0
  11. data/docs/racc-migration.md +34 -6
  12. data/docs/release-readiness.md +163 -0
  13. data/docs/stability.md +124 -0
  14. data/examples/README.md +60 -0
  15. data/examples/calculator.y +44 -0
  16. data/examples/csv.y +32 -0
  17. data/examples/ini.y +70 -0
  18. data/examples/json.y +48 -0
  19. data/examples/tiny_language.y +75 -0
  20. data/lib/ibex/analysis/sets.rb +5 -4
  21. data/lib/ibex/artifact_set.rb +63 -0
  22. data/lib/ibex/cli/ambiguity.rb +66 -0
  23. data/lib/ibex/cli/counterexample_options.rb +6 -2
  24. data/lib/ibex/cli/coverage.rb +173 -0
  25. data/lib/ibex/cli/debug.rb +106 -0
  26. data/lib/ibex/cli/diagnostics.rb +116 -0
  27. data/lib/ibex/cli/documentation.rb +67 -0
  28. data/lib/ibex/cli/error_messages.rb +177 -0
  29. data/lib/ibex/cli/explain.rb +76 -0
  30. data/lib/ibex/cli/formatting.rb +386 -0
  31. data/lib/ibex/cli/generation_artifacts.rb +138 -0
  32. data/lib/ibex/cli/generation_error_messages.rb +39 -0
  33. data/lib/ibex/cli/grammar_tests.rb +122 -0
  34. data/lib/ibex/cli/ir_tools.rb +184 -0
  35. data/lib/ibex/cli/lsp.rb +33 -0
  36. data/lib/ibex/cli/outputs.rb +91 -8
  37. data/lib/ibex/cli/racc_migration.rb +87 -0
  38. data/lib/ibex/cli/samples.rb +92 -0
  39. data/lib/ibex/cli/watch.rb +103 -0
  40. data/lib/ibex/cli.rb +506 -37
  41. data/lib/ibex/codegen/action_locations.rb +103 -0
  42. data/lib/ibex/codegen/action_method_source.rb +210 -0
  43. data/lib/ibex/codegen/action_source.rb +118 -0
  44. data/lib/ibex/codegen/ambiguity.rb +173 -0
  45. data/lib/ibex/codegen/cst_metadata.rb +171 -0
  46. data/lib/ibex/codegen/documentation.rb +170 -0
  47. data/lib/ibex/codegen/explain.rb +312 -0
  48. data/lib/ibex/codegen/generated_action_abi.rb +288 -0
  49. data/lib/ibex/codegen/html.rb +94 -10
  50. data/lib/ibex/codegen/mermaid.rb +43 -0
  51. data/lib/ibex/codegen/railroad.rb +197 -0
  52. data/lib/ibex/codegen/railroad_documentation.rb +59 -0
  53. data/lib/ibex/codegen/rbs.rb +323 -2
  54. data/lib/ibex/codegen/report.rb +46 -6
  55. data/lib/ibex/codegen/ruby.rb +334 -59
  56. data/lib/ibex/codegen/ruby_actions.rb +143 -0
  57. data/lib/ibex/codegen/ruby_ast.rb +121 -0
  58. data/lib/ibex/codegen/ruby_error_messages.rb +28 -0
  59. data/lib/ibex/codegen/ruby_lexer.rb +83 -0
  60. data/lib/ibex/codegen/ruby_syntax.rb +90 -0
  61. data/lib/ibex/codegen/ruby_table_metadata.rb +57 -0
  62. data/lib/ibex/codegen/ruby_value_printers.rb +56 -0
  63. data/lib/ibex/codegen/symbol_labels.rb +1 -1
  64. data/lib/ibex/coverage/collector.rb +188 -0
  65. data/lib/ibex/coverage/event_stream.rb +97 -0
  66. data/lib/ibex/coverage/report.rb +259 -0
  67. data/lib/ibex/coverage/runtime_event_validator.rb +160 -0
  68. data/lib/ibex/coverage.rb +13 -0
  69. data/lib/ibex/error_messages/parser.rb +159 -0
  70. data/lib/ibex/error_messages/parser_v2.rb +198 -0
  71. data/lib/ibex/error_messages/renderer.rb +65 -0
  72. data/lib/ibex/error_messages/sentence_search.rb +196 -0
  73. data/lib/ibex/error_messages/update.rb +169 -0
  74. data/lib/ibex/error_messages.rb +165 -0
  75. data/lib/ibex/frontend/ast.rb +145 -6
  76. data/lib/ibex/frontend/bootstrap_parser.rb +47 -4
  77. data/lib/ibex/frontend/diagnostic.rb +81 -0
  78. data/lib/ibex/frontend/diagnostic_recovery.rb +268 -0
  79. data/lib/ibex/frontend/dsl.rb +66 -7
  80. data/lib/ibex/frontend/formatter.rb +407 -0
  81. data/lib/ibex/frontend/generated_parser.rb +614 -190
  82. data/lib/ibex/frontend/generated_parser_base.rb +164 -30
  83. data/lib/ibex/frontend/generated_parser_includes.rb +61 -0
  84. data/lib/ibex/frontend/generated_parser_metadata.rb +61 -0
  85. data/lib/ibex/frontend/generated_parser_parameters.rb +60 -0
  86. data/lib/ibex/frontend/generation.rb +33 -0
  87. data/lib/ibex/frontend/lexer.rb +162 -10
  88. data/lib/ibex/frontend/lexer_recovery.rb +84 -0
  89. data/lib/ibex/frontend/parser/declarations.rb +247 -11
  90. data/lib/ibex/frontend/parser/parameters.rb +82 -0
  91. data/lib/ibex/frontend/parser/rules.rb +43 -6
  92. data/lib/ibex/frontend/parser.rb +182 -4
  93. data/lib/ibex/frontend/regenerator.rb +26 -1
  94. data/lib/ibex/frontend/resolution.rb +69 -0
  95. data/lib/ibex/frontend/resolver.rb +217 -0
  96. data/lib/ibex/frontend/rule_documentation.rb +103 -0
  97. data/lib/ibex/frontend/source_cursor.rb +132 -8
  98. data/lib/ibex/frontend/source_document.rb +229 -0
  99. data/lib/ibex/frontend/source_loader.rb +150 -0
  100. data/lib/ibex/frontend/source_span.rb +81 -0
  101. data/lib/ibex/frontend/token_adapter/declaration_document_state.rb +47 -0
  102. data/lib/ibex/frontend/token_adapter/declaration_lexer_state.rb +83 -0
  103. data/lib/ibex/frontend/token_adapter/declaration_state.rb +216 -26
  104. data/lib/ibex/frontend/token_adapter/delimiter_tracker.rb +8 -2
  105. data/lib/ibex/frontend/token_adapter/rule_state.rb +60 -2
  106. data/lib/ibex/frontend/token_adapter.rb +8 -3
  107. data/lib/ibex/frontend.rb +13 -2
  108. data/lib/ibex/generation_input.rb +57 -0
  109. data/lib/ibex/generation_manifest.rb +200 -0
  110. data/lib/ibex/generation_transaction.rb +261 -0
  111. data/lib/ibex/generation_transaction_recovery.rb +109 -0
  112. data/lib/ibex/generation_transaction_validation.rb +196 -0
  113. data/lib/ibex/grammar_tests.rb +206 -0
  114. data/lib/ibex/ir/automaton_ir.rb +38 -5
  115. data/lib/ibex/ir/grammar_ir.rb +137 -24
  116. data/lib/ibex/ir/lexer_ir.rb +76 -0
  117. data/lib/ibex/ir/migration.rb +120 -0
  118. data/lib/ibex/ir/serialize.rb +110 -19
  119. data/lib/ibex/ir/validator/automaton.rb +345 -0
  120. data/lib/ibex/ir/validator/base.rb +129 -0
  121. data/lib/ibex/ir/validator/grammar.rb +604 -0
  122. data/lib/ibex/ir/validator/lexer.rb +113 -0
  123. data/lib/ibex/ir/validator.rb +62 -0
  124. data/lib/ibex/ir.rb +57 -4
  125. data/lib/ibex/lalr/build_metrics.rb +23 -0
  126. data/lib/ibex/lalr/builder.rb +379 -52
  127. data/lib/ibex/lalr/conflict.rb +1 -0
  128. data/lib/ibex/lalr/conflict_search.rb +11 -5
  129. data/lib/ibex/lalr/counterexample.rb +20 -5
  130. data/lib/ibex/lalr/direct_lookaheads.rb +236 -0
  131. data/lib/ibex/lalr/ielr_partition.rb +152 -0
  132. data/lib/ibex/lalr/on_error_reductions.rb +74 -0
  133. data/lib/ibex/lalr.rb +7 -0
  134. data/lib/ibex/location.rb +129 -0
  135. data/lib/ibex/lsp/document_handlers.rb +66 -0
  136. data/lib/ibex/lsp/document_store.rb +264 -0
  137. data/lib/ibex/lsp/document_store_diagnostics.rb +44 -0
  138. data/lib/ibex/lsp/document_store_validation.rb +61 -0
  139. data/lib/ibex/lsp/initialization_handlers.rb +78 -0
  140. data/lib/ibex/lsp/navigation_handlers.rb +56 -0
  141. data/lib/ibex/lsp/position_codec.rb +109 -0
  142. data/lib/ibex/lsp/protocol_error.rb +33 -0
  143. data/lib/ibex/lsp/request_handlers.rb +45 -0
  144. data/lib/ibex/lsp/request_support.rb +87 -0
  145. data/lib/ibex/lsp/server.rb +146 -0
  146. data/lib/ibex/lsp/symbol_index.rb +241 -0
  147. data/lib/ibex/lsp/symbol_index_builder.rb +267 -0
  148. data/lib/ibex/lsp/symbol_index_precedence_references.rb +44 -0
  149. data/lib/ibex/lsp/symbol_index_source_queries.rb +61 -0
  150. data/lib/ibex/lsp/symbol_occurrence.rb +17 -0
  151. data/lib/ibex/lsp/transport.rb +118 -0
  152. data/lib/ibex/lsp/workspace.rb +127 -0
  153. data/lib/ibex/lsp/workspace_analyzer.rb +199 -0
  154. data/lib/ibex/lsp.rb +32 -0
  155. data/lib/ibex/normalize/declarations.rb +140 -7
  156. data/lib/ibex/normalize/diagnostics.rb +50 -5
  157. data/lib/ibex/normalize/expander.rb +59 -55
  158. data/lib/ibex/normalize/expression.rb +60 -39
  159. data/lib/ibex/normalize/inline_expansion.rb +414 -0
  160. data/lib/ibex/normalize/inline_validation.rb +174 -0
  161. data/lib/ibex/normalize/lexer.rb +131 -0
  162. data/lib/ibex/normalize/named_references.rb +60 -0
  163. data/lib/ibex/normalize/nodes.rb +46 -0
  164. data/lib/ibex/normalize/parameter_ebnf_lowering.rb +69 -0
  165. data/lib/ibex/normalize/parameter_lowering.rb +126 -0
  166. data/lib/ibex/normalize/parameter_substitution.rb +125 -0
  167. data/lib/ibex/normalize/parameter_validation.rb +140 -0
  168. data/lib/ibex/normalize/parameters.rb +199 -0
  169. data/lib/ibex/normalize/recovery_declarations.rb +84 -0
  170. data/lib/ibex/normalize.rb +179 -14
  171. data/lib/ibex/racc_migration/checker.rb +122 -0
  172. data/lib/ibex/racc_migration/harness.rb +177 -0
  173. data/lib/ibex/racc_migration/report.rb +94 -0
  174. data/lib/ibex/racc_migration.rb +12 -0
  175. data/lib/ibex/rake_task.rb +116 -0
  176. data/lib/ibex/samples.rb +186 -0
  177. data/lib/ibex/table_simulation/result.rb +51 -0
  178. data/lib/ibex/table_simulation/simulator.rb +253 -0
  179. data/lib/ibex/table_simulation/step.rb +60 -0
  180. data/lib/ibex/table_simulation/text.rb +31 -0
  181. data/lib/ibex/table_simulation.rb +13 -0
  182. data/lib/ibex/tables.rb +9 -70
  183. data/lib/ibex/version.rb +1 -1
  184. data/lib/ibex/watch/runner.rb +172 -0
  185. data/lib/ibex/watch/source_snapshot.rb +93 -0
  186. data/lib/ibex/watch.rb +11 -0
  187. data/lib/ibex.rb +25 -1
  188. data/schema/automaton-ir-v1.schema.json +401 -0
  189. data/schema/automaton-ir-v2.schema.json +58 -0
  190. data/schema/benchmark-v1.schema.json +212 -0
  191. data/schema/benchmark-v2.schema.json +61 -0
  192. data/schema/cst-v1.json +170 -0
  193. data/schema/error-ux-v1.schema.json +258 -0
  194. data/schema/explain-v1.schema.json +433 -0
  195. data/schema/frontend-diagnostics-v1.schema.json +154 -0
  196. data/schema/generation-manifest-v1.schema.json +115 -0
  197. data/schema/grammar-ir-v1.schema.json +426 -0
  198. data/schema/grammar-ir-v2.schema.json +779 -0
  199. data/schema/lexer-ir-v1.schema.json +215 -0
  200. data/schema/migration-check-v1.schema.json +60 -0
  201. data/schema/performance-comparison-v1.schema.json +395 -0
  202. data/schema/public-performance-comparison-v1.schema.json +506 -0
  203. data/schema/public-performance-profile-v1.schema.json +360 -0
  204. data/schema/runtime-coverage-v1.schema.json +86 -0
  205. data/schema/runtime-event-v1.schema.json +308 -0
  206. data/schema/table-simulation-v1.schema.json +70 -0
  207. data/sig/ibex/artifact_set.rbs +37 -0
  208. data/sig/ibex/cli/ambiguity.rbs +22 -0
  209. data/sig/ibex/cli/counterexample_options.rbs +2 -0
  210. data/sig/ibex/cli/coverage.rbs +53 -0
  211. data/sig/ibex/cli/debug.rbs +28 -0
  212. data/sig/ibex/cli/diagnostics.rbs +38 -0
  213. data/sig/ibex/cli/documentation.rbs +25 -0
  214. data/sig/ibex/cli/error_messages.rbs +57 -0
  215. data/sig/ibex/cli/explain.rbs +25 -0
  216. data/sig/ibex/cli/formatting.rbs +103 -0
  217. data/sig/ibex/cli/generation_artifacts.rbs +50 -0
  218. data/sig/ibex/cli/generation_error_messages.rbs +19 -0
  219. data/sig/ibex/cli/grammar_tests.rbs +36 -0
  220. data/sig/ibex/cli/ir_tools.rbs +51 -0
  221. data/sig/ibex/cli/lsp.rbs +14 -0
  222. data/sig/ibex/cli/outputs.rbs +17 -0
  223. data/sig/ibex/cli/racc_migration.rbs +30 -0
  224. data/sig/ibex/cli/samples.rbs +30 -0
  225. data/sig/ibex/cli/watch.rbs +43 -0
  226. data/sig/ibex/cli.rbs +104 -5
  227. data/sig/ibex/codegen/action_locations.rbs +45 -0
  228. data/sig/ibex/codegen/action_method_source.rbs +65 -0
  229. data/sig/ibex/codegen/action_source.rbs +50 -0
  230. data/sig/ibex/codegen/ambiguity.rbs +60 -0
  231. data/sig/ibex/codegen/cst_metadata.rbs +59 -0
  232. data/sig/ibex/codegen/documentation.rbs +50 -0
  233. data/sig/ibex/codegen/explain.rbs +85 -0
  234. data/sig/ibex/codegen/generated_action_abi.rbs +101 -0
  235. data/sig/ibex/codegen/html.rbs +18 -2
  236. data/sig/ibex/codegen/mermaid.rbs +16 -0
  237. data/sig/ibex/codegen/railroad.rbs +82 -0
  238. data/sig/ibex/codegen/railroad_documentation.rbs +31 -0
  239. data/sig/ibex/codegen/rbs.rbs +84 -4
  240. data/sig/ibex/codegen/report.rbs +8 -0
  241. data/sig/ibex/codegen/ruby.rbs +97 -23
  242. data/sig/ibex/codegen/ruby_actions.rbs +54 -0
  243. data/sig/ibex/codegen/ruby_ast.rbs +34 -0
  244. data/sig/ibex/codegen/ruby_error_messages.rbs +16 -0
  245. data/sig/ibex/codegen/ruby_lexer.rbs +25 -0
  246. data/sig/ibex/codegen/ruby_syntax.rbs +25 -0
  247. data/sig/ibex/codegen/ruby_table_metadata.rbs +26 -0
  248. data/sig/ibex/codegen/ruby_value_printers.rbs +28 -0
  249. data/sig/ibex/coverage/collector.rbs +76 -0
  250. data/sig/ibex/coverage/event_stream.rbs +42 -0
  251. data/sig/ibex/coverage/report.rbs +100 -0
  252. data/sig/ibex/coverage/runtime_event_validator.rbs +68 -0
  253. data/sig/ibex/coverage.rbs +7 -0
  254. data/sig/ibex/error_messages/parser.rbs +58 -0
  255. data/sig/ibex/error_messages/parser_v2.rbs +67 -0
  256. data/sig/ibex/error_messages/renderer.rbs +23 -0
  257. data/sig/ibex/error_messages/sentence_search.rbs +80 -0
  258. data/sig/ibex/error_messages/update.rbs +43 -0
  259. data/sig/ibex/error_messages.rbs +85 -0
  260. data/sig/ibex/frontend/ast.rbs +208 -19
  261. data/sig/ibex/frontend/bootstrap_parser.rbs +11 -0
  262. data/sig/ibex/frontend/diagnostic.rbs +53 -0
  263. data/sig/ibex/frontend/diagnostic_recovery.rbs +98 -0
  264. data/sig/ibex/frontend/dsl.rbs +33 -4
  265. data/sig/ibex/frontend/formatter.rbs +135 -0
  266. data/sig/ibex/frontend/generated_parser.rbs +218 -68
  267. data/sig/ibex/frontend/generated_parser_base.rbs +61 -10
  268. data/sig/ibex/frontend/generated_parser_includes.rbs +23 -0
  269. data/sig/ibex/frontend/generated_parser_metadata.rbs +23 -0
  270. data/sig/ibex/frontend/generated_parser_parameters.rbs +24 -0
  271. data/sig/ibex/frontend/generation.rbs +6 -0
  272. data/sig/ibex/frontend/lexer.rbs +49 -2
  273. data/sig/ibex/frontend/lexer_recovery.rbs +25 -0
  274. data/sig/ibex/frontend/parser/declarations.rbs +54 -0
  275. data/sig/ibex/frontend/parser/parameters.rbs +28 -0
  276. data/sig/ibex/frontend/parser/rules.rbs +3 -0
  277. data/sig/ibex/frontend/parser.rbs +66 -0
  278. data/sig/ibex/frontend/regenerator.rbs +11 -0
  279. data/sig/ibex/frontend/resolution.rbs +33 -0
  280. data/sig/ibex/frontend/resolver.rbs +91 -0
  281. data/sig/ibex/frontend/rule_documentation.rbs +42 -0
  282. data/sig/ibex/frontend/source_cursor.rbs +36 -3
  283. data/sig/ibex/frontend/source_document.rbs +119 -0
  284. data/sig/ibex/frontend/source_loader.rbs +66 -0
  285. data/sig/ibex/frontend/source_span.rbs +53 -0
  286. data/sig/ibex/frontend/token_adapter/declaration_document_state.rbs +21 -0
  287. data/sig/ibex/frontend/token_adapter/declaration_lexer_state.rbs +27 -0
  288. data/sig/ibex/frontend/token_adapter/declaration_state.rbs +73 -5
  289. data/sig/ibex/frontend/token_adapter/rule_state.rbs +20 -0
  290. data/sig/ibex/frontend/token_adapter.rbs +5 -2
  291. data/sig/ibex/frontend.rbs +1 -1
  292. data/sig/ibex/generation_input.rbs +37 -0
  293. data/sig/ibex/generation_manifest.rbs +67 -0
  294. data/sig/ibex/generation_transaction.rbs +82 -0
  295. data/sig/ibex/generation_transaction_recovery.rbs +36 -0
  296. data/sig/ibex/generation_transaction_validation.rbs +65 -0
  297. data/sig/ibex/grammar_tests.rbs +93 -0
  298. data/sig/ibex/ir/automaton_ir.rbs +8 -2
  299. data/sig/ibex/ir/grammar_ir.rbs +75 -15
  300. data/sig/ibex/ir/lexer_ir.rbs +57 -0
  301. data/sig/ibex/ir/migration.rbs +34 -0
  302. data/sig/ibex/ir/serialize.rbs +23 -6
  303. data/sig/ibex/ir/validator/automaton.rbs +109 -0
  304. data/sig/ibex/ir/validator/base.rbs +65 -0
  305. data/sig/ibex/ir/validator/grammar.rbs +184 -0
  306. data/sig/ibex/ir/validator/lexer.rbs +37 -0
  307. data/sig/ibex/ir/validator.rbs +16 -0
  308. data/sig/ibex/ir.rbs +38 -4
  309. data/sig/ibex/lalr/build_metrics.rbs +20 -0
  310. data/sig/ibex/lalr/builder.rbs +95 -15
  311. data/sig/ibex/lalr/conflict_search.rbs +7 -3
  312. data/sig/ibex/lalr/counterexample.rbs +5 -2
  313. data/sig/ibex/lalr/direct_lookaheads.rbs +86 -0
  314. data/sig/ibex/lalr/ielr_partition.rbs +59 -0
  315. data/sig/ibex/lalr/on_error_reductions.rbs +22 -0
  316. data/sig/ibex/lalr.rbs +6 -0
  317. data/sig/ibex/location.rbs +67 -0
  318. data/sig/ibex/lsp/document_handlers.rbs +24 -0
  319. data/sig/ibex/lsp/document_store.rbs +94 -0
  320. data/sig/ibex/lsp/document_store_diagnostics.rbs +20 -0
  321. data/sig/ibex/lsp/document_store_validation.rbs +26 -0
  322. data/sig/ibex/lsp/initialization_handlers.rbs +30 -0
  323. data/sig/ibex/lsp/navigation_handlers.rbs +30 -0
  324. data/sig/ibex/lsp/position_codec.rbs +40 -0
  325. data/sig/ibex/lsp/protocol_error.rbs +32 -0
  326. data/sig/ibex/lsp/request_handlers.rbs +26 -0
  327. data/sig/ibex/lsp/request_support.rbs +40 -0
  328. data/sig/ibex/lsp/server.rbs +55 -0
  329. data/sig/ibex/lsp/symbol_index.rbs +78 -0
  330. data/sig/ibex/lsp/symbol_index_builder.rbs +87 -0
  331. data/sig/ibex/lsp/symbol_index_precedence_references.rbs +18 -0
  332. data/sig/ibex/lsp/symbol_index_source_queries.rbs +26 -0
  333. data/sig/ibex/lsp/symbol_occurrence.rbs +25 -0
  334. data/sig/ibex/lsp/transport.rbs +35 -0
  335. data/sig/ibex/lsp/workspace.rbs +41 -0
  336. data/sig/ibex/lsp/workspace_analyzer.rbs +69 -0
  337. data/sig/ibex/lsp.rbs +7 -0
  338. data/sig/ibex/normalize/declarations.rbs +31 -0
  339. data/sig/ibex/normalize/diagnostics.rbs +9 -0
  340. data/sig/ibex/normalize/expander.rbs +20 -14
  341. data/sig/ibex/normalize/expression.rbs +10 -10
  342. data/sig/ibex/normalize/inline_expansion.rbs +120 -0
  343. data/sig/ibex/normalize/inline_validation.rbs +40 -0
  344. data/sig/ibex/normalize/lexer.rbs +37 -0
  345. data/sig/ibex/normalize/named_references.rbs +20 -0
  346. data/sig/ibex/normalize/nodes.rbs +14 -0
  347. data/sig/ibex/normalize/parameter_ebnf_lowering.rbs +32 -0
  348. data/sig/ibex/normalize/parameter_lowering.rbs +35 -0
  349. data/sig/ibex/normalize/parameter_substitution.rbs +42 -0
  350. data/sig/ibex/normalize/parameter_validation.rbs +44 -0
  351. data/sig/ibex/normalize/parameters.rbs +46 -0
  352. data/sig/ibex/normalize/recovery_declarations.rbs +24 -0
  353. data/sig/ibex/normalize.rbs +123 -18
  354. data/sig/ibex/racc_migration/checker.rbs +36 -0
  355. data/sig/ibex/racc_migration/harness.rbs +16 -0
  356. data/sig/ibex/racc_migration/report.rbs +53 -0
  357. data/sig/ibex/racc_migration.rbs +8 -0
  358. data/sig/ibex/rake_task.rbs +51 -0
  359. data/sig/ibex/samples.rbs +51 -0
  360. data/sig/ibex/table_simulation/result.rbs +32 -0
  361. data/sig/ibex/table_simulation/simulator.rbs +98 -0
  362. data/sig/ibex/table_simulation/step.rbs +40 -0
  363. data/sig/ibex/table_simulation/text.rbs +14 -0
  364. data/sig/ibex/table_simulation.rbs +7 -0
  365. data/sig/ibex/tables.rbs +0 -26
  366. data/sig/ibex/watch/runner.rbs +48 -0
  367. data/sig/ibex/watch/source_snapshot.rbs +42 -0
  368. data/sig/ibex/watch.rbs +7 -0
  369. data/sig/ibex.rbs +2 -0
  370. metadata +301 -16
  371. data/.rubocop.yml +0 -43
  372. data/CHANGELOG.md +0 -30
  373. data/Rakefile +0 -25
  374. data/Steepfile +0 -10
  375. data/docs/compat-notes.md +0 -37
  376. data/docs/lexer-coverage.md +0 -14
  377. data/docs/phase10-extensions.md +0 -27
  378. data/gemfiles/Gemfile +0 -7
  379. data/gemfiles/Gemfile.lock +0 -98
  380. data/lib/ibex/frontend/grammar.y +0 -156
  381. data/lib/ibex/runtime/parser.rb +0 -360
  382. data/lib/ibex/runtime.rb +0 -8
  383. data/sig/ibex/runtime/parser.rbs +0 -167
  384. data/sig/ibex/runtime.rbs +0 -6
@@ -1,6 +1,6 @@
1
1
  # Grammar reference
2
2
 
3
- Ibex's default `racc` mode accepts the compatible grammar described here. `--mode=extended` or an explicit grammar-file
3
+ Ibex's `default` mode accepts the compatible grammar described here. `--mode=extended` or an explicit grammar-file
4
4
  `pragma extended` adds the marked syntax; extensions are never inferred from a production.
5
5
 
6
6
  ## File structure
@@ -8,6 +8,7 @@ Ibex's default `racc` mode accepts the compatible grammar described here. `--mod
8
8
  ```text
9
9
  class Namespace::Parser < OptionalSuperclass
10
10
  pragma extended # optional; must precede ordinary declarations
11
+ pragma cst # optional automatic concrete-tree mode
11
12
  declarations
12
13
  rule
13
14
  productions
@@ -23,23 +24,232 @@ Ruby copied after the parser class
23
24
  The superclass defaults to `Ibex::Runtime::Parser`. Repeated user-code blocks retain their source order and are concatenated.
24
25
  Grammar comments use `#` through end of line or `/* ... */`.
25
26
 
27
+ Extended roots may import explicit fragment files:
28
+
29
+ ```text
30
+ fragment
31
+ token SHARED
32
+ import "nested/expressions.y"
33
+ rule
34
+ shared_rule: SHARED
35
+ end
36
+ ```
37
+
38
+ Fragments own no class, superclass, pragma, options, expected-conflict count, start symbol, or user-code blocks. Their `rule`
39
+ section may be empty. Token, precedence, conversion, display, and type declarations are merged with their original locations.
40
+ `import "relative/path.y"` appears in the declaration section of a root or fragment and requires extended mode. The older
41
+ `include` spelling remains a compatible alias. Paths are
42
+ resolved relative to the including file, must be double-quoted and relative, cannot contain parent traversal, globs, or NUL, and
43
+ must resolve through symlinks to a regular file below the root grammar's canonical directory.
44
+
45
+ Resolution is deterministic depth-first order at include sites. A canonical file reached twice through a diamond is merged only
46
+ at its first occurrence; a cycle reports its exact canonical path loop. `Frontend::Resolver.new(path, mode: :extended).resolve`
47
+ returns the merged root and canonical dependency closure. `Parser#parse_fragment` parses fragment text without I/O, while normal
48
+ `Parser#parse` continues to return only a root and rejects fragment input. The exposed resolution is recursively immutable,
49
+ including AST strings and locations and defensively copied include provenance. Canonical directory ancestry, rather than a
50
+ string-prefix comparison, enforces the source-root boundary and remains valid when the source root is a filesystem or drive root.
51
+
52
+ Immediately preceding `##` line comments attach documentation to a rule:
53
+
54
+ ```text
55
+ ## First line
56
+ ## one extra leading space is retained
57
+ value: TOKEN
58
+ ```
59
+
60
+ Indentation before the comment is ignored. Ibex removes `##` and one optional space, so the example stores
61
+ `"First line\n one extra leading space is retained"`. The lines must be consecutive and directly above the LHS. Blank lines,
62
+ ordinary `#` comments, block comments, grammar tokens, and opaque actions or heredocs break attachment. The association uses
63
+ lossless segment spans rather than rescanning raw text. Documentation works in roots and fragments and is nullable on
64
+ `AST::Rule`.
65
+
66
+ For repeated definitions, every normalized user production keeps its definition's documentation. The nonterminal symbol uses
67
+ the first nonnil text; a different later nonnil text is a positioned error, while the same text is accepted. Grammar IR v2
68
+ serializes `doc` on symbols and productions. Version 1 omits those fields.
69
+
70
+ `ibex doc [--format=markdown|html|railroad] [-o FILE] [--mode=MODE] grammar.y` renders the canonical resolved grammar. Output
71
+ defaults to Markdown on stdout. HTML is accessible and self-contained, railroad output includes visible wrapped descriptions,
72
+ and all formats escape grammar-controlled text. `-o` writes atomically and cannot alias the input grammar.
73
+
74
+ The programmatic frontend can preserve this file exactly with
75
+ `Ibex::Frontend::Parser.new(source, file: path).parse_document`. The returned source document contains the same semantic AST as
76
+ `parse` plus immutable token, whitespace, line-break, comment, action, user-code marker/body, and EOF segments. `render` reproduces the input
77
+ bytes; spans and `slice` use zero-based half-open byte offsets. Line and column positions remain one-based and count Unicode
78
+ scalar values. Input is interpreted as UTF-8 without transcoding and invalid byte sequences are rejected before lexing.
79
+ `parse_source_document` accepts either a root or, in extended mode, an explicit fragment.
80
+
81
+ `ibex fmt [--mode=default|extended] grammar.y` formats to stdout. `fmt --check FILE...` reports all invalid or noncanonical files,
82
+ and `fmt --write FILE...` validates and stages the complete batch before transactionally replacing changed files. Standard input
83
+ is available as `fmt -`; `--stdin-filename=FILE` supplies its control-byte-free diagnostic name. Check and write modes require
84
+ file paths. Formatting changes only whitespace/newline trivia, retains token, comment, action/heredoc, and user-code bytes in
85
+ order, and preserves existing mixed newline spellings at required boundaries. A new boundary uses the first newline even inside
86
+ opaque text. The output must reparse to an identical location-free AST through a stack-safe iterative comparison and is
87
+ idempotent. Write mode rejects aliased targets, preserves full modes and symlinks, and rolls back every target if rename or
88
+ directory synchronization fails. Same-directory hard-link backups are synchronized before installation. A backup whose target
89
+ could not be restored is retained and reported; cleanup failures after every target was committed are reported as status-0
90
+ warnings because they do not undo the committed update.
91
+
92
+ `ibex lsp [--stdio]` serves the same lossless frontend over LSP 3.17 Content-Length-framed standard IO. Initialization requires a
93
+ local `rootUri` or initial `workspaceFolders`; every document URI must remain within those canonical roots. The server uses
94
+ full-text synchronization and UTF-16 positions. Open buffers override disk throughout fragment resolution, so changing or
95
+ creating an included fragment re-diagnoses all known dependent roots. It supports diagnostics, definition, references,
96
+ prepare-rename, rename, and hover. Rename accepts only defined identifiers, checks scope/collisions, includes open versions in
97
+ the workspace edit, and reparses/re-resolves every affected closure before returning edits. Grammar actions, heredocs, and user
98
+ code are never executed. See [editor setup](editor-setup.md).
99
+
100
+ `parse_with_diagnostics(max_diagnostics: 20)` collects up to the limit independently from the lexical and syntax phases, merges
101
+ them in source order, and returns the globally earliest records. Recovery is deliberately limited to complete declarations,
102
+ rules, or outer alternatives at balanced delimiters, so an error inside a nested group cannot synchronize at that group's `|`.
103
+ The result may expose a partial AST containing later valid rules, but `success?` remains false and the original source document
104
+ does not adopt that AST. `ibex diagnose` renders the same records as text or versioned JSON. After a clean root parse it resolves
105
+ includes and emits the first cross-file security, missing-target, cycle, or fragment-syntax failure as
106
+ `frontend.resolution_error`; cross-file recovery is intentionally bounded to that one record. Permission and other actual
107
+ filesystem read failures remain CLI invocation errors on stderr and do not produce a JSON envelope.
108
+
26
109
  ## Declarations
27
110
 
28
- - `pragma extended` enables extended syntax for this grammar even when the CLI uses its default or explicit `--mode=racc`.
111
+ - `pragma extended` enables extended syntax for this grammar even when the CLI uses its default or explicit `--mode=default`.
29
112
  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
-
113
+ are positioned errors. The frontend records the effective mode on the root AST, and Grammar IR v2 records extended mode
114
+ additively so downstream generators preserve its runtime behavior.
115
+ - `pragma cst` enables extended syntax and builds a pure-syntax Red/Green tree
116
+ in parallel with the ordinary semantic value stack. Distinct pragmas may be
117
+ combined in the class header; repeating either one is an error. Grammar IR
118
+ v2 stores the optional `cst: true` setting.
119
+ - `import "relative/path.y"` inserts one explicit fragment through the canonical resolver. `include` is an accepted
120
+ compatibility spelling. Imports are available only in extended mode. Parsing source text alone performs no filesystem
121
+ access; path-based callers use `Frontend::Resolver` to resolve the import graph.
122
+ - `## text` is a lossless line comment rather than a parser declaration. A consecutive block immediately above a rule supplies
123
+ its documentation as described above.
32
124
  - `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`.
125
+ lowercase names are nonterminals unless they are `error`. In extended mode, `token PLUS "+"` declares `PLUS` and assigns
126
+ `"+"` as its display name; the dedicated `display` declaration remains available.
34
127
  - 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.
128
+ begins with `left`, `right`, or `nonassoc` followed by one or more terminals. Extended `%precedence` assigns a level without
129
+ associativity; an equal-level shift/reduce choice therefore remains an unresolved, counted default shift.
36
130
  - `options no_result_var` makes an action's final expression its value. `omit_action_call` is enabled by default;
37
131
  `no_omit_action_call` disables it.
38
132
  - `expect N` suppresses the warning when exactly N unresolved shift/reduce conflicts remain. Conflicts resolved by precedence are
39
133
  retained in Automaton IR but are not counted.
40
- - `start name` overrides the first rule as the start symbol.
134
+ - Extended `%expect-rr N` records the expected reduce/reduce count. Under `--warnings=error`, generation succeeds only when
135
+ both declared counts match.
136
+ - `start name` overrides the first rule as the start symbol. Extended mode accepts an ordered list such as
137
+ `start program expression`. The first name remains the primary entry for `do_parse`; generated parsers also expose
138
+ `parse_program` and `parse_expression`. Shared construction attributes each conflict to its reachable entries and marks a
139
+ conflict as composite when it exists only after their LALR states are merged. `--entry-isolation` builds disjoint state sets
140
+ for every entry and can remove such composite conflicts at the cost of a larger table.
141
+ - `Parser#expected_tokens_exact` simulates reductions and gotos on a private stack to report only viable lookaheads. Extended
142
+ grammars use this LAC result for `expected_tokens`; compatible grammars keep the historical current-state result.
41
143
  - `convert ... end` changes external token objects. The second column is a quoted string containing Ruby source, not the value
42
144
  itself: `NUM ':number'` uses `:number`, while `NUM '"number"'` uses the String `"number"`.
145
+ - Extended mode accepts `display SYMBOL "human name"` to give a terminal or nonterminal a human-facing label without changing
146
+ its identity. Runtime errors, `expected_tokens`, and text, graph, and HTML reports prefer that label.
147
+ - Extended mode accepts `type SYMBOL "RBS type"` to describe the symbol's semantic value. Display labels and type spellings
148
+ must be non-empty quoted values on the declaration line. Type spellings are copied as opaque RBS and should be checked with
149
+ normal RBS validation. A type declared for an eliminated `%inline` rule is retained on its composition-plan result; a display
150
+ label for an eliminated inline rule is rejected because no runtime or diagnostic symbol remains to consume it.
151
+ - Extended roots accept one `%recover sync: TOKEN ...` declaration. Every name must be a unique declared terminal other than
152
+ `error`. When explicit yacc recovery cannot shift `error`, the runtime discards through the first listed synchronization
153
+ token it encounters, pops to a state that accepts it, and then processes that retained token normally.
154
+ - Extended roots accept repeated `%on_error_reduce NAME ...` declarations for nonterminals. Names on one line share a priority;
155
+ each later declaration has higher priority. A uniquely highest-priority completed production fills only table cells that
156
+ would otherwise be errors, so explicit shifts, reductions, accepts, and conflict decisions remain authoritative.
157
+ - Extended roots accept ordered `%test accept "source"` and `%test reject "source"` declarations. Sources must use
158
+ double-quoted Ruby literals and exact duplicate expectation/source pairs are rejected. Grammar IR v2 retains their decoded
159
+ source and location; ordinary generated parser tables do not.
160
+
161
+ ## Generated lexer (extended mode)
162
+
163
+ ```text
164
+ lexer
165
+ skip /\s+/
166
+ NUMBER /\d+/ { |text| Integer(text, 10) }
167
+ state STRING do
168
+ on '"' { pop_state; emit :STRING_END }
169
+ CHUNK /[^"\\]+/
170
+ end
171
+ on '"' { push_state :STRING; emit :STRING_BEGIN }
172
+ end
173
+ ```
174
+
175
+ Each state tries its rules at the current position with an internal `\A` anchor.
176
+ The longest lexeme wins; declaration order breaks equal-length ties. A named
177
+ rule emits its declared terminal and uses the lexeme as its value. Its optional
178
+ action may return a converted value or call `emit TOKEN, value`. `skip` consumes
179
+ without emitting; `on` must call `emit` or `skip`.
180
+
181
+ `state NAME do ... end` creates an exclusive state. Lexer actions call
182
+ `push_state` and `pop_state`; parser actions use the public `lexer_state`
183
+ reader/writer when grammar context changes tokenization. `INITIAL` is reserved,
184
+ states are flat and unique, named rules must reference declared terminals, and
185
+ patterns that match the empty string are rejected.
186
+
187
+ Generated `parse(source, file: "(input)")` accepts String, IO, or Fiber input.
188
+ IO/Fiber chunks can end inside tokens. Locations use one-based grapheme
189
+ `column`/`grapheme_column`, explicit byte columns, and half-open
190
+ `start_byte`/`end_byte` offsets. Unicode property escapes use Ruby Regexp
191
+ semantics.
192
+
193
+ Regex execution remains subject to Ruby Regexp complexity. The static lint
194
+ flags common nested-quantifier shapes as `lexer_redos`; `--warnings=all,error`
195
+ promotes the warning to an error. This heuristic is not a proof of safety.
196
+ Applications must still bound untrusted input and review patterns. See the
197
+ [lexer migration guide](lexer-migration.md) and [ADR 0014](decisions/0014-versioned-generated-lexer.md).
198
+
199
+ ## Concrete syntax trees
200
+
201
+ With `pragma cst`, every shift and reduction builds immutable pure-syntax
202
+ Green values on a stack parallel to semantic values. Semantic actions execute
203
+ unchanged and never enter syntax children. `parse`, `do_parse`, and `yyparse`
204
+ return the semantic result; generated lexers additionally expose
205
+ `parse_with_syntax(source, file:)`, whose result provides `value`,
206
+ `syntax_root`, and `diagnostics`. The root is always
207
+ `source_file(start-symbol, $eof)`. Lazy `CST::SyntaxNode` and
208
+ `CST::SyntaxToken` wrappers provide parents, offsets, spans, locations,
209
+ pattern matching, and typed `@node` fields.
210
+
211
+ Generated lexers accept `--cst-trivia=leading|balanced|drop`; `attach` is an
212
+ alias for `leading`. Leading ownership places skipped text on the following
213
+ token. Balanced ownership places text through the first newline on the
214
+ preceding token and the remainder on the next token. EOF owns final trivia.
215
+ Drop omits trivia and deliberately disables coordinate and incremental APIs.
216
+
217
+ Lexical failures, yacc recovery, panic discards, and bounded repair retain
218
+ consumed input in error nodes, skipped trivia, or zero-width missing tokens.
219
+ Inspect `diagnostics`, `contains_error?`, `each_error`, and token
220
+ `missing?`/`error?`; an unrecoverable parse is rooted under
221
+ `synthetic_root`. Application exceptions still propagate. Only current
222
+ format-v6 structured CST tables are executable; older CST tables fail before
223
+ token consumption and must be regenerated. See the
224
+ [CST guide](cst.md) and [migration guide](cst-migration.md).
225
+
226
+ ## Generated AST nodes and traversal
227
+
228
+ An extended, action-free alternative may end with an explicit node shape:
229
+
230
+ ```text
231
+ rule
232
+ expression: expression PLUS expression @node Addition(left, operator, right)
233
+ | NUMBER @node Number(value)
234
+ end
235
+ ```
236
+
237
+ Fields map positionally to normalized RHS values. Their count must match, each
238
+ must be a unique non-keyword Ruby local identifier, and a node name must be a
239
+ Ruby constant identifier. Reusing a node name is allowed only with the same
240
+ ordered fields. `@node` cannot be combined with a trailing or middle semantic
241
+ action; write the explicit action instead when construction is not positional.
242
+
243
+ The generated parser defines the classes under its `AST` module. Ruby 3.2 and
244
+ later use `Data`; Ruby 3.0 and 3.1 use an immutable keyword-Struct compatibility
245
+ implementation with the same readers, `deconstruct`, and `deconstruct_keys`.
246
+ Generated RBS types each field from its grammar symbol and infers a fully
247
+ annotated nonterminal as the union of its node classes.
248
+
249
+ `AST::Visitor#visit` dispatches to `visit_<node>` and recursively visits fields
250
+ by default. `AST::Listener#walk` calls `enter_<node>`, walks fields, and calls
251
+ `exit_<node>`. The generated RBS enumerates every hook, so a consumer can
252
+ subclass either base under Steep without maintaining a parallel node list.
43
253
 
44
254
  ## Productions and actions
45
255
 
@@ -56,16 +266,111 @@ end
56
266
  Alternatives use `|`; a trailing semicolon is optional. `= TOKEN` overrides a production's precedence. The `error` terminal
57
267
  enables yacc-style recovery.
58
268
 
269
+ Extended grammars can write `%empty` as the sole RHS item to document an empty alternative. An implicit empty alternative stays
270
+ compatible but produces `implicit_empty` under extended warning analysis.
271
+
59
272
  Actions are opaque Ruby between balanced braces. `val` contains RHS values, `result` begins as `val[0]`, and `_values` is a copy
60
273
  of the surrounding value stack. With `no_result_var`, the action's evaluated value is used directly. A middle action becomes an
61
274
  empty helper production and consumes one value position in the enclosing RHS.
62
275
 
276
+ When a lexer returns `[token, value, location]`, actions can read the corresponding locations as `@1`, `@2`, and so on. `@$`
277
+ is the current reduction's immutable `Ibex::Runtime::LocationSpan`. A nonempty span covers the first through last located RHS
278
+ entry; an empty production is zero-width at the current lookahead and remains unlocated if no lookahead location was supplied.
279
+ A zero-width span's `end_*` coordinates equal its start even if the lookahead supplies wider end coordinates. A middle action
280
+ follows that empty-production rule, while its numbered locations address the visible left context. Numbered
281
+ references outside the action's value range are generation errors. Location expressions in strings, regular expressions,
282
+ symbols, comments, and heredoc bodies remain literal text, and ordinary Ruby instance variables are unchanged.
283
+
284
+ `Ibex::Location` is an immutable one-based source range with optional half-open byte offsets. Instance `join` and class
285
+ `Ibex::Location.join` return a covering range and reject mixed files. Lexer-owned hashes and objects remain valid. Inside an
286
+ action, `loc(1)` and `loc(:name)` are callable equivalents of numbered and named RHS locations, while `result_loc` returns the
287
+ current synthesized span. Calls outside an action, unknown names, and out-of-range positions fail explicitly. Parsers that use
288
+ only two-element tokens and no location-sensitive action or observer do not allocate a parallel location stack.
289
+
63
290
  Action and `inner` backtraces use the original grammar filename and line by default. `--line-convert-all` applies the same mapping
64
291
  to `header` and `footer`; `-l` keeps all backtraces on generated-file lines.
65
292
 
293
+ ### Constructor parameters (extended mode)
294
+
295
+ `%param name` adds a required keyword argument to the generated parser constructor. A quoted RBS type is optional:
296
+
297
+ ```text
298
+ %param context "ParserContext"
299
+ %param lexer
300
+ ```
301
+
302
+ `Parser.new(context: ..., lexer: ...)` stores the objects as `@context` and `@lexer`. Semantic actions receive locals with the
303
+ same names, so the context is available without global state; lexer methods in the `inner` section use the instance variables.
304
+ The generated RBS declares typed instance variables and constructor keywords, using `untyped` when no type is supplied.
305
+ Parameters are root-only, unique Ruby local identifiers, and cannot be Ruby keywords. The generated initializer is prepended,
306
+ so a custom superclass initializer may receive any remaining keyword arguments.
307
+
308
+ ### Parameterized rules (extended mode)
309
+
310
+ A parameterized rule is a structural template:
311
+
312
+ ```text
313
+ list(X): X:value { result = [value] }
314
+ | list(X) ',' X { result = val[0] + [val[2]] }
315
+ wrapped(X): (X | list(X))?
316
+ numbers: wrapped(list(NUM)):items { result = items }
317
+ ```
318
+
319
+ The ordered formals are identifiers and calls may be nested. The callee and opening parenthesis must be byte-adjacent.
320
+ Consequently, `list(NUM)` is a call while `ITEM (A | B)` retains its existing meaning as a symbol followed by an EBNF group.
321
+ A named reference or `?`, `*`, or `+` after the closing parenthesis applies to the specialized result.
322
+
323
+ Templates are not standalone nonterminals and cannot be the start symbol. Repeated definitions must use the same ordered
324
+ formals. Duplicate formals, mixed plain/template definitions, terminal collisions, undefined templates, and arity mismatches
325
+ are positioned errors. Formal occurrences are replaced structurally through nested calls, groups, suffixes, and separated
326
+ lists. `X:value` applies `value` to the substituted symbol or call. A formal `= X` precedence override requires that invocation
327
+ to pass one plain symbol for `X`; ordinary precedence overrides are retained unchanged. Named references inside arguments and
328
+ using a formal as a callee are rejected to avoid ambiguous capture.
329
+
330
+ The Normalizer memoizes a specialization before expanding its body, so direct and mutual same-argument recursion reuse one
331
+ internal `$parameter_N` nonterminal. Resumable item and EBNF continuations on an explicit depth-first worklist preserve ordinary
332
+ helper and production order while avoiding dependence on the Ruby stack. The default is
333
+ 1,000 distinct specializations; programmatic callers can configure the positive-Integer
334
+ `max_parameter_specializations:`. Argument-changing recursive instantiation is rejected by structural cycle detection rather
335
+ than an arbitrary depth boundary. Specialized productions retain template actions,
336
+ precedence, types, documentation, locations, and include chains. Grammar IR v2 records
337
+ `expansion.parameter {rule, arguments}`; v1 output omits the expansion record.
338
+
339
+ ### Inline rules (extended mode)
340
+
341
+ `%inline` directly before a definition marks a reusable phrase for structural substitution:
342
+
343
+ ```text
344
+ %inline atom: NUM:value { result = value }
345
+ | '(' expression ')' { result = val[1] }
346
+ %inline pair(X): X X { result = val }
347
+ expression: pair(atom):values { result = values }
348
+ ```
349
+
350
+ The marker must be the exact `%inline` directive followed by whitespace; `%` inside Ruby actions remains Ruby. Inline
351
+ definitions work in roots and fragments and may be plain or parameterized. They do not become final Grammar IR symbols or
352
+ productions. Every definition of a name must agree on its inline marking and parameter formals. Inline rules cannot collide
353
+ with terminals or be the start symbol. Direct, mutual, or indirect cycles whose path contains an inline rule are rejected,
354
+ including cycles through ordinary rules, nested EBNF, and parameterized calls.
355
+
356
+ Alternatives are substituted left-to-right before LR construction. Nested or repeated uses form a deterministic cartesian
357
+ product. The caller's explicit precedence override wins; otherwise the rightmost precedence-contributing inline phrase retains
358
+ its explicit override, and flattened terminals retain normal rightmost-terminal precedence. `Ibex::Normalizer` accepts a
359
+ positive-Integer `max_inline_expansions:` limit, defaulting to 10,000 materialized productions. Parameter actuals contribute
360
+ cycle edges only when their callee position is transitively live, and both cycle validation and expansion use heap worklists so
361
+ grammar nesting is independent of the Ruby call stack.
362
+
363
+ Eliminated reductions still run their explicit or implicit actions in logical post-order. Named references, `val`, `_values`,
364
+ `@N`, `@$`, empty spans, middle actions, `result`/`no_result_var`, and parser instance methods retain their logical rule view.
365
+ Grammar IR v2 serializes the executable sequence in `action.composition.plan` and records
366
+ `expansion.inline {rule}`; dump/load followed by code generation preserves it. `yyaccept` and `yyerror` stop the remaining
367
+ logical fragments and caller after the current fragment completes, and `yyerrok` does not erase that `yyerror`. Version 1 omits
368
+ both metadata families.
369
+
66
370
  The action scanner handles nested braces, quoted/backtick strings and interpolation, `%q/%Q/%w/%W/%i/%I/%x/%r/%s`, regular
67
371
  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).
372
+ interpolated, and multiple heredocs on one opener line are supported. Heredoc terminators follow their indentation mode, and
373
+ multiple openers on one line are consumed in source order.
69
374
 
70
375
  ## Runtime errors
71
376
 
@@ -73,11 +378,69 @@ The default `on_error(token_id, value, value_stack)` raises `Ibex::ParseError`.
73
378
  production to recover. Unknown external token objects receive a temporary negative internal id, remain printable through
74
379
  `token_to_str`, and always invoke `on_error` before recovery is attempted.
75
380
 
76
- Three optional observer methods default to no-ops. `on_shift(token_id, value, state)` follows each ordinary input-token shift;
381
+ With `%recover sync:`, a returned `on_error` first permits ordinary yacc `error`-token recovery. Only when no stack state can
382
+ shift `error` does panic-mode synchronization begin. Discarded application tokens call `on_discard` with reason `recovery`;
383
+ the selected synchronization token is not discarded. `on_error_recover` and the `recover` event fire once after a stack state
384
+ that accepts the synchronization token is found. EOF before a usable synchronization point rejects the parse.
385
+
386
+ Optional observer methods default to no-ops. `on_shift(token_id, value, state)` follows each ordinary input-token shift;
77
387
  `on_reduce(production_id, values, result)` follows a completed semantic action and goto; and
78
388
  `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.
389
+ unexpected-token context. Their `on_shift_location`, `on_reduce_location`, and `on_error_recover_location` companions add
390
+ locations while preserving the original hook signatures. `on_discard(token_id, value, location, reason)` reports an
391
+ application token removed by yacc recovery. Hook return values are ignored and exceptions propagate. `trace_value_printer=`
392
+ opts a parser into value rendering in `yydebug`; without it, traces never expose semantic values. Extended grammars may define
393
+ symbol-specific `%printer SYMBOL { Ruby expression }` formatters. Their `value` local has the symbol's declared semantic type,
394
+ and declared `%param` locals are also available. A programmatic printer overrides generated symbol formatters. Neither form is
395
+ called unless `yydebug` is true, and formatter failures are rendered by exception class without inspecting the value. See
396
+ [ADR 0010](decisions/0010-committed-runtime-observation.md) for the observation boundary.
397
+
398
+ For external tooling, `observe { |event| ... }` registers an ordered observer and returns an opaque subscription accepted by
399
+ `unobserve`. Events are immutable, sequence-numbered per parse session, and cover `start`, `shift`, `reduce`, `error`, `recover`,
400
+ `discard`, `accept`, and `reject`. Semantic values and locations are bounded JSON summaries rather than live objects.
401
+ `Ibex::Runtime::EventJSONLTracer.attach(parser, io:)` writes the versioned schema at
402
+ `schema/runtime-event-v1.schema.json`; write and serialization failures propagate. This API is separate from the legacy
403
+ hook-shaped `Runtime::JSONLTracer`. See [ADR 0010](decisions/0010-committed-runtime-observation.md).
404
+
405
+ Every parser instance owns immutable `Runtime::ResourceLimits`. The defaults allow a 10,000-entry LR state stack and 100
406
+ recovery entries per parse. Pass `resource_limits:` to the generated parser constructor or replace it while the instance is
407
+ idle. Stack shifts/gotos and recovery entries that exceed their budget raise `Ibex::ResourceLimitError` with structured
408
+ resource, limit, observed value, state, and location data. Concurrent parses share immutable generated tables but must use
409
+ distinct parser instances.
410
+
411
+ Assign an immutable `Ibex::Runtime::RepairPolicy` before parsing to opt into bounded insertion, deletion, and replacement search.
412
+ The default costs are 1/1/2 with maximum cost 3, 5,000 configurations/table actions, eight lookahead records, three successful
413
+ shifts, and a 256-state simulated stack. `on_repair(plan)` observes the selected immutable edits after the one `on_error` call and
414
+ before normal action replay. Insertions carry nil values, replacements retain the original value/location, and search failure
415
+ continues into yacc recovery without reporting the incident twice. Push parsing may return `:need_more` while retaining the
416
+ unexpected token. Semantic `yyerror` is not automatically repaired. See [ADR
417
+ 0012](decisions/0012-bounded-nonexecuting-analysis.md).
418
+ The gallery JSON [error UX evidence](error-ux.md) records the SP-4 baseline:
419
+ 8 of 10 selected plans were assessed useful, so the bounded single-plan feature
420
+ continues as an explicit experimental option.
421
+
422
+ The versioned stream can be converted to grammar-test coverage with `ibex coverage collect EVENTS.jsonl`, combined across
423
+ processes with `coverage merge`, and gated with `coverage check --min-states=PERCENT --min-productions=PERCENT`. State coverage
424
+ counts the initial state plus committed shift, reduce-goto, and recovery destinations; production coverage counts committed
425
+ reductions. Complete sessions and generated-parser metadata are required. Reports follow
426
+ `schema/runtime-coverage-v1.schema.json`; see [ADR 0010](decisions/0010-committed-runtime-observation.md).
427
+
428
+ `ibex debug AUTOMATON.json [TOKEN...]` simulates shifts, reductions, gotos, accept, and error directly from validated Automaton
429
+ IR. It never executes actions. When tokens are omitted, supply one terminal name or unique display name per stdin line; a blank
430
+ line or EOF finishes the input. Use `--format=json` for `schema/table-simulation-v1.schema.json`, and bound pathological tables
431
+ with positive `--max-steps` and `--max-stack`. See [ADR 0012](decisions/0012-bounded-nonexecuting-analysis.md).
432
+
433
+ ## Grammar-declared tests
434
+
435
+ `ibex test [--mode=MODE] [--algorithm=NAME] [--entry-isolation] [--timeout=SECONDS] grammar.y` executes every `%test` in source
436
+ order. The generated parser class must be constructible without arguments and define `parse(source)`. Each case uses a fresh
437
+ instance. A normal return counts as acceptance, `Ibex::Runtime::ParseError` counts as rejection, and lexer/application
438
+ exceptions are test errors rather than syntax rejections. Empty suites and grammars with required `%param` declarations fail
439
+ explicitly.
440
+
441
+ The complete suite runs in an isolated Ruby child process with a ten-second default timeout. Generated footer guards remain
442
+ false because a separate runner loads the parser file. Output is TAP-like and the command exits nonzero on any mismatch,
443
+ exception, timeout, or invalid child result. This is process isolation for reliable tooling, not a sandbox for untrusted code.
81
444
 
82
445
  ## Extended EBNF and names
83
446
 
@@ -93,8 +456,8 @@ Extended mode supports:
93
456
  Parenthesized groups may contain sequences, alternatives, and nested EBNF, for example `(KEY VALUE)*`, `(A | B)+`, or
94
457
  `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
458
  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.
459
+ references inside a group are rejected because the group is lowered behind one outer value slot. Text, DOT, Mermaid, and HTML
460
+ reports render lowered helper nonterminals as their original EBNF expressions instead of exposing generated helper names.
98
461
 
99
462
  Actions and named references are supported on an outer production alternative, but not inside a parenthesized EBNF group.
100
463
  Move the action or binding to a separately named ordinary rule and reference that rule from the group.
@@ -102,9 +465,86 @@ Move the action or binding to a separately named ordinary rule and reference tha
102
465
  ## Strict diagnostics
103
466
 
104
467
  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.
468
+ unused precedence declarations, explicitly declared terminals used only by unreachable rules, and a start symbol that cannot
469
+ derive any terminal sentence. They remain silent by default for compatibility. `--warnings=all` prints them,
470
+ `--warnings=all,error` or `--warnings=error` promotes them to command failures, and `--warnings=none` explicitly suppresses them.
471
+ An unexpected LALR conflict also gets an advisory `--algorithm=ielr` note when IELR removes at least one unresolved
472
+ conflict; this note does not change generation or exit status.
473
+
474
+ `ibex check --ambiguity grammar.y` searches every parser conflict for a complete sentence accepted through two interpretations.
475
+ `--max-tokens` and `--max-configurations` bound the search per conflict; `--algorithm=lr1` excludes conflicts introduced only by
476
+ LALR merging. Exit status 1 means a concrete ambiguity was found, 2 means a configuration budget was exhausted, and 0 means no
477
+ ambiguity was found within the declared bounds. The last result is not a general proof of unambiguity. `--format=json` emits
478
+ the versioned check result and explored counts.
479
+
480
+ When an empty helper created for a middle action participates in a conflict, Automaton IR, text reports, HTML, and
481
+ `ibex explain` retain the action's source location as `midrule_origins`. This makes the otherwise synthetic reduction traceable
482
+ to the grammar expression that introduced it.
483
+
484
+ ## Transactional generation and watch mode
485
+
486
+ Ruby generation renders all requested files before replacing any target. Existing targets keep their modes, `--executable`
487
+ selects an executable parser mode, and generated paths that alias an input, have multiple hard links, or collide by portable
488
+ case/Unicode spelling are rejected. Companion outputs publish before the parser.
489
+
490
+ `--manifest[=FILE]` opts into a version-1 JSON manifest; without `=FILE`, `parser.rb` uses `parser.ibex.json`. The manifest is
491
+ published last and records the exact canonical root, fragments, IR input, and message bytes consumed, relevant generation
492
+ options, and every other artifact's path, size, and SHA-256 digest. `--check --manifest` compares all requested output bytes and
493
+ the manifest without rewriting them. `Ibex::GenerationManifest.validate_file(path)` validates the document and its current
494
+ artifact bytes. For coherent concurrent reads, read the manifest, verify every entry, and retry from a newly read manifest if
495
+ anything is missing or mismatched.
496
+
497
+ `--watch` repeatedly applies the same transaction to Ruby file generation. It observes the root, the latest successful include
498
+ closure, unresolved include attempts, an optional messages file, and repairable output paths. Failed candidates leave the last
499
+ successful generation intact; an unchanged failure is reported once. Source changes during render or publication retry after
500
+ debouncing. Watch mode requires a grammar file and cannot be combined with stdin, `--from`, `--check`, or `--check-only`.
501
+ `SIGINT` and `SIGTERM` exit with status 130 and 143. Rake tasks are timestamp-based and reject `--watch`.
502
+
503
+ ## Example-keyed error messages
504
+
505
+ `ibex errors --list grammar.y` prints a deterministic `ibex-messages v2` template without writing a file. Each entry is keyed by
506
+ a shortest token sentence that reaches a syntax error, rather than by an unstable automaton state number:
507
+
508
+ ```text
509
+ # ibex-messages v2
510
+ sentence: NUM '+' ')'
511
+ ## E0042
512
+ # entry: expression
513
+ # state: 7
514
+ # expected: '(', NUM
515
+ | An operand or opening parenthesis is required before ')'.
516
+ end
517
+ ```
518
+
519
+ The `# state:` and `# expected:` lines are review hints; the sentence and error ID are the durable keys. Message lines start with
520
+ `|`; multiple lines are joined with newlines, and `\\`, `\n`, `\t`, and `\r` are the supported escapes. Blank lines and comments
521
+ are ignored.
522
+
523
+ `ibex errors --update grammar.y` atomically writes `grammar.messages`; use `--update=FILE`, `--algorithm=NAME`, or an IR `--from`
524
+ option to select another destination or automaton. It keeps IDs and message bodies, and reports three review classes on stdout:
525
+ `unreachable` when a saved sentence is no longer an error, `uncovered` for a new error state, and `moved` when a sentence now
526
+ reaches a different state. An existing v1 numeric-state file is accepted and migrated on update. Use `--max-tokens=N` and
527
+ `--max-configurations=N` to bound the shortest-sentence search; both default to the counterexample search limits.
528
+
529
+ Pass the reviewed file to Ruby generation with `--messages=grammar.messages`. Each matching message replaces only the generic
530
+ syntax-error sentence. `ParseError#error_id` exposes its stable `E00xx` identifier, and structured token, location,
531
+ expected-token, suggestion, source-line, and caret data remain available. A saved active sentence that no longer reaches an
532
+ error is rejected with an instruction to run the updater.
533
+
534
+ ## Analysis and visualizations
535
+
536
+ `--emit=sets` writes deterministic JSON containing nullable nonterminals and their FIRST and FOLLOW sets. `--dot=FILE` and
537
+ `--mermaid=FILE` write automaton graphs. `--html=FILE` writes a self-contained report with state search, conflict highlighting,
538
+ and a filter that keeps a selected conflict state and its one-hop neighbors. All three visualizations can be produced while
539
+ generating Ruby or when resuming from Automaton IR. `--railroad=FILE` writes a self-contained SVG railroad diagram from normalized
540
+ Grammar IR, so it is also available before automaton construction and when resuming from Grammar or Automaton IR.
541
+
542
+ `ibex explain grammar.y` is the focused conflict view. `--state=N` and `--token=NAME` select their intersection;
543
+ `--format=text|json` chooses step-by-step text or the version-1 document described by `schema/explain-v1.schema.json`.
544
+ `--algorithm=slr|lalr|ielr|lr1` selects construction, `--mode=default|extended` applies the same frontend mode as generation, and both
545
+ counterexample budget options bound its witness search. Search runs only after state and token selection and only for matching
546
+ conflicts. Token selectors prefer a canonical grammar name, then an exact unique display name. Unknown or ambiguous selectors
547
+ are errors; valid selectors with no matching conflict succeed with an empty result.
108
548
 
109
549
  ## Ruby DSL
110
550
 
@@ -123,5 +563,5 @@ end
123
563
  grammar_ir = Ibex::Normalizer.new(ast).normalize
124
564
  ```
125
565
 
126
- The builder also provides `options`, `expect`, `start`, `convert`, `user_code`, `ref(as:)`, `optional`, `star`, `plus`,
127
- `separated_list`, and `inline`.
566
+ The builder also provides `options`, `expect`, `start`, `convert`, `display`, `type`, `user_code`, `ref(as:)`, `optional`,
567
+ `star`, `plus`, `separated_list`, and `inline`.
@@ -0,0 +1,51 @@
1
+ # Migrating a handwritten lexer
2
+
3
+ The generated lexer uses the same `next_token` contract as a handwritten pull
4
+ lexer, so migration can stay inside the grammar file.
5
+
6
+ Given a handwritten scanner:
7
+
8
+ ```ruby
9
+ def next_token
10
+ @scanner.skip(/\s+/)
11
+ return false if @scanner.eos?
12
+ return [:NUMBER, Integer(text, 10)] if (text = @scanner.scan(/\d+/))
13
+ return [punctuation, nil] if (punctuation = @scanner.scan(/[()+]/))
14
+ end
15
+ ```
16
+
17
+ declare the equivalent rules:
18
+
19
+ ```text
20
+ pragma extended
21
+ token NUMBER
22
+ lexer
23
+ skip /\s+/
24
+ NUMBER /\d+/ { |text| Integer(text, 10) }
25
+ on /[()+]/ { |text| emit text, nil }
26
+ end
27
+ ```
28
+
29
+ Then remove the source/scanner initialization and call generated
30
+ `parse(source, file: ...)`. Existing semantic actions, token names, values,
31
+ parser hooks, and location consumers do not change. A custom `parse` wrapper
32
+ may retain an application-specific default filename:
33
+
34
+ ```ruby
35
+ def parse(source, file: "(expression)") = super
36
+ ```
37
+
38
+ Map scanner modes to `state NAME do ... end`, transitions to `push_state` and
39
+ `pop_state`, and parser-controlled modes to `self.lexer_state = :NAME`.
40
+ Patterns are tried only in the active state.
41
+
42
+ Before deleting a handwritten lexer, compare its token triples with
43
+ `parser.lex(source).next_token`, especially at newlines and multibyte text.
44
+ Generated locations expose grapheme and byte columns plus half-open byte
45
+ offsets. String, IO, and Fiber sources share the same output even when chunks
46
+ split a token.
47
+
48
+ The generator rejects empty matches and invalid regex syntax. Its
49
+ `lexer_redos` lint only detects common risky nested quantifiers; it cannot make
50
+ arbitrary Ruby regular expressions safe. Review patterns and bound untrusted
51
+ input.