rigortype 0.1.18 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +159 -224
  3. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -3
  4. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
  5. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +32 -23
  6. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
  7. data/lib/rigor/analysis/check_rules/rule_walk.rb +151 -23
  8. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
  9. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +9 -3
  10. data/lib/rigor/analysis/check_rules.rb +756 -132
  11. data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
  12. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
  13. data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
  14. data/lib/rigor/analysis/diagnostic.rb +8 -0
  15. data/lib/rigor/analysis/fact_store.rb +5 -4
  16. data/lib/rigor/analysis/rule_catalog.rb +153 -6
  17. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +19 -18
  18. data/lib/rigor/analysis/runner/project_pre_passes.rb +13 -9
  19. data/lib/rigor/analysis/runner.rb +75 -27
  20. data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
  21. data/lib/rigor/analysis/worker_session.rb +31 -25
  22. data/lib/rigor/bleeding_edge.rb +123 -0
  23. data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
  24. data/lib/rigor/cache/descriptor.rb +86 -8
  25. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  26. data/lib/rigor/cache/store.rb +5 -3
  27. data/lib/rigor/cli/annotate_command.rb +122 -16
  28. data/lib/rigor/cli/baseline_command.rb +4 -3
  29. data/lib/rigor/cli/check_command.rb +118 -16
  30. data/lib/rigor/cli/coverage_command.rb +148 -16
  31. data/lib/rigor/cli/coverage_scan.rb +57 -0
  32. data/lib/rigor/cli/explain_command.rb +2 -0
  33. data/lib/rigor/cli/lsp_command.rb +3 -7
  34. data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
  35. data/lib/rigor/cli/mutation_protection_report.rb +73 -0
  36. data/lib/rigor/cli/options.rb +9 -0
  37. data/lib/rigor/cli/plugins_command.rb +4 -5
  38. data/lib/rigor/cli/plugins_renderer.rb +0 -2
  39. data/lib/rigor/cli/protection_renderer.rb +63 -0
  40. data/lib/rigor/cli/protection_report.rb +68 -0
  41. data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
  42. data/lib/rigor/cli/sig_gen_command.rb +2 -1
  43. data/lib/rigor/cli/trace_command.rb +2 -1
  44. data/lib/rigor/cli/triage_command.rb +8 -4
  45. data/lib/rigor/cli/triage_renderer.rb +15 -1
  46. data/lib/rigor/cli/type_of_command.rb +1 -1
  47. data/lib/rigor/cli/type_scan_command.rb +2 -1
  48. data/lib/rigor/cli.rb +12 -3
  49. data/lib/rigor/configuration/dependencies.rb +2 -4
  50. data/lib/rigor/configuration/severity_profile.rb +13 -1
  51. data/lib/rigor/configuration.rb +100 -6
  52. data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
  53. data/lib/rigor/environment/class_registry.rb +4 -3
  54. data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
  55. data/lib/rigor/environment/lockfile_resolver.rb +1 -1
  56. data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
  57. data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
  58. data/lib/rigor/environment/rbs_loader.rb +74 -5
  59. data/lib/rigor/environment.rb +17 -7
  60. data/lib/rigor/flow_contribution/fact.rb +1 -1
  61. data/lib/rigor/flow_contribution.rb +3 -5
  62. data/lib/rigor/inference/acceptance.rb +17 -9
  63. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  64. data/lib/rigor/inference/body_fixpoint.rb +89 -0
  65. data/lib/rigor/inference/budget_trace.rb +29 -2
  66. data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
  67. data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
  68. data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
  69. data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
  70. data/lib/rigor/inference/expression_typer.rb +1072 -71
  71. data/lib/rigor/inference/hkt_body.rb +8 -11
  72. data/lib/rigor/inference/hkt_body_parser.rb +10 -12
  73. data/lib/rigor/inference/hkt_registry.rb +10 -11
  74. data/lib/rigor/inference/macro_block_self_type.rb +2 -2
  75. data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
  76. data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
  77. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +210 -35
  78. data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
  79. data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
  80. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
  81. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
  82. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
  83. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
  84. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
  85. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
  86. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
  87. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +237 -24
  88. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
  89. data/lib/rigor/inference/method_dispatcher.rb +112 -49
  90. data/lib/rigor/inference/method_parameter_binder.rb +56 -2
  91. data/lib/rigor/inference/multi_target_binder.rb +46 -3
  92. data/lib/rigor/inference/mutation_widening.rb +147 -11
  93. data/lib/rigor/inference/narrowing.rb +284 -53
  94. data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
  95. data/lib/rigor/inference/project_patched_methods.rb +4 -7
  96. data/lib/rigor/inference/project_patched_scanner.rb +2 -13
  97. data/lib/rigor/inference/protection_scanner.rb +86 -0
  98. data/lib/rigor/inference/scope_indexer.rb +821 -76
  99. data/lib/rigor/inference/statement_evaluator.rb +1179 -102
  100. data/lib/rigor/inference/struct_fold_safety.rb +181 -0
  101. data/lib/rigor/inference/synthetic_method.rb +7 -7
  102. data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
  103. data/lib/rigor/language_server/completion_provider.rb +6 -12
  104. data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
  105. data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
  106. data/lib/rigor/language_server/hover_provider.rb +2 -3
  107. data/lib/rigor/language_server/hover_renderer.rb +2 -11
  108. data/lib/rigor/language_server/server.rb +9 -17
  109. data/lib/rigor/language_server.rb +4 -5
  110. data/lib/rigor/plugin/base.rb +245 -87
  111. data/lib/rigor/plugin/macro/block_as_method.rb +25 -25
  112. data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
  113. data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
  114. data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
  115. data/lib/rigor/plugin/macro.rb +6 -8
  116. data/lib/rigor/plugin/manifest.rb +49 -90
  117. data/lib/rigor/plugin/node_rule_walk.rb +59 -14
  118. data/lib/rigor/plugin/registry.rb +18 -18
  119. data/lib/rigor/plugin/type_node_resolver.rb +6 -8
  120. data/lib/rigor/protection/mutation_scanner.rb +120 -0
  121. data/lib/rigor/protection/mutator.rb +246 -0
  122. data/lib/rigor/rbs_extended.rb +24 -36
  123. data/lib/rigor/reflection.rb +4 -7
  124. data/lib/rigor/scope/discovery_index.rb +16 -2
  125. data/lib/rigor/scope.rb +185 -16
  126. data/lib/rigor/sig_gen/generator.rb +8 -0
  127. data/lib/rigor/sig_gen/observed_call.rb +3 -3
  128. data/lib/rigor/sig_gen/writer.rb +40 -2
  129. data/lib/rigor/source/constant_path.rb +62 -0
  130. data/lib/rigor/source.rb +1 -0
  131. data/lib/rigor/triage/catalogue.rb +4 -19
  132. data/lib/rigor/triage.rb +69 -1
  133. data/lib/rigor/type/bound_method.rb +2 -11
  134. data/lib/rigor/type/combinator.rb +45 -3
  135. data/lib/rigor/type/constant.rb +2 -11
  136. data/lib/rigor/type/data_class.rb +2 -11
  137. data/lib/rigor/type/data_instance.rb +2 -11
  138. data/lib/rigor/type/hash_shape.rb +2 -11
  139. data/lib/rigor/type/integer_range.rb +2 -11
  140. data/lib/rigor/type/intersection.rb +2 -11
  141. data/lib/rigor/type/nominal.rb +2 -11
  142. data/lib/rigor/type/plain_lattice.rb +37 -0
  143. data/lib/rigor/type/refined.rb +72 -13
  144. data/lib/rigor/type/singleton.rb +2 -11
  145. data/lib/rigor/type/struct_class.rb +75 -0
  146. data/lib/rigor/type/struct_instance.rb +93 -0
  147. data/lib/rigor/type/tuple.rb +5 -15
  148. data/lib/rigor/type.rb +2 -0
  149. data/lib/rigor/version.rb +1 -1
  150. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  151. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
  152. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +16 -32
  153. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
  154. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
  155. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
  156. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +34 -100
  157. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
  158. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
  159. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
  160. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +26 -27
  161. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
  162. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +9 -8
  163. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
  164. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
  165. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
  166. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
  167. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
  168. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
  169. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
  170. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +18 -49
  171. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
  172. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
  173. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +4 -4
  174. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
  175. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
  176. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
  177. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
  178. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +22 -35
  179. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
  180. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
  181. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +16 -23
  182. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
  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 +21 -27
  187. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
  188. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
  189. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
  190. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
  191. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
  192. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
  193. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
  194. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
  195. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
  196. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
  197. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +52 -40
  198. data/sig/rigor/analysis/fact_store.rbs +3 -0
  199. data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
  200. data/sig/rigor/plugin/base.rbs +5 -2
  201. data/sig/rigor/plugin/manifest.rbs +1 -2
  202. data/sig/rigor/scope.rbs +18 -1
  203. data/sig/rigor/type.rbs +37 -1
  204. data/sig/rigor.rbs +1 -1
  205. data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
  206. data/skills/rigor-plugin-author/SKILL.md +6 -4
  207. data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
  208. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
  209. metadata +25 -2
  210. data/lib/rigor/plugin/macro/external_file.rb +0 -143
