rigortype 0.1.19 → 0.2.1

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 (197) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -6
  3. data/data/core_overlay/numeric.rbs +33 -0
  4. data/data/core_overlay/pathname.rbs +25 -0
  5. data/data/core_overlay/string_scanner.rbs +28 -0
  6. data/data/gem_overlay/activesupport/core_ext.rbs +473 -0
  7. data/data/vendored_gem_sigs/ast/ast.rbs +130 -0
  8. data/data/vendored_gem_sigs/bcrypt/bcrypt.rbs +47 -0
  9. data/data/vendored_gem_sigs/bundler/bundler.rbs +238 -0
  10. data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +34 -0
  11. data/data/vendored_gem_sigs/did_you_mean/did_you_mean_extras.rbs +34 -0
  12. data/data/vendored_gem_sigs/idn-ruby/idn.rbs +54 -0
  13. data/data/vendored_gem_sigs/mysql2/client.rbs +55 -0
  14. data/data/vendored_gem_sigs/mysql2/error.rbs +5 -0
  15. data/data/vendored_gem_sigs/mysql2/result.rbs +31 -0
  16. data/data/vendored_gem_sigs/mysql2/statement.rbs +5 -0
  17. data/data/vendored_gem_sigs/nokogiri/nokogiri.rbs +2332 -0
  18. data/data/vendored_gem_sigs/nokogiri/nokogiri_html5.rbs +47 -0
  19. data/data/vendored_gem_sigs/pg/pg.rbs +212 -0
  20. data/data/vendored_gem_sigs/prism/prism_supplement.rbs +44 -0
  21. data/data/vendored_gem_sigs/redis/errors.rbs +50 -0
  22. data/data/vendored_gem_sigs/redis/future.rbs +5 -0
  23. data/data/vendored_gem_sigs/redis/redis.rbs +348 -0
  24. data/data/vendored_gem_sigs/redis/redis_extras.rbs +130 -0
  25. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +226 -0
  26. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -23
  27. data/lib/rigor/analysis/check_rules/rule_walk.rb +3 -21
  28. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
  29. data/lib/rigor/analysis/check_rules.rb +492 -71
  30. data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
  31. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
  32. data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
  33. data/lib/rigor/analysis/fact_store.rb +5 -4
  34. data/lib/rigor/analysis/rule_catalog.rb +153 -6
  35. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +17 -17
  36. data/lib/rigor/analysis/runner/project_pre_passes.rb +9 -8
  37. data/lib/rigor/analysis/runner.rb +17 -6
  38. data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
  39. data/lib/rigor/analysis/worker_session.rb +10 -14
  40. data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
  41. data/lib/rigor/cache/store.rb +5 -3
  42. data/lib/rigor/cli/annotate_command.rb +28 -7
  43. data/lib/rigor/cli/baseline_command.rb +4 -3
  44. data/lib/rigor/cli/check_command.rb +138 -16
  45. data/lib/rigor/cli/coverage_command.rb +138 -31
  46. data/lib/rigor/cli/coverage_mutation.rb +149 -0
  47. data/lib/rigor/cli/coverage_scan.rb +57 -0
  48. data/lib/rigor/cli/explain_command.rb +2 -0
  49. data/lib/rigor/cli/fused_protection_renderer.rb +67 -0
  50. data/lib/rigor/cli/fused_protection_report.rb +76 -0
  51. data/lib/rigor/cli/lsp_command.rb +3 -7
  52. data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
  53. data/lib/rigor/cli/mutation_protection_report.rb +73 -0
  54. data/lib/rigor/cli/options.rb +9 -0
  55. data/lib/rigor/cli/plugins_command.rb +2 -1
  56. data/lib/rigor/cli/protection_renderer.rb +63 -0
  57. data/lib/rigor/cli/protection_report.rb +68 -0
  58. data/lib/rigor/cli/sig_gen_command.rb +2 -1
  59. data/lib/rigor/cli/trace_command.rb +2 -1
  60. data/lib/rigor/cli/triage_command.rb +2 -1
  61. data/lib/rigor/cli/type_of_command.rb +1 -1
  62. data/lib/rigor/cli/type_scan_command.rb +2 -1
  63. data/lib/rigor/cli.rb +3 -2
  64. data/lib/rigor/config_audit.rb +152 -0
  65. data/lib/rigor/configuration/dependencies.rb +2 -4
  66. data/lib/rigor/configuration.rb +57 -7
  67. data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
  68. data/lib/rigor/environment/class_registry.rb +4 -3
  69. data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
  70. data/lib/rigor/environment/lockfile_resolver.rb +1 -1
  71. data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
  72. data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
  73. data/lib/rigor/environment/rbs_loader.rb +76 -5
  74. data/lib/rigor/environment.rb +66 -8
  75. data/lib/rigor/flow_contribution/fact.rb +1 -1
  76. data/lib/rigor/flow_contribution.rb +3 -5
  77. data/lib/rigor/inference/acceptance.rb +17 -9
  78. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  79. data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
  80. data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
  81. data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
  82. data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
  83. data/lib/rigor/inference/expression_typer.rb +20 -28
  84. data/lib/rigor/inference/hkt_body.rb +8 -11
  85. data/lib/rigor/inference/hkt_body_parser.rb +10 -12
  86. data/lib/rigor/inference/hkt_registry.rb +10 -11
  87. data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
  88. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +169 -24
  89. data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
  90. data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
  91. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
  92. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
  93. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
  94. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
  95. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
  96. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +90 -15
  97. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
  98. data/lib/rigor/inference/method_dispatcher.rb +40 -48
  99. data/lib/rigor/inference/mutation_widening.rb +5 -11
  100. data/lib/rigor/inference/narrowing.rb +14 -16
  101. data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
  102. data/lib/rigor/inference/project_patched_methods.rb +4 -7
  103. data/lib/rigor/inference/project_patched_scanner.rb +2 -13
  104. data/lib/rigor/inference/protection_scanner.rb +86 -0
  105. data/lib/rigor/inference/scope_indexer.rb +129 -55
  106. data/lib/rigor/inference/statement_evaluator.rb +271 -114
  107. data/lib/rigor/inference/struct_fold_safety.rb +181 -0
  108. data/lib/rigor/inference/synthetic_method.rb +7 -7
  109. data/lib/rigor/language_server/completion_provider.rb +6 -12
  110. data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
  111. data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
  112. data/lib/rigor/language_server/hover_provider.rb +2 -3
  113. data/lib/rigor/language_server/hover_renderer.rb +2 -11
  114. data/lib/rigor/language_server/server.rb +9 -17
  115. data/lib/rigor/language_server.rb +4 -5
  116. data/lib/rigor/plugin/base.rb +10 -8
  117. data/lib/rigor/plugin/macro/block_as_method.rb +3 -4
  118. data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
  119. data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
  120. data/lib/rigor/plugin/macro.rb +4 -5
  121. data/lib/rigor/plugin/manifest.rb +45 -66
  122. data/lib/rigor/plugin/registry.rb +6 -7
  123. data/lib/rigor/plugin/type_node_resolver.rb +6 -8
  124. data/lib/rigor/protection/diagnostic_oracle.rb +51 -0
  125. data/lib/rigor/protection/mutation_scanner.rb +180 -0
  126. data/lib/rigor/protection/mutator.rb +267 -0
  127. data/lib/rigor/protection/test_suite_oracle.rb +68 -0
  128. data/lib/rigor/rbs_extended.rb +24 -36
  129. data/lib/rigor/reflection.rb +4 -7
  130. data/lib/rigor/scope/discovery_index.rb +14 -2
  131. data/lib/rigor/scope.rb +54 -11
  132. data/lib/rigor/sig_gen/observed_call.rb +3 -3
  133. data/lib/rigor/sig_gen/writer.rb +40 -2
  134. data/lib/rigor/signature_path_audit.rb +92 -0
  135. data/lib/rigor/source/constant_path.rb +62 -0
  136. data/lib/rigor/source.rb +1 -0
  137. data/lib/rigor/type/bound_method.rb +2 -11
  138. data/lib/rigor/type/combinator.rb +16 -3
  139. data/lib/rigor/type/constant.rb +2 -11
  140. data/lib/rigor/type/data_class.rb +2 -11
  141. data/lib/rigor/type/data_instance.rb +2 -11
  142. data/lib/rigor/type/hash_shape.rb +2 -11
  143. data/lib/rigor/type/integer_range.rb +2 -11
  144. data/lib/rigor/type/intersection.rb +2 -11
  145. data/lib/rigor/type/nominal.rb +2 -11
  146. data/lib/rigor/type/plain_lattice.rb +37 -0
  147. data/lib/rigor/type/refined.rb +72 -13
  148. data/lib/rigor/type/singleton.rb +2 -11
  149. data/lib/rigor/type/struct_class.rb +75 -0
  150. data/lib/rigor/type/struct_instance.rb +93 -0
  151. data/lib/rigor/type/tuple.rb +5 -15
  152. data/lib/rigor/type.rb +2 -0
  153. data/lib/rigor/version.rb +1 -1
  154. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  155. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
  156. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +3 -3
  157. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
  158. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
  159. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +7 -10
  160. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
  161. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
  162. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +6 -8
  163. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
  164. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -2
  165. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
  166. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
  167. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
  168. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
  169. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
  170. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
  171. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
  172. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +7 -9
  173. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
  174. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
  175. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +3 -3
  176. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
  177. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
  178. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
  179. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +1 -1
  180. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
  181. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
  182. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +5 -5
  183. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -4
  184. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
  185. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  186. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +19 -14
  187. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
  188. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
  189. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
  190. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
  191. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
  192. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
  193. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
  194. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +28 -41
  195. data/sig/rigor/scope.rbs +9 -1
  196. data/sig/rigor/type.rbs +36 -1
  197. metadata +49 -1
