rigortype 0.1.3 → 0.1.5

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 (149) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +154 -33
  3. data/lib/rigor/analysis/check_rules.rb +10 -18
  4. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +75 -0
  5. data/lib/rigor/analysis/dependency_source_inference/builder.rb +47 -21
  6. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  7. data/lib/rigor/analysis/dependency_source_inference/index.rb +32 -3
  8. data/lib/rigor/analysis/dependency_source_inference/walker.rb +1 -1
  9. data/lib/rigor/analysis/dependency_source_inference.rb +1 -0
  10. data/lib/rigor/analysis/diagnostic.rb +0 -2
  11. data/lib/rigor/analysis/fact_store.rb +26 -6
  12. data/lib/rigor/analysis/result.rb +11 -3
  13. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  14. data/lib/rigor/analysis/run_stats.rb +193 -0
  15. data/lib/rigor/analysis/runner.rb +498 -12
  16. data/lib/rigor/analysis/worker_session.rb +327 -0
  17. data/lib/rigor/builtins/imported_refinements.rb +364 -55
  18. data/lib/rigor/builtins/regex_refinement.rb +17 -12
  19. data/lib/rigor/cache/descriptor.rb +1 -1
  20. data/lib/rigor/cache/rbs_descriptor.rb +3 -1
  21. data/lib/rigor/cache/store.rb +39 -6
  22. data/lib/rigor/cli/diff_command.rb +1 -1
  23. data/lib/rigor/cli/sig_gen_command.rb +173 -0
  24. data/lib/rigor/cli/type_of_command.rb +1 -1
  25. data/lib/rigor/cli/type_scan_renderer.rb +1 -1
  26. data/lib/rigor/cli/type_scan_report.rb +2 -2
  27. data/lib/rigor/cli.rb +61 -3
  28. data/lib/rigor/configuration/dependencies.rb +2 -2
  29. data/lib/rigor/configuration.rb +131 -6
  30. data/lib/rigor/environment/bundle_sig_discovery.rb +198 -0
  31. data/lib/rigor/environment/class_registry.rb +12 -3
  32. data/lib/rigor/environment/lockfile_resolver.rb +125 -0
  33. data/lib/rigor/environment/rbs_collection_discovery.rb +126 -0
  34. data/lib/rigor/environment/rbs_coverage_report.rb +112 -0
  35. data/lib/rigor/environment/rbs_loader.rb +194 -6
  36. data/lib/rigor/environment/reflection.rb +152 -0
  37. data/lib/rigor/environment.rb +109 -6
  38. data/lib/rigor/flow_contribution/conflict.rb +2 -2
  39. data/lib/rigor/flow_contribution/element.rb +1 -1
  40. data/lib/rigor/flow_contribution/fact.rb +1 -1
  41. data/lib/rigor/flow_contribution/merge_result.rb +1 -1
  42. data/lib/rigor/flow_contribution/merger.rb +3 -3
  43. data/lib/rigor/flow_contribution.rb +2 -2
  44. data/lib/rigor/inference/acceptance.rb +35 -1
  45. data/lib/rigor/inference/block_parameter_binder.rb +0 -2
  46. data/lib/rigor/inference/builtins/method_catalog.rb +12 -5
  47. data/lib/rigor/inference/builtins/numeric_catalog.rb +15 -4
  48. data/lib/rigor/inference/coverage_scanner.rb +1 -1
  49. data/lib/rigor/inference/expression_typer.rb +77 -11
  50. data/lib/rigor/inference/fallback.rb +1 -1
  51. data/lib/rigor/inference/macro_block_self_type.rb +96 -0
  52. data/lib/rigor/inference/method_dispatcher/block_folding.rb +3 -5
  53. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +29 -41
  54. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -3
  55. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -4
  56. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +1 -1
  57. data/lib/rigor/inference/method_dispatcher/method_folding.rb +135 -0
  58. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +7 -12
  59. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +27 -11
  60. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +46 -44
  61. data/lib/rigor/inference/method_dispatcher.rb +274 -5
  62. data/lib/rigor/inference/method_parameter_binder.rb +22 -14
  63. data/lib/rigor/inference/narrowing.rb +129 -12
  64. data/lib/rigor/inference/rbs_type_translator.rb +0 -2
  65. data/lib/rigor/inference/scope_indexer.rb +14 -9
  66. data/lib/rigor/inference/statement_evaluator.rb +7 -7
  67. data/lib/rigor/inference/synthetic_method.rb +86 -0
  68. data/lib/rigor/inference/synthetic_method_index.rb +82 -0
  69. data/lib/rigor/inference/synthetic_method_scanner.rb +521 -0
  70. data/lib/rigor/plugin/blueprint.rb +60 -0
  71. data/lib/rigor/plugin/io_boundary.rb +0 -2
  72. data/lib/rigor/plugin/loader.rb +5 -3
  73. data/lib/rigor/plugin/macro/block_as_method.rb +131 -0
  74. data/lib/rigor/plugin/macro/external_file.rb +143 -0
  75. data/lib/rigor/plugin/macro/heredoc_template.rb +201 -0
  76. data/lib/rigor/plugin/macro/trait_registry.rb +198 -0
  77. data/lib/rigor/plugin/macro.rb +31 -0
  78. data/lib/rigor/plugin/manifest.rb +102 -10
  79. data/lib/rigor/plugin/registry.rb +43 -2
  80. data/lib/rigor/plugin/services.rb +1 -1
  81. data/lib/rigor/plugin/type_node_resolver.rb +52 -0
  82. data/lib/rigor/plugin.rb +2 -0
  83. data/lib/rigor/rbs_extended/reporter.rb +91 -0
  84. data/lib/rigor/rbs_extended.rb +131 -32
  85. data/lib/rigor/scope.rb +25 -8
  86. data/lib/rigor/sig_gen/classification.rb +36 -0
  87. data/lib/rigor/sig_gen/generator.rb +1048 -0
  88. data/lib/rigor/sig_gen/layout_index.rb +108 -0
  89. data/lib/rigor/sig_gen/method_candidate.rb +62 -0
  90. data/lib/rigor/sig_gen/observation_collector.rb +391 -0
  91. data/lib/rigor/sig_gen/observed_call.rb +62 -0
  92. data/lib/rigor/sig_gen/path_mapper.rb +116 -0
  93. data/lib/rigor/sig_gen/renderer.rb +157 -0
  94. data/lib/rigor/sig_gen/type_elaborator.rb +92 -0
  95. data/lib/rigor/sig_gen/write_result.rb +48 -0
  96. data/lib/rigor/sig_gen/writer.rb +530 -0
  97. data/lib/rigor/sig_gen.rb +25 -0
  98. data/lib/rigor/trinary.rb +15 -11
  99. data/lib/rigor/type/bot.rb +6 -3
  100. data/lib/rigor/type/bound_method.rb +79 -0
  101. data/lib/rigor/type/combinator.rb +207 -3
  102. data/lib/rigor/type/constant.rb +13 -0
  103. data/lib/rigor/type/hash_shape.rb +0 -2
  104. data/lib/rigor/type/integer_range.rb +7 -7
  105. data/lib/rigor/type/refined.rb +18 -12
  106. data/lib/rigor/type/top.rb +4 -3
  107. data/lib/rigor/type/union.rb +20 -1
  108. data/lib/rigor/type.rb +1 -0
  109. data/lib/rigor/type_node/generic.rb +68 -0
  110. data/lib/rigor/type_node/identifier.rb +38 -0
  111. data/lib/rigor/type_node/indexed_access.rb +41 -0
  112. data/lib/rigor/type_node/integer_literal.rb +29 -0
  113. data/lib/rigor/type_node/name_scope.rb +52 -0
  114. data/lib/rigor/type_node/resolver_chain.rb +56 -0
  115. data/lib/rigor/type_node/string_literal.rb +32 -0
  116. data/lib/rigor/type_node/symbol_literal.rb +28 -0
  117. data/lib/rigor/type_node/union.rb +42 -0
  118. data/lib/rigor/type_node.rb +29 -0
  119. data/lib/rigor/version.rb +1 -1
  120. data/lib/rigor.rb +2 -0
  121. data/sig/rigor/analysis/check_rules/always_truthy_condition_collector.rbs +10 -0
  122. data/sig/rigor/analysis/check_rules/dead_assignment_collector.rbs +10 -0
  123. data/sig/rigor/analysis/dependency_source_inference/gem_resolver.rbs +25 -0
  124. data/sig/rigor/analysis/dependency_source_inference/index.rbs +9 -0
  125. data/sig/rigor/cli/diff_command.rbs +4 -0
  126. data/sig/rigor/cli/explain_command.rbs +4 -0
  127. data/sig/rigor/cli/sig_gen_command.rbs +4 -0
  128. data/sig/rigor/cli/type_scan_command.rbs +3 -0
  129. data/sig/rigor/environment.rbs +8 -2
  130. data/sig/rigor/inference/builtins/method_catalog.rbs +4 -0
  131. data/sig/rigor/inference/builtins/numeric_catalog.rbs +3 -0
  132. data/sig/rigor/inference/builtins.rbs +2 -0
  133. data/sig/rigor/plugin/access_denied_error.rbs +3 -0
  134. data/sig/rigor/plugin/base.rbs +6 -0
  135. data/sig/rigor/plugin/blueprint.rbs +7 -0
  136. data/sig/rigor/plugin/fact_store.rbs +11 -0
  137. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  138. data/sig/rigor/plugin/load_error.rbs +6 -0
  139. data/sig/rigor/plugin/loader.rbs +20 -0
  140. data/sig/rigor/plugin/manifest.rbs +9 -0
  141. data/sig/rigor/plugin/registry.rbs +16 -0
  142. data/sig/rigor/plugin/services.rbs +3 -0
  143. data/sig/rigor/plugin/trust_policy.rbs +4 -0
  144. data/sig/rigor/plugin/type_node_resolver.rbs +3 -0
  145. data/sig/rigor/plugin.rbs +8 -0
  146. data/sig/rigor/scope.rbs +4 -2
  147. data/sig/rigor/type.rbs +28 -6
  148. data/sig/rigor.rbs +35 -2
  149. metadata +90 -1
