rigortype 0.1.19 → 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 (166) hide show
  1. checksums.yaml +4 -4
  2. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -23
  3. data/lib/rigor/analysis/check_rules/rule_walk.rb +3 -21
  4. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
  5. data/lib/rigor/analysis/check_rules.rb +492 -71
  6. data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
  7. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
  8. data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
  9. data/lib/rigor/analysis/fact_store.rb +5 -4
  10. data/lib/rigor/analysis/rule_catalog.rb +153 -6
  11. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +17 -17
  12. data/lib/rigor/analysis/runner/project_pre_passes.rb +9 -8
  13. data/lib/rigor/analysis/runner.rb +17 -6
  14. data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
  15. data/lib/rigor/analysis/worker_session.rb +10 -14
  16. data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
  17. data/lib/rigor/cache/store.rb +5 -3
  18. data/lib/rigor/cli/annotate_command.rb +28 -7
  19. data/lib/rigor/cli/baseline_command.rb +4 -3
  20. data/lib/rigor/cli/check_command.rb +115 -16
  21. data/lib/rigor/cli/coverage_command.rb +148 -16
  22. data/lib/rigor/cli/coverage_scan.rb +57 -0
  23. data/lib/rigor/cli/explain_command.rb +2 -0
  24. data/lib/rigor/cli/lsp_command.rb +3 -7
  25. data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
  26. data/lib/rigor/cli/mutation_protection_report.rb +73 -0
  27. data/lib/rigor/cli/options.rb +9 -0
  28. data/lib/rigor/cli/plugins_command.rb +2 -1
  29. data/lib/rigor/cli/protection_renderer.rb +63 -0
  30. data/lib/rigor/cli/protection_report.rb +68 -0
  31. data/lib/rigor/cli/sig_gen_command.rb +2 -1
  32. data/lib/rigor/cli/trace_command.rb +2 -1
  33. data/lib/rigor/cli/triage_command.rb +2 -1
  34. data/lib/rigor/cli/type_of_command.rb +1 -1
  35. data/lib/rigor/cli/type_scan_command.rb +2 -1
  36. data/lib/rigor/cli.rb +3 -2
  37. data/lib/rigor/configuration/dependencies.rb +2 -4
  38. data/lib/rigor/configuration.rb +45 -7
  39. data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
  40. data/lib/rigor/environment/class_registry.rb +4 -3
  41. data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
  42. data/lib/rigor/environment/lockfile_resolver.rb +1 -1
  43. data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
  44. data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
  45. data/lib/rigor/environment/rbs_loader.rb +49 -5
  46. data/lib/rigor/environment.rb +17 -7
  47. data/lib/rigor/flow_contribution/fact.rb +1 -1
  48. data/lib/rigor/flow_contribution.rb +3 -5
  49. data/lib/rigor/inference/acceptance.rb +17 -9
  50. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  51. data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
  52. data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
  53. data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
  54. data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
  55. data/lib/rigor/inference/expression_typer.rb +20 -28
  56. data/lib/rigor/inference/hkt_body.rb +8 -11
  57. data/lib/rigor/inference/hkt_body_parser.rb +10 -12
  58. data/lib/rigor/inference/hkt_registry.rb +10 -11
  59. data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
  60. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +156 -21
  61. data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
  62. data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
  63. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
  64. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
  65. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
  66. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
  67. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
  68. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +90 -15
  69. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
  70. data/lib/rigor/inference/method_dispatcher.rb +40 -48
  71. data/lib/rigor/inference/mutation_widening.rb +5 -11
  72. data/lib/rigor/inference/narrowing.rb +14 -16
  73. data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
  74. data/lib/rigor/inference/project_patched_methods.rb +4 -7
  75. data/lib/rigor/inference/project_patched_scanner.rb +2 -13
  76. data/lib/rigor/inference/protection_scanner.rb +86 -0
  77. data/lib/rigor/inference/scope_indexer.rb +129 -55
  78. data/lib/rigor/inference/statement_evaluator.rb +244 -114
  79. data/lib/rigor/inference/struct_fold_safety.rb +181 -0
  80. data/lib/rigor/inference/synthetic_method.rb +7 -7
  81. data/lib/rigor/language_server/completion_provider.rb +6 -12
  82. data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
  83. data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
  84. data/lib/rigor/language_server/hover_provider.rb +2 -3
  85. data/lib/rigor/language_server/hover_renderer.rb +2 -11
  86. data/lib/rigor/language_server/server.rb +9 -17
  87. data/lib/rigor/language_server.rb +4 -5
  88. data/lib/rigor/plugin/base.rb +10 -8
  89. data/lib/rigor/plugin/macro/block_as_method.rb +3 -4
  90. data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
  91. data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
  92. data/lib/rigor/plugin/macro.rb +4 -5
  93. data/lib/rigor/plugin/manifest.rb +45 -66
  94. data/lib/rigor/plugin/registry.rb +6 -7
  95. data/lib/rigor/plugin/type_node_resolver.rb +6 -8
  96. data/lib/rigor/protection/mutation_scanner.rb +120 -0
  97. data/lib/rigor/protection/mutator.rb +246 -0
  98. data/lib/rigor/rbs_extended.rb +24 -36
  99. data/lib/rigor/reflection.rb +4 -7
  100. data/lib/rigor/scope/discovery_index.rb +14 -2
  101. data/lib/rigor/scope.rb +54 -11
  102. data/lib/rigor/sig_gen/observed_call.rb +3 -3
  103. data/lib/rigor/sig_gen/writer.rb +40 -2
  104. data/lib/rigor/source/constant_path.rb +62 -0
  105. data/lib/rigor/source.rb +1 -0
  106. data/lib/rigor/type/bound_method.rb +2 -11
  107. data/lib/rigor/type/combinator.rb +16 -3
  108. data/lib/rigor/type/constant.rb +2 -11
  109. data/lib/rigor/type/data_class.rb +2 -11
  110. data/lib/rigor/type/data_instance.rb +2 -11
  111. data/lib/rigor/type/hash_shape.rb +2 -11
  112. data/lib/rigor/type/integer_range.rb +2 -11
  113. data/lib/rigor/type/intersection.rb +2 -11
  114. data/lib/rigor/type/nominal.rb +2 -11
  115. data/lib/rigor/type/plain_lattice.rb +37 -0
  116. data/lib/rigor/type/refined.rb +72 -13
  117. data/lib/rigor/type/singleton.rb +2 -11
  118. data/lib/rigor/type/struct_class.rb +75 -0
  119. data/lib/rigor/type/struct_instance.rb +93 -0
  120. data/lib/rigor/type/tuple.rb +5 -15
  121. data/lib/rigor/type.rb +2 -0
  122. data/lib/rigor/version.rb +1 -1
  123. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  124. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
  125. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +3 -3
  126. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
  127. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
  128. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +7 -10
  129. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
  130. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
  131. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +6 -8
  132. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
  133. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -2
  134. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
  135. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
  136. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
  137. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
  138. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
  139. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
  140. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
  141. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +7 -9
  142. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
  143. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
  144. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +3 -3
  145. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
  146. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
  147. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
  148. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +1 -1
  149. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
  150. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
  151. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +5 -5
  152. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -4
  153. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
  154. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  155. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +19 -14
  156. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
  157. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
  158. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
  159. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
  160. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
  161. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
  162. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
  163. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +28 -41
  164. data/sig/rigor/scope.rbs +9 -1
  165. data/sig/rigor/type.rbs +36 -1
  166. metadata +19 -1