@@ -249,6 +249,12 @@ module Rigor
249
249
  # anonymous local-bound form projects to `Data` itself).
250
250
  accepts(self_type, project_data_instance_to_nominal(other_type), mode: mode)
251
251
  .with_reason("projected DataInstance to Nominal[#{other_type.class_name || 'Data'}]")
252
+ when Type::StructInstance
253
+ # ADR-48 Struct follow-up: same projection as DataInstance — a
254
+ # class-tagged Struct value is exactly one value of its tagging
255
+ # class (the anonymous form projects to `Struct` itself).
256
+ accepts(self_type, project_struct_instance_to_nominal(other_type), mode: mode)
257
+ .with_reason("projected StructInstance to Nominal[#{other_type.class_name || 'Struct'}]")
252
258
  when Type::Difference, Type::Refined
253
259
  # A refinement carrier's value set is a subset of its
254
260
  # base. So if `self` (Nominal) accepts the base, it
@@ -386,6 +392,10 @@ module Rigor
386
392
  Type::Combinator.nominal_of(instance.class_name || "Data")
387
393
  end
388
394
 
395
+ def project_struct_instance_to_nominal(instance)
396
+ Type::Combinator.nominal_of(instance.class_name || "Struct")
397
+ end
398
+
389
399
  def project_hash_shape_to_nominal(shape)
