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,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../trinary"
4
+
5
+ module Rigor
6
+ module Type
7
+ # A hash shape with statically known keys. Inhabitants are Ruby
8
+ # `Hash` instances whose known entries inhabit the corresponding
9
+ # value types. RBS records correspond to the exact closed subset;
10
+ # Rigor extends that carrier with optional keys, read-only entry
11
+ # views, and an open/closed extra-key policy.
12
+ #
13
+ # Keys are restricted to Symbol and String values. Exact closed
14
+ # symbol-keyed shapes erase to the RBS record syntax
15
+ # `{ a: Integer, ?b: String }`; all other shapes degrade to
16
+ # `Hash[K, V]` or raw `Hash` when no useful bounds are available.
17
+ #
18
+ # Equality and hashing are structural over the (key -> Rigor::Type)
19
+ # pair set and policy fields. Hash insertion order is preserved by
20
+ # the underlying storage but does NOT affect equality (matching
21
+ # Ruby's `Hash#==`).
22
+ #
23
+ # See docs/type-specification/rbs-compatible-types.md (records) and
24
+ # docs/type-specification/rigor-extensions.md (hash shape).
25
+ # rubocop:disable Metrics/ClassLength
26
+ class HashShape
27
+ ALLOWED_KEY_CLASSES = [Symbol, String].freeze
28
+ EXTRA_KEY_POLICIES = %i[open closed].freeze
29
+ POLICY_KEYWORDS = %i[required_keys optional_keys read_only_keys extra_keys].freeze
30
+
31
+ attr_reader :pairs, :required_keys, :optional_keys, :read_only_keys, :extra_keys
32
+
33
+ # @param pairs [Hash{Symbol|String => Rigor::Type}] ordered map of
34
+ # keys to declared types. Keys MUST be Symbol or String;
35
+ # values MUST be Rigor::Type instances. The hash is duped and
36
+ # frozen at construction; callers MUST NOT mutate the input
37
+ # afterwards (mutation does not affect the carrier, but the
38
+ # carrier is a value object).
39
+ # @param required_keys [Array<Symbol|String>, nil] keys that MUST
40
+ # be present. When omitted, every non-optional key is required.
41
+ # When supplied without optional_keys, every remaining known key
42
+ # is treated as optional.
43
+ # @param optional_keys [Array<Symbol|String>, nil] keys that MAY
44
+ # be absent. Optional absence is not a stored nil.
45
+ # @param read_only_keys [Array<Symbol|String>] entries that cannot
46
+ # be written through this shape view.
47
+ # @param extra_keys [Symbol] :closed rejects keys outside pairs;
48
+ # :open permits them.
49
+ def initialize(pairs = nil, **keywords)
50
+ pairs, policy = split_constructor_args(pairs, keywords)
51
+ validate_pairs!(pairs)
52
+
53
+ @pairs = pairs.dup.freeze
54
+ apply_policy!(policy)
55
+ freeze
56
+ end
57
+
58
+ def describe(verbosity = :short)
59
+ return "{}" if pairs.empty?
60
+
61
+ rendered = pairs.map { |k, v| render_entry(k, v, verbosity) }
62
+ rendered << "..." if open?
63
+ "{ #{rendered.join(', ')} }"
64
+ end
65
+
66
+ # Erases to the RBS record form `{ a: Integer, ?b: String }`
67
+ # for exact closed symbol-keyed shapes. Open shapes and
68
+ # string-keyed closed shapes degrade to a generic Hash bound.
69
+ def erase_to_rbs
70
+ return "{}" if pairs.empty? && closed?
71
+ return hash_erasure unless closed?
72
+ return hash_erasure if pairs.each_key.any? { |k| !k.is_a?(Symbol) }
73
+
74
+ rendered = pairs.map { |k, v| "#{record_key(k)}: #{v.erase_to_rbs}" }
75
+ "{ #{rendered.join(', ')} }"
76
+ end
77
+
78
+ def open?
79
+ extra_keys == :open
80
+ end
81
+
82
+ def closed?
83
+ extra_keys == :closed
84
+ end
85
+
86
+ def required_key?(key)
87
+ required_keys.include?(key)
88
+ end
89
+
90
+ def optional_key?(key)
91
+ optional_keys.include?(key)
92
+ end
93
+
94
+ def read_only_key?(key)
95
+ read_only_keys.include?(key)
96
+ end
97
+
98
+ def top
99
+ Trinary.no
100
+ end
101
+
102
+ def bot
103
+ Trinary.no
104
+ end
105
+
106
+ def dynamic
107
+ Trinary.no
108
+ end
109
+
110
+ def accepts(other, mode: :gradual)
111
+ Inference::Acceptance.accepts(self, other, mode: mode)
112
+ end
113
+
114
+ def ==(other)
115
+ other.is_a?(HashShape) &&
116
+ pairs == other.pairs &&
117
+ required_keys == other.required_keys &&
118
+ optional_keys == other.optional_keys &&
119
+ read_only_keys == other.read_only_keys &&
120
+ extra_keys == other.extra_keys
121
+ end
122
+ alias eql? ==
123
+
124
+ def hash
125
+ [HashShape, pairs, required_keys, optional_keys, read_only_keys, extra_keys].hash
126
+ end
127
+
128
+ def inspect
129
+ "#<Rigor::Type::HashShape #{describe(:short)}>"
130
+ end
131
+
132
+ private
133
+
134
+ def split_constructor_args(pairs, keywords)
135
+ if pairs.nil?
136
+ policy = keywords.slice(*POLICY_KEYWORDS)
137
+ entries = keywords.except(*POLICY_KEYWORDS)
138
+ return [entries, policy]
139
+ end
140
+
141
+ unknown = keywords.keys - POLICY_KEYWORDS
142
+ raise ArgumentError, "unknown keywords: #{unknown.map(&:inspect).join(', ')}" unless unknown.empty?
143
+
144
+ [pairs, keywords]
145
+ end
146
+
147
+ def validate_pairs!(pairs)
148
+ raise ArgumentError, "pairs must be a Hash, got #{pairs.class}" unless pairs.is_a?(Hash)
149
+
150
+ pairs.each_key do |key|
151
+ next if ALLOWED_KEY_CLASSES.any? { |klass| key.is_a?(klass) }
152
+
153
+ raise ArgumentError, "HashShape keys must be Symbol or String, got #{key.class}"
154
+ end
155
+ end
156
+
157
+ def apply_policy!(policy)
158
+ extra_keys = policy.fetch(:extra_keys, :closed)
159
+ unless EXTRA_KEY_POLICIES.include?(extra_keys)
160
+ raise ArgumentError, "extra_keys must be :open or :closed, got #{extra_keys.inspect}"
161
+ end
162
+
163
+ @extra_keys = extra_keys
164
+ @required_keys, @optional_keys = classify_keys(
165
+ policy.fetch(:required_keys, nil),
166
+ policy.fetch(:optional_keys, nil)
167
+ )
168
+ @read_only_keys = canonical_key_list(policy.fetch(:read_only_keys, []), label: "read_only_keys")
169
+ end
170
+
171
+ def classify_keys(required_source, optional_source)
172
+ required, optional = key_sources(required_source, optional_source)
173
+ required_keys = canonical_key_list(required, label: "required_keys")
174
+ optional_keys = canonical_key_list(optional, label: "optional_keys")
175
+ validate_key_partition(required_keys, optional_keys)
176
+ [required_keys, optional_keys]
177
+ end
178
+
179
+ def key_sources(required_source, optional_source)
180
+ if required_source && optional_source.nil?
181
+ required = Array(required_source)
182
+ optional = pairs.keys - required
183
+ else
184
+ optional = optional_source.nil? ? [] : Array(optional_source)
185
+ required = required_source.nil? ? pairs.keys - optional : Array(required_source)
186
+ end
187
+
188
+ [required, optional]
189
+ end
190
+
191
+ def canonical_key_list(keys, label:)
192
+ keys = Array(keys)
193
+ raise ArgumentError, "#{label} must not contain duplicate keys" unless keys.uniq.size == keys.size
194
+
195
+ unknown = keys - pairs.keys
196
+ raise ArgumentError, "#{label} contains keys not present in pairs: #{unknown.inspect}" unless unknown.empty?
197
+
198
+ keys.sort_by { |key| [key.class.name, key.inspect] }.freeze
199
+ end
200
+
201
+ def validate_key_partition(required, optional)
202
+ overlap = required & optional
203
+ raise ArgumentError, "required_keys and optional_keys overlap: #{overlap.inspect}" unless overlap.empty?
204
+
205
+ missing = pairs.keys - (required + optional)
206
+ return if missing.empty?
207
+
208
+ raise ArgumentError, "keys must be classified as required or optional: #{missing.inspect}"
209
+ end
210
+
211
+ def render_entry(key, value, verbosity)
212
+ prefix = []
213
+ prefix << "readonly" if read_only_key?(key)
214
+ rendered_key = optional_key?(key) ? "?#{render_key(key)}" : render_key(key)
215
+ prefix << "#{rendered_key}:"
216
+ "#{prefix.join(' ')} #{value.describe(verbosity)}"
217
+ end
218
+
219
+ def render_key(key)
220
+ case key
221
+ when Symbol then key.to_s
222
+ when String then key.inspect
223
+ end
224
+ end
225
+
226
+ def record_key(key)
227
+ optional_key?(key) ? "?#{key}" : key.to_s
228
+ end
229
+
230
+ def hash_erasure
231
+ return "Hash[top, top]" if open?
232
+ return "Hash" if pairs.empty?
233
+
234
+ key_type = hash_erasure_key_type
235
+ value_type = Type::Combinator.union(*pairs.values)
236
+ "Hash[#{key_type.erase_to_rbs}, #{value_type.erase_to_rbs}]"
237
+ end
238
+
239
+ def hash_erasure_key_type
240
+ key_types = pairs.keys.map { |key| Type::Combinator.nominal_of(key.class) }
241
+ Type::Combinator.union(*key_types)
242
+ end
243
+ end
244
+ # rubocop:enable Metrics/ClassLength
245
+ end
246
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../trinary"
4
+
5
+ module Rigor
6
+ module Type
7
+ # An instance type for a Ruby class or module. The class is identified by
8
+ # its fully-qualified Ruby name; the registry attached to the
9
+ # environment owns the class lookup.
10
+ #
11
+ # Slice 4 phase 2d adds `type_args`: an ordered, frozen array of
12
+ # `Rigor::Type` values that carry the receiver's generic
13
+ # instantiation. The empty array is the canonical "raw" form
14
+ # (`Nominal[Array]`); a non-empty array represents an applied
15
+ # generic (`Nominal[Array, [Integer]]`). Two Nominals are
16
+ # structurally equal only when their `class_name` AND `type_args`
17
+ # match, so the raw form and any applied form are intentionally
18
+ # distinct values. Acceptance routes treat the raw form leniently
19
+ # for backward compatibility with phase 2b call sites that have not
20
+ # yet learned to carry generics.
21
+ #
22
+ # Type arguments MUST be `Rigor::Type` instances. The constructor
23
+ # freezes the array; callers MUST NOT mutate it after construction.
24
+ #
25
+ # See docs/type-specification/rbs-compatible-types.md.
26
+ class Nominal
27
+ attr_reader :class_name, :type_args
28
+
29
+ def initialize(class_name, type_args = [])
30
+ raise ArgumentError, "class_name must be a String, got #{class_name.class}" unless class_name.is_a?(String)
31
+ raise ArgumentError, "class_name must not be empty" if class_name.empty?
32
+ raise ArgumentError, "type_args must be an Array, got #{type_args.class}" unless type_args.is_a?(Array)
33
+
34
+ @class_name = class_name.freeze
35
+ @type_args = type_args.dup.freeze
36
+ freeze
37
+ end
38
+
39
+ def describe(verbosity = :short)
40
+ return class_name if type_args.empty?
41
+
42
+ rendered = type_args.map { |t| t.describe(verbosity) }.join(", ")
43
+ "#{class_name}[#{rendered}]"
44
+ end
45
+
46
+ def erase_to_rbs
47
+ return class_name if type_args.empty?
48
+
49
+ rendered = type_args.map(&:erase_to_rbs).join(", ")
50
+ "#{class_name}[#{rendered}]"
51
+ end
52
+
53
+ def top
54
+ Trinary.no
55
+ end
56
+
57
+ def bot
58
+ Trinary.no
59
+ end
60
+
61
+ def dynamic
62
+ Trinary.no
63
+ end
64
+
65
+ def accepts(other, mode: :gradual)
66
+ Inference::Acceptance.accepts(self, other, mode: mode)
67
+ end
68
+
69
+ def ==(other)
70
+ other.is_a?(Nominal) && class_name == other.class_name && type_args == other.type_args
71
+ end
72
+ alias eql? ==
73
+
74
+ def hash
75
+ [Nominal, class_name, type_args].hash
76
+ end
77
+
78
+ def inspect
79
+ "#<Rigor::Type::Nominal #{describe(:short)}>"
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../trinary"
4
+
5
+ module Rigor
6
+ module Type
7
+ # The singleton type for a Ruby class or module. Inhabitants are the
8
+ # class object itself (e.g. the constant `Foo`), not its instances.
9
+ # In RBS this corresponds to `singleton(Foo)`.
10
+ #
11
+ # `Singleton[Foo]` and `Nominal[Foo]` share the same `class_name` but
12
+ # are NEVER equal; they describe disjoint values (the class object vs.
13
+ # instances of the class).
14
+ #
15
+ # See docs/type-specification/rbs-compatible-types.md (singleton(T)).
16
+ class Singleton
17
+ attr_reader :class_name
18
+
19
+ def initialize(class_name)
20
+ raise ArgumentError, "class_name must be a String, got #{class_name.class}" unless class_name.is_a?(String)
21
+ raise ArgumentError, "class_name must not be empty" if class_name.empty?
22
+
23
+ @class_name = class_name.freeze
24
+ freeze
25
+ end
26
+
27
+ def describe(_verbosity = :short)
28
+ "singleton(#{class_name})"
29
+ end
30
+
31
+ def erase_to_rbs
32
+ "singleton(#{class_name})"
33
+ end
34
+
35
+ def top
36
+ Trinary.no
37
+ end
38
+
39
+ def bot
40
+ Trinary.no
41
+ end
42
+
43
+ def dynamic
44
+ Trinary.no
45
+ end
46
+
47
+ def accepts(other, mode: :gradual)
48
+ Inference::Acceptance.accepts(self, other, mode: mode)
49
+ end
50
+
51
+ def ==(other)
52
+ other.is_a?(Singleton) && class_name == other.class_name
53
+ end
54
+ alias eql? ==
55
+
56
+ def hash
57
+ [Singleton, class_name].hash
58
+ end
59
+
60
+ def inspect
61
+ "#<Rigor::Type::Singleton #{class_name}>"
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../trinary"
4
+
5
+ module Rigor
6
+ module Type
7
+ # The top of the value lattice: contains every value, including untyped
8
+ # boundaries. See docs/type-specification/special-types.md.
9
+ class Top
10
+ class << self
11
+ def instance
12
+ @instance ||= new.freeze
13
+ end
14
+
15
+ private :new
16
+ end
17
+
18
+ def describe(_verbosity = :short)
19
+ "top"
20
+ end
21
+
22
+ def erase_to_rbs
23
+ "top"
24
+ end
25
+
26
+ def top
27
+ Trinary.yes
28
+ end
29
+
30
+ def bot
31
+ Trinary.no
32
+ end
33
+
34
+ def dynamic
35
+ Trinary.no
36
+ end
37
+
38
+ def accepts(other, mode: :gradual)
39
+ Inference::Acceptance.accepts(self, other, mode: mode)
40
+ end
41
+
42
+ def ==(other)
43
+ other.is_a?(Top)
44
+ end
45
+ alias eql? ==
46
+
47
+ def hash
48
+ Top.hash
49
+ end
50
+
51
+ def inspect
52
+ "#<Rigor::Type::Top>"
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../trinary"
4
+
5
+ module Rigor
6
+ module Type
7
+ # A heterogeneous, fixed-arity array shape. Inhabitants are exactly
8
+ # the Ruby `Array` instances whose length matches `elements.size`
9
+ # and whose element at position `i` inhabits `elements[i]`.
10
+ #
11
+ # In RBS this corresponds to the tuple form `[A, B, C]`. A tuple
12
+ # is always a subtype of `Array[union(elements)]`; method dispatch
13
+ # therefore degrades to the underlying `Nominal[Array, [union]]`
14
+ # while acceptance keeps the per-position precision.
15
+ #
16
+ # Slice 5 phase 1 introduces the carrier and surfaces it from the
17
+ # `ArrayNode` literal handler when every element is a non-splat
18
+ # value. Tuple-aware refinements for `tuple[0]`, `tuple.first`, and
19
+ # destructuring assignment are deferred to Slice 5 phase 2; they
20
+ # will run as a higher-priority dispatch tier above
21
+ # {Rigor::Inference::MethodDispatcher::RbsDispatch}.
22
+ #
23
+ # Equality and hashing are structural across an ordered, frozen
24
+ # element list. The empty Tuple `Tuple[]` is permitted; the array
25
+ # literal handler keeps `[]` as raw `Nominal[Array]` (no element
26
+ # evidence to lock the arity), but external constructors MAY build
27
+ # `Tuple[]` directly when the zero-arity discipline is intended.
28
+ #
29
+ # See docs/type-specification/rbs-compatible-types.md (tuple) and
30
+ # docs/type-specification/rigor-extensions.md (hash-shape and
31
+ # tuple kin).
32
+ class Tuple
33
+ attr_reader :elements
34
+
35
+ def initialize(elements)
36
+ raise ArgumentError, "elements must be an Array, got #{elements.class}" unless elements.is_a?(Array)
37
+
38
+ @elements = elements.dup.freeze
39
+ freeze
40
+ end
41
+
42
+ def describe(verbosity = :short)
43
+ return "[]" if elements.empty?
44
+
45
+ "[#{elements.map { |t| t.describe(verbosity) }.join(', ')}]"
46
+ end
47
+
48
+ def erase_to_rbs
49
+ return "[]" if elements.empty?
50
+
51
+ "[#{elements.map(&:erase_to_rbs).join(', ')}]"
52
+ end
53
+
54
+ def top
55
+ Trinary.no
56
+ end
57
+
58
+ def bot
59
+ Trinary.no
60
+ end
61
+
62
+ def dynamic
63
+ Trinary.no
64
+ end
65
+
66
+ def accepts(other, mode: :gradual)
67
+ Inference::Acceptance.accepts(self, other, mode: mode)
68
+ end
69
+
70
+ def ==(other)
71
+ other.is_a?(Tuple) && elements == other.elements
72
+ end
73
+ alias eql? ==
74
+
75
+ def hash
76
+ [Tuple, elements].hash
77
+ end
78
+
79
+ def inspect
80
+ "#<Rigor::Type::Tuple #{describe(:short)}>"
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../trinary"
4
+
5
+ module Rigor
6
+ module Type
7
+ # A normalized non-empty union of two or more distinct types. Unions are
8
+ # constructed exclusively through Rigor::Type::Combinator.union, which
9
+ # flattens nested unions, deduplicates structurally-equal members, and
10
+ # collapses single-member or empty results to the appropriate scalar
11
+ # type. Direct calls to .new are an internal contract: callers MUST pass
12
+ # an already-normalized members array.
13
+ #
14
+ # See docs/type-specification/normalization.md.
15
+ class Union
16
+ attr_reader :members
17
+
18
+ def initialize(members)
19
+ unless members.is_a?(Array) && members.size >= 2
20
+ raise ArgumentError, "Union requires at least two members; use Combinator.union for normalization"
21
+ end
22
+
23
+ @members = members.freeze
24
+ freeze
25
+ end
26
+
27
+ def describe(verbosity = :short)
28
+ members.map { |m| m.describe(verbosity) }.join(" | ")
29
+ end
30
+
31
+ def erase_to_rbs
32
+ members.map(&:erase_to_rbs).join(" | ")
33
+ end
34
+
35
+ def top
36
+ Trinary.no
37
+ end
38
+
39
+ def bot
40
+ Trinary.no
41
+ end
42
+
43
+ def dynamic
44
+ members.any? { |m| m.respond_to?(:dynamic) && m.dynamic.yes? } ? Trinary.maybe : Trinary.no
45
+ end
46
+
47
+ def accepts(other, mode: :gradual)
48
+ Inference::Acceptance.accepts(self, other, mode: mode)
49
+ end
50
+
51
+ def ==(other)
52
+ other.is_a?(Union) && members == other.members
53
+ end
54
+ alias eql? ==
55
+
56
+ def hash
57
+ [Union, members].hash
58
+ end
59
+
60
+ def inspect
61
+ "#<Rigor::Type::Union #{describe(:short)}>"
62
+ end
63
+ end
64
+ end
65
+ end
data/lib/rigor/type.rb ADDED
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ # Documentation-only ducktype module that names the contract every Rigor
5
+ # type instance MUST satisfy. Concrete type classes do NOT include
6
+ # Rigor::Type; the ducktype is observed structurally.
7
+ #
8
+ # See docs/internal-spec/internal-type-api.md for the binding contract.
9
+ module Type
10
+ end
11
+ end
12
+
13
+ require_relative "type/top"
14
+ require_relative "type/bot"
15
+ require_relative "type/dynamic"
16
+ require_relative "type/nominal"
17
+ require_relative "type/singleton"
18
+ require_relative "type/constant"
19
+ require_relative "type/tuple"
20
+ require_relative "type/hash_shape"
21
+ require_relative "type/union"
22
+ require_relative "type/accepts_result"
23
+ require_relative "type/combinator"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ VERSION = "0.0.1"
5
+ end
data/lib/rigor.rb ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rigor/version"
4
+ require_relative "rigor/configuration"
5
+ require_relative "rigor/trinary"
6
+ require_relative "rigor/type"
7
+ require_relative "rigor/ast"
8
+ require_relative "rigor/environment"
9
+ require_relative "rigor/rbs_extended"
10
+ require_relative "rigor/testing"
11
+ require_relative "rigor/inference/fallback"
12
+ require_relative "rigor/inference/fallback_tracer"
13
+ require_relative "rigor/inference/acceptance"
14
+ require_relative "rigor/inference/rbs_type_translator"
15
+ require_relative "rigor/inference/method_dispatcher"
16
+ require_relative "rigor/inference/expression_typer"
17
+ require_relative "rigor/inference/method_parameter_binder"
18
+ require_relative "rigor/inference/closure_escape_analyzer"
19
+ require_relative "rigor/inference/statement_evaluator"
20
+ require_relative "rigor/scope"
21
+ require_relative "rigor/source"
22
+ require_relative "rigor/inference/scope_indexer"
23
+ require_relative "rigor/inference/coverage_scanner"
24
+ require_relative "rigor/analysis/fact_store"
25
+ require_relative "rigor/analysis/diagnostic"
26
+ require_relative "rigor/analysis/result"
27
+ require_relative "rigor/analysis/check_rules"
28
+ require_relative "rigor/analysis/runner"
29
+ require_relative "rigor/cli"