rigortype 0.4.1 → 0.4.3

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 (230) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/builtins/ruby_core/io.yml +1 -0
  4. data/data/builtins/ruby_core/proc.yml +4 -0
  5. data/data/builtins/ruby_core/random.yml +4 -0
  6. data/data/builtins/ruby_core/struct.yml +3 -0
  7. data/data/core_overlay/env.rbs +34 -0
  8. data/data/core_overlay/gc.rbs +50 -0
  9. data/data/core_overlay/hash.rbs +45 -0
  10. data/data/core_overlay/integer.rbs +31 -0
  11. data/data/core_overlay/io_buffer.rbs +27 -0
  12. data/data/core_overlay/kernel.rbs +40 -0
  13. data/data/core_overlay/match_data.rbs +28 -0
  14. data/data/core_overlay/module.rbs +51 -0
  15. data/data/core_overlay/object_space.rbs +29 -0
  16. data/data/core_overlay/proc.rbs +45 -0
  17. data/data/core_overlay/range.rbs +40 -0
  18. data/data/core_overlay/source_range.rbs +78 -0
  19. data/data/core_overlay/string.rbs +86 -0
  20. data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
  21. data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
  22. data/docs/handbook/01-getting-started.md +3 -3
  23. data/docs/handbook/06-classes.md +3 -1
  24. data/docs/handbook/11-sig-gen.md +18 -5
  25. data/docs/handbook/appendix-liskov.md +5 -0
  26. data/docs/manual/02-cli-reference.md +19 -1
  27. data/docs/manual/03-configuration.md +7 -5
  28. data/docs/manual/04-diagnostics.md +2 -1
  29. data/docs/manual/05-inspecting-types.md +50 -0
  30. data/docs/manual/06-baseline.md +2 -0
  31. data/docs/manual/07-plugins.md +1 -1
  32. data/docs/manual/12-caching.md +18 -1
  33. data/docs/manual/14-rails-quickstart.md +2 -2
  34. data/docs/manual/18-removing-dead-code.md +41 -4
  35. data/docs/manual/plugins/README.md +6 -1
  36. data/docs/manual/plugins/rigor-activerecord.md +21 -0
  37. data/docs/manual/plugins/rigor-alba.md +60 -0
  38. data/docs/manual/plugins/rigor-minitest.md +22 -4
  39. data/docs/manual/plugins/rigor-typelizer.md +71 -0
  40. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
  41. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
  42. data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
  43. data/lib/rigor/analysis/check_rules/rule_ids.rb +11 -0
  44. data/lib/rigor/analysis/check_rules/source_arity.rb +21 -2
  45. data/lib/rigor/analysis/check_rules.rb +292 -59
  46. data/lib/rigor/analysis/dependency_recorder.rb +15 -0
  47. data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
  48. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
  49. data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
  50. data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
  51. data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
  52. data/lib/rigor/analysis/erb_template_detector.rb +2 -1
  53. data/lib/rigor/analysis/incremental.rb +29 -9
  54. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +30 -5
  55. data/lib/rigor/analysis/incremental_run_slot.rb +6 -4
  56. data/lib/rigor/analysis/incremental_session.rb +103 -23
  57. data/lib/rigor/analysis/path_expansion.rb +62 -6
  58. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
  59. data/lib/rigor/analysis/reachability/graph.rb +241 -37
  60. data/lib/rigor/analysis/reachability/scan.rb +490 -77
  61. data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
  62. data/lib/rigor/analysis/rule_catalog.rb +43 -0
  63. data/lib/rigor/analysis/run_cache_key.rb +16 -1
  64. data/lib/rigor/analysis/run_cache_probe.rb +1 -1
  65. data/lib/rigor/analysis/run_stats.rb +28 -6
  66. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +113 -6
  67. data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -19
  68. data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
  69. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  70. data/lib/rigor/analysis/runner.rb +62 -13
  71. data/lib/rigor/analysis/worker_session.rb +8 -5
  72. data/lib/rigor/cache/engine_source.rb +21 -7
  73. data/lib/rigor/cache/incremental_snapshot.rb +55 -5
  74. data/lib/rigor/cli/annotate_command.rb +5 -0
  75. data/lib/rigor/cli/baseline_command.rb +7 -18
  76. data/lib/rigor/cli/check_command.rb +140 -40
  77. data/lib/rigor/cli/docs_command.rb +5 -4
  78. data/lib/rigor/cli/lens_command.rb +144 -0
  79. data/lib/rigor/cli/lsp_command.rb +5 -2
  80. data/lib/rigor/cli/skill_command.rb +3 -3
  81. data/lib/rigor/cli/skill_describe.rb +1 -1
  82. data/lib/rigor/cli/trace_command.rb +5 -0
  83. data/lib/rigor/cli/type_of_command.rb +6 -3
  84. data/lib/rigor/cli/unused_command.rb +166 -27
  85. data/lib/rigor/cli.rb +39 -5
  86. data/lib/rigor/configuration/severity_profile.rb +6 -0
  87. data/lib/rigor/configuration.rb +61 -2
  88. data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
  89. data/lib/rigor/environment/gated_signature_guard.rb +43 -0
  90. data/lib/rigor/environment/gated_signature_plan.rb +339 -0
  91. data/lib/rigor/environment/installed_gem_set.rb +38 -11
  92. data/lib/rigor/environment/lockfile_resolver.rb +37 -6
  93. data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
  94. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  95. data/lib/rigor/environment/rbs_loader.rb +316 -14
  96. data/lib/rigor/environment/required_features.rb +208 -0
  97. data/lib/rigor/environment.rb +33 -8
  98. data/lib/rigor/inference/acceptance.rb +28 -3
  99. data/lib/rigor/inference/builtins/cruby_definers.rb +78 -0
  100. data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
  101. data/lib/rigor/inference/captured_locals.rb +43 -3
  102. data/lib/rigor/inference/def_node_resolver.rb +23 -1
  103. data/lib/rigor/inference/expression_typer.rb +261 -29
  104. data/lib/rigor/inference/global_write_census.rb +7 -4
  105. data/lib/rigor/inference/guard_rebinding.rb +1 -1
  106. data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
  107. data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
  108. data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +286 -0
  109. data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
  110. data/lib/rigor/inference/in_effect_refinements.rb +256 -0
  111. data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
  112. data/lib/rigor/inference/key_presence_guard.rb +303 -0
  113. data/lib/rigor/inference/macro_block_self_type.rb +59 -12
  114. data/lib/rigor/inference/match_rebinding.rb +1 -1
  115. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  116. data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
  117. data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
  118. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
  119. data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
  120. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
  121. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
  122. data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
  123. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
  124. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
  125. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
  126. data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
  127. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
  128. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +247 -28
  129. data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
  130. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
  131. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
  132. data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
  133. data/lib/rigor/inference/method_dispatcher.rb +96 -14
  134. data/lib/rigor/inference/module_function_state.rb +2 -1
  135. data/lib/rigor/inference/narrowing.rb +20 -6
  136. data/lib/rigor/inference/object_mixins.rb +180 -0
  137. data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
  138. data/lib/rigor/inference/project_patched_methods.rb +12 -7
  139. data/lib/rigor/inference/project_patched_scanner.rb +108 -26
  140. data/lib/rigor/inference/refine_census.rb +239 -0
  141. data/lib/rigor/inference/refine_self.rb +317 -0
  142. data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
  143. data/lib/rigor/inference/refined_dispatch/own_method.rb +475 -0
  144. data/lib/rigor/inference/refined_dispatch.rb +320 -0
  145. data/lib/rigor/inference/repeated_or_writes.rb +2 -0
  146. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +13 -0
  147. data/lib/rigor/inference/scope_indexer.rb +827 -65
  148. data/lib/rigor/inference/statement_evaluator.rb +294 -51
  149. data/lib/rigor/inference/string_mutation.rb +9 -3
  150. data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
  151. data/lib/rigor/inference/untyped_rejoin.rb +75 -0
  152. data/lib/rigor/inference/version_guard.rb +19 -10
  153. data/lib/rigor/language_server/project_context.rb +87 -5
  154. data/lib/rigor/language_server/server.rb +18 -0
  155. data/lib/rigor/lens/anchor.rb +115 -0
  156. data/lib/rigor/lens/contract_reader.rb +118 -0
  157. data/lib/rigor/lens/document.rb +113 -0
  158. data/lib/rigor/lens/file_reader.rb +433 -0
  159. data/lib/rigor/lens/members.rb +153 -0
  160. data/lib/rigor/lens/parameters.rb +91 -0
  161. data/lib/rigor/lens/plugins.rb +40 -0
  162. data/lib/rigor/lens/run.rb +280 -0
  163. data/lib/rigor/lens/slot.rb +79 -0
  164. data/lib/rigor/lens.rb +40 -0
  165. data/lib/rigor/plugin/inflector.rb +1 -1
  166. data/lib/rigor/plugin/io_boundary.rb +42 -3
  167. data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
  168. data/lib/rigor/project_environment.rb +48 -6
  169. data/lib/rigor/protection/mutation_cache.rb +3 -2
  170. data/lib/rigor/protection/mutator.rb +1 -1
  171. data/lib/rigor/reflection/constant_ancestors.rb +127 -11
  172. data/lib/rigor/reflection/constant_path.rb +13 -2
  173. data/lib/rigor/reflection.rb +40 -8
  174. data/lib/rigor/scope/discovery_index.rb +40 -1
  175. data/lib/rigor/scope/resolution_chain.rb +159 -11
  176. data/lib/rigor/scope.rb +211 -21
  177. data/lib/rigor/sig_gen/classification.rb +4 -1
  178. data/lib/rigor/sig_gen/generator.rb +100 -10
  179. data/lib/rigor/sig_gen/method_candidate.rb +2 -2
  180. data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
  181. data/lib/rigor/source/alias_names.rb +52 -0
  182. data/lib/rigor/source/literals.rb +3 -2
  183. data/lib/rigor/source/symbol_name.rb +20 -0
  184. data/lib/rigor/type/combinator.rb +34 -2
  185. data/lib/rigor/type/float_range.rb +4 -1
  186. data/lib/rigor/type/nominal.rb +6 -1
  187. data/lib/rigor/version.rb +1 -1
  188. data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
  189. data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
  190. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
  191. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
  192. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
  193. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
  194. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
  195. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
  196. data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
  197. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
  198. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
  199. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
  200. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
  201. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
  202. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
  203. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
  204. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
  205. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
  206. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
  207. data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
  208. data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
  209. data/plugins/rigor-alba/sig/alba.rbs +4 -0
  210. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
  211. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
  212. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +7 -6
  213. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
  214. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
  215. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
  216. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
  217. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
  218. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
  219. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
  220. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
  221. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
  222. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
  223. data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
  224. data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
  225. data/sig/rigor/plugin/io_boundary.rbs +3 -0
  226. data/sig/rigor/scope.rbs +30 -4
  227. data/sig/rigor/source.rbs +4 -0
  228. data/sig/rigor.rbs +9 -4
  229. data/skills/rigor-project-init/references/02-configure.md +2 -2
  230. metadata +83 -1
