rigortype 0.4.7 → 0.4.8

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 (268) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/docs/handbook/01-getting-started.md +1 -1
  8. data/docs/handbook/11-sig-gen.md +35 -0
  9. data/docs/install.md +20 -0
  10. data/docs/manual/01-installation.md +67 -26
  11. data/docs/manual/02-cli-reference.md +307 -91
  12. data/docs/manual/03-configuration.md +41 -3
  13. data/docs/manual/04-diagnostics.md +5 -4
  14. data/docs/manual/08-skills.md +11 -0
  15. data/docs/manual/09-editor-integration.md +8 -0
  16. data/docs/manual/14-rails-quickstart.md +1 -1
  17. data/docs/manual/19-effect-labels.md +3 -4
  18. data/docs/manual/plugins/rigor-actionpack.md +15 -6
  19. data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
  20. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  21. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  22. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  23. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
  24. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
  25. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
  26. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  27. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
  28. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  29. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  30. data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
  31. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  32. data/lib/rigor/analysis/check_rules.rb +598 -99
  33. data/lib/rigor/analysis/crash_signature.rb +43 -1
  34. data/lib/rigor/analysis/dependency_recorder.rb +233 -76
  35. data/lib/rigor/analysis/incremental.rb +11 -1
  36. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
  37. data/lib/rigor/analysis/incremental_session.rb +279 -37
  38. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  39. data/lib/rigor/analysis/rule_catalog.rb +37 -1
  40. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  41. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  42. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
  43. data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
  44. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
  45. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  48. data/lib/rigor/analysis/runner.rb +222 -39
  49. data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
  50. data/lib/rigor/analysis/worker_session.rb +42 -41
  51. data/lib/rigor/bleeding_edge.rb +19 -4
  52. data/lib/rigor/cache/file_digest.rb +39 -31
  53. data/lib/rigor/cache/incremental_snapshot.rb +66 -7
  54. data/lib/rigor/cache/rbs_descriptor.rb +37 -2
  55. data/lib/rigor/cache/rbs_environment.rb +5 -1
  56. data/lib/rigor/cli/baseline_command.rb +15 -2
  57. data/lib/rigor/cli/check_command.rb +4 -2
  58. data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
  59. data/lib/rigor/cli/lens_command.rb +17 -2
  60. data/lib/rigor/cli/sig_gen_command.rb +8 -0
  61. data/lib/rigor/cli/skill_describe.rb +1 -0
  62. data/lib/rigor/cli.rb +5 -2
  63. data/lib/rigor/config_audit.rb +32 -18
  64. data/lib/rigor/configuration/severity_profile.rb +9 -0
  65. data/lib/rigor/configuration.rb +32 -5
  66. data/lib/rigor/effects/collector.rb +53 -21
  67. data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
  68. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  69. data/lib/rigor/environment/default_libraries.rb +17 -0
  70. data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
  71. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  72. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  73. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  74. data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
  75. data/lib/rigor/environment/rbs_loader.rb +312 -63
  76. data/lib/rigor/environment.rb +131 -35
  77. data/lib/rigor/inference/acceptance.rb +50 -15
  78. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  79. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  80. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  81. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  82. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  83. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  84. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  85. data/lib/rigor/inference/block_run_count.rb +9 -2
  86. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  87. data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
  88. data/lib/rigor/inference/captured_locals.rb +2 -0
  89. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  90. data/lib/rigor/inference/clause_widening.rb +1 -1
  91. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  92. data/lib/rigor/inference/construction_escape.rb +56 -15
  93. data/lib/rigor/inference/declaration_walk.rb +4 -0
  94. data/lib/rigor/inference/def_node_resolver.rb +32 -14
  95. data/lib/rigor/inference/def_return_typer.rb +1 -1
  96. data/lib/rigor/inference/definer_resolution.rb +15 -7
  97. data/lib/rigor/inference/definers_index.rb +72 -16
  98. data/lib/rigor/inference/error_info.rb +16 -0
  99. data/lib/rigor/inference/expression_typer.rb +1459 -287
  100. data/lib/rigor/inference/flow_tracer.rb +31 -14
  101. data/lib/rigor/inference/fork_map.rb +3 -0
  102. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  103. data/lib/rigor/inference/global_write_census.rb +8 -6
  104. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  105. data/lib/rigor/inference/indexed_narrowing.rb +57 -8
  106. data/lib/rigor/inference/ivar_writer_census.rb +106 -11
  107. data/lib/rigor/inference/key_presence_guard.rb +51 -25
  108. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  109. data/lib/rigor/inference/last_line.rb +5 -2
  110. data/lib/rigor/inference/loop_value_record.rb +63 -15
  111. data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
  112. data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
  113. data/lib/rigor/inference/match_rebinding.rb +21 -8
  114. data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
  115. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  116. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  117. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  118. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  119. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  120. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  121. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
  122. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
  123. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
  124. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
  125. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  126. data/lib/rigor/inference/method_dispatcher.rb +66 -18
  127. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  128. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  129. data/lib/rigor/inference/module_function_state.rb +13 -2
  130. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  131. data/lib/rigor/inference/mutation_widening.rb +44 -4
  132. data/lib/rigor/inference/narrowing.rb +201 -27
  133. data/lib/rigor/inference/operand_effects.rb +43 -2
  134. data/lib/rigor/inference/optimistic_origin.rb +133 -8
  135. data/lib/rigor/inference/origin_lookup.rb +1 -1
  136. data/lib/rigor/inference/precision_scanner.rb +1 -1
  137. data/lib/rigor/inference/project_method_ownership.rb +28 -10
  138. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  139. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  140. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  141. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  142. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  143. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  144. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
  145. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
  146. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  147. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  148. data/lib/rigor/inference/remover_holder_scan.rb +76 -22
  149. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  150. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
  151. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
  152. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  153. data/lib/rigor/inference/scope_indexer.rb +2079 -183
  154. data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
  155. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  156. data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
  157. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  158. data/lib/rigor/inference/statement_evaluator.rb +970 -250
  159. data/lib/rigor/inference/string_mutation.rb +14 -0
  160. data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
  161. data/lib/rigor/inference/toplevel_def_census.rb +14 -6
  162. data/lib/rigor/inference/unknown_store_widening.rb +1 -1
  163. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  164. data/lib/rigor/inference/void_tail_summary.rb +29 -10
  165. data/lib/rigor/lens/claims/comparison.rb +78 -0
  166. data/lib/rigor/lens/claims/request.rb +131 -0
  167. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  168. data/lib/rigor/lens/claims.rb +208 -0
  169. data/lib/rigor/lens/contract_reader.rb +5 -0
  170. data/lib/rigor/lens/document.rb +1 -1
  171. data/lib/rigor/lens/file_reader.rb +21 -8
  172. data/lib/rigor/lens/run.rb +8 -0
  173. data/lib/rigor/lens.rb +1 -1
  174. data/lib/rigor/library_probe.rb +68 -0
  175. data/lib/rigor/memo_audit.rb +282 -0
  176. data/lib/rigor/plugin/base.rb +15 -2
  177. data/lib/rigor/plugin/io_boundary.rb +25 -3
  178. data/lib/rigor/plugin/manifest.rb +7 -3
  179. data/lib/rigor/plugin/registry.rb +13 -2
  180. data/lib/rigor/plugin/trust_policy.rb +46 -0
  181. data/lib/rigor/protection/mutation_cache.rb +4 -1
  182. data/lib/rigor/reflection/constant_ancestors.rb +79 -22
  183. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  184. data/lib/rigor/reflection/declarations.rb +58 -0
  185. data/lib/rigor/reflection/project_method.rb +177 -0
  186. data/lib/rigor/reflection.rb +2 -0
  187. data/lib/rigor/rooted_glob.rb +18 -1
  188. data/lib/rigor/run_scoped_slots.rb +20 -20
  189. data/lib/rigor/scope/discovery_index.rb +149 -2
  190. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  191. data/lib/rigor/scope/resolution_chain.rb +900 -55
  192. data/lib/rigor/scope.rb +307 -34
  193. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  194. data/lib/rigor/sig_gen/classification.rb +14 -1
  195. data/lib/rigor/sig_gen/generator.rb +222 -13
  196. data/lib/rigor/sig_gen/method_candidate.rb +14 -0
  197. data/lib/rigor/sig_gen/observation_collector.rb +11 -2
  198. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  199. data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
  200. data/lib/rigor/sig_gen/writer.rb +21 -0
  201. data/lib/rigor/sig_gen.rb +1 -0
  202. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  203. data/lib/rigor/source/node_children.rb +13 -0
  204. data/lib/rigor/type/combinator.rb +164 -8
  205. data/lib/rigor/type/interface.rb +60 -0
  206. data/lib/rigor/type.rb +1 -0
  207. data/lib/rigor/type_of_memo/serving.rb +99 -0
  208. data/lib/rigor/type_of_memo/stats.rb +43 -0
  209. data/lib/rigor/type_of_memo.rb +337 -0
  210. data/lib/rigor/typing_state.rb +1094 -0
  211. data/lib/rigor/version.rb +1 -1
  212. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
  213. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
  214. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  215. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  216. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  217. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  218. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  219. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
  220. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
  221. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  222. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
  223. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  224. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
  225. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
  226. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
  227. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  228. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  229. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
  230. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  231. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
  232. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
  233. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
  234. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
  235. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
  236. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  237. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  238. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
  239. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  240. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
  241. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  242. data/sig/rigor/analysis/fact_store.rbs +2 -3
  243. data/sig/rigor/cache/descriptor.rbs +14 -0
  244. data/sig/rigor/environment.rbs +4 -1
  245. data/sig/rigor/plugin/base.rbs +53 -19
  246. data/sig/rigor/plugin/interfaces.rbs +51 -0
  247. data/sig/rigor/plugin/io_boundary.rbs +2 -3
  248. data/sig/rigor/plugin/node_context.rbs +21 -0
  249. data/sig/rigor/reflection.rbs +37 -0
  250. data/sig/rigor/scope.rbs +22 -3
  251. data/sig/rigor/trinary.rbs +1 -0
  252. data/sig/rigor/type.rbs +9 -3
  253. data/sig/rigor.rbs +3 -2
  254. data/skills/rigor-ask/SKILL.md +1 -0
  255. data/skills/rigor-project-init/references/01-detect.md +1 -0
  256. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
  257. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  258. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  259. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  260. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  261. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  262. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  263. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  264. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  265. data/skills/rigor-type-oracle/SKILL.md +26 -1
  266. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  267. metadata +48 -2
  268. data/lib/rigor/inference/memo_audit.rb +0 -55
