rigortype 0.0.2 → 0.0.4

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +24 -7
  3. data/data/builtins/ruby_core/array.yml +1470 -0
  4. data/data/builtins/ruby_core/file.yml +501 -0
  5. data/data/builtins/ruby_core/hash.yml +936 -0
  6. data/data/builtins/ruby_core/io.yml +1594 -0
  7. data/data/builtins/ruby_core/numeric.yml +1809 -0
  8. data/data/builtins/ruby_core/range.yml +389 -0
  9. data/data/builtins/ruby_core/set.yml +594 -0
  10. data/data/builtins/ruby_core/string.yml +1850 -0
  11. data/data/builtins/ruby_core/time.yml +750 -0
  12. data/lib/rigor/analysis/check_rules.rb +97 -4
  13. data/lib/rigor/analysis/runner.rb +4 -0
  14. data/lib/rigor/builtins/imported_refinements.rb +251 -0
  15. data/lib/rigor/configuration.rb +6 -1
  16. data/lib/rigor/inference/acceptance.rb +324 -6
  17. data/lib/rigor/inference/builtins/array_catalog.rb +46 -0
  18. data/lib/rigor/inference/builtins/hash_catalog.rb +40 -0
  19. data/lib/rigor/inference/builtins/method_catalog.rb +90 -0
  20. data/lib/rigor/inference/builtins/numeric_catalog.rb +93 -0
  21. data/lib/rigor/inference/builtins/range_catalog.rb +46 -0
  22. data/lib/rigor/inference/builtins/set_catalog.rb +54 -0
  23. data/lib/rigor/inference/builtins/string_catalog.rb +39 -0
  24. data/lib/rigor/inference/builtins/time_catalog.rb +64 -0
  25. data/lib/rigor/inference/expression_typer.rb +48 -1
  26. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +670 -16
  27. data/lib/rigor/inference/method_dispatcher/file_folding.rb +144 -0
  28. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +215 -0
  29. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +23 -7
  30. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +4 -0
  31. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +240 -4
  32. data/lib/rigor/inference/method_dispatcher.rb +28 -21
  33. data/lib/rigor/inference/method_parameter_binder.rb +29 -4
  34. data/lib/rigor/inference/narrowing.rb +376 -4
  35. data/lib/rigor/inference/scope_indexer.rb +10 -2
  36. data/lib/rigor/inference/statement_evaluator.rb +213 -2
  37. data/lib/rigor/rbs_extended.rb +230 -15
  38. data/lib/rigor/scope.rb +14 -0
  39. data/lib/rigor/type/combinator.rb +159 -1
  40. data/lib/rigor/type/difference.rb +155 -0
  41. data/lib/rigor/type/integer_range.rb +137 -0
  42. data/lib/rigor/type/intersection.rb +135 -0
  43. data/lib/rigor/type/refined.rb +174 -0
  44. data/lib/rigor/type.rb +4 -0
  45. data/lib/rigor/version.rb +1 -1
  46. data/sig/rigor/rbs_extended.rbs +14 -0
  47. data/sig/rigor/scope.rbs +1 -0
  48. data/sig/rigor/type.rbs +91 -1
  49. metadata +25 -1
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "method_catalog"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module Builtins
8
+ # `Range` catalog. Singleton — load once, consult during
9
+ # dispatch.
10
+ #
11
+ # Range is largely immutable: `begin`, `end`, and `excl` are
12
+ # set at construction by `range_initialize` and never mutated
13
+ # afterwards. The blocklist below therefore stays small. The
14
+ # entries we DO need are the iteration methods whose C body
15
+ # routes through a helper the block/yield regex does not
16
+ # recognise, so the classifier mis-flags them as `:leaf`
17
+ # despite yielding to a block.
18
+ RANGE_CATALOG = MethodCatalog.new(
19
+ path: File.expand_path(
20
+ "../../../../data/builtins/ruby_core/range.yml",
21
+ __dir__
22
+ ),
23
+ mutating_selectors: {
24
+ "Range" => Set[
25
+ # `range_initialize` / `range_initialize_copy` write
26
+ # `begin`/`end`/`excl` slots on the receiver; classed
27
+ # `:leaf` because the writes go through the struct
28
+ # accessor not `rb_check_frozen`. Blocked for symmetry
29
+ # with String / Array.
30
+ :initialize, :initialize_copy,
31
+ # `range_reverse_each` yields to its block via
32
+ # `range_each_func` -> caller's block; the regex
33
+ # classifier follows direct `rb_yield*` calls only.
34
+ :reverse_each,
35
+ # `range_percent_step` returns an Enumerator unless a
36
+ # block is supplied, in which case it yields. Treated
37
+ # as block-dependent so the fold tier never invokes it
38
+ # against a literal Range and tries to materialise an
39
+ # Enumerator into a Constant.
40
+ :%
41
+ ]
42
+ }
43
+ )
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "method_catalog"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module Builtins
8
+ # `Set` catalog. Singleton — load once, consult during dispatch.
9
+ #
10
+ # Set was rewritten in C and folded into CRuby for Ruby 3.2+;
11
+ # the reference branch (`ruby_4_0`) ships the implementation in
12
+ # `references/ruby/set.c` with `Init_Set` registering every
13
+ # method directly. There is no `set.rb` prelude — the trailing
14
+ # `rb_provide("set.rb")` makes `require "set"` a no-op against
15
+ # the built-in.
16
+ #
17
+ # The blocklist below catches the catalog `:leaf` entries the
18
+ # C-body classifier mis-attributes. Set's iteration helpers
19
+ # (`set_iter`, `RETURN_SIZED_ENUMERATOR`) and its identity-
20
+ # mode and reset paths drive into helpers the regex classifier
21
+ # does not yet recognise as block-yielding or mutating.
22
+ SET_CATALOG = MethodCatalog.new(
23
+ path: File.expand_path(
24
+ "../../../../data/builtins/ruby_core/set.yml",
25
+ __dir__
26
+ ),
27
+ mutating_selectors: {
28
+ "Set" => Set[
29
+ # Indirect mutators classified `:leaf` because the C
30
+ # classifier did not follow the helper functions:
31
+ #
32
+ # - `initialize_copy` calls `set_copy` to overwrite the
33
+ # receiver's table.
34
+ # - `compare_by_identity` swaps the internal hash type
35
+ # via `set_reset_table_with_type`.
36
+ # - `reset` rebuilds the internal table to dedup after
37
+ # element mutation.
38
+ :initialize_copy, :compare_by_identity, :reset,
39
+ # Block-dependent methods classified `:leaf` because the
40
+ # C body uses `set_iter` / `RETURN_SIZED_ENUMERATOR`
41
+ # rather than calling `rb_yield` directly:
42
+ :each, :classify, :divide,
43
+ # `disjoint?` delegates into `set_i_intersect`, which
44
+ # for non-Set enumerables uses `rb_funcall(other,
45
+ # :any?, ...)` — that is user-redefinable dispatch the
46
+ # classifier missed because the call site is in a
47
+ # sibling function.
48
+ :disjoint?
49
+ ]
50
+ }
51
+ )
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "method_catalog"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module Builtins
8
+ # `String` and `Symbol` catalog. Singleton — load once,
9
+ # consult during dispatch.
10
+ #
11
+ # The blocklist below is the curated set of catalog `:leaf`
12
+ # entries the C-body classifier mis-attributes (the body of
13
+ # `rb_str_replace` calls `str_modifiable` / `str_discard`
14
+ # which the regex-based classifier does not recognise as
15
+ # mutation primitives). Adding to the blocklist is the
16
+ # corrective surface for false positives until the
17
+ # classifier learns the helper functions.
18
+ STRING_CATALOG = MethodCatalog.new(
19
+ path: File.expand_path(
20
+ "../../../../data/builtins/ruby_core/string.yml",
21
+ __dir__
22
+ ),
23
+ mutating_selectors: {
24
+ "String" => Set[
25
+ :replace, :initialize, :initialize_copy, :clear, :<<, :concat, :insert,
26
+ :prepend, :force_encoding, :encode, :scrub, :unicode_normalize, :"[]=",
27
+ :upto, :each_byte, :each_char, :each_codepoint,
28
+ :each_grapheme_cluster, :each_line, :bytesplice
29
+ ],
30
+ "Symbol" => Set[
31
+ # Symbol is immutable in Ruby; the classifier mis-flags
32
+ # `inspect` because `rb_sym_inspect` builds a temporary
33
+ # mutable buffer. Allow it.
34
+ ]
35
+ }
36
+ )
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "method_catalog"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module Builtins
8
+ # `Time` catalog. Singleton — load once, consult during dispatch.
9
+ #
10
+ # Time is a pure-C built-in: the Init block in
11
+ # `references/ruby/time.c` registers the bulk of the surface,
12
+ # and the Ruby-side prelude `references/ruby/timev.rb`
13
+ # contributes the class-side constructors (`Time.now`,
14
+ # `Time.at`, `Time.new`) through Primitive cexpr stubs.
15
+ #
16
+ # Time receivers are not lifted to a `Constant` carrier today
17
+ # (there is no `Time` literal node — the closest is
18
+ # `Time.now` / `Time.new(...)`, which produce `Nominal[Time]`).
19
+ # The catalog wiring therefore mostly governs:
20
+ #
21
+ # 1. The size-projection-equivalent reader surface (`#year`,
22
+ # `#month`, `#hour`, `#sec`, `#wday`, …) — RBS-declared
23
+ # `Integer` is preserved through dispatch.
24
+ # 2. The blocklist below, which keeps the indirect-mutator
25
+ # methods that the C-body classifier mis-flagged as
26
+ # `:leaf` from ever folding through a hypothetical future
27
+ # `Constant<Time>` carrier.
28
+ #
29
+ # The blocklist captures the false-positive `:leaf` entries
30
+ # whose helper functions the regex classifier did not
31
+ # recognise as mutators.
32
+ TIME_CATALOG = MethodCatalog.new(
33
+ path: File.expand_path(
34
+ "../../../../data/builtins/ruby_core/time.yml",
35
+ __dir__
36
+ ),
37
+ mutating_selectors: {
38
+ "Time" => Set[
39
+ # `time_init_copy` writes the `timew` and `vtm` slots on
40
+ # the receiver via `time_set_timew` / `time_set_vtm`.
41
+ # Classed `:leaf` because those setters are not in the
42
+ # mutator regex's helper list. Blocked for symmetry with
43
+ # String / Array / Range / Set initialize_copy entries.
44
+ :initialize_copy,
45
+ # `time_localtime_m` -> `time_localtime` calls
46
+ # `time_modify(time)` to mark the receiver mutable
47
+ # before rewriting its `vtm` cache and `tzmode`. The
48
+ # docstring is explicit ("converts time to local time
49
+ # in place"). The C-body classifier mis-flagged it as
50
+ # `:leaf` because `time_modify` is not in its mutator
51
+ # regex.
52
+ :localtime,
53
+ # `time_gmtime` (registered as both `gmtime` and `utc`
54
+ # against `rb_cTime`) follows the same in-place pattern
55
+ # as `time_localtime`: `time_modify(time)` then a
56
+ # `time_set_vtm` write and `TZMODE_SET_UTC`. Both
57
+ # selectors share the cfunc, so both must be blocked.
58
+ :gmtime, :utc
59
+ ]
60
+ }
61
+ )
62
+ end
63
+ end
64
+ end
@@ -744,6 +744,33 @@ module Rigor
744
744
  arg_types = call_arg_types(node)
