expressir 2.4.29-arm-linux-musl

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 (426) hide show
  1. checksums.yaml +7 -0
  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 +19 -0
  9. data/.github/workflows/release.yml +37 -0
  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 +55 -0
  15. data/.hound.yml +3 -0
  16. data/.rspec +2 -0
  17. data/.rubocop.yml +18 -0
  18. data/.rubocop_todo.yml +297 -0
  19. data/CHANGELOG.md +159 -0
  20. data/Gemfile +18 -0
  21. data/README.adoc +2783 -0
  22. data/Rakefile +41 -0
  23. data/TODO.bugs/01-stale-transformer-autoload.md +39 -0
  24. data/TODO.bugs/02-parser-class-instance-vars.md +36 -0
  25. data/TODO.bugs/03-builder-mutable-state.md +43 -0
  26. data/TODO.bugs/04-formatter-public-send-dispatch.md +53 -0
  27. data/TODO.bugs/05-anonymous-formatter-subclass.md +45 -0
  28. data/TODO.bugs/06-collection-registry-single-source.md +53 -0
  29. data/TODO.bugs/07-require-relative-cleanup.md +42 -0
  30. data/TODO.bugs/08-require-expressir-in-commands.md +34 -0
  31. data/TODO.bugs/09-parser-split.md +53 -0
  32. data/TODO.bugs/10-to-s-override.md +42 -0
  33. data/TODO.bugs/11-parser-class-variables.md +39 -0
  34. data/TODO.bugs/12-marker-modules-vs-registry.md +64 -0
  35. data/TODO.bugs/13-string-literal-scanner-limitation.md +52 -0
  36. data/TODO.bugs/14-model-formatting-leak.md +30 -0
  37. data/TODO.bugs/15-expression-children-macro.md +27 -0
  38. data/TODO.bugs/16-pretty-formatter-duplication.md +28 -0
  39. data/TODO.bugs/17-snake-case-cache-mutable-constant.md +28 -0
  40. data/TODO.bugs/18-const-get-private-constants.md +30 -0
  41. data/TODO.bugs/19-format-methods-public.md +22 -0
  42. data/TODO.bugs/20-coverage-nested-entities-dedup.md +20 -0
  43. data/TODO.bugs/21-operator-tokens-secondary-dispatch.md +21 -0
  44. data/TODO.bugs/22-builder-fast-path-wrappers.md +32 -0
  45. data/TODO.bugs/23-coverage-inverse-maps.md +21 -0
  46. data/TODO.bugs/24-streaming-builder-complexity.md +19 -0
  47. data/TODO.bugs/25-debug-puts-in-production.md +21 -0
  48. data/TODO.bugs/26-generic-entity-children-misplaced.md +21 -0
  49. data/TODO.bugs/27-package-build-god-method.md +19 -0
  50. data/TODO.bugs/28-package-god-class.md +30 -0
  51. data/TODO.bugs/29-validate-ascii-god-class.md +24 -0
  52. data/TODO.bugs/30-unicode-map-extraction.md +19 -0
  53. data/TODO.bugs/README.md +43 -0
  54. data/TODO.max-perf/01-restore-ci-green.md +36 -0
  55. data/TODO.max-perf/02-streaming-parse-path.md +31 -0
  56. data/TODO.max-perf/03-cli-parallel-opt-in.md +27 -0
  57. data/TODO.max-perf/04-benchmark-harness.md +28 -0
  58. data/TODO.max-perf/05-parallel-fidelity-specs.md +22 -0
  59. data/TODO.max-perf/06-builder-cpu-audit.md +41 -0
  60. data/TODO.max-perf/07-upstream-parsanol-roadmap.md +27 -0
  61. data/TODO.max-perf/08-builder-build-perf.md +45 -0
  62. data/TODO.max-perf/09-grammar-cold-start.md +25 -0
  63. data/TODO.max-perf/10-parser-facade-hygiene.md +23 -0
  64. data/TODO.max-perf/11-ci-green-closeout.md +25 -0
  65. data/TODO.max-perf/12-require-boot-profile.md +25 -0
  66. data/TODO.max-perf/13-key-conversion-specs.md +26 -0
  67. data/TODO.max-perf/14-builder-call-handler-audit.md +28 -0
  68. data/TODO.parity-ee/00-overview.md +76 -0
  69. data/TODO.parity-ee/01-annex-g-rule-extraction.md +41 -0
  70. data/TODO.parity-ee/02-eeng-algorithm-inventory.md +8 -0
  71. data/TODO.parity-ee/03-eeng-oracle-harness.md +33 -0
  72. data/TODO.parity-ee/04-shtolo-concatenate.md +44 -0
  73. data/TODO.parity-ee/05-shtolo-longform-flatten.md +53 -0
  74. data/TODO.parity-ee/06-interface-scheduling-parity.md +47 -0
  75. data/TODO.parity-ee/07-semantic-checks-port.md +35 -0
  76. data/TODO.parity-ee/08-pretty-roundtrip-gate.md +27 -0
  77. data/TODO.parity-ee/09-smrl-index-and-listing.md +22 -0
  78. data/TODO.parity-ee/10-interface-dot-graph.md +21 -0
  79. data/TODO.parity-ee/11-import-eeng-tests.md +79 -0
  80. data/TODO.parity-ee/12-full-check-catalog.md +67 -0
  81. data/TODO.parity-ee/13-population-p21.md +54 -0
  82. data/TODO.parity-ee/14-shtolo-completion.md +44 -0
  83. data/TODO.parity-ee/15-part28-xml.md +51 -0
  84. data/TODO.parity-ee/16-compare-patch.md +48 -0
  85. data/TODO.parity-ee/17-official-tests.md +42 -0
  86. data/TODO.parity-ee/18-mim-mapping.md +47 -0
  87. data/TODO.parity-ee/19-architecture.md +97 -0
  88. data/TODO.parity-ee/parity-matrix.md +108 -0
  89. data/TODO.refpath-v2/00-overview.md +64 -0
  90. data/TODO.suma-improvements/00-overview.md +23 -0
  91. data/TODO.suma-improvements/01-prebuilt-platform-gems.md +33 -0
  92. data/TODO.suma-improvements/02-suma-e2e.md +39 -0
  93. data/TODO.suma-improvements/03-item-graph.md +22 -0
  94. data/TODO.suma-improvements/04-compiled-set-default.md +16 -0
  95. data/TODO.suma-improvements/05-validation-in-docs.md +17 -0
  96. data/TODO.suma-improvements/06-memory-remeasure.md +36 -0
  97. data/TODO.suma-improvements/07-part28-xml.md +15 -0
  98. data/TODO.suma-improvements/08-windows-gnu-toolchain.md +15 -0
  99. data/TODO.suma-improvements/09-checker-tranche3.md +13 -0
  100. data/TODO.suma-improvements/10-expressdoc.md +8 -0
  101. data/benchmark/srl_benchmark.rb +458 -0
  102. data/benchmark/srl_native_benchmark.rb +146 -0
  103. data/benchmark/srl_ruby_benchmark.rb +132 -0
  104. data/bin/console +11 -0
  105. data/bin/rspec +30 -0
  106. data/bin/setup +8 -0
  107. data/docs/Gemfile +12 -0
  108. data/docs/_config.yml +141 -0
  109. data/docs/_guides/changes/changes-format.adoc +778 -0
  110. data/docs/_guides/changes/importing-eengine.adoc +898 -0
  111. data/docs/_guides/changes/index.adoc +396 -0
  112. data/docs/_guides/changes/programmatic-usage.adoc +1038 -0
  113. data/docs/_guides/changes/validating-changes.adoc +681 -0
  114. data/docs/_guides/cli/benchmark-performance.adoc +834 -0
  115. data/docs/_guides/cli/coverage-analysis.adoc +921 -0
  116. data/docs/_guides/cli/format-schemas.adoc +547 -0
  117. data/docs/_guides/cli/index.adoc +8 -0
  118. data/docs/_guides/cli/managing-changes.adoc +927 -0
  119. data/docs/_guides/cli/validate-ascii.adoc +645 -0
  120. data/docs/_guides/cli/validate-schemas.adoc +534 -0
  121. data/docs/_guides/formatter/formatter-architecture.adoc +401 -0
  122. data/docs/_guides/index.adoc +165 -0
  123. data/docs/_guides/ler/creating-packages.adoc +664 -0
  124. data/docs/_guides/ler/index.adoc +305 -0
  125. data/docs/_guides/ler/loading-packages.adoc +707 -0
  126. data/docs/_guides/ler/package-formats.adoc +748 -0
  127. data/docs/_guides/ler/querying-packages.adoc +826 -0
  128. data/docs/_guides/ler/step-packages.adoc +385 -0
  129. data/docs/_guides/ler/validating-packages.adoc +750 -0
  130. data/docs/_guides/liquid/basic-templates.adoc +813 -0
  131. data/docs/_guides/liquid/documentation-generation.adoc +1042 -0
  132. data/docs/_guides/liquid/drops-reference.adoc +829 -0
  133. data/docs/_guides/liquid/filters-and-tags.adoc +912 -0
  134. data/docs/_guides/liquid/index.adoc +468 -0
  135. data/docs/_guides/manifests/creating-manifests.adoc +483 -0
  136. data/docs/_guides/manifests/index.adoc +307 -0
  137. data/docs/_guides/manifests/resolving-manifests.adoc +557 -0
  138. data/docs/_guides/manifests/validating-manifests.adoc +713 -0
  139. data/docs/_guides/ruby-api/formatting-schemas.adoc +605 -0
  140. data/docs/_guides/ruby-api/index.adoc +257 -0
  141. data/docs/_guides/ruby-api/parsing-files.adoc +421 -0
  142. data/docs/_guides/ruby-api/search-engine.adoc +609 -0
  143. data/docs/_guides/ruby-api/working-with-repository.adoc +577 -0
  144. data/docs/_pages/data-model.adoc +665 -0
  145. data/docs/_pages/express-language.adoc +506 -0
  146. data/docs/_pages/getting-started.adoc +414 -0
  147. data/docs/_pages/index.adoc +116 -0
  148. data/docs/_pages/introduction.adoc +256 -0
  149. data/docs/_pages/ler-packages.adoc +837 -0
  150. data/docs/_pages/parsers.adoc +709 -0
  151. data/docs/_pages/schema-manifests.adoc +431 -0
  152. data/docs/_references/index.adoc +228 -0
  153. data/docs/_tutorials/creating-ler-package.adoc +735 -0
  154. data/docs/_tutorials/documentation-coverage.adoc +795 -0
  155. data/docs/_tutorials/formatting-schemas.adoc +89 -0
  156. data/docs/_tutorials/index.adoc +231 -0
  157. data/docs/_tutorials/liquid-templates.adoc +806 -0
  158. data/docs/_tutorials/parsing-your-first-schema.adoc +522 -0
  159. data/docs/_tutorials/querying-schemas.adoc +751 -0
  160. data/docs/_tutorials/working-with-multiple-schemas.adoc +676 -0
  161. data/docs/index.adoc +242 -0
  162. data/docs/lychee.toml +90 -0
  163. data/examples/demo_ler_usage.sh +86 -0
  164. data/examples/ler/README.md +111 -0
  165. data/examples/ler/simple_example.ler +0 -0
  166. data/examples/ler/simple_schema.exp +33 -0
  167. data/examples/ler_build.rb +75 -0
  168. data/examples/ler_cli.rb +79 -0
  169. data/examples/ler_demo_complete.rb +276 -0
  170. data/examples/ler_query.rb +91 -0
  171. data/examples/ler_query_examples.rb +305 -0
  172. data/examples/ler_stats.rb +81 -0
  173. data/examples/phase3_demo.rb +159 -0
  174. data/examples/query_demo_simple.rb +131 -0
  175. data/exe/expressir +11 -0
  176. data/exe/expressir-format +14 -0
  177. data/exe/expressir-format-test +82 -0
  178. data/expressir.gemspec +61 -0
  179. data/lib/expressir/3.2/expressir_core.so +0 -0
  180. data/lib/expressir/3.3/expressir_core.so +0 -0
  181. data/lib/expressir/3.4/expressir_core.so +0 -0
  182. data/lib/expressir/4.0/expressir_core.so +0 -0
  183. data/lib/expressir/benchmark.rb +310 -0
  184. data/lib/expressir/changes/item_change.rb +20 -0
  185. data/lib/expressir/changes/mapping_change.rb +16 -0
  186. data/lib/expressir/changes/schema_change.rb +85 -0
  187. data/lib/expressir/changes/version_change.rb +26 -0
  188. data/lib/expressir/changes.rb +9 -0
  189. data/lib/expressir/cli.rb +148 -0
  190. data/lib/expressir/commands/base.rb +25 -0
  191. data/lib/expressir/commands/benchmark.rb +59 -0
  192. data/lib/expressir/commands/benchmark_cache.rb +80 -0
  193. data/lib/expressir/commands/changes.rb +34 -0
  194. data/lib/expressir/commands/changes_import_eengine.rb +146 -0
  195. data/lib/expressir/commands/changes_validate.rb +63 -0
  196. data/lib/expressir/commands/check.rb +56 -0
  197. data/lib/expressir/commands/clean.rb +20 -0
  198. data/lib/expressir/commands/coverage.rb +458 -0
  199. data/lib/expressir/commands/expand.rb +26 -0
  200. data/lib/expressir/commands/file_violations.rb +70 -0
  201. data/lib/expressir/commands/fix.rb +33 -0
  202. data/lib/expressir/commands/flatten.rb +29 -0
  203. data/lib/expressir/commands/format.rb +43 -0
  204. data/lib/expressir/commands/manifest.rb +420 -0
  205. data/lib/expressir/commands/mapping_validate.rb +68 -0
  206. data/lib/expressir/commands/non_ascii_character.rb +49 -0
  207. data/lib/expressir/commands/non_ascii_violation_collection.rb +301 -0
  208. data/lib/expressir/commands/package.rb +1222 -0
  209. data/lib/expressir/commands/parity_inputs.rb +160 -0
  210. data/lib/expressir/commands/validate.rb +106 -0
  211. data/lib/expressir/commands/validate_ascii.rb +94 -0
  212. data/lib/expressir/commands/validate_load.rb +91 -0
  213. data/lib/expressir/commands/version.rb +9 -0
  214. data/lib/expressir/commands/xsd.rb +23 -0
  215. data/lib/expressir/commands.rb +30 -0
  216. data/lib/expressir/config.rb +60 -0
  217. data/lib/expressir/coverage.rb +510 -0
  218. data/lib/expressir/eengine/arm_compare_report.rb +23 -0
  219. data/lib/expressir/eengine/changes_section.rb +16 -0
  220. data/lib/expressir/eengine/compare_report.rb +69 -0
  221. data/lib/expressir/eengine/mim_compare_report.rb +23 -0
  222. data/lib/expressir/eengine/modified_object.rb +21 -0
  223. data/lib/expressir/eengine.rb +9 -0
  224. data/lib/expressir/errors.rb +113 -0
  225. data/lib/expressir/express/adoc_hyperlink_formatter.rb +30 -0
  226. data/lib/expressir/express/adoc_source_formatter.rb +12 -0
  227. data/lib/expressir/express/ast_key_converter.rb +114 -0
  228. data/lib/expressir/express/builder.rb +277 -0
  229. data/lib/expressir/express/builder_context.rb +22 -0
  230. data/lib/expressir/express/builder_registry.rb +397 -0
  231. data/lib/expressir/express/builders/attribute_decl_builder.rb +32 -0
  232. data/lib/expressir/express/builders/built_in_builder.rb +75 -0
  233. data/lib/expressir/express/builders/constant_builder.rb +108 -0
  234. data/lib/expressir/express/builders/declaration_builder.rb +20 -0
  235. data/lib/expressir/express/builders/derive_clause_builder.rb +14 -0
  236. data/lib/expressir/express/builders/derived_attr_builder.rb +28 -0
  237. data/lib/expressir/express/builders/domain_rule_builder.rb +21 -0
  238. data/lib/expressir/express/builders/entity_decl_builder.rb +116 -0
  239. data/lib/expressir/express/builders/explicit_attr_builder.rb +49 -0
  240. data/lib/expressir/express/builders/expression_builder.rb +416 -0
  241. data/lib/expressir/express/builders/function_decl_builder.rb +86 -0
  242. data/lib/expressir/express/builders/helpers.rb +148 -0
  243. data/lib/expressir/express/builders/interface_builder.rb +151 -0
  244. data/lib/expressir/express/builders/inverse_attr_builder.rb +43 -0
  245. data/lib/expressir/express/builders/inverse_attr_type_builder.rb +30 -0
  246. data/lib/expressir/express/builders/inverse_clause_builder.rb +14 -0
  247. data/lib/expressir/express/builders/literal_builder.rb +93 -0
  248. data/lib/expressir/express/builders/procedure_decl_builder.rb +82 -0
  249. data/lib/expressir/express/builders/qualifier_builder.rb +102 -0
  250. data/lib/expressir/express/builders/reference_builder.rb +18 -0
  251. data/lib/expressir/express/builders/rule_decl_builder.rb +97 -0
  252. data/lib/expressir/express/builders/schema_body_decl_builder.rb +18 -0
  253. data/lib/expressir/express/builders/schema_decl_builder.rb +56 -0
  254. data/lib/expressir/express/builders/schema_version_builder.rb +34 -0
  255. data/lib/expressir/express/builders/simple_id_builder.rb +17 -0
  256. data/lib/expressir/express/builders/statement_builder.rb +243 -0
  257. data/lib/expressir/express/builders/subtype_constraint_builder.rb +166 -0
  258. data/lib/expressir/express/builders/syntax_builder.rb +30 -0
  259. data/lib/expressir/express/builders/type_builder.rb +238 -0
  260. data/lib/expressir/express/builders/type_decl_builder.rb +26 -0
  261. data/lib/expressir/express/builders/unique_clause_builder.rb +20 -0
  262. data/lib/expressir/express/builders/unique_rule_builder.rb +41 -0
  263. data/lib/expressir/express/builders/where_clause_builder.rb +20 -0
  264. data/lib/expressir/express/builders.rb +55 -0
  265. data/lib/expressir/express/cache.rb +76 -0
  266. data/lib/expressir/express/checker.rb +812 -0
  267. data/lib/expressir/express/concatenator.rb +79 -0
  268. data/lib/expressir/express/core.rb +95 -0
  269. data/lib/expressir/express/error.rb +124 -0
  270. data/lib/expressir/express/formatter.rb +149 -0
  271. data/lib/expressir/express/formatters/data_types_formatter.rb +362 -0
  272. data/lib/expressir/express/formatters/declarations_formatter.rb +753 -0
  273. data/lib/expressir/express/formatters/expressions_formatter.rb +180 -0
  274. data/lib/expressir/express/formatters/literals_formatter.rb +55 -0
  275. data/lib/expressir/express/formatters/references_formatter.rb +53 -0
  276. data/lib/expressir/express/formatters/remark_formatter.rb +270 -0
  277. data/lib/expressir/express/formatters/remark_item_formatter.rb +28 -0
  278. data/lib/expressir/express/formatters/statements_formatter.rb +272 -0
  279. data/lib/expressir/express/formatters/supertype_expressions_formatter.rb +54 -0
  280. data/lib/expressir/express/formatters.rb +22 -0
  281. data/lib/expressir/express/grammar/parser.rb +712 -0
  282. data/lib/expressir/express/grammar.rb +11 -0
  283. data/lib/expressir/express/hyperlink_formatter.rb +35 -0
  284. data/lib/expressir/express/interface_dot.rb +105 -0
  285. data/lib/expressir/express/lazy_repository.rb +211 -0
  286. data/lib/expressir/express/line_map.rb +48 -0
  287. data/lib/expressir/express/listing.rb +174 -0
  288. data/lib/expressir/express/model_traversal.rb +42 -0
  289. data/lib/expressir/express/model_visitor.rb +25 -0
  290. data/lib/expressir/express/node_position_index.rb +342 -0
  291. data/lib/expressir/express/parallel_files.rb +229 -0
  292. data/lib/expressir/express/parser.rb +523 -0
  293. data/lib/expressir/express/pretty_formatter.rb +586 -0
  294. data/lib/expressir/express/pretty_gate.rb +61 -0
  295. data/lib/expressir/express/refs_overlay.rb +47 -0
  296. data/lib/expressir/express/remark_attacher.rb +1098 -0
  297. data/lib/expressir/express/remark_overlay.rb +86 -0
  298. data/lib/expressir/express/remark_scanner.rb +245 -0
  299. data/lib/expressir/express/resolve_references_model_visitor.rb +29 -0
  300. data/lib/expressir/express/schema_block_scanner.rb +137 -0
  301. data/lib/expressir/express/schema_head_formatter.rb +22 -0
  302. data/lib/expressir/express/schema_plain_source_formatter.rb +11 -0
  303. data/lib/expressir/express/schema_source_formatter.rb +15 -0
  304. data/lib/expressir/express/scope_resolver.rb +223 -0
  305. data/lib/expressir/express/self_schema_reference.rb +126 -0
  306. data/lib/expressir/express/shtolo.rb +679 -0
  307. data/lib/expressir/express/smrl_xml.rb +125 -0
  308. data/lib/expressir/express/source_formatter.rb +15 -0
  309. data/lib/expressir/express/streaming_builder.rb +435 -0
  310. data/lib/expressir/express/xsd.rb +259 -0
  311. data/lib/expressir/express.rb +53 -0
  312. data/lib/expressir/liquid.rb +1 -0
  313. data/lib/expressir/manifest/resolver.rb +210 -0
  314. data/lib/expressir/manifest/validator.rb +192 -0
  315. data/lib/expressir/manifest.rb +6 -0
  316. data/lib/expressir/mapping/refpath.rb +857 -0
  317. data/lib/expressir/mapping.rb +253 -0
  318. data/lib/expressir/model/cache.rb +18 -0
  319. data/lib/expressir/model/concerns.rb +42 -0
  320. data/lib/expressir/model/data_types/aggregate.rb +26 -0
  321. data/lib/expressir/model/data_types/array.rb +25 -0
  322. data/lib/expressir/model/data_types/bag.rb +21 -0
  323. data/lib/expressir/model/data_types/binary.rb +19 -0
  324. data/lib/expressir/model/data_types/boolean.rb +15 -0
  325. data/lib/expressir/model/data_types/enumeration.rb +21 -0
  326. data/lib/expressir/model/data_types/enumeration_item.rb +24 -0
  327. data/lib/expressir/model/data_types/generic.rb +24 -0
  328. data/lib/expressir/model/data_types/generic_entity.rb +24 -0
  329. data/lib/expressir/model/data_types/integer.rb +15 -0
  330. data/lib/expressir/model/data_types/list.rb +23 -0
  331. data/lib/expressir/model/data_types/logical.rb +15 -0
  332. data/lib/expressir/model/data_types/number.rb +15 -0
  333. data/lib/expressir/model/data_types/real.rb +17 -0
  334. data/lib/expressir/model/data_types/select.rb +23 -0
  335. data/lib/expressir/model/data_types/set.rb +21 -0
  336. data/lib/expressir/model/data_types/string.rb +19 -0
  337. data/lib/expressir/model/data_types.rb +25 -0
  338. data/lib/expressir/model/declarations/attribute.rb +38 -0
  339. data/lib/expressir/model/declarations/constant.rb +28 -0
  340. data/lib/expressir/model/declarations/derived_attribute.rb +25 -0
  341. data/lib/expressir/model/declarations/entity.rb +62 -0
  342. data/lib/expressir/model/declarations/function.rb +59 -0
  343. data/lib/expressir/model/declarations/informal_proposition_rule.rb +25 -0
  344. data/lib/expressir/model/declarations/interface.rb +37 -0
  345. data/lib/expressir/model/declarations/interface_item.rb +21 -0
  346. data/lib/expressir/model/declarations/interfaced_item.rb +33 -0
  347. data/lib/expressir/model/declarations/inverse_attribute.rb +25 -0
  348. data/lib/expressir/model/declarations/parameter.rb +28 -0
  349. data/lib/expressir/model/declarations/procedure.rb +57 -0
  350. data/lib/expressir/model/declarations/remark_item.rb +21 -0
  351. data/lib/expressir/model/declarations/rule.rb +66 -0
  352. data/lib/expressir/model/declarations/schema.rb +215 -0
  353. data/lib/expressir/model/declarations/schema_version.rb +19 -0
  354. data/lib/expressir/model/declarations/schema_version_item.rb +19 -0
  355. data/lib/expressir/model/declarations/subtype_constraint.rb +32 -0
  356. data/lib/expressir/model/declarations/type.rb +45 -0
  357. data/lib/expressir/model/declarations/unique_rule.rb +26 -0
  358. data/lib/expressir/model/declarations/variable.rb +28 -0
  359. data/lib/expressir/model/declarations/where_rule.rb +26 -0
  360. data/lib/expressir/model/declarations.rb +31 -0
  361. data/lib/expressir/model/dependency_resolver.rb +268 -0
  362. data/lib/expressir/model/exp_file.rb +42 -0
  363. data/lib/expressir/model/expressions/aggregate_initializer.rb +18 -0
  364. data/lib/expressir/model/expressions/aggregate_initializer_item.rb +20 -0
  365. data/lib/expressir/model/expressions/binary_expression.rb +59 -0
  366. data/lib/expressir/model/expressions/entity_constructor.rb +20 -0
  367. data/lib/expressir/model/expressions/function_call.rb +20 -0
  368. data/lib/expressir/model/expressions/interval.rb +29 -0
  369. data/lib/expressir/model/expressions/query_expression.rb +31 -0
  370. data/lib/expressir/model/expressions/unary_expression.rb +25 -0
  371. data/lib/expressir/model/expressions.rb +18 -0
  372. data/lib/expressir/model/identifier.rb +26 -0
  373. data/lib/expressir/model/indexes/entity_index.rb +103 -0
  374. data/lib/expressir/model/indexes/item_graph.rb +185 -0
  375. data/lib/expressir/model/indexes/reference_index.rb +148 -0
  376. data/lib/expressir/model/indexes/type_index.rb +149 -0
  377. data/lib/expressir/model/indexes.rb +12 -0
  378. data/lib/expressir/model/interface_validator.rb +384 -0
  379. data/lib/expressir/model/literals/binary.rb +17 -0
  380. data/lib/expressir/model/literals/integer.rb +17 -0
  381. data/lib/expressir/model/literals/logical.rb +21 -0
  382. data/lib/expressir/model/literals/real.rb +17 -0
  383. data/lib/expressir/model/literals/string.rb +20 -0
  384. data/lib/expressir/model/literals.rb +13 -0
  385. data/lib/expressir/model/model_element.rb +377 -0
  386. data/lib/expressir/model/references/attribute_reference.rb +19 -0
  387. data/lib/expressir/model/references/group_reference.rb +19 -0
  388. data/lib/expressir/model/references/index_reference.rb +23 -0
  389. data/lib/expressir/model/references/simple_reference.rb +21 -0
  390. data/lib/expressir/model/references.rb +12 -0
  391. data/lib/expressir/model/remark_format.rb +17 -0
  392. data/lib/expressir/model/remark_info.rb +92 -0
  393. data/lib/expressir/model/remark_placement.rb +42 -0
  394. data/lib/expressir/model/repository.rb +483 -0
  395. data/lib/expressir/model/repository_validator.rb +293 -0
  396. data/lib/expressir/model/search_engine.rb +551 -0
  397. data/lib/expressir/model/statements/alias.rb +31 -0
  398. data/lib/expressir/model/statements/assignment.rb +23 -0
  399. data/lib/expressir/model/statements/case.rb +42 -0
  400. data/lib/expressir/model/statements/case_action.rb +20 -0
  401. data/lib/expressir/model/statements/compound.rb +21 -0
  402. data/lib/expressir/model/statements/escape.rb +18 -0
  403. data/lib/expressir/model/statements/if.rb +26 -0
  404. data/lib/expressir/model/statements/null.rb +18 -0
  405. data/lib/expressir/model/statements/procedure_call.rb +22 -0
  406. data/lib/expressir/model/statements/repeat.rb +40 -0
  407. data/lib/expressir/model/statements/return.rb +20 -0
  408. data/lib/expressir/model/statements/skip.rb +18 -0
  409. data/lib/expressir/model/statements.rb +20 -0
  410. data/lib/expressir/model/supertype_expressions/binary_supertype_expression.rb +25 -0
  411. data/lib/expressir/model/supertype_expressions/oneof_supertype_expression.rb +17 -0
  412. data/lib/expressir/model/supertype_expressions.rb +12 -0
  413. data/lib/expressir/model.rb +39 -0
  414. data/lib/expressir/package/builder.rb +231 -0
  415. data/lib/expressir/package/metadata.rb +79 -0
  416. data/lib/expressir/package/reader.rb +149 -0
  417. data/lib/expressir/package.rb +8 -0
  418. data/lib/expressir/schema_manifest.rb +159 -0
  419. data/lib/expressir/schema_manifest_entry.rb +15 -0
  420. data/lib/expressir/version.rb +8 -0
  421. data/lib/expressir.rb +94 -0
  422. data/lib/tasks/verify_remarks.rake +16 -0
  423. data/rakelib/native.rake +24 -0
  424. data/rakelib/oracle.rake +52 -0
  425. data/rakelib/verify_remarks.rake +16 -0
  426. metadata +692 -0
