rigortype 0.1.18 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +159 -224
  3. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -3
  4. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
  5. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +32 -23
  6. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
  7. data/lib/rigor/analysis/check_rules/rule_walk.rb +151 -23
  8. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
  9. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +9 -3
  10. data/lib/rigor/analysis/check_rules.rb +756 -132
  11. data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
  12. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
  13. data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
  14. data/lib/rigor/analysis/diagnostic.rb +8 -0
  15. data/lib/rigor/analysis/fact_store.rb +5 -4
  16. data/lib/rigor/analysis/rule_catalog.rb +153 -6
  17. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +19 -18
  18. data/lib/rigor/analysis/runner/project_pre_passes.rb +13 -9
  19. data/lib/rigor/analysis/runner.rb +75 -27
  20. data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
  21. data/lib/rigor/analysis/worker_session.rb +31 -25
  22. data/lib/rigor/bleeding_edge.rb +123 -0
  23. data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
  24. data/lib/rigor/cache/descriptor.rb +86 -8
  25. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  26. data/lib/rigor/cache/store.rb +5 -3
  27. data/lib/rigor/cli/annotate_command.rb +122 -16
  28. data/lib/rigor/cli/baseline_command.rb +4 -3
  29. data/lib/rigor/cli/check_command.rb +118 -16
  30. data/lib/rigor/cli/coverage_command.rb +148 -16
  31. data/lib/rigor/cli/coverage_scan.rb +57 -0
  32. data/lib/rigor/cli/explain_command.rb +2 -0
  33. data/lib/rigor/cli/lsp_command.rb +3 -7
  34. data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
  35. data/lib/rigor/cli/mutation_protection_report.rb +73 -0
  36. data/lib/rigor/cli/options.rb +9 -0
  37. data/lib/rigor/cli/plugins_command.rb +4 -5
  38. data/lib/rigor/cli/plugins_renderer.rb +0 -2
  39. data/lib/rigor/cli/protection_renderer.rb +63 -0
  40. data/lib/rigor/cli/protection_report.rb +68 -0
  41. data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
  42. data/lib/rigor/cli/sig_gen_command.rb +2 -1
  43. data/lib/rigor/cli/trace_command.rb +2 -1
  44. data/lib/rigor/cli/triage_command.rb +8 -4
  45. data/lib/rigor/cli/triage_renderer.rb +15 -1
  46. data/lib/rigor/cli/type_of_command.rb +1 -1
  47. data/lib/rigor/cli/type_scan_command.rb +2 -1
  48. data/lib/rigor/cli.rb +12 -3
  49. data/lib/rigor/configuration/dependencies.rb +2 -4
  50. data/lib/rigor/configuration/severity_profile.rb +13 -1
  51. data/lib/rigor/configuration.rb +100 -6
  52. data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
  53. data/lib/rigor/environment/class_registry.rb +4 -3
  54. data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
  55. data/lib/rigor/environment/lockfile_resolver.rb +1 -1
  56. data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
  57. data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
  58. data/lib/rigor/environment/rbs_loader.rb +74 -5
  59. data/lib/rigor/environment.rb +17 -7
  60. data/lib/rigor/flow_contribution/fact.rb +1 -1
  61. data/lib/rigor/flow_contribution.rb +3 -5
  62. data/lib/rigor/inference/acceptance.rb +17 -9
  63. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  64. data/lib/rigor/inference/body_fixpoint.rb +89 -0
  65. data/lib/rigor/inference/budget_trace.rb +29 -2
  66. data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
  67. data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
  68. data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
  69. data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
  70. data/lib/rigor/inference/expression_typer.rb +1072 -71
  71. data/lib/rigor/inference/hkt_body.rb +8 -11
  72. data/lib/rigor/inference/hkt_body_parser.rb +10 -12
  73. data/lib/rigor/inference/hkt_registry.rb +10 -11
  74. data/lib/rigor/inference/macro_block_self_type.rb +2 -2
  75. data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
  76. data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
  77. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +210 -35
  78. data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
  79. data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
  80. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
  81. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
  82. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
  83. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
  84. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
  85. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
  86. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
  87. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +237 -24
  88. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
  89. data/lib/rigor/inference/method_dispatcher.rb +112 -49
  90. data/lib/rigor/inference/method_parameter_binder.rb +56 -2
  91. data/lib/rigor/inference/multi_target_binder.rb +46 -3
  92. data/lib/rigor/inference/mutation_widening.rb +147 -11
  93. data/lib/rigor/inference/narrowing.rb +284 -53
  94. data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
  95. data/lib/rigor/inference/project_patched_methods.rb +4 -7
  96. data/lib/rigor/inference/project_patched_scanner.rb +2 -13
  97. data/lib/rigor/inference/protection_scanner.rb +86 -0
  98. data/lib/rigor/inference/scope_indexer.rb +821 -76
  99. data/lib/rigor/inference/statement_evaluator.rb +1179 -102
  100. data/lib/rigor/inference/struct_fold_safety.rb +181 -0
  101. data/lib/rigor/inference/synthetic_method.rb +7 -7
  102. data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
  103. data/lib/rigor/language_server/completion_provider.rb +6 -12
  104. data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
  105. data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
  106. data/lib/rigor/language_server/hover_provider.rb +2 -3
  107. data/lib/rigor/language_server/hover_renderer.rb +2 -11
  108. data/lib/rigor/language_server/server.rb +9 -17
  109. data/lib/rigor/language_server.rb +4 -5
  110. data/lib/rigor/plugin/base.rb +245 -87
  111. data/lib/rigor/plugin/macro/block_as_method.rb +25 -25
  112. data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
  113. data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
  114. data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
  115. data/lib/rigor/plugin/macro.rb +6 -8
  116. data/lib/rigor/plugin/manifest.rb +49 -90
  117. data/lib/rigor/plugin/node_rule_walk.rb +59 -14
  118. data/lib/rigor/plugin/registry.rb +18 -18
  119. data/lib/rigor/plugin/type_node_resolver.rb +6 -8
  120. data/lib/rigor/protection/mutation_scanner.rb +120 -0
  121. data/lib/rigor/protection/mutator.rb +246 -0
  122. data/lib/rigor/rbs_extended.rb +24 -36
  123. data/lib/rigor/reflection.rb +4 -7
  124. data/lib/rigor/scope/discovery_index.rb +16 -2
  125. data/lib/rigor/scope.rb +185 -16
  126. data/lib/rigor/sig_gen/generator.rb +8 -0
  127. data/lib/rigor/sig_gen/observed_call.rb +3 -3
  128. data/lib/rigor/sig_gen/writer.rb +40 -2
  129. data/lib/rigor/source/constant_path.rb +62 -0
  130. data/lib/rigor/source.rb +1 -0
  131. data/lib/rigor/triage/catalogue.rb +4 -19
  132. data/lib/rigor/triage.rb +69 -1
  133. data/lib/rigor/type/bound_method.rb +2 -11
  134. data/lib/rigor/type/combinator.rb +45 -3
  135. data/lib/rigor/type/constant.rb +2 -11
  136. data/lib/rigor/type/data_class.rb +2 -11
  137. data/lib/rigor/type/data_instance.rb +2 -11
  138. data/lib/rigor/type/hash_shape.rb +2 -11
  139. data/lib/rigor/type/integer_range.rb +2 -11
  140. data/lib/rigor/type/intersection.rb +2 -11
  141. data/lib/rigor/type/nominal.rb +2 -11
  142. data/lib/rigor/type/plain_lattice.rb +37 -0
  143. data/lib/rigor/type/refined.rb +72 -13
  144. data/lib/rigor/type/singleton.rb +2 -11
  145. data/lib/rigor/type/struct_class.rb +75 -0
  146. data/lib/rigor/type/struct_instance.rb +93 -0
  147. data/lib/rigor/type/tuple.rb +5 -15
  148. data/lib/rigor/type.rb +2 -0
  149. data/lib/rigor/version.rb +1 -1
  150. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  151. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
  152. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +16 -32
  153. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
  154. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
  155. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
  156. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +34 -100
  157. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
  158. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
  159. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
  160. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +26 -27
  161. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
  162. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +9 -8
  163. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
  164. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
  165. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
  166. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
  167. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
  168. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
  169. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
  170. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +18 -49
  171. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
  172. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
  173. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +4 -4
  174. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
  175. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
  176. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
  177. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
  178. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +22 -35
  179. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
  180. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
  181. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +16 -23
  182. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
  183. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -4
  184. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
  185. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  186. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +21 -27
  187. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
  188. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
  189. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
  190. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
  191. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
  192. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
  193. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
  194. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
  195. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
  196. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
  197. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +52 -40
  198. data/sig/rigor/analysis/fact_store.rbs +3 -0
  199. data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
  200. data/sig/rigor/plugin/base.rbs +5 -2
  201. data/sig/rigor/plugin/manifest.rbs +1 -2
  202. data/sig/rigor/scope.rbs +18 -1
  203. data/sig/rigor/type.rbs +37 -1
  204. data/sig/rigor.rbs +1 -1
  205. data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
  206. data/skills/rigor-plugin-author/SKILL.md +6 -4
  207. data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
  208. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
  209. metadata +25 -2
  210. data/lib/rigor/plugin/macro/external_file.rb +0 -143
