rigortype 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +373 -0
  3. data/README.md +152 -0
  4. data/exe/rigor +9 -0
  5. data/lib/rigor/analysis/check_rules.rb +503 -0
  6. data/lib/rigor/analysis/diagnostic.rb +35 -0
  7. data/lib/rigor/analysis/fact_store.rb +133 -0
  8. data/lib/rigor/analysis/result.rb +29 -0
  9. data/lib/rigor/analysis/runner.rb +119 -0
  10. data/lib/rigor/ast/type_node.rb +41 -0
  11. data/lib/rigor/ast.rb +22 -0
  12. data/lib/rigor/cli/type_of_command.rb +160 -0
  13. data/lib/rigor/cli/type_of_renderer.rb +88 -0
  14. data/lib/rigor/cli/type_scan_command.rb +160 -0
  15. data/lib/rigor/cli/type_scan_renderer.rb +165 -0
  16. data/lib/rigor/cli/type_scan_report.rb +32 -0
  17. data/lib/rigor/cli.rb +195 -0
  18. data/lib/rigor/configuration.rb +49 -0
  19. data/lib/rigor/environment/class_registry.rb +141 -0
  20. data/lib/rigor/environment/rbs_hierarchy.rb +64 -0
  21. data/lib/rigor/environment/rbs_loader.rb +244 -0
  22. data/lib/rigor/environment.rb +177 -0
  23. data/lib/rigor/inference/acceptance.rb +444 -0
  24. data/lib/rigor/inference/block_parameter_binder.rb +198 -0
  25. data/lib/rigor/inference/closure_escape_analyzer.rb +191 -0
  26. data/lib/rigor/inference/coverage_scanner.rb +85 -0
  27. data/lib/rigor/inference/expression_typer.rb +831 -0
  28. data/lib/rigor/inference/fallback.rb +35 -0
  29. data/lib/rigor/inference/fallback_tracer.rb +64 -0
  30. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +102 -0
  31. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +169 -0
  32. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +421 -0
  33. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +336 -0
  34. data/lib/rigor/inference/method_dispatcher.rb +213 -0
  35. data/lib/rigor/inference/method_parameter_binder.rb +257 -0
  36. data/lib/rigor/inference/multi_target_binder.rb +143 -0
  37. data/lib/rigor/inference/narrowing.rb +1008 -0
  38. data/lib/rigor/inference/rbs_type_translator.rb +219 -0
  39. data/lib/rigor/inference/scope_indexer.rb +468 -0
  40. data/lib/rigor/inference/statement_evaluator.rb +1017 -0
  41. data/lib/rigor/rbs_extended.rb +98 -0
  42. data/lib/rigor/scope.rb +340 -0
  43. data/lib/rigor/source/node_locator.rb +104 -0
  44. data/lib/rigor/source/node_walker.rb +37 -0
  45. data/lib/rigor/source.rb +15 -0
  46. data/lib/rigor/testing.rb +65 -0
  47. data/lib/rigor/trinary.rb +108 -0
  48. data/lib/rigor/type/accepts_result.rb +109 -0
  49. data/lib/rigor/type/bot.rb +57 -0
  50. data/lib/rigor/type/combinator.rb +148 -0
  51. data/lib/rigor/type/constant.rb +90 -0
  52. data/lib/rigor/type/dynamic.rb +60 -0
  53. data/lib/rigor/type/hash_shape.rb +246 -0
  54. data/lib/rigor/type/nominal.rb +83 -0
  55. data/lib/rigor/type/singleton.rb +65 -0
  56. data/lib/rigor/type/top.rb +56 -0
  57. data/lib/rigor/type/tuple.rb +84 -0
  58. data/lib/rigor/type/union.rb +65 -0
  59. data/lib/rigor/type.rb +23 -0
  60. data/lib/rigor/version.rb +5 -0
  61. data/lib/rigor.rb +29 -0
  62. data/sig/rigor/analysis/fact_store.rbs +51 -0
  63. data/sig/rigor/ast.rbs +11 -0
  64. data/sig/rigor/environment.rbs +59 -0
  65. data/sig/rigor/inference.rbs +151 -0
  66. data/sig/rigor/rbs_extended.rbs +22 -0
  67. data/sig/rigor/scope.rbs +49 -0
  68. data/sig/rigor/source.rbs +20 -0
  69. data/sig/rigor/testing.rbs +9 -0
  70. data/sig/rigor/trinary.rbs +29 -0
  71. data/sig/rigor/type.rbs +171 -0
  72. data/sig/rigor.rbs +70 -0
  73. metadata +260 -0
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Inference
5
+ FALLBACK_FAMILIES = %i[prism virtual].freeze
6
+ private_constant :FALLBACK_FAMILIES
7
+
8
+ # Immutable value object recorded by the typer whenever Scope#type_of
9
+ # falls back to Dynamic[Top] for a node it does not yet recognise. The
10
+ # contract for emitting these events lives in
11
+ # docs/internal-spec/inference-engine.md (Fail-Soft Policy).
12
+ #
13
+ # Fields:
14
+ # - node_class: the Ruby class of the node that triggered the
15
+ # fallback (e.g. Prism::CallNode, or a Rigor::AST::Node subclass).
16
+ # - location: the Prism source location for real Prism nodes, or
17
+ # nil for synthetic nodes.
18
+ # - family: :prism for real Prism nodes, :virtual for nodes
19
+ # that include Rigor::AST::Node.
20
+ # - inner_type: the Rigor::Type returned to the caller (currently
21
+ # always Dynamic[Top]; later slices may carry richer fallback
22
+ # types).
23
+ Fallback = Data.define(:node_class, :location, :family, :inner_type) do
24
+ def initialize(node_class:, location:, family:, inner_type:)
25
+ raise ArgumentError, "node_class must be a Class, got #{node_class.class}" unless node_class.is_a?(Class)
26
+
27
+ unless FALLBACK_FAMILIES.include?(family)
28
+ raise ArgumentError, "family must be one of #{FALLBACK_FAMILIES.inspect}, got #{family.inspect}"
29
+ end
30
+
31
+ super
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "fallback"
4
+
5
+ module Rigor
6
+ module Inference
7
+ # Mutable observer that accumulates Rigor::Inference::Fallback events
8
+ # emitted by the type-inference engine. Pass an instance to
9
+ # Rigor::Scope#type_of(node, tracer: ...) to record every fail-soft
10
+ # fallback. The tracer MUST NOT change the return value of type_of;
11
+ # see docs/internal-spec/inference-engine.md (Fail-Soft Policy).
12
+ #
13
+ # Future slices may add additional record_* methods (e.g.
14
+ # record_dispatch_miss for Slice 3, record_budget_cutoff for Slice 5);
15
+ # the namespaced method names exist so a single tracer can collect
16
+ # multiple event families without confusing them.
17
+ class FallbackTracer
18
+ def initialize
19
+ @events = []
20
+ end
21
+
22
+ def events
23
+ @events.dup.freeze
24
+ end
25
+
26
+ def record_fallback(event)
27
+ raise ArgumentError, "expected Rigor::Inference::Fallback, got #{event.class}" unless event.is_a?(Fallback)
28
+
29
+ @events << event
30
+ self
31
+ end
32
+
33
+ def empty?
34
+ @events.empty?
35
+ end
36
+
37
+ def size
38
+ @events.size
39
+ end
40
+
41
+ def each(&block)
42
+ return @events.each unless block
43
+
44
+ @events.each(&block)
45
+ self
46
+ end
47
+
48
+ include Enumerable
49
+
50
+ def kinds
51
+ @events.map(&:node_class).uniq
52
+ end
53
+
54
+ def families
55
+ @events.map(&:family).uniq
56
+ end
57
+
58
+ def clear
59
+ @events.clear
60
+ self
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../type"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module MethodDispatcher
8
+ # Slice 2 rule book that folds binary operations on `Rigor::Type::Constant`
9
+ # receivers into another `Constant` whenever:
10
+ #
11
+ # * the receiver is a recognised scalar literal,
12
+ # * exactly one argument is supplied and it is also a `Constant`,
13
+ # * the method name is in the curated whitelist for the receiver's class,
14
+ # * the operation cannot accidentally explode the analyzer (we cap
15
+ # string-fold output at `STRING_FOLD_BYTE_LIMIT` bytes), and
16
+ # * the actual Ruby invocation does not raise.
17
+ #
18
+ # Anything else returns `nil`, signalling "no rule matched" so the
19
+ # caller (`ExpressionTyper`) falls back to `Dynamic[Top]` and records a
20
+ # fail-soft event. Slice 4 (RBS-backed) layers another dispatch tier
21
+ # behind this rule book, but the constant-folding semantics defined
22
+ # here MUST NOT regress: any value reachable by literal arithmetic at
23
+ # parse time is meant to be foldable independent of RBS data.
24
+ module ConstantFolding
25
+ module_function
26
+
27
+ NUMERIC_BINARY = Set[:+, :-, :*, :/, :%, :<, :<=, :>, :>=, :==, :!=, :<=>].freeze
28
+ STRING_BINARY = Set[:+, :*, :==, :!=, :<, :<=, :>, :>=, :<=>].freeze
29
+ SYMBOL_BINARY = Set[:==, :!=, :<=>, :<, :<=, :>, :>=].freeze
30
+ BOOL_BINARY = Set[:&, :|, :^, :==, :!=].freeze
31
+ NIL_BINARY = Set[:==, :!=].freeze
32
+
33
+ STRING_FOLD_BYTE_LIMIT = 4096
34
+
35
+ # @return [Rigor::Type::Constant, nil]
36
+ def try_fold(receiver:, method_name:, args:)
37
+ return nil unless receiver.is_a?(Type::Constant)
38
+ return nil if args.size != 1
39
+
40
+ arg = args.first
41
+ return nil unless arg.is_a?(Type::Constant)
42
+ return nil unless safe?(receiver.value, method_name, arg.value)
43
+
44
+ Type::Combinator.constant_of(receiver.value.public_send(method_name, arg.value))
45
+ rescue StandardError
46
+ nil
47
+ end
48
+
49
+ def safe?(receiver_value, method_name, arg_value)
50
+ ops = ops_for(receiver_value)
51
+ return false unless ops.include?(method_name)
52
+ return false if integer_division_by_zero?(receiver_value, method_name, arg_value)
53
+ return false if string_blow_up?(receiver_value, method_name, arg_value)
54
+
55
+ true
56
+ end
57
+
58
+ def ops_for(receiver_value)
59
+ case receiver_value
60
+ when Integer, Float then NUMERIC_BINARY
61
+ when String then STRING_BINARY
62
+ when Symbol then SYMBOL_BINARY
63
+ when true, false then BOOL_BINARY
64
+ when nil then NIL_BINARY
65
+ else Set.new
66
+ end
67
+ end
68
+
69
+ # Integer / 0 and Integer % 0 raise; Float / 0 and Float / 0.0 return
70
+ # Float::INFINITY or NaN, which are valid `Constant[Float]` values.
71
+ def integer_division_by_zero?(receiver_value, method_name, arg_value)
72
+ return false unless %i[/ %].include?(method_name)
73
+ return false unless receiver_value.is_a?(Integer)
74
+
75
+ arg_value.is_a?(Integer) && arg_value.zero?
76
+ end
77
+
78
+ def string_blow_up?(receiver_value, method_name, arg_value)
79
+ return false unless receiver_value.is_a?(String)
80
+
81
+ case method_name
82
+ when :+ then string_concat_blow_up?(receiver_value, arg_value)
83
+ when :* then string_repeat_blow_up?(receiver_value, arg_value)
84
+ else false
85
+ end
86
+ end
87
+
88
+ def string_concat_blow_up?(receiver_value, arg_value)
89
+ arg_value.is_a?(String) &&
90
+ receiver_value.bytesize + arg_value.bytesize > STRING_FOLD_BYTE_LIMIT
91
+ end
92
+
93
+ def string_repeat_blow_up?(receiver_value, arg_value)
94
+ return false unless arg_value.is_a?(Integer)
95
+ return true if arg_value.negative?
96
+
97
+ receiver_value.bytesize * arg_value > STRING_FOLD_BYTE_LIMIT
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../type"
4
+ require_relative "../acceptance"
5
+ require_relative "../rbs_type_translator"
6
+
7
+ module Rigor
8
+ module Inference
9
+ module MethodDispatcher
10
+ # Picks the RBS overload that should answer a call given the
11
+ # caller's actual argument types. Slice 4 phase 2c shape:
12
+ #
13
+ # 1. Filter overloads by positional arity (required, optional and
14
+ # rest_positionals are honored; required_keywords disqualify the
15
+ # overload because we do not yet thread keyword args through
16
+ # `call_arg_types`).
17
+ # 2. Within the arity-matching overloads, accept the first one
18
+ # whose every (param, arg) pair returns a `yes` or `maybe`
19
+ # answer from `Rigor::Type#accepts(arg, mode: :gradual)`.
20
+ # 3. If no overload matches, fall back to `method_types.first`
21
+ # so existing call sites keep their phase 1 / 2b behavior.
22
+ # This preserves the fail-soft invariant of the dispatcher.
23
+ #
24
+ # The selector is intentionally agnostic about the dispatch kind
25
+ # (instance vs singleton). Both kinds share the same arity and
26
+ # acceptance shape; the difference is only in which `Definition`
27
+ # the caller fetched.
28
+ module OverloadSelector
29
+ module_function
30
+
31
+ # @param method_definition [RBS::Definition::Method]
32
+ # @param arg_types [Array<Rigor::Type>] caller-provided types in
33
+ # positional order. Empty when there are no arguments.
34
+ # @param self_type [Rigor::Type] substitute for `Bases::Self`.
35
+ # @param instance_type [Rigor::Type] substitute for `Bases::Instance`.
36
+ # @param type_vars [Hash{Symbol => Rigor::Type}] substitution map
37
+ # for class-level type variables (Slice 4 phase 2d). The
38
+ # selector threads it through to {RbsTypeTranslator} so
39
+ # parameter types like `::Array[Elem]` substitute Elem before
40
+ # the accepts check, instead of degrading the param to
41
+ # `Array[Dynamic[Top]]`.
42
+ # @param block_required [Boolean] when `true`, only overloads
43
+ # that declare a block clause are considered (Slice 6 phase C
44
+ # sub-phase 1). The fallback also prefers a block-bearing
45
+ # overload over `method_types.first`. When `false` (the
46
+ # Slice 4 phase 2c default) the selector behaves exactly as
47
+ # before: `find` over arity-compatible overloads, falling
48
+ # back to the first declaration.
49
+ # @return [RBS::MethodType, nil] the chosen overload, or nil
50
+ # when the definition has no method types at all.
51
+ # rubocop:disable Metrics/ParameterLists
52
+ def select(method_definition, arg_types:, self_type:, instance_type:, type_vars: {}, block_required: false)
53
+ overloads = method_definition.method_types
54
+ return nil if overloads.empty?
55
+
56
+ match = find_matching_overload(
57
+ overloads,
58
+ arg_types: arg_types,
59
+ self_type: self_type,
60
+ instance_type: instance_type,
61
+ type_vars: type_vars,
62
+ block_required: block_required
63
+ )
64
+ return match if match
65
+ return overloads.find { |mt| overload_has_block?(mt) } if block_required
66
+
67
+ overloads.first
68
+ end
69
+ # rubocop:enable Metrics/ParameterLists
70
+
71
+ def overload_has_block?(method_type)
72
+ method_type.respond_to?(:block) && method_type.block
73
+ end
74
+
75
+ class << self
76
+ private
77
+
78
+ # rubocop:disable Metrics/ParameterLists
79
+ def find_matching_overload(overloads, arg_types:, self_type:, instance_type:, type_vars:, block_required:)
80
+ overloads.find do |method_type|
81
+ next false if block_required && !OverloadSelector.overload_has_block?(method_type)
82
+
83
+ matches?(
84
+ method_type,
85
+ arg_types,
86
+ self_type: self_type,
87
+ instance_type: instance_type,
88
+ type_vars: type_vars
89
+ )
90
+ end
91
+ end
92
+ # rubocop:enable Metrics/ParameterLists
93
+
94
+ def matches?(method_type, arg_types, self_type:, instance_type:, type_vars:)
95
+ return false if method_type.respond_to?(:type_params) && rejects_keyword_required?(method_type)
96
+
97
+ fun = method_type.type
98
+ return false unless arity_compatible?(fun, arg_types.size)
99
+
100
+ params = positional_params_for(fun, arg_types.size)
101
+ params.zip(arg_types).all? do |param, arg|
102
+ accepts_param?(
103
+ param,
104
+ arg,
105
+ self_type: self_type,
106
+ instance_type: instance_type,
107
+ type_vars: type_vars
108
+ )
109
+ end
110
+ end
111
+
112
+ # Slice 4 phase 2c does not pass keyword arguments through the
113
+ # call site (caller passes only positional `arg_types`). An
114
+ # overload that requires keywords is therefore not a viable
115
+ # candidate; we skip it instead of forcing a fallback.
116
+ def rejects_keyword_required?(method_type)
117
+ fun = method_type.type
118
+ return false unless fun.respond_to?(:required_keywords)
119
+
120
+ !fun.required_keywords.empty?
121
+ end
122
+
123
+ def arity_compatible?(fun, actual_count)
124
+ min_arity = fun.required_positionals.size + fun.trailing_positionals.size
125
+ return false if actual_count < min_arity
126
+
127
+ return true if fun.rest_positionals
128
+
129
+ max_arity = min_arity + fun.optional_positionals.size
130
+ actual_count <= max_arity
131
+ end
132
+
133
+ # Builds the list of formal parameter declarations to compare
134
+ # against the actual arguments, in positional order: required
135
+ # first, then as many optionals as needed, then trailing
136
+ # required. Rest_positionals consumes the remainder; we
137
+ # repeat its single declaration for each absorbed argument.
138
+ def positional_params_for(fun, actual_count)
139
+ required = fun.required_positionals
140
+ optional = fun.optional_positionals
141
+ rest = fun.rest_positionals
142
+ trailing = fun.trailing_positionals
143
+
144
+ head = required.dup
145
+ optional_needed = [actual_count - head.size - trailing.size, 0].max
146
+ head.concat(optional.first(optional_needed))
147
+
148
+ absorbed_by_rest = actual_count - head.size - trailing.size
149
+ head.concat([rest] * absorbed_by_rest) if rest && absorbed_by_rest.positive?
150
+
151
+ head.concat(trailing)
152
+ head
153
+ end
154
+
155
+ def accepts_param?(param, arg, self_type:, instance_type:, type_vars:)
156
+ param_type = RbsTypeTranslator.translate(
157
+ param.type,
158
+ self_type: self_type,
159
+ instance_type: instance_type,
160
+ type_vars: type_vars
161
+ )
162
+ result = param_type.accepts(arg, mode: :gradual)
163
+ result.yes? || result.maybe?
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end