rigortype 0.1.17 → 0.1.19
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 +4 -4
- data/README.md +159 -222
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +24 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +29 -0
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +213 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +24 -1
- data/lib/rigor/analysis/check_rules.rb +275 -44
- data/lib/rigor/analysis/diagnostic.rb +8 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +581 -0
- data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +321 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
- data/lib/rigor/analysis/runner.rb +207 -1200
- data/lib/rigor/analysis/worker_session.rb +60 -11
- data/lib/rigor/bleeding_edge.rb +123 -0
- data/lib/rigor/cache/descriptor.rb +86 -8
- data/lib/rigor/cache/incremental_snapshot.rb +10 -4
- data/lib/rigor/cache/rbs_cache_producer.rb +5 -1
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +46 -13
- data/lib/rigor/cli/annotate_command.rb +100 -15
- data/lib/rigor/cli/check_command.rb +708 -0
- data/lib/rigor/cli/ci_detector.rb +94 -0
- data/lib/rigor/cli/diagnostic_formats.rb +345 -0
- data/lib/rigor/cli/plugins_command.rb +2 -4
- data/lib/rigor/cli/plugins_renderer.rb +0 -2
- data/lib/rigor/cli/prism_colorizer.rb +10 -3
- data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
- data/lib/rigor/cli/trace_command.rb +143 -0
- data/lib/rigor/cli/trace_renderer.rb +310 -0
- data/lib/rigor/cli/triage_command.rb +6 -3
- data/lib/rigor/cli/triage_renderer.rb +15 -1
- data/lib/rigor/cli.rb +21 -612
- data/lib/rigor/configuration/severity_profile.rb +13 -1
- data/lib/rigor/configuration.rb +66 -7
- data/lib/rigor/environment/rbs_loader.rb +78 -68
- data/lib/rigor/environment.rb +1 -1
- data/lib/rigor/inference/acceptance.rb +10 -0
- data/lib/rigor/inference/body_fixpoint.rb +89 -0
- data/lib/rigor/inference/budget_trace.rb +29 -2
- data/lib/rigor/inference/expression_typer.rb +1080 -105
- data/lib/rigor/inference/flow_tracer.rb +180 -0
- data/lib/rigor/inference/macro_block_self_type.rb +11 -12
- data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +54 -14
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +148 -10
- data/lib/rigor/inference/method_dispatcher.rb +187 -55
- data/lib/rigor/inference/method_parameter_binder.rb +56 -2
- data/lib/rigor/inference/multi_target_binder.rb +46 -3
- data/lib/rigor/inference/mutation_widening.rb +142 -0
- data/lib/rigor/inference/narrowing.rb +330 -37
- data/lib/rigor/inference/scope_indexer.rb +770 -39
- data/lib/rigor/inference/statement_evaluator.rb +998 -68
- data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
- data/lib/rigor/plugin/additional_initializer.rb +61 -38
- data/lib/rigor/plugin/base.rb +517 -120
- data/lib/rigor/plugin/macro/block_as_method.rb +22 -21
- data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
- data/lib/rigor/plugin/macro.rb +2 -3
- data/lib/rigor/plugin/manifest.rb +4 -24
- data/lib/rigor/plugin/node_rule_walk.rb +192 -0
- data/lib/rigor/plugin/registry.rb +264 -35
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/conformance_checker.rb +86 -1
- data/lib/rigor/scope/discovery_index.rb +60 -0
- data/lib/rigor/scope.rb +199 -204
- data/lib/rigor/sig_gen/generator.rb +8 -0
- data/lib/rigor/sig_gen/observation_collector.rb +6 -6
- data/lib/rigor/source/literals.rb +14 -0
- data/lib/rigor/triage/catalogue.rb +4 -19
- data/lib/rigor/triage.rb +69 -1
- data/lib/rigor/type/combinator.rb +34 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +0 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +13 -29
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +27 -90
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +90 -51
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -29
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +11 -40
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +21 -34
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +11 -18
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +12 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +37 -31
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +8 -29
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +108 -36
- data/sig/rigor/analysis/fact_store.rbs +3 -0
- data/sig/rigor/environment.rbs +0 -2
- data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
- data/sig/rigor/inference.rbs +5 -0
- data/sig/rigor/plugin/base.rbs +6 -4
- data/sig/rigor/plugin/manifest.rbs +1 -2
- data/sig/rigor/scope.rbs +50 -29
- data/sig/rigor/source.rbs +1 -0
- data/sig/rigor/type.rbs +1 -0
- data/sig/rigor.rbs +1 -1
- data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
- data/skills/rigor-ci-setup/SKILL.md +319 -0
- data/skills/rigor-plugin-author/SKILL.md +6 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
- metadata +21 -3
- data/lib/rigor/cache/rbs_instance_definitions.rb +0 -66
- data/lib/rigor/plugin/macro/external_file.rb +0 -143
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Inference
|
|
5
|
+
# Thread-local event recorder behind `rigor trace`: while a block runs
|
|
6
|
+
# under {record}, the inference engine emits a flat, ordered event
|
|
7
|
+
# stream describing HOW it typed the program — expression enter/result
|
|
8
|
+
# pairs, scope binds, union formation, and method-dispatch outcomes.
|
|
9
|
+
# The CLI replays that stream as a terminal animation (or dumps it as
|
|
10
|
+
# JSON); the engine itself never reads the events back, so recording
|
|
11
|
+
# is purely observational and MUST NOT change any inferred type.
|
|
12
|
+
#
|
|
13
|
+
# Modelled on {Analysis::DependencyRecorder}: thread-local state, a
|
|
14
|
+
# module-level activation count so the disabled fast path ({active?})
|
|
15
|
+
# is a plain integer read, and a frozen snapshot for consumers. The
|
|
16
|
+
# instrumented hot paths (`ExpressionTyper#type_of`,
|
|
17
|
+
# `Scope#with_local`, `Type::Combinator.union`,
|
|
18
|
+
# `MethodDispatcher.dispatch`) each guard their emit behind {active?},
|
|
19
|
+
# so a normal (non-tracing) run pays one integer comparison.
|
|
20
|
+
module FlowTracer
|
|
21
|
+
KEY = :__rigor_flow_tracer__
|
|
22
|
+
private_constant :KEY
|
|
23
|
+
|
|
24
|
+
# One animation-relevant moment.
|
|
25
|
+
#
|
|
26
|
+
# kind :enter | :result | :bind | :union | :dispatch
|
|
27
|
+
# depth expression-recursion depth at emit time (0 = statement level)
|
|
28
|
+
# location frozen Hash with :start_line/:start_column/:end_line/
|
|
29
|
+
# :end_column/:start_offset/:end_offset, or nil. Events
|
|
30
|
+
# without a node of their own (:bind, :union) inherit the
|
|
31
|
+
# innermost in-flight expression node's location so the
|
|
32
|
+
# replayer can still highlight the source being evaluated.
|
|
33
|
+
# stack frozen Array of short node-class names, outermost first
|
|
34
|
+
# data frozen kind-specific Hash (types pre-rendered as Strings
|
|
35
|
+
# via `describe(:short)` so events serialise to JSON as-is)
|
|
36
|
+
Event = Data.define(:kind, :depth, :location, :stack, :data)
|
|
37
|
+
|
|
38
|
+
# Mutable per-thread accumulator; only ever touched by the thread
|
|
39
|
+
# that activated it, so no locking is needed on the emit path.
|
|
40
|
+
class Recorder
|
|
41
|
+
attr_reader :events
|
|
42
|
+
|
|
43
|
+
def initialize
|
|
44
|
+
@events = []
|
|
45
|
+
@stack = []
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Brackets one `ExpressionTyper#type_of` recursion: emits :enter,
|
|
49
|
+
# runs the real inference, emits :result with the inferred type,
|
|
50
|
+
# and returns the type unchanged.
|
|
51
|
+
def node(node)
|
|
52
|
+
location = location_of(node)
|
|
53
|
+
name = short_name(node.class)
|
|
54
|
+
emit(:enter, location: location, data: { node: name })
|
|
55
|
+
@stack.push(node)
|
|
56
|
+
result = nil
|
|
57
|
+
begin
|
|
58
|
+
result = yield
|
|
59
|
+
ensure
|
|
60
|
+
@stack.pop
|
|
61
|
+
end
|
|
62
|
+
emit(:result, location: location, data: { node: name, type: FlowTracer.describe(result) })
|
|
63
|
+
result
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def emit(kind, location: nil, data: {})
|
|
67
|
+
@events << Event.new(
|
|
68
|
+
kind: kind,
|
|
69
|
+
depth: @stack.size,
|
|
70
|
+
location: (location || current_location)&.freeze,
|
|
71
|
+
stack: @stack.map { |n| short_name(n.class) }.freeze,
|
|
72
|
+
data: data.freeze
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def current_location
|
|
79
|
+
location_of(@stack.last)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def location_of(node)
|
|
83
|
+
return nil unless node.respond_to?(:location) && node.location
|
|
84
|
+
|
|
85
|
+
loc = node.location
|
|
86
|
+
{
|
|
87
|
+
start_line: loc.start_line, start_column: loc.start_column,
|
|
88
|
+
end_line: loc.end_line, end_column: loc.end_column,
|
|
89
|
+
start_offset: loc.start_offset, end_offset: loc.end_offset
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def short_name(klass)
|
|
94
|
+
klass.name.to_s.split("::").last
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
@active_count = 0
|
|
99
|
+
@mutex = Mutex.new
|
|
100
|
+
|
|
101
|
+
module_function
|
|
102
|
+
|
|
103
|
+
# Activates recording on the current thread for the duration of the
|
|
104
|
+
# block and returns the frozen event list. Nests safely; restores
|
|
105
|
+
# the previous recorder on exit.
|
|
106
|
+
def record
|
|
107
|
+
previous = Thread.current[KEY]
|
|
108
|
+
recorder = Recorder.new
|
|
109
|
+
Thread.current[KEY] = recorder
|
|
110
|
+
@mutex.synchronize { @active_count += 1 }
|
|
111
|
+
yield
|
|
112
|
+
recorder.events.freeze
|
|
113
|
+
ensure
|
|
114
|
+
Thread.current[KEY] = previous
|
|
115
|
+
@mutex.synchronize { @active_count -= 1 }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Plain integer read (GVL-atomic) — the disabled fast path.
|
|
119
|
+
def active?
|
|
120
|
+
@active_count.positive?
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Brackets one expression-typing recursion. Falls through to the
|
|
124
|
+
# bare block when the current thread is not recording (another
|
|
125
|
+
# thread may have flipped {active?}).
|
|
126
|
+
def trace_node(node, &)
|
|
127
|
+
recorder = Thread.current[KEY]
|
|
128
|
+
return yield unless recorder
|
|
129
|
+
|
|
130
|
+
recorder.node(node, &)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# `Scope#with_local` — the moment a local enters the scope.
|
|
134
|
+
def bind(name, type)
|
|
135
|
+
Thread.current[KEY]&.emit(:bind, data: { name: name.to_s, type: describe(type) })
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# `Type::Combinator.union` — the moment branch types merge
|
|
139
|
+
# (including degenerate collapses like `1 | 1 → 1`).
|
|
140
|
+
def union(members, result)
|
|
141
|
+
Thread.current[KEY]&.emit(
|
|
142
|
+
:union,
|
|
143
|
+
data: { members: members.map { |m| describe(m) }.freeze, type: describe(result) }
|
|
144
|
+
)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# `MethodDispatcher.dispatch` — resolution or the fail-soft `nil`
|
|
148
|
+
# ("no rule matched"; the caller will widen to `Dynamic[Top]`).
|
|
149
|
+
def dispatch(receiver:, method_name:, args:, result:, location: nil)
|
|
150
|
+
recorder = Thread.current[KEY]
|
|
151
|
+
return unless recorder
|
|
152
|
+
|
|
153
|
+
recorder.emit(
|
|
154
|
+
:dispatch,
|
|
155
|
+
location: location && location_hash(location),
|
|
156
|
+
data: {
|
|
157
|
+
receiver: describe(receiver), method: method_name.to_s,
|
|
158
|
+
args: args.map { |a| describe(a) }.freeze,
|
|
159
|
+
type: result && describe(result), resolved: !result.nil?
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def describe(type)
|
|
165
|
+
return "nil" if type.nil?
|
|
166
|
+
return type.describe(:short) if type.respond_to?(:describe)
|
|
167
|
+
|
|
168
|
+
type.inspect
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def location_hash(loc)
|
|
172
|
+
{
|
|
173
|
+
start_line: loc.start_line, start_column: loc.start_column,
|
|
174
|
+
end_line: loc.end_line, end_column: loc.end_column,
|
|
175
|
+
start_offset: loc.start_offset, end_offset: loc.end_offset
|
|
176
|
+
}
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -17,7 +17,7 @@ module Rigor
|
|
|
17
17
|
# `MyApp.get(...)` call);
|
|
18
18
|
# - the underlying class `X` equals or inherits from the
|
|
19
19
|
# entry's `receiver_constraint`;
|
|
20
|
-
# - the call's method name is in the entry's `
|
|
20
|
+
# - the call's method name is in the entry's `method_names`.
|
|
21
21
|
#
|
|
22
22
|
# On a match the helper returns the **instance** type of
|
|
23
23
|
# the receiver class (`Nominal[X]`) — the narrowed
|
|
@@ -51,11 +51,16 @@ module Rigor
|
|
|
51
51
|
receiver_class_name = singleton_receiver_class_name(receiver_type)
|
|
52
52
|
return nil if receiver_class_name.nil?
|
|
53
53
|
|
|
54
|
-
verb
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
# ADR-52 WD1 — the verb-keyed table compiled at registry build
|
|
55
|
+
# replaces the per-call plugins × block_as_methods linear scan.
|
|
56
|
+
# Entries arrive in (plugin registration, declaration) order, so
|
|
57
|
+
# the first ancestry match below is the same entry the previous
|
|
58
|
+
# walk returned; the method-name membership the old `matches?` checked
|
|
59
|
+
# is guaranteed by the table key.
|
|
60
|
+
entries = registry.contribution_index.block_entries_for(call_node.name)
|
|
61
|
+
entries.each do |entry|
|
|
62
|
+
if receiver_class_inherits_from?(receiver_class_name, entry.receiver_constraint, environment)
|
|
63
|
+
return instance_type_for(receiver_class_name, environment)
|
|
59
64
|
end
|
|
60
65
|
end
|
|
61
66
|
nil
|
|
@@ -73,12 +78,6 @@ module Rigor
|
|
|
73
78
|
receiver_type.class_name
|
|
74
79
|
end
|
|
75
80
|
|
|
76
|
-
def matches?(entry, verb, receiver_class_name, environment)
|
|
77
|
-
return false unless entry.verbs.include?(verb)
|
|
78
|
-
|
|
79
|
-
receiver_class_inherits_from?(receiver_class_name, entry.receiver_constraint, environment)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
81
|
def receiver_class_inherits_from?(class_name, constraint, environment)
|
|
83
82
|
return true if class_name == constraint
|
|
84
83
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module MethodDispatcher
|
|
8
|
+
# `Array#to_h { |x| [k, v] }` (and the no-block-pair tuple form's
|
|
9
|
+
# block sibling) return-type fold.
|
|
10
|
+
#
|
|
11
|
+
# `Enumerable#to_h` with a block maps every element to a
|
|
12
|
+
# `[key, value]` pair and collects them into a Hash. When the
|
|
13
|
+
# block's inferred return type is a recognizable 2-element
|
|
14
|
+
# `Tuple` (`[K, V]`), this tier projects the pair into a
|
|
15
|
+
# `Hash[K, V]` nominal whose key/value parameters are the
|
|
16
|
+
# widened pair types. Without this fold the call hits the RBS
|
|
17
|
+
# generic and the block's `[K, V]` return is dropped, typing as
|
|
18
|
+
# `Hash[Dynamic[top], Dynamic[top]]`.
|
|
19
|
+
#
|
|
20
|
+
# Value-pinned constants in the pair (`Constant[2]`) are widened
|
|
21
|
+
# to their nominal (`Integer`) for the Hash parameters: the built
|
|
22
|
+
# hash holds many keys, so pinning the parameter to a single
|
|
23
|
+
# element's literal would be unsound for the aggregate. The same
|
|
24
|
+
# widening the loop-body fixpoint applies (`Combinator#
|
|
25
|
+
# widen_value_pinned`) is reused.
|
|
26
|
+
#
|
|
27
|
+
# Declines (returns `nil`, leaving today's RBS answer) when:
|
|
28
|
+
#
|
|
29
|
+
# - there is no block at the call site,
|
|
30
|
+
# - the block return type is not a 2-element `Tuple`, or
|
|
31
|
+
# - the receiver is not an Array-shaped carrier (`Tuple` or
|
|
32
|
+
# `Array` nominal). Hash receivers keep their existing
|
|
33
|
+
# `ShapeDispatch#hash_to_h` identity fold.
|
|
34
|
+
module ArrayToHFolding
|
|
35
|
+
module_function
|
|
36
|
+
|
|
37
|
+
def try_dispatch(context)
|
|
38
|
+
return nil unless context.method_name == :to_h
|
|
39
|
+
|
|
40
|
+
block_type = context.block_type
|
|
41
|
+
return nil unless block_type.is_a?(Type::Tuple)
|
|
42
|
+
return nil unless block_type.elements.size == 2
|
|
43
|
+
return nil unless array_shaped?(context.receiver)
|
|
44
|
+
|
|
45
|
+
key = Type::Combinator.widen_value_pinned(block_type.elements[0])
|
|
46
|
+
value = Type::Combinator.widen_value_pinned(block_type.elements[1])
|
|
47
|
+
Type::Combinator.nominal_of("Hash", type_args: [key, value])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def array_shaped?(receiver)
|
|
51
|
+
case receiver
|
|
52
|
+
when Type::Tuple then true
|
|
53
|
+
when Type::Nominal then receiver.class_name == "Array"
|
|
54
|
+
else false
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -226,13 +226,54 @@ module Rigor
|
|
|
226
226
|
case type
|
|
227
227
|
when Type::Constant then [type.value]
|
|
228
228
|
when Type::Union
|
|
229
|
-
return
|
|
230
|
-
|
|
231
|
-
|
|
229
|
+
return type.members.map(&:value) if type.members.all?(Type::Constant)
|
|
230
|
+
|
|
231
|
+
# A union that mixes `Constant<Integer>` and `IntegerRange`
|
|
232
|
+
# members (e.g. an accumulator's running fixpoint assumption
|
|
233
|
+
# `1 | int<1, 6>`) folds as the bounding interval. The
|
|
234
|
+
# range-arithmetic path (`try_fold_binary_range`) then keeps
|
|
235
|
+
# the result an `IntegerRange` instead of bailing to Dynamic.
|
|
236
|
+
union_integer_bounds(type)
|
|
232
237
|
when Type::IntegerRange then type
|
|
233
238
|
end
|
|
234
239
|
end
|
|
235
240
|
|
|
241
|
+
# Returns the bounding `IntegerRange` over a union whose members
|
|
242
|
+
# are each an Integer `Constant` or an `IntegerRange`; `nil`
|
|
243
|
+
# otherwise (a Float constant or any non-numeric member declines,
|
|
244
|
+
# so precision is never silently lost).
|
|
245
|
+
def union_integer_bounds(union)
|
|
246
|
+
lowers = []
|
|
247
|
+
uppers = []
|
|
248
|
+
union.members.each do |member|
|
|
249
|
+
case member
|
|
250
|
+
when Type::Constant
|
|
251
|
+
return nil unless member.value.is_a?(Integer)
|
|
252
|
+
|
|
253
|
+
lowers << member.value
|
|
254
|
+
uppers << member.value
|
|
255
|
+
when Type::IntegerRange
|
|
256
|
+
lowers << member.lower
|
|
257
|
+
uppers << member.upper
|
|
258
|
+
else
|
|
259
|
+
return nil
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
# `IntegerRange#lower`/`#upper` surface an unbounded edge as
|
|
263
|
+
# `±Float::INFINITY`; `integer_range` wants the `±∞` *sentinel*,
|
|
264
|
+
# so map the extremum back.
|
|
265
|
+
Type::Combinator.integer_range(infinity_to_sentinel(lowers.min),
|
|
266
|
+
infinity_to_sentinel(uppers.max))
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def infinity_to_sentinel(bound)
|
|
270
|
+
case bound
|
|
271
|
+
when -Float::INFINITY then Type::IntegerRange::NEG_INFINITY
|
|
272
|
+
when Float::INFINITY then Type::IntegerRange::POS_INFINITY
|
|
273
|
+
else bound
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
236
277
|
def try_fold_unary(set, method_name)
|
|
237
278
|
case set
|
|
238
279
|
when Array then try_fold_unary_set(set, method_name)
|
|
@@ -1265,17 +1306,16 @@ module Rigor
|
|
|
1265
1306
|
end
|
|
1266
1307
|
end
|
|
1267
1308
|
|
|
1268
|
-
# `String#reverse` / `#swapcase` etc. produce a
|
|
1269
|
-
#
|
|
1270
|
-
#
|
|
1271
|
-
#
|
|
1272
|
-
#
|
|
1273
|
-
#
|
|
1274
|
-
#
|
|
1275
|
-
#
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
false
|
|
1309
|
+
# `String#reverse` / `#swapcase` / `#succ` etc. produce a string
|
|
1310
|
+
# at least as large as the receiver. The binary `:+` / `:*` paths
|
|
1311
|
+
# have their own `string_blow_up?` output guard; this is the unary
|
|
1312
|
+
# analogue — decline to fold a unary String op whose receiver is
|
|
1313
|
+
# already at or beyond `STRING_FOLD_BYTE_LIMIT`, since the folded
|
|
1314
|
+
# output would be just as large and constant-materialising it buys
|
|
1315
|
+
# no precision worth the bytes. Non-String receivers never blow up
|
|
1316
|
+
# through a unary op, so they pass.
|
|
1317
|
+
def string_unary_blow_up?(receiver_value, _method_name)
|
|
1318
|
+
receiver_value.is_a?(String) && receiver_value.bytesize >= STRING_FOLD_BYTE_LIMIT
|
|
1279
1319
|
end
|
|
1280
1320
|
|
|
1281
1321
|
# Scalar / String / Symbol values fold; everything
|
|
@@ -32,7 +32,16 @@ module Rigor
|
|
|
32
32
|
# matches, accept the first arity-and-gradual-accept match
|
|
33
33
|
# (the v0.1.1 behaviour). Alias / Interface / Intersection
|
|
34
34
|
# params still reach this pass, so call sites whose only
|
|
35
|
-
# candidate IS an alias-typed overload keep working.
|
|
35
|
+
# candidate IS an alias-typed overload keep working. One
|
|
36
|
+
# exclusion: an `untyped` argument does NOT gradually match
|
|
37
|
+
# a value-pinning param (`nil` / literal types — carriers
|
|
38
|
+
# that admit only specific values). Those overloads carry
|
|
39
|
+
# value-precise returns (`Kernel#Array: (nil) -> []`,
|
|
40
|
+
# `Regexp#=~: (nil) -> nil`) that would otherwise win purely
|
|
41
|
+
# by list position and inject false constants into the flow;
|
|
42
|
+
# they remain selectable when the argument PROVES the value
|
|
43
|
+
# (strict pass) or when no other overload matches (step 4's
|
|
44
|
+
# fallback picks the first overload regardless).
|
|
36
45
|
# 4. If no overload matches at all, fall back to
|
|
37
46
|
# `method_types.first` so existing call sites keep their
|
|
38
47
|
# phase 1 / 2b behavior. This preserves the fail-soft
|
|
@@ -393,9 +402,32 @@ module Rigor
|
|
|
393
402
|
instance_type: instance_type,
|
|
394
403
|
type_vars: type_vars
|
|
395
404
|
)
|
|
405
|
+
# An `untyped` arg gradually accepts against every param,
|
|
406
|
+
# so a value-pinning param would be "matched" with zero
|
|
407
|
+
# evidence and its value-precise return (`(nil) -> []`)
|
|
408
|
+
# would beat broader overloads purely by list position.
|
|
409
|
+
# Decline the pair; only the strict pass (where the arg
|
|
410
|
+
# proves the value) or the final first-overload fallback
|
|
411
|
+
# may select such an overload. (Pass 1 already skips
|
|
412
|
+
# untyped args entirely, so this only engages pass 2.)
|
|
413
|
+
return false if untyped_arg?(arg) && value_pinning?(param_type)
|
|
414
|
+
|
|
396
415
|
result = param_type.accepts(arg, mode: :gradual)
|
|
397
416
|
result.yes? || result.maybe?
|
|
398
417
|
end
|
|
418
|
+
|
|
419
|
+
# A type that admits only specific VALUES rather than a
|
|
420
|
+
# class of values: a `Constant` carrier (RBS `nil` and
|
|
421
|
+
# literal types both translate to one) or a union made up
|
|
422
|
+
# entirely of them (`true | false`, `1 | 2`, `nil?`-style
|
|
423
|
+
# optionals of literals).
|
|
424
|
+
def value_pinning?(type)
|
|
425
|
+
case type
|
|
426
|
+
when Type::Constant then true
|
|
427
|
+
when Type::Union then type.members.all? { |member| value_pinning?(member) }
|
|
428
|
+
else false
|
|
429
|
+
end
|
|
430
|
+
end
|
|
399
431
|
end
|
|
400
432
|
end
|
|
401
433
|
end
|