@@ -80,6 +80,35 @@ module Rigor
80
80
  Constant.new(value)
81
81
  end
82
82
 
83
+ # Widens every value-pinned (`Constant`) constituent of `type` to its
84
+ # nominal base (`Constant[1]` -> `Nominal["Integer"]`), recursing
85
+ # through unions and leaving non-pinned constituents untouched.
86
+ # `Constant[nil]` is preserved (no nominal base of interest). Shared
87
+ # by the ADR-55 recursive-return fixpoint and the ADR-56 block /
88
+ # loop-body fixpoint (`BodyFixpoint`) to force convergence on the
89
+ # final permitted iteration.
90
+ def widen_value_pinned(type)
91
+ case type
92
+ when Constant
93
+ type.value.nil? ? type : nominal_of(type.value.class.name)
94
+ when Refined
95
+ # A refinement (`non-empty-string`) is a value-narrowed nominal;
96
+ # widen it to its base so an accumulator whose per-pass result is
97
+ # a bounded refinement converges on the final iteration rather
98
+ # than flooring to `Dynamic[top]` (ADR-56).
99
+ widen_value_pinned(type.base)
100
+ when IntegerRange
101
+ # `int<1, 6>` is likewise a value-narrowed `Integer` (it erases to
102
+ # `Integer` in RBS); widen it so a bounded-int accumulator
103
+ # converges (ADR-56).
104
+ nominal_of("Integer")
105
+ when Union
106
+ union(*type.members.map { |member| widen_value_pinned(member) })
107
+ else
108
+ type
109
+ end
110
+ end
111
+
83
112
  # `Object#method(:name)` carrier. Stores the bound
