rigortype 0.0.8 → 0.1.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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +234 -22
  3. data/data/builtins/ruby_core/encoding.yml +210 -0
  4. data/data/builtins/ruby_core/exception.yml +641 -0
  5. data/data/builtins/ruby_core/numeric.yml +3 -2
  6. data/data/builtins/ruby_core/proc.yml +731 -0
  7. data/data/builtins/ruby_core/random.yml +166 -0
  8. data/data/builtins/ruby_core/re.yml +689 -0
  9. data/data/builtins/ruby_core/struct.yml +449 -0
  10. data/lib/rigor/analysis/check_rules.rb +228 -40
  11. data/lib/rigor/analysis/diagnostic.rb +15 -1
  12. data/lib/rigor/analysis/runner.rb +199 -4
  13. data/lib/rigor/builtins/imported_refinements.rb +6 -1
  14. data/lib/rigor/cache/rbs_class_ancestor_table.rb +63 -0
  15. data/lib/rigor/cache/rbs_class_type_param_names.rb +60 -0
  16. data/lib/rigor/cache/rbs_constant_table.rb +15 -51
  17. data/lib/rigor/cache/rbs_descriptor.rb +55 -0
  18. data/lib/rigor/cache/rbs_environment.rb +52 -0
  19. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +40 -0
  20. data/lib/rigor/cache/rbs_instance_definitions.rb +79 -0
  21. data/lib/rigor/cache/rbs_known_class_names.rb +43 -0
  22. data/lib/rigor/cache/store.rb +81 -15
  23. data/lib/rigor/cli.rb +45 -7
  24. data/lib/rigor/configuration/severity_profile.rb +109 -0
  25. data/lib/rigor/configuration.rb +110 -6
  26. data/lib/rigor/environment/rbs_hierarchy.rb +18 -5
  27. data/lib/rigor/environment/rbs_loader.rb +220 -32
  28. data/lib/rigor/environment.rb +11 -2
  29. data/lib/rigor/flow_contribution/conflict.rb +81 -0
  30. data/lib/rigor/flow_contribution/element.rb +53 -0
  31. data/lib/rigor/flow_contribution/fact.rb +88 -0
  32. data/lib/rigor/flow_contribution/merge_result.rb +67 -0
  33. data/lib/rigor/flow_contribution/merger.rb +275 -0
  34. data/lib/rigor/flow_contribution.rb +179 -0
  35. data/lib/rigor/inference/block_parameter_binder.rb +15 -0
  36. data/lib/rigor/inference/builtins/encoding_catalog.rb +67 -0
  37. data/lib/rigor/inference/builtins/exception_catalog.rb +92 -0
  38. data/lib/rigor/inference/builtins/proc_catalog.rb +122 -0
  39. data/lib/rigor/inference/builtins/random_catalog.rb +58 -0
  40. data/lib/rigor/inference/builtins/re_catalog.rb +81 -0
  41. data/lib/rigor/inference/builtins/struct_catalog.rb +55 -0
  42. data/lib/rigor/inference/expression_typer.rb +110 -6
  43. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +16 -1
  44. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +173 -0
  45. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +21 -1
  46. data/lib/rigor/inference/method_dispatcher.rb +2 -0
  47. data/lib/rigor/inference/multi_target_binder.rb +2 -0
  48. data/lib/rigor/inference/narrowing.rb +134 -144
  49. data/lib/rigor/inference/scope_indexer.rb +75 -1
  50. data/lib/rigor/inference/statement_evaluator.rb +380 -40
  51. data/lib/rigor/plugin/access_denied_error.rb +24 -0
  52. data/lib/rigor/plugin/base.rb +241 -0
  53. data/lib/rigor/plugin/io_boundary.rb +102 -0
  54. data/lib/rigor/plugin/load_error.rb +23 -0
  55. data/lib/rigor/plugin/loader.rb +191 -0
  56. data/lib/rigor/plugin/manifest.rb +134 -0
  57. data/lib/rigor/plugin/registry.rb +50 -0
  58. data/lib/rigor/plugin/services.rb +65 -0
  59. data/lib/rigor/plugin/trust_policy.rb +99 -0
  60. data/lib/rigor/plugin.rb +61 -0
  61. data/lib/rigor/rbs_extended.rb +103 -0
  62. data/lib/rigor/reflection.rb +2 -2
  63. data/lib/rigor/type/combinator.rb +72 -0
  64. data/lib/rigor/type/refined.rb +50 -2
  65. data/lib/rigor/version.rb +1 -1
  66. data/lib/rigor.rb +13 -0
  67. data/sig/rigor/environment.rbs +7 -1
  68. data/sig/rigor/inference.rbs +1 -0
  69. data/sig/rigor/rbs_extended.rbs +2 -0
  70. data/sig/rigor/scope.rbs +1 -0
  71. data/sig/rigor/type.rbs +7 -0
  72. data/sig/rigor.rbs +3 -1
  73. metadata +38 -1
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "method_catalog"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module Builtins
8
+ # `Proc` / `Method` / `UnboundMethod` catalog. Singleton —
9
+ # load once, consult during dispatch.
10
+ #
11
+ # The three callable carriers are imported together because
12
+ # `Init_Proc` registers them in a single C init block. They
13
+ # share the same fundamental hazard at the catalog tier:
14
+ # most of their public methods invoke the wrapped Ruby code
15
+ # (the proc body, the bound method's receiver, …) and that
16
+ # code can do anything — read mutable state, call I/O, return
17
+ # different values on successive calls. The static C-body
18
+ # classifier marks these `:leaf` because the C functions
19
+ # themselves do not call `rb_funcall*` / `rb_yield` directly
20
+ # (they delegate through the VM's optimised call paths and
21
+ # method-entry table), but folding any of them at compile
22
+ # time would freeze a value the runtime never actually
23
+ # produces twice.
24
+ #
25
+ # The blocklist below errs aggressively on the side of
26
+ # caution: a hypothetical future `Constant<Proc>` /
27
+ # `Constant<Method>` / `Constant<UnboundMethod>` carrier
28
+ # would have very little to gain from these folds and a
29
+ # great deal to lose if user code ran behind the analyzer's
30
+ # back. Reflective readers (`#arity`, `#parameters`,
31
+ # `#source_location`, `#name`, `#owner`, `#receiver`) remain
32
+ # foldable; the RBS tier still resolves return types for
33
+ # the blocklisted methods so callers do not lose precision.
34
+ PROC_CATALOG = MethodCatalog.new(
35
+ path: File.expand_path(
36
+ "../../../../data/builtins/ruby_core/proc.yml",
37
+ __dir__
38
+ ),
39
+ mutating_selectors: {
40
+ "Proc" => Set[
41
+ # `#call` / `#[]` / `#===` / `#yield` invoke the proc
42
+ # body. The C body routes through
43
+ # `OPTIMIZED_METHOD_TYPE_CALL` (a VM fast path the
44
+ # classifier cannot see into); the proc body can do
45
+ # anything — read globals, mutate captured locals,
46
+ # raise. MUST decline to fold.
47
+ :call,
48
+ :[],
49
+ :===,
50
+ :yield,
51
+ # `#curry` / `#<<` / `#>>` allocate a fresh `Proc`
52
+ # that closes over the receiver (and, for `<<` /
53
+ # `>>`, over the argument). Folding would freeze a
54
+ # specific `Proc` instance whose identity the runtime
55
+ # never actually produces (object_id differs every
56
+ # call), so the catalog tier declines.
57
+ :curry,
58
+ :<<,
59
+ :>>,
60
+ # `#to_proc` returns `self` for `Proc` (cheap), but
61
+ # blocking it keeps the rule shape uniform across the
62
+ # three callable carriers (Method#to_proc allocates a
63
+ # fresh `Proc`).
64
+ :to_proc,
65
+ # Identity-based equality and hashing: `#hash` is
66
+ # derived from the underlying ISeq pointer; `#==` /
67
+ # `#eql?` compare ISeq + binding. Folding to a
68
+ # `Constant<Integer>` / `Constant<bool>` would freeze
69
+ # an answer that depends on memory layout. Defensive.
70
+ :hash,
71
+ :==,
72
+ :eql?,
73
+ # `initialize_copy` is blocklisted by convention so a
74
+ # hypothetical future `Constant<Proc>` carrier cannot
75
+ # fold an aliasing copy through the catalog.
76
+ :initialize_copy
77
+ ],
78
+ "Method" => Set[
79
+ # `#call` / `#[]` / `#===` invoke the bound method.
80
+ # Same hazard as `Proc#call`: arbitrary user code,
81
+ # arbitrary side effects.
82
+ :call,
83
+ :[],
84
+ :===,
85
+ # `#curry` / `#<<` / `#>>` allocate a fresh `Proc`
86
+ # that closes over the bound method.
87
+ :curry,
88
+ :<<,
89
+ :>>,
90
+ # `#to_proc` allocates a fresh `Proc` wrapping the
91
+ # bound method — folding would freeze its object_id.
92
+ # The classifier already marks it `:block_dependent`,
93
+ # but the explicit entry keeps the intent obvious.
94
+ :to_proc,
95
+ # `#unbind` allocates a fresh `UnboundMethod` whose
96
+ # identity differs every call.
97
+ :unbind,
98
+ # Identity-based equality and hashing.
99
+ :hash,
100
+ :==,
101
+ :eql?,
102
+ # `initialize_copy` is blocklisted by convention.
103
+ :initialize_copy
104
+ ],
105
+ "UnboundMethod" => Set[
106
+ # `#bind` allocates a fresh `Method` whose object_id
107
+ # differs every call; `#bind_call` invokes the bound
108
+ # method (already classified `:block_dependent`).
109
+ :bind,
110
+ :bind_call,
111
+ # Identity-based equality and hashing.
112
+ :hash,
113
+ :==,
114
+ :eql?,
115
+ # `initialize_copy` is blocklisted by convention.
116
+ :initialize_copy
117
+ ]
118
+ }
119
+ )
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "method_catalog"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module Builtins
8
+ # `Random` catalog. Singleton — load once, consult during
9
+ # dispatch.
10
+ #
11
+ # The static classifier marks most Random methods `:leaf`
12
+ # because their C bodies do not call `rb_funcall*` /
13
+ # `rb_yield` / `rb_check_frozen` directly. Random is the
14
+ # canonical case where that heuristic under-counts: every
15
+ # call to `#rand` / `#bytes` / `Random.rand` / `Random.bytes`
16
+ # advances the receiver's Mersenne-Twister state through a
17
+ # helper (`rand_random` -> `random_real` / `random_ulong_limited`),
18
+ # so folding any of them statically is unsound.
19
+ # `Random.new_seed` and `Random.urandom` are non-deterministic
20
+ # (different output every call); even though they are
21
+ # functionally pure they would produce a misleading constant
22
+ # at fold time. The whole class is conservative-by-default
23
+ # at the catalog tier; precision flows through the RBS layer.
24
+ RANDOM_CATALOG = MethodCatalog.new(
25
+ path: File.expand_path(
26
+ "../../../../data/builtins/ruby_core/random.yml",
27
+ __dir__
28
+ ),
29
+ mutating_selectors: {
30
+ "Random" => Set[
31
+ # `rand_random` -> `random_real` / `random_ulong_limited`
32
+ # advance the MT state on the receiver (instance #rand)
33
+ # and on `Random::DEFAULT` (singleton .rand). The
34
+ # classifier misses the indirect mutator.
35
+ :rand,
36
+ # `random_bytes` / `random_s_bytes` consume MT output
37
+ # the same way #rand does — every call mutates the
38
+ # underlying generator.
39
+ :bytes,
40
+ # Non-deterministic: each call produces a fresh seed
41
+ # via `with_random_seed` reading platform entropy. Folding
42
+ # to a constant would freeze a value that the runtime
43
+ # never actually returns twice.
44
+ :new_seed,
45
+ # Non-deterministic: reads from platform CSPRNG (e.g.
46
+ # /dev/urandom). Folding is unsound for the same reason
47
+ # as `new_seed`.
48
+ :urandom,
49
+ # `initialize_copy` is blocklisted by convention so a
50
+ # hypothetical future `Constant<Random>` carrier
51
+ # cannot fold an aliasing copy through the catalog.
52
+ :initialize_copy
53
+ ]
54
+ }
55
+ )
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "method_catalog"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module Builtins
8
+ # `Regexp` / `MatchData` catalog. Singleton — load once,
9
+ # consult during dispatch.
10
+ #
11
+ # `Init_Regexp` in `references/ruby/re.c` registers BOTH
12
+ # classes in a single C init block, so the catalog carries
13
+ # both — `Regexp` (the pattern carrier) plus `MatchData`
14
+ # (the result-of-match carrier produced by `Regexp#match` /
15
+ # `String#match` and consulted via `$~`). The catalog wiring
16
+ # therefore mostly governs:
17
+ #
18
+ # 1. The reader surface on each class (`Regexp#source`,
19
+ # `Regexp#options`, `Regexp#casefold?`, `MatchData#size`,
20
+ # `MatchData#captures`, etc.) — RBS-declared returns are
21
+ # preserved through dispatch.
22
+ # 2. The blocklist below, which keeps methods that touch
23
+ # process-global state (the `$~` backref) from being
24
+ # folded. Regexp matching is observably stateful:
25
+ # `Regexp#=~`, `#===` and `#~` all call `rb_backref_set`
26
+ # (writing `$~` and the `$1..$N` / `$&` / `` $` `` / `$'`
27
+ # aliases). A constant-fold that dropped those calls
28
+ # would silently change the visible state of the program,
29
+ # so they MUST decline through to the RBS tier.
30
+ #
31
+ # `Regexp.last_match` and `Regexp.timeout` / `Regexp.timeout=`
32
+ # are class-level (singleton) methods that also touch
33
+ # process-global state, but the dispatcher's catalog lookup
34
+ # only consults `:instance` entries today — class-method calls
35
+ # on a `Singleton` receiver type take the `meta_*` path in
36
+ # `MethodDispatcher` rather than walking `CATALOG_BY_CLASS` —
37
+ # so listing them here would be dead code. Their RBS-tier
38
+ # signatures already widen the answer enough to keep the
39
+ # behaviour sound; revisit if the dispatcher ever grows a
40
+ # singleton-aware catalog path.
41
+ REGEXP_CATALOG = MethodCatalog.new(
42
+ path: File.expand_path(
43
+ "../../../../data/builtins/ruby_core/re.yml",
44
+ __dir__
45
+ ),
46
+ mutating_selectors: {
47
+ "Regexp" => Set[
48
+ # Defensive: aliasing-copy semantics already covered
49
+ # by the `:mutates_self` classifier, listed here for
50
+ # symmetry with String / Array / Hash / Range / Set.
51
+ :initialize_copy,
52
+ # `=~`, `===`, `~` all run `rb_reg_search` (or call
53
+ # `rb_backref_set(Qnil)` directly) — every successful
54
+ # OR failing match writes `$~` and the
55
+ # `$1..$N` / `$&` / `` $` `` / `$'` aliases. Folding
56
+ # would discard the visible side effect.
57
+ :=~,
58
+ :"===",
59
+ :~,
60
+ # `match` is already `:block_dependent` (the C body
61
+ # yields), but it ALSO writes `$~` regardless of the
62
+ # block. Listed here so a future extractor that
63
+ # reclassifies it as `:leaf` (because the yield is
64
+ # behind a helper) does not silently fold it.
65
+ :match
66
+ ],
67
+ "MatchData" => Set[
68
+ # Defensive entry mirroring the other catalogs.
69
+ # `match_init_copy` is already `:leaf` per the
70
+ # extractor (it copies the regs slot in place but
71
+ # uses no helper the C-body regex flags as a
72
+ # mutator); blocked so a future
73
+ # `Constant<MatchData>` carrier never folds an
74
+ # aliasing copy through the catalog.
75
+ :initialize_copy
76
+ ]
77
+ }
78
+ )
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "method_catalog"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module Builtins
8
+ # `Struct` catalog. Singleton — load once, consult during
9
+ # dispatch.
10
+ #
11
+ # `Struct` is a meta-class: `Struct.new(*members)` returns a
12
+ # fresh anonymous subclass — never a `Struct` value. Today
13
+ # Rigor never produces a `Constant<Struct>` carrier (a literal
14
+ # struct instance), so the catalog is defensive: it documents
15
+ # the shape and forbids unsafe folds in case a future tier
16
+ # learns to lift literal struct instances into the value
17
+ # lattice.
18
+ #
19
+ # Subclasses define their own writers (`name=`) at class-build
20
+ # time, so per-instance member accessors do not appear in this
21
+ # YAML — only the generic `[]` / `[]=` pair on the base class.
22
+ # `[]=` is already classified `:mutates_self`; `[]` reads a
23
+ # member but the answer depends on the subclass's member
24
+ # definition, which the catalog does not see, so we blocklist
25
+ # it defensively.
26
+ STRUCT_CATALOG = MethodCatalog.new(
27
+ path: File.expand_path(
28
+ "../../../../data/builtins/ruby_core/struct.yml",
29
+ __dir__
30
+ ),
31
+ mutating_selectors: {
32
+ "Struct" => Set[
33
+ # Defensive: aliasing-copy semantics on a hypothetical
34
+ # `Constant<Struct>` carrier. Convention across the
35
+ # other catalogs (Range, Random, Pathname).
36
+ :initialize_copy,
37
+ # `rb_struct_hash` mixes member values via
38
+ # `rb_hash` -> `rb_funcall(:hash, ...)`. The classifier
39
+ # sees no direct dispatch because the recursion goes
40
+ # through `rb_hash` (a helper), but the answer depends
41
+ # on the member values' `#hash` — user-redefinable.
42
+ # Block to avoid folding a hash that would diverge
43
+ # from the runtime once a member overrides `#hash`.
44
+ :hash,
45
+ # `rb_struct_aref` reads a member by name or index; the
46
+ # answer depends on the subclass's member layout, which
47
+ # the catalog does not carry. Folding without knowing
48
+ # the layout would be unsound.
49
+ :[]
50
+ ]
51
+ }
52
+ )
53
+ end
54
+ end
55
+ end
@@ -65,6 +65,7 @@ module Rigor
65
65
  Prism::NilNode => :type_of_nil,