@@ -31,6 +31,7 @@ require_relative "match_rebinding"
31
31
  require_relative "element_read_widening"
32
32
  require_relative "hash_lookup_mutation"
33
33
  require_relative "indexed_narrowing"
34
+ require_relative "key_presence_guard"
34
35
  require_relative "index_write_widening"
35
36
  require_relative "method_dispatcher"
36
37
  require_relative "method_parameter_binder"
@@ -41,8 +42,10 @@ require_relative "operand_effects"
41
42
  require_relative "operand_walk"
42
43
  require_relative "optimistic_origin"
43
44
  require_relative "return_barrier"
45
+ require_relative "repeated_or_writes"
44
46
  require_relative "rewrite_mutation"
45
47
  require_relative "unknown_store_widening"
48
+ require_relative "untyped_rejoin"
46
49
  require_relative "version_guard"
47
50
 
48
51
  module Rigor
@@ -277,11 +280,15 @@ module Rigor
277
280
  # a declined `class self::X`, a parse-error header), where `class_context` is empty without the code
278
281
  # being at the top level. A `def` (or `class << self`) there runs under an untyped `self` instead of
279
282
  # the top level's nil one, so its implicit-self calls answer `Dynamic` and no top-level rule fires.
283
+ # @param widen_clauses — issue #1231: when true (and a recorder is installed), a rescue or `ensure` clause
284
+ # records its per-node entry scopes under {#widen_clause_entry}'s widening. The check path sets it; every
285
+ # other reader of the index (parameter inference, sig-gen, the scanners) keeps the clause's plain entry.
280
286
  def initialize(scope:, tracer: nil, on_enter: nil, class_context: [].freeze, # rubocop:disable Metrics/ParameterLists
281
287
  lexical_nesting: EMPTY_NESTING, converged_loop_recording: false, next_scope_sink: nil,
282
288
  operand_scope: nil, in_operand: false, operand_recorder: nil, operand_types: nil,
283
- opaque_class: false)
289
+ opaque_class: false, widen_clauses: false)
284
290
  @scope = scope
291
+ @widen_clauses = widen_clauses
285
292
  @opaque_class = opaque_class
286
293
  @tracer = tracer
287
294
  @on_enter = on_enter
@@ -352,7 +359,7 @@ module Rigor
352
359
  @on_enter&.call(node, @scope)
353
360
 
354
361
  handler = HANDLERS[node.class]
355
- return forget_implicit_call_guards(node, send(handler, node)) if handler
362
+ return forget_aliased_key_guards(node, forget_implicit_call_guards(node, send(handler, node))) if handler
356
363
 
357
364
  # Default: the node is treated as a pure expression. Type it through the existing expression typer (which
358
365
  # observes the current scope's locals) and leave the scope unchanged, but for the match globals a call in it
@@ -373,6 +380,16 @@ module Rigor
373
380
  [type, after.forget_guard_narrowings]
374
381
  end
375
382
 