84
113
  # `(receiver, method_name)` pair so the dispatcher can
85
114
  # substitute the original dispatch at `.call` / `.()` /
@@ -376,6 +405,21 @@ module Rigor
376
405
  DataInstance.new(members, class_name)
377
406
  end
378
407
 
408
+ # ADR-48 Struct follow-up — the class object produced by
409
+ # `Struct.new(:x, :y)`. `members` is the ordered Symbol member-name
410
+ # list; `keyword_init` records the `keyword_init:` flag; `class_name`
411
+ # tags the class when known (the named-subclass form).
412
+ def struct_class_of(members:, class_name: nil, keyword_init: false)
413
+ StructClass.new(members, class_name, keyword_init: keyword_init)
414
+ end
415
+
416
+ # ADR-48 Struct follow-up — a `Struct.new` value instance. `members`
417
+ # is the ordered member-name -> value-type map; `class_name` tags the
418
+ # instance's class when known.
419
+ def struct_instance_of(members:, class_name: nil)
420
+ StructInstance.new(members, class_name)
421
+ end
422
+
379
423
  # Normalized union. Flattens nested Unions, deduplicates structurally
380
424
  # equal members, drops Bot, and collapses 0/1-member results.
381
425
  def union(*types)
@@ -900,9 +944,7 @@ module Rigor
900
944
  end
901
945
  end
902
946
 
903
- # ADR-15 Phase 4b.x eager-allocate the singleton
904
- # `Dynamic[Top]` carrier on the main Ractor at load time.
905
- # The `untyped` reader above just returns this ivar.
947
+ # Eager-allocated at load time; see `untyped` method comment above.
906
948
  @untyped = Dynamic.new(Top.instance)
907
949
  end
908
950
  end
@@ -3,6 +3,7 @@
3
3
  require "date"
4
4
  require_relative "../trinary"
5
5
  require_relative "acceptance_router"
6
+ require_relative "plain_lattice"
6
7
 
7
8
  module Rigor
8
9
  module Type
@@ -100,17 +101,7 @@ module Rigor
100
101
  end
101
102
  end
102
103
 
103
- def top
104
- Trinary.no
105
- end
106
-
107
- def bot
108
- Trinary.no
109
- end
110
-
111
- def dynamic
112
- Trinary.no
113
- end
104
+ include Rigor::Type::PlainLattice
114
105
 
115
106
  include Rigor::Type::AcceptanceRouter
116
107
 
@@ -3,6 +3,7 @@
3
3
  require_relative "../trinary"
4
4
  require_relative "../value_semantics"
5
5
  require_relative "acceptance_router"
6
+ require_relative "plain_lattice"
6
7
 
7
8
  module Rigor
8
9
  module Type
@@ -54,17 +55,7 @@ module Rigor
54
55
  "singleton(#{class_name || 'Data'})"