@@ -4,6 +4,7 @@ require "prism"
4
4
 
5
5
  require_relative "../../source/constant_path"
6
6
  require_relative "../../source/node_children"
7
+ require_relative "../../typing_state"
7
8
 
8
9
  module Rigor
9
10
  module Inference
@@ -50,6 +51,9 @@ module Rigor
50
51
  # `undef_method :gets`), other than `&:gets` and the first argument of a call that only names a method
51
52
  # (`respond_to?(:gets)`, `send(:gets)`).
52
53
  class SelfEvidence
54
+ TypingState.memo_field(self, :@scan, reason: "one walk of the file's root node, built on first ask")
55
+ TypingState.memo_field(self, :@const_set_spellings, reason: "one walk of the file's root, built on first ask")
56
+
53
57
  # The context of a reader whose `self` is `main`.
54
58
  MAIN = :main
55
59
  UNKNOWN = :unknown
@@ -83,8 +87,14 @@ module Rigor
83
87
  def initialize(root)
84
88
  @root = root
85
89
  @scan = nil
90
+ @const_set_spellings = nil
86
91
  end
87
92
 
93
+ # Issue #3156 — the constants this file binds by a `const_set` with a literal name on the body's own module
94
+ # (implicit `self` or `self.const_set`) inside a `class` / `module` body, qualified by that body's nesting
95
+ # (`module NS; const_set(:AMid, Other)` binds `"NS::AMid"`). The census files no such write.
96
+ def const_set_spellings = (@const_set_spellings ||= collect_const_sets(@root, nil, Set.new).freeze)
97
+
88
98
  # {MAIN}, the `Prism::ClassNode` or `Prism::ModuleNode` whose body the implicit-self reader `call_node` runs
