hecks 3.14.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (427) hide show
  1. checksums.yaml +4 -4
  2. data/examples/pizzas/adapters/fake_processor.rb +34 -0
  3. data/examples/pizzas/adapters/payment_signature.rb +66 -0
  4. data/examples/pizzas/adapters/payment_webhook.rb +87 -0
  5. data/examples/pizzas/bluebook/pizzas.behaviors +33 -0
  6. data/examples/pizzas/bluebook/pizzas.hecksagon +5 -3
  7. data/examples/pizzas/config.ru +22 -0
  8. data/exe/hecks +1 -1
  9. data/lib/hecks/adapters/driven/prism.rb +0 -2
  10. data/lib/hecks/adapters/driven.rb +0 -2
  11. data/lib/hecks/adapters/driving/json.rb +9 -16
  12. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +2 -2
  13. data/lib/hecks/bluebook/dsl/derivation_words.rb +58 -0
  14. data/lib/hecks/bluebook/dsl/translation_builder.rb +6 -37
  15. data/lib/hecks/bluebook/expression/ast_json.rb +0 -9
  16. data/lib/hecks/bluebook/hecksagon.rb +1 -1
  17. data/lib/hecks/bluebook/meta_validator/translation_judge/rules.rb +7 -10
  18. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +4 -8
  19. data/lib/hecks/bluebook/meta_validator.rb +0 -11
  20. data/lib/hecks/bluebook/translation.rb +18 -10
  21. data/lib/hecks/bluebook/translation_expression/evaluator.rb +63 -0
  22. data/lib/hecks/bluebook/translation_expression/parser.rb +127 -0
  23. data/lib/hecks/bluebook/translation_expression.rb +39 -0
  24. data/lib/hecks/bluebook/translation_plan.rb +70 -0
  25. data/lib/hecks/bluebook.rb +2 -0
  26. data/lib/hecks/chapters.rb +3 -3
  27. data/lib/hecks/cli/project_cli/launcher_source.rb +1 -1
  28. data/lib/hecks/composer/adapters/claude_composer.adapter +3 -0
  29. data/lib/hecks/composer/adapters/claude_composer.rb +83 -0
  30. data/lib/hecks/composer/adapters/fake_agent.adapter +3 -0
  31. data/lib/hecks/composer/adapters/fake_agent.rb +55 -0
  32. data/lib/hecks/composer/adapters/jev.adapter +3 -0
  33. data/lib/hecks/composer/adapters/jev.rb +66 -0
  34. data/lib/hecks/composer/agent.rb +113 -0
  35. data/lib/hecks/composer/assistant.rb +110 -0
  36. data/lib/hecks/composer/behavior_run.rb +53 -0
  37. data/lib/hecks/composer/bluebook/composer.bluebook +178 -0
  38. data/lib/hecks/composer/bluebook/composer.hecksagon +16 -0
  39. data/lib/hecks/composer/board.rb +65 -0
  40. data/lib/hecks/composer/board_edit/command_text.rb +39 -0
  41. data/lib/hecks/composer/board_edit/source.rb +84 -0
  42. data/lib/hecks/composer/board_edit.rb +144 -0
  43. data/lib/hecks/composer/check.rb +89 -0
  44. data/lib/hecks/composer/domains.rb +81 -0
  45. data/lib/hecks/composer/jobs.rb +134 -0
  46. data/lib/hecks/composer/layout.rb +37 -0
  47. data/lib/hecks/composer/live.rb +150 -0
  48. data/lib/hecks/composer/outline.rb +48 -0
  49. data/lib/hecks/composer/page.html +989 -0
  50. data/lib/hecks/composer/panels.rb +142 -0
  51. data/lib/hecks/composer/parse.rb +83 -0
  52. data/lib/hecks/composer/plain.rb +28 -0
  53. data/lib/hecks/composer/ports/composer_agent.port +7 -0
  54. data/lib/hecks/composer/session.rb +141 -0
  55. data/lib/hecks/composer/toolbox.rb +166 -0
  56. data/lib/hecks/composer/web.rb +156 -0
  57. data/lib/hecks/composer/workbench.rb +51 -0
  58. data/lib/hecks/composer/workspace.rb +81 -0
  59. data/lib/hecks/composer.rb +17 -0
  60. data/lib/hecks/corpus/adapters.rb +22 -0
  61. data/lib/hecks/corpus/rust.rb +1 -1
  62. data/lib/hecks/corpus/worlds.rb +19 -0
  63. data/lib/hecks/corpus.rb +5 -0
  64. data/lib/hecks/forms/app/commands.rb +2 -3
  65. data/lib/hecks/forms/app/queries.rb +2 -3
  66. data/lib/hecks/forms/app/records.rb +17 -5
  67. data/lib/hecks/forms/app/responses.rb +3 -1
  68. data/lib/hecks/forms/command_form_renderer.rb +42 -16
  69. data/lib/hecks/forms/field_controls.rb +2 -2
  70. data/lib/hecks/forms/field_renderer.rb +1 -1
  71. data/lib/hecks/forms/field_shape.rb +36 -17
  72. data/lib/hecks/forms/index_renderer.rb +2 -1
  73. data/lib/hecks/forms/page.rb +1 -1
  74. data/lib/hecks/forms/params.rb +0 -13
  75. data/lib/hecks/forms/query_form_renderer.rb +7 -6
  76. data/lib/hecks/forms/record_renderer.rb +11 -7
  77. data/lib/hecks/forms/rule_words.rb +100 -0
  78. data/lib/hecks/forms/value_object_shape.rb +2 -1
  79. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -3
  80. data/lib/hecks/fuzzing/isolated_boot/worlds.rb +3 -6
  81. data/lib/hecks/fuzzing/replay/filters.rb +2 -6
  82. data/lib/hecks/gate/stages.yml +25 -0
  83. data/lib/hecks/grammar/evolve/seeds.rb +0 -6
  84. data/lib/hecks/grammar/evolve.rb +0 -3
  85. data/lib/hecks/grammar/translation.bluebook +1 -1
  86. data/lib/hecks/hecks/adapters/codebase/composing.rb +56 -0
  87. data/lib/hecks/hecks/adapters/codebase/source_tree.rb +3 -1
  88. data/lib/hecks/hecks/adapters/first_admin.rb +3 -3
  89. data/lib/hecks/hecks/adapters/journal_store/changes.rb +0 -26
  90. data/lib/hecks/hecks/adapters/journal_store/examination.rb +2 -23
  91. data/lib/hecks/hecks/adapters/journal_store/readings.rb +3 -11
  92. data/lib/hecks/hecks/adapters/journal_store.rb +0 -11
  93. data/lib/hecks/hecks/adapters/rehearsal/inquiry.rb +4 -11
  94. data/lib/hecks/hecks/adapters/rehearsal/memory_store.rb +0 -9
  95. data/lib/hecks/hecks/adapters/rehearsal/postgres_reading.rb +0 -4
  96. data/lib/hecks/hecks/codebase.behaviors +6 -0
  97. data/lib/hecks/hecks/codebase.bluebook +21 -0
  98. data/lib/hecks/hecks/composer.behaviors +68 -0
  99. data/lib/hecks/hecks/context_map.hecksagon +1 -1
  100. data/lib/hecks/hecks/custodian.behaviors +0 -6
  101. data/lib/hecks/hecks/custodian.bluebook +3 -80
  102. data/lib/hecks/hecks/hecks.hecksagon +2 -2
  103. data/lib/hecks/hecks/hecks.world +1 -1
  104. data/lib/hecks/hecks/rehearsal.bluebook +7 -13
  105. data/lib/hecks/kernel_capabilities/templates.rb +4 -4
  106. data/lib/hecks/kernel_capabilities.rb +1 -1
  107. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -3
  108. data/lib/hecks/language/translation/translation_aggregate.bluebook +21 -24
  109. data/lib/hecks/ports/access_control.rb +2 -2
  110. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -41
  111. data/lib/hecks/ports/persistence/plugins/era/lineage/coverage.rb +7 -7
  112. data/lib/hecks/ports/persistence/plugins/era/lineage/rewrite.rb +25 -2
  113. data/lib/hecks/ports/persistence/plugins/era/lineage/rules.rb +4 -4
  114. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +17 -25
  115. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/base_sql.rb +0 -12
  116. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/domain_rename.rb +1 -1
  117. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +2 -22
  118. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +0 -1
  119. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +9 -3
  120. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +3 -29
  121. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/refusals.rb +2 -26
  122. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -33
  123. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -3
  124. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +2 -4
  125. data/lib/hecks/ports/persistence/plugins/era/translation/expression_sql.rb +52 -0
  126. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +23 -29
  127. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +1 -1
  128. data/lib/hecks/ports/persistence/plugins/era/translation.rb +0 -1
  129. data/lib/hecks/ports/persistence/plugins/era.rb +3 -6
  130. data/lib/hecks/ports.rb +0 -1
  131. data/lib/hecks/projection_files.rb +3 -1
  132. data/lib/hecks/projections/deploy/templates/lambda/nat_comment_oauth.tmpl +1 -1
  133. data/lib/hecks/projections/glossary/entries.rb +10 -4
  134. data/lib/hecks/projections/glossary/groups.rb +42 -0
  135. data/lib/hecks/projections/glossary/html/page.rb +7 -2
  136. data/lib/hecks/projections/glossary/html/parser.rb +1 -1
  137. data/lib/hecks/projections/glossary/html/sections.rb +31 -11
  138. data/lib/hecks/projections/glossary/html.rb +1 -2
  139. data/lib/hecks/projections/glossary/markdown.rb +31 -28
  140. data/lib/hecks/projections/glossary/mermaid.rb +3 -0
  141. data/lib/hecks/projections/glossary/page.css +143 -153
  142. data/lib/hecks/projections/glossary/page.js +63 -3
  143. data/lib/hecks/projections/glossary/sentences/actions.rb +88 -0
  144. data/lib/hecks/projections/glossary/sentences/paths.rb +77 -0
  145. data/lib/hecks/projections/glossary/sentences/queries.rb +90 -0
  146. data/lib/hecks/projections/glossary/sentences/value_objects.rb +32 -5
  147. data/lib/hecks/projections/glossary/sentences.rb +13 -3
  148. data/lib/hecks/projections/glossary/slugs.rb +4 -1
  149. data/lib/hecks/projections/glossary.rb +2 -1
  150. data/lib/hecks/projections/plain_english.rb +97 -0
  151. data/lib/hecks/projections/rust_vocabulary/arguments.rb +2 -2
  152. data/lib/hecks/projections/rust_vocabulary/enum_body.rb +20 -25
  153. data/lib/hecks/projections/rust_vocabulary/tests.rb +2 -2
  154. data/lib/hecks/projections/rust_vocabulary.rb +14 -6
  155. data/lib/hecks/projections/site/cms_editor/color.rb +0 -6
  156. data/lib/hecks/projections/site/cms_editor/templates/src/host.ts.tmpl +7 -2
  157. data/lib/hecks/projections/statements/actions.rb +99 -0
  158. data/lib/hecks/projections/statements/relationships.rb +86 -0
  159. data/lib/hecks/projections/statements.rb +15 -24
  160. data/lib/hecks/projector/exporter/adapters.rb +112 -0
  161. data/lib/hecks/projector/exporter/translations.rb +18 -35
  162. data/lib/hecks/projector/exporter/worlds.rb +103 -0
  163. data/lib/hecks/projector/exporter.rb +5 -1
  164. data/lib/hecks/quality_control/adapters/agent.rb +6 -7
  165. data/lib/hecks/quality_control/adapters/agent_profile.rb +16 -114
  166. data/lib/hecks/quality_control/cli/qa_mine_combinations.rb +1 -1
  167. data/lib/hecks/runtime/loader/boot_steps.rb +0 -20
  168. data/lib/hecks/runtime/outbox.rb +2 -9
  169. data/lib/hecks/runtime/port_operation_interpreter.rb +1 -9
  170. data/lib/hecks/runtime/remote_dispatcher.rb +2 -2
  171. data/lib/hecks/runtime/value/unwrapping.rb +60 -0
  172. data/lib/hecks/runtime/value.rb +10 -21
  173. data/lib/hecks/rust_build/project_rust/target_ir.rb +23 -4
  174. data/lib/hecks/rust_build/wasm.rb +0 -12
  175. data/lib/hecks/rust_build/write_if_changed.rb +12 -12
  176. data/lib/hecks/rust_format.rb +77 -0
  177. data/lib/hecks/tools/fuzz_sweep/findings.rb +3 -9
  178. data/lib/hecks/tools/fuzz_sweep.rb +1 -3
  179. data/lib/hecks/tools/translation_audit/verdicts.rb +3 -28
  180. data/lib/hecks/tools/translation_audit.rb +8 -10
  181. data/lib/hecks/tools/translation_scaffold/reporting.rb +1 -1
  182. data/lib/hecks/version.rb +1 -1
  183. data/lib/hecks/vocabulary.rb +2 -1
  184. data/lib/hecks.rb +3 -0
  185. data/prebuilt/{1a6f596be124228be93eba1d86dbc9d2933190fa389b284331241cb9f27115a0.marshal → ad68f9e06c8068c2a4176b57abf3083d591d5d8980c2b3e897b97331f965b643.marshal} +0 -0
  186. data/prebuilt/{a883a7e568667aa5ac904c82daff356bdf9e996252a36af6def4a4a43efb967e.marshal → ee22ec8642c4006717ba2b80f46e5efb81de459eef9dfec043e838c0ad4fd720.marshal} +0 -0
  187. data/prebuilt/{verdicts-872993b1aa26d73fe48f28251374b8585fed145d02aab42be0f00de386b6ff80.json → verdicts-b69f5dcbba79c20daa0493476fa7c317004a30aa2bc3d3dc6a722ea70ae2b43b.json} +1 -1
  188. data/rust/Cargo.toml +40 -0
  189. data/rust/build/Cargo.toml +36 -0
  190. data/rust/build/clippy.toml +4 -0
  191. data/rust/build/src/adapters_pass.rs +196 -0
  192. data/rust/build/src/binding_pass.rs +54 -13
  193. data/rust/build/src/build_artifact.rs +23 -5
  194. data/rust/build/src/cargo_sync.rs +69 -26
  195. data/rust/build/src/lineage_pass.rs +101 -87
  196. data/rust/build/src/main.rs +11 -2
  197. data/rust/build/src/pipeline.rs +80 -19
  198. data/rust/build/src/reserved_names.rs +6 -50
  199. data/rust/build/src/resolve.rs +82 -35
  200. data/rust/build/src/rustfmt.rs +100 -0
  201. data/rust/build/src/seams_pass.rs +85 -16
  202. data/rust/build/src/subprocess.rs +30 -6
  203. data/rust/build/src/tmp.rs +5 -2
  204. data/rust/build/src/translation_pass.rs +72 -22
  205. data/rust/build/src/world_pass.rs +109 -0
  206. data/rust/clippy.toml +4 -0
  207. data/rust/codegen/Cargo.toml +36 -0
  208. data/rust/codegen/clippy.toml +4 -0
  209. data/rust/codegen/src/asks.rs +40 -14
  210. data/rust/codegen/src/attr.rs +2 -2
  211. data/rust/codegen/src/bridging.rs +676 -117
  212. data/rust/codegen/src/commands.rs +979 -373
  213. data/rust/codegen/src/constraints.rs +88 -25
  214. data/rust/codegen/src/dependency_planning.rs +128 -34
  215. data/rust/codegen/src/domain_generator.rs +306 -170
  216. data/rust/codegen/src/exemplar.rs +87 -32
  217. data/rust/codegen/src/expr_emitter.rs +54 -6
  218. data/rust/codegen/src/fielded.rs +193 -41
  219. data/rust/codegen/src/hecks_naming.rs +124 -16
  220. data/rust/codegen/src/hecks_naming_tests.rs +81 -0
  221. data/rust/codegen/src/json.rs +80 -44
  222. data/rust/codegen/src/json_codec.rs +585 -149
  223. data/rust/codegen/src/literal.rs +104 -138
  224. data/rust/codegen/src/main.rs +151 -52
  225. data/rust/codegen/src/manifest.rs +76 -14
  226. data/rust/codegen/src/mutations.rs +1212 -239
  227. data/rust/codegen/src/naming.rs +111 -60
  228. data/rust/codegen/src/optional_pass.rs +90 -28
  229. data/rust/codegen/src/ports.rs +5 -4
  230. data/rust/codegen/src/prelude.rs +139 -49
  231. data/rust/codegen/src/presenters.rs +335 -0
  232. data/rust/codegen/src/queries.rs +601 -226
  233. data/rust/codegen/src/reactions.rs +268 -74
  234. data/rust/codegen/src/read_models.rs +574 -229
  235. data/rust/codegen/src/reference_specs.rs +16 -3
  236. data/rust/codegen/src/registry.rs +128 -79
  237. data/rust/codegen/src/reserved_names.rs +6 -50
  238. data/rust/codegen/src/shared.rs +71 -22
  239. data/rust/codegen/src/sidecars.rs +15 -5
  240. data/rust/codegen/src/skip_reason.rs +8 -2
  241. data/rust/codegen/src/types.rs +306 -108
  242. data/rust/host/Cargo.lock +1 -0
  243. data/rust/host/Cargo.toml +40 -4
  244. data/rust/host/HECKS_RELEASE +1 -1
  245. data/rust/host/clippy.toml +4 -0
  246. data/rust/host/src/api.rs +698 -218
  247. data/rust/host/src/auth.rs +491 -214
  248. data/rust/host/src/b64.rs +91 -0
  249. data/rust/host/src/bin/expr_harness.rs +6 -2
  250. data/rust/host/src/bin/lineage_harness.rs +47 -11
  251. data/rust/host/src/bin/mint_harness.rs +70 -37
  252. data/rust/host/src/bin/validate_harness.rs +16 -8
  253. data/rust/host/src/boot.rs +144 -68
  254. data/rust/host/src/boundary_fuzz/api.rs +130 -32
  255. data/rust/host/src/boundary_fuzz/auth.rs +108 -23
  256. data/rust/host/src/boundary_fuzz/expr_json.rs +85 -11
  257. data/rust/host/src/boundary_fuzz/ir.rs +164 -12
  258. data/rust/host/src/boundary_fuzz/server.rs +283 -36
  259. data/rust/host/src/boundary_fuzz/web.rs +56 -8
  260. data/rust/host/src/checkout.rs +78 -32
  261. data/rust/host/src/clock.rs +8 -0
  262. data/rust/host/src/commerce_ir.rs +333 -95
  263. data/rust/host/src/crypto.rs +33 -0
  264. data/rust/host/src/dispatch.rs +1037 -331
  265. data/rust/host/src/expr_json.rs +1021 -273
  266. data/rust/host/src/extension.rs +24 -4
  267. data/rust/host/src/field_hints.rs +11 -7
  268. data/rust/host/src/fuzz_support.rs +85 -13
  269. data/rust/host/src/host.rs +100 -0
  270. data/rust/host/src/http.rs +22 -0
  271. data/rust/host/src/ir.rs +326 -16
  272. data/rust/host/src/journal.rs +277 -176
  273. data/rust/host/src/lambda_client.rs +219 -53
  274. data/rust/host/src/lib.rs +16 -4
  275. data/rust/host/src/log.rs +62 -11
  276. data/rust/host/src/mint.rs +1153 -326
  277. data/rust/host/src/needs.rs +185 -44
  278. data/rust/host/src/payments/keystore.rs +197 -45
  279. data/rust/host/src/payments/roles.rs +39 -7
  280. data/rust/host/src/payments/tests.rs +1427 -282
  281. data/rust/host/src/payments.rs +391 -112
  282. data/rust/host/src/platform_signin/attack_tests.rs +5 -5
  283. data/rust/host/src/platform_signin/config.rs +11 -11
  284. data/rust/host/src/platform_signin/cookie.rs +2 -2
  285. data/rust/host/src/platform_signin/extension.rs +2 -1
  286. data/rust/host/src/platform_signin/handler.rs +9 -8
  287. data/rust/host/src/platform_signin/jti.rs +4 -2
  288. data/rust/host/src/platform_signin/jwks.rs +2 -1
  289. data/rust/host/src/platform_signin/jws.rs +2 -37
  290. data/rust/host/src/platform_signin/known_answer_tests.rs +5 -1
  291. data/rust/host/src/platform_signin/mode_tests.rs +5 -3
  292. data/rust/host/src/platform_signin/pages.rs +20 -20
  293. data/rust/host/src/platform_signin/session.rs +8 -1
  294. data/rust/host/src/platform_signin/settings.rs +6 -6
  295. data/rust/host/src/platform_signin/strict_json.rs +12 -9
  296. data/rust/host/src/platform_signin/verify.rs +41 -41
  297. data/rust/host/src/platform_signin/wiring_support.rs +13 -18
  298. data/rust/host/src/presentation.rs +207 -63
  299. data/rust/host/src/presentation_write.rs +502 -187
  300. data/rust/host/src/query_step.rs +69 -18
  301. data/rust/host/src/rate_limit.rs +541 -142
  302. data/rust/host/src/reference_transform.rs +77 -25
  303. data/rust/host/src/reference_validate.rs +443 -104
  304. data/rust/host/src/resend.rs +190 -55
  305. data/rust/host/src/secrets.rs +116 -21
  306. data/rust/host/src/server.rs +555 -104
  307. data/rust/host/src/storage_shape.rs +88 -21
  308. data/rust/host/src/test_pg.rs +17 -7
  309. data/rust/host/src/ui_schema.rs +495 -207
  310. data/rust/host/src/wasm_runner.rs +21 -6
  311. data/rust/host/src/web/commerce.rs +101 -23
  312. data/rust/host/src/web/newsletter/cooldown.rs +10 -3
  313. data/rust/host/src/web/newsletter/template.rs +26 -7
  314. data/rust/host/src/web/newsletter/tests.rs +112 -23
  315. data/rust/host/src/web/newsletter.rs +209 -59
  316. data/rust/host/src/web/newsletter_send.rs +251 -79
  317. data/rust/host/src/web/registration_receipt/tests.rs +16 -4
  318. data/rust/host/src/web/registration_receipt.rs +100 -24
  319. data/rust/host/src/web/registrations.rs +1384 -332
  320. data/rust/host/src/web/resend_webhook.rs +138 -80
  321. data/rust/host/src/web.rs +2416 -508
  322. data/rust/lsp/Cargo.toml +36 -0
  323. data/rust/lsp/clippy.toml +4 -0
  324. data/rust/lsp/src/json.rs +29 -22
  325. data/rust/lsp/src/main.rs +81 -31
  326. data/rust/lsp/src/outline.rs +23 -7
  327. data/rust/lsp/src/rpc.rs +17 -4
  328. data/rust/parser/Cargo.toml +36 -0
  329. data/rust/parser/clippy.toml +4 -0
  330. data/rust/parser/src/build/closed_sets.rs +6 -1
  331. data/rust/parser/src/build/identity.rs +4 -3
  332. data/rust/parser/src/build/mod.rs +1 -1
  333. data/rust/parser/src/build/named_pattern.rs +4 -1
  334. data/rust/parser/src/build/naming.rs +2 -80
  335. data/rust/parser/src/build/pattern_subset.rs +6 -8
  336. data/rust/parser/src/build/query_derive.rs +3 -1
  337. data/rust/parser/src/build/query_inference.rs +14 -5
  338. data/rust/parser/src/build/read_model.rs +4 -1
  339. data/rust/parser/src/build/references.rs +4 -3
  340. data/rust/parser/src/canonical.rs +34 -15
  341. data/rust/parser/src/emit.rs +187 -105
  342. data/rust/parser/src/expr/ast_json.rs +134 -42
  343. data/rust/parser/src/expr/evaluator.rs +27 -9
  344. data/rust/parser/src/expr/mod.rs +54 -13
  345. data/rust/parser/src/expr/resolver.rs +299 -92
  346. data/rust/parser/src/ir.rs +35 -3
  347. data/rust/parser/src/keywords.rs +7168 -516
  348. data/rust/parser/src/lex.rs +11 -25
  349. data/rust/parser/src/lib.rs +1 -0
  350. data/rust/parser/src/main.rs +65 -6
  351. data/rust/parser/src/parse/adapter.rs +157 -0
  352. data/rust/parser/src/parse/aggregate.rs +19 -10
  353. data/rust/parser/src/parse/chapter.rs +16 -17
  354. data/rust/parser/src/parse/command.rs +35 -16
  355. data/rust/parser/src/parse/domain_port.rs +4 -7
  356. data/rust/parser/src/parse/entity.rs +21 -11
  357. data/rust/parser/src/parse/hecksagon.rs +10 -4
  358. data/rust/parser/src/parse/lifecycle.rs +9 -5
  359. data/rust/parser/src/parse/mod.rs +22 -91
  360. data/rust/parser/src/parse/needs.rs +21 -12
  361. data/rust/parser/src/parse/policy.rs +2 -2
  362. data/rust/parser/src/parse/process_manager.rs +9 -11
  363. data/rust/parser/src/parse/query.rs +8 -4
  364. data/rust/parser/src/parse/read_model.rs +57 -16
  365. data/rust/parser/src/parse/value_object.rs +7 -2
  366. data/rust/parser/src/parse/world.rs +322 -0
  367. data/rust/parser/src/ruby_value.rs +7 -6
  368. data/rust/src/exemplar/json.rs +4 -4
  369. data/rust/src/exemplar/mutations.rs +18 -0
  370. data/rust/src/exemplar/read_models.rs +1 -10
  371. data/rust/src/kernel/attribute_shapes/composite.rs +54 -14
  372. data/rust/src/kernel/attribute_shapes/list.rs +2 -2
  373. data/rust/src/kernel/attribute_shapes/mod.rs +8 -8
  374. data/rust/src/kernel/attribute_shapes/scalar.rs +2 -2
  375. data/rust/src/kernel/cli.rs +244 -114
  376. data/rust/src/kernel/dispatch.rs +323 -115
  377. data/rust/src/kernel/dispatch_guard_tests.rs +327 -65
  378. data/rust/src/kernel/expr.rs +203 -86
  379. data/rust/src/kernel/expression_operators/arithmetic.rs +138 -25
  380. data/rust/src/kernel/expression_operators/comparison.rs +87 -2
  381. data/rust/src/kernel/expression_operators/enumeration.rs +106 -22
  382. data/rust/src/kernel/expression_operators/logical.rs +9 -3
  383. data/rust/src/kernel/expression_operators/membership.rs +3 -1
  384. data/rust/src/kernel/expression_operators/mod.rs +34 -21
  385. data/rust/src/kernel/expression_operators/pattern_match.rs +46 -11
  386. data/rust/src/kernel/expression_operators/positional.rs +84 -30
  387. data/rust/src/kernel/expression_operators/presence.rs +52 -13
  388. data/rust/src/kernel/expression_operators/sign_test.rs +3 -1
  389. data/rust/src/kernel/expression_operators/sized.rs +24 -8
  390. data/rust/src/kernel/expression_operators/text.rs +120 -27
  391. data/rust/src/kernel/expression_operators/to_string.rs +10 -4
  392. data/rust/src/kernel/json.rs +244 -148
  393. data/rust/src/kernel/mod.rs +47 -33
  394. data/rust/src/kernel/named_query.rs +63 -15
  395. data/rust/src/kernel/needs.rs +112 -25
  396. data/rust/src/kernel/orchestrate.rs +980 -581
  397. data/rust/src/kernel/orchestrate_guard_tests.rs +538 -140
  398. data/rust/src/kernel/pattern.rs +107 -34
  399. data/rust/src/kernel/present.rs +261 -0
  400. data/rust/src/kernel/query_comparators.rs +118 -41
  401. data/rust/src/kernel/query_ordering.rs +13 -5
  402. data/rust/src/kernel/read_model.rs +479 -181
  403. data/rust/src/kernel/reference_lookup.rs +122 -26
  404. data/rust/src/kernel/repository.rs +293 -66
  405. data/rust/src/kernel/routing.rs +58 -42
  406. data/rust/src/kernel/ruby_float.rs +139 -0
  407. data/rust/src/kernel/vocab/aggregate_dispatch_order.rs +57 -57
  408. data/rust/src/kernel/vocab/entity_dispatch_order.rs +54 -54
  409. data/rust/src/kernel/vocab/field_hint.rs +19 -25
  410. data/rust/src/kernel/vocab/mod.rs +6 -6
  411. data/rust/src/kernel/vocab/query_comparator.rs +23 -24
  412. data/rust/src/kernel/vocab/refusal_template.rs +501 -297
  413. data/rust/src/lib.rs +50 -0
  414. data/rust/src/main.rs +4 -1
  415. data/rust/web/Cargo.toml +36 -0
  416. data/rust/web/clippy.toml +4 -0
  417. metadata +85 -15
  418. data/examples/pizzas/bluebook/hecksagon/mock_stripe_payment_adapter.rb +0 -36
  419. data/lib/hecks/adapters/driven/google_authentication.adapter +0 -3
  420. data/lib/hecks/adapters/driven/google_authentication.rb +0 -99
  421. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +0 -3
  422. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +0 -32
  423. data/lib/hecks/ports/authentication.port +0 -6
  424. data/lib/hecks/ports/authentication.rb +0 -67
  425. data/lib/hecks/ports/persistence/plugins/era/translation/approval_file.rb +0 -255
  426. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +0 -22
  427. data/rust/host/src/approval.rs +0 -652
