rigortype 0.4.7 → 0.4.8
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 +3 -1
- data/data/core_overlay/bigdecimal.rbs +33 -0
- data/data/core_overlay/resolv.rbs +30 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
- data/data/gem_overlay/addressable/addressable.rbs +40 -0
- data/docs/handbook/01-getting-started.md +1 -1
- data/docs/handbook/11-sig-gen.md +35 -0
- data/docs/install.md +20 -0
- data/docs/manual/01-installation.md +67 -26
- data/docs/manual/02-cli-reference.md +307 -91
- data/docs/manual/03-configuration.md +41 -3
- data/docs/manual/04-diagnostics.md +5 -4
- data/docs/manual/08-skills.md +11 -0
- data/docs/manual/09-editor-integration.md +8 -0
- data/docs/manual/14-rails-quickstart.md +1 -1
- data/docs/manual/19-effect-labels.md +3 -4
- data/docs/manual/plugins/rigor-actionpack.md +15 -6
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
- data/docs/manual/plugins/rigor-sorbet.md +17 -3
- data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
- data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
- data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
- data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules.rb +598 -99
- data/lib/rigor/analysis/crash_signature.rb +43 -1
- data/lib/rigor/analysis/dependency_recorder.rb +233 -76
- data/lib/rigor/analysis/incremental.rb +11 -1
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +279 -37
- data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
- data/lib/rigor/analysis/rule_catalog.rb +37 -1
- data/lib/rigor/analysis/run_cache_key.rb +27 -3
- data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
- data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
- data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
- data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
- data/lib/rigor/analysis/runner.rb +222 -39
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
- data/lib/rigor/analysis/worker_session.rb +42 -41
- data/lib/rigor/bleeding_edge.rb +19 -4
- data/lib/rigor/cache/file_digest.rb +39 -31
- data/lib/rigor/cache/incremental_snapshot.rb +66 -7
- data/lib/rigor/cache/rbs_descriptor.rb +37 -2
- data/lib/rigor/cache/rbs_environment.rb +5 -1
- data/lib/rigor/cli/baseline_command.rb +15 -2
- data/lib/rigor/cli/check_command.rb +4 -2
- data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
- data/lib/rigor/cli/lens_command.rb +17 -2
- data/lib/rigor/cli/sig_gen_command.rb +8 -0
- data/lib/rigor/cli/skill_describe.rb +1 -0
- data/lib/rigor/cli.rb +5 -2
- data/lib/rigor/config_audit.rb +32 -18
- data/lib/rigor/configuration/severity_profile.rb +9 -0
- data/lib/rigor/configuration.rb +32 -5
- data/lib/rigor/effects/collector.rb +53 -21
- data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
- data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
- data/lib/rigor/environment/default_libraries.rb +17 -0
- data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
- data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
- data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
- data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
- data/lib/rigor/environment/rbs_loader.rb +312 -63
- data/lib/rigor/environment.rb +131 -35
- data/lib/rigor/inference/acceptance.rb +50 -15
- data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
- data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
- data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
- data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
- data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
- data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
- data/lib/rigor/inference/block_auto_splat.rb +1 -1
- data/lib/rigor/inference/block_run_count.rb +9 -2
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
- data/lib/rigor/inference/captured_locals.rb +2 -0
- data/lib/rigor/inference/census_foreign_block.rb +354 -0
- data/lib/rigor/inference/clause_widening.rb +1 -1
- data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
- data/lib/rigor/inference/construction_escape.rb +56 -15
- data/lib/rigor/inference/declaration_walk.rb +4 -0
- data/lib/rigor/inference/def_node_resolver.rb +32 -14
- data/lib/rigor/inference/def_return_typer.rb +1 -1
- data/lib/rigor/inference/definer_resolution.rb +15 -7
- data/lib/rigor/inference/definers_index.rb +72 -16
- data/lib/rigor/inference/error_info.rb +16 -0
- data/lib/rigor/inference/expression_typer.rb +1459 -287
- data/lib/rigor/inference/flow_tracer.rb +31 -14
- data/lib/rigor/inference/fork_map.rb +3 -0
- data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
- data/lib/rigor/inference/global_write_census.rb +8 -6
- data/lib/rigor/inference/in_effect_refinements.rb +7 -0
- data/lib/rigor/inference/indexed_narrowing.rb +57 -8
- data/lib/rigor/inference/ivar_writer_census.rb +106 -11
- data/lib/rigor/inference/key_presence_guard.rb +51 -25
- data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
- data/lib/rigor/inference/last_line.rb +5 -2
- data/lib/rigor/inference/loop_value_record.rb +63 -15
- data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
- data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
- data/lib/rigor/inference/match_rebinding.rb +21 -8
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
- data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
- data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
- data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
- data/lib/rigor/inference/method_dispatcher.rb +66 -18
- data/lib/rigor/inference/method_parameter_binder.rb +136 -56
- data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
- data/lib/rigor/inference/module_function_state.rb +13 -2
- data/lib/rigor/inference/multi_target_binder.rb +1 -1
- data/lib/rigor/inference/mutation_widening.rb +44 -4
- data/lib/rigor/inference/narrowing.rb +201 -27
- data/lib/rigor/inference/operand_effects.rb +43 -2
- data/lib/rigor/inference/optimistic_origin.rb +133 -8
- data/lib/rigor/inference/origin_lookup.rb +1 -1
- data/lib/rigor/inference/precision_scanner.rb +1 -1
- data/lib/rigor/inference/project_method_ownership.rb +28 -10
- data/lib/rigor/inference/project_patched_methods.rb +65 -13
- data/lib/rigor/inference/project_patched_scanner.rb +120 -25
- data/lib/rigor/inference/project_patched_visibility.rb +39 -3
- data/lib/rigor/inference/rbs_type_translator.rb +68 -15
- data/lib/rigor/inference/receiver_writer_call.rb +67 -0
- data/lib/rigor/inference/recursion_rebinding.rb +157 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
- data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
- data/lib/rigor/inference/refined_dispatch.rb +13 -0
- data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
- data/lib/rigor/inference/remover_holder_scan.rb +76 -22
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
- data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
- data/lib/rigor/inference/scope_indexer.rb +2079 -183
- data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
- data/lib/rigor/inference/self_preserving_block.rb +87 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
- data/lib/rigor/inference/singleton_object_constant.rb +32 -2
- data/lib/rigor/inference/statement_evaluator.rb +970 -250
- data/lib/rigor/inference/string_mutation.rb +14 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
- data/lib/rigor/inference/toplevel_def_census.rb +14 -6
- data/lib/rigor/inference/unknown_store_widening.rb +1 -1
- data/lib/rigor/inference/untyped_rejoin.rb +1 -1
- data/lib/rigor/inference/void_tail_summary.rb +29 -10
- data/lib/rigor/lens/claims/comparison.rb +78 -0
- data/lib/rigor/lens/claims/request.rb +131 -0
- data/lib/rigor/lens/claims/type_reader.rb +216 -0
- data/lib/rigor/lens/claims.rb +208 -0
- data/lib/rigor/lens/contract_reader.rb +5 -0
- data/lib/rigor/lens/document.rb +1 -1
- data/lib/rigor/lens/file_reader.rb +21 -8
- data/lib/rigor/lens/run.rb +8 -0
- data/lib/rigor/lens.rb +1 -1
- data/lib/rigor/library_probe.rb +68 -0
- data/lib/rigor/memo_audit.rb +282 -0
- data/lib/rigor/plugin/base.rb +15 -2
- data/lib/rigor/plugin/io_boundary.rb +25 -3
- data/lib/rigor/plugin/manifest.rb +7 -3
- data/lib/rigor/plugin/registry.rb +13 -2
- data/lib/rigor/plugin/trust_policy.rb +46 -0
- data/lib/rigor/protection/mutation_cache.rb +4 -1
- data/lib/rigor/reflection/constant_ancestors.rb +79 -22
- data/lib/rigor/reflection/declaration_collectors.rb +55 -0
- data/lib/rigor/reflection/declarations.rb +58 -0
- data/lib/rigor/reflection/project_method.rb +177 -0
- data/lib/rigor/reflection.rb +2 -0
- data/lib/rigor/rooted_glob.rb +18 -1
- data/lib/rigor/run_scoped_slots.rb +20 -20
- data/lib/rigor/scope/discovery_index.rb +149 -2
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
- data/lib/rigor/scope/resolution_chain.rb +900 -55
- data/lib/rigor/scope.rb +307 -34
- data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
- data/lib/rigor/sig_gen/classification.rb +14 -1
- data/lib/rigor/sig_gen/generator.rb +222 -13
- data/lib/rigor/sig_gen/method_candidate.rb +14 -0
- data/lib/rigor/sig_gen/observation_collector.rb +11 -2
- data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
- data/lib/rigor/sig_gen/writer.rb +21 -0
- data/lib/rigor/sig_gen.rb +1 -0
- data/lib/rigor/source/forwardable_delegators.rb +122 -0
- data/lib/rigor/source/node_children.rb +13 -0
- data/lib/rigor/type/combinator.rb +164 -8
- data/lib/rigor/type/interface.rb +60 -0
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_of_memo/serving.rb +99 -0
- data/lib/rigor/type_of_memo/stats.rb +43 -0
- data/lib/rigor/type_of_memo.rb +337 -0
- data/lib/rigor/typing_state.rb +1094 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
- data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
- data/sig/rigor/analysis/fact_store.rbs +2 -3
- data/sig/rigor/cache/descriptor.rbs +14 -0
- data/sig/rigor/environment.rbs +4 -1
- data/sig/rigor/plugin/base.rbs +53 -19
- data/sig/rigor/plugin/interfaces.rbs +51 -0
- data/sig/rigor/plugin/io_boundary.rbs +2 -3
- data/sig/rigor/plugin/node_context.rbs +21 -0
- data/sig/rigor/reflection.rbs +37 -0
- data/sig/rigor/scope.rbs +22 -3
- data/sig/rigor/trinary.rbs +1 -0
- data/sig/rigor/type.rbs +9 -3
- data/sig/rigor.rbs +3 -2
- data/skills/rigor-ask/SKILL.md +1 -0
- data/skills/rigor-project-init/references/01-detect.md +1 -0
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
- data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
- data/skills/rigor-rbs-contract/SKILL.md +147 -0
- data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
- data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
- data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
- data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
- data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
- data/skills/rigor-rbs-setup/SKILL.md +3 -1
- data/skills/rigor-type-oracle/SKILL.md +26 -1
- data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
- metadata +48 -2
- data/lib/rigor/inference/memo_audit.rb +0 -55
|
@@ -4,6 +4,7 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../../source/constant_path"
|
|
6
6
|
require_relative "../../source/node_children"
|
|
7
|
+
require_relative "../../typing_state"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
module Inference
|
|
@@ -50,6 +51,9 @@ module Rigor
|
|
|
50
51
|
# `undef_method :gets`), other than `&:gets` and the first argument of a call that only names a method
|
|
51
52
|
# (`respond_to?(:gets)`, `send(:gets)`).
|
|
52
53
|
class SelfEvidence
|
|
54
|
+
TypingState.memo_field(self, :@scan, reason: "one walk of the file's root node, built on first ask")
|
|
55
|
+
TypingState.memo_field(self, :@const_set_spellings, reason: "one walk of the file's root, built on first ask")
|
|
56
|
+
|
|
53
57
|
# The context of a reader whose `self` is `main`.
|
|
54
58
|
MAIN = :main
|
|
55
59
|
UNKNOWN = :unknown
|
|
@@ -83,8 +87,14 @@ module Rigor
|
|
|
83
87
|
def initialize(root)
|
|
84
88
|
@root = root
|
|
85
89
|
@scan = nil
|
|
90
|
+
@const_set_spellings = nil
|
|
86
91
|
end
|
|
87
92
|
|
|
93
|
+
# Issue #3156 — the constants this file binds by a `const_set` with a literal name on the body's own module
|
|
94
|
+
# (implicit `self` or `self.const_set`) inside a `class` / `module` body, qualified by that body's nesting
|
|
95
|
+
# (`module NS; const_set(:AMid, Other)` binds `"NS::AMid"`). The census files no such write.
|
|
96
|
+
def const_set_spellings = (@const_set_spellings ||= collect_const_sets(@root, nil, Set.new).freeze)
|
|
97
|
+
|
|
88
98
|
# {MAIN}, the `Prism::ClassNode` or `Prism::ModuleNode` whose body the implicit-self reader `call_node` runs
|
|
89
99
|
# in, or nil: the file may give it another `self`, or it is not one of this file's readers.
|
|
90
100
|
def context(call_node) = scan.context(call_node)
|
|
@@ -96,6 +106,26 @@ module Rigor
|
|
|
96
106
|
|
|
97
107
|
def scan = (@scan ||= Scan.new(@root))
|
|
98
108
|
|
|
109
|
+
def collect_const_sets(node, namespace, found)
|
|
110
|
+
case node
|
|
111
|
+
when Prism::ClassNode, Prism::ModuleNode
|
|
112
|
+
inner = Source::ConstantPath.pushed_nesting(namespace ? [namespace] : [], node.constant_path)&.first
|
|
113
|
+
collect_const_sets(node.body, inner, found) if inner
|
|
114
|
+
return found
|
|
115
|
+
when Prism::CallNode
|
|
116
|
+
found << "#{namespace}::#{literal_const_set_name(node)}" if namespace && literal_const_set_name(node)
|
|
117
|
+
end
|
|
118
|
+
node&.compact_child_nodes&.each { |child| collect_const_sets(child, namespace, found) }
|
|
119
|
+
found
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def literal_const_set_name(node)
|
|
123
|
+
return nil unless node.name == :const_set && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
124
|
+
|
|
125
|
+
name = node.arguments&.arguments&.first
|
|
126
|
+
name.unescaped if name.is_a?(Prism::SymbolNode) || name.is_a?(Prism::StringNode)
|
|
127
|
+
end
|
|
128
|
+
|
|
99
129
|
# One walk of the file, whose answers {SelfEvidence} keeps once the walk is complete.
|
|
100
130
|
class Scan
|
|
101
131
|
def initialize(root)
|
|
@@ -261,10 +261,13 @@ module Rigor
|
|
|
261
261
|
# The scope a block or lambda body enters with, as {MatchRebinding.block_entry} gives the match globals': `$_`
|
|
262
262
|
# forgotten when the body may set it, which a later iteration then reads, when the frame makes a closure that
|
|
263
263
|
# may, or when the owning call may: its receiver chain and arguments run before the method yields, and
|
|
264
|
-
# `IO.foreach` sets `$_` to each line it yields.
|
|
264
|
+
# `IO.foreach` sets `$_` to each line it yields. Issue #3195 — and whatever the body does when it runs later
|
|
265
|
+
# than where it is written ({FreshFrameBlocks.deferred_body?}): a proc's body, an enumerator's or a hash's
|
|
266
|
+
# default proc reads the `$_` the slot holds when it runs.
|
|
265
267
|
def block_entry(scope, block_node, call_node = nil)
|
|
266
268
|
return scope unless scope.last_line_bound?
|
|
267
|
-
return scope unless
|
|
269
|
+
return scope unless FreshFrameBlocks.deferred_body?(block_node, call_node, scope) ||
|
|
270
|
+
scope.last_line_closure? || may_set?(block_node.body, scope) || call_sets?(call_node, scope)
|
|
268
271
|
|
|
269
272
|
scope.forget_last_line
|
|
270
273
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../
|
|
3
|
+
require_relative "../typing_state"
|
|
4
4
|
|
|
5
5
|
module Rigor
|
|
6
6
|
module Inference
|
|
@@ -17,40 +17,88 @@ module Rigor
|
|
|
17
17
|
# `discovery`): across the cost spec's 10933 reads every match was an identity match, and `lib/` read no record at
|
|
18
18
|
# all (issue #2262). It lives in a per-thread map keyed by the loop node. The map's keys are weak but its values
|
|
19
19
|
# are strong, and a recorded scope reaches its discovery, its environment and the parse tree that holds the key,
|
|
20
|
-
# so a record never goes on its own (issue #2412): the map is a run-scoped slot ({
|
|
21
|
-
# the run that filled it ends.
|
|
20
|
+
# so a record never goes on its own (issue #2412): the map is a run-scoped slot ({TypingState.release_run_scoped}),
|
|
21
|
+
# emptied when the run that filled it ends.
|
|
22
22
|
module LoopValueRecord
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
# ADR-129 WD1 — a Table, and a read-through cache of the computation's own value: a record read back answers
|
|
24
|
+
# what evaluating the loop again from the same scope would.
|
|
25
|
+
RECORD = TypingState.table(
|
|
26
|
+
:loop_value_record,
|
|
27
|
+
run_scoped: true, read_through: true, entries: TypingState::WeakKeyMapEntries,
|
|
28
|
+
reason: "keyed by loop node; a read-through cache of the loop's own evaluation"
|
|
29
|
+
)
|
|
30
|
+
private_constant :RECORD
|
|
25
31
|
|
|
26
|
-
|
|
32
|
+
# ADR-129 WD3 — a Resource read only as a delta: {#loops_evaluated} feeds a before/after comparison taken within
|
|
33
|
+
# one conditional's evaluation, so with no loop evaluated in between the delta is zero.
|
|
34
|
+
LOOPS_EVALUATED = TypingState.resource(
|
|
35
|
+
:loops_evaluated,
|
|
36
|
+
read_exemption: :delta_internal, run_scoped: true,
|
|
37
|
+
reason: "a count of the loops this thread evaluated, incremented in place and read as a delta"
|
|
38
|
+
)
|
|
39
|
+
private_constant :LOOPS_EVALUATED
|
|
40
|
+
|
|
41
|
+
# Issue #2767 — the ADR-55 fixpoint summaries are an ambient input of a loop's value: an ADR-55 fixpoint
|
|
42
|
+
# evaluates a recursive method's body several times from the same body scope object, each time under a wider
|
|
43
|
+
# assumption, so a value recorded in one iteration would answer an identity-matching read in the next one with
|
|
44
|
+
# the narrower iterate, and the fixpoint would converge on it (`[(while …; break(n.zero? ? 1 : q(n - 1)); end)]`
|
|
45
|
+
# typed `[bot]`). The generation moves whenever a summary's assumption is seeded or changed
|
|
46
|
+
# ({advance_generation}), and a record answers only a read in the generation it was written in. The advance in
|
|
47
|
+
# `fixpoint_step` is the one a witness needs (the spec above); the advances at the seed, the entangled degrade
|
|
48
|
+
# and the cap collapse are precautions, since each leaves no in-flight evaluation of that body scope behind.
|
|
49
|
+
#
|
|
50
|
+
# ADR-129 WD3 — a Resource whose value is fixed while the state id is: every advance is a {TypingState::State#rewrite}.
|
|
51
|
+
GENERATION = TypingState.resource(
|
|
52
|
+
:summary_generation,
|
|
53
|
+
read_exemption: :id_determined, run_scoped: true,
|
|
54
|
+
reason: "a count of the ADR-55 summary assumption changes on this thread; a record keys on it"
|
|
55
|
+
)
|
|
56
|
+
private_constant :GENERATION
|
|
27
57
|
|
|
28
|
-
|
|
29
|
-
private_constant :COUNT_KEY
|
|
58
|
+
module_function
|
|
30
59
|
|
|
31
60
|
def record(node, scope, type)
|
|
32
|
-
|
|
61
|
+
TypingState.current.write(LOOPS_EVALUATED, loops_evaluated + 1)
|
|
33
62
|
conditional(node, scope, type)
|
|
34
63
|
end
|
|
35
64
|
|
|
65
|
+
# The current ADR-55 summary generation (#2767), which a record made now carries and a read must match.
|
|
66
|
+
def generation = TypingState.current[GENERATION] || 0
|
|
67
|
+
|
|
68
|
+
# A summary assumption was seeded or changed: no record written before answers a read from now on (#2767).
|
|
69
|
+
def advance_generation
|
|
70
|
+
state = TypingState.current
|
|
71
|
+
state.write(GENERATION, (state[GENERATION] || 0) + 1)
|
|
72
|
+
state.rewrite(GENERATION)
|
|
73
|
+
end
|
|
74
|
+
|
|
36
75
|
# How many loops this thread has evaluated, so a conditional can tell whether its evaluation held one.
|
|
37
|
-
def loops_evaluated =
|
|
76
|
+
def loops_evaluated = TypingState.current[LOOPS_EVALUATED] || 0
|
|
38
77
|
|
|
39
78
|
# The same record for a conditional that evaluated a loop in a branch: `id(c ? (while …) : 0)` types the
|
|
40
79
|
# conditional again from the scope it was evaluated from, and each typing evaluated the loop again from a
|
|
41
80
|
# branch scope of its own, which multiplied the passes of every loop nested in it (issue #2262).
|
|
42
81
|
def conditional(node, scope, type)
|
|
43
|
-
|
|
82
|
+
record = [scope, type, generation]
|
|
83
|
+
state = TypingState.current
|
|
84
|
+
state.log_write(RECORD, node, record)
|
|
85
|
+
state.weak_key_map(RECORD)[node] = record
|
|
44
86
|
type
|
|
45
87
|
end
|
|
46
88
|
|
|
47
|
-
#
|
|
89
|
+
# Runs the evaluation a {fetch} that answered nil falls back to: the read-through Table's miss (ADR-129 WD1),
|
|
90
|
+
# which touches when it writes another Table while a capture is open.
|
|
91
|
+
def miss(&) = TypingState.current.memo_miss(RECORD, &)
|
|
92
|
+
|
|
93
|
+
# The recorded value of `node` evaluated from `scope` itself in the current summary generation, or nil.
|
|
48
94
|
def fetch(node, scope)
|
|
49
|
-
|
|
95
|
+
state = TypingState.current
|
|
96
|
+
slot = state[RECORD]&.[](node)
|
|
97
|
+
state.log_read(RECORD, node, slot || TypingState::ABSENT)
|
|
50
98
|
return nil if slot.nil?
|
|
51
99
|
|
|
52
|
-
recorded, type = slot
|
|
53
|
-
recorded.equal?(scope) ? type : nil
|
|
100
|
+
recorded, type, recorded_generation = slot
|
|
101
|
+
recorded.equal?(scope) && recorded_generation == generation ? type : nil
|
|
54
102
|
end
|
|
55
103
|
end
|
|
56
104
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "../../source/node_children"
|
|
6
|
+
require_relative "../../typing_state"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
module Inference
|
|
@@ -15,6 +16,16 @@ module Rigor
|
|
|
15
16
|
# `$_` (issue #1359), whose frame-wide answers ({#last_line_closure?}, {#last_line_fallback?}) are kept here
|
|
16
17
|
# too, asked only while `$_` is narrowed.
|
|
17
18
|
class Frame
|
|
19
|
+
# ADR-129 G4 — the frame's extensional caches. A scan is keyed on its node (and reading kind) and on the scope's
|
|
20
|
+
# local and instance-variable tables by identity, the inputs a lookup argument's answer reads; the rest of what
|
|
21
|
+
# it reads through the scope (constant resolution over the lexical nesting, discovery and environment) is fixed
|
|
22
|
+
# for the body the frame runs. The other three read the body and the parameters alone.
|
|
23
|
+
TypingState.memo_field(self, :@scans, :@kind_scans, :@self_call_fallback,
|
|
24
|
+
reason: "keyed on the node and the scope's locals and ivars tables by identity; the " \
|
|
25
|
+
"other scope inputs a scan reads are fixed for the frame's body")
|
|
26
|
+
TypingState.memo_field(self, :@forwarded_block, :@body_locals, :@parameter_names,
|
|
27
|
+
reason: "functions of the frame's body and parameters alone")
|
|
28
|
+
|
|
18
29
|
# The body the frame runs, which issue #2555's escape scan reads ({ConstructionEscape.escaped?}).
|
|
19
30
|
attr_reader :body
|
|
20
31
|
# The method's parameters (nil for a class, module or file body), which issue #2608's holder scan reads for a
|
|
@@ -5,40 +5,96 @@ require "prism"
|
|
|
5
5
|
module Rigor
|
|
6
6
|
module Inference
|
|
7
7
|
module MatchRebinding
|
|
8
|
-
# Issue #2443 — the match globals a block enters with when the call that owns it matches before every
|
|
9
|
-
# `String#scan`, `#gsub`, `#gsub!`, `#sub` and `#sub!` set the caller's `$~` to the current match before
|
|
10
|
-
# run of their block, with a Regexp pattern or a String one, so the block reads that match on every run
|
|
8
|
+
# Issue #2443, #1371 — the match globals a block enters with when the call that owns it matches before every
|
|
9
|
+
# yield. `String#scan`, `#gsub`, `#gsub!`, `#sub` and `#sub!` set the caller's `$~` to the current match before
|
|
10
|
+
# each run of their block, with a Regexp pattern or a String one, so the block reads that match on every run
|
|
11
11
|
# whatever the enclosing frame held; the frame-wide entry ({MatchRebinding.block_entry}) would otherwise leave
|
|
12
|
-
# it the narrowing at the call site
|
|
13
|
-
# block read `MatchData?`.
|
|
12
|
+
# it the narrowing at the call site or nothing at all.
|
|
14
13
|
#
|
|
15
14
|
# A receiver that is a String, or a `Dynamic` the analyzer cannot type ({Narrowing.string_match_receiver?}),
|
|
16
|
-
# enters with the call's own match edge ({Narrowing.yielded_match_scope})
|
|
17
|
-
# object whose block-taking `scan` runs no match, and then the block reads
|
|
18
|
-
#
|
|
19
|
-
# class, `Pathname`, a union with either — keeps the frame-wide entry: a method
|
|
20
|
-
# matches in a frame of its own, so its block reads the caller's `$~` as the frame
|
|
21
|
-
# implicit-self call.
|
|
15
|
+
# enters with the call's own match edge ({Narrowing.yielded_match_scope}), which forgets what the frame held.
|
|
16
|
+
# A `Dynamic` receiver may be another object whose block-taking `scan` runs no match, and then the block reads
|
|
17
|
+
# the caller's `$~`; the name is taken as String's there, the overwhelmingly common case. A receiver known to
|
|
18
|
+
# be anything else — a project class, `Pathname`, a union with either — keeps the frame-wide entry: a method
|
|
19
|
+
# defined in Ruby runs its matches in a frame of its own, so its block reads the caller's `$~` as the frame
|
|
20
|
+
# rules leave it. So does an implicit-self call.
|
|
21
|
+
#
|
|
22
|
+
# The block form of `grep` and `grep_v` runs `pattern === element` in the caller's frame before each yield,
|
|
23
|
+
# which a Regexp pattern answers by setting `$~`: `grep` yields after a match, `grep_v` after a failed one. With
|
|
24
|
+
# a regex literal for the pattern, a `grep` block enters with that match edge and a `grep_v` block with the
|
|
25
|
+
# globals unbound. `Enumerable` is mixed into classes of every kind, so any explicit receiver counts but one
|
|
26
|
+
# whose `grep` the project defines ({ProjectMethodOwnership}): that method matches, if at all, in a frame of
|
|
27
|
+
# its own, and its block keeps the frame-wide entry as a project `scan`'s does. Where a refinement that defines
|
|
28
|
+
# the name is in effect at the call (issue #3198), the method that runs is the refinement's or `Enumerable`'s,
|
|
29
|
+
# and this does not tell which, so the block enters with what `Enumerable`'s would bind read untyped. Any other
|
|
30
|
+
# pattern (`grep(Integer)`) keeps the frame-wide entry too, since its `===` may leave `$~` alone.
|
|
22
31
|
module YieldingMatch
|
|
23
32
|
NAMES = Set[:scan, :gsub, :gsub!, :sub, :sub!].freeze
|
|
24
|
-
|
|
33
|
+
GREP_NAMES = Set[:grep, :grep_v].freeze
|
|
34
|
+
private_constant :NAMES, :GREP_NAMES
|
|
25
35
|
|
|
26
36
|
module_function
|
|
27
37
|
|
|
28
38
|
# `scope` with the match globals `call_node`'s block enters with; `scope` itself for any other block.
|
|
29
39
|
# `operand_scope` types the receiver.
|
|
30
40
|
def entry(scope, block_node, call_node, operand_scope)
|
|
31
|
-
return scope unless
|
|
32
|
-
|
|
41
|
+
return scope unless own_block_call?(block_node, call_node)
|
|
42
|
+
|
|
43
|
+
name = call_node.name
|
|
44
|
+
if NAMES.include?(name)
|
|
45
|
+
string_entry(scope, call_node, operand_scope)
|
|
46
|
+
elsif GREP_NAMES.include?(name)
|
|
47
|
+
grep_entry(scope, call_node, operand_scope)
|
|
48
|
+
else
|
|
49
|
+
scope
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def string_entry(scope, call_node, operand_scope)
|
|
54
|
+
return scope unless Narrowing.string_match_receiver?(operand_scope.type_of(call_node.receiver), operand_scope)
|
|
55
|
+
|
|
56
|
+
Narrowing.yielded_match_scope(scope, pattern_argument(call_node))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def grep_entry(scope, call_node, operand_scope)
|
|
60
|
+
pattern = pattern_argument(call_node)
|
|
61
|
+
return scope unless pattern.is_a?(Prism::RegularExpressionNode) ||
|
|
62
|
+
pattern.is_a?(Prism::InterpolatedRegularExpressionNode)
|
|
63
|
+
return scope if project_method?(call_node, operand_scope)
|
|
64
|
+
|
|
65
|
+
entry = call_node.name == :grep_v ? scope.forget_match_globals : Narrowing.yielded_match_scope(scope, pattern)
|
|
66
|
+
refined?(call_node, operand_scope) ? entry.untyped_match_globals : entry
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# True when the receiver answers the call with a method the project defines, or is a carrier whose class
|
|
70
|
+
# cannot be resolved; an untyped receiver is not one.
|
|
71
|
+
def project_method?(call_node, operand_scope)
|
|
72
|
+
targets = ProjectMethodOwnership.targets(operand_scope.type_of(call_node.receiver))
|
|
73
|
+
return true if targets.nil?
|
|
74
|
+
|
|
75
|
+
targets.any? do |class_name, kind|
|
|
76
|
+
ProjectMethodOwnership.defines?(class_name, call_node.name, kind, operand_scope)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Issue #3198 — true when a refinement in effect at the call defines its name on any class: a refined method
|
|
81
|
+
# is Ruby's, and matches, if at all, in a frame of its own. The refined class is not compared with the
|
|
82
|
+
# receiver, so the call may still be `Enumerable`'s.
|
|
83
|
+
def refined?(call_node, operand_scope)
|
|
84
|
+
SelfPreservingBlock.refined_in_effect?(call_node, call_node.name, operand_scope)
|
|
85
|
+
end
|
|
33
86
|
|
|
34
|
-
|
|
87
|
+
# The call's first argument, which each of these methods takes as its pattern; nil without one.
|
|
88
|
+
def pattern_argument(call_node)
|
|
89
|
+
call_node.arguments&.arguments&.first
|
|
35
90
|
end
|
|
36
91
|
|
|
37
|
-
def
|
|
38
|
-
call_node.is_a?(Prism::CallNode) &&
|
|
92
|
+
def own_block_call?(block_node, call_node)
|
|
93
|
+
call_node.is_a?(Prism::CallNode) && !call_node.receiver.nil? &&
|
|
39
94
|
block_node.is_a?(Prism::BlockNode) && call_node.block.equal?(block_node)
|
|
40
95
|
end
|
|
41
|
-
private_class_method :
|
|
96
|
+
private_class_method :string_entry, :grep_entry, :project_method?, :refined?, :pattern_argument,
|
|
97
|
+
:own_block_call?
|
|
42
98
|
end
|
|
43
99
|
end
|
|
44
100
|
end
|
|
@@ -7,6 +7,8 @@ require_relative "../type"
|
|
|
7
7
|
require_relative "block_call_timing"
|
|
8
8
|
require_relative "fresh_frame_blocks"
|
|
9
9
|
require_relative "guard_rebinding"
|
|
10
|
+
require_relative "project_method_ownership"
|
|
11
|
+
require_relative "self_preserving_block"
|
|
10
12
|
require_relative "stored_block_call"
|
|
11
13
|
require_relative "match_rebinding/frame"
|
|
12
14
|
require_relative "match_rebinding/operands"
|
|
@@ -364,14 +366,20 @@ module Rigor
|
|
|
364
366
|
# ractor's root block with the globals unbound, since it reads a slot of its own, and a `define_method` body
|
|
365
367
|
# with a narrowed global untyped, since it reads the definer's slot whenever the method is called. `$_`, which
|
|
366
368
|
# shares the slot, enters on the same terms ({LastLine.block_entry}, issue #1359). A closure's body, which runs
|
|
367
|
-
# whenever it is called, enters with `$!`, `$@` and `$?` unbound ({FreshFrameBlocks.closure_entry}, issue #1360)
|
|
369
|
+
# whenever it is called, enters with `$!`, `$@` and `$?` unbound ({FreshFrameBlocks.closure_entry}, issue #1360),
|
|
370
|
+
# and with the match globals unbound whatever its body does ({FreshFrameBlocks.proc_body?}, issue #1371): the
|
|
371
|
+
# narrowing where a lambda or proc is written proves nothing about the `$~` it reads when it is called. The
|
|
372
|
+
# block of `Enumerator.new` or `Hash.new` on the core class runs later too, but often in the statement that
|
|
373
|
+
# makes it, so where none of the rules above forgets them it enters with a narrowed global untyped
|
|
374
|
+
# ({FreshFrameBlocks.deferred_constructor?}, issue #3195).
|
|
368
375
|
#
|
|
369
376
|
# Issue #1429 — the entry also restores a guard's narrowing of a global or constant where the body may run after
|
|
370
377
|
# code that rebinds it ({GuardRebinding.block_entry}).
|
|
371
378
|
#
|
|
372
|
-
# Issue #2443 — the block of a call that matches before every yield (`scan`, `gsub`, `sub`, …
|
|
373
|
-
# untyped receiver enters with that call's own match edge
|
|
374
|
-
# its body runs still rebinds them through the statement-level
|
|
379
|
+
# Issue #2443, #1371 — the block of a call that matches before every yield (`scan`, `gsub`, `sub`, … on a String
|
|
380
|
+
# or untyped receiver, `grep` and `grep_v` with a regex literal) enters with that call's own match edge in place
|
|
381
|
+
# of the entry above ({YieldingMatch}); a match its body runs still rebinds them through the statement-level
|
|
382
|
+
# rules.
|
|
375
383
|
def block_entry(scope, block_node, call_node = nil)
|
|
376
384
|
operand_scope = scope
|
|
377
385
|
scope = GuardRebinding.block_entry(scope, block_node, call_node)
|
|
@@ -384,10 +392,15 @@ module Rigor
|
|
|
384
392
|
|
|
385
393
|
def frame_entry(scope, block_node, call_node)
|
|
386
394
|
return scope unless scope.match_globals_bound?
|
|
387
|
-
return scope
|
|
388
|
-
(call_node.is_a?(Prism::CallNode) && operands_may_rebind?(call_node, scope))
|
|
395
|
+
return scope.forget_match_globals if entry_forgets?(scope, block_node, call_node)
|
|
389
396
|
|
|
390
|
-
scope.
|
|
397
|
+
FreshFrameBlocks.deferred_constructor?(block_node, call_node, scope) ? scope.untyped_match_globals : scope
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def entry_forgets?(scope, block_node, call_node)
|
|
401
|
+
FreshFrameBlocks.proc_body?(block_node, call_node, scope) ||
|
|
402
|
+
scope.match_rebinding_closure? || entry_may_match?(block_node.body, scope, call_node) ||
|
|
403
|
+
(call_node.is_a?(Prism::CallNode) && operands_may_rebind?(call_node, scope))
|
|
391
404
|
end
|
|
392
405
|
|
|
393
406
|
def entry_may_match?(body, scope, call_node)
|
|
@@ -396,7 +409,7 @@ module Rigor
|
|
|
396
409
|
|
|
397
410
|
body.is_a?(Prism::Node) && scan(body, scope, base: true)
|
|
398
411
|
end
|
|
399
|
-
private_class_method :frame_entry, :entry_may_match?
|
|
412
|
+
private_class_method :frame_entry, :entry_forgets?, :entry_may_match?
|
|
400
413
|
end
|
|
401
414
|
end
|
|
402
415
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../../type"
|
|
4
4
|
require_relative "iterator_dispatch"
|
|
5
|
+
require_relative "core_enumerable_receiver"
|
|
5
6
|
|
|
6
7
|
module Rigor
|
|
7
8
|
module Inference
|
|
@@ -37,12 +38,12 @@ module Rigor
|
|
|
37
38
|
module BlockFolding
|
|
38
39
|
module_function
|
|
39
40
|
|
|
40
|
-
FILTER_KEEP_ON_TRUTHY = Set[:select, :filter, :take_while].freeze
|
|
41
|
+
FILTER_KEEP_ON_TRUTHY = Set[:select, :filter, :find_all, :take_while].freeze
|
|
41
42
|
FILTER_KEEP_ON_FALSEY = Set[:reject, :drop_while].freeze
|
|
42
43
|
|
|
43
|
-
# The filter methods `Hash` defines itself, returning a Hash. `
|
|
44
|
-
# are Enumerable's and return an Array of `[key, value]` pairs; `Set` and
|
|
45
|
-
#
|
|
44
|
+
# The filter methods `Hash` defines itself, returning a Hash. `find_all` / `take_while` /
|
|
45
|
+
# `drop_while` on a Hash are Enumerable's and return an Array of `[key, value]` pairs; `Set` and
|
|
46
|
+
# `Range` define none of the six, so all of theirs return an Array too.
|
|
46
47
|
HASH_RETURNING_FILTERS = Set[:select, :filter, :reject].freeze
|
|
47
48
|
|
|
48
49
|
# `filter_receiver_kind`'s answer for a nominal receiver; an unlisted class declines.
|
|
@@ -52,21 +53,22 @@ module Rigor
|
|
|
52
53
|
|
|
53
54
|
PREDICATE_METHODS = Set[:all?, :any?, :none?].freeze
|
|
54
55
|
|
|
55
|
-
# Methods whose answer is `nil` when the block always returns Ruby-falsey — `find` / `detect`
|
|
56
|
-
# short-circuit to nil when nothing matches, `find_index` / `index` likewise.
|
|
57
|
-
# on the falsey side
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
# Methods whose answer is `nil` when the block always returns Ruby-falsey — `find` / `detect` /
|
|
57
|
+
# `rfind` short-circuit to nil when nothing matches, `find_index` / `index` / `rindex` likewise.
|
|
58
|
+
# These methods only fold on the falsey side here; the truthy-block side requires per-position
|
|
59
|
+
# analysis.
|
|
60
|
+
FALSEY_BLOCK_NIL_METHODS = Set[:find, :detect, :rfind, :find_index, :index, :rindex].freeze
|
|
61
|
+
ARRAY_ONLY_FALSEY_NIL_METHODS = Set[:rfind, :index, :rindex].freeze
|
|
61
62
|
|
|
62
63
|
# Block-taking `count` returns the number of elements for which the block is truthy. With a
|
|
63
64
|
# Constant-falsey block the answer is unconditionally `Constant[0]`; with a Constant-truthy block on
|
|
64
65
|
# a finitely-sized receiver it is `Constant[size]`.
|
|
65
66
|
COUNT_METHOD = :count
|
|
66
67
|
|
|
67
|
-
# `min_by` / `max_by` pick
|
|
68
|
-
# but never WHETHER one is returned: only an empty receiver answers `nil
|
|
69
|
-
|
|
68
|
+
# `min_by` / `max_by` / `minmax_by` pick edge elements by the block's key, so the block's value
|
|
69
|
+
# decides WHICH element(s) but never WHETHER one is returned: only an empty receiver answers `nil`
|
|
70
|
+
# (or `[nil, nil]` for `minmax_by`).
|
|
71
|
+
EDGE_BY_METHODS = Set[:min_by, :max_by, :minmax_by].freeze
|
|
70
72
|
|
|
71
73
|
# `context.block_type` is the inferred return type of the call's block; `nil` means "no block at the
|
|
72
74
|
# call site" and disqualifies every rule here.
|
|
@@ -76,32 +78,44 @@ module Rigor
|
|
|
76
78
|
args = context.args
|
|
77
79
|
block_type = context.block_type
|
|
78
80
|
return nil if receiver.nil? || block_type.nil?
|
|
79
|
-
return fold_edge_by(receiver, args) if EDGE_BY_METHODS.include?(method_name)
|
|
81
|
+
return fold_edge_by(receiver, method_name, args) if EDGE_BY_METHODS.include?(method_name)
|
|
80
82
|
|
|
83
|
+
# Issue #3242 — the predicate, `find`-family and `count` folds read no receiver shape, so a class that
|
|
84
|
+
# may define the method itself declines (the filter folds already do, through `filter_receiver_kind`).
|
|
81
85
|
truthiness = constant_truthiness(block_type)
|
|
82
|
-
return nil if truthiness.nil?
|
|
86
|
+
return nil if truthiness.nil? || CoreEnumerableReceiver.project_receiver?(receiver)
|
|
83
87
|
|
|
84
88
|
if PREDICATE_METHODS.include?(method_name)
|
|
85
89
|
fold_predicate(receiver, method_name, truthiness, args)
|
|
86
90
|
elsif filter_method?(method_name)
|
|
87
91
|
fold_filter(receiver, method_name, truthiness)
|
|
88
92
|
elsif FALSEY_BLOCK_NIL_METHODS.include?(method_name)
|
|
89
|
-
fold_falsey_nil_short_circuit(method_name, truthiness, args)
|
|
93
|
+
fold_falsey_nil_short_circuit(receiver, method_name, truthiness, args)
|
|
90
94
|
elsif method_name == COUNT_METHOD
|
|
91
95
|
fold_count(receiver, truthiness, args)
|
|
92
96
|
end
|
|
93
97
|
end
|
|
94
98
|
|
|
95
|
-
# Issue #1333 — `min_by` / `max_by` with a block and no count on a
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
|
|
99
|
-
# cannot say which, so the answer is the whole element union. An empty receiver, one whose size is
|
|
100
|
-
# not static (`Array[T]`), and the count form (`min_by(2) { … }`, an Array) decline to RBS.
|
|
101
|
-
def fold_edge_by(receiver, args)
|
|
99
|
+
# Issue #1333 — `min_by` / `max_by` / `minmax_by` with a block and no count on a Tuple or constant
|
|
100
|
+
# integer `Range` answer the receiver's element type without `nil` when non-empty (or `[nil, nil]`
|
|
101
|
+
# for an empty `minmax_by`), avoiding false `nil` receivers on `min_by` / `minmax_by` calls.
|
|
102
|
+
def fold_edge_by(receiver, method_name, args)
|
|
102
103
|
return nil unless args.empty?
|
|
103
|
-
return nil unless receiver_emptiness(receiver) == :non_empty
|
|
104
104
|
|
|
105
|
+
emptiness = receiver_emptiness(receiver)
|
|
106
|
+
if method_name == :minmax_by && emptiness == :empty
|
|
107
|
+
nil_const = Type::Combinator.constant_of(nil)
|
|
108
|
+
return Type::Combinator.tuple_of(nil_const, nil_const)
|
|
109
|
+
end
|
|
110
|
+
return nil unless emptiness == :non_empty
|
|
111
|
+
|
|
112
|
+
element = edge_by_element_type(receiver)
|
|
113
|
+
return nil if element.nil?
|
|
114
|
+
|
|
115
|
+
method_name == :minmax_by ? Type::Combinator.tuple_of(element, element) : element
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def edge_by_element_type(receiver)
|
|
105
119
|
case receiver
|
|
106
120
|
when Type::Tuple then Type::Combinator.union(*receiver.elements)
|
|
107
121
|
when Type::Constant then IteratorDispatch.element_type_of(receiver) if receiver.value.is_a?(Range)
|
|
@@ -288,13 +302,13 @@ module Rigor
|
|
|
288
302
|
end
|
|
289
303
|
end
|
|
290
304
|
|
|
291
|
-
# `find` / `detect` / `find_index` / `index`
|
|
292
|
-
# provably falsey.
|
|
293
|
-
#
|
|
294
|
-
|
|
295
|
-
def fold_falsey_nil_short_circuit(_method_name, truthiness, args)
|
|
305
|
+
# `find` / `detect` / `find_index` (and Array-only `rfind` / `index` / `rindex`) short-circuit to
|
|
306
|
+
# nil when the block is provably falsey. Calls with a positional argument decline so the RBS tier
|
|
307
|
+
# answers value-search calls.
|
|
308
|
+
def fold_falsey_nil_short_circuit(receiver, method_name, truthiness, args)
|
|
296
309
|
return nil unless args.empty?
|
|
297
310
|
return nil unless truthiness == :falsey
|
|
311
|
+
return nil if ARRAY_ONLY_FALSEY_NIL_METHODS.include?(method_name) && filter_receiver_kind(receiver) != :array
|
|
298
312
|
|
|
299
313
|
Type::Combinator.constant_of(nil)
|
|
300
314
|
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module MethodDispatcher
|
|
8
|
+
# Issue #2686 — `Rigor::Plugin::Base.node_rule(node_class) { |node, ...| }` yields instances of the class it is
|
|
9
|
+
# given, which RBS cannot say: `[N < ::Prism::Node] (singleton(N))` does not resolve (RBS has no
|
|
10
|
+
# `singleton(<type variable>)`). `sig/rigor/plugin/base.rbs` declares per-class overloads for the classes
|
|
11
|
+
# the bundled plugins register and a `Prism::Node` fallback. Read through the overloads alone, two calls bind
|
|
12
|
+
# `node` wrong:
|
|
13
|
+
#
|
|
14
|
+
# - a class with no overload of its own (`node_rule(Prism::IfNode)`) takes the fallback, so `node` is
|
|
15
|
+
# `Prism::Node` and `node.predicate` reported `call.undefined-method`;
|
|
16
|
+
# - a class argument the engine cannot read (`def self.reg(k) = node_rule(k) { ... }`) matches every overload
|
|
17
|
+
# gradually, and the block probe took the first, `Prism::CallNode`.
|
|
18
|
+
#
|
|
19
|
+
# This rebinds the block's first parameter from the class argument:
|
|
20
|
+
#
|
|
21
|
+
# - `singleton(X)` binds `X`, and a union of class objects the union of their instances;
|
|
22
|
+
# - any other argument (an untyped parameter, a `Class` value) binds `Dynamic[top]`, so the body is not
|
|
23
|
+
# checked against a class the call may not register.
|
|
24
|
+
#
|
|
25
|
+
# The rest of the block's parameters keep the overload's binding; every overload declares them alike.
|
|
26
|
+
#
|
|
27
|
+
# A per-class overload set generated from Prism's node classes was the alternative. It was declined: the
|
|
28
|
+
# gemspec admits `prism >= 1.0, < 2.0`, and a generated set names classes later minors added, which an older
|
|
29
|
+
# Prism's RBS does not declare, so loading Rigor's own `sig/` would fail there.
|
|
30
|
+
#
|
|
31
|
+
# Only the declaration Rigor owns is read, by the method's RBS owner, so a project's own `node_rule` is not.
|
|
32
|
+
module ClassKeyedBlockParams
|
|
33
|
+
OWNER = "::Rigor::Plugin::Base"
|
|
34
|
+
METHOD = :node_rule
|
|
35
|
+
private_constant :OWNER, :METHOD
|
|
36
|
+
|
|
37
|
+
module_function
|
|
38
|
+
|
|
39
|
+
# @param method_definition — the RBS declaration the probe read.
|
|
40
|
+
# @param params — the block parameters the overloads bound.
|
|
41
|
+
# @return the block parameters, the first rebound for `Base.node_rule`.
|
|
42
|
+
def rebind(method_definition, method_name, args, params)
|
|
43
|
+
return params unless method_name == METHOD
|
|
44
|
+
return params unless method_definition.defined_in.to_s == OWNER
|
|
45
|
+
|
|
46
|
+
[instance_of(args.first), *params.drop(1)]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The instances the class argument names, or `Dynamic[top]`.
|
|
50
|
+
def instance_of(arg)
|
|
51
|
+
case arg
|
|
52
|
+
when Type::Singleton
|
|
53
|
+
Type::Combinator.nominal_of(arg.class_name)
|
|
54
|
+
when Type::Union
|
|
55
|
+
return Type::Combinator.untyped unless arg.members.all?(Type::Singleton)
|
|
56
|
+
|
|
57
|
+
Type::Combinator.union(*arg.members.map { |member| Type::Combinator.nominal_of(member.class_name) })
|
|
58
|
+
else
|
|
59
|
+
Type::Combinator.untyped
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module MethodDispatcher
|
|
8
|
+
# Issue #3242 — whether a receiver's `find` / `detect` / `find_index` / `count` / `all?` / `any?` / `none?`
|
|
9
|
+
# may be other than core's. {BlockFolding}'s constant-block folds for those methods read no receiver shape, so
|
|
10
|
+
# they answer core's semantics, and they are sound only on a carrier known to be a core collection or
|
|
11
|
+
# enumerable. Anything else may define the method itself: a project class (`class Bag; def find = (yield; 8);
|
|
12
|
+
# end`), a subclass of a core class (its own `Nominal`), a class object (`def self.find`), a `Data.define` or
|
|
13
|
+
# `Struct.new` class, or a scalar that has no such method at all.
|
|
14
|
+
module CoreEnumerableReceiver
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# The nominal receivers whose methods above are core's: Array's, Hash's, Set's, Range's own, or
|
|
18
|
+
# Enumerable's. `RBS::Unnamed::ENVClass` is `ENV`, which extends Enumerable.
|
|
19
|
+
NOMINALS = Ractor.make_shareable(
|
|
20
|
+
Set[
|
|
21
|
+
"Array", "Hash", "Set", "Range", "Enumerable", "Enumerator", "Enumerator::Lazy",
|
|
22
|
+
"Enumerator::ArithmeticSequence", "Enumerator::Chain", "Enumerator::Product", "Dir",
|
|
23
|
+
"RBS::Unnamed::ENVClass"
|
|
24
|
+
].freeze
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# False only for a carrier known to be a core collection or enumerable: a `Tuple` / `HashShape` literal
|
|
28
|
+
# shape, a `Constant` `Range` or `Set`, a `Nominal` in {NOMINALS}, `Top`, and a `Dynamic` / `Difference` /
|
|
29
|
+
# `Refined` / `Union` / `Intersection` over those. Every other receiver is true, so the folds decline.
|
|
30
|
+
def project_receiver?(receiver)
|
|
31
|
+
case receiver
|
|
32
|
+
when Type::Tuple, Type::HashShape, Type::Top then false
|
|
33
|
+
when Type::Constant then !(receiver.value.is_a?(Range) || receiver.value.is_a?(::Set))
|
|
34
|
+
when Type::Nominal then !NOMINALS.include?(receiver.class_name)
|
|
35
|
+
when Type::Difference, Type::Refined then project_receiver?(receiver.base)
|
|
36
|
+
when Type::Union, Type::Intersection then receiver.members.any? { |member| project_receiver?(member) }
|
|
37
|
+
when Type::Dynamic then project_receiver?(receiver.static_facet)
|
|
38
|
+
else true
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|