89
99
  # in, or nil: the file may give it another `self`, or it is not one of this file's readers.
90
100
  def context(call_node) = scan.context(call_node)
@@ -96,6 +106,26 @@ module Rigor
96
106
 
97
107
  def scan = (@scan ||= Scan.new(@root))
98
108
 
109
+ def collect_const_sets(node, namespace, found)
110
+ case node
111
+ when Prism::ClassNode, Prism::ModuleNode
112
+ inner = Source::ConstantPath.pushed_nesting(namespace ? [namespace] : [], node.constant_path)&.first
113
+ collect_const_sets(node.body, inner, found) if inner
114
+ return found
115
+ when Prism::CallNode
116
+ found << "#{namespace}::#{literal_const_set_name(node)}" if namespace && literal_const_set_name(node)
117
+ end
118
+ node&.compact_child_nodes&.each { |child| collect_const_sets(child, namespace, found) }
119
+ found
120
+ end
121
+
122
+ def literal_const_set_name(node)
123
+ return nil unless node.name == :const_set && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
124
+
125
+ name = node.arguments&.arguments&.first
126
+ name.unescaped if name.is_a?(Prism::SymbolNode) || name.is_a?(Prism::StringNode)
127
+ end
128
+
99
129
  # One walk of the file, whose answers {SelfEvidence} keeps once the walk is complete.
100
130
  class Scan
101
131
  def initialize(root)
@@ -261,10 +261,13 @@ module Rigor
261
261
  # The scope a block or lambda body enters with, as {MatchRebinding.block_entry} gives the match globals': `$_`
262
262
  # forgotten when the body may set it, which a later iteration then reads, when the frame makes a closure that
263
263
  # may, or when the owning call may: its receiver chain and arguments run before the method yields, and
264
- # `IO.foreach` sets `$_` to each line it yields.
264
+ # `IO.foreach` sets `$_` to each line it yields. Issue #3195 — and whatever the body does when it runs later
265
+ # than where it is written ({FreshFrameBlocks.deferred_body?}): a proc's body, an enumerator's or a hash's
266
+ # default proc reads the `$_` the slot holds when it runs.
265
267
  def block_entry(scope, block_node, call_node = nil)
266
268
  return scope unless scope.last_line_bound?
267
- return scope unless scope.last_line_closure? || may_set?(block_node.body, scope) || call_sets?(call_node, scope)
269
+ return scope unless FreshFrameBlocks.deferred_body?(block_node, call_node, scope) ||
270
+ scope.last_line_closure? || may_set?(block_node.body, scope) || call_sets?(call_node, scope)
268
271
 
269
272
  scope.forget_last_line
270
273
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../run_scoped_slots"
3
+ require_relative "../typing_state"
4
4
 
5
5
  module Rigor
6
6
  module Inference
@@ -17,40 +17,88 @@ module Rigor
17
17
  # `discovery`): across the cost spec's 10933 reads every match was an identity match, and `lib/` read no record at
18
18
  # all (issue #2262). It lives in a per-thread map keyed by the loop node. The map's keys are weak but its values
19
19
  # are strong, and a recorded scope reaches its discovery, its environment and the parse tree that holds the key,
20
- # so a record never goes on its own (issue #2412): the map is a run-scoped slot ({RunScopedSlots}), emptied when
21
- # the run that filled it ends.
20
+ # so a record never goes on its own (issue #2412): the map is a run-scoped slot ({TypingState.release_run_scoped}),
21
+ # emptied when the run that filled it ends.
22
22
  module LoopValueRecord
23
- KEY = RunScopedSlots.register(:rigor_loop_value_record)
24
- private_constant :KEY
23
+ # ADR-129 WD1 — a Table, and a read-through cache of the computation's own value: a record read back answers
24
+ # what evaluating the loop again from the same scope would.
25
+ RECORD = TypingState.table(
26
+ :loop_value_record,
27
+ run_scoped: true, read_through: true, entries: TypingState::WeakKeyMapEntries,
28
+ reason: "keyed by loop node; a read-through cache of the loop's own evaluation"
29
+ )
30
+ private_constant :RECORD
25
31
 
26
- module_function
32
+ # ADR-129 WD3 — a Resource read only as a delta: {#loops_evaluated} feeds a before/after comparison taken within
33
+ # one conditional's evaluation, so with no loop evaluated in between the delta is zero.
34
+ LOOPS_EVALUATED = TypingState.resource(
35
+ :loops_evaluated,
36
+ read_exemption: :delta_internal, run_scoped: true,
37
+ reason: "a count of the loops this thread evaluated, incremented in place and read as a delta"
38
+ )
39
+ private_constant :LOOPS_EVALUATED
40
+
41
+ # Issue #2767 — the ADR-55 fixpoint summaries are an ambient input of a loop's value: an ADR-55 fixpoint
42
+ # evaluates a recursive method's body several times from the same body scope object, each time under a wider
43
+ # assumption, so a value recorded in one iteration would answer an identity-matching read in the next one with
44
+ # the narrower iterate, and the fixpoint would converge on it (`[(while …; break(n.zero? ? 1 : q(n - 1)); end)]`
45
+ # typed `[bot]`). The generation moves whenever a summary's assumption is seeded or changed
46
+ # ({advance_generation}), and a record answers only a read in the generation it was written in. The advance in
47
+ # `fixpoint_step` is the one a witness needs (the spec above); the advances at the seed, the entangled degrade
48
+ # and the cap collapse are precautions, since each leaves no in-flight evaluation of that body scope behind.
49
+ #
50
+ # ADR-129 WD3 — a Resource whose value is fixed while the state id is: every advance is a {TypingState::State#rewrite}.
51
+ GENERATION = TypingState.resource(
52
+ :summary_generation,
53
+ read_exemption: :id_determined, run_scoped: true,
54
+ reason: "a count of the ADR-55 summary assumption changes on this thread; a record keys on it"
55
+ )
56
+ private_constant :GENERATION
27
57
 