66
66
  # Locals
67
67
  Prism::LocalVariableReadNode => :local_read,
68
+ Prism::ItLocalVariableReadNode => :it_read,
68
69
  Prism::LocalVariableWriteNode => :type_of_assignment_write,
69
70
  # Containers and pass-throughs
70
71
  Prism::ArrayNode => :array_type_for,
@@ -171,9 +172,11 @@ module Rigor
171
172
  Prism::WhileNode => :type_of_loop,
172
173
  Prism::UntilNode => :type_of_loop,
173
174
  Prism::ForNode => :type_of_dynamic_top,
174
- Prism::DefinedNode => :type_of_dynamic_top,
175
- Prism::MatchPredicateNode => :type_of_dynamic_top,
176
- Prism::MatchRequiredNode => :type_of_dynamic_top,
175
+ Prism::DefinedNode => :type_of_defined,
176
+ Prism::NumberedReferenceReadNode => :type_of_string_or_nil,
177
+ Prism::BackReferenceReadNode => :type_of_string_or_nil,
178
+ Prism::MatchPredicateNode => :type_of_match_predicate,
179
+ Prism::MatchRequiredNode => :type_of_match_required,
177
180
  Prism::MatchWriteNode => :type_of_dynamic_top,
178
181
  # Literal containers