@@ -11,8 +11,8 @@ module Rigor
11
11
  # catalog is NOT routed through
12
12
  # `MethodDispatcher::ConstantFolding::CATALOG_BY_CLASS`
13
13
  # (which dispatches on the receiver's concrete class).
14
- # The data is consumed by future include-aware lookup —
15
- # see `docs/CURRENT_WORK.md` for the planned slice.
14
+ # The data is wired into `MODULE_CATALOGS` in
15
+ # `MethodDispatcher::ConstantFolding` (ancestor-chain lookup).
16
16
  ENUMERABLE_CATALOG = MethodCatalog.for_topic(
17
17
  "enumerable",
18
18
  mutating_selectors: {
@@ -27,6 +27,24 @@ module Rigor
27
27
  FOLDABLE_PURITIES = Set["leaf", "trivial", "leaf_when_numeric"].freeze
28
28
  EMPTY_CATALOG = { "classes" => {} }.freeze
29
29
 
30
+ # Selectors that are classified `:leaf` by the C-body analysis
31
+ # (they read no global mutable state in the C sense) but whose
32
+ # result is NOT reproducible across Ruby processes, so they must
33
+ # never be folded into a `Constant`:
34
+ #
35
+ # - `hash` — every core `#hash` (`String`/`Symbol`/`Integer`/
36
+ # `Float`/…) is salted with a per-process SipHash seed, so
37
+ # `"x".hash` differs in every process. Folding bakes one
38
+ # process's value into the type and the on-disk cache.
39
+ # - `object_id` / `__id__` — identity-allocated per process.
40
+ #
41
+ # This is a UNIVERSAL block (across every catalogued class)
42
+ # because `hash` / `object_id` are `Object`-level and present on
43
+ # every receiver; a per-class blocklist would silently miss a
44
+ # class. The deterministic siblings (`inspect`, `to_s`) are
45
+ # unaffected.
46
+ NON_REPRODUCIBLE_SELECTORS = Set[:hash, :object_id, :__id__].freeze
47
+
30
48
  # Shared root for the offline-generated catalogues. Resolving it
31
49
  # here keeps the repo-relative `../../../../` hop in one place
32
50
  # instead of copying it into every per-topic loader.
@@ -59,6 +77,7 @@ module Rigor
59
77
 
60
78
  def safe_for_folding?(class_name, selector, kind: :instance)
61
79
  class_name_str = class_name.to_s
80
+ return false if NON_REPRODUCIBLE_SELECTORS.include?(selector.to_sym)
62
81
  return false if blocked?(class_name_str, selector)
63
82
 
64
83
  entry = method_entry(class_name_str, selector, kind: kind)
@@ -22,7 +22,15 @@ module Rigor
22
22
  :replace, :initialize, :initialize_copy, :clear, :<<, :concat, :insert,
23
23
  :prepend, :force_encoding, :encode, :scrub, :unicode_normalize, :"[]=",
24
24
  :upto, :each_byte, :each_char, :each_codepoint,
25
- :each_grapheme_cluster, :each_line, :bytesplice
25
+ :each_grapheme_cluster, :each_line, :bytesplice,
26
+ # `crypt` is not a mutator but is blocked from folding for the
27
+ # same "do not bake a non-pure result into a Constant" reason:
28
+ # `rb_str_crypt` delegates to the platform `crypt(3)`, whose
29
+ # output (algorithm and digest) varies by libc / OS, so
30
+ # `"x".crypt("ab")` is not deterministic across the platforms
31
+ # an analyzed project may target. The catalog classifies it
32
+ # `:leaf` from its C body; this entry overrides that.
33
+ :crypt
26
34
  ],
27
35
  "Symbol" => Set[
28
36
  # Symbol is immutable in Ruby; the classifier mis-flags
@@ -4,6 +4,7 @@ require "prism"
4
4
 
5
5
  require_relative "../type"
6
6
  require_relative "../ast"
7
+ require_relative "../source/constant_path"
7
8
  require_relative "../analysis/self_call_resolution_recorder"
8
9
  require_relative "block_parameter_binder"
9
10
  require_relative "body_fixpoint"
@@ -14,6 +15,7 @@ require_relative "indexed_narrowing"
14
15
  require_relative "macro_block_self_type"
15
16
  require_relative "method_dispatcher"
16
17
  require_relative "narrowing"
18
+ require_relative "struct_fold_safety"
17
19
 
18
20
  module Rigor
19
21
  module Inference
@@ -323,12 +325,7 @@ module Rigor
323
325
  dynamic_top
324
326
  end
325
327
 
326
- # Recognised value-bearing position the Slice 2 engine does not yet
327
- # narrow: self, instance/class/global variable reads, block bodies.
328
- # Slice 3+ refines these in place; for now we acknowledge the node
329
- # class so the coverage scanner stops flagging it without recording
330
- # a fail-soft event for every occurrence.
331
- # Slice A-engine. `Prism::SelfNode` resolves to the scope's
328
+ # `Prism::SelfNode` resolves to the scope's
332
329
  # `self_type` when one has been injected (by
333
330
  # `StatementEvaluator` at class-body and method-body
334
331
  # boundaries) or `Dynamic[Top]` at the top level. Class-body
@@ -412,7 +409,7 @@ module Rigor
412
409
  end
413
410
 
414
411
  def type_of_constant_path(node)
415
- full_name = build_constant_path_name(node)
412
+ full_name = Source::ConstantPath.qualified_name_or_nil(node)
416
413
  return fallback_for(node, family: :prism) if full_name.nil?
417
414
 
418
415
  resolve_constant_name(full_name) || fallback_for(node, family: :prism)
@@ -481,24 +478,6 @@ module Rigor
481
478
  end
482
479
  end
483
480
 
484
- # Builds the dotted-colon name for a `Foo`, `Foo::Bar`, or `::Foo`
485
- # path. Returns nil when an inner segment is not itself a constant
486
- # reference (for example `expr::Foo`), so the caller can fall back.
487
- def build_constant_path_name(node)
488
- case node
489
- when Prism::ConstantReadNode
490
- node.name.to_s
491
- when Prism::ConstantPathNode
492
- parent = node.parent
493
- return node.name.to_s if parent.nil?
494
-
495
- parent_name = build_constant_path_name(parent)
496
- return nil if parent_name.nil?
497
-
498
- "#{parent_name}::#{node.name}"
499
- end
500
- end
501
-
502
481
  # Slice 5 phase 1 upgrades hash literals to `HashShape{...}`
503
482
  # when every entry is a static `AssocNode` whose key is a
504
483
  # `SymbolNode` or `StringNode` with a known value (covering the
@@ -837,7 +816,7 @@ module Rigor
837
816
  # Other pattern shapes (Range, Regexp, custom `===`) stay
838
817
  # `:maybe` — the existing union fallback handles them.
839
818
  def case_when_pattern_certainty(subject_type, pattern_node)
840
- class_name = build_constant_path_name(pattern_node)
819
+ class_name = Source::ConstantPath.qualified_name_or_nil(pattern_node)
841
820
  return Narrowing.class_pattern_certainty(subject_type, class_name, environment: scope.environment) if class_name
842
821
 
843
822
  literal = literal_pattern_value(pattern_node)
@@ -925,7 +904,8 @@ module Rigor
925
904
  end
926
905
 
927
906
  # `while` and `until` loops produce nil unless interrupted by
928
- # `break VALUE`, which Slice 3 phase 1 does not yet model.
907
+ # `break VALUE`; the expression value of `break VALUE` is not yet
908
+ # modeled (scope break-path propagation landed in `eval_loop`).
929
909
  # Returning Constant[nil] is safe and matches Ruby semantics for
930
910
  # the common case.
931
911
  def type_of_loop(_node)
@@ -2382,7 +2362,19 @@ module Rigor
2382
2362
  environment: scope.environment,
2383
2363
  locals: locals.freeze,
2384
2364
  self_type: receiver,
2385
- discovery: scope.discovery
2365
+ discovery: scope.discovery,
2366
+ struct_fold_safe_locals: struct_fold_safe_locals_for(def_node.body)
2367
+ )
2368
+ end
2369
+
2370
+ # ADR-48 Struct slice 3 — the fold-safe-local set for a method body
2371
+ # (runs only on a return-memo miss, so the per-call cost is bounded —
2372
+ # measured perf-neutral). Struct member layouts of constant receivers
2373
+ # are resolved through the discovery side-table the body scope inherits.
2374
+ def struct_fold_safe_locals_for(body)
2375
+ StructFoldSafety.fold_safe_locals(
2376
+ body,
2377
+ ->(name) { scope.struct_member_layout(name)&.[](:members) }
2386
2378
  )
2387
2379
  end
2388
2380
 
@@ -7,18 +7,15 @@ module Rigor
7
7
  # piece of a Rigor-side type expression that the reducer
8
8
  # ({HktReducer}) walks against a concrete argument list.
9
9
  #
10
- # Slice 2a ships a programmatic constructor surface only:
11
- # plugin and Rigor-bundled overlay authors build a body
12
- # tree by hand using these node types. The string-grammar
13
- # parser that reads `Definition#body` (the raw String slot
14
- # already populated by Slice 1's `HktDirectives.parse_define`)
15
- # into a tree is Slice 2b's deliverable; until it ships, the
16
- # `body` String stays opaque and `body_tree` is the
17
- # evaluable form.
10
+ # Slice 2a ships a programmatic constructor surface; plugin
11
+ # and Rigor-bundled overlay authors may build a body tree
12
+ # by hand using these node types. The string-grammar parser
13
+ # (`HktBodyParser`, Slice 2b, shipped) reads `Definition#body`
14
+ # (populated by Slice 1's `HktDirectives.parse_define`) into
15
+ # this node tree; `body_tree` is the evaluable form.
18
16
  #
19
- # The five node types cover the JSON.parse and dry-monads
20
- # use cases ADR-20 § Implementation slicing names as
21
- # near-term adopters:
17
+ # The nine node types cover JSON.parse, dry-monads, and the
18
+ # ADR-20 § D3 conditional / membership forms (shipped):
22
19
  #
23
20
  # - {TypeLeaf} — wraps a fully-built `Rigor::Type`
24
21
  # (use for atoms like `nil`, `Constant<true>`,
@@ -11,29 +11,27 @@ module Rigor
11
11
  # `%a{rigor:v1:hkt_define}` payloads) into the `HktBody`
12
12
  # node tree the Slice 2a reducer evaluates against.
13
13
  #
14
- # The minimum-viable grammar covered here is the
15
- # union-of-atoms-and-parameterised-forms subset of ADR-20
16
- # § D3 sufficient for `JSON.parse`'s `json::value`
17
- # recursive sum and for any other recursive-data-shape
18
- # signatures (Lisp value trees, dry-types refinements
19
- # without conditionals). The conditional / indexed-access
20
- # forms (`E <: T ? A : B`, `E in [k1, k2]`) drafted in D3
21
- # remain a follow-up slice — bodies that contain `?`
22
- # raise `ParseError` and the calling directive parser
23
- # drops the body_tree (the body String remains stored and
24
- # the reducer falls back to `app.bound`).
14
+ # The grammar implements the full ADR-20 § D3 subset:
15
+ # union-of-atoms/parameterised-forms for `JSON.parse`'s
16
+ # `json::value` recursive sum, plus the conditional and
17
+ # membership forms shipped in subsequent slices. Indexed-access
18
+ # forms remain deferred (no concrete demand yet).
25
19
  #
26
- # ## Grammar (slice 2b)
20
+ # ## Grammar
27
21
  #
28
22
  # body := union
29
23
  # union := type_expr ("|" type_expr)*
30
24
  # type_expr := atom | nominal_app | app_ref | param
25
+ # | conditional
31
26
  # atom := "nil" | "true" | "false" | "bool" | "untyped"
32
27
  # param := UCNAME (when UCNAME ∈ params)
33
28
  # nominal_app := class_name ("[" type_expr ("," type_expr)* "]")?
34
29
  # class_name := "::"? UCNAME ("::" UCNAME)*
35
30
  # app_ref := "App" "[" uri "," type_expr ("," type_expr)* "]"
36
31
  # uri := IDENT ("::" IDENT)+
32
+ # conditional := "(" test "?" union ":" union ")"
33
+ # test := type_expr ("<:" | "==") type_expr
34
+ # | type_expr "in" "[" type_expr ("," type_expr)* "]"
37
35
  # UCNAME := /[A-Z]\w*/
38
36
  # IDENT := /[a-z_]\w*/
39
37
  #
@@ -10,12 +10,12 @@ module Rigor
10
10
  # `%a{rigor:v1:hkt_define: ...}` annotations in shipped
11
11
  # `.rbs` files.
12
12
  #
13
- # Slice 1 keeps the registry **opaque**: it stores the
14
- # registration metadata (arity, variance, bound) and the
15
- # un-evaluated definition body (a raw String Slice 2
16
- # introduces the conditional / indexed-access evaluator that
17
- # parses the body and reduces `Type::App` instances against
18
- # it). The carrier never needs to read from the registry
13
+ # The registry stores registration metadata (arity, variance,
14
+ # bound) and the definition body as both a raw String and an
15
+ # evaluable `HktBody` node tree. `HktReducer` (Slice 2a) and
16
+ # `HktBodyParser` (Slice 2b) are both shipped and reduce
17
+ # `Type::App` instances against the definition. The carrier
18
+ # never needs to read from the registry
19
19
  # because Slice 1's `Type::App` carries its `bound` directly;
20
20
  # the registry exists at this slice solely so the parser
21
21
  # round-trip and downstream slices have a stable target API.
@@ -65,16 +65,15 @@ module Rigor
65
65
  # definition.
66
66
  #
67
67
  # `body` is the raw String payload from the `%a{...}`
68
- # annotation (Slice 1's parser populates it). It stays
69
- # opaque until Slice 2b's body-string parser lands.
68
+ # annotation (Slice 1's parser populates it); parsed into
69
+ # `body_tree` by `HktBodyParser` (Slice 2b, shipped).
70
70
  #
71
- # `body_tree` is the optional evaluable form: a
71
+ # `body_tree` is the evaluable form: a
72
72
  # `Rigor::Inference::HktBody::*` node tree the Slice 2a
73
73
  # reducer walks against the application's concrete
74
74
  # arguments. Plugin and Rigor-bundled overlay authors
75
75
  # construct it programmatically through
76
- # {with_body_tree}; the Slice 2b string parser will set
77
- # it from `body` once it ships. The reducer treats a
76
+ # {.definition_with_body_tree}. The reducer treats a
78
77
  # `nil` `body_tree` as "definition not yet evaluable"
79
78
  # and returns the registered bound.
80
79
  Definition = Data.define(:uri, :params, :body, :body_tree, :source_path, :source_line) do
@@ -42,10 +42,7 @@ module Rigor
42
42
  #
43
43
  # This is the single place the call-context field list is
44
44
  # enumerated — the whole point of the value object is to absorb
45
- # the wide keyword list the tiers used to each redeclare. The
46
- # ParameterLists disable here retires the per-tier disables (the
47
- # `RbsDispatch` quartet-plus signatures) rather than adding to
48
- # them.
45
+ # the wide keyword list the tiers used to each redeclare.
49
46
  def self.build(receiver:, method_name:, args:, # rubocop:disable Metrics/ParameterLists
50
47
  block_type: nil, environment: nil, call_node: nil,
51
48
  scope: nil, self_type_override: nil, public_only: false)
@@ -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,30 @@ 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?,
88
117
  :succ, :pred, :next, :abs, :magnitude,
89
118
  :bit_length, :to_s, :to_i, :to_int, :to_f,
90
119
  :floor, :ceil, :round, :truncate, :chr,
91
- :inspect, :hash, :-@, :+@, :~
120
+ :inspect, :-@, :+@, :~, :to_r, :to_c
92
121
  ].freeze