383
+ # Issue #1703 — a write that stores a `key?` guard's receiver or key root elsewhere (`g = h`) aliases it, so
384
+ # the guard ends ({KeyPresenceGuard.invalidate_after_write}).
385
+ def forget_aliased_key_guards(node, result)
386
+ type, after = result
387
+ return result unless after.is_a?(Scope)
388
+
389
+ guarded = KeyPresenceGuard.invalidate_after_write(node, after)
390
+ guarded.equal?(after) ? result : [type, guarded]
391
+ end
392
+
376
393
  # Issue #1359 — the nodes whose handler runs their parts in order, each from the scope the parts before it
377
394
  # left, so a `$_` reader among them forgets it where it runs ({LastLine}) and a read before it keeps the
378
395
  # narrowing.
@@ -473,9 +490,10 @@ module Rigor
473
490
  # `opts[k] ||= raise KeyError` is a guard, never `bot`, and `@flags[n] ||= false` is `true` after an
474
491
  # `@flags[n] = true` elsewhere, never provably `false`.
475
492
  #
476
- # Nor is a site a block-return pass marked (`Scope#repeated_or_write?`, {RepeatedOrWrites}): the pass types
477
- # every run of a repeating body from one entry scope, so its slot's gradual type may be what an EARLIER run
478
- # stored rather than an absence of evidence.
493
+ # Nor is a marked site (`Scope#repeated_or_write?`, {RepeatedOrWrites}): a block-return pass, and this
494
+ # evaluator's statement and write-back passes ({#with_repeated_or_write_marks}), type every run of a repeating
495
+ # body from one entry scope, so its slot's gradual type may be what an EARLIER run stored rather than an
496
+ # absence of evidence.
479
497
  def index_compound_write_value(node)
480
498
  return index_write_stored_type(node, scope) unless node.is_a?(Prism::IndexOrWriteNode)
481
499
 
@@ -1169,7 +1187,8 @@ module Rigor
1169
1187
 
1170
1188
  [
1171
1189
  Type::Combinator.union(then_type, else_type),
1172
- join_with_nil_injection(then_scope, else_scope)
1190
+ UntypedRejoin.apply(join_with_nil_injection(then_scope, else_scope), post_pred,
1191
+ [node.statements, node.subsequent])
1173
1192
  ]
1174
1193
  end
1175
1194
 
@@ -1204,7 +1223,8 @@ module Rigor
1204
1223
 
1205
1224
  [
1206
1225
  Type::Combinator.union(then_type, else_type),
1207
- join_with_nil_injection(then_scope, else_scope)
1226
+ UntypedRejoin.apply(join_with_nil_injection(then_scope, else_scope), post_pred,
1227
+ [node.statements, node.else_clause])
1208
1228
  ]
1209
1229
  end
1210
1230
 
@@ -1275,7 +1295,7 @@ module Rigor
1275
1295
  subject_type, post_pred = node.predicate ? sub_eval(node.predicate, scope) : [nil, scope]
1276
1296
  branch_results, falsey_scope = eval_case_when_branches(subject_type, node.predicate, node.conditions, post_pred)
1277
1297
  if pattern_case_matches_every_path?(node, branch_results)
1278
- return unmatched_pattern_result(branch_results, node.conditions)
1298
+ return unmatched_pattern_result(branch_results, node.conditions, post_pred)
1279
1299
  end
1280
1300
 
1281
1301
  else_result = eval_case_else(node.else_clause, falsey_scope)
@@ -1284,7 +1304,7 @@ module Rigor
1284
1304
  branch_nodes = [*node.conditions, node.else_clause]
1285
1305
  [
1286
1306
  Type::Combinator.union(*all_results.map(&:first)),
1287
- join_case_branch_scopes(all_results, branch_nodes)
1307
+ join_case_branch_scopes(all_results, branch_nodes, post_pred)
1288
1308
  ]
1289
1309
  end
1290
1310
 
@@ -1299,8 +1319,9 @@ module Rigor
1299
1319
  node.is_a?(Prism::CaseMatchNode) && node.else_clause.nil? && !branch_results.empty?
1300
1320
  end
1301
1321
 
1302
- def unmatched_pattern_result(branch_results, branch_nodes)
1303
- [Type::Combinator.union(*branch_results.map(&:first)), join_case_branch_scopes(branch_results, branch_nodes)]
1322
+ def unmatched_pattern_result(branch_results, branch_nodes, entry)
1323
+ [Type::Combinator.union(*branch_results.map(&:first)),
1324
+ join_case_branch_scopes(branch_results, branch_nodes, entry)]
1304
1325
  end
1305
1326
 
1306
1327
  # Joins the post-scopes of every `when`/`in`/`else` branch, dropping the scope of any branch that terminates
@@ -1309,15 +1330,16 @@ module Rigor
1309
1330
  # `branch_terminates?` rule `eval_if`/`eval_unless` already apply to the if/else merge: e.g. `case x; when 1 then
1310
1331
  # v="a"; when 2 then v="b"; else raise; end` keeps `v: "a" | "b"` instead of `... | nil`. When every branch
1311
1332
  # terminates the merge is itself unreachable; fall back to the full join so the continuation scope stays
1312
- # well-formed.
1313
- def join_case_branch_scopes(results, nodes)
1333
+ # well-formed. A local the clauses only narrowed from `Dynamic[top]` re-joins to it ({UntypedRejoin}, issue
1334
+ # #1840), read against the post-predicate `entry`.
1335
+ def join_case_branch_scopes(results, nodes, entry)
1314
1336
  live = []
1315
1337
  results.each_with_index do |(type, branch_scope), i|
1316
1338
  live << branch_scope unless branch_terminates?(nodes[i], type)
1317
1339
  end
1318
1340
 
1319
1341
  live = results.map(&:last) if live.empty?
1320
- reduce_scopes_with_nil_injection(live)
1342
+ UntypedRejoin.apply(reduce_scopes_with_nil_injection(live), entry, nodes)
1321
1343
  end
1322
1344
 
1323
1345
  def eval_case_when_branches(subject_type, subject, conditions, entry_scope)
@@ -1466,7 +1488,8 @@ module Rigor
1466
1488
  # prefix of the body, and so reads it forgotten whenever the body may set it.
1467
1489
  entry = edge ? forget_guard_if_rebinds(LastLine.forget_if_set(scope, node), node) : scope
1468
1490
  primary_type, primary_scope = eval_begin_primary_under(node, entry, edge: edge)
1469
- rescue_chain = collect_rescue_chain_results(node.rescue_clause, rescue_entry_scope(node, entry), edge: edge)
1491
+ rescue_chain = collect_rescue_chain_results(node.rescue_clause, rescue_entry_scope(node, entry),
1492
+ edge: edge, writes: rescue_writes(node, edge))
1470
1493
 
1471
1494
  # B2.1 — retry-edge widening. When a `retry` in the rescue chain targets this `begin`, control re-enters the
1472
1495
  # primary body carrying every rebind made before the retry: the arm's (`rescue; tries += 1; retry; end`), and
@@ -1485,13 +1508,132 @@ module Rigor
1485
1508
  exit_scope = reduce_scopes_with_nil_injection([primary_scope, *live_rescues.map(&:last)])
1486
1509
  end
1487
1510
 