179
182
  Prism::LambdaNode => :type_of_lambda,
@@ -298,6 +301,45 @@ module Rigor
298
301
  dynamic_top
299
302
  end
300
303
 
304
+ # `defined?(expr)` returns `String | nil` per Ruby semantics —
305
+ # a description of the expression's category (`"local-variable"`,
306
+ # `"method"`, ...) when defined, or `nil` when not. The argument
307
+ # is not evaluated (it is statically inspected by the runtime),
308
+ # so the typer does not recurse into it.
309
+ def type_of_defined(_node)
310
+ Type::Combinator.union(
311
+ Type::Combinator.nominal_of("String"),
312
+ Type::Combinator.constant_of(nil)
313
+ )
314
+ end
315
+
316
+ # `$1`, `$&`, `$'`, `$+`, `$\`` — the regex back-reference and
317
+ # numbered-capture globals each carry `String | nil`. They share
318
+ # the typer because the typing rule is identical regardless of
319
+ # which back-reference shape Prism emitted.
320
+ def type_of_string_or_nil(_node)
321
+ Type::Combinator.union(
322
+ Type::Combinator.nominal_of("String"),
323
+ Type::Combinator.constant_of(nil)
324
+ )
325
+ end
326
+
327
+ # `expr in pattern` — pattern-match predicate. Returns `true`
328
+ # when the pattern matches, `false` otherwise.
329
+ def type_of_match_predicate(_node)
330
+ Type::Combinator.union(
331
+ Type::Combinator.constant_of(true),
332
+ Type::Combinator.constant_of(false)
333
+ )
334
+ end
335
+
336
+ # `expr => pattern` — one-line pattern-match assertion. Raises
337
+ # `NoMatchingPatternError` on mismatch; on success the expression
338
+ # itself evaluates to `nil`.
339
+ def type_of_match_required(_node)
340
+ Type::Combinator.constant_of(nil)
341
+ end
342
+
301
343
  # The expression `Foo` evaluates to the *class object* `Foo`, not