@@ -55,7 +55,7 @@ module Rigor
55
55
  # In every conflict case the result keeps the higher-tier value
56
56
  # for that slot, records a {Conflict} with both provenances, and
57
57
  # continues processing the remaining slots / contributions.
58
- module Merger # rubocop:disable Metrics/ModuleLength
58
+ module Merger
59
59
  AUTHORITY_TIERS = {
60
60
  builtin: 0,
61
61
  rbs_extended: 1,
@@ -112,7 +112,7 @@ module Rigor
112
112
  fold_role_conformance(state, contribution)
113
113
  end
114
114
 
115
- def fold_return_type(state, contribution, tier) # rubocop:disable Metrics/AbcSize
115
+ def fold_return_type(state, contribution, tier)
116
116
  incoming = contribution.return_type
117
117
  return if incoming.nil?
118
118
 
@@ -202,7 +202,7 @@ module Rigor
202
202
  end
203
203
  end
204
204
 
205
- def build_conflict(target:, edge:, kind:, reason:, provenances:, message:) # rubocop:disable Metrics/ParameterLists
205
+ def build_conflict(target:, edge:, kind:, reason:, provenances:, message:)
206
206
  Conflict.new(target: target, edge: edge, kind: kind, reason: reason,
207
207
  provenances: provenances, message: message)
208
208
  end
@@ -32,7 +32,7 @@ module Rigor
32
32
  # `descriptor` is the {Rigor::Cache::Descriptor} this
33
33
  # contribution attaches to (or `nil` when the contribution does
34
34
  # not need its own cache slice).
35
- Provenance = Data.define(:source_family, :plugin_id, :node, :descriptor) do
35
+ class Provenance < Data.define(:source_family, :plugin_id, :node, :descriptor)
36
36
  def self.builtin
37
37
  new(source_family: :builtin, plugin_id: nil, node: nil, descriptor: nil)
38
38
  end
@@ -122,7 +122,7 @@ module Rigor
122
122
  # | role_conformance | normal | role | (per-role target) |
123
123
  #
124
124
  # @return [Array<Element>]
125
- def to_element_list # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
125
+ def to_element_list # rubocop:disable Metrics/AbcSize
126
126
  elements = []
127
127
  elements << element_for(:return, :normal, :return_type, return_type) unless return_type.nil?
128
128
  Array(truthy_facts).each { |fact| elements << element_for(fact_target(fact), :truthy, :truthy_fact, fact) }
@@ -327,10 +327,44 @@ module Rigor
327
327
  )
