rigortype 0.1.19 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -6
  3. data/data/core_overlay/numeric.rbs +33 -0
  4. data/data/core_overlay/pathname.rbs +25 -0
  5. data/data/core_overlay/string_scanner.rbs +28 -0
  6. data/data/gem_overlay/activesupport/core_ext.rbs +473 -0
  7. data/data/vendored_gem_sigs/ast/ast.rbs +130 -0
  8. data/data/vendored_gem_sigs/bcrypt/bcrypt.rbs +47 -0
  9. data/data/vendored_gem_sigs/bundler/bundler.rbs +238 -0
  10. data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +34 -0
  11. data/data/vendored_gem_sigs/did_you_mean/did_you_mean_extras.rbs +34 -0
  12. data/data/vendored_gem_sigs/idn-ruby/idn.rbs +54 -0
  13. data/data/vendored_gem_sigs/mysql2/client.rbs +55 -0
  14. data/data/vendored_gem_sigs/mysql2/error.rbs +5 -0
  15. data/data/vendored_gem_sigs/mysql2/result.rbs +31 -0
  16. data/data/vendored_gem_sigs/mysql2/statement.rbs +5 -0
  17. data/data/vendored_gem_sigs/nokogiri/nokogiri.rbs +2332 -0
  18. data/data/vendored_gem_sigs/nokogiri/nokogiri_html5.rbs +47 -0
  19. data/data/vendored_gem_sigs/pg/pg.rbs +212 -0
  20. data/data/vendored_gem_sigs/prism/prism_supplement.rbs +44 -0
  21. data/data/vendored_gem_sigs/redis/errors.rbs +50 -0
  22. data/data/vendored_gem_sigs/redis/future.rbs +5 -0
  23. data/data/vendored_gem_sigs/redis/redis.rbs +348 -0
  24. data/data/vendored_gem_sigs/redis/redis_extras.rbs +130 -0
  25. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +226 -0
  26. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -23
  27. data/lib/rigor/analysis/check_rules/rule_walk.rb +3 -21
  28. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
  29. data/lib/rigor/analysis/check_rules.rb +492 -71
  30. data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
  31. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
  32. data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
  33. data/lib/rigor/analysis/fact_store.rb +5 -4
  34. data/lib/rigor/analysis/rule_catalog.rb +153 -6
  35. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +17 -17
  36. data/lib/rigor/analysis/runner/project_pre_passes.rb +9 -8
  37. data/lib/rigor/analysis/runner.rb +17 -6
  38. data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
  39. data/lib/rigor/analysis/worker_session.rb +10 -14
  40. data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
  41. data/lib/rigor/cache/store.rb +5 -3
  42. data/lib/rigor/cli/annotate_command.rb +28 -7
  43. data/lib/rigor/cli/baseline_command.rb +4 -3
  44. data/lib/rigor/cli/check_command.rb +138 -16
  45. data/lib/rigor/cli/coverage_command.rb +138 -31
  46. data/lib/rigor/cli/coverage_mutation.rb +149 -0
  47. data/lib/rigor/cli/coverage_scan.rb +57 -0
  48. data/lib/rigor/cli/explain_command.rb +2 -0
  49. data/lib/rigor/cli/fused_protection_renderer.rb +67 -0
  50. data/lib/rigor/cli/fused_protection_report.rb +76 -0
  51. data/lib/rigor/cli/lsp_command.rb +3 -7
  52. data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
  53. data/lib/rigor/cli/mutation_protection_report.rb +73 -0
  54. data/lib/rigor/cli/options.rb +9 -0
  55. data/lib/rigor/cli/plugins_command.rb +2 -1
  56. data/lib/rigor/cli/protection_renderer.rb +63 -0
  57. data/lib/rigor/cli/protection_report.rb +68 -0
  58. data/lib/rigor/cli/sig_gen_command.rb +2 -1
  59. data/lib/rigor/cli/trace_command.rb +2 -1
  60. data/lib/rigor/cli/triage_command.rb +2 -1
  61. data/lib/rigor/cli/type_of_command.rb +1 -1
  62. data/lib/rigor/cli/type_scan_command.rb +2 -1
  63. data/lib/rigor/cli.rb +3 -2
  64. data/lib/rigor/config_audit.rb +152 -0
  65. data/lib/rigor/configuration/dependencies.rb +2 -4
  66. data/lib/rigor/configuration.rb +57 -7
  67. data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
  68. data/lib/rigor/environment/class_registry.rb +4 -3
  69. data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
  70. data/lib/rigor/environment/lockfile_resolver.rb +1 -1
  71. data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
  72. data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
  73. data/lib/rigor/environment/rbs_loader.rb +76 -5
  74. data/lib/rigor/environment.rb +66 -8
  75. data/lib/rigor/flow_contribution/fact.rb +1 -1
  76. data/lib/rigor/flow_contribution.rb +3 -5
  77. data/lib/rigor/inference/acceptance.rb +17 -9
  78. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  79. data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
  80. data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
  81. data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
  82. data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
  83. data/lib/rigor/inference/expression_typer.rb +20 -28
  84. data/lib/rigor/inference/hkt_body.rb +8 -11
  85. data/lib/rigor/inference/hkt_body_parser.rb +10 -12
  86. data/lib/rigor/inference/hkt_registry.rb +10 -11
  87. data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
  88. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +169 -24
  89. data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
  90. data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
  91. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
  92. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
  93. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
  94. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
  95. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
  96. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +90 -15
  97. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
  98. data/lib/rigor/inference/method_dispatcher.rb +40 -48
  99. data/lib/rigor/inference/mutation_widening.rb +5 -11
  100. data/lib/rigor/inference/narrowing.rb +14 -16
  101. data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
  102. data/lib/rigor/inference/project_patched_methods.rb +4 -7
  103. data/lib/rigor/inference/project_patched_scanner.rb +2 -13
  104. data/lib/rigor/inference/protection_scanner.rb +86 -0
  105. data/lib/rigor/inference/scope_indexer.rb +129 -55
  106. data/lib/rigor/inference/statement_evaluator.rb +271 -114
  107. data/lib/rigor/inference/struct_fold_safety.rb +181 -0
  108. data/lib/rigor/inference/synthetic_method.rb +7 -7
  109. data/lib/rigor/language_server/completion_provider.rb +6 -12
  110. data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
  111. data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
  112. data/lib/rigor/language_server/hover_provider.rb +2 -3
  113. data/lib/rigor/language_server/hover_renderer.rb +2 -11
  114. data/lib/rigor/language_server/server.rb +9 -17
  115. data/lib/rigor/language_server.rb +4 -5
  116. data/lib/rigor/plugin/base.rb +10 -8
  117. data/lib/rigor/plugin/macro/block_as_method.rb +3 -4
  118. data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
  119. data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
  120. data/lib/rigor/plugin/macro.rb +4 -5
  121. data/lib/rigor/plugin/manifest.rb +45 -66
  122. data/lib/rigor/plugin/registry.rb +6 -7
  123. data/lib/rigor/plugin/type_node_resolver.rb +6 -8
  124. data/lib/rigor/protection/diagnostic_oracle.rb +51 -0
  125. data/lib/rigor/protection/mutation_scanner.rb +180 -0
  126. data/lib/rigor/protection/mutator.rb +267 -0
  127. data/lib/rigor/protection/test_suite_oracle.rb +68 -0
  128. data/lib/rigor/rbs_extended.rb +24 -36
  129. data/lib/rigor/reflection.rb +4 -7
  130. data/lib/rigor/scope/discovery_index.rb +14 -2
  131. data/lib/rigor/scope.rb +54 -11
  132. data/lib/rigor/sig_gen/observed_call.rb +3 -3
  133. data/lib/rigor/sig_gen/writer.rb +40 -2
  134. data/lib/rigor/signature_path_audit.rb +92 -0
  135. data/lib/rigor/source/constant_path.rb +62 -0
  136. data/lib/rigor/source.rb +1 -0
  137. data/lib/rigor/type/bound_method.rb +2 -11
  138. data/lib/rigor/type/combinator.rb +16 -3
  139. data/lib/rigor/type/constant.rb +2 -11
  140. data/lib/rigor/type/data_class.rb +2 -11
  141. data/lib/rigor/type/data_instance.rb +2 -11
  142. data/lib/rigor/type/hash_shape.rb +2 -11
  143. data/lib/rigor/type/integer_range.rb +2 -11
  144. data/lib/rigor/type/intersection.rb +2 -11
  145. data/lib/rigor/type/nominal.rb +2 -11
  146. data/lib/rigor/type/plain_lattice.rb +37 -0
  147. data/lib/rigor/type/refined.rb +72 -13
  148. data/lib/rigor/type/singleton.rb +2 -11
  149. data/lib/rigor/type/struct_class.rb +75 -0
  150. data/lib/rigor/type/struct_instance.rb +93 -0
  151. data/lib/rigor/type/tuple.rb +5 -15
  152. data/lib/rigor/type.rb +2 -0
  153. data/lib/rigor/version.rb +1 -1
  154. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  155. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
  156. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +3 -3
  157. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
  158. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
  159. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +7 -10
  160. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
  161. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
  162. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +6 -8
  163. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
  164. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -2
  165. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
  166. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
  167. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
  168. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
  169. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
  170. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
  171. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
  172. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +7 -9
  173. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
  174. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
  175. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +3 -3
  176. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
  177. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
  178. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
  179. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +1 -1
  180. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
  181. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
  182. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +5 -5
  183. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -4
  184. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
  185. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  186. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +19 -14
  187. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
  188. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
  189. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
  190. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
  191. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
  192. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
  193. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
  194. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +28 -41
  195. data/sig/rigor/scope.rbs +9 -1
  196. data/sig/rigor/type.rbs +36 -1
  197. metadata +49 -1