28
- COUNT_KEY = RunScopedSlots.register(:rigor_loop_value_count)
29
- private_constant :COUNT_KEY
58
+ module_function
30
59
 
31
60
  def record(node, scope, type)
32
- Thread.current[COUNT_KEY] = loops_evaluated + 1
61
+ TypingState.current.write(LOOPS_EVALUATED, loops_evaluated + 1)
33
62
  conditional(node, scope, type)
34
63
  end
35
64
 
65
+ # The current ADR-55 summary generation (#2767), which a record made now carries and a read must match.
66
+ def generation = TypingState.current[GENERATION] || 0
67
+
68
+ # A summary assumption was seeded or changed: no record written before answers a read from now on (#2767).
69
+ def advance_generation
70
+ state = TypingState.current
71
+ state.write(GENERATION, (state[GENERATION] || 0) + 1)
72
+ state.rewrite(GENERATION)
73
+ end
74
+
36
75
  # How many loops this thread has evaluated, so a conditional can tell whether its evaluation held one.
37
- def loops_evaluated = Thread.current[COUNT_KEY] || 0
76
+ def loops_evaluated = TypingState.current[LOOPS_EVALUATED] || 0
38
77
 
39
78
  # The same record for a conditional that evaluated a loop in a branch: `id(c ? (while …) : 0)` types the
40
79
  # conditional again from the scope it was evaluated from, and each typing evaluated the loop again from a
41
80
  # branch scope of its own, which multiplied the passes of every loop nested in it (issue #2262).
42
81
  def conditional(node, scope, type)
43
- (Thread.current[KEY] ||= ObjectSpace::WeakKeyMap.new)[node] = [scope, type]
82
+ record = [scope, type, generation]
83
+ state = TypingState.current
84
+ state.log_write(RECORD, node, record)
85
+ state.weak_key_map(RECORD)[node] = record
44
86
  type
45
87
  end
46
88
 
47
- # The recorded value of `node` evaluated from `scope` itself, or nil.
89
+ # Runs the evaluation a {fetch} that answered nil falls back to: the read-through Table's miss (ADR-129 WD1),
90
+ # which touches when it writes another Table while a capture is open.
91
+ def miss(&) = TypingState.current.memo_miss(RECORD, &)
92
+
93
+ # The recorded value of `node` evaluated from `scope` itself in the current summary generation, or nil.
48
94
  def fetch(node, scope)
49
- slot = Thread.current[KEY]&.[](node)
95
+ state = TypingState.current
96
+ slot = state[RECORD]&.[](node)
97
+ state.log_read(RECORD, node, slot || TypingState::ABSENT)
50
98
  return nil if slot.nil?
51
99
 
52
- recorded, type = slot
53
- recorded.equal?(scope) ? type : nil
100
+ recorded, type, recorded_generation = slot
101
+ recorded.equal?(scope) && recorded_generation == generation ? type : nil
54
102
  end
55
103
  end
56
104
  end
@@ -3,6 +3,7 @@
3
3
  require "prism"
4
4
 
5
5
  require_relative "../../source/node_children"
6
+ require_relative "../../typing_state"
6
7
 
7
8
  module Rigor
8
9
  module Inference
@@ -15,6 +16,16 @@ module Rigor
15
16
  # `$_` (issue #1359), whose frame-wide answers ({#last_line_closure?}, {#last_line_fallback?}) are kept here
16
17
  # too, asked only while `$_` is narrowed.
17
18
  class Frame
19
+ # ADR-129 G4 — the frame's extensional caches. A scan is keyed on its node (and reading kind) and on the scope's
20
+ # local and instance-variable tables by identity, the inputs a lookup argument's answer reads; the rest of what
21
+ # it reads through the scope (constant resolution over the lexical nesting, discovery and environment) is fixed
22
+ # for the body the frame runs. The other three read the body and the parameters alone.
23
+ TypingState.memo_field(self, :@scans, :@kind_scans, :@self_call_fallback,
24
+ reason: "keyed on the node and the scope's locals and ivars tables by identity; the " \
25
+ "other scope inputs a scan reads are fixed for the frame's body")
26
+ TypingState.memo_field(self, :@forwarded_block, :@body_locals, :@parameter_names,
27
+ reason: "functions of the frame's body and parameters alone")
28
+
18
29
  # The body the frame runs, which issue #2555's escape scan reads ({ConstructionEscape.escaped?}).
19
30
  attr_reader :body
20
31
  # The method's parameters (nil for a class, module or file body), which issue #2608's holder scan reads for a
@@ -5,40 +5,96 @@ require "prism"
5
5
  module Rigor
6
6
  module Inference
7
7
  module MatchRebinding
8
- # Issue #2443 — the match globals a block enters with when the call that owns it matches before every yield.
9
- # `String#scan`, `#gsub`, `#gsub!`, `#sub` and `#sub!` set the caller's `$~` to the current match before each
10
- # run of their block, with a Regexp pattern or a String one, so the block reads that match on every run
8
+ # Issue #2443, #1371 — the match globals a block enters with when the call that owns it matches before every
9
+ # yield. `String#scan`, `#gsub`, `#gsub!`, `#sub` and `#sub!` set the caller's `$~` to the current match before
10
+ # each run of their block, with a Regexp pattern or a String one, so the block reads that match on every run
11
11
  # whatever the enclosing frame held; the frame-wide entry ({MatchRebinding.block_entry}) would otherwise leave
12
- # it the narrowing at the call site (issue #1371) or nothing at all, and `Regexp.last_match.begin(0)` in the
13
- # block read `MatchData?`.
12
+ # it the narrowing at the call site or nothing at all.
14
13
  #
15
14
  # A receiver that is a String, or a `Dynamic` the analyzer cannot type ({Narrowing.string_match_receiver?}),