390
400
  return Type::Combinator.nominal_of(Hash) if shape.pairs.empty?
391
401
 
@@ -822,15 +832,13 @@ module Rigor
822
832
  Type::AcceptsResult.no(mode: mode, reasons: reason)
823
833
  end
824
834
 
825
- # Slice 4 phase 2c uses Ruby's actual class hierarchy to answer
826
- # "is D a subclass of C?". This works for any class loadable
827
- # through Object.const_get -- core, stdlib, and live application
828
- # classes. When either name fails to resolve we surface "maybe":
829
- # the caller (overload selector) treats yes/maybe identically,
830
- # so the conservative answer keeps overload coverage intact.
831
- # Slice 5 will replace this with an RBS-driven hierarchy lookup
832
- # so ahead-of-time type checking no longer relies on Ruby
833
- # loading the application classes.
835
+ # Uses Ruby's actual class hierarchy via Object.const_get to answer
836
+ # "is D a subclass of C?" for core, stdlib, and application classes.
837
+ # When either name fails to resolve we surface "maybe": the caller
838
+ # (overload selector) treats yes/maybe identically, so the conservative
839
+ # answer keeps overload coverage intact. RbsHierarchy exists but this
840
+ # path does not yet consult it; migration to an RBS-driven lookup
841
+ # is deferred.
834
842
  def class_subtype_result(target_name:, actual_name:, mode:, kind:)
835
843
  return Type::AcceptsResult.yes(mode: mode, reasons: "exact name match") if target_name == actual_name
836
844
 