@@ -40,7 +40,7 @@ module Rigor
40
40
  # receiver/argument combination.
41
41
  #
42
42
  # Anything else returns `nil`, signalling "no rule matched" so the
43
- # caller (`ExpressionTyper`) falls back to `Dynamic[Top]` and records a
43
+ # caller (`MethodDispatcher`) falls back to `Dynamic[Top]` and records a
44
44
  # fail-soft event. Slice 4 (RBS-backed) layers another dispatch tier
45
45
  # behind this rule book, but the constant-folding semantics defined
46
46
  # here MUST NOT regress: any value reachable by literal arithmetic at
@@ -51,7 +51,7 @@ module Rigor
51
51
  NUMERIC_BINARY = Set[
52
52
  :+, :-, :*, :/, :%, :**, :&, :|, :^, :<<, :>>,
53
53
  :<, :<=, :>, :>=, :==, :!=, :<=>,
54
- :gcd, :lcm, :fdiv
54
+ :gcd, :lcm, :fdiv, :quo, :ceildiv, :[]
55
55
  ].freeze
56
56
  STRING_BINARY = Set[
57
57
  :+, :*, :==, :!=, :<, :<=, :>, :>=, :<=>,
@@ -60,12 +60,31 @@ module Rigor
60
60
  :match?, :index, :rindex, :center, :ljust, :rjust,
61
61
  # 1-arg pure transforms/queries whose output never exceeds the
62
62
  # input: `delete`/`squeeze` shrink the string, `count` → Integer.
63
- :delete, :count, :squeeze
63
+ :delete, :count, :squeeze,
64
+ # ASCII / Unicode-case-fold comparison — deterministic, no
65
+ # locale read: `casecmp` → -1/0/1, `casecmp?` → bool/nil.
66
+ :casecmp, :casecmp?
64
67
  ].freeze