1488
- if node.ensure_clause
1489
- carry_jumps_through_ensure(node.ensure_clause, jump_marks)
1490
- _ensure_type, ensure_scope = sub_eval(node.ensure_clause, exit_scope)
1491
- exit_scope = ensure_scope
1511
+ [exit_type, node.ensure_clause ? eval_begin_ensure(node, exit_scope, jump_marks) : exit_scope]
1512
+ end
1513
+
1514
+ # The scope past `node`'s `ensure` clause, entered with `exit_scope`, the `next` and `break` scopes recorded past
1515
+ # `jump_marks` carried through it.
1516
+ def eval_begin_ensure(node, exit_scope, jump_marks)
1517
+ carry_jumps_through_ensure(node.ensure_clause, jump_marks)
1518
+ eval_widened_clause(node.ensure_clause, node.ensure_clause.statements, exit_scope, ensure_writes(node)).last
1519
+ end
1520
+
1521
+ # Issue #1231 — a rescue clause runs after the body raised from any point in it, and an `ensure` clause after the
1522
+ # body, its `else` or a rescue clause raised as well as after they finished, so a clause reads each local or
1523
+ # instance variable those wrote at a value the scope it enters with does not hold: `conn = nil; begin; conn =
1524
+ # open_conn; Integer(s); rescue; conn.size; end` reads the connection. The engine does not track where the body
1525
+ # raises (the precise recorder of #1397 put new false positives on correct code); it widens instead, and
1526
+ # deliberately imprecisely. Each name the clause mentions ({#clause_mentions}) that its entry binds and those
1527
+ # clauses write, outside a nested `def` or class body, reads as `Dynamic[top]`, which reports nothing. A plain
1528
+ # join would read `x` as `1?` in `x = 1; begin; foo; x = nil; rescue; x.succ; end`, where it cannot be nil, and a
1529
+ # `Dynamic` that keeps the join as its facet still narrows: `x.nil?` on `Dynamic[1 | 2]` leaves a `nil` member
1530
+ # for the next call to report. A name written inside a nested `ensure` clause, which also runs on the way out of
1531
+ # a raise, is covered by the same floor.
1532
+ #
1533
+ # The names the clauses of `node` read from: the primary body's for a rescue clause, none under a retry `edge`.
1534
+ def rescue_writes(node, edge)
1535
+ clause_writes([node.statements]) if node.rescue_clause && edge.nil? && widening_recorded?
1536
+ end
1537
+
1538
+ # For the `ensure` clause: the writes of the body, its `else` and every rescue clause.
1539
+ def ensure_writes(node)
1540
+ return nil unless widening_recorded?
1541
+
1542
+ sources = [node.statements, node.else_clause]
1543
+ current = node.rescue_clause
1544
+ while current
1545
+ sources << current
1546
+ current = current.subsequent
1492
1547
  end
1548
+ clause_writes(sources)
1549
+ end
1493
1550
 
1494
- [exit_type, exit_scope]
1551
+ # The local and instance variable names `sources` write, a nested block's included (it may write an outer local),
1552
+ # or nil for none. A `def` or class body writes nothing here.
1553
+ def clause_writes(sources)
1554
+ writes = nil
1555
+ sources.each { |source| writes = collect_clause_names(source, CLAUSE_WRITE_NODES, nil, writes) if source }
1556
+ writes
1557
+ end
1558
+
1559
+ CLAUSE_WRITE_NODES = Set[
1560
+ *CapturedLocals::LOCAL_WRITE_NODES,
1561
+ Prism::InstanceVariableWriteNode,
1562
+ Prism::InstanceVariableOperatorWriteNode,
1563
+ Prism::InstanceVariableOrWriteNode,
1564
+ Prism::InstanceVariableAndWriteNode,
1565
+ Prism::InstanceVariableTargetNode
1566
+ ].freeze
1567
+ private_constant :CLAUSE_WRITE_NODES
1568
+
1569
+ CLAUSE_MENTION_NODES = Set[
1570
+ Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode, *CLAUSE_WRITE_NODES
1571
+ ].freeze
1572
+ private_constant :CLAUSE_MENTION_NODES
1573
+
1574
+ # The names of the `node_classes` nodes under `node` (restricted to `among` when given) added to `found`, outside
1575
+ # a nested `def` or class body.
1576
+ def collect_clause_names(node, node_classes, among, found)
1577
+ return found if SCOPE_BODY_NODES.any? { |klass| node.is_a?(klass) }
1578
+
1579
+ if node_classes.include?(node.class) && (among.nil? || among.include?(node.name))
1580
+ (found ||= Set.new) << node.name
1581
+ end
1582
+ node.rigor_each_child { |child| found = collect_clause_names(child, node_classes, among, found) }
1583
+ found
1584
+ end
1585
+
1586
+ # `clause_scope` with each name of `writes` that `statements` mentions, and `clause_scope` binds, rebound to
1587
+ # `Dynamic[top]`, or nil when none changes.
1588
+ def widen_clause_entry(clause_scope, statements, writes)
1589
+ return nil if writes.nil? || statements.nil? || !widening_recorded?
1590
+
1591
+ names = collect_clause_names(statements, CLAUSE_MENTION_NODES, writes, nil)
1592
+ return nil if names.nil?
1593
+
1594
+ untyped = Type::Combinator.untyped
1595
+ widened = names.reduce(clause_scope) do |acc, name|
1596
+ kind = name.start_with?("@") ? :ivar : :local
1597
+ current = clause_scope.public_send(VAR_KIND_GETTERS.fetch(kind), name)
1598
+ current.nil? || current == untyped ? acc : rebind_variable(acc, kind, name, untyped)
1599
+ end
1600
+ widened.equal?(clause_scope) ? nil : widened
1601
+ end
1602
+
1603
+ # Whether a widened pass would record anything: only the check path asks for one ({#initialize}'s
1604
+ # `widen_clauses`), and a pass nothing records is skipped, so a nested clause inside an unrecorded pass is
1605
+ # evaluated once.
1606
+ def widening_recorded?
1607
+ @widen_clauses && !(@on_enter.nil? && @operand_recorder.nil?)
1608
+ end
1609
+
1610
+ # `[type, scope]` of a rescue or `ensure` clause entered with `clause_scope`. When {#widen_clause_entry} widens
1611
+ # it, the clause is evaluated twice: under the widened entry for the per-node index, which is what the check
1612
+ # rules read, and under `clause_scope` without recording, for the value and the scope the code after the
1613
+ # `begin` continues from. The widened pass collects no `return`, `next`, `break` or retry-edge scope, so the
1614
+ # widening reaches neither an inferred return nor a loop: it can only take diagnostics away inside the clause.
1615
+ def eval_widened_clause(node, statements, clause_scope, writes)
1616
+ widened = widen_clause_entry(clause_scope, statements, writes)
1617
+ return sub_eval(node, clause_scope) unless widened
1618
+
1619
+ eval_widened_pass(node, widened)
1620
+ sub_eval(node, clause_scope, **UNRECORDED)
1621
+ end
1622
+
1623
+ # The recorded pass over a widened clause: it reaches no `return`, `next`, `break` or retry-frame sink.
1624
+ def eval_widened_pass(node, widened)
1625
+ return if node.nil?
1626
+
1627
+ without_jump_sinks { sub_eval(node, widened, next_scope_sink: nil) }
1628
+ end
1629
+
1630
+ # Runs `block` with no `return`, `next`, `break` or retry-frame sink installed.
1631
+ def without_jump_sinks
1632
+ previous = CLAUSE_SINK_KEYS.map { |key| Thread.current[key] }
1633
+ CLAUSE_SINK_KEYS.each { |key| Thread.current[key] = nil }
1634
+ yield
1635
+ ensure
1636
+ CLAUSE_SINK_KEYS.zip(previous).each { |key, value| Thread.current[key] = value } if previous
1495
1637
  end