@@ -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
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../type"
4
+ require_relative "budget_trace"
5
+
6
+ module Rigor
7
+ module Inference
8
+ # ADR-56 WD3 — the single capped-fixpoint mechanism shared by the
9
+ # non-escaping block captured-local write-back (slice A) and the
10
+ # loop-body fixpoint (slice B). Computes the continuation binding of a
11
+ # set of locals that a body (a block body or a loop body) may rebind
12
+ # across an unknown number (0..N) of iterations.
13
+ #
14
+ # The body may run zero times, so the seed (pre-state) binding is kept
15
+ # as a join constituent throughout; it may compound (`a = [a]`), so the
16
+ # join is iterated to a fixed point under a hard cap (ADR-41 WD4). On
17
+ # the final permitted iteration value-pinned constituents are widened to
18
+ # their nominal base to force convergence; a local that still moves is
19
+ # collapsed to `Dynamic[top]` — the established escaping-block floor —
20
+ # and a {BudgetTrace::BLOCK_WRITEBACK_CAP} hit is recorded.
21
+ #
22
+ # The mechanism is parameterized over an `evaluate_body` callable so
23
+ # slice B can reuse it: given the current per-name bindings it returns
24
+ # the per-name exit bindings produced by one body evaluation from those
25
+ # bindings (names the body leaves unwritten in a given pass simply do
26
+ # not appear in the returned hash).
27
+ module BodyFixpoint
28
+ # One body evaluation per iteration; ADR-55's shape (cap 3).
29
+ CAP = 3
30
+
31
+ module_function
32
+
33
+ # @param names [Array<Symbol>] the outer locals the body can rebind.
34
+ # @param seed_bindings [Hash{Symbol=>Type}] pre-state binding per name.
35
+ # @param widen [#call] value-pinned widener (Constant -> Nominal).
36
+ # @param evaluate_body [#call] `bindings -> exit_bindings` — evaluates
37
+ # the body once from `bindings` (the per-name current assumption) and
38
+ # returns the per-name exit binding it produced.
39
+ # @return [Hash{Symbol=>Type}] the continuation binding per name.
40
+ def converge(names:, seed_bindings:, widen:, evaluate_body:)
41
+ return {} if names.empty?
42
+
43
+ # Running assumption per name; seeded with the pre-state binding,
44
+ # which stays a join constituent throughout (0-iteration soundness).
45
+ assumption = seed_bindings.dup
46
+
47
+ (0...CAP).each do |iteration|
48
+ last_iteration = iteration == CAP - 1
49
+ exit_bindings = evaluate_body.call(assumption)
50
+
51
+ stable = true
52
+ names.each do |name|
53
+ exit_type = exit_bindings[name]
54
+ next if exit_type.nil? # body did not write it this pass
55
+
56
+ if last_iteration
57
+ # On the final permitted iteration widen BOTH the running
58
+ # assumption and the fresh exit type to their nominal bases
59
+ # before joining: Rigor's `union` keeps `Constant[1]` and
60
+ # `Nominal[Integer]` as distinct members, so an accumulator
61
+ # (`+=`/`*=`) producing a fresh constant per pass would never
62
+ # converge without collapsing both sides first. If the join
63
+ # is still wider than the widened assumption (structural
64
+ # compounding, `a = [a]`), the local floors to `Dynamic[top]`.
65
+ base = widen.call(assumption[name])
66
+ joined = Type::Combinator.union(base, widen.call(exit_type))
67
+ if joined == base
68
+ assumption[name] = joined
69
+ else
70
+ BudgetTrace.hit(BudgetTrace::BLOCK_WRITEBACK_CAP)
71
+ assumption[name] = Type::Combinator.untyped
72
+ end
73
+ else
74
+ joined = Type::Combinator.union(assumption[name], exit_type)
75
+ next if joined == assumption[name]
76
+
77
+ stable = false
78
+ assumption[name] = joined
79
+ end
80
+ end
81
+
82
+ break if stable
83
+ end
84
+
85
+ assumption
86
+ end
87
+ end
88
+ end
89
+ end
@@ -20,6 +20,12 @@ module Rigor
20
20
  # hit the 100-node BFS cap and gave up resolving the self-call.