328
328
  return class_result if class_result.no?
329
329
 
330
- args_result = accepts_nominal_args(self_type, other_type, mode)
330
+ # Parametrized-ancestor projection. When `actual <:= target`
331
+ # holds at the class level but the type-arg arities differ,
332
+ # the actual's parametrization has to be projected into the
333
+ # target's view before the element-wise covariance check.
334
+ # The canonical case is `Hash[K, V] <:= Enumerable[[K, V]]`:
335
+ # Hash carries two type_args, Enumerable carries one, and
336
+ # the inherited parametrization at the Enumerable boundary
337
+ # is `Tuple[K, V]`. RBS encodes this as
338
+ # `include Enumerable[[K, V]]` in `Hash`'s definition.
339
+ projected_other = project_to_target_arity(self_type, other_type) || other_type
340
+ args_result = accepts_nominal_args(self_type, projected_other, mode)
331
341
  combine_results(class_result, args_result, mode)
332
342
  end
333
343
 
344
+ # Returns `other_type` rewritten so its type_args have the
345
+ # same arity as `self_type.type_args`, or `nil` if no
346
+ # projection is known. Today only the Hash → Enumerable
347
+ # projection is hand-rolled; a general RBS-driven
348
+ # implementation that consults `definition.ancestors[i].args`
349
+ # for arbitrary subclass / module-include relations is the
350
+ # principled follow-up.
351
+ def project_to_target_arity(self_type, other_type)
352
+ return nil if self_type.type_args.size == other_type.type_args.size
353
+ return nil if self_type.type_args.empty? || other_type.type_args.empty?
354
+
355
+ if self_type.class_name == "Enumerable" &&
356
+ other_type.class_name == "Hash" &&
357
+ self_type.type_args.size == 1 &&
358
+ other_type.type_args.size == 2
359
+ return Type::Combinator.nominal_of(
360
+ "Hash",
361
+ type_args: [Type::Combinator.tuple_of(*other_type.type_args)]
362
+ )
363
+ end
364
+
365
+ nil
366
+ end
367
+
334
368
  def project_tuple_to_nominal(tuple)
335
369
  if tuple.elements.empty?
336
370
  Type::Combinator.nominal_of(Array)
@@ -45,7 +45,6 @@ module Rigor
45
45
  # scope MUST NOT observe them and the binder leaves them unbound.
46
46
  #
47
47
  # See docs/internal-spec/inference-engine.md for the binding contract.
48
- # rubocop:disable Metrics/ClassLength
49
48
  class BlockParameterBinder
50
49
  # @param expected_param_types [Array<Rigor::Type>] positional block
51
50
  # parameter types in order. Indices the binder cannot fill from
@@ -208,6 +207,5 @@ module Rigor
208
207
  @expected_param_types[index] || Type::Combinator.untyped
209
208
  end
210
209
  end
211
- # rubocop:enable Metrics/ClassLength
212
210
  end
213
211
  end
@@ -30,7 +30,16 @@ module Rigor
30
30
  def initialize(path:, mutating_selectors: {})
31
31
  @path = path
32
32
  @mutating_selectors = mutating_selectors.transform_values(&:freeze).freeze
33
- @catalog = nil
33
+ # ADR-15 Phase 4b.x — eager-load so the instance is
34
+ # safe to `Ractor.make_shareable`. Lazy init via
35
+ # `@catalog ||= load_catalog` would write to a
36
+ # potentially-frozen instance the first time a
37
+ # worker Ractor consults the catalog, raising
38
+ # `FrozenError`. The YAML parse is a once-per-process
39
+ # cost and the catalogs are constructed at module
40
+ # load time anyway, so eager init is free in
41
+ # practice.
42
+ @catalog = load_catalog
34
43
  end
35
44
 
36
45
  def safe_for_folding?(class_name, selector, kind: :instance)