1496
1638
 
1497
1639
  # The scope a rescue clause of `node` enters with: it runs after any prefix of the body, so neither a `$_`
@@ -1589,6 +1731,10 @@ module Rigor
1589
1731
  RETRY_FRAMES_KEY = :rigor_retry_frames
1590
1732
  private_constant :RETRY_FRAMES_KEY
1591
1733
 
1734
+ # The thread-local sinks a widened clause pass ({#without_jump_sinks}) leaves uninstalled.
1735
+ CLAUSE_SINK_KEYS = [RETURN_SINK_KEY, NEXT_SINK_KEY, BREAK_SINK_KEY, BREAK_VALUE_SINK_KEY, RETRY_FRAMES_KEY].freeze
1736
+ private_constant :CLAUSE_SINK_KEYS
1737
+
1592
1738
  # The retry edge of `node`, or nil when no `retry` in its rescue chain targets it. Allocation-free for a `begin`
1593
1739
  # no `retry` targets.
1594
1740
  def retry_edge_for(node)
@@ -1865,13 +2011,23 @@ module Rigor
1865
2011
  # each arm holding one. The latter still carries a rebind the `retry` itself does not see: a write an `ensure`
1866
2012
  # runs on the way out (`begin; retry; ensure; tries += 1; end`), or one made before a `retry` the evaluator only
1867
2013
  # types (`log(tries < 5 ? retry : :gave_up)`).
1868
- def collect_rescue_chain_results(rescue_node, entry_scope, edge: nil)
2014
+ #
2015
+ # Issue #1231 — `writes` widens the scope each clause enters with, as {#eval_widened_clause} does, before its
2016
+ # reference is bound, so `rescue => e` still binds `e` to the exception. It is nil under a retry edge, which
2017
+ # already carries every scope the primary body raises from ({#widen_entry_for_retry}).
2018
+ def collect_rescue_chain_results(rescue_node, entry_scope, edge: nil, writes: nil)
1869
2019
  on_enter = edge ? retry_scope_recorder(edge) : @on_enter
1870
2020
  results = []
1871
2021
  current = rescue_node
1872
2022
  while current
2023
+ widened = widen_clause_entry(entry_scope, current.statements, writes)
2024
+ eval_widened_pass(current.statements, bind_rescue_reference(current, widened)) if widened
1873
2025
  rescue_scope = bind_rescue_reference(current, entry_scope)
1874
- arm = eval_branch_or_nil(current.statements, rescue_scope, on_enter: on_enter)
2026
+ arm = if widened
2027
+ sub_eval(current.statements, rescue_scope, **UNRECORDED)
2028
+ else
2029
+ eval_branch_or_nil(current.statements, rescue_scope, on_enter: on_enter)
2030
+ end
1875
2031
  edge.retry_scopes << arm.last if edge&.retrying_arms&.include?(current)
1876
2032
  results << [arm, current]
1877
2033
  current = current.subsequent
@@ -1941,7 +2097,7 @@ module Rigor
1941
2097
  # the single-pass join before returning.
1942
2098
  if names.empty?
1943
2099
  fast = loop_content_writeback(node.statements, base_scope, pre_body: post_pred)
1944
- return [Type::Combinator.constant_of(nil), narrow_loop_exit_edge(node, fast)]
2100
+ return [Type::Combinator.constant_of(nil), loop_exit(node, post_pred, fast)]
1945
2101
  end
1946
2102
 
1947
2103
  post_loop = converged_loop_scope(node, post_pred, base_scope, names, body_first, jumps)
@@ -1950,7 +2106,7 @@ module Rigor
1950
2106
  # :idle` under `end while state != :idle`) is dead in every converged pass. The single pass runs from the
1951
2107
  # un-narrowed post-predicate scope, so its arms stand in for that first iteration.
1952
2108
  post_loop = join_break_scopes(post_loop, first_breaks, names) if node.begin_modifier?
1953
- post_loop = narrow_loop_exit_edge(node, post_loop)
2109
+ post_loop = loop_exit(node, post_pred, post_loop)
1954
2110
  [Type::Combinator.constant_of(nil), post_loop]
1955
2111
  end
1956
2112
 
@@ -2037,6 +2193,12 @@ module Rigor
2037
2193
  join_break_scopes(post_loop, arms, names)
2038
2194
  end
2039
2195
 
2196
+ # A loop's continuation: a local its body and predicate only narrowed from `Dynamic[top]` re-joins to it
2197
+ # ({UntypedRejoin}, issue #1840), then the exit edge narrows.
2198
+ def loop_exit(node, post_pred, post_loop)
2199
+ narrow_loop_exit_edge(node, UntypedRejoin.apply(post_loop, post_pred, [node.predicate, node.statements]))
2200
+ end
2201
+
2040
2202
  # Item 4 — loop-exit predicate narrowing. A `while pred` / `until pred` loop exits PRECISELY on the predicate's
2041
2203
  # exit edge: `while` exits when `pred` is falsey, `until` when `pred` is truthy. So after the loop the
2042
2204
  # predicate-assignment target carries the exit polarity — `until line = io.gets; …; end; line.foo` reads `line`
@@ -2368,6 +2530,12 @@ module Rigor
2368
2530
  stores
2369
2531
  end
2370
2532
 
2533
+ # A `for` loop's continuation, with a local the body only narrowed from `Dynamic[top]` re-joined to it
2534
+ # ({UntypedRejoin}, issue #1840). The index is written on every iteration.
2535
+ def for_exit(node, post_coll, continuation)
2536
+ UntypedRejoin.apply(continuation, post_coll, [node.index, node.statements])
2537
+ end
2538
+
2371
2539
  # `for index in collection; body; end`. Unlike `each {}` blocks, `for` does NOT create a new variable scope: the
2372
2540
  # index variable AND every local written in the body leak to the surrounding scope. The collection is evaluated
2373
2541
  # once; the body runs zero or more times, so the post-loop scope is the join of the no-iteration scope (just
@@ -2394,7 +2562,7 @@ module Rigor
2394
2562
  continuation = join_with_nil_injection(post_coll, body_scope)
2395
2563
  pre_existing, body_first = loop_body_local_writes(node.statements, post_coll)
2396
2564
  continuation = join_break_scopes(continuation, breaks, pre_existing + body_first)