@@ -183,9 +183,8 @@ module Rigor
183
183
  # `|*rest|` binds an Array of the leftover positional arguments.
184
184
  # The expected-types array is per-position, not per-rest; we
185
185
  # cannot reliably pick a single element type for rest, so we
186
- # default to `Array[Dynamic[Top]]`. Slice C sub-phase 2 may
187
- # tighten this when the receiving method's RBS rest type is
188
- # available.
186
+ # default to `Array[Dynamic[Top]]`. Element-type precision for
187
+ # rest parameters is deferred (demand-gated).
189
188
  def bind_rest(params_node, bindings)
190
189
  rest = params_node.rest
191
190
  return unless rest.respond_to?(:name) && rest&.name
@@ -11,8 +11,8 @@ module Rigor
11
11
  # catalog is NOT routed through
12
12
  # `MethodDispatcher::ConstantFolding::CATALOG_BY_CLASS`
13
13
  # (which dispatches on the receiver's concrete class).
14
- # The data is consumed by future include-aware lookup —
15
- # see `docs/CURRENT_WORK.md` for the planned slice.
14
+ # The data is wired into `MODULE_CATALOGS` in
15
+ # `MethodDispatcher::ConstantFolding` (ancestor-chain lookup).
16
16
  COMPARABLE_CATALOG = MethodCatalog.for_topic(
17
17
  "comparable",
18
18
  mutating_selectors: {
@@ -11,8 +11,8 @@ module Rigor
11
11
  # catalog is NOT routed through
12
12
  # `MethodDispatcher::ConstantFolding::CATALOG_BY_CLASS`
13
13
  # (which dispatches on the receiver's concrete class).
14
- # The data is consumed by future include-aware lookup —
15
- # see `docs/CURRENT_WORK.md` for the planned slice.
14
+ # The data is wired into `MODULE_CATALOGS` in
15
+ # `MethodDispatcher::ConstantFolding` (ancestor-chain lookup).
16
16
  ENUMERABLE_CATALOG = MethodCatalog.for_topic(
17
17
  "enumerable",
18
18
  mutating_selectors: {
@@ -27,6 +27,24 @@ module Rigor
27
27
  FOLDABLE_PURITIES = Set["leaf", "trivial", "leaf_when_numeric"].freeze
28
28
  EMPTY_CATALOG = { "classes" => {} }.freeze
29
29
 
30
+ # Selectors that are classified `:leaf` by the C-body analysis
31
+ # (they read no global mutable state in the C sense) but whose
32
+ # result is NOT reproducible across Ruby processes, so they must
33
+ # never be folded into a `Constant`:
34
+ #
35
+ # - `hash` — every core `#hash` (`String`/`Symbol`/`Integer`/
36
+ # `Float`/…) is salted with a per-process SipHash seed, so
37
+ # `"x".hash` differs in every process. Folding bakes one
38
+ # process's value into the type and the on-disk cache.
39
+ # - `object_id` / `__id__` — identity-allocated per process.
40
+ #
41
+ # This is a UNIVERSAL block (across every catalogued class)
42
+ # because `hash` / `object_id` are `Object`-level and present on
43
+ # every receiver; a per-class blocklist would silently miss a
44
+ # class. The deterministic siblings (`inspect`, `to_s`) are
45
+ # unaffected.
46
+ NON_REPRODUCIBLE_SELECTORS = Set[:hash, :object_id, :__id__].freeze
47
+
30
48
  # Shared root for the offline-generated catalogues. Resolving it
31
49
  # here keeps the repo-relative `../../../../` hop in one place
32
50
  # instead of copying it into every per-topic loader.
@@ -59,6 +77,7 @@ module Rigor
59
77
 
60
78
  def safe_for_folding?(class_name, selector, kind: :instance)
61
79
  class_name_str = class_name.to_s
80
+ return false if NON_REPRODUCIBLE_SELECTORS.include?(selector.to_sym)
62
81
  return false if blocked?(class_name_str, selector)
63
82
 
64
83
  entry = method_entry(class_name_str, selector, kind: kind)
@@ -22,7 +22,15 @@ module Rigor
22
22
  :replace, :initialize, :initialize_copy, :clear, :<<, :concat, :insert,
23
23
  :prepend, :force_encoding, :encode, :scrub, :unicode_normalize, :"[]=",
24
24
  :upto, :each_byte, :each_char, :each_codepoint,
25
- :each_grapheme_cluster, :each_line, :bytesplice
25
+ :each_grapheme_cluster, :each_line, :bytesplice,
26
+ # `crypt` is not a mutator but is blocked from folding for the
27
+ # same "do not bake a non-pure result into a Constant" reason:
28
+ # `rb_str_crypt` delegates to the platform `crypt(3)`, whose
29
+ # output (algorithm and digest) varies by libc / OS, so
30
+ # `"x".crypt("ab")` is not deterministic across the platforms
31
+ # an analyzed project may target. The catalog classifies it
32
+ # `:leaf` from its C body; this entry overrides that.
33
+ :crypt
26
34
  ],
27
35
  "Symbol" => Set[
28
36
  # Symbol is immutable in Ruby; the classifier mis-flags
@@ -4,6 +4,7 @@ require "prism"
4
4
 
5
5
  require_relative "../type"
6
6
  require_relative "../ast"
7
+ require_relative "../source/constant_path"
7
8
  require_relative "../analysis/self_call_resolution_recorder"
8
9
  require_relative "block_parameter_binder"
9
10
  require_relative "body_fixpoint"
@@ -14,6 +15,7 @@ require_relative "indexed_narrowing"
14
15
  require_relative "macro_block_self_type"
15
16
  require_relative "method_dispatcher"
16
17
  require_relative "narrowing"
18
+ require_relative "struct_fold_safety"
17
19
 
18
20
  module Rigor
19
21
  module Inference
@@ -323,12 +325,7 @@ module Rigor
323
325
  dynamic_top
324
326
  end
325
327
 
326
- # Recognised value-bearing position the Slice 2 engine does not yet
327
- # narrow: self, instance/class/global variable reads, block bodies.
328
- # Slice 3+ refines these in place; for now we acknowledge the node
329
- # class so the coverage scanner stops flagging it without recording
330
- # a fail-soft event for every occurrence.
331
- # Slice A-engine. `Prism::SelfNode` resolves to the scope's
328
+ # `Prism::SelfNode` resolves to the scope's
332
329
  # `self_type` when one has been injected (by
333
330
  # `StatementEvaluator` at class-body and method-body
334
331
  # boundaries) or `Dynamic[Top]` at the top level. Class-body
@@ -412,7 +409,7 @@ module Rigor
412
409
  end
413
410
 
414
411
  def type_of_constant_path(node)
415
- full_name = build_constant_path_name(node)
412
+ full_name = Source::ConstantPath.qualified_name_or_nil(node)
416
413
  return fallback_for(node, family: :prism) if full_name.nil?
417
414
 
418
415
  resolve_constant_name(full_name) || fallback_for(node, family: :prism)
@@ -481,24 +478,6 @@ module Rigor
481
478
  end
482
479
  end
483
480
 
484
- # Builds the dotted-colon name for a `Foo`, `Foo::Bar`, or `::Foo`
485
- # path. Returns nil when an inner segment is not itself a constant
486
- # reference (for example `expr::Foo`), so the caller can fall back.
487
- def build_constant_path_name(node)
488
- case node
489
- when Prism::ConstantReadNode
490
- node.name.to_s
491
- when Prism::ConstantPathNode
492
- parent = node.parent
493
- return node.name.to_s if parent.nil?
494
-
495
- parent_name = build_constant_path_name(parent)
496
- return nil if parent_name.nil?
497
-
498
- "#{parent_name}::#{node.name}"
499
- end
500
- end
501
-
502
481
  # Slice 5 phase 1 upgrades hash literals to `HashShape{...}`
503
482
  # when every entry is a static `AssocNode` whose key is a
504
483
  # `SymbolNode` or `StringNode` with a known value (covering the
@@ -837,7 +816,7 @@ module Rigor
837
816
  # Other pattern shapes (Range, Regexp, custom `===`) stay
838
817
  # `:maybe` — the existing union fallback handles them.
839
818
  def case_when_pattern_certainty(subject_type, pattern_node)
840
- class_name = build_constant_path_name(pattern_node)
819
+ class_name = Source::ConstantPath.qualified_name_or_nil(pattern_node)
841
820
  return Narrowing.class_pattern_certainty(subject_type, class_name, environment: scope.environment) if class_name
842
821
 
843
822
  literal = literal_pattern_value(pattern_node)
@@ -925,7 +904,8 @@ module Rigor
925
904
  end
926
905
 
927
906
  # `while` and `until` loops produce nil unless interrupted by
928
- # `break VALUE`, which Slice 3 phase 1 does not yet model.
907
+ # `break VALUE`; the expression value of `break VALUE` is not yet
908
+ # modeled (scope break-path propagation landed in `eval_loop`).
929
909
  # Returning Constant[nil] is safe and matches Ruby semantics for
930
910
  # the common case.
931
911
  def type_of_loop(_node)
@@ -2382,7 +2362,19 @@ module Rigor
2382
2362
  environment: scope.environment,
2383
2363
  locals: locals.freeze,
2384
2364
  self_type: receiver,
2385
- discovery: scope.discovery
2365
+ discovery: scope.discovery,
2366
+ struct_fold_safe_locals: struct_fold_safe_locals_for(def_node.body)
2367
+ )
2368
+ end
2369
+
2370
+ # ADR-48 Struct slice 3 — the fold-safe-local set for a method body
2371
+ # (runs only on a return-memo miss, so the per-call cost is bounded —
2372
+ # measured perf-neutral). Struct member layouts of constant receivers
2373
+ # are resolved through the discovery side-table the body scope inherits.
2374
+ def struct_fold_safe_locals_for(body)
2375
+ StructFoldSafety.fold_safe_locals(
2376
+ body,
2377
+ ->(name) { scope.struct_member_layout(name)&.[](:members) }
2386
2378
  )
2387
2379
  end
2388
2380
 
@@ -7,18 +7,15 @@ module Rigor
7
7
  # piece of a Rigor-side type expression that the reducer
8
8
  # ({HktReducer}) walks against a concrete argument list.
9
9
  #
10
- # Slice 2a ships a programmatic constructor surface only:
11
- # plugin and Rigor-bundled overlay authors build a body
12
- # tree by hand using these node types. The string-grammar
13
- # parser that reads `Definition#body` (the raw String slot
14
- # already populated by Slice 1's `HktDirectives.parse_define`)
15
- # into a tree is Slice 2b's deliverable; until it ships, the
16
- # `body` String stays opaque and `body_tree` is the
17
- # evaluable form.
10
+ # Slice 2a ships a programmatic constructor surface; plugin
11
+ # and Rigor-bundled overlay authors may build a body tree
12
+ # by hand using these node types. The string-grammar parser
13
+ # (`HktBodyParser`, Slice 2b, shipped) reads `Definition#body`
14
+ # (populated by Slice 1's `HktDirectives.parse_define`) into
15
+ # this node tree; `body_tree` is the evaluable form.
18
16
  #
19
- # The five node types cover the JSON.parse and dry-monads
20
- # use cases ADR-20 § Implementation slicing names as
21
- # near-term adopters:
17
+ # The nine node types cover JSON.parse, dry-monads, and the
18
+ # ADR-20 § D3 conditional / membership forms (shipped):
22
19
  #
23
20
  # - {TypeLeaf} — wraps a fully-built `Rigor::Type`
24
21
  # (use for atoms like `nil`, `Constant<true>`,
@@ -11,29 +11,27 @@ module Rigor
11
11
  # `%a{rigor:v1:hkt_define}` payloads) into the `HktBody`
12
12
  # node tree the Slice 2a reducer evaluates against.
13
13
  #
14
- # The minimum-viable grammar covered here is the
15
- # union-of-atoms-and-parameterised-forms subset of ADR-20
16
- # § D3 sufficient for `JSON.parse`'s `json::value`
17
- # recursive sum and for any other recursive-data-shape
18
- # signatures (Lisp value trees, dry-types refinements
19
- # without conditionals). The conditional / indexed-access
20
- # forms (`E <: T ? A : B`, `E in [k1, k2]`) drafted in D3
21
- # remain a follow-up slice — bodies that contain `?`
22
- # raise `ParseError` and the calling directive parser
23
- # drops the body_tree (the body String remains stored and
24
- # the reducer falls back to `app.bound`).
14
+ # The grammar implements the full ADR-20 § D3 subset:
15
+ # union-of-atoms/parameterised-forms for `JSON.parse`'s
16
+ # `json::value` recursive sum, plus the conditional and
17
+ # membership forms shipped in subsequent slices. Indexed-access
18
+ # forms remain deferred (no concrete demand yet).
25
19
  #
26
- # ## Grammar (slice 2b)
20
+ # ## Grammar
27
21
  #
28
22
  # body := union
29
23
  # union := type_expr ("|" type_expr)*
30
24
  # type_expr := atom | nominal_app | app_ref | param
25
+ # | conditional
31
26
  # atom := "nil" | "true" | "false" | "bool" | "untyped"
32
27
  # param := UCNAME (when UCNAME ∈ params)
33
28
  # nominal_app := class_name ("[" type_expr ("," type_expr)* "]")?
34
29
  # class_name := "::"? UCNAME ("::" UCNAME)*
35
30
  # app_ref := "App" "[" uri "," type_expr ("," type_expr)* "]"
36
31
  # uri := IDENT ("::" IDENT)+
32
+ # conditional := "(" test "?" union ":" union ")"
33
+ # test := type_expr ("<:" | "==") type_expr
34
+ # | type_expr "in" "[" type_expr ("," type_expr)* "]"
37
35
  # UCNAME := /[A-Z]\w*/
38
36
  # IDENT := /[a-z_]\w*/
39
37
  #
@@ -10,12 +10,12 @@ module Rigor
10
10
  # `%a{rigor:v1:hkt_define: ...}` annotations in shipped
11
11
  # `.rbs` files.
12
12
  #
13
- # Slice 1 keeps the registry **opaque**: it stores the
14
- # registration metadata (arity, variance, bound) and the
15
- # un-evaluated definition body (a raw String Slice 2
16
- # introduces the conditional / indexed-access evaluator that
17
- # parses the body and reduces `Type::App` instances against
18
- # it). The carrier never needs to read from the registry
13
+ # The registry stores registration metadata (arity, variance,
14
+ # bound) and the definition body as both a raw String and an
15
+ # evaluable `HktBody` node tree. `HktReducer` (Slice 2a) and
16
+ # `HktBodyParser` (Slice 2b) are both shipped and reduce
17
+ # `Type::App` instances against the definition. The carrier
18
+ # never needs to read from the registry
19
19
  # because Slice 1's `Type::App` carries its `bound` directly;
20
20
  # the registry exists at this slice solely so the parser
21
21
  # round-trip and downstream slices have a stable target API.
@@ -65,16 +65,15 @@ module Rigor
65
65
  # definition.
66
66
  #
67
67
  # `body` is the raw String payload from the `%a{...}`
68
- # annotation (Slice 1's parser populates it). It stays
69
- # opaque until Slice 2b's body-string parser lands.
68
+ # annotation (Slice 1's parser populates it); parsed into
69
+ # `body_tree` by `HktBodyParser` (Slice 2b, shipped).
70
70
  #
71
- # `body_tree` is the optional evaluable form: a
71
+ # `body_tree` is the evaluable form: a
72
72
  # `Rigor::Inference::HktBody::*` node tree the Slice 2a
73
73
  # reducer walks against the application's concrete
74
74
  # arguments. Plugin and Rigor-bundled overlay authors
75
75
  # construct it programmatically through
76
- # {with_body_tree}; the Slice 2b string parser will set
77
- # it from `body` once it ships. The reducer treats a
76
+ # {.definition_with_body_tree}. The reducer treats a
78
77
  # `nil` `body_tree` as "definition not yet evaluable"
79
78
  # and returns the registered bound.
80
79
  Definition = Data.define(:uri, :params, :body, :body_tree, :source_path, :source_line) do
@@ -42,10 +42,7 @@ module Rigor
42
42
  #
43
43
  # This is the single place the call-context field list is
44
44
  # enumerated — the whole point of the value object is to absorb
45
- # the wide keyword list the tiers used to each redeclare. The
46
- # ParameterLists disable here retires the per-tier disables (the
47
- # `RbsDispatch` quartet-plus signatures) rather than adding to
48
- # them.
45
+ # the wide keyword list the tiers used to each redeclare.
49
46
  def self.build(receiver:, method_name:, args:, # rubocop:disable Metrics/ParameterLists
50
47
  block_type: nil, environment: nil, call_node: nil,
51
48
  scope: nil, self_type_override: nil, public_only: false)