@@ -52,7 +61,7 @@ module Rigor
52
61
  end
53
62
 
54
63
  def reset!
55
- @catalog = nil
64
+ @catalog = load_catalog
56
65
  end
57
66
 
58
67
  private
@@ -72,9 +81,7 @@ module Rigor
72
81
  per_class.include?(selector.to_sym) || per_class.include?(selector_str.to_sym)
73
82
  end
74
83
 
75
- def catalog
76
- @catalog ||= load_catalog
77
- end
84
+ attr_reader :catalog
78
85
 
79
86
  def load_catalog
80
87
  return EMPTY_CATALOG unless File.exist?(@path)
@@ -68,15 +68,21 @@ module Rigor
68
68
  # Used by tests to drop the cached catalog so a different
69
69
  # path or content can be exercised. Production code MUST
70
70
  # NOT call this during normal operation.
71
+ #
72
+ # ADR-15 Phase 4b.x — reset re-loads eagerly so the
73
+ # singleton-class `@catalog` ivar stays populated, and
74
+ # the loaded Hash is deep-shared via `Ractor.make_shareable`
75
+ # so a worker Ractor reading the ivar via `catalog.dig(...)`
76
+ # does not trip `Ractor::IsolationError`. Plain `.freeze`
77
+ # is insufficient: YAML parses to a nested Hash/Array/String
78
+ # graph and only the outer Hash would be frozen.
71
79
  def reset!
72
- @catalog = nil
80
+ @catalog = Ractor.make_shareable(load_catalog)
73
81
  end
74
82
 
75
83
  private
76
84
 
77
- def catalog
78
- @catalog ||= load_catalog
79
- end
85
+ attr_reader :catalog
80
86
 
81
87
  def load_catalog
82
88
  return EMPTY_CATALOG unless File.exist?(CATALOG_PATH)
@@ -87,6 +93,11 @@ module Rigor
87
93
  EMPTY_CATALOG
88
94
  end
89
95
  end
96
+
97
+ # ADR-15 Phase 4b.x — eager-load on the main Ractor at
98
+ # module-load time so worker Ractors only READ the
99
+ # populated singleton-class `@catalog` ivar.
100
+ reset!
90
101
  end
91
102
  end
92
103
  end
@@ -24,7 +24,7 @@ module Rigor
24
24
  # hot inference path: it allocates a tracer per visited node and discards
25
25
  # the inferred type values.
26
26
  class CoverageScanner
27
- Result = Data.define(:visits, :unrecognized, :events) do
27
+ class Result < Data.define(:visits, :unrecognized, :events)
28
28
  # @return [Integer] sum of all visits across node classes.
29
29
  def visited_count
30
30
  visits.values.sum
@@ -6,6 +6,7 @@ require_relative "../type"
6
6
  require_relative "../ast"
7
7
  require_relative "block_parameter_binder"
8
8
  require_relative "fallback"
9
+ require_relative "macro_block_self_type"
9
10
  require_relative "method_dispatcher"
10
11
 
11
12
  module Rigor
@@ -111,7 +112,21 @@ module Rigor
111
112
  Prism::IndexOrWriteNode => :type_of_assignment_write,
112
113
  Prism::IndexAndWriteNode => :type_of_assignment_write,
113
114
  Prism::MultiWriteNode => :type_of_assignment_write,
115
+ # LHS-only target nodes (destructuring assignment, pattern matching,
116
+ # `for x in xs`, block parameter `|a, (b, c)|`). They have no value
117
+ # to extract — the type-of pass acknowledges the node class so the
118
+ # coverage scanner stops flagging it; binding the inner names back
119
+ # into the scope is the StatementEvaluator / MultiTargetBinder /
120
+ # BlockParameterBinder side's concern.
114
121
  Prism::LocalVariableTargetNode => :type_of_non_value,
122
+ Prism::MultiTargetNode => :type_of_non_value,
123
+ Prism::InstanceVariableTargetNode => :type_of_non_value,
124
+ Prism::ClassVariableTargetNode => :type_of_non_value,
125
+ Prism::GlobalVariableTargetNode => :type_of_non_value,
126
+ Prism::ConstantTargetNode => :type_of_non_value,
127
+ Prism::ConstantPathTargetNode => :type_of_non_value,
128
+ Prism::CallTargetNode => :type_of_non_value,
129
+ Prism::IndexTargetNode => :type_of_non_value,
115
130
  # Hashes and interpolation
116
131
  Prism::HashNode => :type_of_hash,
117
132
  Prism::KeywordHashNode => :type_of_hash,
@@ -931,7 +946,6 @@ module Rigor
931
946
  # for the CallNode itself (the inner type_of calls already record
932
947
  # their own fallbacks for unrecognised receivers/args, so the tracer
933
948
  # captures both the immediate dispatch miss and the deeper cause).
934
- # rubocop:disable Metrics/CyclomaticComplexity
935
949
  def call_type_for(node)
936
950
  receiver = call_receiver_type_for(node)
937
951
  arg_types = call_arg_types(node)
@@ -1004,7 +1018,6 @@ module Rigor
1004
1018
 
1005
1019
  fallback_for(node, family: :prism)
1006
1020
  end
1007
- # rubocop:enable Metrics/CyclomaticComplexity
1008
1021
 
1009
1022
  # v0.0.2 #5 — re-types the body of a user-defined
1010
1023
  # instance method with the call site's argument types
@@ -1160,9 +1173,20 @@ module Rigor
1160
1173
  # when typing the body raises (defensive against malformed