93
122
  FLOAT_UNARY = Set[
94
123
  :zero?, :positive?, :negative?,
95
124
  :nan?, :finite?, :infinite?,
96
125
  :abs, :magnitude, :floor, :ceil, :round, :truncate,
97
126
  :next_float, :prev_float,
98
- :to_s, :to_i, :to_int, :to_f,
99
- :inspect, :hash, :-@, :+@
127
+ :to_s, :to_i, :to_int, :to_f, :to_r, :rationalize,
128
+ :inspect, :-@, :+@
100
129
  ].freeze
101
130
  STRING_UNARY = Set[
102
131
  :upcase, :downcase, :capitalize, :swapcase,
@@ -104,20 +133,29 @@ module Rigor
104
133
  :empty?, :strip, :lstrip, :rstrip, :chomp, :chop, :squeeze,
105
134
  :to_s, :to_str, :to_sym, :intern,
106
135
  :to_i, :to_f, :ord, :chr, :hex, :oct, :succ, :next,
107
- :inspect, :hash
136
+ :sum, :inspect
108
137
  ].freeze
109
138
  SYMBOL_UNARY = Set[
110
139
  :to_s, :to_sym, :to_proc, :length, :size,
111
140
  :empty?, :upcase, :downcase, :capitalize,
112
- :swapcase, :inspect, :hash
141
+ :swapcase, :succ, :next, :inspect
113
142
  ].freeze