2397
- [Type::Combinator.constant_of(nil), continuation]
2565
+ [Type::Combinator.constant_of(nil), for_exit(node, post_coll, continuation)]
2398
2566
  end
2399
2567
 
2400
2568
  # Issue #1429 — a `for` body runs after the collection's `each`, which may be a project method, and after itself.
@@ -2554,7 +2722,7 @@ module Rigor
2554
2722
 
2555
2723
  # A dead RHS is still evaluated and its scope still joins, so a write inside it nil-injects exactly as
2556
2724
  # before; only its value is dropped, because it cannot be the value of the expression.
2557
- joined_scope = join_with_nil_injection(left_scope, right_scope)
2725
+ joined_scope = UntypedRejoin.apply(join_with_nil_injection(left_scope, right_scope), left_scope, [node.right])
2558
2726
  type = skipped_type
2559
2727
  type = Type::Combinator.union(skipped_type, right_type) unless right_operand_dead?(node, left_type, left_scope)
2560
2728
  return [type, joined_scope] unless edges
@@ -2994,13 +3162,15 @@ module Rigor
2994
3162
  # outer receiver is itself a chain node (e.g. `x.last << y`) do NOT drop narrowings keyed on `x` — only direct
2995
3163
  # calls against the root variable invalidate the chain.
2996
3164
  post_scope = IndexedNarrowing.invalidate_chain_after_call(call_node: node, current_scope: post_scope)
3165
+ # Issue #1703 — a non-literal-key `key?` guard the call may break ({KeyPresenceGuard.breaks?}).
3166
+ post_scope = KeyPresenceGuard.invalidate_after_call(node, post_scope)
2997
3167
  # B2.2 — intervening method call ivar invalidation. An implicit-self / self-receiver call could mutate any ivar
2998
3168
  # of the enclosing class (we cannot prove purity without an effect system). Reset each ivar whose current local
2999
3169
  # binding has narrowed below the class-ivar seed back to the seed itself, so a subsequent `if @flag` predicate
3000
3170
  # observes the seed's union (not the pre-call narrowed value). Always-safe (only widens; no new facts). See
3001
3171
  # [`docs/CURRENT_WORK.md`](../../../docs/CURRENT_WORK.md) § "Flow-folding" — G2 intervening-call case.
3002
3172
  post_scope = invalidate_ivars_for_intervening_call(node, post_scope) if statement_call
3003
- # C1 — regex match-data globals (`$~`, `$1..$9`, `$&`, …) are narrowed to non-nil on a successful-match edge; a
3173
+ # C1 — regex match-data globals (`$~`, `$1..$N`, `$&`, …) are narrowed to non-nil on a successful-match edge; a
3004
3174
  # later call that itself runs a regex match rebinds them, so the narrowed facts must be dropped. We forget them
3005
3175
  # only when the call may run a match in this frame ({#rebinds_match_globals?}). A call provably match-free on a
3006
3176
  # known receiver — `$3.to_i`, `year < 50` — does NOT clobber, so the multi-statement `m = /…/ =~ s; …; use($2)`
@@ -3529,7 +3699,9 @@ module Rigor
3529
3699
  end
3530
3700
 
3531
3701
  def enter_call_block(node, block)
3532
- block_entry = narrow_define_method_block_self(node, repeating_block_entry(node, block))
3702
+ block_entry = narrow_define_method_block_self(
3703
+ node, with_repeated_or_write_marks(node, block, repeating_block_entry(node, block))
3704
+ )
3533
3705
  # #319 — `Class.new do ... end` and friends evaluate their block as a CLASS BODY (`class_eval`
3534
3706
  # semantics): `self` is the freshly created class, so a `def` inside defines an instance method on it
3535
3707
  # and `attr_reader` runs as a class-level macro. Enter the block under the same `self_type` /
@@ -3559,15 +3731,19 @@ module Rigor
3559
3731
  # does not type as a class object (a gem class with no RBS) keeps the name as written: the body is still some
3560
3732
  # class's body, and leaving it on the enclosing `self` made a `refine` at the file's top level report every
3561
3733
  # implicit-self call in it as `call.unresolved-toplevel`.
3734
+ #
3735
+ # Issue #1689 — a `refine` call the file's {InEffectRefinements} places in a class body is the class's own
3736
+ # method, not `Module#refine`, so its block is entered as any other block.
3562
3737
  def refined_class_context(node)
3563
3738
  target = ScopeIndexer.refine_target(node)
3564
3739
  return nil if target.nil?
3565
3740
 
3566
- refined = scope.type_of(target)
3567
- name = refined.is_a?(Type::Singleton) ? refined.class_name : Source::ConstantPath.qualified_name(target)
3741
+ return nil if InEffectRefinements.class_body_refine?(scope, node)
3742
+
3743
+ name = ScopeIndexer.refined_class_name(target, scope)
3568
3744
  return nil if name.nil?
3569
3745
 
3570
- [ClassFrame.new(name: name.delete_prefix("::"), singleton: false, refinement: true)]
3746
+ [ClassFrame.new(name: name, singleton: false, refinement: true)]
3571
3747
  end
3572
3748
 
3573
3749
  # The block calls whose body `return` leaves only the block ({ReturnBarrier.block_call?}). Like a `->` body
@@ -3601,7 +3777,9 @@ module Rigor
3601
3777
  narrowed = DefineMethodBlockSelf.narrow_self_type_for(
3602
3778
  scope: scope, call_node: call_node
3603
3779
  )
3604
- narrowed ? block_entry.with_self_type(narrowed) : block_entry
3780
+ return block_entry unless narrowed
3781
+
3782
+ block_entry.with_block_self_type(narrowed, keeps_unknown: scope.block_self_narrowing_unknown?(call_node))
3605
3783
  end
3606
3784
 
3607
3785
  # Enters a meta-new `block` as the body of the class `class_context` names: `self_type` is that class's
@@ -3616,8 +3794,17 @@ module Rigor
3616
3794
  # record a `Outer::K` entry: that is a rung Ruby's constant lookup does not have. The `class_context`
3617
3795
  # frame is still pushed — it is what a nested `def` registers its method under — which is exactly the
3618
3796
  # divergence that makes the chain a separate record rather than a view of the frame stack.
3797
+ #
3798
+ # Issue #1759 — a `refine X do … end` block runs on the refinement module, not on X, which Rigor does not
3799
+ # model: the body keeps X's frame and singleton so its `def`s and implicit-self calls resolve as before, but
3800
+ # carries the unknown-`self` mark ({Scope#block_self_unknown?}), so an explicit `self.import_methods(M)` is
3801
+ # not checked against `singleton(X)`. The indexer leaves a value-position refine block unknown the same way.
3619
3802
  def enter_meta_class_body(block, block_entry, class_context)
3620
- entry = block_entry.with_self_type(self_type_for_class_body(class_context)).with_singleton_class_body(false)
3803
+ # Issue #1717 — the body runs on the new class whatever encloses it, so its `self` is known.
3804
+ entry = block_entry.with_block_self_type(self_type_for_class_body(class_context),
3805
+ keeps_unknown: class_context.last.refinement &&
3806
+ Scope::REFINEMENT_BLOCK_SELF)
3807
+ .with_singleton_class_body(false)
3621
3808
  sub_eval(block, stamp_nesting(entry, @lexical_nesting), class_context: class_context)