65
68
  SYMBOL_BINARY = Set[:==, :!=, :<=>, :<, :<=, :>, :>=].freeze
66
69
  BOOL_BINARY = Set[:&, :|, :^, :==, :!=, :===].freeze
67
70
  NIL_BINARY = Set[:==, :!=].freeze
68
- RATIONAL_BINARY = Set[:div, :modulo, :%, :remainder, :fdiv].freeze
71
+ # Rational arithmetic / ordering are exact and pure. Division
72
+ # (`/`) and `**` may return a `Float`/`Complex` for some operands,
73
+ # all of which are foldable `Constant` value classes. `==` / `!=`
74
+ # are deliberately EXCLUDED: `Rational#==` (`nurat_eqeq_p`) routes
75
+ # through `rb_funcall(:==)` on the operands — user-redefinable —
76
+ # so the catalog classifies it `:dispatch` and the equality stays
77
+ # the RBS `bool`. (The set would otherwise bypass that gate.)
78
+ RATIONAL_BINARY = Set[
79
+ :+, :-, :*, :/, :**, :<=>, :<, :<=, :>, :>=,
80
+ :div, :modulo, :%, :remainder, :fdiv, :quo
81
+ ].freeze
82
+ # Complex arithmetic. `ops_for` gains a `Complex` branch so these
83
+ # reach the binary fold path (Complex was previously unary-only).
84
+ # `/` and `**` stay foldable (Complex result). `==` / `!=` are
85
+ # excluded for the same reason as Rational (`nucomp_eqeq_p`
86
+ # delegates to operand `==`); ordering is undefined for Complex.
87
+ COMPLEX_BINARY = Set[:+, :-, :*, :/, :**].freeze
69
88
 
70
89
  # v0.0.3 C — pure unary catalogue. Each method must:
71
90
  # - take zero arguments,
@@ -83,20 +102,35 @@ module Rigor
83
102
  # user-defined `def is_odd(n) = n.odd?` so
84
103
  # `Parity.new.is_odd(3)` types as `Constant[true]`
85
104
  # rather than the RBS-widened `bool`.
105
+ # NOTE: `:hash` is deliberately NOT in any of these sets.
106
+ # `Object#hash` (and the `String`/`Symbol`/`Integer`/`Float`
107
+ # overrides) is salted with a per-process SipHash seed, so
108
+ # `"abc".hash` returns a different Integer in every Ruby
109
+ # process. Folding it to a `Constant` would bake one process's
110
+ # value into the type (and the on-disk cache), making the
111
+ # result non-deterministic across runs — a violation of the
112
+ # purity contract this catalogue rests on. A literal's `.hash`
113
+ # therefore stays the RBS-widened `Integer`. The deterministic
114
+ # siblings `:inspect` / `:to_s` remain folded.
86
115
  INTEGER_UNARY = Set[
87
116
  :odd?, :even?, :zero?, :positive?, :negative?,
117
+ # `finite?` / `infinite?` are total on Integer (`true` / `nil`
118
+ # always) and round out the numeric predicate family — the Float
119
+ # sibling already folds them. `nonzero?` returns `self` (non-zero)
120
+ # or `nil`, both foldable Constants.
121
+ :finite?, :infinite?, :nonzero?,
88
122
  :succ, :pred, :next, :abs, :magnitude,
89
123
  :bit_length, :to_s, :to_i, :to_int, :to_f,
90
124
  :floor, :ceil, :round, :truncate, :chr,
91
- :inspect, :hash, :-@, :+@, :~
125
+ :inspect, :-@, :+@, :~, :to_r, :to_c
92
126
  ].freeze
93
127
  FLOAT_UNARY = Set[
94
- :zero?, :positive?, :negative?,
95
- :nan?, :finite?, :infinite?,
128
+ :zero?, :positive?, :negative?, :nonzero?,
129
+ :nan?, :finite?, :infinite?, :integer?,
96
130
  :abs, :magnitude, :floor, :ceil, :round, :truncate,
97
131
  :next_float, :prev_float,
98
- :to_s, :to_i, :to_int, :to_f,
99
- :inspect, :hash, :-@, :+@
132
+ :to_s, :to_i, :to_int, :to_f, :to_r, :rationalize,
133
+ :inspect, :-@, :+@
100
134
  ].freeze
101
135
  STRING_UNARY = Set[
102
136
  :upcase, :downcase, :capitalize, :swapcase,
@@ -104,20 +138,33 @@ module Rigor
104
138
  :empty?, :strip, :lstrip, :rstrip, :chomp, :chop, :squeeze,
105
139
  :to_s, :to_str, :to_sym, :intern,
106
140
  :to_i, :to_f, :ord, :chr, :hex, :oct, :succ, :next,
107
- :inspect, :hash
141
+ :sum, :inspect
108
142
  ].freeze
109
143
  SYMBOL_UNARY = Set[
110
144
  :to_s, :to_sym, :to_proc, :length, :size,
111
145
  :empty?, :upcase, :downcase, :capitalize,
112
- :swapcase, :inspect, :hash
146
+ :swapcase, :succ, :next, :inspect,
147
+ # `name` (the frozen-string accessor), `id2name` (alias of
148
+ # `to_s`), and `intern` (alias of `to_sym`) are pure reads of the
149
+ # symbol's text — siblings of the already-folded `to_s` / `to_sym`.
150
+ :name, :id2name, :intern
113
151
  ].freeze