1161
1174
  # subtrees); the dispatcher then runs in its no-block-aware
1162
1175
  # path.
1176
+ #
1177
+ # ADR-14 gap-#3 (d): a `Prism::BlockArgumentNode` carrying
1178
+ # `&:symbol` (the Symbol#to_proc shorthand) is treated as
1179
+ # a block. The block's return type is computed by
1180
+ # dispatching `:symbol` on the expected block param type
1181
+ # (per `Symbol#to_proc`'s `{ |x| x.symbol }` semantics).
1182
+ # A precise inner dispatch produces the right return; any
1183
+ # failure step falls back to `Dynamic[Top]` so the
1184
+ # dispatcher still SEES a block — selecting the block-
1185
+ # bearing overload of e.g. `Hash#transform_values` over
1186
+ # the no-block overload that returns `Enumerator`.
1163
1187
  def block_return_type_for(call_node, receiver_type, arg_types)
1164
- block_node = call_node.block
1165
- return nil unless block_node.is_a?(Prism::BlockNode)
1188
+ block_arg = call_node.block
1189
+ return nil if block_arg.nil?
1166
1190
  return nil if receiver_type.nil?
1167
1191
 
1168
1192
  expected = MethodDispatcher.expected_block_param_types(
@@ -1171,13 +1195,59 @@ module Rigor
1171
1195
  arg_types: arg_types,
1172
1196
  environment: scope.environment
1173
1197
  )
1174
- bindings = BlockParameterBinder.new(expected_param_types: expected).bind(block_node)
1175
- block_scope = bindings.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
1176
- type_block_body(block_node, block_scope)
1198
+ # ADR-16 Tier A: when a registered plugin's `block_as_methods`
1199
+ # entry matches `(receiver_type, call_node.name)`, narrow the
1200
+ # block body's `self_type` to the receiver class's instance
1201
+ # type. The narrowing is `nil` for unmatched calls, leaving
1202
+ # the existing scope contract unchanged.
1203
+ narrowed_self = MacroBlockSelfType.narrow_self_type_for(
1204
+ scope: scope, call_node: call_node, receiver_type: receiver_type
1205
+ )
1206
+ block_return_for(block_arg, expected, narrowed_self_type: narrowed_self)
1177
1207
  rescue StandardError
1178
1208
  nil
1179
1209
  end
1180
1210
 
1211
+ def block_return_for(block_arg, expected, narrowed_self_type: nil)
1212
+ case block_arg
1213
+ when Prism::BlockNode
1214
+ bindings = BlockParameterBinder.new(expected_param_types: expected).bind(block_arg)
1215
+ block_scope = bindings.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
1216
+ block_scope = block_scope.with_self_type(narrowed_self_type) if narrowed_self_type
1217
+ type_block_body(block_arg, block_scope)
1218
+ when Prism::BlockArgumentNode
1219
+ symbol_block_return_type(block_arg, expected)
1220
+ end
1221
+ end
1222
+
1223
+ # `&:symbol` desugars to a one-arg Proc that dispatches
1224
+ # `symbol` against its argument. When the param type is
1225
+ # known and the resulting inner dispatch is precise,
1226
+ # this returns the precise carrier; otherwise it
1227
+ # returns `Dynamic[Top]` (still non-nil) so the outer
1228
+ # dispatcher selects the block-bearing overload.
1229
+ # `&proc_local` / `&method(:foo)` and friends — anything
1230
+ # not a bare SymbolNode — still resolve to
1231
+ # `Dynamic[Top]` for the same block-presence signal.
1232
+ def symbol_block_return_type(block_arg, expected_param_types)
1233
+ expression = block_arg.expression
1234
+ return dynamic_top unless expression.is_a?(Prism::SymbolNode)
1235
+
1236
+ param_type = expected_param_types&.first
1237
+ return dynamic_top if param_type.nil?
1238
+
1239
+ result = MethodDispatcher.dispatch(
1240
+ receiver_type: param_type,
1241
+ method_name: expression.unescaped.to_sym,
1242
+ arg_types: [],
1243
+ block_type: nil,
1244
+ environment: scope.environment,
1245
+ call_node: block_arg,
1246
+ scope: scope
1247
+ )
1248
+ result || dynamic_top
1249
+ end
1250
+
1181
1251
  def type_block_body(block_node, block_scope)
1182
1252
  body = block_node.body
1183
1253
  return Type::Combinator.constant_of(nil) if body.nil?
@@ -1213,7 +1283,6 @@ module Rigor
1213
1283
  PER_ELEMENT_RANGE_LIMIT = 8
1214
1284
  private_constant :PER_ELEMENT_RANGE_LIMIT
1215
1285
 
1216
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1217
1286
  def try_per_element_block_fold(call_node, receiver_type)
1218
1287
  return nil unless PER_ELEMENT_TUPLE_METHODS.include?(call_node.name)
1219
1288
  return nil if find_family_with_args?(call_node)
@@ -1231,7 +1300,6 @@ module Rigor
1231
1300
 
1232
1301
  assemble_per_element_result(call_node.name, per_position, element_types)
1233
1302
  end
1234
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1235
1303
 
1236
1304
  # Returns the per-position element types for a finite,
1237
1305
  # statically-known receiver shape — or nil when the
@@ -1254,7 +1322,6 @@ module Rigor
1254
1322
  end
1255
1323
  end
1256
1324
 
1257
- # rubocop:disable Metrics/CyclomaticComplexity
1258
1325
  def constant_range_elements(value)
