rigortype 0.4.4 → 0.4.5
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 +5 -5
- data/docs/handbook/11-sig-gen.md +90 -17
- data/docs/manual/02-cli-reference.md +39 -9
- data/docs/manual/03-configuration.md +10 -8
- data/docs/manual/04-diagnostics.md +16 -5
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +2 -1
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +8 -1
- data/docs/manual/18-removing-dead-code.md +18 -7
- data/docs/manual/plugins/rigor-rspec.md +101 -1
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +13 -4
- data/lib/rigor/analysis/check_rules.rb +228 -15
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +49 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +166 -24
- data/lib/rigor/analysis/path_expansion.rb +3 -1
- data/lib/rigor/analysis/reachability/graph.rb +90 -13
- data/lib/rigor/analysis/reachability/scan.rb +279 -39
- data/lib/rigor/analysis/rule_catalog.rb +17 -4
- data/lib/rigor/analysis/runner/pool_coordinator.rb +69 -16
- data/lib/rigor/analysis/runner.rb +69 -65
- data/lib/rigor/analysis/severity_stamp.rb +11 -0
- data/lib/rigor/analysis/worker_session.rb +12 -1
- data/lib/rigor/cache/descriptor.rb +40 -7
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +26 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/command.rb +3 -1
- data/lib/rigor/cli/docs_command.rb +3 -2
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +3 -2
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +32 -11
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +60 -15
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +29 -3
- data/lib/rigor/effects/signature_sources.rb +3 -1
- data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
- data/lib/rigor/environment/gated_signature_guard.rb +2 -1
- data/lib/rigor/environment/gated_signature_plan.rb +2 -1
- data/lib/rigor/environment/installed_gem_set.rb +2 -1
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
- data/lib/rigor/environment/rbs_loader.rb +21 -1
- data/lib/rigor/inference/block_call_timing.rb +7 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/captured_locals.rb +44 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/definer_resolution.rb +87 -20
- data/lib/rigor/inference/expression_typer.rb +35 -5
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher.rb +67 -3
- data/lib/rigor/inference/module_function_state.rb +137 -42
- data/lib/rigor/inference/narrowing.rb +54 -10
- data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
- data/lib/rigor/inference/project_patched_methods.rb +168 -4
- data/lib/rigor/inference/project_patched_mixins.rb +162 -0
- data/lib/rigor/inference/project_patched_scanner.rb +51 -12
- data/lib/rigor/inference/project_patched_visibility.rb +214 -0
- data/lib/rigor/inference/project_seed_tables.rb +120 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
- data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer.rb +675 -105
- data/lib/rigor/inference/self_call_ivar_writes.rb +11 -6
- data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
- data/lib/rigor/inference/statement_evaluator.rb +446 -139
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/lens/contract_reader.rb +45 -2
- data/lib/rigor/lens/document.rb +2 -1
- data/lib/rigor/lens/file_reader.rb +12 -2
- data/lib/rigor/lens/members.rb +16 -2
- data/lib/rigor/lens/run.rb +34 -7
- data/lib/rigor/mcp/server.rb +2 -2
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/protection/discovery_seed.rb +9 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +27 -0
- data/lib/rigor/scope/discovery_index.rb +5 -0
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
- data/lib/rigor/scope/resolution_chain.rb +40 -5
- data/lib/rigor/scope.rb +45 -2
- data/lib/rigor/sig_gen/alias_index.rb +2 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +167 -22
- data/lib/rigor/sig_gen/layout_index.rb +5 -2
- data/lib/rigor/sig_gen/observation_collector.rb +137 -11
- data/lib/rigor/sig_gen/renderer.rb +13 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- data/lib/rigor/sig_gen/write_reporter.rb +203 -0
- data/lib/rigor/sig_gen/write_result.rb +3 -1
- data/lib/rigor/sig_gen/writer.rb +5 -3
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/constant.rb +10 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
- data/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +4 -3
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +13 -1
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../type"
|
|
6
|
+
require_relative "reflective_ivar_write"
|
|
7
|
+
require_relative "refined_dispatch/own_method"
|
|
8
|
+
require_relative "self_call_ivar_writes"
|
|
9
|
+
|
|
10
|
+
module Rigor
|
|
11
|
+
module Inference
|
|
12
|
+
# The intervening-call reset of instance variables. An implicit-self or `self.` call may write any instance
|
|
13
|
+
# variable of `self`, so past one each instance variable whose binding is narrower than its class-ivar seed reads
|
|
14
|
+
# the union of the two again; a call proven to write only some ({SelfCallIvarWrites.of}, issue #1596) widens only
|
|
15
|
+
# those. A call on another receiver cannot reach `self`'s instance variables and widens nothing.
|
|
16
|
+
#
|
|
17
|
+
# {.widen} is the reset itself, which the statement evaluator applies past a statement call
|
|
18
|
+
# (`StatementEvaluator#invalidate_ivars_for_intervening_call`). Issue #1963 — {.after_operand} applies it past
|
|
19
|
+
# the calls in a condition operand, where a narrowing an earlier operand or clause made would otherwise survive
|
|
20
|
+
# them: `if @x.nil? && reset!` read `@x` as `nil` in the body although `reset!` assigns it.
|
|
21
|
+
module InterveningIvarReset
|
|
22
|
+
# Nodes whose body does not run where it is written, or runs on another `self`: a `def`, and a class or
|
|
23
|
+
# module body, where `@x` is the class's own instance variable.
|
|
24
|
+
NON_RUNNING_NODES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
25
|
+
# Issue #1402 — the nodes that run code the program does not show on `self`'s behalf: `yield` runs the
|
|
26
|
+
# method's block, and `super` the superclass's method on `self`.
|
|
27
|
+
UNKNOWN_CODE_NODES = [Prism::YieldNode, Prism::SuperNode, Prism::ForwardingSuperNode].freeze
|
|
28
|
+
# Issue #1402 — the names of a proc's call (`blk.call`, `blk.()`, `blk.yield`), which runs the block the method
|
|
29
|
+
# was given as `yield` does.
|
|
30
|
+
PROC_CALL_NAMES = Set[:call, :yield].freeze
|
|
31
|
+
# Issue #1402 — the calls whose block runs with `self` bound to the receiver: on a receiver that is provably
|
|
32
|
+
# another object ({.foreign_self_block?}), an instance variable the block writes, and a method it calls
|
|
33
|
+
# without a receiver, are that object's.
|
|
34
|
+
SELF_REBINDING_BLOCK_CALLS = Set[
|
|
35
|
+
:instance_eval, :instance_exec, :class_eval, :class_exec, :module_eval, :module_exec, :define_method,
|
|
36
|
+
:define_singleton_method
|
|
37
|
+
].freeze
|
|
38
|
+
# The classes whose instances may be a class or module object: a `self` of one of them may be the receiver of
|
|
39
|
+
# `Foo.class_eval`, and an instance of one of them may be a `self` that is a class object.
|
|
40
|
+
CLASS_OBJECT_CLASSES = Set["Class", "Module", "Object", "BasicObject"].freeze
|
|
41
|
+
private_constant :NON_RUNNING_NODES, :UNKNOWN_CODE_NODES, :PROC_CALL_NAMES, :SELF_REBINDING_BLOCK_CALLS,
|
|
42
|
+
:CLASS_OBJECT_CLASSES
|
|
43
|
+
|
|
44
|
+
module_function
|
|
45
|
+
|
|
46
|
+
# True when `node` is a call that may write `self`'s instance variables: an implicit-self or `self.` call
|
|
47
|
+
# ({.self_call?}). Issue #1402 — and a `yield`, a `super`, or a proc's call on a local ({.proc_call?}), which
|
|
48
|
+
# run code the program does not show; {SelfCallIvarWrites.of} proves no write set for any of them.
|
|
49
|
+
def candidate?(node)
|
|
50
|
+
return true if UNKNOWN_CODE_NODES.any? { |klass| node.is_a?(klass) }
|
|
51
|
+
return false unless node.is_a?(Prism::CallNode)
|
|
52
|
+
|
|
53
|
+
self_call?(node) || proc_call?(node)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Whether `node`, a call, has no receiver or `self` as its receiver.
|
|
57
|
+
def self_call?(node)
|
|
58
|
+
receiver = node.receiver
|
|
59
|
+
receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Issue #1402 — `blk.call` / `blk.()` / `blk.yield` on a local: the method's own `&blk`, or a proc it was
|
|
63
|
+
# handed, runs code that may call back into `self`, as `yield` does. Read by name, since an unannotated
|
|
64
|
+
# `&blk` binds `Dynamic`.
|
|
65
|
+
def proc_call?(node)
|
|
66
|
+
PROC_CALL_NAMES.include?(node.name) && node.receiver.is_a?(Prism::LocalVariableReadNode)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Issue #1402 — whether `call_node`'s block runs on an object that is provably not `self`: a
|
|
70
|
+
# {SELF_REBINDING_BLOCK_CALLS} call whose receiver, typed in `scope`, cannot be `self` ({.other_object?}).
|
|
71
|
+
# A receiver that may be `self` at run time (`obj = self; obj.instance_eval { … }`, a block parameter, an
|
|
72
|
+
# `Object` or `Dynamic` value, a class the analysis cannot order against `self`'s) is not foreign.
|
|
73
|
+
def foreign_self_block?(call_node, scope)
|
|
74
|
+
return false unless call_node.is_a?(Prism::CallNode) && SELF_REBINDING_BLOCK_CALLS.include?(call_node.name)
|
|
75
|
+
return false if self_call?(call_node)
|
|
76
|
+
|
|
77
|
+
other_object?(scope.type_of(call_node.receiver), scope)
|
|
78
|
+
rescue StandardError
|
|
79
|
+
false
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Whether a value of `type` cannot be `scope`'s `self`. Only an instance method of a class answers: `self` is
|
|
83
|
+
# then an instance of that class (one other than `Class`, `Module`, `Object` and `BasicObject`, and no module,
|
|
84
|
+
# declared in source or RBS), and a class object (`Singleton[X]`: `self.class`, a constant naming a class or
|
|
85
|
+
# module) is not one. Where `self` is a class object, or a module's instance (`module_function`, `extend
|
|
86
|
+
# self`), any receiver may be `self` — a subclass, the module itself — and none is foreign. An instance
|
|
87
|
+
# receiver is never foreign: its class may be a subclass of `self`'s that the environment cannot order.
|
|
88
|
+
def other_object?(type, scope)
|
|
89
|
+
self_type = scope.self_type
|
|
90
|
+
return false unless self_type.is_a?(Type::Nominal) && plain_class?(self_type.class_name, scope)
|
|
91
|
+
|
|
92
|
+
type.is_a?(Type::Singleton)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Whether `class_name` names a class whose instances are no class or module object, and not a module.
|
|
96
|
+
def plain_class?(class_name, scope)
|
|
97
|
+
name = class_name.to_s.delete_prefix("::")
|
|
98
|
+
!CLASS_OBJECT_CLASSES.include?(name) && !RefinedDispatch::OwnMethod.module_name?(scope, name) &&
|
|
99
|
+
!module_subclass?(name, scope)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Whether `name` may be a subclass of `Module` (`class Meta < Module`), whose instance is a module object.
|
|
103
|
+
# Read off the class's instance `Scope::ResolutionChain`, which resolves a superclass written relative to the
|
|
104
|
+
# subclass's namespace (`class Meta < Base` in `module NS`) as Ruby does: true when an entry is one the
|
|
105
|
+
# environment orders `:subclass` or `:equal` to `Module` (`Module` itself, `Class`, a project class whose
|
|
106
|
+
# signature declares it, an external superclass), when an external superclass names no class the
|
|
107
|
+
# environment knows (it may be `Module`), or when the chain is truncated or unsettled.
|
|
108
|
+
def module_subclass?(name, scope)
|
|
109
|
+
chain = Scope::ResolutionChain.for(scope, name, :instance, :methods)
|
|
110
|
+
return true if chain.truncated? || chain.unsettled?
|
|
111
|
+
|
|
112
|
+
environment = scope.environment
|
|
113
|
+
chain.search(scope) { |entry| module_entry?(entry, environment) || nil } ? true : false
|
|
114
|
+
end
|
|
115
|
+
private_class_method :module_subclass?
|
|
116
|
+
|
|
117
|
+
# Whether a chain entry makes the class a possible `Module` subclass: a project entry {.module_like?}, or an
|
|
118
|
+
# external superclass that names no class the environment knows, or one {.module_like?}. An external mixin
|
|
119
|
+
# (`include Enumerable`) does not.
|
|
120
|
+
def module_entry?(entry, environment)
|
|
121
|
+
return module_like?(entry.name.to_s, environment) unless entry.external?
|
|
122
|
+
return false unless entry.superclass_edge
|
|
123
|
+
|
|
124
|
+
known = entry.candidates.map { |candidate| candidate.to_s.delete_prefix("::") }
|
|
125
|
+
.select { |candidate| environment.class_known?(candidate) }
|
|
126
|
+
known.empty? || known.any? { |candidate| module_like?(candidate, environment) }
|
|
127
|
+
end
|
|
128
|
+
private_class_method :module_entry?
|
|
129
|
+
|
|
130
|
+
def module_like?(name, environment)
|
|
131
|
+
%i[subclass equal].include?(environment.class_ordering(name, "Module"))
|
|
132
|
+
end
|
|
133
|
+
private_class_method :module_like?
|
|
134
|
+
private_class_method :plain_class?
|
|
135
|
+
|
|
136
|
+
# Issue #1402 — `scope` past a statement call whose receiver or arguments run code the program does not
|
|
137
|
+
# show (`@items << yield`, `puts(yield)`, `log(blk.call)`, `items.each(&blk)`): {.widen} over every narrowed
|
|
138
|
+
# name, as past a statement `yield`. Every other call in them is the operand walk's.
|
|
139
|
+
def after_operands(call_node, scope)
|
|
140
|
+
return scope unless narrowed_ivars?(scope)
|
|
141
|
+
|
|
142
|
+
found = []
|
|
143
|
+
collect_calls(call_node.receiver, false, found, scope, unknown_only: true)
|
|
144
|
+
collect_calls(call_node.arguments, false, found, scope, unknown_only: true)
|
|
145
|
+
block = call_node.block
|
|
146
|
+
found << block if block.is_a?(Prism::BlockArgumentNode) && block.expression.is_a?(Prism::LocalVariableReadNode)
|
|
147
|
+
return scope if found.empty?
|
|
148
|
+
|
|
149
|
+
widen(scope, mark: true) { nil }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Issue #1402 — `scope` past a call on another receiver whose literal block may make a {.candidate?} call
|
|
153
|
+
# (`[1].each { reset }`, `items.each { yield }`): {.widen} over the names those calls may write, as
|
|
154
|
+
# {.after_operand} widens past the calls in a condition. In a block that runs on another object
|
|
155
|
+
# ({.foreign_self_block?}) only a `yield`, a `super` or a proc's call counts: its implicit-self calls are that
|
|
156
|
+
# object's, while the method's block runs as it always does. A literal-name
|
|
157
|
+
# `instance_variable_set` on `self` ({ReflectiveIvarWrite.literal_name}) writes the one ivar it names, which
|
|
158
|
+
# the block's captured-binding write-back already joins, so it is not counted.
|
|
159
|
+
def after_block(call_node, scope)
|
|
160
|
+
block = call_node.block
|
|
161
|
+
return scope unless block.is_a?(Prism::BlockNode)
|
|
162
|
+
return scope unless narrowed_ivars?(scope)
|
|
163
|
+
|
|
164
|
+
calls = []
|
|
165
|
+
if foreign_self_block?(call_node, scope)
|
|
166
|
+
collect_calls(block, false, calls, scope, unknown_only: true)
|
|
167
|
+
else
|
|
168
|
+
collect_calls(block, false, calls, scope)
|
|
169
|
+
calls.reject! { |call| ReflectiveIvarWrite.literal_name(call) }
|
|
170
|
+
end
|
|
171
|
+
return scope if calls.empty?
|
|
172
|
+
|
|
173
|
+
widen(scope, mark: true) { proven_writes(calls, scope) }
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# `scope` with each instance variable narrowed below its class-ivar seed widened back over the seed. The block
|
|
177
|
+
# answers the names the call may write, or nil for any; it runs at most once, and only when an instance
|
|
178
|
+
# variable is narrowed, since a proof costs a read of the callee. An instance variable whose binding carries
|
|
179
|
+
# the declaration mark keeps it (issue #1889). With `mark: true` a binding whose narrowing had ruled `nil` out
|
|
180
|
+
# takes the mark when widened, so a `nil` only the seed brings back is not fuel for
|
|
181
|
+
# `call.possible-nil-receiver` (ADR-58), as a declared one is not. A narrowing that kept `nil`
|
|
182
|
+
# (`return if @tag`, `@tag.nil?`) widens unmarked, so a read the program really makes on `nil` still reports.
|
|
183
|
+
def widen(scope, mark: false)
|
|
184
|
+
seed = class_ivar_seed(scope)
|
|
185
|
+
return scope if seed.nil? || seed.empty?
|
|
186
|
+
|
|
187
|
+
computed = false
|
|
188
|
+
writes = nil
|
|
189
|
+
seed.reduce(scope) do |widened, (name, seed_type)|
|
|
190
|
+
local_type = scope.ivar(name)
|
|
191
|
+
next widened if local_type.nil? || local_type == seed_type
|
|
192
|
+
|
|
193
|
+
unless computed
|
|
194
|
+
writes = yield
|
|
195
|
+
computed = true
|
|
196
|
+
end
|
|
197
|
+
next widened if writes && !writes.include?(name)
|
|
198
|
+
|
|
199
|
+
joined = Type::Combinator.union(local_type, seed_type)
|
|
200
|
+
widened = widened.with_ivar(name, joined)
|
|
201
|
+
marked = (mark && nil_free?(local_type)) || scope.declaration_sourced?(:ivar, name)
|
|
202
|
+
marked ? widened.seed_declaration_sourced_ivar(name, joined) : widened
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Issue #1963 — `scope` past the calls `node`, a condition operand, makes: {.widen} over the names every
|
|
207
|
+
# candidate call in it may write, or every name when one of them is not proven. A call's literal block may run,
|
|
208
|
+
# so its calls count; a `def`, a class or module body and (unless `run_lambdas`) a lambda literal run nothing
|
|
209
|
+
# on this `self` there.
|
|
210
|
+
#
|
|
211
|
+
# A binding whose narrowing ruled `nil` out takes the declaration mark when widened ({.widen}'s `mark:`): the
|
|
212
|
+
# reset forgets the narrowing without making the variable newly nil-reportable, since a call the proof cannot
|
|
213
|
+
# clear is far more often one that leaves the variable alone (`if @tag && items.any?` then `t = @tag;
|
|
214
|
+
# t.upcase`) than one that assigns `nil`. A `when` test passes `run_lambdas: true`, since its `===` runs a
|
|
215
|
+
# lambda literal's body.
|
|
216
|
+
def after_operand(node, scope, run_lambdas: false)
|
|
217
|
+
return scope unless narrowed_ivars?(scope)
|
|
218
|
+
|
|
219
|
+
calls = candidate_calls(node, run_lambdas, scope)
|
|
220
|
+
return scope if calls.empty?
|
|
221
|
+
|
|
222
|
+
widen(scope, mark: true) { proven_writes(calls, scope) }
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Whether a candidate call in `node` may write the instance variable `name`. A `when` test passes
|
|
226
|
+
# `run_lambdas: true`, since its `===` runs a lambda literal's body. A write to it in `node` is the caller's
|
|
227
|
+
# to check.
|
|
228
|
+
def may_write?(node, name, scope, run_lambdas: false)
|
|
229
|
+
calls = candidate_calls(node, run_lambdas, scope)
|
|
230
|
+
return false if calls.empty?
|
|
231
|
+
|
|
232
|
+
proven = proven_writes(calls, scope)
|
|
233
|
+
proven.nil? || proven.include?(name)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# The union of the names each call may write ({SelfCallIvarWrites.of}), or nil when one is not proven.
|
|
237
|
+
def proven_writes(calls, scope)
|
|
238
|
+
calls.reduce(Set.new) do |acc, call|
|
|
239
|
+
writes = SelfCallIvarWrites.of(call, scope)
|
|
240
|
+
break nil if writes.nil?
|
|
241
|
+
|
|
242
|
+
acc.merge(writes)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# The candidate calls under `node`, in source order.
|
|
247
|
+
def candidate_calls(node, run_lambdas, scope)
|
|
248
|
+
found = []
|
|
249
|
+
collect_calls(node, run_lambdas, found, scope)
|
|
250
|
+
found
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Issue #1402 — with `unknown_only: true` only the `yield`s, `super`s and proc calls ({.unknown_code?}) count,
|
|
254
|
+
# wherever they sit. A block that runs on another object ({.foreign_self_block?}) is read that way: its
|
|
255
|
+
# implicit-self calls are that object's, while the method's block runs as anywhere else.
|
|
256
|
+
def collect_calls(node, run_lambdas, found, scope, unknown_only: false)
|
|
257
|
+
return unless node.is_a?(Prism::Node)
|
|
258
|
+
return if NON_RUNNING_NODES.any? { |klass| node.is_a?(klass) }
|
|
259
|
+
return if !run_lambdas && node.is_a?(Prism::LambdaNode)
|
|
260
|
+
|
|
261
|
+
found << node if unknown_only ? unknown_code?(node) : candidate?(node)
|
|
262
|
+
node.compact_child_nodes.each do |child|
|
|
263
|
+
foreign = !unknown_only && child.is_a?(Prism::BlockNode) && foreign_self_block?(node, scope)
|
|
264
|
+
collect_calls(child, run_lambdas, found, scope, unknown_only: unknown_only || foreign)
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
private_class_method :collect_calls
|
|
268
|
+
|
|
269
|
+
# Issue #1402 — a `yield`, a `super` or a proc's call ({.proc_call?}).
|
|
270
|
+
def unknown_code?(node)
|
|
271
|
+
UNKNOWN_CODE_NODES.any? { |klass| node.is_a?(klass) } || (node.is_a?(Prism::CallNode) && proc_call?(node))
|
|
272
|
+
end
|
|
273
|
+
private_class_method :unknown_code?
|
|
274
|
+
|
|
275
|
+
def nil_free?(type)
|
|
276
|
+
Narrowing.narrow_non_nil(type) == type
|
|
277
|
+
end
|
|
278
|
+
private_class_method :nil_free?
|
|
279
|
+
|
|
280
|
+
# Whether any instance variable is bound narrower than its class-ivar seed: the only state {.widen} changes.
|
|
281
|
+
def narrowed_ivars?(scope)
|
|
282
|
+
seed = class_ivar_seed(scope)
|
|
283
|
+
return false if seed.nil? || seed.empty?
|
|
284
|
+
|
|
285
|
+
seed.any? do |name, seed_type|
|
|
286
|
+
local_type = scope.ivar(name)
|
|
287
|
+
!local_type.nil? && local_type != seed_type
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
private_class_method :narrowed_ivars?
|
|
291
|
+
|
|
292
|
+
def class_ivar_seed(scope)
|
|
293
|
+
self_type = scope.self_type
|
|
294
|
+
class_name =
|
|
295
|
+
case self_type
|
|
296
|
+
when Type::Nominal, Type::Singleton then self_type.class_name
|
|
297
|
+
end
|
|
298
|
+
class_name && scope.class_ivars_for(class_name)
|
|
299
|
+
end
|
|
300
|
+
private_class_method :class_ivar_seed
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
@@ -47,6 +47,25 @@ module Rigor
|
|
|
47
47
|
# same call, another plugin's included, is not consulted. A `:lexical` entry (issue #1667) keeps the caller's
|
|
48
48
|
# `self`, so its `self_type` is the calling scope's.
|
|
49
49
|
def match_for(scope:, call_node:, receiver_type:)
|
|
50
|
+
entry = first_matching_entry(scope, call_node, receiver_type)
|
|
51
|
+
return nil if entry.nil?
|
|
52
|
+
|
|
53
|
+
receiver_name = singleton_receiver_class_name(receiver_type) || nominal_receiver_class_name(receiver_type)
|
|
54
|
+
entry_match(entry, receiver_name, scope, scope.environment)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Whether `call_node`'s block runs as an instance method of the receiver class (issue #1768): its first matching
|
|
58
|
+
# entry binds `:receiver_instance`, so the block body's `self` is the instance {#match_for} narrows to and its
|
|
59
|
+
# `@x` writes are that instance's ivars, as a `def` body's are. A `:lexical` or named-`self_type` entry runs the
|
|
60
|
+
# block on another object, and no entry means no narrowing.
|
|
61
|
+
def runs_as_receiver_instance_method?(scope:, call_node:, receiver_type:)
|
|
62
|
+
entry = first_matching_entry(scope, call_node, receiver_type)
|
|
63
|
+
!entry.nil? && entry.self_type == Plugin::Macro::BlockAsMethod::SELF_TYPE_RECEIVER_INSTANCE
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The first registered entry that matches `call_node` on `receiver_type`, in (plugin registration, declaration)
|
|
67
|
+
# order, or nil.
|
|
68
|
+
def first_matching_entry(scope, call_node, receiver_type)
|
|
50
69
|
return nil if receiver_type.nil?
|
|
51
70
|
|
|
52
71
|
environment = scope&.environment
|
|
@@ -61,12 +80,7 @@ module Rigor
|
|
|
61
80
|
# (plugin registration, declaration) order; the method-name membership is guaranteed
|
|
62
81
|
# by the table key.
|
|
63
82
|
entries = registry.contribution_index.block_entries_for(call_node.name)
|
|
64
|
-
entries.
|
|
65
|
-
next unless entry_matches?(entry, singleton_name, nominal_name, call_node, scope, environment)
|
|
66
|
-
|
|
67
|
-
return entry_match(entry, singleton_name || nominal_name, scope, environment)
|
|
68
|
-
end
|
|
69
|
-
nil
|
|
83
|
+
entries.find { |entry| entry_matches?(entry, singleton_name, nominal_name, call_node, scope, environment) }
|
|
70
84
|
end
|
|
71
85
|
|
|
72
86
|
# The {Match} a matching entry contributes: a `:lexical` one keeps the calling scope's `self`, unknown where the
|
|
@@ -190,6 +190,35 @@ module Rigor
|
|
|
190
190
|
|
|
191
191
|
STRING_FOLD_BYTE_LIMIT = 4096
|
|
192
192
|
|
|
193
|
+
# #2115 — the numeric twin of `STRING_FOLD_BYTE_LIMIT`. A folded Integer (and each Integer part of a
|
|
194
|
+
# folded Rational / Complex) is at most this many bits, the largest bit length whose decimal
|
|
195
|
+
# rendering still fits in `STRING_FOLD_BYTE_LIMIT` digits: `2 ** 13_605` folds, `2 ** 13_606`
|
|
196
|
+
# (bit length 13_607) does not. Computing `2 ** 100_000_000` takes Ruby milliseconds, but
|
|
197
|
+
# rendering it in decimal — which describing, sorting and printing the `Constant` does — takes
|
|
198
|
+
# minutes. `**` / `pow` / `<<` / `>>` also check their operands before invoking, so the big value
|
|
199
|
+
# is never computed.
|
|
200
|
+
INTEGER_FOLD_BIT_LIMIT = (STRING_FOLD_BYTE_LIMIT * Math.log2(10)).floor
|
|
201
|
+
|
|
202
|
+
# #2115 — the most elements a fold walks out of a `Constant<Range>` receiver. A method that
|
|
203
|
+
# enumerates the range (`to_a`, `to_set`, `all?`, `include?` on a String range, …) declines when
|
|
204
|
+
# the range holds more elements, or is endless (`1..Float::INFINITY`); a method that reads only
|
|
205
|
+
# the endpoints (`RANGE_ENDPOINT_METHODS`) still folds whatever the range's size.
|
|
206
|
+
RANGE_ENUMERATION_LIMIT = 1024
|
|
207
|
+
|
|
208
|
+
# The `Range` methods whose CRuby body reads the endpoints without walking the elements, so they
|
|
209
|
+
# stay foldable on a range past `RANGE_ENUMERATION_LIMIT`. `include?` / `member?` join them on a
|
|
210
|
+
# numeric range, where CRuby answers them by `cover?`.
|
|
211
|
+
RANGE_ENDPOINT_METHODS = Set[
|
|
212
|
+
:begin, :end, :exclude_end?, :cover?, :===, :size, :to_s, :inspect, :==, :eql?, :hash
|
|
213
|
+
].freeze
|
|
214
|
+
RANGE_COVER_METHODS = Set[:include?, :member?].freeze
|
|
215
|
+
private_constant :RANGE_ENDPOINT_METHODS, :RANGE_COVER_METHODS
|
|
216
|
+
|
|
217
|
+
# A result `INTEGER_FOLD_BIT_LIMIT` keeps from becoming a `Constant`: the fold still knows its class,
|
|
218
|
+
# and answers `Nominal[class_name]` for it. Falling through to RBS instead would read `Integer#**`'s
|
|
219
|
+
# declared `Numeric`, on which `(2 ** 20_000).even?` is an undefined method.
|
|
220
|
+
WidenedResult = Data.define(:class_name)
|
|
221
|
+
|
|
193
222
|
# Input cartesian product hard cap. Keeps fold cost bounded even when the receiver and argument are
|
|
194
223
|
# both `Union[Constant…]`. 5 × 5 = 25 inputs is permitted; 6 × 6 = 36 is not. The user-facing payoff
|
|
195
224
|
# (a precise small enum) drops off fast past this range and CRuby method invocation cost adds up.
|
|
@@ -649,45 +678,41 @@ module Rigor
|
|
|
649
678
|
def range_constant_unary(range, method_name)
|
|
650
679
|
case method_name
|
|
651
680
|
when :to_a, :entries then range_to_a_tuple(range)
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
681
|
+
# #2115 — `Range#min` / `#max` / `#minmax` / `#size` are O(1) on Integer endpoints in CRuby, so
|
|
682
|
+
# none of these materialises the range. The no-arg `first` / `last` read the endpoints, not the
|
|
683
|
+
# elements: `first` is `begin` and `last` is `end` whether the range is empty, reversed or
|
|
684
|
+
# exclusive (`(5..1).first` is 5, `(1...10).last` is 10). Only `min` / `max` depend on emptiness
|
|
685
|
+
# and exclusivity (`(5..1).min` is nil, `(1...10).max` is 9).
|
|
686
|
+
when :first then Type::Combinator.constant_of(range.begin)
|
|
687
|
+
when :last then Type::Combinator.constant_of(range.end)
|
|
688
|
+
when :min then Type::Combinator.constant_of(range.min)
|
|
689
|
+
when :max then Type::Combinator.constant_of(range.max)
|
|
690
|
+
when :count, :size, :length then Type::Combinator.constant_of(range.size)
|
|
691
|
+
when :minmax then Type::Combinator.tuple_of(*range.minmax.map { |v| Type::Combinator.constant_of(v) })
|
|
656
692
|
# `range.sum` is closed-form (Gauss) for an integer range, so a huge range still costs O(1) and
|
|
657
|
-
# yields a single Integer — no materialisation
|
|
658
|
-
# by the Integer-endpoint guard in the caller.
|
|
659
|
-
when :sum then
|
|
693
|
+
# yields a single Integer — no materialisation; only `INTEGER_FOLD_BIT_LIMIT` applies. Endless
|
|
694
|
+
# ranges are already excluded by the Integer-endpoint guard in the caller.
|
|
695
|
+
when :sum then range_sum_constant(range)
|
|
660
696
|
end
|
|
661
697
|
end
|
|
662
698
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
699
|
+
# The Gauss sum of a range of large endpoints can still pass `INTEGER_FOLD_BIT_LIMIT`; it is then
|
|
700
|
+
# an `Integer` (both endpoints are).
|
|
701
|
+
def range_sum_constant(range)
|
|
702
|
+
sum = range.sum
|
|
703
|
+
return Type::Combinator.nominal_of("Integer") if oversized_number?(sum)
|
|
667
704
|
|
|
668
|
-
Type::Combinator.
|
|
705
|
+
Type::Combinator.constant_of(sum)
|
|
669
706
|
end
|
|
670
707
|
|
|
671
|
-
def
|
|
672
|
-
|
|
673
|
-
return
|
|
674
|
-
|
|
675
|
-
Type::Combinator.constant_of(edge == :first ? values.first : values.last)
|
|
676
|
-
end
|
|
708
|
+
def range_to_a_tuple(range)
|
|
709
|
+
# `Range#size` is O(1) on Integer endpoints: check the cardinality before materialising.
|
|
710
|
+
return nil if range.size > RANGE_TO_A_LIMIT
|
|
677
711
|
|
|
678
|
-
def range_minmax_tuple(range)
|
|
679
712
|
values = range.to_a
|
|
680
|
-
if values.empty?
|
|
681
|
-
return Type::Combinator.tuple_of(
|
|
682
|
-
Type::Combinator.constant_of(nil),
|
|
683
|
-
Type::Combinator.constant_of(nil)
|
|
684
|
-
)
|
|
685
|
-
end
|
|
713
|
+
return Type::Combinator.tuple_of if values.empty?
|
|
686
714
|
|
|
687
|
-
Type::Combinator.tuple_of(
|
|
688
|
-
Type::Combinator.constant_of(values.first),
|
|
689
|
-
Type::Combinator.constant_of(values.last)
|
|
690
|
-
)
|
|
715
|
+
Type::Combinator.tuple_of(*values.map { |v| Type::Combinator.constant_of(v) })
|
|
691
716
|
end
|
|
692
717
|
|
|
693
718
|
# `(1..10).first(3)` / `.take(3)` / `.last(3)` — the 1-arg head / tail forms. `first`/`last` already
|
|
@@ -1175,6 +1200,17 @@ module Rigor
|
|
|
1175
1200
|
# "Integer-ness" was already implied by the inputs (so the widening fallback only fires for
|
|
1176
1201
|
# arithmetic over integers).
|
|
1177
1202
|
def build_constant_type(values, source: nil)
|
|
1203
|
+
widened, values = values.partition { |value| value.is_a?(WidenedResult) }
|
|
1204
|
+
return build_scalar_constant_type(values, source) if widened.empty?
|
|
1205
|
+
|
|
1206
|
+
nominals = widened.map(&:class_name).uniq.map { |name| Type::Combinator.nominal_of(name) }
|
|
1207
|
+
return Type::Combinator.union(*nominals) if values.empty?
|
|
1208
|
+
|
|
1209
|
+
constants = build_scalar_constant_type(values, source)
|
|
1210
|
+
constants && Type::Combinator.union(constants, *nominals)
|
|
1211
|
+
end
|
|
1212
|
+
|
|
1213
|
+
def build_scalar_constant_type(values, source)
|
|
1178
1214
|
return nil if values.empty?
|
|
1179
1215
|
|
|
1180
1216
|
unique = values.uniq
|
|
@@ -1607,9 +1643,12 @@ module Rigor
|
|
|
1607
1643
|
# `flat_map`.
|
|
1608
1644
|
def invoke_binary(receiver_value, method_name, arg_value)
|
|
1609
1645
|
return nil unless safe?(receiver_value, method_name, arg_value)
|
|
1646
|
+
return nil if range_enumeration_blow_up?(receiver_value, method_name)
|
|
1610
1647
|
|
|
1611
|
-
|
|
1612
|
-
|
|
1648
|
+
widened = numeric_blow_up_class(receiver_value, method_name, arg_value)
|
|
1649
|
+
return [WidenedResult.new(widened)] if widened
|
|
1650
|
+
|
|
1651
|
+
fold_result(receiver_value.public_send(method_name, arg_value))
|
|
1613
1652
|
rescue StandardError
|
|
1614
1653
|
nil
|
|
1615
1654
|
end
|
|
@@ -1620,8 +1659,7 @@ module Rigor
|
|
|
1620
1659
|
def invoke_ternary(receiver_value, method_name, av0, av1)
|
|
1621
1660
|
return nil unless ternary_method_allowed?(receiver_value, method_name)
|
|
1622
1661
|
|
|
1623
|
-
|
|
1624
|
-
foldable_constant_value?(result) ? [result] : nil
|
|
1662
|
+
fold_result(receiver_value.public_send(method_name, av0, av1))
|
|
1625
1663
|
rescue StandardError
|
|
1626
1664
|
nil
|
|
1627
1665
|
end
|
|
@@ -1630,13 +1668,21 @@ module Rigor
|
|
|
1630
1668
|
def invoke_unary(receiver_value, method_name)
|
|
1631
1669
|
return nil unless unary_safe?(receiver_value, method_name)
|
|
1632
1670
|
return nil if string_unary_blow_up?(receiver_value, method_name)
|
|
1671
|
+
return nil if range_enumeration_blow_up?(receiver_value, method_name)
|
|
1633
1672
|
|
|
1634
|
-
|
|
1635
|
-
foldable_constant_value?(result) ? [result] : nil
|
|
1673
|
+
fold_result(receiver_value.public_send(method_name))
|
|
1636
1674
|
rescue StandardError
|
|
1637
1675
|
nil
|
|
1638
1676
|
end
|
|
1639
1677
|
|
|
1678
|
+
# `[result]` for a foldable value, `[WidenedResult]` for a number past `INTEGER_FOLD_BIT_LIMIT`, nil
|
|
1679
|
+
# for anything else.
|
|
1680
|
+
def fold_result(result)
|
|
1681
|
+
return [WidenedResult.new(result.class.name)] if oversized_number?(result)
|
|
1682
|
+
|
|
1683
|
+
foldable_constant_value?(result) ? [result] : nil
|
|
1684
|
+
end
|
|
1685
|
+
|
|
1640
1686
|
def unary_safe?(receiver_value, method_name)
|
|
1641
1687
|
return true if unary_ops_for(receiver_value).include?(method_name)
|
|
1642
1688
|
|
|
@@ -1775,7 +1821,44 @@ module Rigor
|
|
|
1775
1821
|
# with the widened class.
|
|
1776
1822
|
return false if value.is_a?(Float) && value.nan?
|
|
1777
1823
|
return false if value.is_a?(Complex) && complex_nan?(value)
|
|
1824
|
+
return false if oversized_number?(value)
|
|
1825
|
+
|
|
1826
|
+
true
|
|
1827
|
+
end
|
|
1828
|
+
|
|
1829
|
+
# True when an Integer, or an Integer part of a Rational / Complex, is past `INTEGER_FOLD_BIT_LIMIT`.
|
|
1830
|
+
def oversized_number?(value)
|
|
1831
|
+
case value
|
|
1832
|
+
when Integer then value.bit_length > INTEGER_FOLD_BIT_LIMIT
|
|
1833
|
+
when Rational then oversized_number?(value.numerator) || oversized_number?(value.denominator)
|
|
1834
|
+
when Complex then oversized_number?(value.real) || oversized_number?(value.imaginary)
|
|
1835
|
+
else false
|
|
1836
|
+
end
|
|
1837
|
+
end
|
|
1838
|
+
|
|
1839
|
+
# #2115 — true when `receiver_value` is a `Range` the method would walk element by element and the
|
|
1840
|
+
# range holds more than `RANGE_ENUMERATION_LIMIT` elements (an endless one never stops).
|
|
1841
|
+
def range_enumeration_blow_up?(receiver_value, method_name)
|
|
1842
|
+
return false unless receiver_value.is_a?(Range)
|
|
1843
|
+
return false if RANGE_ENDPOINT_METHODS.include?(method_name)
|
|
1844
|
+
return false if RANGE_COVER_METHODS.include?(method_name) && numeric_range?(receiver_value)
|
|
1845
|
+
|
|
1846
|
+
!range_within_enumeration_budget?(receiver_value)
|
|
1847
|
+
end
|
|
1778
1848
|
|
|
1849
|
+
def numeric_range?(range)
|
|
1850
|
+
range.begin.is_a?(Numeric) || range.end.is_a?(Numeric)
|
|
1851
|
+
end
|
|
1852
|
+
|
|
1853
|
+
# Takes at most `RANGE_ENUMERATION_LIMIT + 1` elements (`Range#first(n)` stops there, endless or
|
|
1854
|
+
# not). A range that cannot iterate at all (a Float or Time begin) raises, and so does every
|
|
1855
|
+
# enumerating method on it before doing any work, so it counts as within budget: the invoke's own
|
|
1856
|
+
# rescue declines an enumerating fold, and `(t0..t1).include?(t)`, which CRuby answers by `cover?`,
|
|
1857
|
+
# keeps folding. A beginless range raises `RangeError` here, which the invoke's own rescue turns into
|
|
1858
|
+
# a declined fold, as the enumerating method itself would raise.
|
|
1859
|
+
def range_within_enumeration_budget?(range)
|
|
1860
|
+
range.first(RANGE_ENUMERATION_LIMIT + 1).size <= RANGE_ENUMERATION_LIMIT
|
|
1861
|
+
rescue TypeError
|
|
1779
1862
|
true
|
|
1780
1863
|
end
|
|
1781
1864
|
|
|
@@ -1796,6 +1879,58 @@ module Rigor
|
|
|
1796
1879
|
true
|
|
1797
1880
|
end
|
|
1798
1881
|
|
|
1882
|
+
# #2115 — the result's class name when `**` / `pow` / `<<` / `>>` would produce a number past
|
|
1883
|
+
# `INTEGER_FOLD_BIT_LIMIT`, read from the operands so the huge value is never computed; nil
|
|
1884
|
+
# otherwise. `fold_result` applies the same bound to every other result after the fact.
|
|
1885
|
+
def numeric_blow_up_class(receiver_value, method_name, arg_value)
|
|
1886
|
+
return nil unless arg_value.is_a?(Integer)
|
|
1887
|
+
|
|
1888
|
+
case method_name
|
|
1889
|
+
when :**, :pow then power_blow_up_class(receiver_value, arg_value)
|
|
1890
|
+
when :<<, :>> then "Integer" if shift_blow_up?(receiver_value, method_name, arg_value)
|
|
1891
|
+
end
|
|
1892
|
+
end
|
|
1893
|
+
|
|
1894
|
+
# An Integer base to a negative power is a Rational (`2 ** -3 == (1/8)`); a Rational or Complex
|
|
1895
|
+
# base keeps its class.
|
|
1896
|
+
def power_blow_up_class(base, exponent)
|
|
1897
|
+
return nil unless power_blow_up?(base, exponent)
|
|
1898
|
+
return base.class.name unless base.is_a?(Integer)
|
|
1899
|
+
|
|
1900
|
+
exponent.negative? ? "Rational" : "Integer"
|
|
1901
|
+
end
|
|
1902
|
+
|
|
1903
|
+
# A lower bound on the result's bit length is `floor(log2 |base|) * |exponent|`; past the limit the
|
|
1904
|
+
# result is declined anyway. A Float base (or Float part) stays in Float arithmetic and never grows.
|
|
1905
|
+
def power_blow_up?(base, exponent)
|
|
1906
|
+
bits = power_base_bits(base)
|
|
1907
|
+
!bits.nil? && bits * exponent.abs > INTEGER_FOLD_BIT_LIMIT
|
|
1908
|
+
end
|
|
1909
|
+
|
|
1910
|
+
# `floor(log2 |base|)` for an Integer, the larger of its terms' for a Rational. A Complex part
|
|
1911
|
+
# counts one bit more: `(1+1i) ** n` grows although neither part is past 1.
|
|
1912
|
+
def power_base_bits(base)
|
|
1913
|
+
case base
|
|
1914
|
+
when Integer then base.abs.bit_length - 1
|
|
1915
|
+
when Rational then [base.numerator.abs.bit_length, base.denominator.bit_length].max - 1
|
|
1916
|
+
when Complex then complex_power_base_bits(base)
|
|
1917
|
+
end
|
|
1918
|
+
end
|
|
1919
|
+
|
|
1920
|
+
def complex_power_base_bits(base)
|
|
1921
|
+
parts = [base.real, base.imaginary]
|
|
1922
|
+
return nil unless parts.all? { |part| part.is_a?(Integer) || part.is_a?(Rational) }
|
|
1923
|
+
|
|
1924
|
+
parts.map { |part| power_base_bits(part) + 1 }.max
|
|
1925
|
+
end
|
|
1926
|
+
|
|
1927
|
+
def shift_blow_up?(receiver_value, method_name, arg_value)
|
|
1928
|
+
return false unless receiver_value.is_a?(Integer) && !receiver_value.zero?
|
|
1929
|
+
|
|
1930
|
+
shift = method_name == :<< ? arg_value : -arg_value
|
|
1931
|
+
receiver_value.bit_length + shift > INTEGER_FOLD_BIT_LIMIT
|
|
1932
|
+
end
|
|
1933
|
+
|
|
1799
1934
|
def ops_for(receiver_value)
|
|
1800
1935
|
case receiver_value
|
|
1801
1936
|
when Integer, Float then NUMERIC_BINARY
|