expressir 1.2.10-aarch64-linux → 2.4.29-aarch64-linux

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 (467) hide show
  1. checksums.yaml +4 -4
  2. data/.cargo/config.toml +3 -0
  3. data/.github/scripts/test_installed_gem.rb +34 -0
  4. data/.github/workflows/codeql.yml +35 -0
  5. data/.github/workflows/docs.yml +99 -0
  6. data/.github/workflows/links.yml +100 -0
  7. data/.github/workflows/native-gems.yml +201 -0
  8. data/.github/workflows/rake.yml +10 -296
  9. data/.github/workflows/release.yml +26 -114
  10. data/.github/workflows/rust-ext.yml +41 -0
  11. data/.github/workflows/stress.yml +36 -0
  12. data/.github/workflows/validate_schemas.yml +48 -0
  13. data/.github/workflows/verify_remarks.yml +33 -0
  14. data/.gitignore +33 -1
  15. data/.rubocop.yml +11 -11
  16. data/.rubocop_todo.yml +297 -0
  17. data/CHANGELOG.md +159 -0
  18. data/Gemfile +15 -1
  19. data/README.adoc +2675 -47
  20. data/Rakefile +34 -10
  21. data/TODO.bugs/01-stale-transformer-autoload.md +39 -0
  22. data/TODO.bugs/02-parser-class-instance-vars.md +36 -0
  23. data/TODO.bugs/03-builder-mutable-state.md +43 -0
  24. data/TODO.bugs/04-formatter-public-send-dispatch.md +53 -0
  25. data/TODO.bugs/05-anonymous-formatter-subclass.md +45 -0
  26. data/TODO.bugs/06-collection-registry-single-source.md +53 -0
  27. data/TODO.bugs/07-require-relative-cleanup.md +42 -0
  28. data/TODO.bugs/08-require-expressir-in-commands.md +34 -0
  29. data/TODO.bugs/09-parser-split.md +53 -0
  30. data/TODO.bugs/10-to-s-override.md +42 -0
  31. data/TODO.bugs/11-parser-class-variables.md +39 -0
  32. data/TODO.bugs/12-marker-modules-vs-registry.md +64 -0
  33. data/TODO.bugs/13-string-literal-scanner-limitation.md +52 -0
  34. data/TODO.bugs/14-model-formatting-leak.md +30 -0
  35. data/TODO.bugs/15-expression-children-macro.md +27 -0
  36. data/TODO.bugs/16-pretty-formatter-duplication.md +28 -0
  37. data/TODO.bugs/17-snake-case-cache-mutable-constant.md +28 -0
  38. data/TODO.bugs/18-const-get-private-constants.md +30 -0
  39. data/TODO.bugs/19-format-methods-public.md +22 -0
  40. data/TODO.bugs/20-coverage-nested-entities-dedup.md +20 -0
  41. data/TODO.bugs/21-operator-tokens-secondary-dispatch.md +21 -0
  42. data/TODO.bugs/22-builder-fast-path-wrappers.md +32 -0
  43. data/TODO.bugs/23-coverage-inverse-maps.md +21 -0
  44. data/TODO.bugs/24-streaming-builder-complexity.md +19 -0
  45. data/TODO.bugs/25-debug-puts-in-production.md +21 -0
  46. data/TODO.bugs/26-generic-entity-children-misplaced.md +21 -0
  47. data/TODO.bugs/27-package-build-god-method.md +19 -0
  48. data/TODO.bugs/28-package-god-class.md +30 -0
  49. data/TODO.bugs/29-validate-ascii-god-class.md +24 -0
  50. data/TODO.bugs/30-unicode-map-extraction.md +19 -0
  51. data/TODO.bugs/README.md +43 -0
  52. data/TODO.max-perf/01-restore-ci-green.md +36 -0
  53. data/TODO.max-perf/02-streaming-parse-path.md +31 -0
  54. data/TODO.max-perf/03-cli-parallel-opt-in.md +27 -0
  55. data/TODO.max-perf/04-benchmark-harness.md +28 -0
  56. data/TODO.max-perf/05-parallel-fidelity-specs.md +22 -0
  57. data/TODO.max-perf/06-builder-cpu-audit.md +41 -0
  58. data/TODO.max-perf/07-upstream-parsanol-roadmap.md +27 -0
  59. data/TODO.max-perf/08-builder-build-perf.md +45 -0
  60. data/TODO.max-perf/09-grammar-cold-start.md +25 -0
  61. data/TODO.max-perf/10-parser-facade-hygiene.md +23 -0
  62. data/TODO.max-perf/11-ci-green-closeout.md +25 -0
  63. data/TODO.max-perf/12-require-boot-profile.md +25 -0
  64. data/TODO.max-perf/13-key-conversion-specs.md +26 -0
  65. data/TODO.max-perf/14-builder-call-handler-audit.md +28 -0
  66. data/TODO.parity-ee/00-overview.md +76 -0
  67. data/TODO.parity-ee/01-annex-g-rule-extraction.md +41 -0
  68. data/TODO.parity-ee/02-eeng-algorithm-inventory.md +8 -0
  69. data/TODO.parity-ee/03-eeng-oracle-harness.md +33 -0
  70. data/TODO.parity-ee/04-shtolo-concatenate.md +44 -0
  71. data/TODO.parity-ee/05-shtolo-longform-flatten.md +53 -0
  72. data/TODO.parity-ee/06-interface-scheduling-parity.md +47 -0
  73. data/TODO.parity-ee/07-semantic-checks-port.md +35 -0
  74. data/TODO.parity-ee/08-pretty-roundtrip-gate.md +27 -0
  75. data/TODO.parity-ee/09-smrl-index-and-listing.md +22 -0
  76. data/TODO.parity-ee/10-interface-dot-graph.md +21 -0
  77. data/TODO.parity-ee/11-import-eeng-tests.md +79 -0
  78. data/TODO.parity-ee/12-full-check-catalog.md +67 -0
  79. data/TODO.parity-ee/13-population-p21.md +54 -0
  80. data/TODO.parity-ee/14-shtolo-completion.md +44 -0
  81. data/TODO.parity-ee/15-part28-xml.md +51 -0
  82. data/TODO.parity-ee/16-compare-patch.md +48 -0
  83. data/TODO.parity-ee/17-official-tests.md +42 -0
  84. data/TODO.parity-ee/18-mim-mapping.md +47 -0
  85. data/TODO.parity-ee/19-architecture.md +97 -0
  86. data/TODO.parity-ee/parity-matrix.md +108 -0
  87. data/TODO.refpath-v2/00-overview.md +64 -0
  88. data/TODO.suma-improvements/00-overview.md +23 -0
  89. data/TODO.suma-improvements/01-prebuilt-platform-gems.md +33 -0
  90. data/TODO.suma-improvements/02-suma-e2e.md +39 -0
  91. data/TODO.suma-improvements/03-item-graph.md +22 -0
  92. data/TODO.suma-improvements/04-compiled-set-default.md +16 -0
  93. data/TODO.suma-improvements/05-validation-in-docs.md +17 -0
  94. data/TODO.suma-improvements/06-memory-remeasure.md +36 -0
  95. data/TODO.suma-improvements/07-part28-xml.md +15 -0
  96. data/TODO.suma-improvements/08-windows-gnu-toolchain.md +15 -0
  97. data/TODO.suma-improvements/09-checker-tranche3.md +13 -0
  98. data/TODO.suma-improvements/10-expressdoc.md +8 -0
  99. data/benchmark/srl_benchmark.rb +458 -0
  100. data/benchmark/srl_native_benchmark.rb +146 -0
  101. data/benchmark/srl_ruby_benchmark.rb +132 -0
  102. data/bin/rspec +2 -1
  103. data/docs/Gemfile +12 -0
  104. data/docs/_config.yml +141 -0
  105. data/docs/_guides/changes/changes-format.adoc +778 -0
  106. data/docs/_guides/changes/importing-eengine.adoc +898 -0
  107. data/docs/_guides/changes/index.adoc +396 -0
  108. data/docs/_guides/changes/programmatic-usage.adoc +1038 -0
  109. data/docs/_guides/changes/validating-changes.adoc +681 -0
  110. data/docs/_guides/cli/benchmark-performance.adoc +834 -0
  111. data/docs/_guides/cli/coverage-analysis.adoc +921 -0
  112. data/docs/_guides/cli/format-schemas.adoc +547 -0
  113. data/docs/_guides/cli/index.adoc +8 -0
  114. data/docs/_guides/cli/managing-changes.adoc +927 -0
  115. data/docs/_guides/cli/validate-ascii.adoc +645 -0
  116. data/docs/_guides/cli/validate-schemas.adoc +534 -0
  117. data/docs/_guides/formatter/formatter-architecture.adoc +401 -0
  118. data/docs/_guides/index.adoc +165 -0
  119. data/docs/_guides/ler/creating-packages.adoc +664 -0
  120. data/docs/_guides/ler/index.adoc +305 -0
  121. data/docs/_guides/ler/loading-packages.adoc +707 -0
  122. data/docs/_guides/ler/package-formats.adoc +748 -0
  123. data/docs/_guides/ler/querying-packages.adoc +826 -0
  124. data/docs/_guides/ler/step-packages.adoc +385 -0
  125. data/docs/_guides/ler/validating-packages.adoc +750 -0
  126. data/docs/_guides/liquid/basic-templates.adoc +813 -0
  127. data/docs/_guides/liquid/documentation-generation.adoc +1042 -0
  128. data/docs/_guides/liquid/drops-reference.adoc +829 -0
  129. data/docs/_guides/liquid/filters-and-tags.adoc +912 -0
  130. data/docs/_guides/liquid/index.adoc +468 -0
  131. data/docs/_guides/manifests/creating-manifests.adoc +483 -0
  132. data/docs/_guides/manifests/index.adoc +307 -0
  133. data/docs/_guides/manifests/resolving-manifests.adoc +557 -0
  134. data/docs/_guides/manifests/validating-manifests.adoc +713 -0
  135. data/docs/_guides/ruby-api/formatting-schemas.adoc +605 -0
  136. data/docs/_guides/ruby-api/index.adoc +257 -0
  137. data/docs/_guides/ruby-api/parsing-files.adoc +421 -0
  138. data/docs/_guides/ruby-api/search-engine.adoc +609 -0
  139. data/docs/_guides/ruby-api/working-with-repository.adoc +577 -0
  140. data/docs/_pages/data-model.adoc +665 -0
  141. data/docs/_pages/express-language.adoc +506 -0
  142. data/docs/_pages/getting-started.adoc +414 -0
  143. data/docs/_pages/index.adoc +116 -0
  144. data/docs/_pages/introduction.adoc +256 -0
  145. data/docs/_pages/ler-packages.adoc +837 -0
  146. data/docs/_pages/parsers.adoc +709 -0
  147. data/docs/_pages/schema-manifests.adoc +431 -0
  148. data/docs/_references/index.adoc +228 -0
  149. data/docs/_tutorials/creating-ler-package.adoc +735 -0
  150. data/docs/_tutorials/documentation-coverage.adoc +795 -0
  151. data/docs/_tutorials/formatting-schemas.adoc +89 -0
  152. data/docs/_tutorials/index.adoc +231 -0
  153. data/docs/_tutorials/liquid-templates.adoc +806 -0
  154. data/docs/_tutorials/parsing-your-first-schema.adoc +522 -0
  155. data/docs/_tutorials/querying-schemas.adoc +751 -0
  156. data/docs/_tutorials/working-with-multiple-schemas.adoc +676 -0
  157. data/docs/index.adoc +242 -0
  158. data/docs/lychee.toml +90 -0
  159. data/examples/demo_ler_usage.sh +86 -0
  160. data/examples/ler/README.md +111 -0
  161. data/examples/ler/simple_example.ler +0 -0
  162. data/examples/ler/simple_schema.exp +33 -0
  163. data/examples/ler_build.rb +75 -0
  164. data/examples/ler_cli.rb +79 -0
  165. data/examples/ler_demo_complete.rb +276 -0
  166. data/examples/ler_query.rb +91 -0
  167. data/examples/ler_query_examples.rb +305 -0
  168. data/examples/ler_stats.rb +81 -0
  169. data/examples/phase3_demo.rb +159 -0
  170. data/examples/query_demo_simple.rb +131 -0
  171. data/exe/expressir +6 -17
  172. data/exe/expressir-format +14 -0
  173. data/exe/{format-test → expressir-format-test} +8 -7
  174. data/expressir.gemspec +50 -37
  175. data/lib/expressir/3.2/expressir_core.so +0 -0
  176. data/lib/expressir/3.3/expressir_core.so +0 -0
  177. data/lib/expressir/3.4/expressir_core.so +0 -0
  178. data/lib/expressir/4.0/expressir_core.so +0 -0
  179. data/lib/expressir/benchmark.rb +310 -0
  180. data/lib/expressir/changes/item_change.rb +20 -0
  181. data/lib/expressir/changes/mapping_change.rb +16 -0
  182. data/lib/expressir/changes/schema_change.rb +85 -0
  183. data/lib/expressir/changes/version_change.rb +26 -0
  184. data/lib/expressir/changes.rb +9 -0
  185. data/lib/expressir/cli.rb +138 -11
  186. data/lib/expressir/commands/base.rb +25 -0
  187. data/lib/expressir/commands/benchmark.rb +59 -0
  188. data/lib/expressir/commands/benchmark_cache.rb +80 -0
  189. data/lib/expressir/commands/changes.rb +34 -0
  190. data/lib/expressir/commands/changes_import_eengine.rb +146 -0
  191. data/lib/expressir/commands/changes_validate.rb +63 -0
  192. data/lib/expressir/commands/check.rb +56 -0
  193. data/lib/expressir/commands/clean.rb +20 -0
  194. data/lib/expressir/commands/coverage.rb +458 -0
  195. data/lib/expressir/commands/expand.rb +26 -0
  196. data/lib/expressir/commands/file_violations.rb +70 -0
  197. data/lib/expressir/commands/fix.rb +33 -0
  198. data/lib/expressir/commands/flatten.rb +29 -0
  199. data/lib/expressir/commands/format.rb +43 -0
  200. data/lib/expressir/commands/manifest.rb +420 -0
  201. data/lib/expressir/commands/mapping_validate.rb +68 -0
  202. data/lib/expressir/commands/non_ascii_character.rb +49 -0
  203. data/lib/expressir/commands/non_ascii_violation_collection.rb +301 -0
  204. data/lib/expressir/commands/package.rb +1222 -0
  205. data/lib/expressir/commands/parity_inputs.rb +160 -0
  206. data/lib/expressir/commands/validate.rb +106 -0
  207. data/lib/expressir/commands/validate_ascii.rb +94 -0
  208. data/lib/expressir/commands/validate_load.rb +91 -0
  209. data/lib/expressir/commands/version.rb +9 -0
  210. data/lib/expressir/commands/xsd.rb +23 -0
  211. data/lib/expressir/commands.rb +30 -0
  212. data/lib/expressir/config.rb +40 -3
  213. data/lib/expressir/coverage.rb +510 -0
  214. data/lib/expressir/eengine/arm_compare_report.rb +23 -0
  215. data/lib/expressir/eengine/changes_section.rb +16 -0
  216. data/lib/expressir/eengine/compare_report.rb +69 -0
  217. data/lib/expressir/eengine/mim_compare_report.rb +23 -0
  218. data/lib/expressir/eengine/modified_object.rb +21 -0
  219. data/lib/expressir/eengine.rb +9 -0
  220. data/lib/expressir/errors.rb +113 -0
  221. data/lib/expressir/express/adoc_hyperlink_formatter.rb +30 -0
  222. data/lib/expressir/express/adoc_source_formatter.rb +12 -0
  223. data/lib/expressir/express/ast_key_converter.rb +114 -0
  224. data/lib/expressir/express/builder.rb +277 -0
  225. data/lib/expressir/express/builder_context.rb +22 -0
  226. data/lib/expressir/express/builder_registry.rb +397 -0
  227. data/lib/expressir/express/builders/attribute_decl_builder.rb +32 -0
  228. data/lib/expressir/express/builders/built_in_builder.rb +75 -0
  229. data/lib/expressir/express/builders/constant_builder.rb +108 -0
  230. data/lib/expressir/express/builders/declaration_builder.rb +20 -0
  231. data/lib/expressir/express/builders/derive_clause_builder.rb +14 -0
  232. data/lib/expressir/express/builders/derived_attr_builder.rb +28 -0
  233. data/lib/expressir/express/builders/domain_rule_builder.rb +21 -0
  234. data/lib/expressir/express/builders/entity_decl_builder.rb +116 -0
  235. data/lib/expressir/express/builders/explicit_attr_builder.rb +49 -0
  236. data/lib/expressir/express/builders/expression_builder.rb +416 -0
  237. data/lib/expressir/express/builders/function_decl_builder.rb +86 -0
  238. data/lib/expressir/express/builders/helpers.rb +148 -0
  239. data/lib/expressir/express/builders/interface_builder.rb +151 -0
  240. data/lib/expressir/express/builders/inverse_attr_builder.rb +43 -0
  241. data/lib/expressir/express/builders/inverse_attr_type_builder.rb +30 -0
  242. data/lib/expressir/express/builders/inverse_clause_builder.rb +14 -0
  243. data/lib/expressir/express/builders/literal_builder.rb +93 -0
  244. data/lib/expressir/express/builders/procedure_decl_builder.rb +82 -0
  245. data/lib/expressir/express/builders/qualifier_builder.rb +102 -0
  246. data/lib/expressir/express/builders/reference_builder.rb +18 -0
  247. data/lib/expressir/express/builders/rule_decl_builder.rb +97 -0
  248. data/lib/expressir/express/builders/schema_body_decl_builder.rb +18 -0
  249. data/lib/expressir/express/builders/schema_decl_builder.rb +56 -0
  250. data/lib/expressir/express/builders/schema_version_builder.rb +34 -0
  251. data/lib/expressir/express/builders/simple_id_builder.rb +17 -0
  252. data/lib/expressir/express/builders/statement_builder.rb +243 -0
  253. data/lib/expressir/express/builders/subtype_constraint_builder.rb +166 -0
  254. data/lib/expressir/express/builders/syntax_builder.rb +30 -0
  255. data/lib/expressir/express/builders/type_builder.rb +238 -0
  256. data/lib/expressir/express/builders/type_decl_builder.rb +26 -0
  257. data/lib/expressir/express/builders/unique_clause_builder.rb +20 -0
  258. data/lib/expressir/express/builders/unique_rule_builder.rb +41 -0
  259. data/lib/expressir/express/builders/where_clause_builder.rb +20 -0
  260. data/lib/expressir/express/builders.rb +55 -0
  261. data/lib/expressir/express/cache.rb +43 -18
  262. data/lib/expressir/express/checker.rb +812 -0
  263. data/lib/expressir/express/concatenator.rb +79 -0
  264. data/lib/expressir/express/core.rb +95 -0
  265. data/lib/expressir/express/error.rb +124 -0
  266. data/lib/expressir/express/formatter.rb +89 -1548
  267. data/lib/expressir/express/formatters/data_types_formatter.rb +362 -0
  268. data/lib/expressir/express/formatters/declarations_formatter.rb +753 -0
  269. data/lib/expressir/express/formatters/expressions_formatter.rb +180 -0
  270. data/lib/expressir/express/formatters/literals_formatter.rb +55 -0
  271. data/lib/expressir/express/formatters/references_formatter.rb +53 -0
  272. data/lib/expressir/express/formatters/remark_formatter.rb +270 -0
  273. data/lib/expressir/express/formatters/remark_item_formatter.rb +28 -0
  274. data/lib/expressir/express/formatters/statements_formatter.rb +272 -0
  275. data/lib/expressir/express/formatters/supertype_expressions_formatter.rb +54 -0
  276. data/lib/expressir/express/formatters.rb +22 -0
  277. data/lib/expressir/express/grammar/parser.rb +712 -0
  278. data/lib/expressir/express/grammar.rb +11 -0
  279. data/lib/expressir/express/hyperlink_formatter.rb +4 -5
  280. data/lib/expressir/express/interface_dot.rb +105 -0
  281. data/lib/expressir/express/lazy_repository.rb +211 -0
  282. data/lib/expressir/express/line_map.rb +48 -0
  283. data/lib/expressir/express/listing.rb +174 -0
  284. data/lib/expressir/express/model_traversal.rb +42 -0
  285. data/lib/expressir/express/model_visitor.rb +11 -10
  286. data/lib/expressir/express/node_position_index.rb +342 -0
  287. data/lib/expressir/express/parallel_files.rb +229 -0
  288. data/lib/expressir/express/parser.rb +501 -62
  289. data/lib/expressir/express/pretty_formatter.rb +586 -0
  290. data/lib/expressir/express/pretty_gate.rb +61 -0
  291. data/lib/expressir/express/refs_overlay.rb +47 -0
  292. data/lib/expressir/express/remark_attacher.rb +1098 -0
  293. data/lib/expressir/express/remark_overlay.rb +86 -0
  294. data/lib/expressir/express/remark_scanner.rb +245 -0
  295. data/lib/expressir/express/resolve_references_model_visitor.rb +7 -9
  296. data/lib/expressir/express/schema_block_scanner.rb +137 -0
  297. data/lib/expressir/express/schema_head_formatter.rb +4 -5
  298. data/lib/expressir/express/schema_plain_source_formatter.rb +11 -0
  299. data/lib/expressir/express/schema_source_formatter.rb +15 -0
  300. data/lib/expressir/express/scope_resolver.rb +223 -0
  301. data/lib/expressir/express/self_schema_reference.rb +126 -0
  302. data/lib/expressir/express/shtolo.rb +679 -0
  303. data/lib/expressir/express/smrl_xml.rb +125 -0
  304. data/lib/expressir/express/source_formatter.rb +15 -0
  305. data/lib/expressir/express/streaming_builder.rb +435 -0
  306. data/lib/expressir/express/xsd.rb +259 -0
  307. data/lib/expressir/express.rb +53 -0
  308. data/lib/expressir/liquid.rb +1 -0
  309. data/lib/expressir/manifest/resolver.rb +210 -0
  310. data/lib/expressir/manifest/validator.rb +192 -0
  311. data/lib/expressir/manifest.rb +6 -0
  312. data/lib/expressir/mapping/refpath.rb +857 -0
  313. data/lib/expressir/mapping.rb +253 -0
  314. data/lib/expressir/model/cache.rb +10 -9
  315. data/lib/expressir/model/concerns.rb +42 -0
  316. data/lib/expressir/model/data_types/aggregate.rb +8 -13
  317. data/lib/expressir/model/data_types/array.rb +15 -21
  318. data/lib/expressir/model/data_types/bag.rb +11 -15
  319. data/lib/expressir/model/data_types/binary.rb +9 -12
  320. data/lib/expressir/model/data_types/boolean.rb +7 -2
  321. data/lib/expressir/model/data_types/enumeration.rb +11 -15
  322. data/lib/expressir/model/data_types/enumeration_item.rb +5 -7
  323. data/lib/expressir/model/data_types/generic.rb +6 -8
  324. data/lib/expressir/model/data_types/generic_entity.rb +11 -13
  325. data/lib/expressir/model/data_types/integer.rb +7 -2
  326. data/lib/expressir/model/data_types/list.rb +13 -18
  327. data/lib/expressir/model/data_types/logical.rb +7 -2
  328. data/lib/expressir/model/data_types/number.rb +7 -2
  329. data/lib/expressir/model/data_types/real.rb +7 -9
  330. data/lib/expressir/model/data_types/select.rb +13 -18
  331. data/lib/expressir/model/data_types/set.rb +11 -15
  332. data/lib/expressir/model/data_types/string.rb +9 -12
  333. data/lib/expressir/model/data_types.rb +25 -0
  334. data/lib/expressir/model/declarations/attribute.rb +19 -28
  335. data/lib/expressir/model/declarations/constant.rb +10 -16
  336. data/lib/expressir/model/declarations/derived_attribute.rb +25 -0
  337. data/lib/expressir/model/declarations/entity.rb +39 -30
  338. data/lib/expressir/model/declarations/function.rb +32 -40
  339. data/lib/expressir/model/declarations/informal_proposition_rule.rb +25 -0
  340. data/lib/expressir/model/declarations/interface.rb +25 -16
  341. data/lib/expressir/model/declarations/interface_item.rb +9 -11
  342. data/lib/expressir/model/declarations/interfaced_item.rb +15 -19
  343. data/lib/expressir/model/declarations/inverse_attribute.rb +25 -0
  344. data/lib/expressir/model/declarations/parameter.rb +10 -16
  345. data/lib/expressir/model/declarations/procedure.rb +30 -37
  346. data/lib/expressir/model/declarations/remark_item.rb +11 -11
  347. data/lib/expressir/model/declarations/rule.rb +38 -43
  348. data/lib/expressir/model/declarations/schema.rb +162 -64
  349. data/lib/expressir/model/declarations/schema_version.rb +8 -11
  350. data/lib/expressir/model/declarations/schema_version_item.rb +8 -11
  351. data/lib/expressir/model/declarations/subtype_constraint.rb +14 -22
  352. data/lib/expressir/model/declarations/type.rb +18 -18
  353. data/lib/expressir/model/declarations/unique_rule.rb +8 -13
  354. data/lib/expressir/model/declarations/variable.rb +10 -16
  355. data/lib/expressir/model/declarations/where_rule.rb +8 -13
  356. data/lib/expressir/model/declarations.rb +31 -0
  357. data/lib/expressir/model/dependency_resolver.rb +268 -0
  358. data/lib/expressir/model/exp_file.rb +42 -0
  359. data/lib/expressir/model/expressions/aggregate_initializer.rb +8 -9
  360. data/lib/expressir/model/expressions/aggregate_initializer_item.rb +10 -12
  361. data/lib/expressir/model/expressions/binary_expression.rb +42 -36
  362. data/lib/expressir/model/expressions/entity_constructor.rb +10 -12
  363. data/lib/expressir/model/expressions/function_call.rb +10 -12
  364. data/lib/expressir/model/expressions/interval.rb +18 -23
  365. data/lib/expressir/model/expressions/query_expression.rb +11 -15
  366. data/lib/expressir/model/expressions/unary_expression.rb +13 -15
  367. data/lib/expressir/model/expressions.rb +18 -0
  368. data/lib/expressir/model/identifier.rb +18 -26
  369. data/lib/expressir/model/indexes/entity_index.rb +103 -0
  370. data/lib/expressir/model/indexes/item_graph.rb +185 -0
  371. data/lib/expressir/model/indexes/reference_index.rb +148 -0
  372. data/lib/expressir/model/indexes/type_index.rb +149 -0
  373. data/lib/expressir/model/indexes.rb +12 -0
  374. data/lib/expressir/model/interface_validator.rb +384 -0
  375. data/lib/expressir/model/literals/binary.rb +7 -9
  376. data/lib/expressir/model/literals/integer.rb +7 -9
  377. data/lib/expressir/model/literals/logical.rb +10 -12
  378. data/lib/expressir/model/literals/real.rb +7 -9
  379. data/lib/expressir/model/literals/string.rb +10 -12
  380. data/lib/expressir/model/literals.rb +13 -0
  381. data/lib/expressir/model/model_element.rb +286 -117
  382. data/lib/expressir/model/references/attribute_reference.rb +9 -12
  383. data/lib/expressir/model/references/group_reference.rb +9 -12
  384. data/lib/expressir/model/references/index_reference.rb +11 -15
  385. data/lib/expressir/model/references/simple_reference.rb +11 -14
  386. data/lib/expressir/model/references.rb +12 -0
  387. data/lib/expressir/model/remark_format.rb +17 -0
  388. data/lib/expressir/model/remark_info.rb +92 -0
  389. data/lib/expressir/model/remark_placement.rb +42 -0
  390. data/lib/expressir/model/repository.rb +472 -12
  391. data/lib/expressir/model/repository_validator.rb +293 -0
  392. data/lib/expressir/model/search_engine.rb +551 -0
  393. data/lib/expressir/model/statements/alias.rb +12 -16
  394. data/lib/expressir/model/statements/assignment.rb +12 -11
  395. data/lib/expressir/model/statements/case.rb +31 -14
  396. data/lib/expressir/model/statements/case_action.rb +9 -11
  397. data/lib/expressir/model/statements/compound.rb +10 -8
  398. data/lib/expressir/model/statements/escape.rb +10 -2
  399. data/lib/expressir/model/statements/if.rb +15 -14
  400. data/lib/expressir/model/statements/null.rb +10 -2
  401. data/lib/expressir/model/statements/procedure_call.rb +11 -11
  402. data/lib/expressir/model/statements/repeat.rb +21 -28
  403. data/lib/expressir/model/statements/return.rb +9 -8
  404. data/lib/expressir/model/statements/skip.rb +10 -2
  405. data/lib/expressir/model/statements.rb +20 -0
  406. data/lib/expressir/model/supertype_expressions/binary_supertype_expression.rb +13 -17
  407. data/lib/expressir/model/supertype_expressions/oneof_supertype_expression.rb +7 -9
  408. data/lib/expressir/model/supertype_expressions.rb +12 -0
  409. data/lib/expressir/model.rb +38 -78
  410. data/lib/expressir/package/builder.rb +231 -0
  411. data/lib/expressir/package/metadata.rb +79 -0
  412. data/lib/expressir/package/reader.rb +149 -0
  413. data/lib/expressir/package.rb +8 -0
  414. data/lib/expressir/schema_manifest.rb +159 -0
  415. data/lib/expressir/schema_manifest_entry.rb +15 -0
  416. data/lib/expressir/version.rb +6 -1
  417. data/lib/expressir.rb +79 -9
  418. data/lib/tasks/verify_remarks.rake +16 -0
  419. data/rakelib/native.rake +24 -0
  420. data/rakelib/oracle.rake +52 -0
  421. data/rakelib/verify_remarks.rake +16 -0
  422. metadata +431 -129
  423. data/.cross_rubies +0 -20
  424. data/.gitmodules +0 -6
  425. data/.yardopts +0 -11
  426. data/docs/development.md +0 -90
  427. data/exe/format +0 -18
  428. data/exe/generate-parser +0 -51
  429. data/lib/expressir/cli/ui.rb +0 -36
  430. data/lib/expressir/express/2.7/express_parser.so +0 -0
  431. data/lib/expressir/express/3.0/express_parser.so +0 -0
  432. data/lib/expressir/express/3.1/express_parser.so +0 -0
  433. data/lib/expressir/express/3.2/express_parser.so +0 -0
  434. data/lib/expressir/express/visitor.rb +0 -2588
  435. data/lib/expressir/model/data_type.rb +0 -9
  436. data/lib/expressir/model/declaration.rb +0 -9
  437. data/lib/expressir/model/expression.rb +0 -9
  438. data/lib/expressir/model/literal.rb +0 -9
  439. data/lib/expressir/model/reference.rb +0 -9
  440. data/lib/expressir/model/statement.rb +0 -9
  441. data/lib/expressir/model/supertype_expression.rb +0 -9
  442. data/rakelib/antlr4-native.rake +0 -161
  443. data/rakelib/cross-ruby.rake +0 -368
  444. data/spec/acceptance/version_spec.rb +0 -27
  445. data/spec/expressir/express/cache_spec.rb +0 -89
  446. data/spec/expressir/express/formatter_spec.rb +0 -173
  447. data/spec/expressir/express/parser_spec.rb +0 -141
  448. data/spec/expressir/model/model_element_spec.rb +0 -318
  449. data/spec/spec_helper.rb +0 -24
  450. data/spec/support/console_helper.rb +0 -29
  451. data/spec/syntax/multiple.exp +0 -23
  452. data/spec/syntax/multiple.yaml +0 -198
  453. data/spec/syntax/multiple_formatted.exp +0 -71
  454. data/spec/syntax/multiple_hyperlink_formatted.exp +0 -71
  455. data/spec/syntax/multiple_schema_head_hyperlink_formatted.exp +0 -13
  456. data/spec/syntax/remark.exp +0 -193
  457. data/spec/syntax/remark.yaml +0 -471
  458. data/spec/syntax/remark_formatted.exp +0 -228
  459. data/spec/syntax/single.exp +0 -4
  460. data/spec/syntax/single.yaml +0 -18
  461. data/spec/syntax/single_formatted.exp +0 -10
  462. data/spec/syntax/single_formatted.yaml +0 -36
  463. data/spec/syntax/syntax.exp +0 -333
  464. data/spec/syntax/syntax.yaml +0 -3509
  465. data/spec/syntax/syntax_formatted.exp +0 -902
  466. data/spec/syntax/syntax_hyperlink_formatted.exp +0 -902
  467. data/spec/syntax/syntax_schema_head_formatted.exp +0 -18