1259
1326
  return nil unless value.is_a?(Range)
1260
1327
  return nil unless value.begin.is_a?(Integer) && value.end.is_a?(Integer)
@@ -1264,7 +1331,6 @@ module Rigor
1264
1331
 
1265
1332
  value.to_a.map { |v| Type::Combinator.constant_of(v) }
1266
1333
  end
1267
- # rubocop:enable Metrics/CyclomaticComplexity
1268
1334
 
1269
1335
  # `index(value)` and `find_index(value)` carry a positional
1270
1336
  # argument and search by `==` rather than running the block.
@@ -20,7 +20,7 @@ module Rigor
20
20
  # - inner_type: the Rigor::Type returned to the caller (currently
21
21
  # always Dynamic[Top]; later slices may carry richer fallback
22
22
  # types).
23
- Fallback = Data.define(:node_class, :location, :family, :inner_type) do
23
+ class Fallback < Data.define(:node_class, :location, :family, :inner_type)
24
24
  def initialize(node_class:, location:, family:, inner_type:)
25
25
  raise ArgumentError, "node_class must be a Class, got #{node_class.class}" unless node_class.is_a?(Class)
26
26
 
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../type"
4
+
5
+ module Rigor
6
+ module Inference
7
+ # ADR-16 Tier A — engine hook. Consults every registered
8
+ # plugin manifest's `block_as_methods` entries to decide
9
+ # whether a block call site qualifies for `Scope#self_type`
10
+ # narrowing.
11
+ #
12
+ # The match contract for a class-level DSL like Sinatra's
13
+ # `class MyApp < Sinatra::Base; get '/foo' do ... end; end`:
14
+ #
15
+ # - the call's lexical receiver type is `Singleton[X]`
16
+ # (the implicit-self in a class body, or an explicit
17
+ # `MyApp.get(...)` call);
18
+ # - the underlying class `X` equals or inherits from the
19
+ # entry's `receiver_constraint`;
20
+ # - the call's method name is in the entry's `verbs`.
21
+ #
22
+ # On a match the helper returns the **instance** type of
23
+ # the receiver class (`Nominal[X]`) — the narrowed
24
+ # `self_type` for the block body, matching Sinatra's
25
+ # runtime semantics where `Sinatra::Base#generate_method`
26
+ # turns the block into an instance method of the user's
27
+ # app class.
28
+ #
29
+ # Slice 1b ships the floor only (per ADR-16 § WD13):
30
+ # bare-identifier method lookups inside the block resolve
31
+ # through the inference engine's normal `self_type`-driven
32
+ # path, so methods declared on `Sinatra::Base` (RBS or
33
+ # otherwise) become visible. Precision additions —
34
+ # parameter-typed block params, declared per-verb argument
35
+ # contracts — are ceiling concerns for later slices.
36
+ module MacroBlockSelfType
37
+ module_function
38
+
39
+ # @param scope [Rigor::Scope]
40
+ # @param call_node [Prism::CallNode]
41
+ # @param receiver_type [Rigor::Type, nil]
42
+ # @return [Rigor::Type, nil] the narrowed self-type, or
43
+ # `nil` when no registered entry matches the call shape.
44
+ def narrow_self_type_for(scope:, call_node:, receiver_type:)
45
+ return nil if receiver_type.nil?
46
+
47
+ environment = scope&.environment
48
+ registry = environment&.plugin_registry
49
+ return nil if registry.nil? || registry.empty?
50
+
51
+ receiver_class_name = singleton_receiver_class_name(receiver_type)
52
+ return nil if receiver_class_name.nil?
53
+
54
+ verb = call_node.name
55
+ registry.plugins.each do |plugin|
56
+ plugin.manifest.block_as_methods.each do |entry| # rigor:disable undefined-method
57
+ return instance_type_for(receiver_class_name, environment) if matches?(entry, verb, receiver_class_name,
58
+ environment)
59
+ end
60
+ end
61
+ nil
62
+ end
63
+
64
+ # Tier A's match contract is intentionally narrow:
65
+ # class-level DSL calls (receiver is `Singleton[X]`) only.
66
+ # Instance-receiver calls and DSL forms whose block body
67
+ # binds a different `self` (Concern's `included do`,
68
+ # `instance_eval { ... }`) are handled by later slices
69
+ # (Concern walker, Tier D, etc.) — not Tier A.
70
+ def singleton_receiver_class_name(receiver_type)
71
+ return nil unless receiver_type.is_a?(Type::Singleton)
72
+
73
+ receiver_type.class_name
74
+ end
75
+
76
+ def matches?(entry, verb, receiver_class_name, environment)
77
+ return false unless entry.verbs.include?(verb)
78
+
79
+ receiver_class_inherits_from?(receiver_class_name, entry.receiver_constraint, environment)
80
+ end
81
+
82
+ def receiver_class_inherits_from?(class_name, constraint, environment)
83
+ return true if class_name == constraint
84
+
85
+ ordering = environment.class_ordering(class_name, constraint)
86
+ %i[equal subclass].include?(ordering)
87
+ rescue StandardError
88
+ false
89
+ end
90
+
91
+ def instance_type_for(class_name, environment)
92
+ environment.nominal_for_name(class_name) || Type::Nominal.new(class_name)
93
+ end
94
+ end
95
+ end
96
+ end
@@ -37,7 +37,7 @@ module Rigor
37
37
  # tuple — element-wise block re-evaluation against
38
38
  # `Constant<Array>` receivers (the `map` / `filter_map` /
39
39
  # `flat_map` precision tier) is reserved for a later slice.