302
344
  # an instance. From Slice 4 phase 2b on we therefore type a
303
345
  # bare-constant reference as `Singleton[Foo]`; method dispatch on
@@ -472,10 +514,35 @@ module Rigor
472
514
  [keys, values]
473
515
  end
474
516
 
475
- def type_of_interpolated_string(_node)
517
+ # An interpolated string `"#{a}b#{c}"` is `literal-string`
518
+ # when every part contributes literal-bearing material:
519
+ # plain text segments are literal by construction, embedded
520
+ # expressions count when their type is itself literal-string-
521
+ # compatible (a `Constant<String>`, the `literal-string`
522
+ # carrier, an `Intersection` containing it, or a `Union`
523
+ # whose members all qualify). Otherwise the result widens to
524
+ # plain `Nominal[String]` as before.
525
+ def type_of_interpolated_string(node)
526
+ return Type::Combinator.literal_string if interpolation_parts_literal?(node.parts)
527
+
476
528
  Type::Combinator.nominal_of(String)
477
529
  end
478
530
 
531
+ def interpolation_parts_literal?(parts)
532
+ parts.all? { |part| interpolation_part_literal?(part) }
533
+ end
534
+
535
+ def interpolation_part_literal?(part)
536
+ case part
537
+ when Prism::StringNode
538
+ true
539
+ when Prism::EmbeddedStatementsNode, Prism::EmbeddedVariableNode
540
+ Type::Combinator.literal_string_compatible?(type_of(part))
541
+ else
542
+ false
543
+ end
544
+ end
545
+
479
546
  def type_of_interpolated_symbol(_node)