114
- BOOL_UNARY = Set[:!, :to_s, :inspect, :hash, :&, :|, :^].freeze
115
- NIL_UNARY = Set[:nil?, :!, :to_s, :to_a, :to_h, :inspect, :hash].freeze
152
+ BOOL_UNARY = Set[:!, :to_s, :inspect, :&, :|, :^].freeze
153
+ NIL_UNARY = Set[:nil?, :!, :to_s, :to_a, :to_h, :inspect].freeze
116
154
  RATIONAL_UNARY = Set[
117
155
  :zero?, :integer?, :real, :abs2,
118
- :conj, :conjugate, :nonzero?
156
+ :conj, :conjugate, :nonzero?,
157
+ :numerator, :denominator, :abs, :magnitude,
158
+ :to_f, :to_i, :to_int, :to_r, :rationalize,
159
+ :floor, :ceil, :round, :truncate,
160
+ :-@, :+@
161
+ ].freeze
162
+ COMPLEX_UNARY = Set[
163
+ :zero?, :nonzero?,
164
+ :abs, :magnitude, :abs2, :arg, :angle, :phase,
165
+ :conjugate, :conj, :real, :imaginary, :imag,
166
+ :to_c, :-@, :+@
119
167
  ].freeze
120
- COMPLEX_UNARY = Set[:zero?, :nonzero?].freeze
121
168
 
122
169
  STRING_FOLD_BYTE_LIMIT = 4096
123
170
 
@@ -386,9 +433,15 @@ module Rigor
386
433
  # Only fires on a single-receiver Range with finite integer
387
434
  # endpoints; mixed unions fall through so the existing
388
435
  # union-of-Constants path keeps the rest of the arms.
389
- RANGE_FOLD_METHODS = Set[:to_a, :first, :last, :min, :max, :count, :size, :length, :entries, :minmax].freeze
436
+ RANGE_FOLD_METHODS = Set[:to_a, :first, :last, :min, :max, :count, :size, :length, :entries, :minmax,
437
+ :sum].freeze
438
+ # 1-arg head/tail projections on a `Constant<Range>`. `first(n)` /
439
+ # `take(n)` return the first `n` elements, `last(n)` the final `n` —
440
+ # each lifts to a per-position `Tuple[Constant[Integer]…]`. The
441
+ # no-arg `first` / `last` stay on the unary path (single Integer).
442
+ RANGE_FOLD_BINARY_METHODS = Set[:first, :last, :take].freeze
390
443
  RANGE_TO_A_LIMIT = 16
391
- private_constant :RANGE_FOLD_METHODS, :RANGE_TO_A_LIMIT
444
+ private_constant :RANGE_FOLD_METHODS, :RANGE_FOLD_BINARY_METHODS, :RANGE_TO_A_LIMIT
392
445
 
393
446
  def try_fold_range_constant_unary(receiver_values, method_name)
394
447
  return nil unless RANGE_FOLD_METHODS.include?(method_name)
@@ -408,6 +461,11 @@ module Rigor
408
461
  when :last, :max then range_endpoint_constant(range, :last)
409
462
  when :count, :size, :length then Type::Combinator.constant_of(range.to_a.size)
410
463
  when :minmax then range_minmax_tuple(range)
464
+ # `range.sum` is closed-form (Gauss) for an integer range, so a
465
+ # huge range still costs O(1) and yields a single Integer — no
466
+ # materialisation, no cap needed. Endless ranges are already
467
+ # excluded by the Integer-endpoint guard in the caller.
468
+ when :sum then Type::Combinator.constant_of(range.sum)
411
469
  end
412
470
  end
413
471
 
@@ -441,10 +499,46 @@ module Rigor
441
499
  )
442
500
  end
443
501
 
502
+ # `(1..10).first(3)` / `.take(3)` / `.last(3)` — the 1-arg head /
503
+ # tail forms. `first`/`last` already fold no-arg through the unary
504
+ # path; this is the n-arg sibling, mirroring the Tuple carrier's
505
+ # `first(n)`/`take(n)` handlers. Lifts to `Tuple[Constant…]`.
506
+ def try_fold_range_constant_binary(receiver_values, method_name, arg_values)
507
+ return nil unless RANGE_FOLD_BINARY_METHODS.include?(method_name)
508
+ return nil unless receiver_values.size == 1 && arg_values.size == 1
509
+
510
+ range = receiver_values.first
511
+ return nil unless range.is_a?(Range)
512
+ return nil unless range.begin.is_a?(Integer) && range.end.is_a?(Integer)
513
+
514
+ range_take_tuple(range, method_name, arg_values.first)
515
+ rescue StandardError
516
+ nil
517
+ end
518
+
519
+ def range_take_tuple(range, method_name, count)
520
+ return nil unless count.is_a?(Integer) && !count.negative?
521
+ # `first(n)`/`last(n)`/`take(n)` materialise at most `min(n, size)`
522
+ # elements; cap that count so a huge `n` (or range) never blows up
523
+ # the Constant. `Range#size` is O(1) for integer endpoints.
524
+ return nil if [count, range.size].min > RANGE_TO_A_LIMIT
525
+
526
+ values = method_name == :last ? range.last(count) : range.first(count)
527
+ return Type::Combinator.tuple_of if values.empty?
528
+
529
+ Type::Combinator.tuple_of(*values.map { |v| Type::Combinator.constant_of(v) })
530
+ end
531
+
444
532
  def try_fold_binary_set(receiver_values, method_name, arg_values)
533
+ range_lift = try_fold_range_constant_binary(receiver_values, method_name, arg_values)
534
+ return range_lift if range_lift
535
+
445
536
  string_lift = try_fold_string_array_binary(receiver_values, method_name, arg_values)
446
537
  return string_lift if string_lift
447
538
 