745
745
  block_type = block_return_type_for(node, receiver, arg_types)
746
746
 
747
+ # v0.0.3 A — implicit-self calls prefer a same-named
748
+ # top-level `def` over RBS dispatch. Without this,
749
+ # a helper like `def select(...)` defined inside an
750
+ # `RSpec.describe ... do ... end` block mis-routes
751
+ # through `Enumerable#select` / `Object#select` and
752
+ # the caller observes `Array[Elem]` instead of the
753
+ # helper's actual return type. The check fires only
754
+ # for `node.receiver.nil?` (true implicit self), so
755
+ # explicit-receiver dispatch is unaffected.
756
+ local_def = node.receiver.nil? ? scope.top_level_def_for(node.name) : nil
757
+ if local_def
758
+ local_inference = infer_top_level_user_method(local_def, receiver, arg_types)
759
+ return local_inference if local_inference
760
+
761
+ # The local def matches by name but the
762
+ # parameter shape is too complex for the first-
763
+ # iteration binder (kwargs / optionals / rest).
764
+ # Returning `Dynamic[Top]` is the safest answer:
765
+ # we know RBS dispatch would be wrong (the
766
+ # method is user-defined and shadows whatever
767
+ # ancestor method the dispatch would find), and
768
+ # `Dynamic[Top]` propagates correctly through
769
+ # downstream call chains without surfacing
770
+ # misleading false-positive diagnostics.
771
+ return dynamic_top
772
+ end
773
+
747
774
  result = MethodDispatcher.dispatch(
748
775
  receiver_type: receiver,
749
776
  method_name: node.name,
@@ -786,6 +813,22 @@ module Rigor
786
813
  # - the inference is already in progress for this
787
814
  # (class, method, signature) tuple — recursion
788
815
  # safety net.
816
+ # v0.0.3 A — re-types a top-level (or DSL-block-nested)
817
+ # `def` discovered by `ScopeIndexer` under the
818
+ # `TOP_LEVEL_DEF_KEY` sentinel. Mirrors the
819
+ # `infer_user_method_return` shape but uses the
820
+ # current `scope.self_type` (or implicit `Object`)
821
+ # as the receiver carrier so the body's own self is
822
+ # consistent with the call site's. Returns nil when
823
+ # the parameter shape disqualifies the def, when the
824
+ # body is empty, or when a recursion cycle is
825
+ # detected.
826
+ def infer_top_level_user_method(def_node, receiver, arg_types)
827
+ infer_user_method_return(def_node, receiver, arg_types)
828
+ rescue StandardError
829
+ nil
830
+ end
831
+
789
832
  def try_user_method_inference(receiver, call_node, arg_types)
790
833
  return nil unless receiver.is_a?(Type::Nominal)
791
834
 
@@ -806,7 +849,11 @@ module Rigor
806
849
  body_scope = build_user_method_body_scope(def_node, receiver, arg_types)
807
850
  return nil if body_scope.nil?
808
851
 
809
- signature = [receiver.class_name, def_node.name, arg_types.map { |t| t.describe(:short) }]
852
+ # Recursion-guard signature. Uses `describe(:short)`
853
+ # so non-Nominal receivers (e.g. the implicit
854
+ # `Object` carrier used for top-level / DSL-block
855
+ # defs in v0.0.3 A) can participate without raising.
856
+ signature = [receiver.describe(:short), def_node.name, arg_types.map { |t| t.describe(:short) }]
810
857
  stack = (Thread.current[INFERENCE_GUARD_KEY] ||= [])
811
858
  return Type::Combinator.untyped if stack.include?(signature)
812
859