480
547
  Type::Combinator.nominal_of(Symbol)
481
548
  end
@@ -698,9 +765,38 @@ module Rigor
698
765
  def type_of_range(node)
699
766
  left_static, left = static_range_endpoint(node.left)
700
767
  right_static, right = static_range_endpoint(node.right)
701
- return Type::Combinator.nominal_of(Range) unless left_static && right_static
768
+ return Type::Combinator.constant_of(Range.new(left, right, node.exclude_end?)) if left_static && right_static
769
+
770
+ nominal_range_for_endpoints(node.left, node.right)
771
+ end
772
+
773
+ # Derives `Nominal[Range, [T]]` from the endpoint expression
774
+ # types when at least one endpoint is statically typeable. The
775
+ # element parameter is the union of the endpoint types (lifted
776
+ # from `Constant<v>` to `Nominal<v.class>` so the carrier matches
777
+ # what `Range#each` would yield). Falls back to bare
778
+ # `Nominal[Range]` when no endpoint contributes a typable shape.
779
+ def nominal_range_for_endpoints(left_node, right_node)
780
+ endpoints = [left_node, right_node].compact.map { |n| range_endpoint_element_type(n) }
781
+ endpoints.reject! { |t| t.equal?(Type::Combinator.untyped) }
782
+ return Type::Combinator.nominal_of("Range") if endpoints.empty?
783
+
784
+ Type::Combinator.nominal_of("Range", type_args: [Type::Combinator.union(*endpoints)])
785
+ end
786
+
787
+ def range_endpoint_element_type(node)
788
+ type = type_of(node)
789
+ case type
790
+ when Type::Constant
791
+ value = type.value
792
+ return Type::Combinator.untyped if value.nil?
702
793
 