114
- BOOL_UNARY = Set[:!, :to_s, :inspect, :hash, :&, :|, :^].freeze
115
- NIL_UNARY = Set[:nil?, :!, :to_s, :to_a, :to_h, :inspect, :hash].freeze
143
+ BOOL_UNARY = Set[:!, :to_s, :inspect, :&, :|, :^].freeze
144
+ NIL_UNARY = Set[:nil?, :!, :to_s, :to_a, :to_h, :inspect].freeze
116
145
  RATIONAL_UNARY = Set[
117
146
  :zero?, :integer?, :real, :abs2,
118
- :conj, :conjugate, :nonzero?
147
+ :conj, :conjugate, :nonzero?,
148
+ :numerator, :denominator, :abs, :magnitude,
149
+ :to_f, :to_i, :to_int, :to_r, :rationalize,
150
+ :floor, :ceil, :round, :truncate,
151
+ :-@, :+@
152
+ ].freeze
153
+ COMPLEX_UNARY = Set[
154
+ :zero?, :nonzero?,
155
+ :abs, :magnitude, :abs2, :arg, :angle, :phase,
156
+ :conjugate, :conj, :real, :imaginary, :imag,
157
+ :to_c, :-@, :+@
119
158
  ].freeze
120
- COMPLEX_UNARY = Set[:zero?, :nonzero?].freeze
121
159
 