40
- module BlockFolding # rubocop:disable Metrics/ModuleLength
40
+ module BlockFolding
41
41
  module_function
42
42
 
43
43
  FILTER_KEEP_ON_TRUTHY = Set[:select, :filter, :take_while].freeze
@@ -69,7 +69,6 @@ module Rigor
69
69
  # the call's block. `nil` means "no block at the call site"
70
70
  # and disqualifies every rule here.
71
71
  # @return [Rigor::Type, nil]
72
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
73
72
  def try_fold(receiver:, method_name:, args:, block_type:)
74
73
  return nil if receiver.nil? || block_type.nil?
75
74
 
@@ -86,7 +85,6 @@ module Rigor
86
85
  fold_count(receiver, truthiness, args)
87
86
  end
88
87
  end
89
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
90
88
 
91
89
  def filter_method?(method_name)
92
90
  FILTER_KEEP_ON_TRUTHY.include?(method_name) ||
@@ -154,7 +152,7 @@ module Rigor
154
152
  end
155
153
 
156
154
  # @return [:always_true, :always_false, :bool, nil]
157
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
155
+ # rubocop:disable Metrics/CyclomaticComplexity
158
156
  def predicate_decision(method_name, truthiness, emptiness)
159
157
  case method_name
160
158
  when :all?
@@ -177,7 +175,7 @@ module Rigor
177
175
  :bool
178
176
  end
179
177
  end
180
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
178
+ # rubocop:enable Metrics/CyclomaticComplexity
181
179
 
182
180
  def bool_union