703
- Type::Combinator.constant_of(Range.new(left, right, node.exclude_end?))
794
+ Type::Combinator.nominal_of(value.class.name)
795
+ when Type::IntegerRange
796
+ Type::Combinator.nominal_of("Integer")
797
+ else
798
+ type
799
+ end
704
800
  end
705
801
 
706
802
  # v0.0.7 — non-interpolated regex literals lift to
@@ -721,6 +817,7 @@ module Rigor
721
817
  def static_range_endpoint(node)
722
818
  return [true, nil] if node.nil?
723
819
  return [true, node.value] if node.is_a?(Prism::IntegerNode)
820
+ return [true, node.unescaped] if node.is_a?(Prism::StringNode) && node.respond_to?(:unescaped)
724
821
 
725
822
  [false, nil]
726
823
  end
@@ -780,6 +877,13 @@ module Rigor
780
877
  scope.local(node.name) || dynamic_top
781
878
  end
782
879
 
880
+ # `it` (Ruby 3.4) — `ItLocalVariableReadNode` carries no `name`
881
+ # field; the implicit name is always `:it`, matching the binding
882
+ # `BlockParameterBinder` installs for `Prism::ItParametersNode`.
883
+ def it_read(_node)
884
+ scope.local(:it) || dynamic_top
885
+ end
886
+
783
887
  # Slice 5 phase 1 upgrades array literals to `Tuple[T1..Tn]`