122
160
  STRING_FOLD_BYTE_LIMIT = 4096
123
161
 
@@ -386,9 +424,15 @@ module Rigor
386
424
  # Only fires on a single-receiver Range with finite integer
387
425
  # endpoints; mixed unions fall through so the existing
388
426
  # 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
427
+ RANGE_FOLD_METHODS = Set[:to_a, :first, :last, :min, :max, :count, :size, :length, :entries, :minmax,
428
+ :sum].freeze
429
+ # 1-arg head/tail projections on a `Constant<Range>`. `first(n)` /
430
+ # `take(n)` return the first `n` elements, `last(n)` the final `n` —
431
+ # each lifts to a per-position `Tuple[Constant[Integer]…]`. The
432
+ # no-arg `first` / `last` stay on the unary path (single Integer).
433
+ RANGE_FOLD_BINARY_METHODS = Set[:first, :last, :take].freeze
390
434
  RANGE_TO_A_LIMIT = 16
391
- private_constant :RANGE_FOLD_METHODS, :RANGE_TO_A_LIMIT
435
+ private_constant :RANGE_FOLD_METHODS, :RANGE_FOLD_BINARY_METHODS, :RANGE_TO_A_LIMIT
392
436
 
393
437
  def try_fold_range_constant_unary(receiver_values, method_name)