3622
3809
  end
3623
3810
 
@@ -4114,7 +4301,7 @@ module Rigor
4114
4301
  # The `break` scopes of one unrecorded pass from `converged` — for a capped fixpoint, whose widened binding no
4115
4302
  # pass has run from.
4116
4303
  def converged_break_arms(call_node, block, converged, targets)
4117
- entry = block_pass_entry(call_node, block, converged)
4304
+ entry = with_repeated_or_write_marks(call_node, block, block_pass_entry(call_node, block, converged))
4118
4305
  sink, = collect_break_scopes { sub_eval(block, entry, **UNRECORDED) }
4119
4306
  targeted_scopes(sink, targets)
4120
4307
  end
@@ -4914,7 +5101,8 @@ module Rigor
4914
5101
  # params / `;`-locals re-bound as usual) and returns the per-name exit binding for `names`. Used as the
4915
5102
  # `BodyFixpoint` body-evaluator.
4916
5103
  def block_exit_bindings(call_node, block, bindings, names)
4917
- _type, exit_scope = sub_eval(block, block_pass_entry(call_node, block, bindings))
5104
+ entry = with_repeated_or_write_marks(call_node, block, block_pass_entry(call_node, block, bindings))
5105
+ _type, exit_scope = sub_eval(block, entry)
4918
5106
  names.to_h { |name| [name, CapturedLocals.bound_type(exit_scope, name)] }
4919
5107
  end
4920
5108
 
@@ -4930,6 +5118,38 @@ module Rigor
4930
5118
  end
4931
5119
  end
4932
5120
 
5121
+ # Issue #1336 — `entry` with the index `||=` sites of a repeating block marked as the generic block-return pass
5122
+ # marks them ({RepeatedOrWrites}, `Scope#with_repeated_or_writes`). The statement pass and every ADR-56
5123
+ # write-back pass type the body from one entry scope just as that pass does, so a slot an earlier run stored
5124
+ # into reads as a lone `Dynamic` there too, and the memoizing reading ({#index_compound_write_value}) answered
5125
+ # one site's rvalue: `last = (cache[:k] ||= :f).itself` beside a `cache[:k] ||= :e` wrote `:f` alone back
5126
+ # into `last`, and `last == :e` folded always-falsey. A call that runs its block at most once marks nothing.
5127
+ def with_repeated_or_write_marks(call_node, block, entry)
5128
+ sites = repeated_or_write_sites(call_node, block)
5129
+ sites.empty? ? entry : entry.with_repeated_or_writes(sites)
5130
+ end
5131
+
5132
+ # The sites {#with_repeated_or_write_marks} lays, once per block: a body that spells no `||=` — nearly every
5133
+ # one — answers none without the repetition question.
5134
+ def repeated_or_write_sites(call_node, block)
5135
+ memo = (@repeated_or_write_sites ||= {}.compare_by_identity)
5136
+ return memo[block] if memo.key?(block)
5137
+
5138
+ memo[block] = compute_repeated_or_write_sites(call_node, block)
5139
+ end
5140
+
5141
+ def compute_repeated_or_write_sites(call_node, block)
5142
+ body = block.body
5143
+ return NO_REPEATED_OR_WRITES if body.nil? || !RepeatedOrWrites.may_hold_or_write?(body)
5144
+ return NO_REPEATED_OR_WRITES if repeating_block_class(call_node).nil?
5145
+
5146
+ stores = CapturedLocals.content_mutations(block, scope, non_locals: true)
5147
+ RepeatedOrWrites.sites(block, stores, scope).at(nil)
5148
+ end
5149
+
5150
+ NO_REPEATED_OR_WRITES = [].freeze
5151
+ private_constant :NO_REPEATED_OR_WRITES
5152
+
4933
5153
  NO_CAPTURE_BINDINGS = {}.freeze
4934
5154
  private_constant :NO_CAPTURE_BINDINGS
4935
5155
 
@@ -5140,27 +5360,31 @@ module Rigor
5140
5360
  # ({ExpressionTyper#block_body_self_narrowing}) already applies the same contract to block-return
5141
5361
  # typing; without it here the recorded per-node scopes — what `dump_type`/`assert_type` and the
5142
5362
  # survey read — keep the enclosing `self_type` and every DSL call inside stays `Dynamic[top]`.
5143
- narrowed = call_node && narrow_macro_block_self(call_node)
5144
- scope_with_params = scope_with_params.with_self_type(narrowed) if narrowed
5363
+ # Issue #1667 — the entry's `refinements:` are put in effect in the body alongside.
5364
+ match = call_node && macro_block_match(call_node)
5365
+ if match
5366
+ keeps_unknown = scope.block_self_narrowing_unknown?(call_node)
5367
+ scope_with_params = MacroBlockSelfType.apply(scope_with_params, match, keeps_unknown: keeps_unknown)
5368
+ end
5145
5369
  block_local_names(block_node).reduce(scope_with_params) do |acc, name|
5146
5370
  acc.with_local(name, Type::Combinator.constant_of(nil))
5147
5371
  end
5148
5372
  end
5149
5373
 
5150
- # The receiver an ADR-16 `block_as_methods:` match is keyed on: the explicit receiver's type, or the
5374
+ # The ADR-16 `block_as_methods:` match for `call_node`, keyed on the explicit receiver's type, or on the
5151
5375
  # current `self_type` for an implicit-self DSL call (the `params do` / `namespace do` shapes, whose
5152
5376
  # receiver is the enclosing `Singleton[X]`). A miss leaves the entry scope as built — the false-
5153
5377
  # positive-safe direction.
5154
- def narrow_macro_block_self(call_node)
5378
+ def macro_block_match(call_node)
5155
5379
  receiver_type =
5156
5380
  if call_node.receiver
5157
5381
  explicit_receiver_type(call_node)
5158
5382
  else
5159
- scope.self_type
5383
+ MacroBlockSelfType.implicit_receiver_type(scope)
5160
5384
  end
5161
5385
  return nil if receiver_type.nil?
5162
5386
 
5163
- MacroBlockSelfType.narrow_self_type_for(
5387
+ MacroBlockSelfType.match_for(
5164
5388
  scope: scope, call_node: call_node, receiver_type: receiver_type
5165
5389
  )
5166
5390
  rescue StandardError
@@ -5193,7 +5417,8 @@ module Rigor
5193
5417
  method_name: call_node.name,
5194
5418
  arg_types: arg_types,
5195
5419
  environment: scope.environment,
5196
- scope: scope
5420
+ scope: scope,
5421
+ call_node: call_node
5197
5422
  )
5198
5423
  rescue StandardError
5199
5424
  []
@@ -5218,13 +5443,19 @@ module Rigor
5218
5443
  fresh = build_fresh_body_scope
5219
5444
  # Issue #1518 — `class << self` inside an unnameable class body keeps that body's opacity: it opens the
5220
5445
  # singleton of a class nothing names, and its empty frame stack is not the top level.