784
888
  # when every element is a non-splat value. Splatted entries
785
889
  # (`[*xs, 1]`) preserve the Slice 4 phase 2d behavior: we union
@@ -14,6 +14,12 @@ require_relative "../builtins/enumerable_catalog"
14
14
  require_relative "../builtins/rational_catalog"
15
15
  require_relative "../builtins/complex_catalog"
16
16
  require_relative "../builtins/pathname_catalog"
17
+ require_relative "../builtins/random_catalog"
18
+ require_relative "../builtins/struct_catalog"
19
+ require_relative "../builtins/encoding_catalog"
20
+ require_relative "../builtins/re_catalog"
21
+ require_relative "../builtins/proc_catalog"
22
+ require_relative "../builtins/exception_catalog"
17
23
 
18
24
  module Rigor
19
25
  module Inference
@@ -1077,7 +1083,16 @@ module Rigor
1077
1083
  [Date, [Builtins::DATE_CATALOG, "Date"]],
1078
1084
  [Rational, [Builtins::RATIONAL_CATALOG, "Rational"]],
1079
1085
  [Complex, [Builtins::COMPLEX_CATALOG, "Complex"]],
1080
- [Pathname, [Builtins::PATHNAME_CATALOG, "Pathname"]]
1086
+ [Pathname, [Builtins::PATHNAME_CATALOG, "Pathname"]],
1087
+ [Random, [Builtins::RANDOM_CATALOG, "Random"]],
1088
+ [Struct, [Builtins::STRUCT_CATALOG, "Struct"]],
1089
+ [Encoding, [Builtins::ENCODING_CATALOG, "Encoding"]],
1090
+ [Regexp, [Builtins::REGEXP_CATALOG, "Regexp"]],
1091
+ [MatchData, [Builtins::REGEXP_CATALOG, "MatchData"]],
1092
+ [Proc, [Builtins::PROC_CATALOG, "Proc"]],
1093
+ [Method, [Builtins::PROC_CATALOG, "Method"]],
1094
+ [UnboundMethod, [Builtins::PROC_CATALOG, "UnboundMethod"]],
1095
+ [Exception, [Builtins::EXCEPTION_CATALOG, "Exception"]]
1081
1096
  ].freeze
1082
1097
  private_constant :CATALOG_BY_CLASS
1083
1098