394
438
  return nil unless RANGE_FOLD_METHODS.include?(method_name)
@@ -408,6 +452,11 @@ module Rigor
408
452
  when :last, :max then range_endpoint_constant(range, :last)
409
453
  when :count, :size, :length then Type::Combinator.constant_of(range.to_a.size)
410
454
  when :minmax then range_minmax_tuple(range)
455
+ # `range.sum` is closed-form (Gauss) for an integer range, so a
456
+ # huge range still costs O(1) and yields a single Integer — no
457
+ # materialisation, no cap needed. Endless ranges are already
458
+ # excluded by the Integer-endpoint guard in the caller.
459
+ when :sum then Type::Combinator.constant_of(range.sum)
411
460
  end
412
461
  end
413
462
 
@@ -441,10 +490,46 @@ module Rigor
441
490
  )
442
491
  end
443
492
 
493
+ # `(1..10).first(3)` / `.take(3)` / `.last(3)` — the 1-arg head /
494
+ # tail forms. `first`/`last` already fold no-arg through the unary
495
+ # path; this is the n-arg sibling, mirroring the Tuple carrier's
496
+ # `first(n)`/`take(n)` handlers. Lifts to `Tuple[Constant…]`.
497
+ def try_fold_range_constant_binary(receiver_values, method_name, arg_values)
498
+ return nil unless RANGE_FOLD_BINARY_METHODS.include?(method_name)
499
+ return nil unless receiver_values.size == 1 && arg_values.size == 1
500
+
501
+ range = receiver_values.first
502
+ return nil unless range.is_a?(Range)
503
+ return nil unless range.begin.is_a?(Integer) && range.end.is_a?(Integer)
504
+
505
+ range_take_tuple(range, method_name, arg_values.first)
506
+ rescue StandardError
507
+ nil
508
+ end
509
+
510
+ def range_take_tuple(range, method_name, count)
511
+ return nil unless count.is_a?(Integer) && !count.negative?
512
+ # `first(n)`/`last(n)`/`take(n)` materialise at most `min(n, size)`
513
+ # elements; cap that count so a huge `n` (or range) never blows up
514
+ # the Constant. `Range#size` is O(1) for integer endpoints.
515
+ return nil if [count, range.size].min > RANGE_TO_A_LIMIT
516
+
517
+ values = method_name == :last ? range.last(count) : range.first(count)
518
+ return Type::Combinator.tuple_of if values.empty?
519
+
520
+ Type::Combinator.tuple_of(*values.map { |v| Type::Combinator.constant_of(v) })
521
+ end
522
+
444
523
  def try_fold_binary_set(receiver_values, method_name, arg_values)