539
+ integer_lift = try_fold_integer_array_binary(receiver_values, method_name, arg_values)
540
+ return integer_lift if integer_lift
541
+
448
542
  pathname_lift = try_fold_pathname_binary(receiver_values, method_name, arg_values)
449
543
  return pathname_lift if pathname_lift
450
544
 
@@ -456,15 +550,21 @@ module Rigor
456
550
  end
457
551
  build_constant_type(results, source: receiver_values + arg_values)
458
552
  end
459
- # v0.0.7 — `Constant<String>#chars` / `bytes` / `lines` /
460
- # `split` (no-arg) return a Ruby Array of foldable
461
- # scalars; `foldable_constant_value?` rejects Array
553
+ # v0.0.7 — `Constant<String>#chars` / `bytes` / `codepoints` /
554
+ # `grapheme_clusters` / `lines` / `split` (no-arg) return a Ruby
555
+ # Array of foldable scalars; `foldable_constant_value?` rejects Array
462
556
  # results, so the standard unary path declines. Lift the
463
557
  # Array to a per-position `Tuple[Constant…]` directly,
464
558
  # capped at `STRING_ARRAY_LIFT_LIMIT` to keep the result
465
- # bounded for long strings.
466
- STRING_ARRAY_UNARY_METHODS = Set[:chars, :bytes, :lines, :split].freeze
467
- STRING_ARRAY_BINARY_METHODS = Set[:split, :scan].freeze
559
+ # bounded for long strings. (`codepoints` yields per-character
560
+ # Integer codepoints, the sibling of the byte-valued `bytes`;
561
+ # `grapheme_clusters` is the extended-grapheme sibling of `chars`.)
562
+ STRING_ARRAY_UNARY_METHODS = Set[:chars, :bytes, :codepoints, :grapheme_clusters, :lines, :split].freeze
563
+ # `partition` / `rpartition` always return a fixed 3-element
564
+ # `[head, separator, tail]` Array whose members are substrings of
565
+ # the receiver (bounded by the input), so they lift to a precise
566
+ # 3-slot `Tuple[Constant…]`.
567
+ STRING_ARRAY_BINARY_METHODS = Set[:split, :scan, :partition, :rpartition].freeze
468
568
  STRING_ARRAY_LIFT_LIMIT = 32
469
569
  private_constant :STRING_ARRAY_UNARY_METHODS,
470
570
  :STRING_ARRAY_BINARY_METHODS,
@@ -494,6 +594,14 @@ module Rigor
494
594
  INTEGER_ARRAY_UNARY_METHODS = Set[:digits].freeze
495
595
  private_constant :INTEGER_ARRAY_UNARY_METHODS
496
596
 
597
+ # 1-arg Integer methods that return an Array of foldable
598
+ # Integers: `digits(base)` (base-n place values; raises on a
599
+ # negative receiver or base < 2 → declines) and `gcdlcm(other)`
600
+ # (the fixed `[gcd, lcm]` pair). Both are pure arithmetic; the
601
+ # result lifts to a `Tuple[Constant[Integer]…]`.
602
+ INTEGER_ARRAY_BINARY_METHODS = Set[:digits, :gcdlcm].freeze
603
+ private_constant :INTEGER_ARRAY_BINARY_METHODS
604
+
497
605
  # v0.0.7 — `Constant<Pathname>` delegates to a curated set
498
606
  # of pure path-manipulation methods. Pathname is immutable
499
607
  # in Ruby (per its docstring) and the catalog classifies
@@ -613,6 +721,25 @@ module Rigor
613
721
  nil
614
722
  end
615
723
 
724
+ # `Constant<Integer>#digits(base)` / `#gcdlcm(other)` — the
725
+ # 1-arg Array-returning Integer methods. `digits(base)` declines
726
+ # on a negative receiver (the unary path's guard); other domain
727
+ # errors (base < 2) raise and are rescued. `gcdlcm` is total over
728
+ # Integer args.
729
+ def try_fold_integer_array_binary(receiver_values, method_name, arg_values)
730
+ return nil unless INTEGER_ARRAY_BINARY_METHODS.include?(method_name)
731
+ return nil unless receiver_values.size == 1 && arg_values.size == 1
732
+
733
+ receiver = receiver_values.first
734
+ arg = arg_values.first
735
+ return nil unless receiver.is_a?(Integer) && arg.is_a?(Integer)
736
+ return nil if method_name == :digits && receiver.negative?
737
+
738
+ lift_array_result(receiver.public_send(method_name, arg))
739
+ rescue StandardError
740
+ nil
741
+ end
742
+
616
743
  # `Constant<Complex>#rect` / `#rectangular` — lifts `[real, imaginary]`
617
744
  # to `Tuple[Constant[re], Constant[im]]`. Both components are always
618
745
  # numeric (Integer or Float for literal complexes), so they satisfy
@@ -1334,7 +1461,24 @@ module Rigor
1334
1461
  private_constant :FOLDABLE_CONSTANT_CLASSES
1335
1462
 
1336
1463
  def foldable_constant_value?(value)
1337
- FOLDABLE_CONSTANT_CLASSES.any? { |klass| value.is_a?(klass) }
1464
+ return false unless FOLDABLE_CONSTANT_CLASSES.any? { |klass| value.is_a?(klass) }
1465
+
1466
+ # A NaN result (`0.0 / 0.0`, `Float::NAN`-propagating arithmetic,
1467
+ # or a NaN-bearing Complex) is non-reflexive under `==`, so a
1468
+ # `Constant[NaN]` would break the `==` / `eql?` / `hash` contract
1469
+ # `build_constant_type` relies on for union dedup. Decline the
1470
+ # fold and let the RBS tier answer with the widened class.
1471
+ return false if value.is_a?(Float) && value.nan?
1472
+ return false if value.is_a?(Complex) && complex_nan?(value)
1473
+
1474
+ true
1475
+ end
1476
+
1477
+ # True when either component of a Complex is NaN.
1478
+ def complex_nan?(value)
1479
+ real = value.real
1480
+ imag = value.imaginary
1481
+ (real.is_a?(Float) && real.nan?) || (imag.is_a?(Float) && imag.nan?)
1338
1482
  end