55
56
  end
56
57
 
57
- def top
58
- Trinary.no
59
- end
60
-
61
- def bot
62
- Trinary.no
63
- end
64
-
65
- def dynamic
66
- Trinary.no
67
- end
58
+ include Rigor::Type::PlainLattice
68
59
 
69
60
  include Rigor::Type::AcceptanceRouter
70
61
 
@@ -3,6 +3,7 @@
3
3
  require_relative "../trinary"
4
4
  require_relative "../value_semantics"
5
5
  require_relative "acceptance_router"
6
+ require_relative "plain_lattice"
6
7
 
7
8
  module Rigor
8
9
  module Type
@@ -74,17 +75,7 @@ module Rigor
74
75
  name
75
76
  end
76
77
 
77
- def top
78
- Trinary.no
79
- end
80
-
81
- def bot
82
- Trinary.no
83
- end
84
-
85
- def dynamic
86
- Trinary.no
87
- end
78
+ include Rigor::Type::PlainLattice
88
79
 
89
80
  include Rigor::Type::AcceptanceRouter
90
81
 
@@ -3,6 +3,7 @@
3
3
  require_relative "../trinary"
4
4
  require_relative "../value_semantics"
5
5
  require_relative "acceptance_router"
6
+ require_relative "plain_lattice"
6
7
 
7
8
  module Rigor
8
9
  module Type
@@ -96,17 +97,7 @@ module Rigor
96
97
  read_only_keys.include?(key)
97
98
  end
98
99
 
99
- def top
100
- Trinary.no
101
- end
102
-
103
- def bot
104
- Trinary.no
105
- end
106
-
107
- def dynamic
108
- Trinary.no
109
- end
100
+ include Rigor::Type::PlainLattice
110
101
 
111
102
  include Rigor::Type::AcceptanceRouter
112
103
 
@@ -3,6 +3,7 @@
3
3
  require_relative "../trinary"
4
4
  require_relative "../value_semantics"
5
5
  require_relative "acceptance_router"
6
+ require_relative "plain_lattice"
6
7
 
7
8
  module Rigor
8
9
  module Type
@@ -103,17 +104,7 @@ module Rigor
103
104
  "Integer"
104
105
  end
105
106
 
106
- def top
107
- Trinary.no
108
- end
109
-
110
- def bot
111
- Trinary.no
112
- end
113
-
114
- def dynamic
115
- Trinary.no
116
- end
107
+ include Rigor::Type::PlainLattice
117
108
 
118
109
  include Rigor::Type::AcceptanceRouter
119
110
 
@@ -3,6 +3,7 @@
3
3
  require_relative "../trinary"
4
4
  require_relative "../value_semantics"
5
5
  require_relative "acceptance_router"
6
+ require_relative "plain_lattice"
6
7
 
7
8
  module Rigor
8
9
  module Type
@@ -62,17 +63,7 @@ module Rigor
62
63
  members.first.erase_to_rbs
63
64
  end
64
65
 
65
- def top
66
- Trinary.no
67
- end
68
-
69
- def bot
70
- Trinary.no
71
- end
72
-
73
- def dynamic
74
- Trinary.no
75
- end
66
+ include Rigor::Type::PlainLattice
76
67
 
77
68
  include Rigor::Type::AcceptanceRouter
78
69
 
@@ -3,6 +3,7 @@
3
3
  require_relative "../trinary"
4
4
  require_relative "../value_semantics"
5
5
  require_relative "acceptance_router"
6
+ require_relative "plain_lattice"
6
7
 
7
8
  module Rigor
8
9
  module Type
@@ -52,17 +53,7 @@ module Rigor
52
53
  "#{class_name}[#{rendered}]"
53
54
  end
54
55
 
55
- def top
56
- Trinary.no
57
- end
58
-
59
- def bot
60
- Trinary.no
61
- end
62
-
63
- def dynamic
64
- Trinary.no
65
- end
56
+ include Rigor::Type::PlainLattice
66
57
 
67
58
  include Rigor::Type::AcceptanceRouter