21
21
  # - {HKT_FUEL_EXHAUSTED} — `HktReducer` ran out of its reduction
22
22
  # fuel budget and unwound to `app.bound`.
23
+ # - {RECURSION_UNROLL_FUEL} — the constant-arg recursion unroll
24
+ # (ADR-55 slice 1) exhausted its per-entry fuel and fell back to
25
+ # the plain `(receiver, method)` guard (in-cycle call → `Dynamic[top]`).
26
+ # - {RECURSION_FIXPOINT_CAP} — the fixpoint return-summary iteration
27
+ # (ADR-55 slice 2) hit its 3-evaluation cap without converging and
28
+ # collapsed the summary to `untyped` (today's behaviour).
23
29
  #
24
30
  # Enabled only when `RIGOR_BUDGET_TRACE` is set (to any non-empty
25
31
  # value) in the environment, or via {enable!} in tests. When
@@ -34,8 +40,29 @@ module Rigor
34
40
  RECURSION_GUARD = :recursion_guard
35
41
  ANCESTOR_WALK_LIMIT = :ancestor_walk_limit
36
42
  HKT_FUEL_EXHAUSTED = :hkt_fuel_exhausted
37
-
38
- CATEGORIES = [RECURSION_GUARD, ANCESTOR_WALK_LIMIT, HKT_FUEL_EXHAUSTED].freeze
43
+ # `ExpressionTyper#infer_user_method_return` exhausted its
44
+ # constant-arg unroll fuel (ADR-55 slice 1) and fell back to the
45
+ # plain `(receiver, method)` recursion guard — i.e. the in-cycle
46
+ # call widened to `Dynamic[top]` exactly as it does without the
47
+ # unroll.
48
+ RECURSION_UNROLL_FUEL = :recursion_unroll_fuel
49
+ # `ExpressionTyper#infer_user_method_return` ran the fixpoint
50
+ # return-summary iteration (ADR-55 slice 2) to its 3-evaluation cap
51
+ # without reaching convergence and collapsed the summary to
52
+ # `untyped` — the in-cycle result widens to `Dynamic[top]` exactly
53
+ # as it does without the fixpoint.
54
+ RECURSION_FIXPOINT_CAP = :recursion_fixpoint_cap
55
+ # `BodyFixpoint#converge` (ADR-56 slice A — non-escaping block
56
+ # captured-local write-back) ran its 3-evaluation cap without the
57
+ # written local's join converging and collapsed that local to
58
+ # `Dynamic[top]` (the escaping-block floor). Shared by slice B's
59
+ # loop-body fixpoint.
60
+ BLOCK_WRITEBACK_CAP = :block_writeback_cap
61
+
62
+ CATEGORIES = [
63
+ RECURSION_GUARD, ANCESTOR_WALK_LIMIT, HKT_FUEL_EXHAUSTED, RECURSION_UNROLL_FUEL,
64
+ RECURSION_FIXPOINT_CAP, BLOCK_WRITEBACK_CAP
65
+ ].freeze
39
66
 
40
67
  # Distribution (histogram) categories — read-only observations of
41
68
  # a value's size at a site, used to choose budget defaults from an
@@ -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