16
- # enters with the call's own match edge ({Narrowing.yielded_match_scope}). A `Dynamic` receiver may be another
17
- # object whose block-taking `scan` runs no match, and then the block reads the caller's `$~`; narrowing it
18
- # anyway can only drop a diagnostic, never add one (ADR-5). A receiver known to be anything else — a project
19
- # class, `Pathname`, a union with either — keeps the frame-wide entry: a method defined in Ruby runs its
20
- # matches in a frame of its own, so its block reads the caller's `$~` as the frame rules leave it. So does an
21
- # implicit-self call.
15
+ # enters with the call's own match edge ({Narrowing.yielded_match_scope}), which forgets what the frame held.
16
+ # A `Dynamic` receiver may be another object whose block-taking `scan` runs no match, and then the block reads
17
+ # the caller's `$~`; the name is taken as String's there, the overwhelmingly common case. A receiver known to
18
+ # be anything else — a project class, `Pathname`, a union with either — keeps the frame-wide entry: a method
19
+ # defined in Ruby runs its matches in a frame of its own, so its block reads the caller's `$~` as the frame
20
+ # rules leave it. So does an implicit-self call.
21
+ #
22
+ # The block form of `grep` and `grep_v` runs `pattern === element` in the caller's frame before each yield,
23
+ # which a Regexp pattern answers by setting `$~`: `grep` yields after a match, `grep_v` after a failed one. With
24
+ # a regex literal for the pattern, a `grep` block enters with that match edge and a `grep_v` block with the
25
+ # globals unbound. `Enumerable` is mixed into classes of every kind, so any explicit receiver counts but one
26
+ # whose `grep` the project defines ({ProjectMethodOwnership}): that method matches, if at all, in a frame of
27
+ # its own, and its block keeps the frame-wide entry as a project `scan`'s does. Where a refinement that defines
28
+ # the name is in effect at the call (issue #3198), the method that runs is the refinement's or `Enumerable`'s,
29
+ # and this does not tell which, so the block enters with what `Enumerable`'s would bind read untyped. Any other
30
+ # pattern (`grep(Integer)`) keeps the frame-wide entry too, since its `===` may leave `$~` alone.
22
31
  module YieldingMatch
23
32
  NAMES = Set[:scan, :gsub, :gsub!, :sub, :sub!].freeze
24
- private_constant :NAMES
33
+ GREP_NAMES = Set[:grep, :grep_v].freeze
34
+ private_constant :NAMES, :GREP_NAMES
25
35
 
26
36
  module_function
27
37
 
28
38
  # `scope` with the match globals `call_node`'s block enters with; `scope` itself for any other block.
29
39
  # `operand_scope` types the receiver.
30
40
  def entry(scope, block_node, call_node, operand_scope)
31
- return scope unless yielding_call?(block_node, call_node) &&
32
- Narrowing.string_match_receiver?(operand_scope.type_of(call_node.receiver), operand_scope)
41
+ return scope unless own_block_call?(block_node, call_node)
42
+
43
+ name = call_node.name
44
+ if NAMES.include?(name)
45
+ string_entry(scope, call_node, operand_scope)
46
+ elsif GREP_NAMES.include?(name)
47
+ grep_entry(scope, call_node, operand_scope)
48
+ else
49
+ scope
50
+ end
51
+ end
52
+
53
+ def string_entry(scope, call_node, operand_scope)
54
+ return scope unless Narrowing.string_match_receiver?(operand_scope.type_of(call_node.receiver), operand_scope)
55
+
56
+ Narrowing.yielded_match_scope(scope, pattern_argument(call_node))
57
+ end
58
+
59
+ def grep_entry(scope, call_node, operand_scope)
60
+ pattern = pattern_argument(call_node)
61
+ return scope unless pattern.is_a?(Prism::RegularExpressionNode) ||
62
+ pattern.is_a?(Prism::InterpolatedRegularExpressionNode)
63
+ return scope if project_method?(call_node, operand_scope)
64
+
65
+ entry = call_node.name == :grep_v ? scope.forget_match_globals : Narrowing.yielded_match_scope(scope, pattern)
66
+ refined?(call_node, operand_scope) ? entry.untyped_match_globals : entry
67
+ end
68
+
69
+ # True when the receiver answers the call with a method the project defines, or is a carrier whose class
70
+ # cannot be resolved; an untyped receiver is not one.
71
+ def project_method?(call_node, operand_scope)
72
+ targets = ProjectMethodOwnership.targets(operand_scope.type_of(call_node.receiver))
73
+ return true if targets.nil?
74
+
75
+ targets.any? do |class_name, kind|
76
+ ProjectMethodOwnership.defines?(class_name, call_node.name, kind, operand_scope)
77
+ end
78
+ end
79
+
80
+ # Issue #3198 — true when a refinement in effect at the call defines its name on any class: a refined method
81
+ # is Ruby's, and matches, if at all, in a frame of its own. The refined class is not compared with the
82
+ # receiver, so the call may still be `Enumerable`'s.
83
+ def refined?(call_node, operand_scope)
84
+ SelfPreservingBlock.refined_in_effect?(call_node, call_node.name, operand_scope)
85
+ end
33
86
 
34
- Narrowing.yielded_match_scope(scope)
87
+ # The call's first argument, which each of these methods takes as its pattern; nil without one.
88
+ def pattern_argument(call_node)
89
+ call_node.arguments&.arguments&.first
35
90
  end
36
91
 
37
- def yielding_call?(block_node, call_node)
38
- call_node.is_a?(Prism::CallNode) && NAMES.include?(call_node.name) && !call_node.receiver.nil? &&
92
+ def own_block_call?(block_node, call_node)
93
+ call_node.is_a?(Prism::CallNode) && !call_node.receiver.nil? &&
39
94
  block_node.is_a?(Prism::BlockNode) && call_node.block.equal?(block_node)
40
95
  end
41
- private_class_method :yielding_call?
96
+ private_class_method :string_entry, :grep_entry, :project_method?, :refined?, :pattern_argument,
97
+ :own_block_call?
42
98
  end