@@ -12,7 +12,11 @@ pub fn binary(expr: &Expr, ctx: &EvalContext) -> Result<Value, Refusal> {
12
12
  Expr::Sub(l, r) => ("subtraction", "-", l, r),
13
13
  Expr::Mul(l, r) => ("multiplication", "*", l, r),
14
14
  Expr::Div(l, r) => ("division", "/", l, r),
15
- _ => return Err(Refusal::TypeMismatch(format!("arithmetic::binary called with a non-binary node {expr:?} — a router bug"))),
15
+ _ => {
16
+ return Err(Refusal::TypeMismatch(format!(
17
+ "arithmetic::binary called with a non-binary node {expr:?} — a router bug"
18
+ )))
19
+ }
16
20
  };
17
21
  let (left, right) = (eval(l, ctx)?, eval(r, ctx)?);
18
22
  match symbol {
@@ -25,20 +29,49 @@ pub fn binary(expr: &Expr, ctx: &EvalContext) -> Result<Value, Refusal> {
25
29
 
26
30
  // Integer operands use the checked operation; anything else is a Float result that must stay
27
31
  // finite, as `add` does. Ruby promotes to Bignum; this kernel refuses instead.
28
- fn checked(name: &str, symbol: &str, lhs: &Value, rhs: &Value, int_op: fn(i64, i64) -> Option<i64>, float_op: fn(f64, f64) -> f64) -> Result<Value, Refusal> {
32
+ fn checked(
33
+ name: &str,
34
+ symbol: &str,
35
+ lhs: &Value,
36
+ rhs: &Value,
37
+ int_op: fn(i64, i64) -> Option<i64>,
38
+ float_op: fn(f64, f64) -> f64,
39
+ ) -> Result<Value, Refusal> {
29
40
  if let (Value::Int(l), Value::Int(r)) = (lhs, rhs) {
30
- return int_op(*l, *r).map(Value::Int).ok_or_else(|| eval_error(format!("{name} overflowed: {l} {symbol} {r} does not fit in a 64-bit integer")));
41
+ return int_op(*l, *r).map(Value::Int).ok_or_else(|| {
42
+ eval_error(format!(
43
+ "{name} overflowed: {l} {symbol} {r} does not fit in a 64-bit integer"
44
+ ))
45
+ });
31
46
  }
32
47
  let l = require_number(lhs, name)?;
33
48
  let r = require_number(rhs, name)?;
34
- finite(name, symbol, l, r, float_op(l, r))
49
+ finite(name, symbol, (lhs, rhs), float_op(l, r))
35
50
  }
36
51
 
37
- fn finite(name: &str, symbol: &str, l: f64, r: f64, result: f64) -> Result<Value, Refusal> {
52
+ // The operands are quoted as Ruby prints them: an Integer bare, a Float as `Float#to_s`.
53
+ fn finite(
54
+ name: &str,
55
+ symbol: &str,
56
+ operands: (&Value, &Value),
57
+ result: f64,
58
+ ) -> Result<Value, Refusal> {
38
59
  if result.is_finite() {
39
60
  return Ok(Value::Float(result));
40
61
  }
41
- Err(eval_error(format!("{name} overflowed: {l} {symbol} {r} is not a finite number")))
62
+ Err(eval_error(format!(
63
+ "{name} overflowed: {} {symbol} {} is not a finite number",
64
+ quoted(operands.0),
65
+ quoted(operands.1)
66
+ )))
67
+ }
68
+
69
+ fn quoted(v: &Value) -> String {
70
+ match v {
71
+ Value::Int(i) => i.to_string(),
72
+ Value::Float(f) => crate::kernel::ruby_float::to_s(*f),
73
+ other => format!("{other:?}"),
74
+ }
42
75
  }
43
76
 
44
77
  // Integer division floors toward negative infinity, as Ruby's `Integer#/` does (Rust's `/`
@@ -49,21 +82,31 @@ fn divide(lhs: &Value, rhs: &Value) -> Result<Value, Refusal> {
49
82
  return Err(eval_error("divided by 0".to_string()));
50
83
  }
51
84
  if let (Value::Int(l), Value::Int(r)) = (lhs, rhs) {
52
- return floored_div(*l, *r).map(Value::Int).ok_or_else(|| eval_error(format!("division overflowed: {l} / {r} does not fit in a 64-bit integer")));
85
+ return floored_div(*l, *r).map(Value::Int).ok_or_else(|| {
86
+ eval_error(format!(
87
+ "division overflowed: {l} / {r} does not fit in a 64-bit integer"
88
+ ))
89
+ });
53
90
  }
54
91
  let l = require_number(lhs, "division")?;
55
- finite("division", "/", l, r, l / r)
92
+ finite("division", "/", (lhs, rhs), l / r)
56
93
  }
57
94
 
58
95
  // `None` only for `i64::MIN / -1`, the one quotient that does not fit.
59
96
  fn floored_div(l: i64, r: i64) -> Option<i64> {
60
97
  let quotient = l.checked_div(r)?;
61
- Some(if l % r != 0 && ((l < 0) != (r < 0)) { quotient - 1 } else { quotient })
98
+ Some(if l % r != 0 && ((l < 0) != (r < 0)) {
99
+ quotient - 1
100
+ } else {
101
+ quotient
102
+ })
62
103
  }
63
104
 
64
105
  pub fn modulo(expr: &Expr, ctx: &EvalContext) -> Result<Value, Refusal> {
65
106
  let Expr::Modulo { receiver, divisor } = expr else {
66
- return Err(Refusal::TypeMismatch(format!("arithmetic::modulo called with a non-modulo node {expr:?} — a router bug")));
107
+ return Err(Refusal::TypeMismatch(format!(
108
+ "arithmetic::modulo called with a non-modulo node {expr:?} — a router bug"
109
+ )));
67
110
  };
68
111
 
69
112
  let r = require_number(&eval(receiver, ctx)?, "modulo")?.trunc() as i64;
@@ -75,13 +118,17 @@ pub fn modulo(expr: &Expr, ctx: &EvalContext) -> Result<Value, Refusal> {
75
118
  }
76
119
 
77
120
  // Floored like Ruby's `Integer#%` (the result takes the divisor's sign); Rust's `%` truncates.
78
- fn floored_mod(r: i64, d: i64) -> i64 {
121
+ const fn floored_mod(r: i64, d: i64) -> i64 {
79
122
  // `i64::MIN % -1` overflows in Rust's `%`; every integer model answers 0 (C3.3).
80
123
  if d == -1 {
81
124
  return 0;
82
125
  }
83
126
  let raw = r % d;
84
- if raw != 0 && (raw < 0) != (d < 0) { raw + d } else { raw }
127
+ if raw != 0 && (raw < 0) != (d < 0) {
128
+ raw + d
129
+ } else {
130
+ raw
131
+ }
85
132
  }
86
133
 
87
134
  #[cfg(test)]
@@ -102,9 +149,20 @@ mod division_tests {
102
149
 
103
150
  #[test]
104
151
  fn a_money_percentage_rounds_down() {
105
- let product = checked("multiplication", "*", &Value::Int(10801), &Value::Int(50), i64::checked_mul, |a, b| a * b).unwrap();
106
-
107
- assert_eq!(divide(&product, &Value::Int(100)).unwrap(), Value::Int(5400));
152
+ let product = checked(
153
+ "multiplication",
154
+ "*",
155
+ &Value::Int(10801),
156
+ &Value::Int(50),
157
+ i64::checked_mul,
158
+ |a, b| a * b,
159
+ )
160
+ .unwrap();
161
+
162
+ assert_eq!(
163
+ divide(&product, &Value::Int(100)).unwrap(),
164
+ Value::Int(5400)
165
+ );
108
166
  }
109
167
 
110
168
  #[test]
@@ -120,8 +178,24 @@ mod division_tests {
120
178
 
121
179
  #[test]
122
180
  fn overflowing_products_and_differences_are_faults() {
123
- assert!(checked("multiplication", "*", &Value::Int(i64::MAX), &Value::Int(2), i64::checked_mul, |a, b| a * b).is_err());
124
- assert!(checked("subtraction", "-", &Value::Int(i64::MIN), &Value::Int(1), i64::checked_sub, |a, b| a - b).is_err());
181
+ assert!(checked(
182
+ "multiplication",
183
+ "*",
184
+ &Value::Int(i64::MAX),
185
+ &Value::Int(2),
186
+ i64::checked_mul,
187
+ |a, b| a * b
188
+ )
189
+ .is_err());
190
+ assert!(checked(
191
+ "subtraction",
192
+ "-",
193
+ &Value::Int(i64::MIN),
194
+ &Value::Int(1),
195
+ i64::checked_sub,
196
+ |a, b| a - b
197
+ )
198
+ .is_err());
125
199
  }
126
200
  }
127
201
 
@@ -159,6 +233,18 @@ mod float_sum_tests {
159
233
  let result = sum(&Value::Float(1.0e308), &Value::Float(1.0e308));
160
234
  assert!(result.is_err(), "1e308 + 1e308 must fault, not answer inf");
161
235
  }
236
+
237
+ // The fault quotes operands as Ruby prints them, not as Rust's 309-digit expansion.
238
+ #[test]
239
+ fn quotes_the_operands_in_rubys_float_format() {
240
+ let fault = sum(&Value::Float(1.0e308), &Value::Float(1.0e308)).unwrap_err();
241
+ assert_eq!(
242
+ fault,
243
+ crate::kernel::Refusal::Fault(
244
+ "addition overflowed: 1.0e+308 + 1.0e+308 is not a finite number".to_string()
245
+ )
246
+ );
247
+ }
162
248
  }
163
249
 
164
250
  #[cfg(test)]
@@ -173,7 +259,10 @@ mod sum_tests {
173
259
 
174
260
  #[test]
175
261
  fn mixed_int_float_promotes_to_float_like_ruby_coercion() {
176
- assert_eq!(sum(&Value::Int(2), &Value::Float(3.5)).unwrap(), Value::Float(5.5));
262
+ assert_eq!(
263
+ sum(&Value::Int(2), &Value::Float(3.5)).unwrap(),
264
+ Value::Float(5.5)
265
+ );
177
266
  }
178
267
 
179
268
  // Ruby's `Integer#+` promotes to Bignum and never overflows; this kernel has no bignum, so
@@ -181,18 +270,27 @@ mod sum_tests {
181
270
  #[test]
182
271
  fn refuses_cleanly_on_overflow_instead_of_panicking_or_wrapping() {
183
272
  let result = sum(&Value::Int(i64::MAX), &Value::Int(1));
184
- assert!(result.is_err(), "i64::MAX + 1 must refuse, not silently wrap to i64::MIN");
273
+ assert!(
274
+ result.is_err(),
275
+ "i64::MAX + 1 must refuse, not silently wrap to i64::MIN"
276
+ );
185
277
  }
186
278
 
187
279
  #[test]
188
280
  fn refuses_cleanly_on_negative_overflow() {
189
281
  let result = sum(&Value::Int(i64::MIN), &Value::Int(-1));
190
- assert!(result.is_err(), "i64::MIN + -1 must refuse, not silently wrap to i64::MAX");
282
+ assert!(
283
+ result.is_err(),
284
+ "i64::MIN + -1 must refuse, not silently wrap to i64::MAX"
285
+ );
191
286
  }
192
287
 
193
288
  #[test]
194
289
  fn does_not_refuse_right_at_the_boundary() {
195
- assert_eq!(sum(&Value::Int(i64::MAX - 1), &Value::Int(1)).unwrap(), Value::Int(i64::MAX));
290
+ assert_eq!(
291
+ sum(&Value::Int(i64::MAX - 1), &Value::Int(1)).unwrap(),
292
+ Value::Int(i64::MAX)
293
+ );
196
294
  }
197
295
  }
198
296
 
@@ -200,14 +298,22 @@ mod sum_tests {
200
298
  // instead of panicking (debug) or wrapping (release).
201
299
  fn sum(lhs: &Value, rhs: &Value) -> Result<Value, Refusal> {
202
300
  if let (Value::Int(l), Value::Int(r)) = (lhs, rhs) {
203
- return l.checked_add(*r).map(Value::Int).ok_or_else(|| eval_error(format!("addition overflowed: {l} + {r} does not fit in a 64-bit integer")));
301
+ return l.checked_add(*r).map(Value::Int).ok_or_else(|| {
302
+ eval_error(format!(
303
+ "addition overflowed: {l} + {r} does not fit in a 64-bit integer"
304
+ ))
305
+ });
204
306
  }
205
307
  let l = require_number(lhs, "addition")?;
206
308
  let r = require_number(rhs, "addition")?;
207
309
  let sum = l + r;
208
310
  // C3.4 — a non-finite Float sum faults like an Integer overflow.
209
311
  if !sum.is_finite() {
210
- return Err(eval_error(format!("addition overflowed: {l} + {r} is not a finite number")));
312
+ return Err(eval_error(format!(
313
+ "addition overflowed: {} + {} is not a finite number",
314
+ quoted(lhs),
315
+ quoted(rhs)
316
+ )));
211
317
  }
212
318
  Ok(Value::Float(sum))
213
319
  }
@@ -236,14 +342,21 @@ mod path_operand_tests {
236
342
  struct Record(Charge);
237
343
  impl Fielded for Record {
238
344
  fn field(&self, name: &str) -> Option<Field<'_>> {
239
- (name == "charged").then(|| Field::Nested(&self.0))
345
+ (name == "charged").then_some(Field::Nested(&self.0))
240
346
  }
241
347
  }
242
348
 
243
349
  fn percent_of(path: &'static str) -> Value {
244
350
  let times = Expr::Mul(Box::new(Expr::Lookup(path)), Box::new(Expr::Int(50)));
245
351
  let expr = Expr::Div(Box::new(times), Box::new(Expr::Int(100)));
246
- interpret(&expr, &EvalContext { args: &NoFields, instance: &Record(Charge(10801)) }).expect("evaluates")
352
+ interpret(
353
+ &expr,
354
+ &EvalContext {
355
+ args: &NoFields,
356
+ instance: &Record(Charge(10801)),
357
+ },
358
+ )
359
+ .expect("evaluates")
247
360
  }
248
361
 
249
362
  #[test]
@@ -14,7 +14,9 @@ pub struct Comparison {
14
14
 
15
15
  pub fn interpret(expr: &Expr, ctx: &EvalContext) -> Result<Value, Refusal> {
16
16
  let Expr::Compare { op, left, right } = expr else {
17
- return Err(Refusal::TypeMismatch(format!("comparison::interpret called with a non-comparison node {expr:?} — a router bug")));
17
+ return Err(Refusal::TypeMismatch(format!(
18
+ "comparison::interpret called with a non-comparison node {expr:?} — a router bug"
19
+ )));
18
20
  };
19
21
 
20
22
  let l = eval(left, ctx)?;
@@ -34,15 +36,98 @@ fn less_than(lhs: &Value, rhs: &Value) -> Result<bool, Refusal> {
34
36
  (Some(l), Some(r)) => Ok(l < r),
35
37
  _ => match (lhs, rhs) {
36
38
  (Value::Str(l), Value::Str(r)) => Ok(l < r),
37
- _ => Err(eval_error(format!("comparison of {lhs:?} with {rhs:?} failed"))),
39
+ _ => Err(eval_error(format!(
40
+ "comparison of {} with {} failed",
41
+ ruby_class_of(lhs),
42
+ describe(rhs)
43
+ ))),
38
44
  },
39
45
  }
40
46
  }
41
47
 
48
+ /// Names `value`'s class the way Ruby's `Evaluator.class_of` does: `nil` for nil, else the
49
+ /// Ruby class name (`Integer`, `Float`, `String`, `TrueClass`, `FalseClass`, `Array`).
50
+ const fn ruby_class_of(value: &Value) -> &'static str {
51
+ match value {
52
+ Value::Nil => "nil",
53
+ Value::Int(_) => "Integer",
54
+ Value::Float(_) => "Float",
55
+ Value::Str(_) => "String",
56
+ Value::Bool(true) => "TrueClass",
57
+ Value::Bool(false) => "FalseClass",
58
+ Value::List(_) | Value::Array(_) => "Array",
59
+ }
60
+ }
61
+
62
+ /// Renders `value` the way Ruby's `Rendering.describe` does: `nil`, JSON for an array, else
63
+ /// `inspect`. A length-only `List` carries no elements, so it renders as an empty array.
64
+ fn describe(value: &Value) -> String {
65
+ match value {
66
+ Value::Nil => "nil".to_string(),
67
+ Value::Int(n) => n.to_string(),
68
+ Value::Bool(b) => b.to_string(),
69
+ Value::Float(f) => format!("{f:?}"),
70
+ Value::Str(s) => format!("{s:?}"),
71
+ Value::List(_) => "[]".to_string(),
72
+ Value::Array(items) => {
73
+ let parts: Vec<String> = items.iter().map(describe_json).collect();
74
+ format!("[{}]", parts.join(","))
75
+ }
76
+ }
77
+ }
78
+
79
+ /// An array element as JSON: `null` for nil, else the same text `describe` gives.
80
+ fn describe_json(value: &Value) -> String {
81
+ match value {
82
+ Value::Nil => "null".to_string(),
83
+ other => describe(other),
84
+ }
85
+ }
86
+
42
87
  /// `pub(crate)` so `membership.rs` reuses the numeric-coerced equality for array haystacks.
88
+ #[allow(clippy::float_cmp)] // reason: Ruby's `==` on numerics is exact IEEE equality, not a margin.
43
89
  pub(crate) fn values_equal(lhs: &Value, rhs: &Value) -> bool {
44
90
  match (scalar::numeric(lhs), scalar::numeric(rhs)) {
45
91
  (Some(l), Some(r)) => l == r,
46
92
  _ => lhs == rhs,
47
93
  }
48
94
  }
95
+
96
+ #[cfg(test)]
97
+ mod tests {
98
+ use super::*;
99
+
100
+ fn failure(lhs: &Value, rhs: &Value) -> String {
101
+ match less_than(lhs, rhs) {
102
+ Err(Refusal::Fault(message)) => message,
103
+ other => format!("expected a Fault, got {other:?}"),
104
+ }
105
+ }
106
+
107
+ #[test]
108
+ fn ordering_against_nil_reads_like_ruby() {
109
+ assert_eq!(
110
+ failure(&Value::Int(0), &Value::Nil),
111
+ "comparison of Integer with nil failed"
112
+ );
113
+ }
114
+
115
+ #[test]
116
+ fn mixed_kinds_name_the_ruby_class_then_inspect_the_other_side() {
117
+ assert_eq!(
118
+ failure(&Value::Str("a".into()), &Value::Int(5)),
119
+ "comparison of String with 5 failed"
120
+ );
121
+ assert_eq!(
122
+ failure(&Value::Nil, &Value::Str("a".into())),
123
+ "comparison of nil with \"a\" failed"
124
+ );
125
+ assert_eq!(
126
+ failure(
127
+ &Value::Bool(true),
128
+ &Value::Array(vec![Value::Int(1), Value::Nil])
129
+ ),
130
+ "comparison of TrueClass with [1,null] failed"
131
+ );
132
+ }
133
+ }
@@ -3,17 +3,31 @@
3
3
  // Every element is evaluated before aggregating, not short-circuited, so an error on any
4
4
  // element surfaces exactly as it does in Ruby.
5
5
  use crate::kernel::attribute_shapes::composite;
6
- use crate::kernel::expr::{eval_error, interpret as eval, lookup_items, BlockMode, Bound, EvalContext, Expr, Field, Value};
6
+ use crate::kernel::expr::{
7
+ eval_error, interpret as eval, lookup_items, BlockMode, Bound, EvalContext, Expr, Field, Value,
8
+ };
7
9
  use crate::kernel::Refusal;
8
10
 
9
11
  pub fn interpret(expr: &Expr, ctx: &EvalContext) -> Result<Value, Refusal> {
10
12
  match expr {
11
- Expr::BlockPredicate { mode, receiver, param, predicate } => {
13
+ Expr::BlockPredicate {
14
+ mode,
15
+ receiver,
16
+ param,
17
+ predicate,
18
+ } => {
12
19
  let items = elements(receiver, ctx, mode.ruby_name())?;
13
20
  let mut outcomes = Vec::with_capacity(items.len());
14
21
  for item in items {
15
- let bound = Bound { name: param, value: item, rest: ctx.args };
16
- let inner = EvalContext { args: &bound, instance: ctx.instance };
22
+ let bound = Bound {
23
+ name: param,
24
+ value: item,
25
+ rest: ctx.args,
26
+ };
27
+ let inner = EvalContext {
28
+ args: &bound,
29
+ instance: ctx.instance,
30
+ };
17
31
  outcomes.push(eval(predicate, &inner)?.truthy());
18
32
  }
19
33
  Ok(Value::Bool(match mode {
@@ -22,12 +36,24 @@ pub fn interpret(expr: &Expr, ctx: &EvalContext) -> Result<Value, Refusal> {
22
36
  BlockMode::None => !outcomes.iter().any(|b| *b),
23
37
  }))
24
38
  }
25
- Expr::Find { receiver, param, predicate, path } => {
39
+ Expr::Find {
40
+ receiver,
41
+ param,
42
+ predicate,
43
+ path,
44
+ } => {
26
45
  let items = elements(receiver, ctx, "find")?;
27
46
  for item in items {
28
47
  let accepted = {
29
- let bound = Bound { name: param, value: borrow(&item), rest: ctx.args };
30
- let inner = EvalContext { args: &bound, instance: ctx.instance };
48
+ let bound = Bound {
49
+ name: param,
50
+ value: borrow(&item),
51
+ rest: ctx.args,
52
+ };
53
+ let inner = EvalContext {
54
+ args: &bound,
55
+ instance: ctx.instance,
56
+ };
31
57
  eval(predicate, &inner)?.truthy()
32
58
  };
33
59
  if accepted {
@@ -37,12 +63,18 @@ pub fn interpret(expr: &Expr, ctx: &EvalContext) -> Result<Value, Refusal> {
37
63
  // Nothing matched, and a path walked from nothing is nil too.
38
64
  Ok(Value::Nil)
39
65
  }
40
- _ => Err(Refusal::TypeMismatch(format!("enumeration::interpret called with a non-enumeration node {expr:?} — a router bug"))),
66
+ _ => Err(Refusal::TypeMismatch(format!(
67
+ "enumeration::interpret called with a non-enumeration node {expr:?} — a router bug"
68
+ ))),
41
69
  }
42
70
  }
43
71
 
44
72
  /// Only a `Lookup` names a list field; any other receiver is evaluated just to word the error.
45
- fn elements<'a>(receiver: &Expr, ctx: &EvalContext<'a>, op: &str) -> Result<Vec<Field<'a>>, Refusal> {
73
+ fn elements<'a>(
74
+ receiver: &Expr,
75
+ ctx: &EvalContext<'a>,
76
+ op: &str,
77
+ ) -> Result<Vec<Field<'a>>, Refusal> {
46
78
  match receiver {
47
79
  Expr::Lookup(path) => lookup_items(path, ctx, op),
48
80
  other => {
@@ -64,7 +96,7 @@ fn project(item: Field<'_>, path: &[&str]) -> Result<Value, Refusal> {
64
96
  let mut current = item;
65
97
  let rendered = path.join(".");
66
98
  for seg in path {
67
- current = composite::step(current, seg, "find")?;
99
+ current = composite::step(&current, seg, "find")?;
68
100
  }
69
101
  composite::finish(current, &format!("find {{ }}.{rendered}"))
70
102
  }
@@ -106,7 +138,12 @@ mod tests {
106
138
  fn items(&self, name: &str) -> Option<Vec<Field<'_>>> {
107
139
  match name {
108
140
  "seats" => Some(self.seats.iter().map(|s| Field::Nested(s)).collect()),
109
- "tags" => Some(self.tags.iter().map(|t| Field::Value(Value::Str(t.clone()))).collect()),
141
+ "tags" => Some(
142
+ self.tags
143
+ .iter()
144
+ .map(|t| Field::Value(Value::Str(t.clone())))
145
+ .collect(),
146
+ ),
110
147
  _ => None,
111
148
  }
112
149
  }
@@ -114,20 +151,44 @@ mod tests {
114
151
 
115
152
  fn roster() -> Roster {
116
153
  Roster {
117
- seats: vec![Seat { number: 1, taken: true }, Seat { number: 2, taken: false }, Seat { number: 3, taken: true }],
118
- tags: vec!["a".to_string(), "".to_string()],
154
+ seats: vec![
155
+ Seat {
156
+ number: 1,
157
+ taken: true,
158
+ },
159
+ Seat {
160
+ number: 2,
161
+ taken: false,
162
+ },
163
+ Seat {
164
+ number: 3,
165
+ taken: true,
166
+ },
167
+ ],
168
+ tags: vec!["a".to_string(), String::new()],
119
169
  number: 99,
120
170
  }
121
171
  }
122
172
 
123
- const EQ: Comparison = Comparison { less_than: false, equal: true, negated: false };
173
+ const EQ: Comparison = Comparison {
174
+ less_than: false,
175
+ equal: true,
176
+ negated: false,
177
+ };
124
178
 
125
179
  fn eq(left: Expr, right: Expr) -> Expr {
126
- Expr::Compare { op: EQ, left: Box::new(left), right: Box::new(right) }
180
+ Expr::Compare {
181
+ op: EQ,
182
+ left: Box::new(left),
183
+ right: Box::new(right),
184
+ }
127
185
  }
128
186
 
129
187
  fn run(expr: &Expr, instance: &dyn Fielded) -> Value {
130
- let ctx = EvalContext { args: &NoFields, instance };
188
+ let ctx = EvalContext {
189
+ args: &NoFields,
190
+ instance,
191
+ };
131
192
  eval(expr, &ctx).expect("evaluates")
132
193
  }
133
194
 
@@ -158,7 +219,11 @@ mod tests {
158
219
 
159
220
  #[test]
160
221
  fn an_empty_list_answers_like_ruby() {
161
- let empty = Roster { seats: vec![], tags: vec![], number: 0 };
222
+ let empty = Roster {
223
+ seats: vec![],
224
+ tags: vec![],
225
+ number: 0,
226
+ };
162
227
  let over = |mode| Expr::BlockPredicate {
163
228
  mode,
164
229
  receiver: Box::new(Expr::Lookup("seats")),
@@ -193,13 +258,19 @@ mod tests {
193
258
  mode: BlockMode::None,
194
259
  receiver: Box::new(Expr::Lookup("seats")),
195
260
  param: "o",
196
- predicate: Box::new(eq(Expr::Lookup("o.number"), Expr::Add(Box::new(Expr::Lookup("s.number")), Box::new(Expr::Int(1))))),
261
+ predicate: Box::new(eq(
262
+ Expr::Lookup("o.number"),
263
+ Expr::Add(Box::new(Expr::Lookup("s.number")), Box::new(Expr::Int(1))),
264
+ )),
197
265
  };
198
266
  let outer = Expr::BlockPredicate {
199
267
  mode: BlockMode::Any,
200
268
  receiver: Box::new(Expr::Lookup("seats")),
201
269
  param: "s",
202
- predicate: Box::new(Expr::And(Box::new(eq(Expr::Lookup("s.taken"), Expr::Bool(false))), Box::new(inner))),
270
+ predicate: Box::new(Expr::And(
271
+ Box::new(eq(Expr::Lookup("s.taken"), Expr::Bool(false))),
272
+ Box::new(inner),
273
+ )),
203
274
  };
204
275
  assert_eq!(run(&outer, &roster()), Value::Bool(false));
205
276
  }
@@ -213,7 +284,14 @@ mod tests {
213
284
  path: &["number"],
214
285
  };
215
286
  assert_eq!(run(&free_number(false), &roster()), Value::Int(2));
216
- let none_free = Roster { seats: vec![Seat { number: 1, taken: true }], tags: vec![], number: 0 };
287
+ let none_free = Roster {
288
+ seats: vec![Seat {
289
+ number: 1,
290
+ taken: true,
291
+ }],
292
+ tags: vec![],
293
+ number: 0,
294
+ };
217
295
  assert_eq!(run(&free_number(false), &none_free), Value::Nil);
218
296
  }
219
297
 
@@ -225,8 +303,14 @@ mod tests {
225
303
  param: "n",
226
304
  predicate: Box::new(Expr::Bool(true)),
227
305
  };
228
- let ctx = EvalContext { args: &NoFields, instance: &roster() };
306
+ let ctx = EvalContext {
307
+ args: &NoFields,
308
+ instance: &roster(),
309
+ };
229
310
  let err = eval(&wrong, &ctx).unwrap_err();
230
- assert!(format!("{err:?}").contains("any? expects a list"), "{err:?}");
311
+ assert!(
312
+ format!("{err:?}").contains("any? expects a list"),
313
+ "{err:?}"
314
+ );
231
315
  }
232
316
  }
@@ -5,9 +5,15 @@ use crate::kernel::Refusal;
5
5
 
6
6
  pub fn interpret(expr: &Expr, ctx: &EvalContext) -> Result<Value, Refusal> {
7
7
  match expr {
8
- Expr::Or(l, r) => Ok(Value::Bool(eval(l, ctx)?.truthy() || eval(r, ctx)?.truthy())),
9
- Expr::And(l, r) => Ok(Value::Bool(eval(l, ctx)?.truthy() && eval(r, ctx)?.truthy())),
8
+ Expr::Or(l, r) => Ok(Value::Bool(
9
+ eval(l, ctx)?.truthy() || eval(r, ctx)?.truthy(),
10
+ )),
11
+ Expr::And(l, r) => Ok(Value::Bool(
12
+ eval(l, ctx)?.truthy() && eval(r, ctx)?.truthy(),
13
+ )),
10
14
  Expr::Not(n) => Ok(Value::Bool(!eval(n, ctx)?.truthy())),
11
- _ => Err(Refusal::TypeMismatch(format!("logical::interpret called with a non-logical node {expr:?} — a router bug"))),
15
+ _ => Err(Refusal::TypeMismatch(format!(
16
+ "logical::interpret called with a non-logical node {expr:?} — a router bug"
17
+ ))),
12
18
  }
13
19
  }
@@ -6,7 +6,9 @@ use crate::kernel::Refusal;
6
6
 
7
7
  pub fn interpret(expr: &Expr, ctx: &EvalContext) -> Result<Value, Refusal> {
8
8
  let Expr::Include { haystack, needle } = expr else {
9
- return Err(Refusal::TypeMismatch(format!("membership::interpret called with a non-membership node {expr:?} — a router bug")));
9
+ return Err(Refusal::TypeMismatch(format!(
10
+ "membership::interpret called with a non-membership node {expr:?} — a router bug"
11
+ )));
10
12
  };
11
13
 
12
14
  match (eval(haystack, ctx)?, eval(needle, ctx)?) {