1339
1483
 
1340
1484
  def safe?(receiver_value, method_name, arg_value)
@@ -1355,6 +1499,7 @@ module Rigor
1355
1499
  when true, false then BOOL_BINARY
1356
1500
  when nil then NIL_BINARY
1357
1501
  when Rational then RATIONAL_BINARY
1502
+ when Complex then COMPLEX_BINARY
1358
1503
  else Set.new
1359
1504
  end
1360
1505
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../../type"
4
4
  require_relative "singleton_folding"
5
+ require_relative "member_shape_projection"
5
6
 
6
7
  module Rigor
7
8
  module Inference
@@ -30,6 +31,10 @@ module Rigor
30
31
  module DataFolding
31
32
  module_function
32
33
 
34
+ # The `[]` / `to_h` / `deconstruct` / `members` / `with` projections
35
+ # and the reader-redefinition guard are shared with {StructFolding}.
36
+ extend MemberShapeProjection
37
+
33
38
  # @return [Rigor::Type, nil] the folded result, or nil to defer.
34
39
  def try_dispatch(context)
35
40
  receiver = context.receiver
@@ -165,81 +170,12 @@ module Rigor
165
170
  when :deconstruct then instance_deconstruct(instance)
166
171
  when :deconstruct_keys then instance_deconstruct_keys(instance, args)
167
172
  when :members then instance_members(instance)
168
- when :with then instance_with(instance, args)
173
+ when :with
174
+ instance_with(instance, args) do |members, class_name|
175
+ Type::Combinator.data_instance_of(members: members, class_name: class_name)
176
+ end
169
177
  end
170
178
  end
171
-
172
- # A `Data.define` class body (the `class Point < Data.define(:x);
173
- # def x; …; end; end` subclass body, or a `Const = Data.define(:x) do
174
- # def x; …; end; end` block) can redefine a member's synthesised
175
- # reader. When it does, `inst.x` runs that `def`, NOT the member, so
176
- # folding the read to the member type would be unsound (a downstream
177
- # FP). Both named forms register the override as a real `def` node
178
- # under the class name, so an entry in the project def-node table is
179
- # the discriminator (the synthesised reader has no def node). The
180
- # value accessors `[]` / `to_h` / `deconstruct` bypass the reader and
181
- # stay foldable, so this gate is on the bare member read only.
182
- def reader_overridden?(instance, method_name, scope)
183
- class_name = instance.class_name
184
- return false if class_name.nil? || scope.nil?
185
-
186
- !scope.user_def_for(class_name, method_name).nil?
187
- end
188
-
189
- def instance_index(instance, args)
190
- return nil unless args.size == 1
191
-
192
- arg = args.first
193
- return nil unless arg.is_a?(Type::Constant)
194
-
195
- key = arg.value
196
- case key
197
- when Symbol
198
- instance.members[key]
199
- when Integer
200
- values = instance.members.values
201
- idx = key.negative? ? key + values.size : key
202
- values[idx] if idx && idx >= 0 && idx < values.size
203
- end
204
- end
205
-
206
- def instance_to_h(instance)
207
- Type::Combinator.hash_shape_of(instance.members.dup)
208
- end
209
-
210
- def instance_deconstruct(instance)
211
- Type::Combinator.tuple_of(*instance.members.values)
212
- end
213
-
214
- # `deconstruct_keys(nil)` / `deconstruct_keys([:x])` both yield a
215
- # subset of the member map; the conservative, always-correct answer
216
- # is the full closed member shape.
217
- def instance_deconstruct_keys(instance, args)
218
- return nil unless args.size <= 1
219
-
220
- Type::Combinator.hash_shape_of(instance.members.dup)
221
- end
222
-
223
- def instance_members(instance)
224
- Type::Combinator.tuple_of(*instance.member_names.map { |name| Type::Combinator.constant_of(name) })
225
- end
226
-
227
- # `Data#with(x: 9)` returns a new frozen copy with the named members
228
- # overridden. Only a closed keyword `HashShape` whose keys are a
229
- # subset of the members folds; anything else defers (RBS resolves
230
- # `with` to `self`, returning the unchanged instance type).
231
- def instance_with(instance, args)
232
- return instance if args.empty?
233
- return nil unless args.size == 1
234
-
235
- shape = args.first
236
- return nil unless shape.is_a?(Type::HashShape) && shape.closed?
237
- return nil unless shape.optional_keys.empty?
238
- return nil unless shape.pairs.keys.all? { |key| instance.members.key?(key) }
239
-
240
- merged = instance.members.merge(shape.pairs)
241
- Type::Combinator.data_instance_of(members: merged, class_name: instance.class_name)
242
- end
243
179
  end
244
180
  end
245
181
  end
@@ -54,11 +54,10 @@ module Rigor
54
54
  # *correctness-preservingly* proved" excludes Constants whose
55
55
  # value is host-specific.
56
56
  module FileFolding