43
99
  end
44
100
  end
@@ -7,6 +7,8 @@ require_relative "../type"
7
7
  require_relative "block_call_timing"
8
8
  require_relative "fresh_frame_blocks"
9
9
  require_relative "guard_rebinding"
10
+ require_relative "project_method_ownership"
11
+ require_relative "self_preserving_block"
10
12
  require_relative "stored_block_call"
11
13
  require_relative "match_rebinding/frame"
12
14
  require_relative "match_rebinding/operands"
@@ -364,14 +366,20 @@ module Rigor
364
366
  # ractor's root block with the globals unbound, since it reads a slot of its own, and a `define_method` body
365
367
  # with a narrowed global untyped, since it reads the definer's slot whenever the method is called. `$_`, which
366
368
  # shares the slot, enters on the same terms ({LastLine.block_entry}, issue #1359). A closure's body, which runs
367
- # whenever it is called, enters with `$!`, `$@` and `$?` unbound ({FreshFrameBlocks.closure_entry}, issue #1360).
369
+ # whenever it is called, enters with `$!`, `$@` and `$?` unbound ({FreshFrameBlocks.closure_entry}, issue #1360),
370
+ # and with the match globals unbound whatever its body does ({FreshFrameBlocks.proc_body?}, issue #1371): the
371
+ # narrowing where a lambda or proc is written proves nothing about the `$~` it reads when it is called. The
372
+ # block of `Enumerator.new` or `Hash.new` on the core class runs later too, but often in the statement that
373
+ # makes it, so where none of the rules above forgets them it enters with a narrowed global untyped
374
+ # ({FreshFrameBlocks.deferred_constructor?}, issue #3195).
368
375
  #
369
376
  # Issue #1429 — the entry also restores a guard's narrowing of a global or constant where the body may run after
370
377
  # code that rebinds it ({GuardRebinding.block_entry}).
371
378
  #
372
- # Issue #2443 — the block of a call that matches before every yield (`scan`, `gsub`, `sub`, …) on a String or
373
- # untyped receiver enters with that call's own match edge, laid over the entry above ({YieldingMatch}); a match
374
- # its body runs still rebinds them through the statement-level rules.
379
+ # Issue #2443, #1371 — the block of a call that matches before every yield (`scan`, `gsub`, `sub`, … on a String
380
+ # or untyped receiver, `grep` and `grep_v` with a regex literal) enters with that call's own match edge in place
381
+ # of the entry above ({YieldingMatch}); a match its body runs still rebinds them through the statement-level
382
+ # rules.
375
383
  def block_entry(scope, block_node, call_node = nil)
376
384
  operand_scope = scope
377
385
  scope = GuardRebinding.block_entry(scope, block_node, call_node)
@@ -384,10 +392,15 @@ module Rigor
384
392
 
385
393
  def frame_entry(scope, block_node, call_node)
386
394
  return scope unless scope.match_globals_bound?
387
- return scope unless scope.match_rebinding_closure? || entry_may_match?(block_node.body, scope, call_node) ||
388
- (call_node.is_a?(Prism::CallNode) && operands_may_rebind?(call_node, scope))
395
+ return scope.forget_match_globals if entry_forgets?(scope, block_node, call_node)
389
396
 
390
- scope.forget_match_globals
397
+ FreshFrameBlocks.deferred_constructor?(block_node, call_node, scope) ? scope.untyped_match_globals : scope
398
+ end
399
+
400
+ def entry_forgets?(scope, block_node, call_node)
401
+ FreshFrameBlocks.proc_body?(block_node, call_node, scope) ||
402
+ scope.match_rebinding_closure? || entry_may_match?(block_node.body, scope, call_node) ||
403
+ (call_node.is_a?(Prism::CallNode) && operands_may_rebind?(call_node, scope))
391
404
  end
392
405
 
393
406
  def entry_may_match?(body, scope, call_node)
@@ -396,7 +409,7 @@ module Rigor
396
409
 
397
410
  body.is_a?(Prism::Node) && scan(body, scope, base: true)
398
411
  end
399
- private_class_method :frame_entry, :entry_may_match?
412
+ private_class_method :frame_entry, :entry_forgets?, :entry_may_match?
400
413
  end
401
414
  end
402
415
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../../type"
4
4
  require_relative "iterator_dispatch"
5
+ require_relative "core_enumerable_receiver"
5
6
 
6
7
  module Rigor
7
8
  module Inference
@@ -37,12 +38,12 @@ module Rigor
37
38
  module BlockFolding
38
39
  module_function
39
40
 
40
- FILTER_KEEP_ON_TRUTHY = Set[:select, :filter, :take_while].freeze
41
+ FILTER_KEEP_ON_TRUTHY = Set[:select, :filter, :find_all, :take_while].freeze
41
42
  FILTER_KEEP_ON_FALSEY = Set[:reject, :drop_while].freeze
42
43
 
43
- # The filter methods `Hash` defines itself, returning a Hash. `take_while` / `drop_while` on a Hash
44
- # are Enumerable's and return an Array of `[key, value]` pairs; `Set` and `Range` define none of the
45
- # five, so all of theirs return an Array too.
44
+ # The filter methods `Hash` defines itself, returning a Hash. `find_all` / `take_while` /
45
+ # `drop_while` on a Hash are Enumerable's and return an Array of `[key, value]` pairs; `Set` and
46
+ # `Range` define none of the six, so all of theirs return an Array too.
46
47
  HASH_RETURNING_FILTERS = Set[:select, :filter, :reject].freeze
47
48
 
48
49
  # `filter_receiver_kind`'s answer for a nominal receiver; an unlisted class declines.
@@ -52,21 +53,22 @@ module Rigor
52
53
 
53
54
  PREDICATE_METHODS = Set[:all?, :any?, :none?].freeze
54
55
 