524
+ range_lift = try_fold_range_constant_binary(receiver_values, method_name, arg_values)
525
+ return range_lift if range_lift
526
+
445
527
  string_lift = try_fold_string_array_binary(receiver_values, method_name, arg_values)
446
528
  return string_lift if string_lift
447
529
 
530
+ integer_lift = try_fold_integer_array_binary(receiver_values, method_name, arg_values)
531
+ return integer_lift if integer_lift
532
+
448
533
  pathname_lift = try_fold_pathname_binary(receiver_values, method_name, arg_values)
449
534
  return pathname_lift if pathname_lift
450
535
 
@@ -456,15 +541,20 @@ module Rigor
456
541
  end
457
542
  build_constant_type(results, source: receiver_values + arg_values)
458
543
  end
459
- # v0.0.7 — `Constant<String>#chars` / `bytes` / `lines` /
460
- # `split` (no-arg) return a Ruby Array of foldable
544
+ # v0.0.7 — `Constant<String>#chars` / `bytes` / `codepoints` /
545
+ # `lines` / `split` (no-arg) return a Ruby Array of foldable
461
546
  # scalars; `foldable_constant_value?` rejects Array
462
547
  # results, so the standard unary path declines. Lift the
463
548
  # Array to a per-position `Tuple[Constant…]` directly,
464
549
  # 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
550
+ # bounded for long strings. (`codepoints` yields per-character
551
+ # Integer codepoints, the sibling of the byte-valued `bytes`.)
552
+ STRING_ARRAY_UNARY_METHODS = Set[:chars, :bytes, :codepoints, :lines, :split].freeze
553
+ # `partition` / `rpartition` always return a fixed 3-element
554
+ # `[head, separator, tail]` Array whose members are substrings of
555
+ # the receiver (bounded by the input), so they lift to a precise
556
+ # 3-slot `Tuple[Constant…]`.
557
+ STRING_ARRAY_BINARY_METHODS = Set[:split, :scan, :partition, :rpartition].freeze
468
558
  STRING_ARRAY_LIFT_LIMIT = 32