5221
- opaque_self ||= node.is_a?(Prism::SingletonClassNode) && @opaque_class && new_context.empty?
5222
- body_self = opaque_self ? Type::Combinator.untyped : self_type_for_class_body(new_context)
5446
+ singleton_body = node.is_a?(Prism::SingletonClassNode)
5447
+ opaque_self ||= singleton_body && @opaque_class && new_context.empty?
5448
+ # Issue #1763 — a `class << self` in a `refine X` block (the refinement frame {#singleton_context_for} built)
5449
+ # opens the refinement module's singleton class, which Rigor does not model, as the `def`s in it already read
5450
+ # ({#self_type_for_method_body}); `Singleton[X]` there checked `self.m` and a local holding `self` against X's
5451
+ # class methods. A `class << X` names X's real singleton class and takes a frame without the flag.
5452
+ untyped_self = opaque_self || (singleton_body && new_context.last&.refinement)
5453
+ body_self = untyped_self ? Type::Combinator.untyped : self_type_for_class_body(new_context)
5223
5454
  fresh = fresh.with_self_type(body_self) if body_self
5224
5455
  # Issue #963 — `self` in a `class << ...` body is the SINGLETON class, which shares the `Singleton[X]`
5225
5456
  # carrier a `class X` body gets. The mark is the only thing that tells the two apart downstream, and a
5226
5457
  # `def` reached from this body clears it by starting from a fresh scope.
5227
- fresh = fresh.with_singleton_class_body(node.is_a?(Prism::SingletonClassNode))
5458
+ fresh = fresh.with_singleton_class_body(singleton_body)
5228
5459
  fresh = fresh.with_match_frame(node.body)
5229
5460
  fresh = stamp_nesting(fresh, new_nesting)
5230
5461
  sub_eval(node.body, fresh, class_context: new_context, lexical_nesting: new_nesting, opaque_class: opaque_self)
@@ -5254,7 +5485,7 @@ module Rigor
5254
5485
  # then receive the parameter bindings. Slice 7 phase 2: instance defs ALSO seed their `ivars` map from the
5255
5486
  # class-level accumulator so `def get; @x; end` reads the type that a sibling `def init; @x = 1; end` wrote.
5256
5487
  fresh = build_fresh_body_scope
5257
- body_self = self_type_for_method_body(singleton: singleton)
5488
+ body_self = self_type_for_method_body(singleton: singleton, receiver: def_node.receiver)
5258
5489
  fresh = fresh.with_self_type(body_self) if body_self
5259
5490
  # A `def` opens no new `Module.nesting` entry — the body resolves constants against the chain of the
5260
5491
  # declaration that encloses it, which is what the evaluator is already carrying (#652).
@@ -5389,12 +5620,16 @@ module Rigor
5389
5620
  # chain's throwaway intermediate Scopes were a top `Scope#rebuild` source (ADR-44). Local-empty by design; the
5390
5621
  # discovery index is inherited whole by reference (ADR-53 Track A), so a table added to the index can no longer
5391
5622
  # be dropped here by a missed per-field copy.
5623
+ #
5624
+ # Issue #1667 — a plugin-declared refined block's modules carry into a `def` or class body written in it: CRuby
5625
+ # defines the method with the block's cref, and a class body's cref inherits the outer one's refinements.
5392
5626
  Scope.new(
5393
5627
  environment: scope.environment,
5394
5628
  locals: {}.freeze,
5395
5629
  source_path: scope.source_path,
5396
5630
  discovery: scope.discovery,
5397
- dynamic_origins: scope.dynamic_origins
5631
+ dynamic_origins: scope.dynamic_origins,
5632
+ declared_refinements: scope.declared_refinements
5398
5633
  )
5399
5634
  end
5400
5635
 
@@ -5444,11 +5679,15 @@ module Rigor
5444
5679
  # `Nominal[Foo]`.
5445
5680
  #
5446
5681
  # Returns nil for top-level defs that have no enclosing class.
5447
- def self_type_for_method_body(singleton:)
5682
+ def self_type_for_method_body(singleton:, receiver: nil)
5448
5683
  path = current_class_path
5449
5684
  # Issue #1518 — a `def` in a class nothing names has a receiver, just not one Rigor can type; nil would
5450
5685
  # read its body as the top level and its implicit-self calls as unresolved top-level calls.
5451
5686
  return (Type::Combinator.untyped if @opaque_class) if path.nil?
5687
+ # Issue #1759 — a `def self.m` in a `refine X` block (or a `def m` in a `class << self` there) is a method
5688
+ # of the refinement module, which Rigor does not model; reading it as `Singleton[X]` checked its
5689
+ # explicit-self calls against X's class methods. A `def X.m` there does define X's class method.
5690
+ return Type::Combinator.untyped if singleton && @class_context.last.refinement && refinement_self_def?(receiver)
5452
5691
 
5453
5692
  if singleton
5454
5693
  Type::Combinator.singleton_of(path)
@@ -5457,6 +5696,10 @@ module Rigor
5457
5696
  end
5458
5697
  end
5459
5698
 
5699
+ # Issue #1759 — whether a singleton `def` under a refinement frame defines a method on the refinement module
5700
+ # (`def self.m`, or `def m` in a `class << self`) rather than on an explicitly named receiver (`def X.m`).
5701
+ def refinement_self_def?(receiver) = receiver.nil? || receiver.is_a?(Prism::SelfNode)
5702
+
5460
5703
  def singleton_context_for(node)
5461
5704
  case node.expression
5462
5705
  when Prism::SelfNode
@@ -5464,7 +5707,7 @@ module Rigor
5464
5707
 
5465
5708
  outer = @class_context[0..-2]
5466
5709
  last = @class_context.last
5467
- outer + [ClassFrame.new(name: last.name, singleton: true)]
5710
+ outer + [ClassFrame.new(name: last.name, singleton: true, refinement: last.refinement)]
5468
5711
  when Prism::ConstantReadNode, Prism::ConstantPathNode
5469
5712
  target = singleton_constant_target(node.expression)
5470
5713
  return @class_context unless target
@@ -5611,6 +5854,7 @@ module Rigor
5611
5854
  class_context: class_context,
5612
5855
  lexical_nesting: lexical_nesting,
5613
5856
  converged_loop_recording: @converged_loop_recording,
5857
+ widen_clauses: @widen_clauses,
5614
5858
  next_scope_sink: next_scope_sink,
5615
5859
  operand_scope: operand_scope,
5616
5860
  in_operand: in_operand,
@@ -5905,8 +6149,7 @@ module Rigor
5905
6149
  # it is frozen, and the dispatcher already projects it (see `DataFolding`).
5906
6150
  def struct_instance_projection(subject_type, method_name, scope, subject_node)
5907
6151
  return nil unless subject_type.is_a?(Type::StructInstance)
5908
- return nil unless MethodDispatcher::StructMaterialization.materialization_call?(subject_node, subject_type,
5909
- scope)
6152
+ return nil unless MethodDispatcher::StructMaterialization.materialization_call?(subject_node, scope)
5910
6153
 
5911
6154
  case method_name
5912
6155
  when :deconstruct then Type::Combinator.tuple_of(*subject_type.members.values)