57
- # File class methods that the analyzer can fold *when the
58
- # fold is platform-safe to perform*. Today every entry is
59
- # platform-sensitive (every one observes `File::SEPARATOR`
60
- # or `File::ALT_SEPARATOR`); the gate below requires the
61
- # opt-in flag for any of them to fire.
57
+ # File class methods the analyzer can fold when the opt-in
58
+ # flag is set. Currently identical to PLATFORM_DEPENDENT_METHODS
59
+ # — separated for a future non-platform-sensitive tier that
60
+ # can fold without the opt-in flag.
62
61
  FILE_PURE_CLASS_METHODS = Set[
63
62
  :basename,
64
63
  :dirname,
@@ -72,8 +71,8 @@ module Rigor
72
71
  # Methods whose result depends on host directory-separator
73
72
  # semantics (`/` on POSIX, `/` AND `\` on Windows, drive
74
73
  # letters, UNC paths). Folding these would bake the
75
- # analyzer-host's platform into the inferred type. The opt-
76
- # in flag below controls whether to do it anyway.
74
+ # analyzer-host's platform into the inferred type. The opt-in
75
+ # flag below controls whether to do it anyway.
77
76
  PLATFORM_DEPENDENT_METHODS = Set[
78
77
  :basename, :dirname, :extname, :join, :split, :absolute_path?
79
78
  ].freeze
@@ -175,24 +175,18 @@ module Rigor
175
175
  type.is_a?(Type::Constant) && type.value.is_a?(Symbol)
176
176
  end
177
177
 
178
- # Element-yielding Enumerable methods covered as a v0.0.5
179
- # placeholder. RBS already binds the block parameter
180
- # correctly for plain `Array[T]` / `Set[T]` / `Range[T]`
181
- # receivers via generic substitution; this tier exists so
182
- # Tuple- and HashShape-shaped receivers reach the block
183
- # body with the precise per-position element union /
184
- # `Tuple[K, V]` pair rather than the projected
178
+ # Element-yielding Enumerable methods covered as a placeholder.
179
+ # RBS already binds the block parameter correctly for plain
180
+ # `Array[T]` / `Set[T]` / `Range[T]` receivers via generic
181
+ # substitution; this tier exists so Tuple- and HashShape-shaped
182
+ # receivers reach the block body with the precise per-position
183
+ # element union / `Tuple[K, V]` pair rather than the projected
185
184
  # `Array[union]` / `Hash[K, V]` widening.
186
185
  #
187
- # NOTE (v0.0.5): the per-method coverage here (group_by,
188
- # partition, each_slice, each_cons) is intentionally
189
- # narrow. The longer-term direction is to move
190
- # Enumerable-aware projections into a plugin tier modelled
191
- # after PHPStan's extension API (ADR-2). The placeholders
192
- # below stay until the plugin surface is in place; once it
193
- # ships, this dispatcher loses these arms and the
194
- # equivalent rules move into a built-in plugin loaded at
195
- # boot.
186
+ # NOTE: `Plugin::NodeRuleWalk` (ADR-52 WD4) is now in place as
187
+ # the intended migration target for these Enumerable projections.
188
+ # The four methods (group_by, partition, each_slice, each_cons)
189
+ # remain here pending that migration.
196
190
  def single_element_block_params(receiver)
197
191
  element = element_type_of(receiver)
198
192
  return nil if element.nil?
@@ -43,14 +43,21 @@ module Rigor
43
43
  private_constant :NUMERIC_CONSTRUCTORS
44
44
 
45
45
  # `Kernel#Integer(s)` predicate-aware refinement set
46
- # (v0.1.1 Track 1 slice 2b). Both `decimal-int-string` and
47
- # `numeric-string` describe digit-only ASCII strings, so
48
- # `Integer(s)` is total over the carrier domain and the
49
- # result is `>= 0`. The default `base: 10` invocation
50
- # accepts the same shape `String#to_i` does for these
51
- # predicates; the `Integer(s, base)` overload is left for
52
- # a later slice.
53
- INTEGER_REFINEMENT_PREDICATES = Set[:decimal_int, :numeric].freeze
46
+ # (v0.1.1 Track 1 slice 2b). `decimal-int-string` is the
47
+ # only string refinement whose every inhabitant `Integer(s)`
48
+ # parses without remainder, so the result is a plain
49
+ # `Integer` but NOT `non-negative-int`: the predicate
50
+ # `/\A-?\d+\z/` admits a leading sign, so `"-7"` is a valid
51
+ # decimal-int-string and `Integer("-7") == -7 < 0`. The
52
+ # narrowing is total (every inhabitant parses) but not `>= 0`,
53
+ # so it lands on `universal_int`. `numeric-string` is
54
+ # deliberately NOT in this set at all: since it was widened to
55
+ # the full Ruby numeric-literal grammar (floats, hex, rational,
56
+ # imaginary, signs), `Integer(numeric_string)` would raise for
57
+ # a `"1.5"` / `"2i"` inhabitant — not even total — so it falls
58
+ # through to RBS `Integer`. The `Integer(s, base)` overload is
59
+ # left for a later slice.
60
+ INTEGER_REFINEMENT_PREDICATES = Set[:decimal_int].freeze
54
61
  private_constant :INTEGER_REFINEMENT_PREDICATES
55
62
 
56
63
  def try_dispatch(context)
@@ -70,7 +77,7 @@ module Rigor
70
77
  # paths, tried in order:
71
78
  #
72
79
  # 1. A `Refined[String, predicate]` argument whose predicate
73
- # is a digit-only carrier narrows to `non-negative-int`
80
+ # is a total-parse carrier narrows to `universal_int`
74
81
  # (see {try_integer_from_refinement}).
75
82
  # 2. A `Constant` String or Numeric argument — optionally
76
83
  # with a `Constant[Integer]` base — runs the actual
@@ -120,9 +127,14 @@ module Rigor
120
127
  # `Kernel#Integer(s)` over a `Refined[String, predicate]`
121
128
  # whose predicate is in {INTEGER_REFINEMENT_PREDICATES}.
122
129
  # Mirrors the `String#to_i` projection in `ShapeDispatch`
123
- # (v0.1.1 slice 2a) — the result is always
124
- # `non-negative-int`. Returns nil for any other arg shape
125
- # so the RBS tier handles the generic `Integer(arg)` case.
130
+ # (v0.1.1 slice 2a) — the result is `universal_int`, NOT
131
+ # `non-negative-int`: a decimal-int-string admits a leading
132
+ # sign (`"-7"`), so the parsed Integer can be negative. The
133
+ # carrier stays an `IntegerRange` (rather than declining to
134
+ # the RBS `Nominal[Integer]`) so downstream range narrowing
135
+ # still has a range to intersect. Returns nil for any other
136
+ # arg shape so the RBS tier handles the generic `Integer(arg)`
137
+ # case.
126
138
  def try_integer_from_refinement(args)
127
139
  return nil unless args.size == 1
128
140
 
@@ -133,7 +145,7 @@ module Rigor
133
145
  return nil unless base.is_a?(Type::Nominal) && base.class_name == "String"
134
146
  return nil unless INTEGER_REFINEMENT_PREDICATES.include?(arg.predicate_id)
135
147
 
136
- Type::Combinator.non_negative_int
148
+ Type::Combinator.universal_int
137
149
  end
138
150
 
139
151
  def try_array(args)
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../type"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module MethodDispatcher
8
+ # The member-shape projections shared by {DataFolding} and
9
+ # {StructFolding}. A `DataInstance` and a `StructInstance` expose the
10
+ # same surface — an ordered `members` map, `member_names`, and a
11
+ # `class_name` — so the value projections off that surface
12
+ # (`[]` / `to_h` / `deconstruct` / `deconstruct_keys` / `members` /
13
+ # `with`) and the reader-redefinition guard are identical between the
14
+ # two folders. Only `#with`'s carrier constructor differs
15
+ # (`data_instance_of` vs `struct_instance_of`), so it takes a block
16
+ # that builds the new instance from the merged member map.
17
+ #
18
+ # Both folders `extend` this module so the projections resolve as
19
+ # their own module functions (matching their `module_function` style).
20
+ module MemberShapeProjection
21
+ # A Data/Struct subclass body can redefine a member's synthesised
22
+ # reader (`def x`); when it does, `inst.x` runs that `def`, not the
23
+ # member, so folding the read would be unsound. A real `def` node
24
+ # under the class name is the discriminator (the synthesised reader
25
+ # has none), so an entry in the project def-node table gates the
26
+ # bare member read off.
27
+ def reader_overridden?(instance, method_name, scope)
28
+ class_name = instance.class_name
29
+ return false if class_name.nil? || scope.nil?
30
+
31
+ !scope.user_def_for(class_name, method_name).nil?
32
+ end
33
+
34
+ def instance_index(instance, args)
35
+ return nil unless args.size == 1
36
+
37
+ arg = args.first
38
+ return nil unless arg.is_a?(Type::Constant)
39
+
40
+ key = arg.value
41
+ case key
42
+ when Symbol
43
+ instance.members[key]
44
+ when Integer
45
+ values = instance.members.values
46
+ idx = key.negative? ? key + values.size : key
47
+ values[idx] if idx && idx >= 0 && idx < values.size
48
+ end
49
+ end
50
+
51
+ def instance_to_h(instance)
52
+ Type::Combinator.hash_shape_of(instance.members.dup)
53
+ end
54
+
55
+ def instance_deconstruct(instance)
56
+ Type::Combinator.tuple_of(*instance.members.values)
57
+ end
58
+
59
+ # `deconstruct_keys(nil)` / `deconstruct_keys([:x])` both yield a
60
+ # subset of the member map; the conservative, always-correct answer
61
+ # is the full closed member shape.
62
+ def instance_deconstruct_keys(instance, args)
63
+ return nil unless args.size <= 1
64
+
65
+ Type::Combinator.hash_shape_of(instance.members.dup)
66
+ end
67
+
68
+ def instance_members(instance)
69
+ Type::Combinator.tuple_of(*instance.member_names.map { |name| Type::Combinator.constant_of(name) })
70
+ end
71
+
72
+ # `#with(x: 9)` returns a new copy with the named members
73
+ # overridden. Only a closed keyword `HashShape` whose keys are a
74
+ # subset of the members folds; anything else defers (RBS resolves
75
+ # `with` to `self`, returning the unchanged instance type). The
76
+ # carrier constructor differs per folder, so the caller supplies it
77
+ # as a block taking the merged member map and the class name.
78
+ def instance_with(instance, args)
79
+ return instance if args.empty?
80
+ return nil unless args.size == 1
81
+
82
+ shape = args.first
83
+ return nil unless shape.is_a?(Type::HashShape) && shape.closed?
84
+ return nil unless shape.optional_keys.empty?
85
+ return nil unless shape.pairs.keys.all? { |key| instance.members.key?(key) }
86
+
87
+ merged = instance.members.merge(shape.pairs)
88
+ yield(merged, instance.class_name)
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -25,9 +25,7 @@ module Rigor
25
25
  # so without this preference, an alias-typed overload like
26
26
  # `Array#[](::int) -> Elem` would beat the strict
27
27
  # `Array#[](Range) -> Array[Elem]?` overload for a Range
28
- # argument. (Surfaced during v0.1.1 self-analysis; see the
29
- # "Interface-strictness on overload selection" item in
30
- # `docs/ROADMAP.md`.)
28
+ # argument.
31
29
  # 3. **Pass 2 — gradual fall-back.** If no fully strict overload
32
30
  # matches, accept the first arity-and-gradual-accept match
33
31
  # (the v0.1.1 behaviour). Alias / Interface / Intersection