@@ -0,0 +1,342 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressir
4
+ module Express
5
+ # Builds and queries an index of model nodes by source position.
6
+ #
7
+ # One interface (`nearest_node_to`, `node_for_end_scope_at`, `each_node`),
8
+ # one implementation. Extracted from RemarkAttacher so the tree-walk +
9
+ # byte→line translation + nearest-node heuristics live in one place,
10
+ # tested independently of any remark logic.
11
+ #
12
+ # Reference: ISO 10303-11 — the parser yields source spans on ModelElement
13
+ # instances; this module turns those spans into a line-keyed index that
14
+ # remark attachment can query.
15
+ class NodePositionIndex
16
+ EMPTY = [].freeze
17
+ private_constant :EMPTY
18
+
19
+ # Single source of truth for "what collections does this node have"
20
+ # lives on the model — each class declares its own via the
21
+ # `collection_attributes` macro. Referenced by both RemarkAttacher
22
+ # and NodePositionIndex via this alias for backward compatibility.
23
+ COLLECTION_REGISTRY = Model::ModelElement.collection_registry
24
+
25
+ attr_reader :nodes
26
+
27
+ # Semantic entries starting on `line` (node order).
28
+ def starting_at(line)
29
+ semantic_by_line[line] || EMPTY
30
+ end
31
+
32
+ # Semantic entries ending on `line` (node order).
33
+ def ending_at(line)
34
+ semantic_by_end_line[line] || EMPTY
35
+ end
36
+
37
+ # Semantic entries whose span covers `line` (node order within the
38
+ # covering band).
39
+ def spanning(line)
40
+ band = line / BAND
41
+ bands = span_bands
42
+ return EMPTY unless bands.key?(band)
43
+
44
+ bands[band].select { |n| line.between?(n[:line], n[:end_line]) }
45
+ end
46
+
47
+ # Semantic entries with end_line < line, ascending by end_line.
48
+ def semantic_ending_before(line)
49
+ sorted = semantic_by_end_line_list
50
+ idx = sorted.bsearch_index { |n| n[:end_line] >= line } || sorted.size
51
+ sorted[0...idx]
52
+ end
53
+
54
+ # The first semantic entry (smallest line), for preamble remarks.
55
+ def first_semantic
56
+ first_by_line = semantic_by_line.keys.min
57
+ first_by_line && semantic_by_line[first_by_line]&.first
58
+ end
59
+
60
+ # Entries owned by `owner` within any of `collections`, in node order.
61
+ # Identity-keyed: ownership is object identity (the tree-walker's
62
+ # `.equal?` semantics), and value-equal model objects must not
63
+ # collide.
64
+ def children_in(owner, collections)
65
+ per_collection = children_index[owner]
66
+ return EMPTY unless per_collection
67
+
68
+ entries = per_collection.values_at(*collections).compact.flatten(1)
69
+ entries.sort_by { |n| node_order(n) }
70
+ end
71
+
72
+ # First node (in node order) that is_a?(type), memoized per type.
73
+ def first_node_of_type(type)
74
+ @first_of_type ||= {}
75
+ @first_of_type[type] ||= @nodes.find { |n| n[:node].is_a?(type) }&.dig(:node)
76
+ end
77
+
78
+ def semantic_entries
79
+ @semantic_entries ||=
80
+ @nodes.select { |n| n[:line] && semantic?(n[:node]) }
81
+ end
82
+
83
+ # Non-vivifying: missed lookups must not materialize keys, or
84
+ # `keys.min`-style queries see phantom lines.
85
+ def semantic_by_line
86
+ @semantic_by_line ||= begin
87
+ h = {}
88
+ semantic_entries.each { |n| (h[n[:line]] ||= []) << n }
89
+ h
90
+ end
91
+ end
92
+
93
+ def semantic_by_end_line
94
+ @semantic_by_end_line ||= begin
95
+ h = {}
96
+ semantic_entries.each { |n| (h[n[:end_line]] ||= []) << n if n[:end_line] }
97
+ h
98
+ end
99
+ end
100
+
101
+ def semantic_by_end_line_list
102
+ @semantic_by_end_line_list ||=
103
+ semantic_entries.select { |n| n[:end_line] }.sort_by { |n| n[:end_line] }
104
+ end
105
+
106
+ def children_index
107
+ @children_index ||= begin
108
+ index = {}.compare_by_identity
109
+ @nodes.each do |n|
110
+ next unless n[:owner] && n[:line]
111
+
112
+ per_owner = (index[n[:owner]] ||= {})
113
+ (per_owner[n[:collection]] ||= []) << n
114
+ end
115
+ index
116
+ end
117
+ end
118
+
119
+ def span_bands
120
+ @span_bands ||= begin
121
+ bands = Hash.new { |hash, key| hash[key] = [] }
122
+ semantic_entries.each do |n|
123
+ next unless n[:end_line]
124
+
125
+ ((n[:line] / BAND)..(n[:end_line] / BAND)).each do |band|
126
+ bands[band] << n
127
+ end
128
+ end
129
+ bands
130
+ end
131
+ end
132
+
133
+ BAND = 1024
134
+ private_constant :BAND
135
+
136
+ def initialize(model, line_map)
137
+ @model = model
138
+ @line_map = line_map
139
+ @nodes = build_sorted_nodes
140
+ @node_order = nil
141
+ end
142
+
143
+ # Returns the most-specific node whose span contains `remark_line`,
144
+ # preferring same-line starts/ends, then smallest containing span.
145
+ # Skips nodes that cannot own such a remark; see {#remark_scope?}.
146
+ def nearest_node_to(remark_line)
147
+ same_start = starting_at(remark_line).select { |n| remark_scope?(n[:node]) }
148
+ return same_start.last[:node] if same_start.any?
149
+
150
+ same_end = ending_at(remark_line).select { |n| remark_scope?(n[:node]) }
151
+ return same_end.last[:node] if same_end.any?
152
+
153
+ containing = spanning(remark_line).select { |n| remark_scope?(n[:node]) }
154
+
155
+ if containing.any?
156
+ exp_file_node = containing.find { |n| n[:node].is_a?(Model::ExpFile) }
157
+ if exp_file_node
158
+ first_schema_offset = exp_file_node[:node].schemas&.first&.source_offset
159
+ if first_schema_offset && remark_line < @line_map.line_number(first_schema_offset)
160
+ return exp_file_node[:node]
161
+ end
162
+ end
163
+ # Prefer non-ExpFile nodes (Schema, Entity, etc.) over ExpFile
164
+ # when both span the same range, since ExpFile is a file-level
165
+ # container and remarks inside a schema belong to the schema.
166
+ candidates = containing.reject { |n| n[:node].is_a?(Model::ExpFile) }
167
+ candidates = containing if candidates.empty?
168
+ candidates.min_by { |n| n[:end_line] - n[:line] }[:node]
169
+ else
170
+ before = semantic_ending_before(remark_line)
171
+ if before.any?
172
+ before.max_by { |n| n[:end_line] }[:node]
173
+ else
174
+ # Remark is before all nodes (e.g., preamble comment before SCHEMA).
175
+ # Attach to the first semantic node.
176
+ first_semantic&.dig(:node)
177
+ end
178
+ end
179
+ end
180
+
181
+ # Returns the node whose END_XXX declaration is on `remark_line`, when
182
+ # the line text matches one of the END_XXX keywords. Used to attach
183
+ # remarks that appear immediately after a scope closes.
184
+ def node_for_end_scope_at(remark_line, line_content)
185
+ node_type = case line_content
186
+ when /END_SCHEMA/i then Model::Declarations::Schema
187
+ when /END_ENTITY/i then Model::Declarations::Entity
188
+ when /END_TYPE/i then Model::Declarations::Type
189
+ when /END_FUNCTION/i then Model::Declarations::Function
190
+ when /END_PROCEDURE/i then Model::Declarations::Procedure
191
+ when /END_RULE/i then Model::Declarations::Rule
192
+ end
193
+ return nil unless node_type
194
+
195
+ candidates = (0..2).flat_map do |back|
196
+ ending_at(remark_line - back)
197
+ end.select do |n|
198
+ n[:node].is_a?(node_type) &&
199
+ n[:end_line] <= remark_line && n[:end_line] >= remark_line - 2
200
+ end
201
+
202
+ candidates.min_by { |n| node_order(n) }&.dig(:node) ||
203
+ first_node_of_type(node_type)
204
+ end
205
+
206
+ private
207
+
208
+ def node_order(entry)
209
+ @node_order ||= begin
210
+ map = {}.compare_by_identity
211
+ @nodes.each_with_index { |n, i| map[n] = i }
212
+ map
213
+ end
214
+ @node_order[entry]
215
+ end
216
+
217
+ def semantic?(node)
218
+ !node.is_a?(Model::Repository) && !node.is_a?(Model::Cache)
219
+ end
220
+
221
+ # Whether a node can own a remark sitting on a line of its own.
222
+ # Distinct from Model::TakesInlineRemark, which asks whether a node can
223
+ # own a remark written AFTER it on the same line: a statement answers
224
+ # yes to both; an interface clause only to that one.
225
+ #
226
+ # An interface clause is indexed (so a remark trailing it can find it
227
+ # via the same-line lookup), but it encloses nothing: an own-line
228
+ # remark below `REFERENCE FROM x;` introduces whatever comes next, and
229
+ # giving it to the clause would both misplace it and lose it, since
230
+ # nothing renders remarks there.
231
+ def remark_scope?(node)
232
+ semantic?(node) && !node.is_a?(Model::Declarations::Interface)
233
+ end
234
+
235
+ def build_sorted_nodes
236
+ result = []
237
+ collect_nodes(@model, result, Set.new)
238
+ result.sort_by!.with_index { |n, i| [n[:position] || Float::INFINITY, i] }
239
+ result
240
+ end
241
+
242
+ def collect_nodes(node, result, visited, owner: nil, collection: nil)
243
+ return unless node
244
+ return if visited.include?(node.object_id)
245
+
246
+ visited.add(node.object_id)
247
+
248
+ if node.is_a?(Model::ModelElement) && node.source && node.source_offset
249
+ record_node_position(node, result, owner, collection)
250
+ else
251
+ result << { node: node, position: nil, line: nil, end_line: nil,
252
+ owner: owner, collection: collection }
253
+ end
254
+
255
+ collect_children(node, result, visited)
256
+ end
257
+
258
+ def record_node_position(node, result, owner, collection)
259
+ pos = node.source_offset
260
+ valid = position_valid?(pos, node)
261
+ unless valid
262
+ result << { node: node, position: nil, line: nil, end_line: nil,
263
+ owner: owner, collection: collection }
264
+ return
265
+ end
266
+
267
+ line = @line_map.line_number(pos)
268
+ source_end_line = @line_map.line_number(pos + node.source.length)
269
+ children_end_line = children_end_line_for(node)
270
+ end_line = [source_end_line, children_end_line].compact.max || source_end_line
271
+
272
+ result << { node: node, position: pos, line: line, end_line: end_line,
273
+ owner: owner, collection: collection }
274
+ end
275
+
276
+ # The parser returns source_offset=0 for leaf nodes (WhereRule) where
277
+ # it cannot determine the actual position. Accept position=0 only when
278
+ # the source is a declaration keyword line — those legitimately start
279
+ # at the beginning of the file.
280
+ def position_valid?(pos, node)
281
+ return true if pos.positive?
282
+ return false unless pos.zero? && node.source
283
+
284
+ node.source.to_s.start_with?(
285
+ "SCHEMA", "ENTITY", "TYPE", "FUNCTION",
286
+ "PROCEDURE", "RULE", "CONSTANT", "VARIABLE",
287
+ "USE", "REFERENCE", "END_SCHEMA", "END_ENTITY",
288
+ "END_TYPE", "END_FUNCTION", "END_PROCEDURE",
289
+ "END_RULE", "END_CONSTANT", "END_VARIABLE"
290
+ )
291
+ end
292
+
293
+ def children_end_line_for(node)
294
+ end_lines = []
295
+
296
+ if node.is_a?(Model::Declarations::Schema)
297
+ Array(node.children).each do |child|
298
+ next unless child.is_a?(Model::ModelElement) && child.source_offset && child.source
299
+
300
+ end_lines << @line_map.line_number(child.source_offset + child.source.length)
301
+ end
302
+ end
303
+
304
+ each_collection_on(node) do |item|
305
+ next unless item.is_a?(Model::ModelElement) && item.source_offset && item.source
306
+
307
+ end_lines << @line_map.line_number(item.source_offset + item.source.length)
308
+ end
309
+
310
+ end_lines.max
311
+ end
312
+
313
+ def collect_children(node, result, visited)
314
+ if node.is_a?(Model::Declarations::Schema)
315
+ Array(node.children).each do |c|
316
+ collect_nodes(c, result, visited, owner: node, collection: :children)
317
+ end
318
+ end
319
+
320
+ each_collection_on(node) do |item, attr|
321
+ collect_nodes(item, result, visited, owner: node, collection: attr)
322
+ end
323
+ end
324
+
325
+ # Yields each child in any declared collection on the node (with the
326
+ # collection attribute name), based on the type-driven
327
+ # COLLECTION_REGISTRY. Returns nothing for nodes whose class is not
328
+ # registered.
329
+ def each_collection_on(node)
330
+ attrs = COLLECTION_REGISTRY[node.class]
331
+ return unless attrs
332
+
333
+ attrs.each do |attr|
334
+ collection = node.public_send(attr)
335
+ next unless collection.is_a?(Array)
336
+
337
+ collection.each { |item| yield(item, attr) }
338
+ end
339
+ end
340
+ end
341
+ end
342
+ end
@@ -0,0 +1,229 @@
1
+ require "etc"
2
+
3
+ module Expressir
4
+ module Express
5
+ # Fork-based worker pool for parsing many EXPRESS files in parallel.
6
+ # The native parser holds the GVL for the whole parse, so process-level
7
+ # parallelism is the only way to use multiple cores. Files are
8
+ # independent until reference resolution, which stays in the parent.
9
+ #
10
+ # Unlike sequential parsing, the progress block fires in file order
11
+ # only after all files have been parsed.
12
+ class ParallelFiles
13
+ DEFAULT_MAX_PROCESSES = 4
14
+ FRAME_HEADER_BYTES = 4
15
+
16
+ FORK_SUPPORTED = Process.respond_to?(:fork).freeze
17
+
18
+ # Forking is never the default: a library must not spawn processes on
19
+ # behalf of its host (forked children inherit broken thread and lock
20
+ # state, and fork does not exist on all Rubies). Parallelism requires
21
+ # an explicit max_processes > 1 from the caller and a platform that
22
+ # supports fork (e.g. not Windows); otherwise the request degrades
23
+ # to sequential parsing.
24
+ def self.sequential?(files, max_processes)
25
+ !FORK_SUPPORTED || max_processes.nil? || max_processes <= 1 ||
26
+ files.size < 3
27
+ end
28
+
29
+ # @param files [Array<String>] EXPRESS file paths
30
+ # @param max_processes [Integer, nil] worker cap; nil auto-selects
31
+ # @param parse [Proc] callback taking a file path, returning an ExpFile
32
+ # @param strict [Boolean] re-raise every error, including
33
+ # Error::SchemaParseFailure, instead of skipping the file
34
+ # @yield [file, exp_file, error] called in original file order
35
+ # @return [Array<Expressir::Model::ExpFile, nil>] parsed files in order;
36
+ # nil marks a file that failed with Error::SchemaParseFailure
37
+ def self.run(files, parse:, max_processes: nil, strict: false, &block)
38
+ new(files, max_processes, parse, block, strict).run
39
+ end
40
+
41
+ def initialize(files, max_processes, parse, block, strict)
42
+ @files = files
43
+ @parse = parse
44
+ @block = block
45
+ @strict = strict
46
+ @worker_count = [
47
+ files.size - 1,
48
+ max_processes || [Etc.nprocessors, DEFAULT_MAX_PROCESSES].min,
49
+ ].min
50
+ end
51
+
52
+ def run
53
+ job_pipes = Array.new(@worker_count) { IO.pipe }
54
+ result_pipes = Array.new(@worker_count) { IO.pipe }
55
+ pids = spawn_workers(job_pipes, result_pipes)
56
+
57
+ files_results = schedule_jobs(job_pipes, result_pipes)
58
+
59
+ ordered_pass(files_results)
60
+ ensure
61
+ cleanup(job_pipes, result_pipes, pids)
62
+ end
63
+
64
+ private
65
+
66
+ def spawn_workers(job_pipes, result_pipes)
67
+ job_pipes.each_index.map do |i|
68
+ job_r, job_w = job_pipes[i]
69
+ result_r, result_w = result_pipes[i]
70
+ fork do
71
+ job_w.close
72
+ result_r.close
73
+ other_pipes = (job_pipes + result_pipes).flatten -
74
+ [job_r, result_w]
75
+ other_pipes.each { |io| io.close unless io.closed? }
76
+ worker_loop(job_r, result_w)
77
+ end
78
+ end
79
+ end
80
+
81
+ # Assigns one job at a time to whichever worker reports a result, so
82
+ # each file is parsed exactly once and each worker holds at most one
83
+ # in-flight job (keeping result pipes free of interleaved frames).
84
+ def schedule_jobs(job_pipes, result_pipes)
85
+ writers = job_pipes.map(&:last)
86
+ readers = result_pipes.map(&:first)
87
+ files_results = Array.new(@files.size)
88
+ next_job = 0
89
+ busy = {}
90
+
91
+ writers.each_index do |wi|
92
+ break if next_job == @files.size
93
+
94
+ dispatch(writers[wi], next_job)
95
+ busy[wi] = true
96
+ next_job += 1
97
+ end
98
+
99
+ until busy.empty?
100
+ ready, = IO.select(readers.values_at(*busy.keys))
101
+ ready.each do |io|
102
+ wi = readers.index(io)
103
+ payload = Marshal.load(read_frame(io)) # rubocop:disable Security/MarshalLoad
104
+ files_results[payload[:index]] = payload
105
+
106
+ if next_job < @files.size
107
+ dispatch(writers[wi], next_job)
108
+ next_job += 1
109
+ else
110
+ writers[wi].close unless writers[wi].closed?
111
+ busy.delete(wi)
112
+ end
113
+ end
114
+ end
115
+
116
+ files_results
117
+ end
118
+
119
+ def dispatch(writer, job_index)
120
+ write_frame(writer, pack_frame(Marshal.dump([job_index, @files[job_index]])))
121
+ end
122
+
123
+ def worker_loop(job_r, result_w)
124
+ until job_r.eof?
125
+ data = read_frame(job_r)
126
+ index, file = Marshal.load(data) # rubocop:disable Security/MarshalLoad
127
+ begin
128
+ payload = { index: index, exp_file: @parse.call(file) }
129
+ rescue StandardError => e
130
+ payload = { index: index, error: transferable_error(e) }
131
+ end
132
+ write_frame(result_w, pack_frame(Marshal.dump(payload)))
133
+ end
134
+ rescue Errno::EPIPE
135
+ # parent went away; nothing to report to
136
+ exit!(0)
137
+ end
138
+
139
+ # SIGTERM cannot interrupt a worker blocked in the native parser (the
140
+ # GVL is held), so escalate to SIGKILL after a grace period instead of
141
+ # blocking in waitpid forever.
142
+ def cleanup(job_pipes, result_pipes, pids)
143
+ (job_pipes.to_a + result_pipes.to_a).each do |r, w|
144
+ r.close unless r.closed?
145
+ w.close unless w.closed?
146
+ end
147
+ pids.to_a.each { |pid| Process.kill("TERM", pid) if alive?(pid) }
148
+
149
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 2
150
+ pids.to_a.each do |pid|
151
+ while alive?(pid) && Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
152
+ Process.waitpid(pid, Process::WNOHANG)
153
+ sleep 0.05
154
+ end
155
+ Process.kill("KILL", pid) if alive?(pid)
156
+ reap(pid)
157
+ end
158
+ end
159
+
160
+ def ordered_pass(files_results)
161
+ files_results.each_with_index do |payload, index|
162
+ error = payload[:error]
163
+ @block&.call(@files[index], payload[:exp_file], error)
164
+ raise error if error && (@strict ||
165
+ !error.is_a?(Error::SchemaParseFailure))
166
+ end
167
+
168
+ files_results.map do |payload|
169
+ payload[:error] ? nil : payload[:exp_file]
170
+ end
171
+ end
172
+
173
+ # Errors carrying native-parser state (e.g. Parsanol::ParseFailed with
174
+ # its cause tree) cannot cross a fork boundary; rebuild them without
175
+ # the untransferable internals, preserving class and message.
176
+ def transferable_error(error)
177
+ Marshal.dump(error)
178
+ error
179
+ rescue StandardError
180
+ if error.is_a?(Error::SchemaParseFailure)
181
+ Error::SchemaParseFailure.new(error.filename,
182
+ StandardError.new(error.message))
183
+ else
184
+ StandardError.new(error.message)
185
+ end
186
+ end
187
+
188
+ def pack_frame(data)
189
+ [data.bytesize].pack("N") + data
190
+ end
191
+
192
+ def write_frame(io, frame)
193
+ io.write(frame)
194
+ end
195
+
196
+ def read_frame(io)
197
+ header = read_exactly(io, FRAME_HEADER_BYTES)
198
+ read_exactly(io, header.unpack1("N"))
199
+ end
200
+
201
+ def read_exactly(io, count)
202
+ data = +""
203
+ while data.bytesize < count
204
+ chunk = io.read(count - data.bytesize)
205
+ unless chunk
206
+ raise Error::ParallelParseError,
207
+ "worker exited before sending its result"
208
+ end
209
+
210
+ data << chunk
211
+ end
212
+ data
213
+ end
214
+
215
+ def alive?(pid)
216
+ Process.kill(0, pid)
217
+ true
218
+ rescue Errno::ESRCH, Errno::EPERM
219
+ false
220
+ end
221
+
222
+ def reap(pid)
223
+ Process.waitpid(pid)
224
+ rescue Errno::ECHILD, Errno::EINVAL
225
+ nil
226
+ end
227
+ end
228
+ end
229
+ end