@@ -0,0 +1,1098 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressir
4
+ module Express
5
+ # Handles attaching remarks (comments) to model elements after parsing.
6
+ #
7
+ # Two collaborators sit behind the `attach` interface:
8
+ # - {ScopeResolver} answers "which scope contains line N?"
9
+ # - {NodePositionIndex} answers "which model node is nearest line N?"
10
+ #
11
+ # Remark scanning itself lives in {RemarkScanner}; line→byte lookup lives
12
+ # in {LineMap}. This class is the orchestrator: it walks the remarks, asks
13
+ # the collaborators for targets, and writes the remarks onto the model.
14
+ #
15
+ # NOTE: Post-processing remark attachment has inherent limitations for scope-based
16
+ # matching. Remarks with simple tags (like "WR1") inside scopes (TYPE, ENTITY, etc.)
17
+ # cannot be perfectly matched without parsing context. This implementation prioritizes:
18
+ # 1. Exact path matches (e.g., "schema.entity.WR1")
19
+ # 2. Proximity-based matching for simple tags
20
+ # 3. NOT creating spurious schema-level items for ambiguous tags
21
+ class RemarkAttacher
22
+ # Type-driven registry: maps each model class to its collection attributes.
23
+ # Shared with {NodePositionIndex} via that class's own copy of the table.
24
+ # Two declarations rather than a cross-reference so each module is loadable
25
+ # on its own without forcing the other to load.
26
+ COLLECTION_REGISTRY = NodePositionIndex::COLLECTION_REGISTRY
27
+
28
+ # Collections holding executable statements — the regions a body
29
+ # comment can belong to.
30
+ STATEMENT_REGIONS = %i[
31
+ statements else_statements action_statements otherwise_statements
32
+ ].freeze
33
+
34
+ # Expression and statement child attributes are declared on the model
35
+ # via `child_attributes :foo, :bar, ...`. See TODO.bugs/15.
36
+ EXPRESSION_CHILDREN = Model::ModelElement.child_attributes_registry
37
+
38
+ # `WHERE <label> :` clause headers; the captured label maps to the
39
+ # 1-based line number(s) it appears on.
40
+ WHERE_CLAUSE_PATTERN = /\A\s*WHERE\s+(\w+)\s*:/i
41
+
42
+ def initialize(source)
43
+ @source = source
44
+ @attached_spans = Set.new
45
+ @line_map = LineMap.new(source.b)
46
+ @model = nil
47
+ @scope_resolver = nil
48
+ @node_index = nil
49
+ @owner_map = nil
50
+ @active_scope_map = nil
51
+ end
52
+
53
+ def attach(model)
54
+ @model = model
55
+ remarks = RemarkScanner.new(@source).scan
56
+
57
+ @node_index = NodePositionIndex.new(model, @line_map)
58
+ @scope_resolver = ScopeResolver.new(
59
+ source: @source,
60
+ model: model,
61
+ nodes_with_positions: @node_index.nodes,
62
+ )
63
+
64
+ attach_tagged_remarks(remarks)
65
+ attach_untagged_remarks(remarks)
66
+
67
+ model
68
+ ensure
69
+ # Free expensive data structures once attachment is over. On the
70
+ # raising path this also drops the memoized ownership map, which
71
+ # would otherwise outlive the node index it was derived from.
72
+ @source = nil
73
+ @scope_resolver = nil
74
+ @node_index = nil
75
+ @line_map = nil
76
+ @owner_map = nil
77
+ @active_scope_map = nil
78
+ @source_lines_for_where_clause = nil
79
+ @where_clause_line_index = nil
80
+ end
81
+
82
+ private
83
+
84
+ # ----- Tagged remark attachment -----
85
+
86
+ def attach_tagged_remarks(remarks)
87
+ tagged = remarks.select(&:tag)
88
+ return if tagged.empty?
89
+
90
+ tagged.each do |remark|
91
+ next if @attached_spans.include?(remark.position)
92
+
93
+ tag = remark.tag
94
+ target = nil
95
+
96
+ containing_scope = @scope_resolver.containing_scope_for(remark.line)
97
+
98
+ # Check if this is an informal proposition tag (IP\d+)
99
+ if tag.match?(/^IP\d+$/)
100
+ scope = containing_scope || @scope_resolver.find_by_source_text(remark.line)
101
+ if scope && supports_informal_propositions?(scope)
102
+ target = create_or_find_informal_proposition(scope, tag)
103
+ end
104
+ end
105
+
106
+ # Standard path-based lookup
107
+ if target.nil?
108
+ # Handle prefixed tags like wr:WR1, ip:IP1, ur:UR1
109
+ if tag.include?(":") && !tag.include?(".")
110
+ target = handle_prefixed_tag(tag, containing_scope, @model,
111
+ get_schema_ids(@model))
112
+ end
113
+
114
+ # Strategy 1: Try exact path lookup
115
+ if target.nil?
116
+ target = find_by_exact_path(@model, tag)
117
+ end
118
+
119
+ # Strategy 1b: For paths with dots, try with scope path prefix first
120
+ if target.nil? && tag.include?(".")
121
+ if containing_scope && function_rule_procedure?(containing_scope)
122
+ scope_path = build_scope_path(containing_scope)
123
+ if scope_path
124
+ target = find_by_exact_path(@model, "#{scope_path}.#{tag}")
125
+ end
126
+ end
127
+
128
+ if target.nil?
129
+ schema_ids = get_schema_ids(@model)
130
+ schema_ids.each do |schema_id|
131
+ target = find_by_exact_path(@model, "#{schema_id}.#{tag}")
132
+ break if target
133
+ end
134
+ end
135
+ end
136
+
137
+ # Strategy 2: For simple tags, find in containing scope first
138
+ if target.nil? && !tag.include?(".")
139
+ if containing_scope
140
+ target = find_node_in_scope(containing_scope, tag)
141
+
142
+ if target.nil? && supports_where_rules?(containing_scope)
143
+ target = find_target_in_where_clause(containing_scope, tag,
144
+ remark.line)
145
+ end
146
+
147
+ if target.nil? && !function_rule_procedure?(containing_scope)
148
+ schema_ids = get_schema_ids(@model)
149
+ schema_ids.each do |schema_id|
150
+ target = find_by_exact_path(@model, "#{schema_id}.#{tag}")
151
+ break if target
152
+ end
153
+ end
154
+ else
155
+ schema_ids = get_schema_ids(@model)
156
+ schema_ids.each do |schema_id|
157
+ target = find_by_exact_path(@model, "#{schema_id}.#{tag}")
158
+ break if target
159
+ end
160
+ end
161
+ end
162
+
163
+ # Strategy 3: Create implicit item for qualified paths only
164
+ if target.nil? && tag.include?(".")
165
+ if containing_scope && function_rule_procedure?(containing_scope)
166
+ scope_path = build_scope_path(containing_scope)
167
+ if scope_path
168
+ target = create_implicit_remark_item(@model, "#{scope_path}.#{tag}",
169
+ get_schema_ids(@model))
170
+ end
171
+ end
172
+ if target.nil?
173
+ target = create_implicit_remark_item(@model, tag,
174
+ get_schema_ids(@model))
175
+ end
176
+ end
177
+
178
+ # Strategy 4: For simple tags at schema level, create implicit item
179
+ if target.nil? && !tag.include?(".")
180
+ schema_ids = get_schema_ids(@model)
181
+ if schema_ids.any?
182
+ target = create_implicit_remark_item_at_schema(@model, tag,
183
+ schema_ids.first)
184
+ end
185
+ end
186
+ end
187
+
188
+ if target
189
+ add_remark(target, remark.text, format: remark.format, tag: remark.tag,
190
+ source_offset: remark.position)
191
+ @attached_spans << remark.position
192
+ end
193
+ end
194
+ end
195
+
196
+ # ----- Untagged remark attachment -----
197
+
198
+ def attach_untagged_remarks(remarks)
199
+ untagged = remarks.reject(&:tag)
200
+ return unless untagged.any?
201
+
202
+ untagged.each do |remark|
203
+ next if @attached_spans.include?(remark.position)
204
+
205
+ line_content = line_content_for(remark.line)
206
+ if end_scope_line?(line_content)
207
+ matched_node = @node_index.node_for_end_scope_at(remark.line, line_content)
208
+ if matched_node
209
+ add_remark(matched_node, remark.text, format: remark.format, tag: nil,
210
+ source_offset: remark.position)
211
+ @attached_spans << remark.position
212
+ next
213
+ end
214
+ end
215
+
216
+ target, placement, region = find_body_comment_target(remark)
217
+ if target
218
+ add_remark(target, remark.text, format: remark.format, tag: nil,
219
+ placement: placement, region: region)
220
+ @attached_spans << remark.position
221
+ next
222
+ end
223
+
224
+ matched_node = @node_index.nearest_node_to(remark.line)
225
+ if matched_node
226
+ add_remark(matched_node, remark.text, format: remark.format, tag: nil,
227
+ source_offset: remark.position)
228
+ @attached_spans << remark.position
229
+ end
230
+ end
231
+ end
232
+
233
+ def end_scope_line?(line_content)
234
+ line_content =~ /END_(SCHEMA|ENTITY|TYPE|FUNCTION|PROCEDURE|RULE)/i
235
+ end
236
+
237
+ # Own-line body comments belong to the next statement in the same
238
+ # statement region (Function body, THEN branch, ELSE branch, loop body):
239
+ # attached there with LEADING placement. A comment with no following
240
+ # statement in its region closes that region, so it attaches to the
241
+ # region's owner with TRAILING placement and the region's name — an IF
242
+ # owns two bodies that close at different keywords.
243
+ #
244
+ # Returns [nil, nil, nil] — "use the legacy fallback" — when the remark
245
+ # shares a line with a node or sits outside any statement-bearing node.
246
+ def find_body_comment_target(remark)
247
+ line = remark.line
248
+ # An own-line comment shares its line with no node. A node STARTING
249
+ # here means the remark is an inline tail (code; -- note). The
250
+ # end-line check is restricted to statements: container end_lines are
251
+ # child-derived approximations that can collide with comment lines.
252
+ if inline_remark?(remark)
253
+ target, placement, region = inline_target(remark,
254
+ @node_index.starting_at(line))
255
+ return [target, placement, region] if target
256
+
257
+ # A remark trailing a mid-construct keyword (`ELSE -- why`) starts
258
+ # nothing and closes nothing; handled before the legacy fallthrough,
259
+ # which would hand it to the enclosing construct unplaced — where
260
+ # nothing renders it.
261
+ mid = mid_keyword_inline_target(remark)
262
+ return mid if mid
263
+ end
264
+
265
+ # A closing keyword on the next code line is decisive: the comment
266
+ # closes that body. Without this check the comment would instead be
267
+ # read as leading the next statement of an OUTER region, which is
268
+ # where it would wrongly render.
269
+ closing = closing_region_target(line)
270
+ return closing if closing.first
271
+
272
+ enclosing, region, = statement_region_for(line)
273
+ return [nil, nil, nil] unless region
274
+
275
+ following = region
276
+ .select { |n| n[:line] > line }
277
+ .min_by { |n| n[:position] }
278
+ if following
279
+ return [following[:node], Model::RemarkPlacement::LEADING, nil]
280
+ end
281
+
282
+ # No following statement and no closing keyword above: the comment is
283
+ # not demonstrably inside this body (it may sit after the whole
284
+ # declaration). Keep the legacy attachment rather than guessing.
285
+ [enclosing[:node], nil, nil]
286
+ end
287
+
288
+ # Whether the remark trails code on its own line. Decided from the
289
+ # source text before it, not from node positions: container end lines
290
+ # are child-derived approximations that collide with comment lines and
291
+ # would misread an own-line comment as a trailing one.
292
+ def inline_remark?(remark)
293
+ content = line_content_for(remark.line).to_s
294
+ opener = content.index("--")
295
+ return false unless opener
296
+
297
+ !content[0...opener].strip.empty?
298
+ end
299
+
300
+ # A comment trailing code on its line belongs to the statement it
301
+ # follows: `x := 1; -- why`, or the opener of a compound one, as in
302
+ # `IF x THEN -- why`.
303
+ #
304
+ # Multi-line statements were once excluded here, because appending a
305
+ # remark to one would carry it down to its closing keyword. They are
306
+ # admitted now that the formatter can put an opener remark back on the
307
+ # first line; the OPENER_REGION on the returned placement is what tells
308
+ # it to, and without that region the old appending behaviour stands.
309
+ def inline_target(remark, nodes)
310
+ started_here = nodes.select do |n|
311
+ n[:node].is_a?(Model::TakesInlineRemark) &&
312
+ n[:line] == remark.line &&
313
+ n[:position] && n[:position] < remark.position
314
+ end
315
+
316
+ # Of the two that began here, whichever starts later is the one the
317
+ # remark follows: `x := 0; IF n > 0 THEN -- why` trails the IF.
318
+ began = [single_line_owner(started_here, remark.line),
319
+ opener_owner(started_here, remark.line)]
320
+ .compact.max_by { |n| n[:position] }
321
+
322
+ owner = began || closing_owner(nodes, remark)
323
+ return [nil, nil, nil] unless owner
324
+
325
+ [owner[:node], Model::RemarkPlacement::INLINE,
326
+ opener_region(owner, remark.line)]
327
+ end
328
+
329
+ # A node beginning and ending on the remark's line. Ranked by where it
330
+ # ends, so of several sharing the line the outermost complete one wins:
331
+ # `CASE n OF 1 : x := 2; END_CASE; -- why` trails the CASE, not its
332
+ # action.
333
+ def single_line_owner(candidates, line)
334
+ candidates
335
+ .select { |n| n[:end_line] == line }
336
+ .max_by { |n| n[:position] + n[:node].source.to_s.length }
337
+ end
338
+
339
+ # A node whose opener is on the remark's line but which continues past
340
+ # it. Ranked by where it STARTS, not where it ends: such a node's source
341
+ # spans its whole body, so ranking by end would let an enclosing block
342
+ # outrank the one the remark actually follows.
343
+ def opener_owner(candidates, line)
344
+ candidates
345
+ .select { |n| n[:end_line] && n[:end_line] > line }
346
+ .max_by { |n| n[:position] }
347
+ end
348
+
349
+ # Nothing began on the remark's line, so it may still be closing one
350
+ # that began earlier:
351
+ #
352
+ # REFERENCE FROM x
353
+ # (a, b); -- why
354
+ #
355
+ # Only reached as a fallback. Were it folded into the main lookup, an
356
+ # enclosing block would become a candidate for every remark inside it,
357
+ # since a node's end line is derived from its children.
358
+ def closing_owner(nodes, remark)
359
+ nodes
360
+ .select do |n|
361
+ n[:node].is_a?(Model::TakesInlineRemark) &&
362
+ n[:end_line] == remark.line && n[:line] &&
363
+ n[:line] < remark.line &&
364
+ n[:position] && n[:position] < remark.position
365
+ end
366
+ .max_by { |n| n[:position] }
367
+ end
368
+
369
+ # A remark trailing a node that closes on its line follows the whole
370
+ # node, so it is written back the way an ordinary inline remark is. One
371
+ # that continues past the line trailed the node's opener instead, and
372
+ # belongs back there rather than after the closing keyword.
373
+ def opener_region(owner, line)
374
+ return nil unless owner[:end_line] && owner[:end_line] > line
375
+
376
+ Model::RemarkPlacement::OPENER_REGION
377
+ end
378
+
379
+ # Lines beginning with a mid-construct keyword. The keyword closes
380
+ # nothing and opens nothing, but the remark trailing it belongs to the
381
+ # construct it branches — written back after the keyword, not after the
382
+ # construct's END_.
383
+ MID_KEYWORD_REGIONS = {
384
+ /\AELSE\b/i => [Model::Statements::If,
385
+ Model::RemarkPlacement::ELSE_REGION],
386
+ /\AOTHERWISE\b/i => [Model::Statements::Case,
387
+ Model::RemarkPlacement::OTHERWISE_REGION],
388
+ }.freeze
389
+
390
+ # Attach `ELSE -- why` / `OTHERWISE : -- why` to the innermost
391
+ # enclosing IF / CASE with INLINE placement and the keyword's region.
392
+ # Returns [nil, nil, nil] when the line head is no mid-construct
393
+ # keyword or no construct of the right kind spans the line.
394
+ def mid_keyword_inline_target(remark)
395
+ content = line_content_for(remark.line).to_s
396
+ opener = content.index("--")
397
+ return [nil, nil, nil] unless opener
398
+
399
+ owner = MID_KEYWORD_REGIONS.find { |pattern, _| content[0...opener].strip.match?(pattern) }
400
+ return [nil, nil, nil] unless owner
401
+
402
+ klass, region = owner[1]
403
+ innermost = @node_index.nodes.select do |n|
404
+ n[:node].is_a?(klass) && n[:line] && n[:end_line] &&
405
+ n[:line] < remark.line && n[:end_line] > remark.line
406
+ end.max_by { |n| n[:line] }
407
+ return [nil, nil, nil] unless innermost
408
+
409
+ [innermost[:node], Model::RemarkPlacement::INLINE, region]
410
+ end
411
+
412
+ # Which closing keyword ends which region of which owner. A comment
413
+ # sitting between a body's last statement and one of these keywords
414
+ # closes that body.
415
+ CLOSING_KEYWORDS = {
416
+ /\AELSE\b/i => [Model::Statements::If, :statements],
417
+ /\AEND_IF\b/i => [Model::Statements::If, :else_statements],
418
+ /\AOTHERWISE\b/i => [Model::Statements::Case, :action_statements],
419
+ /\AEND_CASE\b/i => [Model::Statements::Case, :otherwise_statements],
420
+ /\AEND_REPEAT\b/i => [Model::Statements::Repeat, :statements],
421
+ /\AEND_ALIAS\b/i => [Model::Statements::Alias, :statements],
422
+ /\AEND\s*;/i => [Model::Statements::Compound, :statements],
423
+ # A RULE's executable body ends at WHERE, not at END_RULE.
424
+ /\AWHERE\b/i => [Model::Declarations::Rule, :statements],
425
+ /\AEND_FUNCTION\b/i => [Model::Declarations::Function, :statements],
426
+ /\AEND_PROCEDURE\b/i => [Model::Declarations::Procedure, :statements],
427
+ # END_RULE closes the WHERE section when the rule has one.
428
+ /\AEND_RULE\b/i => [Model::Declarations::Rule, :where_rules],
429
+ }.freeze
430
+
431
+ # Regions whose owner may not have that body, in which case the
432
+ # keyword closes the earlier region instead.
433
+ REGION_FALLBACKS = {
434
+ [Model::Statements::If, :else_statements] =>
435
+ [:statements, ->(n) { n.else_statements&.length&.positive? }],
436
+ [Model::Statements::Case, :otherwise_statements] =>
437
+ [:action_statements, ->(n) { !n.otherwise_statement.nil? }],
438
+ [Model::Declarations::Rule, :where_rules] =>
439
+ [:statements, ->(n) { n.where_rules&.length&.positive? }],
440
+ }.freeze
441
+
442
+ # A node's indexed span stops at its last child, so a comment written
443
+ # after that child but before the node's closing keyword sits outside
444
+ # every span and never reaches statement_region_for. Resolve it from
445
+ # the keyword that follows: it names both the owner type and the body
446
+ # being closed.
447
+ def closing_region_target(line)
448
+ keyword_owner, region, keyword_line = closing_keyword_after(line)
449
+ return [nil, nil, nil] unless keyword_owner
450
+
451
+ # The owner is the construct the keyword actually closes — the
452
+ # innermost one still open at that line. Picking the latest node of
453
+ # the right class instead would grab an already-closed inner block
454
+ # (nested IFs) or an unrelated earlier declaration (a RULE, when the
455
+ # WHERE really belongs to an ENTITY).
456
+ opener_line = active_opener_line(keyword_line, keyword_owner)
457
+ return [nil, nil, nil] unless opener_line
458
+
459
+ owner = @node_index.starting_at(opener_line).find do |n|
460
+ n[:node].is_a?(keyword_owner)
461
+ end
462
+ return [nil, nil, nil] unless owner
463
+
464
+ # END_IF closes the THEN body when there is no ELSE; END_CASE closes
465
+ # the last action when there is no OTHERWISE.
466
+ fallback, present = REGION_FALLBACKS[[keyword_owner, region]]
467
+ region = fallback if fallback && !present.call(owner[:node])
468
+
469
+ [owner[:node], Model::RemarkPlacement::TRAILING, region.to_s]
470
+ end
471
+
472
+ # The first non-blank, non-comment source line after `line`.
473
+ # Source keywords that open a nestable construct, paired with the class
474
+ # of node they produce. Used to find which construct a closing keyword
475
+ # actually belongs to.
476
+ OPENERS = [
477
+ [/\bIF\b.*?\bTHEN\b/i, Model::Statements::If],
478
+ [/\bCASE\b.*?\bOF\b/i, Model::Statements::Case],
479
+ [/\bREPEAT\b/i, Model::Statements::Repeat],
480
+ [/\bALIAS\b/i, Model::Statements::Alias],
481
+ [/\bBEGIN\b/i, Model::Statements::Compound],
482
+ [/\A\s*FUNCTION\b/i, Model::Declarations::Function],
483
+ [/\A\s*PROCEDURE\b/i, Model::Declarations::Procedure],
484
+ [/\A\s*RULE\b/i, Model::Declarations::Rule],
485
+ [/\A\s*ENTITY\b/i, :other],
486
+ [/\A\s*TYPE\b/i, :other],
487
+ ].freeze
488
+
489
+ CLOSERS = /\bEND_IF\b|\bEND_CASE\b|\bEND_REPEAT\b|\bEND_ALIAS\b|\bEND_FUNCTION\b|\bEND_PROCEDURE\b|\bEND_RULE\b|\bEND_ENTITY\b|\bEND_TYPE\b|\bEND\s*;/i
490
+
491
+ # Strips what must not be scanned for keywords: string literals and a
492
+ # trailing `--` remark. Without this, `x := 'IF a THEN'` or a comment
493
+ # mentioning REPEAT would push a construct that never opened.
494
+ def keyword_scannable(content)
495
+ without_strings = content.gsub(/'[^']*'/, "''")
496
+ tail = without_strings.index("--")
497
+ tail ? without_strings[0...tail] : without_strings
498
+ end
499
+
500
+ # The opening line of the innermost construct still open at
501
+ # `keyword_line`, or nil when that construct is not of `expected_class`.
502
+ def active_opener_line(keyword_line, expected_class)
503
+ active = active_scope_map[keyword_line]
504
+ return nil unless active && active[0] == expected_class
505
+
506
+ active[1]
507
+ end
508
+
509
+ # Line number => the construct open at the START of that line, as
510
+ # [class, opening_line]. Built once per source: rescanning from line 1
511
+ # for every trailing comment is quadratic, and on a comment-dense file
512
+ # that cost dominates parsing entirely.
513
+ def active_scope_map
514
+ @active_scope_map ||= build_active_scope_map
515
+ end
516
+
517
+ def build_active_scope_map
518
+ map = {}
519
+ stack = []
520
+ (1..source_line_count).each do |ln|
521
+ map[ln] = stack.last
522
+ content = keyword_scannable(line_content_for(ln).to_s.strip)
523
+ next if content.empty? || content.start_with?("--")
524
+
525
+ line_events(content).each do |_offset, kind, klass|
526
+ kind == :open ? stack << [klass, ln] : stack.pop
527
+ end
528
+ end
529
+ map
530
+ end
531
+
532
+ # Opener/closer events on one line, ordered by where they appear.
533
+ # EVERY occurrence is collected, not just the first: a line holding two
534
+ # complete IF blocks contributes two openers and two closers, and
535
+ # recording only one opener would over-pop the enclosing construct.
536
+ def line_events(content)
537
+ events = []
538
+ OPENERS.each do |pattern, klass|
539
+ content.enum_for(:scan, pattern).each do
540
+ events << [Regexp.last_match.begin(0), :open, klass]
541
+ end
542
+ end
543
+ content.enum_for(:scan, CLOSERS).each do
544
+ events << [Regexp.last_match.begin(0), :close, nil]
545
+ end
546
+ events.sort_by(&:first)
547
+ end
548
+
549
+ def closing_keyword_after(line)
550
+ probe = line + 1
551
+ # Skip further comment lines AND blank lines: a comment separated
552
+ # from its closing keyword by an empty line still closes that body.
553
+ while probe <= source_line_count
554
+ content = line_content_for(probe).to_s.strip
555
+ break unless content.empty? || content.start_with?("--")
556
+
557
+ probe += 1
558
+ end
559
+ content = line_content_for(probe).to_s.strip
560
+ CLOSING_KEYWORDS.each do |pattern, owner_region|
561
+ return [*owner_region, probe] if content.match?(pattern)
562
+ end
563
+ [nil, nil, nil]
564
+ end
565
+
566
+ def statement_region_for(line, nodes = nil)
567
+ candidates = (nodes || @node_index.spanning(line)).select do |n|
568
+ n[:line] && n[:end_line] && n[:line] <= line && n[:end_line] >= line &&
569
+ (n[:node].is_a?(Model::Statement) || function_rule_procedure?(n[:node]))
570
+ end
571
+ enclosing = innermost_candidate(candidates)
572
+ return [nil, nil, nil] unless enclosing
573
+
574
+ children = @node_index.children_in(enclosing[:node], STATEMENT_REGIONS)
575
+ return [enclosing, nil, nil] if children.empty?
576
+
577
+ preceding = children.select { |n| n[:line] < line }.max_by { |n| n[:position] }
578
+ following = children.select { |n| n[:line] > line }.min_by { |n| n[:position] }
579
+ region_attr = region_attr_for(line, preceding, following)
580
+ return [enclosing, nil, nil] unless region_attr
581
+
582
+ [enclosing, children.select { |n| n[:collection] == region_attr }, region_attr]
583
+ end
584
+
585
+ # Node end lines are child-derived approximations, so a parent's span
586
+ # can come out SMALLER than a child's and span size alone picks the
587
+ # wrong container. Ownership links are exact: drop every candidate
588
+ # that is an ancestor of another candidate, then pick the smallest
589
+ # span among the true leaves.
590
+ def innermost_candidate(candidates)
591
+ return candidates.first if candidates.length <= 1
592
+
593
+ owner_of = owner_map
594
+ ancestors = Set.new.compare_by_identity
595
+ candidates.each do |cand|
596
+ current = owner_of[cand[:node]]
597
+ while current
598
+ ancestors << current
599
+ current = owner_of[current]
600
+ end
601
+ end
602
+
603
+ leaves = candidates.reject { |n| ancestors.include?(n[:node]) }
604
+ (leaves.empty? ? candidates : leaves).min_by { |n| n[:end_line] - n[:line] }
605
+ end
606
+
607
+ # The node index is immutable during attachment, so its ownership map
608
+ # only needs to be built once for all body remarks.
609
+ def owner_map
610
+ # Identity comparison must be enabled BEFORE the hash is populated.
611
+ # Model elements compare by value, so two distinct-but-equal nodes
612
+ # would collapse into one entry during a plain build, and switching
613
+ # to identity afterwards cannot recover the lost entry.
614
+ @owner_map ||= @node_index.nodes.each_with_object(
615
+ {}.compare_by_identity,
616
+ ) { |n, map| map[n[:node]] = n[:owner] }
617
+ end
618
+
619
+ # The keyword that opens each region, for regions that follow another
620
+ # region of the same owner. A comment in the gap belongs to whichever
621
+ # side of this keyword it was written on.
622
+ REGION_OPENERS = {
623
+ else_statements: /\A(?:.*;)?\s*ELSE(?:\s*--.*)?\z/i,
624
+ otherwise_statements: /\A\s*OTHERWISE\b/i,
625
+ }.freeze
626
+
627
+ # A comment between two regions of the same owner — between the THEN
628
+ # body and ELSE, or between the last CASE action and OTHERWISE — sits
629
+ # on one side of the keyword that opens the second region. The gap can
630
+ # hold only that keyword and comments, so scanning it is exact.
631
+ # Comment lines are skipped so prose mentioning the keyword cannot
632
+ # match.
633
+ def region_attr_for(line, preceding, following)
634
+ return following&.dig(:collection) unless preceding
635
+
636
+ following_attr = following&.dig(:collection)
637
+ opener = REGION_OPENERS[following_attr]
638
+ if opener && following_attr != preceding[:collection]
639
+ opener_line = (preceding[:end_line]...following[:line]).find do |ln|
640
+ content = line_content_for(ln).strip
641
+ !content.start_with?("--") && opener.match?(content)
642
+ end
643
+ return following_attr if opener_line && line > opener_line
644
+ end
645
+
646
+ preceding[:collection]
647
+ end
648
+
649
+ # ----- Tag resolution (within a scope) -----
650
+
651
+ def find_node_in_scope(scope, tag)
652
+ return nil unless scope
653
+
654
+ collections_on(scope).each do |collection|
655
+ collection.each do |item|
656
+ next unless item.is_a?(Model::HasRemarks)
657
+ return item if item.id == tag
658
+ end
659
+ end
660
+
661
+ types = get_collection(scope, :types)
662
+ types&.each do |type|
663
+ result = find_enumeration_item_in_type(type, tag)
664
+ return result if result
665
+ end
666
+
667
+ statements = get_collection(scope, :statements)
668
+ statements&.each do |stmt|
669
+ result = find_node_in_statement(stmt, tag)
670
+ return result if result
671
+
672
+ result = find_query_in_expression(stmt, tag)
673
+ return result if result
674
+ end
675
+
676
+ nil
677
+ end
678
+
679
+ def find_enumeration_item_in_type(type, tag)
680
+ return nil unless type
681
+ return nil unless type.is_a?(Model::Declarations::Type)
682
+
683
+ type.enumeration_items&.each do |item|
684
+ return item if item.id == tag
685
+ end
686
+
687
+ ut = type.underlying_type
688
+ return nil unless ut.is_a?(Model::DataTypes::Enumeration) && ut.items
689
+
690
+ ut.items.each { |item| return item if item.id == tag }
691
+ nil
692
+ end
693
+
694
+ def find_query_in_expression(node, tag, visited = Set.new)
695
+ return nil unless node
696
+ return nil unless node.is_a?(Model::ModelElement)
697
+ return nil if visited.include?(node.object_id)
698
+
699
+ visited.add(node.object_id)
700
+
701
+ return node if node.is_a?(Model::Expressions::QueryExpression) && node.id == tag
702
+
703
+ attrs = EXPRESSION_CHILDREN[node.class]
704
+ return nil unless attrs
705
+
706
+ attrs.each do |attr|
707
+ value = node.public_send(attr)
708
+ case value
709
+ when Array
710
+ value.each do |val|
711
+ next unless val.is_a?(Model::ModelElement)
712
+
713
+ result = find_query_in_expression(val, tag, visited)
714
+ return result if result
715
+ end
716
+ when Model::ModelElement
717
+ result = find_query_in_expression(value, tag, visited)
718
+ return result if result
719
+ end
720
+ end
721
+
722
+ nil
723
+ end
724
+
725
+ def function_rule_procedure?(node)
726
+ return false unless node
727
+
728
+ node.is_a?(Model::Declarations::Function) ||
729
+ node.is_a?(Model::Declarations::Rule) ||
730
+ node.is_a?(Model::Declarations::Procedure)
731
+ end
732
+
733
+ # Handle prefixed tags like wr:WR1, ip:IP1, ur:UR1
734
+ def handle_prefixed_tag(tag, containing_scope, model, schema_ids)
735
+ prefix, id = tag.split(":")
736
+ return nil unless id
737
+
738
+ collection_attr = case prefix.downcase
739
+ when "wr" then :where_rules
740
+ when "ip" then :informal_propositions
741
+ when "ur" then :unique_rules
742
+ end
743
+ return nil unless collection_attr
744
+
745
+ collection = get_collection(containing_scope, collection_attr)
746
+ if collection
747
+ found = collection.find { |item| item.is_a?(Model::ModelElement) && item.id == id }
748
+ return found if found
749
+ end
750
+
751
+ schema_ids.each do |schema_id|
752
+ full_path = "#{schema_id}.#{tag.tr(':', '.')}"
753
+ found = safe_find(model, full_path)
754
+ return found if found
755
+ end
756
+
757
+ nil
758
+ end
759
+
760
+ # Find target for remarks inside WHERE clauses by scanning source lines
761
+ # for `WHERE <id>:` patterns. Lives here (not in ScopeResolver) because
762
+ # it's about WHERE-rule membership, not scope membership.
763
+ def find_target_in_where_clause(scope, tag, remark_line)
764
+ return nil unless supports_where_rules?(scope)
765
+
766
+ where_rules = get_collection(scope, :where_rules)
767
+ return nil unless where_rules&.any?
768
+
769
+ where_clause_lines = where_clause_line_index
770
+
771
+ where_rules.each do |wr|
772
+ next unless wr.id
773
+
774
+ where_clause_lines.fetch(wr.id, []).each do |line_num|
775
+ next unless line_num < remark_line
776
+
777
+ return create_remark_item(wr, tag) if remark_line.between?(line_num, line_num + 5)
778
+ end
779
+ end
780
+
781
+ nil
782
+ end
783
+
784
+ # Single scan over the source lines mapping each `WHERE <id>:` label to
785
+ # its 1-based line number, so per-remark lookups stop re-testing every
786
+ # line against every WHERE rule's regex.
787
+ def where_clause_line_index
788
+ @where_clause_line_index ||= begin
789
+ index = Hash.new { |h, k| h[k] = [] }
790
+ source_lines_for_where_clause.each_with_index do |line, idx|
791
+ if (match = line.match(WHERE_CLAUSE_PATTERN))
792
+ index[match[1]] << (idx + 1)
793
+ end
794
+ end
795
+ index
796
+ end
797
+ end
798
+
799
+ def source_lines_for_where_clause
800
+ # @source is set for the duration of `attach`; freed at the end.
801
+ @source_lines_for_where_clause ||= @source.lines
802
+ end
803
+
804
+ def find_node_in_statement(stmt, tag)
805
+ case stmt
806
+ when Model::Statements::Alias
807
+ return stmt if stmt.id == tag
808
+ when Model::Statements::Repeat
809
+ return stmt if stmt.id == tag
810
+ when Model::Expressions::QueryExpression
811
+ return stmt if stmt.id == tag
812
+ end
813
+ nil
814
+ end
815
+
816
+ def build_scope_path(node)
817
+ return nil unless node
818
+
819
+ parts = []
820
+ current = node
821
+
822
+ while current
823
+ if current.is_a?(Model::ModelElement) && current.id
824
+ parts.unshift(current.id)
825
+ end
826
+
827
+ break if current.is_a?(Model::Declarations::Schema)
828
+
829
+ current = current.parent
830
+ end
831
+
832
+ parts.empty? ? nil : parts.join(".")
833
+ end
834
+
835
+ # ----- Path-based lookup -----
836
+
837
+ def find_by_exact_path(model, path)
838
+ return nil unless path
839
+ return nil unless repository?(model) || exp_file?(model)
840
+
841
+ result = safe_find(model, path)
842
+ return result if result
843
+
844
+ normalized = path.tr(":", ".")
845
+ normalized == path ? nil : safe_find(model, normalized)
846
+ end
847
+
848
+ # ----- Target creation -----
849
+
850
+ def create_implicit_remark_item_at_schema(model, item_id, schema_id)
851
+ return nil unless repository?(model) || exp_file?(model)
852
+
853
+ schema = safe_find(model, schema_id)
854
+ return nil unless schema.is_a?(Model::Declarations::Schema)
855
+
856
+ # Strip "wr:" prefix convention (e.g. "wr:IP1" → "IP1")
857
+ ip_id = extract_ip_id(item_id)
858
+ if ip_id && supports_informal_propositions?(schema)
859
+ return create_or_find_informal_proposition(schema, ip_id)
860
+ end
861
+
862
+ return nil unless supports_remark_items?(schema)
863
+
864
+ existing = schema.remark_items&.find { |ri| ri.id == item_id }
865
+ return existing if existing
866
+
867
+ create_remark_item(schema, item_id)
868
+ end
869
+
870
+ def create_implicit_remark_item(model, path, schema_ids = [])
871
+ return nil unless repository?(model) || exp_file?(model)
872
+
873
+ # Handle "wr:IP1" convention: extract parent path and IP id directly
874
+ # so normalize_path doesn't split "wr:IP1" into "wr"."IP1" segments.
875
+ ip_prefix_match = path.match(/\A(.+)\.(\w+):(IP\d+)\z/)
876
+ if ip_prefix_match
877
+ parent_path = ip_prefix_match[1]
878
+ ip_id = ip_prefix_match[3]
879
+ parent = safe_find(model, parent_path)
880
+ if parent.nil? && schema_ids.any?
881
+ schema_ids.each do |schema_id|
882
+ parent = safe_find(model, "#{schema_id}.#{parent_path}")
883
+ break if parent
884
+ end
885
+ end
886
+
887
+ if parent && supports_informal_propositions?(parent)
888
+ return create_or_find_informal_proposition(parent, ip_id)
889
+ end
890
+ end
891
+
892
+ clean_path = normalize_path(path)
893
+ parts = clean_path.split(".")
894
+ return nil if parts.length < 2
895
+
896
+ (parts.length - 1).downto(1) do |i|
897
+ parent_path = parts[0...i].join(".")
898
+ item_id = parts[i]
899
+
900
+ parent = safe_find(model, parent_path)
901
+
902
+ if parent.nil? && schema_ids.any?
903
+ schema_ids.each do |schema_id|
904
+ parent = safe_find(model, "#{schema_id}.#{parent_path}")
905
+ break if parent
906
+ end
907
+ end
908
+
909
+ next unless parent
910
+
911
+ return create_item_at_parent(parent, item_id)
912
+ end
913
+
914
+ nil
915
+ end
916
+
917
+ def normalize_path(path)
918
+ return path unless path.include?(":")
919
+
920
+ path_part, _, item_suffix = path.rpartition(":")
921
+ "#{path_part}.#{item_suffix}"
922
+ end
923
+
924
+ def create_item_at_parent(parent, item_id)
925
+ # Strip "wr:" prefix convention (e.g. "wr:IP1" → "IP1")
926
+ ip_id = extract_ip_id(item_id)
927
+ if ip_id && supports_informal_propositions?(parent)
928
+ return create_or_find_informal_proposition(parent, ip_id)
929
+ end
930
+
931
+ return nil unless supports_remark_items?(parent)
932
+
933
+ existing = parent.remark_items&.find { |ri| ri.id == item_id }
934
+ return existing if existing
935
+
936
+ create_remark_item(parent, item_id)
937
+ end
938
+
939
+ def create_or_find_informal_proposition(parent, id)
940
+ return nil unless supports_informal_propositions?(parent)
941
+
942
+ existing = parent.informal_propositions&.find { |ip| ip.id == id }
943
+ return existing if existing
944
+
945
+ ip = Model::Declarations::InformalPropositionRule.new(id: id)
946
+ ip.parent = parent
947
+ parent.informal_propositions ||= []
948
+ parent.informal_propositions << ip
949
+ safe_reset_children_by_id(parent)
950
+
951
+ remark_item = Model::Declarations::RemarkItem.new(id: id)
952
+ remark_item.parent = ip
953
+ ip.remark_items ||= []
954
+ ip.remark_items << remark_item
955
+ safe_reset_children_by_id(ip)
956
+
957
+ remark_item
958
+ end
959
+
960
+ def create_remark_item(parent, id)
961
+ item = Model::Declarations::RemarkItem.new(id: id)
962
+ item.parent = parent
963
+ parent.remark_items ||= []
964
+ parent.remark_items << item
965
+ safe_reset_children_by_id(parent)
966
+ item
967
+ end
968
+
969
+ # ----- Remark storage -----
970
+
971
+ def add_remark(node, text, format: Model::RemarkFormat::TAIL, tag: nil,
972
+ placement: nil, region: nil, source_offset: nil)
973
+ return unless node
974
+ return unless node.is_a?(Model::ModelElement)
975
+ # An empty remark body carries no content; storing it produced
976
+ # remarks == [""] entries (GH-363). Bare `--IPn:` informal
977
+ # propositions keep their declarations through
978
+ # create_or_find_informal_proposition, which does not run through
979
+ # here — only the redundant empty-body storage drops.
980
+ return if text.nil? || text.strip.empty?
981
+
982
+ if supports_remarks?(node)
983
+ if node_has_remarks?(node)
984
+ node.remarks ||= []
985
+ node.remarks << text
986
+ end
987
+
988
+ if tag.nil?
989
+ remark_info = Model::RemarkInfo.new(text: text, format: format,
990
+ placement: placement,
991
+ region: region,
992
+ source_offset: source_offset)
993
+ node.untagged_remarks ||= []
994
+ node.untagged_remarks << remark_info
995
+ end
996
+ end
997
+ end
998
+
999
+ # ----- Type predicates -----
1000
+
1001
+ def supports_remarks?(obj)
1002
+ obj.is_a?(Model::ModelElement)
1003
+ end
1004
+
1005
+ def node_has_remarks?(obj)
1006
+ obj.is_a?(Model::HasRemarks)
1007
+ end
1008
+
1009
+ def supports_remark_items?(obj)
1010
+ obj.is_a?(Model::HasRemarkItems)
1011
+ end
1012
+
1013
+ def supports_informal_propositions?(obj)
1014
+ obj.is_a?(Model::HasInformalPropositions)
1015
+ end
1016
+
1017
+ # Extracts an IP id from a potentially prefixed item_id.
1018
+ # "wr:IP1" → "IP1", "IP1" → "IP1", "WR1" → nil
1019
+ def extract_ip_id(item_id)
1020
+ bare = item_id.sub(/\A\w+:/, "")
1021
+ bare.match?(/^IP\d+$/) ? bare : nil
1022
+ end
1023
+
1024
+ def supports_where_rules?(obj)
1025
+ obj.is_a?(Model::HasWhereRules)
1026
+ end
1027
+
1028
+ def repository?(obj)
1029
+ obj.is_a?(Model::Repository)
1030
+ end
1031
+
1032
+ def exp_file?(obj)
1033
+ obj.is_a?(Model::ExpFile)
1034
+ end
1035
+
1036
+ def cache?(obj)
1037
+ obj.is_a?(Model::Cache)
1038
+ end
1039
+
1040
+ def get_schema_ids(model)
1041
+ if repository?(model) || exp_file?(model)
1042
+ model.schemas.filter_map(&:id)
1043
+ else
1044
+ []
1045
+ end
1046
+ end
1047
+
1048
+ # ----- Collection access -----
1049
+
1050
+ # Type-driven collection access — returns all collections for a node's type.
1051
+ def collections_on(node)
1052
+ attrs = COLLECTION_REGISTRY[node.class]
1053
+ return [] unless attrs
1054
+
1055
+ attrs.filter_map do |attr|
1056
+ col = node.public_send(attr)
1057
+ col if col.is_a?(Array)
1058
+ end
1059
+ end
1060
+
1061
+ # Type-driven single collection access — returns one named collection if the type has it.
1062
+ def get_collection(node, attr_name)
1063
+ return nil unless COLLECTION_REGISTRY[node.class]&.include?(attr_name)
1064
+
1065
+ collection = node.public_send(attr_name)
1066
+ collection if collection.is_a?(Array)
1067
+ end
1068
+
1069
+ # ----- Helpers -----
1070
+
1071
+ def safe_find(model, path)
1072
+ return nil unless model
1073
+
1074
+ model.find(path)
1075
+ rescue StandardError
1076
+ nil
1077
+ end
1078
+
1079
+ def safe_reset_children_by_id(obj)
1080
+ return unless obj
1081
+ return unless obj.is_a?(Model::ScopeContainer)
1082
+
1083
+ obj.reset_children_by_id
1084
+ end
1085
+
1086
+ def line_content_for(line_num)
1087
+ lines = source_lines_for_where_clause
1088
+ return "" if line_num < 1 || line_num > lines.length
1089
+
1090
+ lines[line_num - 1]
1091
+ end
1092
+
1093
+ def source_line_count
1094
+ source_lines_for_where_clause.length
1095
+ end
1096
+ end
1097
+ end
1098
+ end