183
181
  Type::Combinator.union(
@@ -149,7 +149,6 @@ module Rigor
149
149
  # and runs the format. Symbol keys are kept as
150
150
  # Symbols (matching Ruby's `%{key}` resolution).
151
151
  # Anything else declines so the RBS tier widens.
152
- # rubocop:disable Metrics/CyclomaticComplexity
153
152
  def try_fold_string_format(receiver, method_name, args)
154
153
  return nil unless method_name == :%
155
154
  return nil unless args.size == 1
@@ -166,7 +165,6 @@ module Rigor
166
165
  rescue StandardError
167
166
  nil
168
167
  end
169
- # rubocop:enable Metrics/CyclomaticComplexity
170
168
 
171
169
  def format_argument_value(arg)
172
170
  case arg
@@ -275,7 +273,6 @@ module Rigor
275
273
  [result]
276
274
  end
277
275
 
278
- # rubocop:disable Metrics/CyclomaticComplexity
279
276
  def try_fold_unary_set(receiver_values, method_name)
280
277
  range_lift = try_fold_range_constant_unary(receiver_values, method_name)
281
278
  return range_lift if range_lift
@@ -299,8 +296,6 @@ module Rigor
299
296
  end
300
297
  build_constant_type(results, source: receiver_values)
301
298
  end
302
- # rubocop:enable Metrics/CyclomaticComplexity
303
-
304
299
  # v0.0.7 — `Constant<Range>#to_a` and the no-arg
305
300
  # `first` / `last` / `min` / `max` short-circuit through a
306
301
  # Range-specific arm that catalog dispatch cannot reach:
@@ -353,7 +348,6 @@ module Rigor
353
348
  Type::Combinator.constant_of(edge == :first ? values.first : values.last)
354
349
  end
355
350
 
356
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
357
351
  def try_fold_binary_set(receiver_values, method_name, arg_values)
358
352
  string_lift = try_fold_string_array_binary(receiver_values, method_name, arg_values)
359
353
  return string_lift if string_lift
@@ -369,8 +363,6 @@ module Rigor
369
363
  end
370
364
  build_constant_type(results, source: receiver_values + arg_values)
371
365
  end
372
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
373
-
374
366
  # v0.0.7 — `Constant<String>#chars` / `bytes` / `lines` /
375
367
  # `split` (no-arg) return a Ruby Array of foldable
376
368
  # scalars; `foldable_constant_value?` rejects Array
@@ -425,7 +417,6 @@ module Rigor
425
417
  nil
426
418
  end
427
419
 
428
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
429
420
  def try_fold_pathname_binary(receiver_values, method_name, arg_values)
430
421
  return nil unless PATHNAME_PURE_BINARY.include?(method_name)
431
422
  return nil unless receiver_values.size == 1 && arg_values.size == 1
@@ -442,7 +433,6 @@ module Rigor
442
433
  rescue StandardError
443
434
  nil
444
435
  end
445
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
446
436
 
447
437
  def try_fold_string_array_unary(receiver_values, method_name)
448
438
  return nil unless STRING_ARRAY_UNARY_METHODS.include?(method_name)
@@ -459,7 +449,6 @@ module Rigor
459
449
  # `Constant<String>#split(arg)` / `#scan(arg)` — lift the
460
450
  # Array result to a Tuple when both sides are statically
461
451
  # known and the cardinality fits.
462
- # rubocop:disable Metrics/CyclomaticComplexity
463
452
  def try_fold_string_array_binary(receiver_values, method_name, arg_values)
464
453
  return nil unless STRING_ARRAY_BINARY_METHODS.include?(method_name)
465
454
  return nil unless receiver_values.size == 1 && arg_values.size == 1
@@ -473,7 +462,6 @@ module Rigor
473
462
  rescue StandardError
474
463
  nil
475
464
  end
476
- # rubocop:enable Metrics/CyclomaticComplexity
477
465
 
478
466
  def lift_array_result(result)
479
467
  return nil unless result.is_a?(Array)
@@ -1040,10 +1028,10 @@ module Rigor
1040
1028
  # class's ancestor chain at lookup time; the catalog
1041
1029
  # corresponds to the module-mode YAML at
1042
1030
  # `data/builtins/ruby_core/<topic>.yml`.
1043
- MODULE_CATALOGS = [
1044
- [Comparable, Builtins::COMPARABLE_CATALOG, "Comparable"],
1045
- [Enumerable, Builtins::ENUMERABLE_CATALOG, "Enumerable"]
1046
- ].freeze
1031
+ MODULE_CATALOGS = Ractor.make_shareable([
1032
+ [Comparable, Builtins::COMPARABLE_CATALOG, "Comparable"],
1033
+ [Enumerable, Builtins::ENUMERABLE_CATALOG, "Enumerable"]
1034
+ ])
1047
1035
  private_constant :MODULE_CATALOGS
1048
1036
 
1049
1037
  # Returns the `(catalog, class_name)` pairs for every
@@ -1069,31 +1057,31 @@ module Rigor
1069
1057
  # Otherwise a `DateTime` receiver would match the `Date`
1070
1058
  # arm first and the catalog would consult the Date entry
1071
1059
  # in `DATE_CATALOG` for the wrong class.
1072
- CATALOG_BY_CLASS = [
1073
- [Integer, [Builtins::NumericCatalog, "Integer"]],
1074
- [Float, [Builtins::NumericCatalog, "Float"]],
1075
- [String, [Builtins::STRING_CATALOG, "String"]],
1076
- [Symbol, [Builtins::STRING_CATALOG, "Symbol"]],
1077
- [Array, [Builtins::ARRAY_CATALOG, "Array"]],
1078
- [Hash, [Builtins::HASH_CATALOG, "Hash"]],
1079
- [Range, [Builtins::RANGE_CATALOG, "Range"]],
1080
- [::Set, [Builtins::SET_CATALOG, "Set"]],
1081
- [Time, [Builtins::TIME_CATALOG, "Time"]],
1082
- [DateTime, [Builtins::DATE_CATALOG, "DateTime"]],
1083
- [Date, [Builtins::DATE_CATALOG, "Date"]],
1084
- [Rational, [Builtins::RATIONAL_CATALOG, "Rational"]],
1085
- [Complex, [Builtins::COMPLEX_CATALOG, "Complex"]],
1086
- [Pathname, [Builtins::PATHNAME_CATALOG, "Pathname"]],
1087
- [Random, [Builtins::RANDOM_CATALOG, "Random"]],
1088
- [Struct, [Builtins::STRUCT_CATALOG, "Struct"]],
1089
- [Encoding, [Builtins::ENCODING_CATALOG, "Encoding"]],
1090
- [Regexp, [Builtins::REGEXP_CATALOG, "Regexp"]],
1091
- [MatchData, [Builtins::REGEXP_CATALOG, "MatchData"]],
1092
- [Proc, [Builtins::PROC_CATALOG, "Proc"]],
1093
- [Method, [Builtins::PROC_CATALOG, "Method"]],
1094
- [UnboundMethod, [Builtins::PROC_CATALOG, "UnboundMethod"]],
1095
- [Exception, [Builtins::EXCEPTION_CATALOG, "Exception"]]
1096
- ].freeze
1060
+ CATALOG_BY_CLASS = Ractor.make_shareable([
1061
+ [Integer, [Builtins::NumericCatalog, "Integer"]],
1062
+ [Float, [Builtins::NumericCatalog, "Float"]],
1063
+ [String, [Builtins::STRING_CATALOG, "String"]],
1064
+ [Symbol, [Builtins::STRING_CATALOG, "Symbol"]],
1065
+ [Array, [Builtins::ARRAY_CATALOG, "Array"]],
1066
+ [Hash, [Builtins::HASH_CATALOG, "Hash"]],
1067
+ [Range, [Builtins::RANGE_CATALOG, "Range"]],
1068
+ [::Set, [Builtins::SET_CATALOG, "Set"]],
1069
+ [Time, [Builtins::TIME_CATALOG, "Time"]],
1070
+ [DateTime, [Builtins::DATE_CATALOG, "DateTime"]],
1071
+ [Date, [Builtins::DATE_CATALOG, "Date"]],
1072
+ [Rational, [Builtins::RATIONAL_CATALOG, "Rational"]],
1073
+ [Complex, [Builtins::COMPLEX_CATALOG, "Complex"]],
1074
+ [Pathname, [Builtins::PATHNAME_CATALOG, "Pathname"]],
1075
+ [Random, [Builtins::RANDOM_CATALOG, "Random"]],
1076
+ [Struct, [Builtins::STRUCT_CATALOG, "Struct"]],
1077
+ [Encoding, [Builtins::ENCODING_CATALOG, "Encoding"]],
1078
+ [Regexp, [Builtins::REGEXP_CATALOG, "Regexp"]],
1079
+ [MatchData, [Builtins::REGEXP_CATALOG, "MatchData"]],
1080
+ [Proc, [Builtins::PROC_CATALOG, "Proc"]],
1081
+ [Method, [Builtins::PROC_CATALOG, "Method"]],
1082
+ [UnboundMethod, [Builtins::PROC_CATALOG, "UnboundMethod"]],
1083
+ [Exception, [Builtins::EXCEPTION_CATALOG, "Exception"]]
1084
+ ])
1097
1085
  private_constant :CATALOG_BY_CLASS
1098
1086
 
1099
1087
  # Returns `[catalog, class_name]` for receivers we have a