68
59
 
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../trinary"
4
+
5
+ module Rigor
6
+ module Type
7
+ # Supplies the lattice-membership trio for the "plain" carriers — the
8
+ # concrete value types that are neither a lattice extreme (`Top` /
9
+ # `Bot` / `Dynamic`) nor a wrapper that computes membership from an
10
+ # inner type.
11
+ #
12
+ # Every such carrier answers `top` / `bot` / `dynamic` with the same
13
+ # `Trinary.no` ("this value is not that lattice point"), so the trio
14
+ # lived as a byte-identical copy in a dozen carriers. The extremes
15
+ # override the relevant member (`Top#top` / `Bot#bot` /
16
+ # `Dynamic#dynamic` answer `Trinary.yes`) and the delegators (`App`,
17
+ # `Difference`, `Refined`, `Union`) compute `dynamic` from their inner
18
+ # type(s); none of those include this module.
19
+ #
20
+ # Mirrors the existing {AcceptanceRouter} / `ValueSemantics` mixins —
21
+ # narrow trait sharing, never carrier inheritance (which the type-object
22
+ # contract forbids).
23
+ module PlainLattice
24
+ def top
25
+ Trinary.no
26
+ end
27
+
28
+ def bot
29
+ Trinary.no
30
+ end
31
+
32
+ def dynamic
33
+ Trinary.no
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "prism"
4
+
3
5
  require_relative "../trinary"
4
6
  require_relative "../value_semantics"
5
7
  require_relative "acceptance_router"
@@ -104,18 +106,31 @@ module Rigor
104
106
  # so callers can pass any `Constant#value` without a
105
107
  # type-prefilter.
106
108
  #
107
- # Plugin-contributed predicates land here once ADR-2 is
108
- # in flight; today the table is closed over the v0.0.4
109
- # built-in catalogue.
109
+ # Plugin-contributed predicates are not yet wired; today
110
+ # the table covers the built-in catalogue.
110
111
  #
111
112
  # Recogniser policy:
112
113
  #
113
- # - `:numeric` is deliberately conservative only decimal
114
- # integer and plain-decimal-fraction strings are
115
- # recognised, mirroring `imported-built-in-types.md`'s
116
- # "Rigor's numeric-string predicate" wording. Looser
117
- # forms (scientific, hex, rational) MAY join later
118
- # without breaking the registry contract.
114
+ # - `:numeric` recognises a string that is a *single Ruby
115
+ # numeric literal* exactly the syntax that, written in
116
+ # Ruby source, evaluates to an `Integer` / `Float` /
117
+ # `Rational` / `Complex`. The recogniser delegates to the
118
+ # real Ruby parser ({Refined.ruby_numeric_literal?} via
119
+ # Prism), so it tracks Ruby's grammar precisely: decimal /
120
+ # `0x` hex / `0o` (or leading-zero) octal / `0b` binary /
121
+ # `0d` decimal integers, underscore digit separators
122
+ # (`1_000`), decimal fractions and scientific floats
123
+ # (`1.5`, `1E-5`), and the `r` rational / `i` imaginary
124
+ # suffixes (`1r`, `2i`, `0xffr`). A single leading sign is
125
+ # folded into the literal (`-1`, `+1.5`), but a doubled
126
+ # sign (`--1`, `++1`) parses as a unary-operator chain — a
127
+ # `CallNode`, not a literal — and is rejected, as are
128
+ # multi-dot junk (`1.2.3`), partial literals (`0x`, `1_`),
129
+ # whitespace-padded strings, and — crucially — non-ASCII
130
+ # "digits" (full-width `1`, superscript `²`, other Unicode
131
+ # number characters): Ruby's lexer only accepts `[0-9]` in
132
+ # a numeric literal, so those are `CallNode`s too. The
133
+ # stricter base-N predicates below remain proper subsets.
119
134
  # - `:decimal_int` is "what `Integer(s, 10)` would parse
120
135
  # without remainder" — one or more decimal digits,
121
136
  # optional leading sign, no whitespace, no fractional
@@ -127,20 +142,64 @@ module Rigor
127
142
  # not octal-int-string. This matches the typical user
128
143
  # intent — a refinement marks a string that "looks like
129
144
  # octal", not "happens to be base-8 valid".
130
- NUMERIC_STRING_PATTERN = /\A-?\d+(?:\.\d+)?\z/
131
145
  DECIMAL_INT_STRING_PATTERN = /\A-?\d+\z/
132
146
  OCTAL_INT_STRING_PATTERN = /\A-?(?:0[oO][0-7]+|0[0-7]+)\z/
133
147
  HEX_INT_STRING_PATTERN = /\A-?0[xX][0-9a-fA-F]+\z/
134
- private_constant :NUMERIC_STRING_PATTERN, :DECIMAL_INT_STRING_PATTERN,
148
+ private_constant :DECIMAL_INT_STRING_PATTERN,
135
149
  :OCTAL_INT_STRING_PATTERN, :HEX_INT_STRING_PATTERN
136
150
 