55
- # Methods whose answer is `nil` when the block always returns Ruby-falsey — `find` / `detect`
56
- # short-circuit to nil when nothing matches, `find_index` / `index` likewise. These methods only fold
57
- # on the falsey side for now; the truthy-block side requires per-position analysis (the index of the
58
- # first kept element, or the element itself, depend on the receiver's shape and on which positions
59
- # actually evaluate to truthy).
60
- FALSEY_BLOCK_NIL_METHODS = Set[:find, :detect, :find_index, :index].freeze
56
+ # Methods whose answer is `nil` when the block always returns Ruby-falsey — `find` / `detect` /
57
+ # `rfind` short-circuit to nil when nothing matches, `find_index` / `index` / `rindex` likewise.
58
+ # These methods only fold on the falsey side here; the truthy-block side requires per-position
59
+ # analysis.
60
+ FALSEY_BLOCK_NIL_METHODS = Set[:find, :detect, :rfind, :find_index, :index, :rindex].freeze
61
+ ARRAY_ONLY_FALSEY_NIL_METHODS = Set[:rfind, :index, :rindex].freeze
61
62
 
62
63
  # Block-taking `count` returns the number of elements for which the block is truthy. With a
63
64
  # Constant-falsey block the answer is unconditionally `Constant[0]`; with a Constant-truthy block on
64
65
  # a finitely-sized receiver it is `Constant[size]`.
65
66
  COUNT_METHOD = :count
66
67
 
67
- # `min_by` / `max_by` pick one element by the block's key, so the block's value decides WHICH element
68
- # but never WHETHER one is returned: only an empty receiver answers `nil`.
69
- EDGE_BY_METHODS = Set[:min_by, :max_by].freeze
68
+ # `min_by` / `max_by` / `minmax_by` pick edge elements by the block's key, so the block's value
69
+ # decides WHICH element(s) but never WHETHER one is returned: only an empty receiver answers `nil`
70
+ # (or `[nil, nil]` for `minmax_by`).
71
+ EDGE_BY_METHODS = Set[:min_by, :max_by, :minmax_by].freeze
70
72
 
71
73
  # `context.block_type` is the inferred return type of the call's block; `nil` means "no block at the
72
74
  # call site" and disqualifies every rule here.
@@ -76,32 +78,44 @@ module Rigor
76
78
  args = context.args
77
79
  block_type = context.block_type
78
80
  return nil if receiver.nil? || block_type.nil?
79
- return fold_edge_by(receiver, args) if EDGE_BY_METHODS.include?(method_name)
81
+ return fold_edge_by(receiver, method_name, args) if EDGE_BY_METHODS.include?(method_name)
80
82
 
83
+ # Issue #3242 — the predicate, `find`-family and `count` folds read no receiver shape, so a class that
84
+ # may define the method itself declines (the filter folds already do, through `filter_receiver_kind`).
81
85
  truthiness = constant_truthiness(block_type)
82
- return nil if truthiness.nil?
86
+ return nil if truthiness.nil? || CoreEnumerableReceiver.project_receiver?(receiver)
83
87
 
84
88
  if PREDICATE_METHODS.include?(method_name)
85
89
  fold_predicate(receiver, method_name, truthiness, args)
86
90
  elsif filter_method?(method_name)
87
91
  fold_filter(receiver, method_name, truthiness)
88
92
  elsif FALSEY_BLOCK_NIL_METHODS.include?(method_name)
89
- fold_falsey_nil_short_circuit(method_name, truthiness, args)
93
+ fold_falsey_nil_short_circuit(receiver, method_name, truthiness, args)
90
94
  elsif method_name == COUNT_METHOD
91
95
  fold_count(receiver, truthiness, args)
92
96
  end
93
97
  end
94
98
 
95
- # Issue #1333 — `min_by` / `max_by` with a block and no count on a non-empty Tuple or a non-empty
96
- # constant integer `Range` answer the receiver's element type: the RBS `Elem?` return adds `nil` for
97
- # the empty receiver, and `[1, 2].min_by { |s| rand(3) } + 1` then reported a nil receiver on correct
98
- # code. The block's value picks which element, and one `block_type` typed from the call's entry scope
99
- # cannot say which, so the answer is the whole element union. An empty receiver, one whose size is
100
- # not static (`Array[T]`), and the count form (`min_by(2) { … }`, an Array) decline to RBS.
101
- def fold_edge_by(receiver, args)
99
+ # Issue #1333 — `min_by` / `max_by` / `minmax_by` with a block and no count on a Tuple or constant
100
+ # integer `Range` answer the receiver's element type without `nil` when non-empty (or `[nil, nil]`
101
+ # for an empty `minmax_by`), avoiding false `nil` receivers on `min_by` / `minmax_by` calls.
102
+ def fold_edge_by(receiver, method_name, args)
102
103
  return nil unless args.empty?
103
- return nil unless receiver_emptiness(receiver) == :non_empty
104
104
 
105
+ emptiness = receiver_emptiness(receiver)
106
+ if method_name == :minmax_by && emptiness == :empty
107
+ nil_const = Type::Combinator.constant_of(nil)
108
+ return Type::Combinator.tuple_of(nil_const, nil_const)
109
+ end
110
+ return nil unless emptiness == :non_empty
111
+
112
+ element = edge_by_element_type(receiver)
113
+ return nil if element.nil?
114
+
115
+ method_name == :minmax_by ? Type::Combinator.tuple_of(element, element) : element
116
+ end
117
+
118
+ def edge_by_element_type(receiver)
105
119
  case receiver
106
120
  when Type::Tuple then Type::Combinator.union(*receiver.elements)
107
121
  when Type::Constant then IteratorDispatch.element_type_of(receiver) if receiver.value.is_a?(Range)
@@ -288,13 +302,13 @@ module Rigor
288
302
  end
289
303
  end
290
304
 
291
- # `find` / `detect` / `find_index` / `index` (block form) short-circuit to nil when the block is
292
- # provably falsey. `index` and `find_index` also accept a non-block argument form
293
- # (`arr.index(value)`); we decline whenever the call carries a positional argument so the RBS tier
294
- # still answers the value-search variant correctly.
295
- def fold_falsey_nil_short_circuit(_method_name, truthiness, args)
305
+ # `find` / `detect` / `find_index` (and Array-only `rfind` / `index` / `rindex`) short-circuit to
306
+ # nil when the block is provably falsey. Calls with a positional argument decline so the RBS tier
307
+ # answers value-search calls.
308
+ def fold_falsey_nil_short_circuit(receiver, method_name, truthiness, args)
296
309
  return nil unless args.empty?
