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
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ require "digest"
5
+
6
+ module Ibex
7
+ # Digest and identity of source bytes actually consumed by one generation.
8
+ class GenerationInput
9
+ attr_reader :path #: String
10
+ attr_reader :sha256 #: String
11
+ attr_reader :bytesize #: Integer
12
+ attr_reader :dev #: Integer
13
+ attr_reader :ino #: Integer
14
+ attr_reader :access_paths #: Array[String]
15
+
16
+ # @rbs (String path, String source, ?access_path: String) -> void
17
+ def initialize(path, source, access_path: path)
18
+ @path = File.realpath(path).freeze
19
+ stat = File.stat(@path)
20
+ @sha256 = Digest::SHA256.hexdigest(source).freeze
21
+ @bytesize = source.bytesize
22
+ @dev = stat.dev
23
+ @ino = stat.ino
24
+ @access_paths = [File.expand_path(access_path)] #: Array[String]
25
+ end
26
+
27
+ # Record another lexical path that supplied these canonical bytes.
28
+ # @rbs (String path) -> void
29
+ def add_access_path(path)
30
+ expanded = File.expand_path(path)
31
+ @access_paths << expanded unless @access_paths.include?(expanded)
32
+ end
33
+
34
+ # @rbs () -> Hash[String, untyped]
35
+ def to_h
36
+ { "path" => @path, "sha256" => @sha256, "bytesize" => @bytesize }
37
+ end
38
+
39
+ # Detect content changes even when size and timestamps are preserved.
40
+ # @rbs () -> bool
41
+ def current?
42
+ source = File.binread(@path)
43
+ stat = File.stat(@path)
44
+ lexical_paths_current? && stat.dev == @dev && stat.ino == @ino &&
45
+ source.bytesize == @bytesize && Digest::SHA256.hexdigest(source) == @sha256
46
+ rescue SystemCallError
47
+ false
48
+ end
49
+
50
+ private
51
+
52
+ # @rbs () -> bool
53
+ def lexical_paths_current?
54
+ @access_paths.all? { |path| File.realpath(path) == @path }
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ require "digest"
5
+ require "json"
6
+
7
+ module Ibex
8
+ # Deterministic description of one published parser generation.
9
+ module GenerationManifest
10
+ SCHEMA_VERSION = 1 #: Integer
11
+ IDENTIFIER = "generation" #: String
12
+ ROOT_KEYS = %w[ibex_manifest schema_version input options artifacts].freeze #: Array[String]
13
+ INPUT_KEYS = %w[root sha256 files].freeze #: Array[String]
14
+ FILE_KEYS = %w[path sha256 bytesize].freeze #: Array[String]
15
+ ARTIFACT_KEYS = %w[kind path sha256 bytesize].freeze #: Array[String]
16
+
17
+ module_function
18
+
19
+ # @rbs (ArtifactSet artifacts, source_records: Array[GenerationInput], options: Hash[String, untyped]) -> String
20
+ def render(artifacts, source_records:, options:)
21
+ files = source_records.map(&:to_h)
22
+ document = {
23
+ "ibex_manifest" => IDENTIFIER,
24
+ "schema_version" => SCHEMA_VERSION,
25
+ "input" => {
26
+ "root" => files.fetch(0).fetch("path"),
27
+ "sha256" => input_digest(files),
28
+ "files" => files
29
+ },
30
+ "options" => sorted_hash(options),
31
+ "artifacts" => artifacts.map { |artifact| artifact_entry(artifact) }
32
+ }
33
+ "#{JSON.pretty_generate(document)}\n"
34
+ end
35
+
36
+ # Validate the manifest shape and, by default, every published artifact digest.
37
+ # @rbs (String source, ?verify_artifacts: bool) -> Hash[String, untyped]
38
+ def validate(source, verify_artifacts: true)
39
+ document = JSON.parse(source)
40
+ validate_document(document)
41
+ verify_entries(document.fetch("artifacts")) if verify_artifacts
42
+ document
43
+ rescue JSON::ParserError, KeyError, TypeError, ArgumentError => e
44
+ raise Ibex::Error, "(manifest):1:1: invalid generation manifest: #{e.message}"
45
+ end
46
+
47
+ # @rbs (String path, ?verify_artifacts: bool) -> Hash[String, untyped]
48
+ def validate_file(path, verify_artifacts: true)
49
+ validate(File.binread(path), verify_artifacts: verify_artifacts)
50
+ rescue SystemCallError, ArgumentError => e
51
+ raise Ibex::Error, "#{path}:1:1: cannot read generation manifest: #{e.message}"
52
+ end
53
+
54
+ # @rbs (Artifact artifact) -> Hash[String, untyped]
55
+ def artifact_entry(artifact)
56
+ {
57
+ "kind" => artifact.kind.to_s,
58
+ "path" => File.expand_path(artifact.path),
59
+ "sha256" => Digest::SHA256.hexdigest(artifact.content),
60
+ "bytesize" => artifact.content.bytesize
61
+ }
62
+ end
63
+ private_class_method :artifact_entry
64
+
65
+ # @rbs (Array[Hash[String, untyped]] files) -> String
66
+ def input_digest(files)
67
+ digest = Digest::SHA256.new
68
+ files.each do |entry|
69
+ digest << entry.fetch("path") << "\0"
70
+ digest << entry.fetch("sha256") << "\0"
71
+ digest << entry.fetch("bytesize").to_s << "\0"
72
+ end
73
+ digest.hexdigest
74
+ end
75
+ private_class_method :input_digest
76
+
77
+ # @rbs (Hash[String, untyped] value) -> Hash[String, untyped]
78
+ def sorted_hash(value)
79
+ value.keys.sort.to_h do |key|
80
+ child = value.fetch(key)
81
+ normalized = child.is_a?(Hash) ? sorted_hash(child) : child
82
+ [key, normalized]
83
+ end
84
+ end
85
+ private_class_method :sorted_hash
86
+
87
+ # @rbs (untyped document) -> void
88
+ def validate_document(document)
89
+ raise TypeError, "document must be an object" unless document.is_a?(Hash)
90
+
91
+ exact_keys!(document, ROOT_KEYS, "document")
92
+ validate_identity(document)
93
+ validate_input(document.fetch("input"))
94
+ artifacts = document.fetch("artifacts")
95
+ raise TypeError, "options must be an object" unless document.fetch("options").is_a?(Hash)
96
+ raise TypeError, "artifacts must be an array" unless artifacts.is_a?(Array)
97
+ raise TypeError, "artifacts must not be empty" if artifacts.empty?
98
+
99
+ validate_entries(artifacts, "artifact", require_kind: true)
100
+ end
101
+ private_class_method :validate_document
102
+
103
+ # @rbs (Hash[untyped, untyped] document) -> void
104
+ def validate_identity(document)
105
+ raise TypeError, "unsupported manifest identifier" unless document["ibex_manifest"] == IDENTIFIER
106
+ raise TypeError, "unsupported schema_version" unless document["schema_version"] == SCHEMA_VERSION
107
+ end
108
+ private_class_method :validate_identity
109
+
110
+ # @rbs (untyped input) -> void
111
+ def validate_input(input)
112
+ raise TypeError, "input must be an object" unless input.is_a?(Hash)
113
+
114
+ exact_keys!(input, INPUT_KEYS, "input")
115
+
116
+ files = input.fetch("files")
117
+ raise TypeError, "input files must be a non-empty array" unless files.is_a?(Array) && !files.empty?
118
+
119
+ validate_entries(files, "input file")
120
+ expected = input_digest(files)
121
+ root = input.fetch("root")
122
+ raise TypeError, "input root must be a non-empty string" unless root.is_a?(String) && !root.empty?
123
+ raise TypeError, "input digest mismatch" unless input.fetch("sha256") == expected
124
+ raise TypeError, "input root mismatch" unless root == files.fetch(0).fetch("path")
125
+ end
126
+ private_class_method :validate_input
127
+
128
+ # @rbs (Array[untyped] entries, String label, ?require_kind: bool) -> void
129
+ def validate_entries(entries, label, require_kind: false)
130
+ entries.each { |entry| validate_entry(entry, label, require_kind) }
131
+ end
132
+ private_class_method :validate_entries
133
+
134
+ # @rbs (untyped entry, String label, bool require_kind) -> void
135
+ def validate_entry(entry, label, require_kind)
136
+ raise TypeError, "#{label} must be an object" unless entry.is_a?(Hash)
137
+
138
+ exact_keys!(entry, require_kind ? ARTIFACT_KEYS : FILE_KEYS, label)
139
+ validate_path(entry.fetch("path"), label)
140
+ validate_kind(entry.fetch("kind"), label) if require_kind
141
+ validate_digest(entry.fetch("sha256"), label)
142
+ bytesize = entry.fetch("bytesize")
143
+ return if bytesize.is_a?(Integer) && bytesize >= 0
144
+
145
+ raise TypeError, "#{label} bytesize must be non-negative"
146
+ end
147
+ private_class_method :validate_entry
148
+
149
+ # @rbs (untyped path, String label) -> void
150
+ def validate_path(path, label)
151
+ raise TypeError, "#{label} path must be a non-empty string" unless path.is_a?(String) && !path.empty?
152
+ return unless path.include?("\0") || path.match?(/[[:cntrl:]]/)
153
+
154
+ raise TypeError, "#{label} path must not contain control characters"
155
+ end
156
+ private_class_method :validate_path
157
+
158
+ # @rbs (untyped kind, String label) -> void
159
+ def validate_kind(kind, label)
160
+ return if kind.is_a?(String) && !kind.empty?
161
+
162
+ raise TypeError, "#{label} kind must be a non-empty string"
163
+ end
164
+ private_class_method :validate_kind
165
+
166
+ # @rbs (untyped digest, String label) -> void
167
+ def validate_digest(digest, label)
168
+ return if digest.is_a?(String) && digest.match?(/\A[0-9a-f]{64}\z/)
169
+
170
+ raise TypeError, "#{label} digest must be SHA-256"
171
+ end
172
+ private_class_method :validate_digest
173
+
174
+ # @rbs (Hash[untyped, untyped] value, Array[String] expected, String label) -> void
175
+ def exact_keys!(value, expected, label)
176
+ keys = value.keys
177
+ raise TypeError, "#{label} keys must be strings" unless keys.all?(String)
178
+
179
+ extras = keys - expected
180
+ missing = expected - keys
181
+ raise TypeError, "#{label} has unknown property #{extras.fetch(0)}" unless extras.empty?
182
+ raise KeyError, "#{label} is missing #{missing.fetch(0)}" unless missing.empty?
183
+ end
184
+ private_class_method :exact_keys!
185
+
186
+ # @rbs (Array[untyped] entries) -> void
187
+ def verify_entries(entries)
188
+ entries.each do |entry|
189
+ path = entry.fetch("path")
190
+ source = File.binread(path)
191
+ raise TypeError, "artifact bytesize mismatch for #{path}" unless source.bytesize == entry.fetch("bytesize")
192
+ raise TypeError, "artifact digest mismatch for #{path}" unless
193
+ Digest::SHA256.hexdigest(source) == entry.fetch("sha256")
194
+ end
195
+ rescue SystemCallError => e
196
+ raise TypeError, "cannot read artifact: #{e.message}"
197
+ end
198
+ private_class_method :verify_entries
199
+ end
200
+ end
@@ -0,0 +1,261 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ require "digest"
5
+ require_relative "error"
6
+ require_relative "generation_transaction_recovery"
7
+ require_relative "generation_transaction_validation"
8
+
9
+ module Ibex
10
+ # Publishes a rendered ArtifactSet with rollback and a manifest-last marker.
11
+ class GenerationTransaction
12
+ include GenerationTransactionRecovery
13
+ include GenerationTransactionValidation
14
+
15
+ class SourceChanged < Ibex::Error; end
16
+
17
+ class Error < Ibex::Error
18
+ attr_reader :rollback_failed #: bool
19
+ attr_reader :recovery_artifacts #: Array[String]
20
+
21
+ # @rbs (String message, ?rollback_failed: bool, ?recovery_artifacts: Array[String]) -> void
22
+ def initialize(message, rollback_failed: false, recovery_artifacts: [])
23
+ super(message)
24
+ @rollback_failed = rollback_failed
25
+ @recovery_artifacts = recovery_artifacts.freeze
26
+ end
27
+ end
28
+
29
+ # rubocop:disable Layout/LineLength
30
+ # @rbs (ArtifactSet artifacts, ?warning: ^(String) -> void, ?stability_check: ^() -> bool, ?source_records: Array[GenerationInput], ?lock_sleeper: ^(Float) -> void) -> void
31
+ def initialize(artifacts, warning: ->(_message) {}, stability_check: -> { true }, source_records: [],
32
+ lock_sleeper: ->(seconds) { sleep(seconds) })
33
+ @artifacts = artifacts
34
+ @warning = warning
35
+ @stability_check = stability_check
36
+ @source_records = source_records
37
+ @lock_sleeper = lock_sleeper
38
+ @locks = [] #: Array[File]
39
+ @locked_paths = [] #: Array[String]
40
+ @records = [] #: Array[Hash[Symbol, untyped]]
41
+ @committed = false
42
+ @temporary_sequence = 0
43
+ end
44
+ # rubocop:enable Layout/LineLength
45
+
46
+ # @rbs () -> void
47
+ def commit
48
+ @committed = false
49
+ prepare_records
50
+ validate_source_boundaries!
51
+ acquire_locks
52
+ prepare_records
53
+ validate_source_boundaries!
54
+ validate_locked_targets!
55
+ ensure_sources_stable!("before staging outputs")
56
+ stage_records
57
+ ensure_sources_stable!("while staging outputs")
58
+ backup_records
59
+ sync_directories!
60
+ install_group(:non_marker)
61
+ sync_directories!
62
+ ensure_sources_stable!("before parser publication")
63
+ install_group(:parser)
64
+ sync_directories!
65
+ ensure_sources_stable!("after parser publication")
66
+ install_group(:manifest)
67
+ sync_directories!
68
+ ensure_sources_stable!("after manifest publication")
69
+ @committed = true
70
+ cleanup_success
71
+ rescue StandardError, SignalException => e
72
+ @committed ? report_post_commit_failure(e) : handle_failure(e)
73
+ ensure
74
+ release_locks
75
+ end
76
+
77
+ private
78
+
79
+ # @rbs () -> void
80
+ def prepare_records
81
+ @records = @artifacts.map do |artifact|
82
+ target = resolved_target(artifact.path)
83
+ {
84
+ artifact: artifact, target: target, directory: File.dirname(target),
85
+ stage: nil, backup: nil, backed_up: false, installed: false,
86
+ existed: File.exist?(target), mode: target_mode(artifact, target)
87
+ }
88
+ end
89
+ grouped = @records.group_by do |record|
90
+ portable_target_key(record.fetch(:target))
91
+ end
92
+ collision = grouped.find { |_key, records| records.length > 1 }
93
+ return unless collision
94
+
95
+ kinds = collision.fetch(1).map { |record| record.fetch(:artifact).kind }.join(", ")
96
+ targets = collision.fetch(1).map { |record| record.fetch(:target) }.join(", ")
97
+ raise Error, "(generation):1:1: artifact targets collide: #{targets} (#{kinds})"
98
+ end
99
+
100
+ # @rbs (Artifact artifact, String target) -> Integer
101
+ def target_mode(artifact, target)
102
+ return artifact.mode if artifact.mode
103
+ return File.stat(target).mode & 0o7777 if File.exist?(target)
104
+
105
+ mask = File.umask
106
+ File.umask(mask)
107
+ 0o666 & ~mask
108
+ end
109
+
110
+ # @rbs () -> void
111
+ def acquire_locks
112
+ lock_paths = @records.map { |record| lock_path(record.fetch(:target)) }.uniq.sort
113
+ validate_lock_boundaries!(lock_paths)
114
+ @locked_paths = lock_paths
115
+ @locks = [] #: Array[File]
116
+ lock_paths.each do |path|
117
+ flags = File::RDWR | File::CREAT
118
+ flags |= File.const_get(:NOFOLLOW) if File.const_defined?(:NOFOLLOW)
119
+ lock = File.new(path, flags, 0o600)
120
+ @locks << lock
121
+ validate_lock_identity!(path, lock)
122
+ acquire_lock(lock, path)
123
+ validate_lock_identity!(path, lock)
124
+ end
125
+ end
126
+
127
+ # @rbs (File lock, String path) -> void
128
+ def acquire_lock(lock, path)
129
+ until lock.flock(File::LOCK_EX | File::LOCK_NB)
130
+ unless @stability_check.call
131
+ raise SourceChanged, "(generation):1:1: generation cancelled while waiting for lock: #{path}"
132
+ end
133
+
134
+ @lock_sleeper.call(0.05)
135
+ end
136
+ end
137
+
138
+ # @rbs (String target) -> String
139
+ def lock_path(target)
140
+ digest = Digest::SHA256.hexdigest(target).slice(0, 16)
141
+ File.join(File.dirname(target), ".ibex-generation-#{digest}.lock")
142
+ end
143
+
144
+ # @rbs () -> void
145
+ def stage_records
146
+ @records.each do |record|
147
+ temporary = create_temporary_file(record.fetch(:directory), ".stage")
148
+ begin
149
+ record[:stage] = temporary.path.dup
150
+ temporary.binmode
151
+ temporary.write(record.fetch(:artifact).content)
152
+ temporary.flush
153
+ temporary.chmod(record.fetch(:mode))
154
+ temporary.fsync
155
+ ensure
156
+ temporary.close
157
+ end
158
+ end
159
+ end
160
+
161
+ # @rbs () -> void
162
+ def backup_records
163
+ @records.each do |record|
164
+ target = record.fetch(:target)
165
+ next unless File.exist?(target)
166
+
167
+ validate_existing_target!(target)
168
+ backup = create_backup_link(target, record.fetch(:directory))
169
+ record[:backup] = backup
170
+ record[:backed_up] = true
171
+ end
172
+ end
173
+
174
+ # @rbs (String directory, String suffix) -> File
175
+ def create_temporary_file(directory, suffix)
176
+ with_temporary_path(directory, suffix) do |path|
177
+ flags = File::WRONLY | File::CREAT | File::EXCL
178
+ flags |= File.const_get(:NOFOLLOW) if File.const_defined?(:NOFOLLOW)
179
+ return File.new(path, flags, 0o600)
180
+ end
181
+ raise Error, "(generation):1:1: temporary file is unavailable in #{directory}"
182
+ end
183
+
184
+ # @rbs (String target, String directory) -> String
185
+ def create_backup_link(target, directory)
186
+ with_temporary_path(directory, ".backup") do |path|
187
+ File.link(target, path)
188
+ return path
189
+ end
190
+ raise Error, "(generation):1:1: backup path is unavailable in #{directory}"
191
+ end
192
+
193
+ # @rbs (String directory, String suffix) { (String) -> void } -> void
194
+ def with_temporary_path(directory, suffix)
195
+ 100.times do
196
+ @temporary_sequence += 1
197
+ name = ".ibex-generation-#{Process.pid}-#{object_id.to_s(36)}-#{@temporary_sequence.to_s(36)}#{suffix}"
198
+ yield File.join(directory, name)
199
+ rescue Errno::EEXIST
200
+ next
201
+ end
202
+ end
203
+
204
+ # @rbs (Symbol group) -> void
205
+ def install_group(group)
206
+ selected = @records.select { |record| publication_group(record.fetch(:artifact)) == group }
207
+ selected.sort_by { |record| record.fetch(:target) }.each do |record|
208
+ File.rename(record.fetch(:stage), record.fetch(:target))
209
+ record[:stage] = nil
210
+ record[:installed] = true
211
+ end
212
+ end
213
+
214
+ # @rbs (Artifact artifact) -> Symbol
215
+ def publication_group(artifact)
216
+ return :manifest if artifact.kind == :manifest
217
+ return :parser if artifact.kind == :parser
218
+
219
+ :non_marker
220
+ end
221
+
222
+ # @rbs () -> void
223
+ def sync_directories!
224
+ @records.map { |record| record.fetch(:directory) }.uniq.sort.each do |directory|
225
+ File.open(directory, File::RDONLY, &:fsync)
226
+ end
227
+ end
228
+
229
+ # @rbs () -> void
230
+ def cleanup_success
231
+ failures = cleanup_artifacts(preserve_backups: false)
232
+ sync_failures = sync_directories_best_effort
233
+ messages = failures + sync_failures
234
+ warn_best_effort("generation cleanup incomplete: #{messages.join('; ')}") unless messages.empty?
235
+ end
236
+
237
+ # @rbs (Exception error) -> void
238
+ def report_post_commit_failure(error)
239
+ warn_best_effort("generation committed but cleanup failed: #{error.message}")
240
+ end
241
+
242
+ # @rbs (String message) -> void
243
+ def warn_best_effort(message)
244
+ @warning.call(message)
245
+ rescue StandardError
246
+ nil
247
+ end
248
+
249
+ # @rbs () -> void
250
+ def release_locks
251
+ @locks.reverse_each do |lock|
252
+ lock.flock(File::LOCK_UN)
253
+ lock.close
254
+ rescue StandardError
255
+ nil
256
+ end
257
+ @locks = [] #: Array[File]
258
+ @locked_paths = [] #: Array[String]
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ module Ibex
5
+ # Failure recovery and best-effort cleanup for GenerationTransaction.
6
+ module GenerationTransactionRecovery
7
+ private
8
+
9
+ # @rbs (Exception cause) -> bot
10
+ def handle_failure(cause)
11
+ rollback_failures = rollback
12
+ cleanup_failures = cleanup_artifacts(preserve_backups: !rollback_failures.empty?)
13
+ sync_failures = sync_directories_best_effort
14
+ raise cause if source_change_clean?(cause, rollback_failures, cleanup_failures, sync_failures)
15
+
16
+ recovery = recovery_artifacts
17
+ details = failure_details(cause, rollback_failures, cleanup_failures, sync_failures, recovery)
18
+ raise GenerationTransaction::Error.new(
19
+ "(generation):1:1: #{details.join(' | ')}",
20
+ rollback_failed: !rollback_failures.empty?, recovery_artifacts: recovery
21
+ ), cause: cause
22
+ end
23
+
24
+ # @rbs (Exception cause, Array[String] rollback, Array[String] cleanup, Array[String] sync) -> bool
25
+ def source_change_clean?(cause, rollback, cleanup, sync)
26
+ cause.is_a?(GenerationTransaction::SourceChanged) && rollback.empty? && cleanup.empty? && sync.empty?
27
+ end
28
+
29
+ # rubocop:disable Layout/LineLength
30
+ # @rbs (Exception cause, Array[String] rollback, Array[String] cleanup, Array[String] sync, Array[String] recovery) -> Array[String]
31
+ def failure_details(cause, rollback, cleanup, sync, recovery)
32
+ details = [cause.message]
33
+ details << "rollback failed: #{rollback.join('; ')}" unless rollback.empty?
34
+ details << "cleanup failed: #{cleanup.join('; ')}" unless cleanup.empty?
35
+ details << "directory sync failed: #{sync.join('; ')}" unless sync.empty?
36
+ details << "recovery artifacts: #{recovery.join(', ')}" unless recovery.empty?
37
+ details
38
+ end
39
+ # rubocop:enable Layout/LineLength
40
+
41
+ # @rbs () -> Array[String]
42
+ def recovery_artifacts
43
+ @records.filter_map do |record|
44
+ backup = record[:backup]
45
+ backup if backup && File.exist?(backup)
46
+ end
47
+ end
48
+
49
+ # @rbs () -> Array[String]
50
+ def rollback
51
+ failures = [] #: Array[String]
52
+ @records.reverse_each do |record|
53
+ rollback_record(record)
54
+ rescue StandardError => e
55
+ failures << "#{record.fetch(:target)}: #{e.message}"
56
+ end
57
+ failures
58
+ end
59
+
60
+ # @rbs (Hash[Symbol, untyped] record) -> void
61
+ def rollback_record(record)
62
+ target = record.fetch(:target)
63
+ if record.fetch(:backed_up)
64
+ if record.fetch(:installed)
65
+ File.rename(record.fetch(:backup), target)
66
+ else
67
+ File.unlink(record.fetch(:backup))
68
+ end
69
+ record[:backup] = nil
70
+ record[:backed_up] = false
71
+ record[:installed] = false
72
+ elsif record.fetch(:installed)
73
+ File.unlink(target)
74
+ record[:installed] = false
75
+ end
76
+ end
77
+
78
+ # @rbs (preserve_backups: bool) -> Array[String]
79
+ def cleanup_artifacts(preserve_backups:)
80
+ failures = [] #: Array[String]
81
+ @records.each do |record|
82
+ paths = [record[:stage]]
83
+ paths << record[:backup] unless preserve_backups
84
+ paths.compact.each { |path| remove_artifact(path, failures) }
85
+ end
86
+ failures
87
+ end
88
+
89
+ # @rbs (String path, Array[String] failures) -> void
90
+ def remove_artifact(path, failures)
91
+ File.unlink(path)
92
+ rescue Errno::ENOENT
93
+ nil
94
+ rescue StandardError => e
95
+ failures << "#{path}: #{e.message}"
96
+ end
97
+
98
+ # @rbs () -> Array[String]
99
+ def sync_directories_best_effort
100
+ failures = [] #: Array[String]
101
+ @records.map { |record| record.fetch(:directory) }.uniq.sort.each do |directory|
102
+ File.open(directory, File::RDONLY, &:fsync)
103
+ rescue StandardError => e
104
+ failures << "#{directory}: #{e.message}"
105
+ end
106
+ failures
107
+ end
108
+ end
109
+ end