469
559
  private_constant :STRING_ARRAY_UNARY_METHODS,
470
560
  :STRING_ARRAY_BINARY_METHODS,
@@ -494,6 +584,14 @@ module Rigor
494
584
  INTEGER_ARRAY_UNARY_METHODS = Set[:digits].freeze
495
585
  private_constant :INTEGER_ARRAY_UNARY_METHODS
496
586
 
587
+ # 1-arg Integer methods that return an Array of foldable
588
+ # Integers: `digits(base)` (base-n place values; raises on a
589
+ # negative receiver or base < 2 → declines) and `gcdlcm(other)`
590
+ # (the fixed `[gcd, lcm]` pair). Both are pure arithmetic; the
591
+ # result lifts to a `Tuple[Constant[Integer]…]`.
592
+ INTEGER_ARRAY_BINARY_METHODS = Set[:digits, :gcdlcm].freeze
593
+ private_constant :INTEGER_ARRAY_BINARY_METHODS
594
+
497
595
  # v0.0.7 — `Constant<Pathname>` delegates to a curated set
498
596
  # of pure path-manipulation methods. Pathname is immutable
499
597
  # in Ruby (per its docstring) and the catalog classifies
@@ -613,6 +711,25 @@ module Rigor
613
711
  nil
614
712
  end
615
713
 
714
+ # `Constant<Integer>#digits(base)` / `#gcdlcm(other)` — the
715
+ # 1-arg Array-returning Integer methods. `digits(base)` declines
716
+ # on a negative receiver (the unary path's guard); other domain
717
+ # errors (base < 2) raise and are rescued. `gcdlcm` is total over
718
+ # Integer args.
719
+ def try_fold_integer_array_binary(receiver_values, method_name, arg_values)
720
+ return nil unless INTEGER_ARRAY_BINARY_METHODS.include?(method_name)
721
+ return nil unless receiver_values.size == 1 && arg_values.size == 1
722
+
723
+ receiver = receiver_values.first
724
+ arg = arg_values.first
725
+ return nil unless receiver.is_a?(Integer) && arg.is_a?(Integer)
726
+ return nil if method_name == :digits && receiver.negative?
727
+
728
+ lift_array_result(receiver.public_send(method_name, arg))
729
+ rescue StandardError
730
+ nil
731
+ end
732
+
616
733
  # `Constant<Complex>#rect` / `#rectangular` — lifts `[real, imaginary]`
617
734
  # to `Tuple[Constant[re], Constant[im]]`. Both components are always
618
735
  # numeric (Integer or Float for literal complexes), so they satisfy
@@ -1334,7 +1451,24 @@ module Rigor
1334
1451
  private_constant :FOLDABLE_CONSTANT_CLASSES
1335
1452
 
1336
1453
  def foldable_constant_value?(value)
1337
- FOLDABLE_CONSTANT_CLASSES.any? { |klass| value.is_a?(klass) }
1454
+ return false unless FOLDABLE_CONSTANT_CLASSES.any? { |klass| value.is_a?(klass) }
1455
+
1456
+ # A NaN result (`0.0 / 0.0`, `Float::NAN`-propagating arithmetic,
1457
+ # or a NaN-bearing Complex) is non-reflexive under `==`, so a
1458
+ # `Constant[NaN]` would break the `==` / `eql?` / `hash` contract
1459
+ # `build_constant_type` relies on for union dedup. Decline the
1460
+ # fold and let the RBS tier answer with the widened class.
1461
+ return false if value.is_a?(Float) && value.nan?
1462
+ return false if value.is_a?(Complex) && complex_nan?(value)
1463
+
1464
+ true
1465
+ end
1466
+
1467
+ # True when either component of a Complex is NaN.
1468
+ def complex_nan?(value)
1469
+ real = value.real
1470
+ imag = value.imaginary
1471
+ (real.is_a?(Float) && real.nan?) || (imag.is_a?(Float) && imag.nan?)
1338
1472
  end
1339
1473
 
1340
1474
  def safe?(receiver_value, method_name, arg_value)
@@ -1355,6 +1489,7 @@ module Rigor
1355
1489
  when true, false then BOOL_BINARY
1356
1490
  when nil then NIL_BINARY
1357
1491
  when Rational then RATIONAL_BINARY
1492
+ when Complex then COMPLEX_BINARY
1358
1493
  else Set.new
1359
1494
  end
1360
1495
  end