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.
- checksums.yaml +7 -0
- data/LICENSE +373 -0
- data/README.md +152 -0
- data/exe/rigor +9 -0
- data/lib/rigor/analysis/check_rules.rb +503 -0
- data/lib/rigor/analysis/diagnostic.rb +35 -0
- data/lib/rigor/analysis/fact_store.rb +133 -0
- data/lib/rigor/analysis/result.rb +29 -0
- data/lib/rigor/analysis/runner.rb +119 -0
- data/lib/rigor/ast/type_node.rb +41 -0
- data/lib/rigor/ast.rb +22 -0
- data/lib/rigor/cli/type_of_command.rb +160 -0
- data/lib/rigor/cli/type_of_renderer.rb +88 -0
- data/lib/rigor/cli/type_scan_command.rb +160 -0
- data/lib/rigor/cli/type_scan_renderer.rb +165 -0
- data/lib/rigor/cli/type_scan_report.rb +32 -0
- data/lib/rigor/cli.rb +195 -0
- data/lib/rigor/configuration.rb +49 -0
- data/lib/rigor/environment/class_registry.rb +141 -0
- data/lib/rigor/environment/rbs_hierarchy.rb +64 -0
- data/lib/rigor/environment/rbs_loader.rb +244 -0
- data/lib/rigor/environment.rb +177 -0
- data/lib/rigor/inference/acceptance.rb +444 -0
- data/lib/rigor/inference/block_parameter_binder.rb +198 -0
- data/lib/rigor/inference/closure_escape_analyzer.rb +191 -0
- data/lib/rigor/inference/coverage_scanner.rb +85 -0
- data/lib/rigor/inference/expression_typer.rb +831 -0
- data/lib/rigor/inference/fallback.rb +35 -0
- data/lib/rigor/inference/fallback_tracer.rb +64 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +102 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +169 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +421 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +336 -0
- data/lib/rigor/inference/method_dispatcher.rb +213 -0
- data/lib/rigor/inference/method_parameter_binder.rb +257 -0
- data/lib/rigor/inference/multi_target_binder.rb +143 -0
- data/lib/rigor/inference/narrowing.rb +1008 -0
- data/lib/rigor/inference/rbs_type_translator.rb +219 -0
- data/lib/rigor/inference/scope_indexer.rb +468 -0
- data/lib/rigor/inference/statement_evaluator.rb +1017 -0
- data/lib/rigor/rbs_extended.rb +98 -0
- data/lib/rigor/scope.rb +340 -0
- data/lib/rigor/source/node_locator.rb +104 -0
- data/lib/rigor/source/node_walker.rb +37 -0
- data/lib/rigor/source.rb +15 -0
- data/lib/rigor/testing.rb +65 -0
- data/lib/rigor/trinary.rb +108 -0
- data/lib/rigor/type/accepts_result.rb +109 -0
- data/lib/rigor/type/bot.rb +57 -0
- data/lib/rigor/type/combinator.rb +148 -0
- data/lib/rigor/type/constant.rb +90 -0
- data/lib/rigor/type/dynamic.rb +60 -0
- data/lib/rigor/type/hash_shape.rb +246 -0
- data/lib/rigor/type/nominal.rb +83 -0
- data/lib/rigor/type/singleton.rb +65 -0
- data/lib/rigor/type/top.rb +56 -0
- data/lib/rigor/type/tuple.rb +84 -0
- data/lib/rigor/type/union.rb +65 -0
- data/lib/rigor/type.rb +23 -0
- data/lib/rigor/version.rb +5 -0
- data/lib/rigor.rb +29 -0
- data/sig/rigor/analysis/fact_store.rbs +51 -0
- data/sig/rigor/ast.rbs +11 -0
- data/sig/rigor/environment.rbs +59 -0
- data/sig/rigor/inference.rbs +151 -0
- data/sig/rigor/rbs_extended.rbs +22 -0
- data/sig/rigor/scope.rbs +49 -0
- data/sig/rigor/source.rbs +20 -0
- data/sig/rigor/testing.rbs +9 -0
- data/sig/rigor/trinary.rbs +29 -0
- data/sig/rigor/type.rbs +171 -0
- data/sig/rigor.rbs +70 -0
- 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"
|
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"
|