297
310
  return nil unless truthiness == :falsey
311
+ return nil if ARRAY_ONLY_FALSEY_NIL_METHODS.include?(method_name) && filter_receiver_kind(receiver) != :array
298
312
 
299
313
  Type::Combinator.constant_of(nil)
300
314
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../type"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module MethodDispatcher
8
+ # Issue #2686 — `Rigor::Plugin::Base.node_rule(node_class) { |node, ...| }` yields instances of the class it is
9
+ # given, which RBS cannot say: `[N < ::Prism::Node] (singleton(N))` does not resolve (RBS has no
10
+ # `singleton(<type variable>)`). `sig/rigor/plugin/base.rbs` declares per-class overloads for the classes
11
+ # the bundled plugins register and a `Prism::Node` fallback. Read through the overloads alone, two calls bind
12
+ # `node` wrong:
13
+ #
14
+ # - a class with no overload of its own (`node_rule(Prism::IfNode)`) takes the fallback, so `node` is
15
+ # `Prism::Node` and `node.predicate` reported `call.undefined-method`;
16
+ # - a class argument the engine cannot read (`def self.reg(k) = node_rule(k) { ... }`) matches every overload
17
+ # gradually, and the block probe took the first, `Prism::CallNode`.
18
+ #
19
+ # This rebinds the block's first parameter from the class argument:
20
+ #
21
+ # - `singleton(X)` binds `X`, and a union of class objects the union of their instances;
22
+ # - any other argument (an untyped parameter, a `Class` value) binds `Dynamic[top]`, so the body is not
23
+ # checked against a class the call may not register.
24
+ #
25
+ # The rest of the block's parameters keep the overload's binding; every overload declares them alike.
26
+ #
27
+ # A per-class overload set generated from Prism's node classes was the alternative. It was declined: the
28
+ # gemspec admits `prism >= 1.0, < 2.0`, and a generated set names classes later minors added, which an older
29
+ # Prism's RBS does not declare, so loading Rigor's own `sig/` would fail there.
30
+ #
31
+ # Only the declaration Rigor owns is read, by the method's RBS owner, so a project's own `node_rule` is not.
32
+ module ClassKeyedBlockParams
33
+ OWNER = "::Rigor::Plugin::Base"
34
+ METHOD = :node_rule
35
+ private_constant :OWNER, :METHOD
36
+
37
+ module_function
38
+
39
+ # @param method_definition — the RBS declaration the probe read.
40
+ # @param params — the block parameters the overloads bound.
41
+ # @return the block parameters, the first rebound for `Base.node_rule`.
42
+ def rebind(method_definition, method_name, args, params)
43
+ return params unless method_name == METHOD
44
+ return params unless method_definition.defined_in.to_s == OWNER
45
+
46
+ [instance_of(args.first), *params.drop(1)]
47
+ end
48
+
49
+ # The instances the class argument names, or `Dynamic[top]`.
50
+ def instance_of(arg)
51
+ case arg
52
+ when Type::Singleton
53
+ Type::Combinator.nominal_of(arg.class_name)
54
+ when Type::Union
55
+ return Type::Combinator.untyped unless arg.members.all?(Type::Singleton)
56
+
57
+ Type::Combinator.union(*arg.members.map { |member| Type::Combinator.nominal_of(member.class_name) })
58
+ else
59
+ Type::Combinator.untyped
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../type"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module MethodDispatcher
8
+ # Issue #3242 — whether a receiver's `find` / `detect` / `find_index` / `count` / `all?` / `any?` / `none?`
9
+ # may be other than core's. {BlockFolding}'s constant-block folds for those methods read no receiver shape, so
10
+ # they answer core's semantics, and they are sound only on a carrier known to be a core collection or
11
+ # enumerable. Anything else may define the method itself: a project class (`class Bag; def find = (yield; 8);
12
+ # end`), a subclass of a core class (its own `Nominal`), a class object (`def self.find`), a `Data.define` or
13
+ # `Struct.new` class, or a scalar that has no such method at all.
14
+ module CoreEnumerableReceiver
15
+ module_function
16
+
17
+ # The nominal receivers whose methods above are core's: Array's, Hash's, Set's, Range's own, or
18
+ # Enumerable's. `RBS::Unnamed::ENVClass` is `ENV`, which extends Enumerable.
19
+ NOMINALS = Ractor.make_shareable(
20
+ Set[
21
+ "Array", "Hash", "Set", "Range", "Enumerable", "Enumerator", "Enumerator::Lazy",
22
+ "Enumerator::ArithmeticSequence", "Enumerator::Chain", "Enumerator::Product", "Dir",
23
+ "RBS::Unnamed::ENVClass"
24
+ ].freeze
25
+ )
26
+
27
+ # False only for a carrier known to be a core collection or enumerable: a `Tuple` / `HashShape` literal
28
+ # shape, a `Constant` `Range` or `Set`, a `Nominal` in {NOMINALS}, `Top`, and a `Dynamic` / `Difference` /
29
+ # `Refined` / `Union` / `Intersection` over those. Every other receiver is true, so the folds decline.
30
+ def project_receiver?(receiver)
31
+ case receiver
32
+ when Type::Tuple, Type::HashShape, Type::Top then false
33
+ when Type::Constant then !(receiver.value.is_a?(Range) || receiver.value.is_a?(::Set))
34
+ when Type::Nominal then !NOMINALS.include?(receiver.class_name)
35
+ when Type::Difference, Type::Refined then project_receiver?(receiver.base)
36
+ when Type::Union, Type::Intersection then receiver.members.any? { |member| project_receiver?(member) }
37
+ when Type::Dynamic then project_receiver?(receiver.static_facet)
38
+ else true
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end