151
+ # Prism node classes that represent a numeric literal. A
152
+ # string is a numeric-string exactly when the parser reduces
153
+ # the whole input to a single one of these (the leading sign
154
+ # is already folded into the literal by the parser).
155
+ NUMERIC_LITERAL_NODES = [
156
+ Prism::IntegerNode,
157
+ Prism::FloatNode,
158
+ Prism::RationalNode,
159
+ Prism::ImaginaryNode
160
+ ].freeze
161
+ private_constant :NUMERIC_LITERAL_NODES
162
+
163
+ # Cheap pre-filter applied before invoking the parser: every
164
+ # Ruby numeric literal starts with an ASCII digit, optionally
165
+ # preceded by exactly one sign. Strings that fail this never
166
+ # reach Prism (the common non-numeric case stays allocation-
167
+ # and parse-free).
168
+ NUMERIC_LITERAL_PREFIX = /\A[+-]?\d/
169
+ private_constant :NUMERIC_LITERAL_PREFIX
170
+
171
+ # @param value [Object] typically a `Constant#value`
172
+ # @return [Boolean] true when `value` is a String that is a
173
+ # single, complete Ruby numeric literal. Total over
174
+ # arbitrary input — never raises (Prism reports malformed
175
+ # input through `errors`, it does not throw).
176
+ def self.ruby_numeric_literal?(value)
177
+ return false unless value.is_a?(String)
178
+ return false if value.empty?
179
+ # A numeric literal carries no whitespace; reject any
180
+ # leading / trailing / interior space so the *whole* string
181
+ # must be the literal (Prism would otherwise accept a
182
+ # trailing-space `"1 "`).
183
+ return false if value.match?(/\s/)
184
+ return false unless value.match?(NUMERIC_LITERAL_PREFIX)
185
+
186
+ result = Prism.parse(value)
187
+ return false unless result.errors.empty?
188
+
189
+ body = result.value.statements&.body
190
+ return false unless body && body.size == 1
191
+
192
+ node = body.first
193
+ NUMERIC_LITERAL_NODES.any? { |klass| node.is_a?(klass) }
194
+ end
195
+
137
196
  PREDICATES = {
138
197
  lowercase: ->(v) { v.is_a?(String) && v == v.downcase },
139
198
  not_lowercase: ->(v) { v.is_a?(String) && v != v.downcase },
140
199
  uppercase: ->(v) { v.is_a?(String) && v == v.upcase },
141
200
  not_uppercase: ->(v) { v.is_a?(String) && v != v.upcase },
142
- numeric: ->(v) { v.is_a?(String) && NUMERIC_STRING_PATTERN.match?(v) },
143
- not_numeric: ->(v) { v.is_a?(String) && !NUMERIC_STRING_PATTERN.match?(v) },
201
+ numeric: ->(v) { ruby_numeric_literal?(v) },
202
+ not_numeric: ->(v) { v.is_a?(String) && !ruby_numeric_literal?(v) },
144
203
  decimal_int: ->(v) { v.is_a?(String) && DECIMAL_INT_STRING_PATTERN.match?(v) },
145
204
  octal_int: ->(v) { v.is_a?(String) && OCTAL_INT_STRING_PATTERN.match?(v) },
146
205
  hex_int: ->(v) { v.is_a?(String) && HEX_INT_STRING_PATTERN.match?(v) },
@@ -3,6 +3,7 @@
3
3
  require_relative "../trinary"
4
4
  require_relative "../value_semantics"
5
5
  require_relative "acceptance_router"
6
+ require_relative "plain_lattice"
6
7
 
7
8
  module Rigor
8
9
  module Type
@@ -34,17 +35,7 @@ module Rigor
34
35
  "singleton(#{class_name})"
35
36
  end
36
37
 
37
- def top
38
- Trinary.no
39
- end
40
-
41
- def bot
42
- Trinary.no
43
- end
44
-
45
- def dynamic
46
- Trinary.no
47
- end
38
+ include Rigor::Type::PlainLattice
48
39
 
49
40
  include Rigor::Type::AcceptanceRouter
50
41
 
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../trinary"
4
+ require_relative "../value_semantics"
5
+ require_relative "acceptance_router"
6
+ require_relative "plain_lattice"
7
+
8
+ module Rigor
9
+ module Type
10
+ # The class object produced by `Struct.new(:x, :y)` (ADR-48 Struct
11
+ # follow-up). The mutable sibling of {DataClass}: it models the *class*
12
+ # (the value bound to `Point` in `Point = Struct.new(:x, :y)`, or the
13
+ # anonymous superclass in `class Point < Struct.new(:x, :y)`), carrying
14
+ # the ordered member-name list so `Point.new(...)` can materialise a
15
+ # {StructInstance}.
16
+ #
17
+ # `keyword_init` records the `Struct.new(..., keyword_init: true)` flag
18
+ # so `.new` only materialises a precise instance for the matching call
19
+ # form — a positional `.new(1, 2)` on a `keyword_init: true` struct, or
20
+ # a keyword `.new(x: 1)` on a positional struct, is a different runtime
21
+ # shape and must degrade rather than fold a wrong member map.
22
+ #
23
+ # `class_name` carries the binding name when known (the named-subclass
24
+ # form) and is `nil` for the anonymous result of a bare `Struct.new(...)`
25
+ # before it is assigned to a constant.
26
+ #
27
+ # Equality and hashing are structural over the member list, the class
28
+ # name, and the keyword-init flag.
29
+ #
30
+ # See docs/adr/48-data-struct-value-folding.md § "Struct follow-up".
31
+ class StructClass
32
+ attr_reader :members, :class_name, :keyword_init
33
+
34
+ # @param members [Array<Symbol>] ordered member names.
35
+ # @param class_name [String, nil] the bound class name, or nil for
36
+ # the anonymous `Struct.new(...)` result.
37
+ # @param keyword_init [Boolean] the `keyword_init:` flag.
38
+ def initialize(members, class_name = nil, keyword_init: false)
39
+ unless members.is_a?(Array) && members.all?(Symbol)
40
+ raise ArgumentError, "members must be an Array of Symbols, got #{members.inspect}"
41
+ end
42
+ unless class_name.nil? || (class_name.is_a?(String) && !class_name.empty?)
43
+ raise ArgumentError, "class_name must be a non-empty String or nil, got #{class_name.inspect}"
44
+ end
45
+
46
+ @members = members.dup.freeze
47
+ @class_name = class_name&.freeze
48
+ @keyword_init = keyword_init ? true : false
49
+ freeze
50
+ end
51
+
52
+ def describe(_verbosity = :short)
53
+ return "singleton(#{class_name})" if class_name
54
+
55
+ "Struct.new(#{members.map(&:inspect).join(', ')})"
56
+ end
57
+
58
+ def erase_to_rbs
59
+ "singleton(#{class_name || 'Struct'})"
60
+ end
61
+
62
+ include Rigor::Type::PlainLattice
63
+
64
+ include Rigor::Type::AcceptanceRouter
65
+
66
+ include Rigor::ValueSemantics
67
+
68
+ value_fields :members, :class_name, :keyword_init
69
+
70
+ def inspect
71
+ "#<Rigor::Type::StructClass #{describe(:short)}>"
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../trinary"
4
+ require_relative "../value_semantics"
5
+ require_relative "acceptance_router"
6
+ require_relative "plain_lattice"
7
+
8
+ module Rigor
9
+ module Type
10
+ # A `Struct.new` value instance (ADR-48 Struct follow-up) —
11
+ # `Point.new(1, 2)`. The mutable sibling of {DataInstance}: a closed,
12
+ # total, class-tagged member map (member name -> value type),
13
+ # HashShape-shaped but nominal.
14
+ #
15
+ # Unlike {DataInstance}, a `Struct` instance is **mutable** — `s.x = v`,
16
+ # `s[:x] = v`, and escape can invalidate the member map. The folding
17
+ # tier therefore only projects member reads off a **fresh** instance
18
+ # (the transient receiver of a `.new(...).x` / `.with(...).x` chain,
19
+ # which provably cannot have been mutated between materialisation and
20
+ # the read); a read off a *stored* binding degrades to `Dynamic[top]`
21
+ # rather than fold a possibly-stale member value. Promoting the
22
+ # fold to mutation-free bound locals is the deferred slice 3 (see ADR).
23
+ #
24
+ # That mutability-gating lives in the dispatch tier (`StructFolding`),
25
+ # not the carrier: the carrier itself just records the member map. Like
26
+ # {DataInstance}, non-folded methods project to the `Struct` nominal (or
27
+ # the tagged class) through {RbsDispatch}'s `receiver_descriptor`, so
28
+ # non-member calls resolve without mis-firing undefined-method.
29
+ #
30
+ # Equality and hashing are structural over the (member -> type) map and
31
+ # the class name.
32
+ #
33
+ # See docs/adr/48-data-struct-value-folding.md § "Struct follow-up".
34
+ class StructInstance
35
+ attr_reader :members, :class_name
36
+
37
+ # @param members [Hash{Symbol => Rigor::Type}] ordered member -> type
38
+ # map. Every declared member is present (Struct instances are total).
39
+ # @param class_name [String, nil] the tagging class name, or nil for
40
+ # an instance of an anonymous `Struct.new(...)` class.
41
+ def initialize(members, class_name = nil)
42
+ unless members.is_a?(Hash) && members.each_key.all?(Symbol)
43
+ raise ArgumentError, "members must be a Hash with Symbol keys, got #{members.inspect}"
44
+ end
45
+ unless class_name.nil? || (class_name.is_a?(String) && !class_name.empty?)
46
+ raise ArgumentError, "class_name must be a non-empty String or nil, got #{class_name.inspect}"
47
+ end
48
+
49
+ @members = members.dup.freeze
50
+ @class_name = class_name&.freeze
51
+ freeze
52
+ end
53
+
54
+ # @return [Array<Symbol>] ordered member names.
55
+ def member_names
56
+ members.keys
57
+ end
58
+
59
+ # @return [Rigor::Type, nil] the member's value type, or nil when the
60
+ # name is not a declared member.
61
+ def member_type(name)
62
+ members[name]
63
+ end
64
+
65
+ def describe(verbosity = :short)
66
+ rendered = members.map { |name, type| "#{name}: #{type.describe(verbosity)}" }
67
+ "#{class_name || 'Struct'}(#{rendered.join(', ')})"
68
+ end
69
+
70
+ # Erases to the tagging class nominal (conservative: the structural
71
+ # members are not RBS-expressible as a class instance). The
72
+ # anonymous case erases to the `Struct` supertype.
73
+ def erase_to_rbs
74
+ name = class_name
75
+ return "Struct" if name.nil?
76
+
77
+ name
78
+ end
79
+
80
+ include Rigor::Type::PlainLattice
81
+
82
+ include Rigor::Type::AcceptanceRouter
83
+
84
+ include Rigor::ValueSemantics
85
+
86
+ value_fields :members, :class_name
87
+
88
+ def inspect
89
+ "#<Rigor::Type::StructInstance #{describe(:short)}>"
90
+ end
91
+ end
92
+ end
93
+ end
@@ -3,6 +3,7 @@
3
3
  require_relative "../trinary"
4
4
  require_relative "../value_semantics"
5
5
  require_relative "acceptance_router"
6
+ require_relative "plain_lattice"
6
7
 
7
8
  module Rigor
8
9
  module Type
@@ -17,10 +18,9 @@ module Rigor
17
18
  #
18
19
  # Slice 5 phase 1 introduces the carrier and surfaces it from the
19
20
  # `ArrayNode` literal handler when every element is a non-splat
20
- # value. Tuple-aware refinements for `tuple[0]`, `tuple.first`, and
21
- # destructuring assignment are deferred to Slice 5 phase 2; they
22
- # will run as a higher-priority dispatch tier above
23
- # {Rigor::Inference::MethodDispatcher::RbsDispatch}.
21
+ # value. Tuple-aware refinements (`tuple[0]`, `tuple.first`,
22
+ # destructuring) are implemented in `ShapeDispatch`, which runs
23
+ # above {Rigor::Inference::MethodDispatcher::RbsDispatch}.
24
24
  #
25
25
  # Equality and hashing are structural across an ordered, frozen
26
26
  # element list. The empty Tuple `Tuple[]` is permitted; the array
@@ -53,17 +53,7 @@ module Rigor
53
53
  "[#{elements.map(&:erase_to_rbs).join(', ')}]"
54
54
  end
55
55
 
56
- def top
57
- Trinary.no
58
- end
59
-
60
- def bot
61
- Trinary.no
62
- end
63
-
64
- def dynamic
65
- Trinary.no
66
- end
56
+ include Rigor::Type::PlainLattice
67
57
 
68
58
  include Rigor::Type::AcceptanceRouter
69
59
 
data/lib/rigor/type.rb CHANGED
@@ -21,6 +21,8 @@ require_relative "type/tuple"
21
21
  require_relative "type/hash_shape"
22
22
  require_relative "type/data_class"
23
23
  require_relative "type/data_instance"
24
+ require_relative "type/struct_class"
25
+ require_relative "type/struct_instance"
24
26
  require_relative "type/union"
25
27
  require_relative "type/difference"
26
28
  require_relative "type/refined"
data/lib/rigor/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rigor
4
- VERSION = "0.1.18"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -26,7 +26,7 @@ module Rigor
26
26
  # `stream_for` call) so the analyzer knows it can't
27
27
  # be sure of every stream name.
28
28
  #
29
- # Limitations (intentional for v0.1.0):
29
+ # Intentional limitations:
30
30
  #
31
31
  # - Direct-superclass match only.
32
32
  # - Public-vs-private is not tracked; the framework