rigortype 0.4.6 → 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 +4 -2
- data/data/core_overlay/bigdecimal.rbs +33 -0
- data/data/core_overlay/resolv.rbs +30 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +45 -13
- data/data/gem_overlay/addressable/addressable.rbs +40 -0
- data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
- data/docs/handbook/01-getting-started.md +6 -3
- data/docs/handbook/11-sig-gen.md +85 -3
- data/docs/handbook/appendix-phpstan.md +4 -4
- data/docs/install.md +20 -0
- data/docs/manual/01-installation.md +67 -26
- data/docs/manual/02-cli-reference.md +455 -113
- data/docs/manual/03-configuration.md +76 -5
- data/docs/manual/04-diagnostics.md +5 -4
- data/docs/manual/06-baseline.md +6 -4
- data/docs/manual/08-skills.md +11 -0
- data/docs/manual/09-editor-integration.md +21 -2
- data/docs/manual/10-mcp-server.md +38 -2
- data/docs/manual/12-caching.md +4 -3
- 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 +32 -15
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +23 -1
- data/docs/manual/plugins/rigor-sorbet.md +17 -3
- data/lib/rigor/analysis/baseline.rb +24 -27
- data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
- data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +12 -4
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +16 -4
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -1
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
- data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +41 -12
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
- data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
- data/lib/rigor/analysis/check_rules/rule_ids.rb +9 -2
- data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
- data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules.rb +720 -141
- data/lib/rigor/analysis/crash_signature.rb +45 -3
- data/lib/rigor/analysis/dependency_recorder.rb +237 -80
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
- data/lib/rigor/analysis/edit_replay.rb +93 -6
- data/lib/rigor/analysis/fact_store.rb +3 -3
- data/lib/rigor/analysis/incremental.rb +11 -1
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +4 -3
- data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
- data/lib/rigor/analysis/incremental_session.rb +306 -53
- data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
- data/lib/rigor/analysis/rule_catalog.rb +39 -3
- data/lib/rigor/analysis/run_cache_key.rb +27 -3
- data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
- data/lib/rigor/analysis/runner/declaration_position.rb +8 -8
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +264 -112
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +4 -9
- data/lib/rigor/analysis/runner/pool_coordinator.rb +173 -328
- data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
- data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
- data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
- data/lib/rigor/analysis/runner.rb +298 -109
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +62 -23
- data/lib/rigor/analysis/template_unit_positions.rb +2 -3
- data/lib/rigor/analysis/template_units.rb +9 -1
- data/lib/rigor/analysis/worker_session.rb +65 -71
- data/lib/rigor/bleeding_edge.rb +22 -10
- data/lib/rigor/builtins/imported_refinements.rb +6 -8
- data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
- data/lib/rigor/builtins/regex_refinement.rb +4 -6
- data/lib/rigor/cache/descriptor.rb +6 -5
- data/lib/rigor/cache/file_digest.rb +39 -31
- data/lib/rigor/cache/incremental_snapshot.rb +95 -17
- data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
- data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
- data/lib/rigor/cache/rbs_constant_table.rb +1 -1
- data/lib/rigor/cache/rbs_descriptor.rb +90 -4
- data/lib/rigor/cache/rbs_environment.rb +8 -1
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
- data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
- data/lib/rigor/cache/store.rb +11 -10
- data/lib/rigor/ci_detector.rb +1 -1
- data/lib/rigor/cli/annotate_command.rb +24 -19
- data/lib/rigor/cli/baseline_command.rb +160 -57
- data/lib/rigor/cli/catalogue.rb +256 -0
- data/lib/rigor/cli/check_command.rb +112 -104
- data/lib/rigor/cli/check_runner_factory.rb +2 -2
- data/lib/rigor/cli/command.rb +15 -1
- data/lib/rigor/cli/coverage_command.rb +21 -8
- data/lib/rigor/cli/coverage_mutation.rb +5 -7
- data/lib/rigor/cli/coverage_renderer.rb +2 -1
- data/lib/rigor/cli/coverage_scan.rb +3 -5
- data/lib/rigor/cli/diff_command.rb +25 -20
- data/lib/rigor/cli/docs_command.rb +1 -2
- data/lib/rigor/cli/doctor_command.rb +12 -12
- data/lib/rigor/cli/effects_command.rb +50 -40
- data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
- data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
- data/lib/rigor/cli/effects_renderer.rb +2 -1
- data/lib/rigor/cli/effects_snapshot_command.rb +26 -18
- data/lib/rigor/cli/explain_command.rb +18 -8
- data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
- data/lib/rigor/cli/json_document.rb +41 -0
- data/lib/rigor/cli/lens_command.rb +29 -10
- data/lib/rigor/cli/lsp_command.rb +250 -23
- data/lib/rigor/cli/mcp_command.rb +12 -7
- data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
- data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
- data/lib/rigor/cli/options.rb +40 -1
- data/lib/rigor/cli/plugin_command.rb +93 -46
- data/lib/rigor/cli/plugins_command.rb +17 -18
- data/lib/rigor/cli/plugins_renderer.rb +10 -6
- data/lib/rigor/cli/protection_fork_scan.rb +2 -2
- data/lib/rigor/cli/protection_renderer.rb +2 -1
- data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
- data/lib/rigor/cli/sig_gen_command.rb +92 -27
- data/lib/rigor/cli/skill_command.rb +32 -3
- data/lib/rigor/cli/skill_deep_probe.rb +1 -1
- data/lib/rigor/cli/skill_describe.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +14 -9
- data/lib/rigor/cli/triage_command.rb +10 -11
- data/lib/rigor/cli/triage_renderer.rb +2 -1
- data/lib/rigor/cli/type_of_command.rb +16 -11
- data/lib/rigor/cli/type_of_project_scope.rb +1 -4
- data/lib/rigor/cli/type_of_renderer.rb +4 -1
- data/lib/rigor/cli/type_scan_command.rb +14 -9
- data/lib/rigor/cli/type_scan_renderer.rb +2 -1
- data/lib/rigor/cli/unused_command.rb +27 -17
- data/lib/rigor/cli/upgrade_command.rb +5 -0
- data/lib/rigor/cli.rb +214 -56
- data/lib/rigor/config_audit.rb +33 -19
- data/lib/rigor/configuration/severity_profile.rb +14 -5
- data/lib/rigor/configuration.rb +104 -35
- data/lib/rigor/effects/collector.rb +53 -21
- data/lib/rigor/effects/definition_context.rb +0 -2
- data/lib/rigor/effects/envelope.rb +1 -1
- data/lib/rigor/effects/framework_units.rb +1 -1
- data/lib/rigor/effects/mutation_classifier.rb +4 -7
- data/lib/rigor/effects/propagator.rb +1 -1
- data/lib/rigor/effects/registry.rb +2 -2
- data/lib/rigor/effects/snapshot.rb +1 -1
- data/lib/rigor/effects/taint_cause.rb +0 -2
- data/lib/rigor/effects/unit_scan.rb +9 -13
- data/lib/rigor/environment/bundle_sig_discovery.rb +64 -33
- data/lib/rigor/environment/class_registry.rb +3 -3
- data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
- data/lib/rigor/environment/core_root.rb +77 -0
- data/lib/rigor/environment/default_libraries.rb +18 -1
- data/lib/rigor/environment/deferred_signature_plan.rb +381 -0
- data/lib/rigor/environment/gated_signature_guard.rb +1 -1
- data/lib/rigor/environment/gated_signature_plan.rb +5 -2
- data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
- data/lib/rigor/environment/lockfile_resolver.rb +28 -12
- data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
- data/lib/rigor/environment/member_consistency.rb +56 -7
- data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
- data/lib/rigor/environment/project_path_gems.rb +124 -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 +12 -15
- data/lib/rigor/environment/rbs_loader.rb +772 -110
- data/lib/rigor/environment/reflection.rb +7 -7
- data/lib/rigor/environment/required_features.rb +0 -3
- data/lib/rigor/environment/ruby_library_roots.rb +1 -1
- data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
- data/lib/rigor/environment.rb +169 -42
- data/lib/rigor/flow_contribution/merger.rb +4 -4
- data/lib/rigor/inference/acceptance.rb +136 -40
- 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_parameter_binder.rb +2 -3
- data/lib/rigor/inference/block_run_count.rb +185 -0
- data/lib/rigor/inference/budget_trace.rb +8 -2
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
- data/lib/rigor/inference/callee_write_census.rb +815 -0
- 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 +258 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
- data/lib/rigor/inference/declaration_walk.rb +4 -0
- data/lib/rigor/inference/deep_erasure.rb +39 -7
- data/lib/rigor/inference/def_handle.rb +1 -1
- data/lib/rigor/inference/def_node_resolver.rb +33 -15
- data/lib/rigor/inference/def_return_typer.rb +47 -5
- data/lib/rigor/inference/definer_resolution.rb +19 -9
- data/lib/rigor/inference/definers_index.rb +162 -0
- data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
- data/lib/rigor/inference/element_read_widening.rb +49 -3
- data/lib/rigor/inference/error_info.rb +16 -0
- data/lib/rigor/inference/expression_typer.rb +1559 -320
- data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
- 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 +81 -5
- data/lib/rigor/inference/global_writer_reach.rb +260 -0
- data/lib/rigor/inference/hkt_body.rb +1 -2
- data/lib/rigor/inference/hkt_body_parser.rb +3 -5
- data/lib/rigor/inference/hkt_reducer.rb +2 -2
- data/lib/rigor/inference/hkt_registry.rb +3 -5
- data/lib/rigor/inference/in_effect_refinements.rb +7 -0
- data/lib/rigor/inference/indexed_narrowing.rb +61 -15
- data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
- data/lib/rigor/inference/ivar_writer_census.rb +154 -8
- data/lib/rigor/inference/key_presence_guard.rb +51 -24
- 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 +66 -14
- data/lib/rigor/inference/match_rebinding/frame.rb +57 -0
- data/lib/rigor/inference/match_rebinding/yielding_match.rb +101 -0
- data/lib/rigor/inference/match_rebinding.rb +28 -5
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +44 -30
- data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -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/iterator_dispatch.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +51 -27
- data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +606 -78
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +467 -84
- data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
- data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher.rb +123 -25
- 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 +24 -3
- data/lib/rigor/inference/multi_target_binder.rb +2 -2
- data/lib/rigor/inference/mutation_rejoin.rb +240 -4
- data/lib/rigor/inference/mutation_widening.rb +199 -29
- data/lib/rigor/inference/narrowing.rb +238 -50
- data/lib/rigor/inference/operand_effects.rb +43 -2
- data/lib/rigor/inference/optimistic_origin.rb +134 -9
- data/lib/rigor/inference/origin_lookup.rb +1 -1
- data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
- data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
- data/lib/rigor/inference/pre_eval_constants.rb +1 -1
- data/lib/rigor/inference/precision_scanner.rb +1 -1
- data/lib/rigor/inference/project_method_ownership.rb +30 -9
- 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/project_yielder.rb +11 -17
- data/lib/rigor/inference/rbs_type_translator.rb +68 -15
- data/lib/rigor/inference/receiver_alias.rb +20 -0
- 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 +57 -17
- data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -16
- 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 +287 -0
- data/lib/rigor/inference/remover_narrowing.rb +224 -0
- data/lib/rigor/inference/rewrite_mutation.rb +53 -2
- data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
- data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +70 -26
- data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
- data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
- data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +295 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
- data/lib/rigor/inference/scope_indexer.rb +2213 -226
- data/lib/rigor/inference/self_call_ivar_writes.rb +103 -12
- data/lib/rigor/inference/self_preserving_block.rb +87 -0
- data/lib/rigor/inference/set_mutation.rb +189 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +42 -8
- data/lib/rigor/inference/singleton_object_constant.rb +32 -2
- data/lib/rigor/inference/statement_evaluator.rb +3268 -358
- data/lib/rigor/inference/string_mutation.rb +14 -0
- data/lib/rigor/inference/struct_fold_safety.rb +4 -6
- data/lib/rigor/inference/synthetic_method_scanner.rb +40 -17
- data/lib/rigor/inference/toplevel_def_census.rb +67 -0
- data/lib/rigor/inference/unknown_store_widening.rb +103 -4
- data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
- data/lib/rigor/inference/untyped_rejoin.rb +1 -1
- data/lib/rigor/inference/void_tail_summary.rb +29 -9
- data/lib/rigor/language_server/buffer_table.rb +3 -3
- data/lib/rigor/language_server/completion_provider.rb +26 -20
- data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
- data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
- data/lib/rigor/language_server/folding_range_provider.rb +1 -2
- data/lib/rigor/language_server/hover_provider.rb +1 -7
- data/lib/rigor/language_server/hover_renderer.rb +6 -8
- data/lib/rigor/language_server/project_context.rb +39 -1
- data/lib/rigor/language_server/publish_batcher.rb +3 -4
- data/lib/rigor/language_server/selection_range_provider.rb +2 -4
- data/lib/rigor/language_server/server.rb +7 -3
- data/lib/rigor/language_server/signature_help_provider.rb +4 -4
- 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/plugins.rb +1 -1
- 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/mcp/server.rb +73 -2
- data/lib/rigor/mcp.rb +1 -1
- data/lib/rigor/memo_audit.rb +282 -0
- data/lib/rigor/plugin/access_denied_error.rb +1 -2
- data/lib/rigor/plugin/base.rb +15 -2
- data/lib/rigor/plugin/bundled_catalog.rb +1 -12
- data/lib/rigor/plugin/io_boundary.rb +26 -4
- data/lib/rigor/plugin/loader.rb +25 -8
- data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
- data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
- data/lib/rigor/plugin/manifest.rb +16 -16
- data/lib/rigor/plugin/registry.rb +16 -5
- data/lib/rigor/plugin/services.rb +2 -3
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
- data/lib/rigor/plugin/trust_policy.rb +48 -2
- data/lib/rigor/plugin/type_node_resolver.rb +2 -4
- data/lib/rigor/plugin.rb +6 -8
- data/lib/rigor/plugin_gap_advisory.rb +5 -1
- data/lib/rigor/project_environment.rb +16 -2
- data/lib/rigor/protection/analysis_guard.rb +1 -1
- data/lib/rigor/protection/discovery_seed.rb +44 -5
- data/lib/rigor/protection/mutation_cache.rb +4 -1
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
- data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
- data/lib/rigor/rbs_extended/reporter.rb +27 -3
- data/lib/rigor/rbs_extended.rb +2 -3
- data/lib/rigor/reflection/constant_ancestors.rb +81 -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 +32 -1
- data/lib/rigor/run_scoped_slots.rb +38 -0
- data/lib/rigor/runtime/jit.rb +1 -1
- data/lib/rigor/scope/discovery_index.rb +168 -8
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
- data/lib/rigor/scope/resolution_chain.rb +901 -54
- data/lib/rigor/scope.rb +644 -37
- data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
- data/lib/rigor/sig_gen/classification.rb +18 -1
- data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
- data/lib/rigor/sig_gen/generator.rb +588 -66
- data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
- data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
- data/lib/rigor/sig_gen/layout_index.rb +3 -3
- data/lib/rigor/sig_gen/method_candidate.rb +37 -4
- data/lib/rigor/sig_gen/observation_collector.rb +14 -7
- data/lib/rigor/sig_gen/observed_call.rb +1 -1
- data/lib/rigor/sig_gen/path_mapper.rb +6 -0
- data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
- data/lib/rigor/sig_gen/renderer.rb +15 -9
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +62 -2
- data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
- data/lib/rigor/sig_gen/write_reporter.rb +63 -14
- data/lib/rigor/sig_gen/write_result.rb +23 -12
- data/lib/rigor/sig_gen/writer.rb +238 -37
- 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/acceptance_router.rb +2 -2
- data/lib/rigor/type/accepts_result.rb +1 -1
- data/lib/rigor/type/app.rb +4 -6
- data/lib/rigor/type/combinator.rb +173 -21
- data/lib/rigor/type/constant.rb +4 -5
- data/lib/rigor/type/hash_shape.rb +21 -40
- data/lib/rigor/type/interface.rb +60 -0
- data/lib/rigor/type/union.rb +1 -1
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_node/identifier.rb +1 -2
- data/lib/rigor/type_node/resolver_chain.rb +1 -1
- data/lib/rigor/type_node/string_literal.rb +1 -2
- data/lib/rigor/type_node/symbol_literal.rb +1 -2
- data/lib/rigor/type_node/union.rb +3 -6
- 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/value_semantics.rb +1 -10
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +3 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +115 -36
- 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/effects.rb +1 -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-activejob/lib/rigor/plugin/activejob.rb +4 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +132 -33
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -6
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +54 -17
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +11 -14
- data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +30 -9
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +55 -13
- 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-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +8 -7
- data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +8 -2
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +200 -4
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +4 -3
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +5 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +210 -3
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +97 -24
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -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 +4 -8
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
- data/sig/rigor/analysis/baseline.rbs +0 -3
- data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
- data/sig/rigor/analysis/fact_store.rbs +2 -2
- data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
- data/sig/rigor/cache/descriptor.rbs +14 -0
- data/sig/rigor/cli/diff_command.rbs +1 -1
- data/sig/rigor/cli/explain_command.rbs +1 -1
- data/sig/rigor/effects/registry.rbs +2 -2
- data/sig/rigor/environment.rbs +4 -1
- data/sig/rigor/inference.rbs +4 -1
- data/sig/rigor/plugin/additional_initializer.rbs +0 -5
- data/sig/rigor/plugin/base.rbs +53 -18
- data/sig/rigor/plugin/effect_attribution.rbs +3 -3
- data/sig/rigor/plugin/effect_edge.rbs +1 -1
- data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
- data/sig/rigor/plugin/interfaces.rbs +51 -0
- data/sig/rigor/plugin/io_boundary.rbs +3 -3
- data/sig/rigor/plugin/manifest.rbs +9 -9
- data/sig/rigor/plugin/node_context.rbs +21 -0
- data/sig/rigor/plugin/protocol_contract.rbs +3 -7
- data/sig/rigor/reflection.rbs +37 -0
- data/sig/rigor/scope.rbs +43 -4
- 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-plugin-author/SKILL.md +4 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
- data/skills/rigor-plugin-review/SKILL.md +3 -3
- data/skills/rigor-project-init/references/01-detect.md +1 -0
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +17 -2
- 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 +82 -3
|
@@ -14,7 +14,10 @@ require_relative "block_call_timing"
|
|
|
14
14
|
require_relative "block_repetition"
|
|
15
15
|
require_relative "block_parameter_binder"
|
|
16
16
|
require_relative "body_fixpoint"
|
|
17
|
+
require_relative "../memo_audit"
|
|
17
18
|
require_relative "callee_mutation_chain"
|
|
19
|
+
require_relative "toplevel_def_census"
|
|
20
|
+
require_relative "callee_write_census"
|
|
18
21
|
require_relative "captured_locals"
|
|
19
22
|
require_relative "dynamic_origin"
|
|
20
23
|
require_relative "error_info"
|
|
@@ -25,11 +28,15 @@ require_relative "../analysis/check_rules/inferred_param_guard"
|
|
|
25
28
|
require_relative "../analysis/check_rules/published_constant_guard"
|
|
26
29
|
require_relative "struct_fold_safety"
|
|
27
30
|
require_relative "closure_escape_analyzer"
|
|
31
|
+
require_relative "self_preserving_block"
|
|
28
32
|
require_relative "content_join"
|
|
33
|
+
require_relative "construction_escape"
|
|
29
34
|
require_relative "define_method_block_self"
|
|
30
35
|
require_relative "macro_block_self_type"
|
|
36
|
+
require_relative "global_writer_reach"
|
|
31
37
|
require_relative "guard_rebinding"
|
|
32
38
|
require_relative "self_call_ivar_writes"
|
|
39
|
+
require_relative "direct_ivar_writes"
|
|
33
40
|
require_relative "intervening_ivar_reset"
|
|
34
41
|
require_relative "match_rebinding"
|
|
35
42
|
require_relative "clause_widening"
|
|
@@ -42,6 +49,7 @@ require_relative "method_dispatcher"
|
|
|
42
49
|
require_relative "method_parameter_binder"
|
|
43
50
|
require_relative "multi_target_binder"
|
|
44
51
|
require_relative "mutation_widening"
|
|
52
|
+
require_relative "out_buffer_mutation"
|
|
45
53
|
require_relative "narrowing"
|
|
46
54
|
require_relative "operand_effects"
|
|
47
55
|
require_relative "operand_walk"
|
|
@@ -54,6 +62,8 @@ require_relative "rewrite_mutation"
|
|
|
54
62
|
require_relative "unknown_store_widening"
|
|
55
63
|
require_relative "untyped_rejoin"
|
|
56
64
|
require_relative "version_guard"
|
|
65
|
+
require_relative "../run_scoped_slots"
|
|
66
|
+
require_relative "../typing_state"
|
|
57
67
|
|
|
58
68
|
module Rigor
|
|
59
69
|
module Inference
|
|
@@ -186,13 +196,22 @@ module Rigor
|
|
|
186
196
|
# return type. The flow value of a `return` is still `Bot` (it transfers control rather than producing a value);
|
|
187
197
|
# the sink only records what the method *returns* through that edge. nil means "not collecting" — a top-level /
|
|
188
198
|
# DSL-block walk, or inside a nested `def` barrier (whose returns belong to the inner method).
|
|
189
|
-
|
|
190
|
-
|
|
199
|
+
#
|
|
200
|
+
# ADR-129 WD1 — the installation is a Frame and each record is a Resource touch.
|
|
201
|
+
RETURN_SINK = TypingState.frame(
|
|
202
|
+
:return_sink,
|
|
203
|
+
contents: :resource, reason: "installed per method body; each `return` records into it",
|
|
204
|
+
witness: "spec/fixtures/typing_state_witnesses/return_sink.rb"
|
|
205
|
+
)
|
|
206
|
+
private_constant :RETURN_SINK
|
|
191
207
|
|
|
192
208
|
# Thread-local flag: set while a side pass ({.provisional_returns}) re-types part of a body from a scope other
|
|
193
209
|
# than the one the statement walk holds, so the `return`s it reaches are filed as provisional ({ReturnSink}).
|
|
194
|
-
|
|
195
|
-
|
|
210
|
+
PROVISIONAL_RETURNS = TypingState.frame(
|
|
211
|
+
:provisional_returns, reason: "a flag installed around a side pass",
|
|
212
|
+
witness: "spec/fixtures/typing_state_witnesses/provisional_returns.rb"
|
|
213
|
+
)
|
|
214
|
+
private_constant :PROVISIONAL_RETURNS
|
|
196
215
|
|
|
197
216
|
# The `return` sink {.with_return_sink} installs. Issue #2299 — a `return` the statement walk records is typed
|
|
198
217
|
# with its real bindings; one a side pass records ({.provisional_returns}) may read a name the pass skipped the
|
|
@@ -207,8 +226,9 @@ module Rigor
|
|
|
207
226
|
@provisional = []
|
|
208
227
|
end
|
|
209
228
|
|
|
210
|
-
|
|
211
|
-
|
|
229
|
+
# `provisional` is the {PROVISIONAL_RETURNS} frame the evaluator reached the `return` under.
|
|
230
|
+
def record(node, type, provisional)
|
|
231
|
+
if provisional
|
|
212
232
|
@provisional << [node, type]
|
|
213
233
|
else
|
|
214
234
|
@types << type
|
|
@@ -226,14 +246,18 @@ module Rigor
|
|
|
226
246
|
# Thread-local sink (an Array of `[NextNode, Type]`) collecting the value each `next` carries out of the block it
|
|
227
247
|
# leaves, so `ExpressionTyper#type_block_body` can join those arms into the block's value type. Issue #841: the
|
|
228
248
|
# block-return pass modelled only the fall-through tail, so `ops.all? { |o| next false unless o; true }` read as
|
|
229
|
-
# `Constant[true]` and folded the call to always-truthy — the same defect `
|
|
249
|
+
# `Constant[true]` and folded the call to always-truthy — the same defect `RETURN_SINK` fixes one level up for
|
|
230
250
|
# a method's early `return`. The flow value of a `next` stays `Bot`.
|
|
231
251
|
#
|
|
232
252
|
# The sink also collects `next`s from nested blocks / loops / defs evaluated under the same installation (they do
|
|
233
253
|
# not install their own), so the consumer filters by node identity against a statically computed
|
|
234
254
|
# directly-targeting set, exactly as the break sink does. nil means "not collecting".
|
|
235
|
-
|
|
236
|
-
|
|
255
|
+
NEXT_SINK = TypingState.frame(
|
|
256
|
+
:next_sink,
|
|
257
|
+
contents: :resource, reason: "installed per block body; each `next` records into it",
|
|
258
|
+
witness: "spec/fixtures/typing_state_witnesses/jump_sinks.rb"
|
|
259
|
+
)
|
|
260
|
+
private_constant :NEXT_SINK
|
|
237
261
|
|
|
238
262
|
# Thread-local sink (an Array of `[BreakNode, Scope]`) collecting the scope at each `break` reached while
|
|
239
263
|
# evaluating a loop body, so `eval_loop` / `eval_for` can join a `break`-path binding (`flag = true; break`) into
|
|
@@ -241,21 +265,33 @@ module Rigor
|
|
|
241
265
|
# installs its own sink, restored on exit, so an inner loop's break does not leak to the outer one. A `break`
|
|
242
266
|
# inside a block / nested loop targets that inner construct, not the lexical loop — filtered out by the loop's
|
|
243
267
|
# {JumpTargets} set ({#loop_jumps}). See docs/notes/20260615-loop-break-binding-propagation-design.md.
|
|
244
|
-
|
|
245
|
-
|
|
268
|
+
#
|
|
269
|
+
# ADR-129 WD3 — a construct reads the sink's size before it runs and again after, to find the jumps it recorded
|
|
270
|
+
# itself ({#restoring_error_info}, {#ensure_jump_marks}), so a read of the contents is delta-internal.
|
|
271
|
+
BREAK_SINK = TypingState.frame(
|
|
272
|
+
:break_sink,
|
|
273
|
+
contents: :resource, read_exemption: :delta_internal,
|
|
274
|
+
reason: "installed per loop body; each `break` records into it, and an enclosing `ensure` rewrites them",
|
|
275
|
+
witness: "spec/fixtures/typing_state_witnesses/jump_sinks.rb"
|
|
276
|
+
)
|
|
277
|
+
private_constant :BREAK_SINK
|
|
246
278
|
|
|
247
279
|
# Thread-local sink (an Array of `[BreakNode, Type]`) collecting the value each `break` carries out of the
|
|
248
280
|
# construct it leaves. Issue #853: `break value` terminates the yielding CALL and is that call's value, so —
|
|
249
|
-
# unlike `next`, which {
|
|
281
|
+
# unlike `next`, which {NEXT_SINK} joins into the block — these arms are consumed one level up, by
|
|
250
282
|
# `ExpressionTyper#call_dispatch_type_for`. Without them `ops.all? { |o| break false unless o; true }` folded to
|
|
251
283
|
# `Constant[true]` and warned about a program that really can answer false.
|
|
252
284
|
#
|
|
253
|
-
# Separate from {
|
|
285
|
+
# Separate from {BREAK_SINK}, which records SCOPES for the loop-continuation join: the two consumers want
|
|
254
286
|
# different things from the same node, and a loop body evaluated under a call's collection must keep feeding its
|
|
255
287
|
# own sink. Both are filtered by node identity against a statically computed directly-targeting set, so a `break`
|
|
256
288
|
# that belongs to an inner loop or block never reaches the wrong consumer. nil means "not collecting".
|
|
257
|
-
|
|
258
|
-
|
|
289
|
+
BREAK_VALUE_SINK = TypingState.frame(
|
|
290
|
+
:break_value_sink,
|
|
291
|
+
contents: :resource, reason: "installed per yielding call; each `break` records into it",
|
|
292
|
+
witness: "spec/fixtures/typing_state_witnesses/jump_sinks.rb"
|
|
293
|
+
)
|
|
294
|
+
private_constant :BREAK_VALUE_SINK
|
|
259
295
|
|
|
260
296
|
# Lexical class frame: the `name:` field is the qualified class name as it would render in Ruby (e.g.,
|
|
261
297
|
# `"Foo::Bar"`); the `singleton:` field is `true` for `class << self` frames so nested defs resolve to
|
|
@@ -348,18 +384,9 @@ module Rigor
|
|
|
348
384
|
# so a `def` evaluated inside another method's body (which itself installed a sink) does not corrupt the outer
|
|
349
385
|
# one. Used by `ExpressionTyper#infer_user_method_return` to join the explicit returns into the inferred
|
|
350
386
|
# method-return type.
|
|
351
|
-
def self.with_return_sink
|
|
352
|
-
previous = Thread.current[RETURN_SINK_KEY]
|
|
353
|
-
previous_provisional = Thread.current[PROVISIONAL_RETURNS_KEY]
|
|
387
|
+
def self.with_return_sink(&)
|
|
354
388
|
sink = ReturnSink.new
|
|
355
|
-
|
|
356
|
-
Thread.current[PROVISIONAL_RETURNS_KEY] = nil
|
|
357
|
-
begin
|
|
358
|
-
result = yield
|
|
359
|
-
ensure
|
|
360
|
-
Thread.current[RETURN_SINK_KEY] = previous
|
|
361
|
-
Thread.current[PROVISIONAL_RETURNS_KEY] = previous_provisional
|
|
362
|
-
end
|
|
389
|
+
result = TypingState.current.with_pair(RETURN_SINK, sink, PROVISIONAL_RETURNS, nil, &)
|
|
363
390
|
[result, sink.types]
|
|
364
391
|
end
|
|
365
392
|
|
|
@@ -367,48 +394,54 @@ module Rigor
|
|
|
367
394
|
# on exit. Issue #2299 — {ExpressionTyper#type_block_body}'s tail-only answer types `return r if r` without the
|
|
368
395
|
# `r = yield e` before it, so `r` read `Dynamic[top]` and that went into the enclosing method's return type
|
|
369
396
|
# although the statement walk of the same block recorded `r` with its real binding.
|
|
370
|
-
def self.provisional_returns
|
|
371
|
-
|
|
372
|
-
Thread.current[PROVISIONAL_RETURNS_KEY] = true
|
|
373
|
-
begin
|
|
374
|
-
yield
|
|
375
|
-
ensure
|
|
376
|
-
Thread.current[PROVISIONAL_RETURNS_KEY] = previous
|
|
377
|
-
end
|
|
397
|
+
def self.provisional_returns(&)
|
|
398
|
+
TypingState.current.with(PROVISIONAL_RETURNS, true, &)
|
|
378
399
|
end
|
|
379
400
|
|
|
380
401
|
# Runs `block` with a fresh `next` sink installed, then yields the collected `[NextNode, Type]` pairs to the
|
|
381
402
|
# caller. Stacks like the return sink, and for the same reason: a block body evaluated inside another block body's
|
|
382
403
|
# evaluation must not spill its arms into the outer collection. Used by `ExpressionTyper#type_block_body` to join
|
|
383
404
|
# the `next` arms into the block's value type.
|
|
384
|
-
def self.with_next_sink
|
|
385
|
-
previous = Thread.current[NEXT_SINK_KEY]
|
|
405
|
+
def self.with_next_sink(&)
|
|
386
406
|
sink = []
|
|
387
|
-
|
|
388
|
-
begin
|
|
389
|
-
result = yield
|
|
390
|
-
ensure
|
|
391
|
-
Thread.current[NEXT_SINK_KEY] = previous
|
|
392
|
-
end
|
|
407
|
+
result = TypingState.current.with(NEXT_SINK, sink, &)
|
|
393
408
|
[result, sink]
|
|
394
409
|
end
|
|
395
410
|
|
|
411
|
+
# Runs `block` with none of {CALLER_SINKS} installed: a user-method body re-typed for its return starts outside
|
|
412
|
+
# its caller's constructs (ADR-129 WD1: the return memo forces these Frames at its entry). Installs only when one
|
|
413
|
+
# is set, read with `forcing_read`, so the common entry costs no frame.
|
|
414
|
+
def self.outside_caller_sinks(&)
|
|
415
|
+
state = TypingState.current
|
|
416
|
+
return yield if CALLER_SINKS.all? { |slot| state.forcing_read(slot).nil? }
|
|
417
|
+
|
|
418
|
+
state.without(CALLER_SINKS, &)
|
|
419
|
+
end
|
|
420
|
+
|
|
396
421
|
# Runs `block` with a fresh `break`-value sink installed, then yields the collected `[BreakNode, Type]` pairs to
|
|
397
422
|
# the caller. Stacks like the other sinks: a block body evaluated inside another block body's evaluation must not
|
|
398
423
|
# spill its arms into the outer collection. Used by `ExpressionTyper#call_dispatch_type_for` to union the `break`
|
|
399
424
|
# arms into the yielding call's type (issue #853).
|
|
400
|
-
def self.with_break_value_sink
|
|
401
|
-
previous = Thread.current[BREAK_VALUE_SINK_KEY]
|
|
425
|
+
def self.with_break_value_sink(&)
|
|
402
426
|
sink = []
|
|
403
|
-
|
|
404
|
-
begin
|
|
405
|
-
result = yield
|
|
406
|
-
ensure
|
|
407
|
-
Thread.current[BREAK_VALUE_SINK_KEY] = previous
|
|
408
|
-
end
|
|
427
|
+
result = TypingState.current.with(BREAK_VALUE_SINK, sink, &)
|
|
409
428
|
[result, sink]
|
|
410
429
|
end
|
|
411
430
|
|
|
431
|
+
# Issue #1246 — enters only `call`'s block, from the receiver scope, as the statement form enters it
|
|
432
|
+
# ({#evaluate_block_if_present}): the per-node scope index's evaluation of a block a value-position call holds
|
|
433
|
+
# ({ScopeIndexer#evaluate_unentered_block}). The call's receiver and arguments are left to the index walk.
|
|
434
|
+
#
|
|
435
|
+
# Such a block enters with the unknown-`self` mark only ({Scope#with_block_self_unknown}), as the walk entered
|
|
436
|
+
# it, not with the statement form's opaque-`self` mark ({Scope#entering_opaque_block}): issue #1717 keeps the
|
|
437
|
+
# latter, which gates binding a call to a cross-file top-level `def`, away from value-position blocks.
|
|
438
|
+
def evaluate_block_of(call)
|
|
439
|
+
@value_position_block = call.block
|
|
440
|
+
evaluate_block_if_present(call)
|
|
441
|
+
ensure
|
|
442
|
+
@value_position_block = nil
|
|
443
|
+
end
|
|
444
|
+
|
|
412
445
|
# Evaluate `node` under the receiver scope. Returns `[type, scope']` where `type` is the value the node produces
|
|
413
446
|
# and `scope'` is the scope observable after the node has run. The receiver scope is never mutated.
|
|
414
447
|
def evaluate(node)
|
|
@@ -517,9 +550,13 @@ module Rigor
|
|
|
517
550
|
# def hands on to a self-call counts whether or not the callee resolves: the session's empty scope resolves no
|
|
518
551
|
# callee, and without it an edit that starts or stops forwarding a parameter to a mutating callee would leave
|
|
519
552
|
# the effects equal and the callers' floors stale.
|
|
553
|
+
#
|
|
554
|
+
# Issue #2296 — the parameters the def mutates inside a block, or through a Set-only adder, count too: they
|
|
555
|
+
# floor the caller's argument through {#floor_callee_escaped_args_for_call}'s element-keeping channels.
|
|
520
556
|
def content_mutated_parameter_positions(def_node)
|
|
521
|
-
slots =
|
|
522
|
-
census = callee_content_mutated_parameters(def_node).values
|
|
557
|
+
slots = forwarding_slots(def_node)
|
|
558
|
+
census = callee_content_mutated_parameters(def_node).values +
|
|
559
|
+
own_block_content_parameters(def_node).values + own_set_content_parameters(def_node).values
|
|
523
560
|
each_forwarding_call(def_node, slots) { |_, forwarded| census += forwarded.map { |name, _| slots[name] } }
|
|
524
561
|
positions, keywords = census.uniq.partition { |slot| slot.is_a?(Integer) }
|
|
525
562
|
positions.sort + keywords.sort
|
|
@@ -531,25 +568,703 @@ module Rigor
|
|
|
531
568
|
# callee floor ({#widen_callee_escaped_argument_captures}) floors after the call, and the ones
|
|
532
569
|
# {CapturedLocals.content_mutations} reports as a block's callee-mutated captures.
|
|
533
570
|
def content_mutated_arguments(call_node)
|
|
534
|
-
|
|
571
|
+
callees = argument_mutating_callees(call_node)
|
|
572
|
+
return NO_ARGUMENT_READS if callees.empty?
|
|
573
|
+
|
|
574
|
+
reads = callees.flat_map do |def_node, offset, owner|
|
|
575
|
+
arguments_for_slots(call_node, census_of(owner, :callee_content_mutated_parameters, def_node), offset)
|
|
576
|
+
end
|
|
577
|
+
callees.size == 1 ? reads : reads.uniq
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
# The `[def, offset]` pairs a self-dispatch `call_node` that passes a local resolves to
|
|
581
|
+
# ({#resolve_self_callees}) — the defs whose census the callee floor reads; empty for any other call.
|
|
582
|
+
#
|
|
583
|
+
# Issue #2336 — a call no def answers that runs code the floor cannot see against its arguments (`pr.call(acc)`,
|
|
584
|
+
# `obj.method(:poke).call(acc)`, `send(name, acc)`; {#opaque_sink_call?}) answers {OPAQUE_CALLEES}: every
|
|
585
|
+
# argument it passes reads as written.
|
|
586
|
+
def argument_mutating_callees(call_node)
|
|
587
|
+
unless self_dispatch_call?(call_node)
|
|
588
|
+
found = receiver_callees(call_node)
|
|
589
|
+
return found unless found.empty? && opaque_sink_call?(call_node)
|
|
590
|
+
|
|
591
|
+
return call_passes_local_argument?(call_node) ? OPAQUE_CALLEES : NO_CALLEES
|
|
592
|
+
end
|
|
535
593
|
# Fast path — only a local passed as an argument can be reported, so a call with none skips the def
|
|
536
594
|
# resolution and the body scan entirely (the overwhelming common case).
|
|
537
|
-
return
|
|
595
|
+
return NO_CALLEES unless call_passes_local_argument?(call_node)
|
|
596
|
+
|
|
597
|
+
found = resolve_self_callees(call_node)
|
|
598
|
+
found.empty? && opaque_sink_call?(call_node) ? OPAQUE_CALLEES : found
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
# True for a call whose arguments go to code no def shows: `call` / `instance_exec` / `class_exec` /
|
|
602
|
+
# `module_exec` on an explicit receiver (a Proc, a Method, a lambda), or a `send` whose target is no literal
|
|
603
|
+
# ({CalleeWriteCensus}, which reads the same calls inside a callee).
|
|
604
|
+
def opaque_sink_call?(call_node)
|
|
605
|
+
name = call_node.name
|
|
606
|
+
return !CalleeWriteCensus.literal_send?(call_node) if SEND_HAND_OFFS.include?(name)
|
|
607
|
+
return false unless OPAQUE_SINKS.include?(name)
|
|
608
|
+
|
|
609
|
+
# A receiverless `call` is a self-call to a def of that name; `instance_exec` and the rest run their block on
|
|
610
|
+
# any receiver.
|
|
611
|
+
name != :call || !self_dispatch_call?(call_node)
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
OPAQUE_SINKS = %i[call instance_exec class_exec module_exec].to_set.freeze
|
|
615
|
+
private_constant :OPAQUE_SINKS
|
|
616
|
+
|
|
617
|
+
# Issue #2510, #2515 — the `[def, 0, owner]` triple an explicit-receiver `recv.name(local)` call resolves to when
|
|
618
|
+
# `recv` types as a project class object (`Const`, a bare constant or any constant path, or a local holding
|
|
619
|
+
# one), whose singleton chain has `name` (`def self.name`, `class << self`, `module_function`, an `extend`ed
|
|
620
|
+
# module, the project superclass's), or as an instance of a project class (`Opts.new.add_format(...)`,
|
|
621
|
+
# `h = Opts.new; h.add_format(...)`), whose instance chain has `name`: the same census a self-call reads then
|
|
622
|
+
# floors the passed local, so `Opts.add_format(opts, options)` handing `options` to a block that writes it
|
|
623
|
+
# leaves the literal behind. `owner` is a {CalleeOwner}: the receiver type the callee's own hand-offs
|
|
624
|
+
# (`really(parser, options)`) resolve self-calls in, not the caller's class. `resolved_census` keys the shared
|
|
625
|
+
# memo by the receiver's class and the def node, whose recorded nesting its constants resolve in. A def the
|
|
626
|
+
# receiver inherits (a superclass's, an included module's) resolves to {FLOOR_ALL} ({#walked_owner}). A receiver
|
|
627
|
+
# of any other type resolves to nothing, as does `Const.new` of a class that defines its own `self.new`; `send`
|
|
628
|
+
# hand-offs are not followed through an explicit receiver. The hand-offs inside a callee, whose arguments are
|
|
629
|
+
# parameters, take {#hand_off_receiver_callees}.
|
|
630
|
+
def receiver_callees(call_node)
|
|
631
|
+
return NO_CALLEES if call_node.receiver.nil?
|
|
632
|
+
return NO_CALLEES unless call_passes_local_argument?(call_node)
|
|
633
|
+
|
|
634
|
+
receiver_callees_in(scope, call_node)
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
# {#receiver_callees} for one scope `call_node`'s receiver (or `receiver`, the constant form a callee's local
|
|
638
|
+
# receiver was assigned) is typed in.
|
|
639
|
+
def receiver_callees_in(typing_scope, call_node, receiver = call_node.receiver)
|
|
640
|
+
return NO_CALLEES if overridden_constructor?(receiver, typing_scope)
|
|
641
|
+
|
|
642
|
+
receiver_type = typing_scope.type_of(receiver)
|
|
643
|
+
own = callees_for_receiver_type(receiver_type, call_node.name)
|
|
644
|
+
return own unless own.empty? && send_hand_off?(call_node)
|
|
645
|
+
|
|
646
|
+
shifted_callees(callees_for_receiver_type(receiver_type, send_target(call_node)))
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
# Issue #2336 — the method a `send` hand-off names (`recv.send(:poke, h)`, `send("poke", h)`).
|
|
650
|
+
def send_target(call_node) = call_node.arguments.arguments.first.unescaped.to_sym
|
|
651
|
+
|
|
652
|
+
# `callees` with each offset one further: the callees of a `send` hand-off's target, whose first argument is the
|
|
653
|
+
# name. A capped answer stays as it is.
|
|
654
|
+
def shifted_callees(callees)
|
|
655
|
+
return callees if callees.empty? || callees.equal?(CAPPED_HAND_OFF)
|
|
656
|
+
|
|
657
|
+
shifted = callees.map { |def_node, offset, owner| [def_node, offset + 1, owner] }
|
|
658
|
+
callees.is_a?(UnionCallees) ? UnionCallees.new(shifted) : shifted
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
# `[def, 0, owner]` for `name` called on a receiver of `receiver_type`, or none.
|
|
662
|
+
def callees_for_receiver_type(receiver_type, name)
|
|
663
|
+
if receiver_type.is_a?(Type::Union)
|
|
664
|
+
return receiver_type.members.flat_map { |member| callees_for_receiver_type(member, name) }
|
|
665
|
+
.uniq { |callee, _, owner| [callee.object_id, owner] }
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
singleton = receiver_type.is_a?(Type::Singleton)
|
|
669
|
+
return NO_CALLEES unless singleton || receiver_type.is_a?(Type::Nominal)
|
|
670
|
+
|
|
671
|
+
class_name = receiver_type.class_name
|
|
672
|
+
found, defining =
|
|
673
|
+
if singleton then scope.singleton_def_through_ancestors(class_name, name)
|
|
674
|
+
else scope.user_def_through_ancestors(class_name, name)
|
|
675
|
+
end
|
|
676
|
+
side = singleton ? :singleton : :instance
|
|
677
|
+
return unresolved_receiver_callees(receiver_type, side, name) if found.nil?
|
|
678
|
+
|
|
679
|
+
[[found, 0, walked_owner(defining, class_name, CalleeOwner.new(receiver_type))]]
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
# `owner`, the owner a def found on `defining` by a lookup on `class_name` is walked under, or {FLOOR_ALL} for a
|
|
683
|
+
# def the class inherits (`defining` is not `class_name`). An inherited def is not walked (#2521): walking it
|
|
684
|
+
# exposed the own-def census's blind spots, which #2595 closes ({#callee_write_census}), and the hand-offs #2336
|
|
685
|
+
# lists, which stay open and which the floor masks.
|
|
686
|
+
def walked_owner(defining, class_name, owner)
|
|
687
|
+
defining == class_name ? owner : FLOOR_ALL
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
# Issue #2336 — what a call of `name` on a receiver of `receiver_type` reaches when its `side` chain has no def of
|
|
691
|
+
# the name: a body an ownerless definition row may give it ({#receiver_ownerless_body?}), floored outright; on
|
|
692
|
+
# the singleton side, a same-named instance def
|
|
693
|
+
# written as a `module_function def x` argument ({Scope#module_function_row_def?}), which the singleton def table
|
|
694
|
+
# does not hold, walked as a class method; else the chain's `method_missing`.
|
|
695
|
+
def unresolved_receiver_callees(receiver_type, side, name)
|
|
696
|
+
class_name = receiver_type.class_name
|
|
697
|
+
return OPAQUE_CALLEES if receiver_ownerless_body?(name, class_name)
|
|
698
|
+
|
|
699
|
+
if side == :singleton
|
|
700
|
+
found, defining = scope.user_def_through_ancestors(class_name, name)
|
|
701
|
+
if found && scope.module_function_row_def?(found, defining)
|
|
702
|
+
return [[found, 0, walked_owner(defining, class_name, CalleeOwner.new(receiver_type))]]
|
|
703
|
+
end
|
|
704
|
+
end
|
|
705
|
+
method_missing_callees(class_name, side, name, 0)
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
# Issue #2336 — true when an ownerless definition row may give `name` a body on `class_name`, the receiver of an
|
|
709
|
+
# explicit-receiver call ({Scope#ownerless_definition?}). A `define_method` / `define_singleton_method` /
|
|
710
|
+
# `alias_method` call or a def in a block claims no owner, so it may be the receiver's own and always counts. A
|
|
711
|
+
# top-level def lives on `Object`, so a method RBS declares on the receiver's class answers ahead of it
|
|
712
|
+
# (`Bag < Hash` and a top-level `def store`): the top-level rows count only when RBS declares none.
|
|
713
|
+
def receiver_ownerless_body?(name, class_name)
|
|
714
|
+
return true if scope.ownerless_definition?(name, top_level: false)
|
|
715
|
+
return false if rbs_declares?(class_name, name)
|
|
716
|
+
|
|
717
|
+
scope.ownerless_definition?(name)
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
# Issue #2336 — true when a body no def table holds may answer the receiverless call of `name` in `class_name`'s
|
|
721
|
+
# `side` chain, which no def, `method_missing` or top-level def answers: a definer the chain records
|
|
722
|
+
# (`define_method(:format)` in the class body; {Scope#discovered_method_through_ancestors?}), or, for a name RBS
|
|
723
|
+
# does not declare on `Object` (`Class` on the singleton side), any ownerless definition row. A name `Object`
|
|
724
|
+
# declares (`warn`, `format`) is answered by that method once the chain has no definer of it, so a `def warn` in a
|
|
725
|
+
# `refine Kernel` block or a `define_method` on another class does not count.
|
|
726
|
+
def self_ownerless_body?(name, class_name, side)
|
|
727
|
+
return true if scope.discovered_method_through_ancestors?(class_name, name, side)
|
|
728
|
+
return false if Reflection.instance_method_definition(side == :singleton ? "Class" : "Object", name,
|
|
729
|
+
scope: scope)
|
|
730
|
+
|
|
731
|
+
scope.ownerless_definition?(name)
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
# Issue #2521 (#2570 review) — the `[def, offset, FLOOR_ALL]` a call of `name` that resolves to no def on
|
|
735
|
+
# `class_name`'s `side` chain reaches when the chain defines `method_missing` and `name` is not a method every
|
|
736
|
+
# object (`Object`, or `Class` on the singleton side) answers: Ruby calls `method_missing(name, *args)`, so its
|
|
737
|
+
# parameters sit `offset` places ahead of the call's arguments (-1; 0 for a `send(:name, ...)` hand-off).
|
|
738
|
+
def method_missing_callees(class_name, side, name, offset)
|
|
739
|
+
found =
|
|
740
|
+
if side == :singleton then scope.singleton_def_through_ancestors(class_name, :method_missing).first
|
|
741
|
+
else scope.user_def_through_ancestors(class_name, :method_missing).first
|
|
742
|
+
end
|
|
743
|
+
return NO_CALLEES if found.nil?
|
|
744
|
+
|
|
745
|
+
base = side == :singleton ? "Class" : "Object"
|
|
746
|
+
return NO_CALLEES if Reflection.instance_method_definition(base, name, scope: scope)
|
|
747
|
+
|
|
748
|
+
[[found, offset - 1, FLOOR_ALL]]
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
# What a cross-class hand-off carries instead of an evaluator: the receiver type, which is `self` in the callee.
|
|
752
|
+
# The def's lexical nesting is not carried: it is a property of the def node ({#recorded_def_nesting}), which
|
|
753
|
+
# keys the census memo and the chain's nodes beside it. Holds no scope, so the run-scoped census memo that keeps
|
|
754
|
+
# an {CalleeMutationChain::Edge} alive keeps no caller state alive.
|
|
755
|
+
CalleeOwner = Data.define(:self_type)
|
|
756
|
+
|
|
757
|
+
# The owner of an inherited def ({#walked_owner}): its census and hand-offs are not walked. Every content
|
|
758
|
+
# parameter reads as mutated, so the argument is floored, and a {BudgetTrace::CALLEE_CHAIN_CAP} hit is recorded.
|
|
759
|
+
FLOOR_ALL = :floor_all
|
|
760
|
+
|
|
761
|
+
# Issue #2515 — the owner of a top-level def that may not be the method the call runs (several defs of the name,
|
|
762
|
+
# an alias, a class chain that may answer it first; {ToplevelDefCensus}, {#top_level_callees}): the def is not
|
|
763
|
+
# read, and EVERY argument the call passes reads as written, whatever the def's own parameters are, since another
|
|
764
|
+
# def of the name may take more of them. A hand-off to it floors the handed parameter outright.
|
|
765
|
+
FLOOR_EVERY = :floor_every
|
|
766
|
+
|
|
767
|
+
# Issue #2336 — the answer for a call no def resolves whose arguments the floor reads as written: an opaque sink
|
|
768
|
+
# ({#opaque_sink_call?}), or a name a definer other than a `def` may give a body ({#receiver_ownerless_body?},
|
|
769
|
+
# {#self_ownerless_body?}). No def is
|
|
770
|
+
# read; every argument floors as under {FLOOR_EVERY}.
|
|
771
|
+
OPAQUE_CALLEES = [[nil, 0, FLOOR_EVERY].freeze].freeze
|
|
772
|
+
|
|
773
|
+
# The owner of a top-level def reached from inside a class: its hand-offs resolve at the top level, where `self`
|
|
774
|
+
# is no instance of the caller's class.
|
|
775
|
+
TOPLEVEL_OWNER = CalleeOwner.new(nil)
|
|
776
|
+
OVER_APPROXIMATING = [FLOOR_ALL, FLOOR_EVERY].freeze
|
|
777
|
+
private_constant :CalleeOwner, :FLOOR_ALL, :FLOOR_EVERY, :OPAQUE_CALLEES, :TOPLEVEL_OWNER, :OVER_APPROXIMATING
|
|
778
|
+
|
|
779
|
+
# True for `Const.new` where the class defines its own `self.new`, whose result is not an instance of `Const`.
|
|
780
|
+
def overridden_constructor?(receiver, typing_scope)
|
|
781
|
+
return false unless receiver.is_a?(Prism::CallNode) && receiver.name == :new
|
|
782
|
+
|
|
783
|
+
klass = typing_scope.type_of(receiver.receiver) if constant_node?(receiver.receiver)
|
|
784
|
+
klass.is_a?(Type::Singleton) &&
|
|
785
|
+
!typing_scope.singleton_def_through_ancestors(klass.class_name, :new).first.nil?
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
# Issue #2522 — the callees a hand-off inside `def_node`'s body (`Help.c(p, o)`, `Helper.new.c(p, o)`) resolves
|
|
789
|
+
# to: the receiver typed once under the `Module.nesting` the declaration walk recorded for `def_node`, so a bare
|
|
790
|
+
# constant follows Ruby's lookup (the lexical chain, the innermost cref's ancestors, the top level) for the
|
|
791
|
+
# spelling the def is actually written under (`class B::Opts` reads `::Helper`, `module B; class Opts` reads
|
|
792
|
+
# `B::Helper`). The nesting is a function of the def node, never of the caller, so the answer cannot depend on
|
|
793
|
+
# which caller reached the memoised edges first. A def with no recorded nesting takes
|
|
794
|
+
# {#unrecorded_hand_off_callees}.
|
|
795
|
+
def hand_off_receiver_callees(call_node, def_node, receiver = call_node.receiver)
|
|
796
|
+
nesting = exact_def_nesting(def_node)
|
|
797
|
+
return unrecorded_hand_off_callees(call_node, receiver) if nesting.nil?
|
|
798
|
+
|
|
799
|
+
receiver_callees_in(scope.with_lexical_nesting(nesting), call_node, receiver)
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
# The def's recorded `Module.nesting` ({Scope::DiscoveryIndex#recorded_def_nesting}), nil when the declaration
|
|
803
|
+
# walk recorded none.
|
|
804
|
+
def recorded_def_nesting(def_node) = scope.discovery.recorded_def_nesting(def_node)
|
|
805
|
+
|
|
806
|
+
# The nesting the hand-offs of `def_node`, run with `self_type`, resolve their constants in exactly, or nil. A
|
|
807
|
+
# def in a `class << self` body answers nil: there Ruby's cref is the singleton class, whose ancestors are not
|
|
808
|
+
# the class's (#2570 review), and the recorded nesting does not name it. Such a def is receiverless, runs on the
|
|
809
|
+
# class object, and is not in its cref's instance def table, which a `module_function` / `extend self` def or an
|
|
810
|
+
# `extend`ed module's def (whose cref is the module itself) is.
|
|
811
|
+
def exact_def_nesting(def_node, self_type = scope.self_type)
|
|
812
|
+
nesting = recorded_def_nesting(def_node)
|
|
813
|
+
return nesting unless self_type.is_a?(Type::Singleton) && def_node.receiver.nil?
|
|
814
|
+
return nil if nesting.nil? || nesting.empty?
|
|
815
|
+
|
|
816
|
+
scope.user_def_for(nesting.first, def_node.name).equal?(def_node) ? nesting : nil
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
# A hand-off in a def whose nesting was not recorded (a def under a header that renders no name, a bundle-served
|
|
820
|
+
# def outside a rehydrating run): the spelling of the defining class is unknown (`module A; class B::C` has the
|
|
821
|
+
# nesting `["A::B::C", "A"]`, `class A::B::C` has `["A::B::C"]`), so the union of two resolutions that try
|
|
822
|
+
# every spelling it could have had, each of which only adds callees:
|
|
823
|
+
#
|
|
824
|
+
# - By name: a bare constant `K` as each prefix of the class name followed by `K`, and as the top-level `K`; a
|
|
825
|
+
# rooted one (`::K`) as `K` alone.
|
|
826
|
+
# - Typed: the receiver typed under each truncation of the nested-spelling nesting (`["A::B::C", "A::B", "A"]`
|
|
827
|
+
# gives `["A::B::C"]`, `["A::B::C", "A::B"]` and the whole chain), which sees an alias, an inherited or
|
|
828
|
+
# included constant and a constant holding an instance.
|
|
829
|
+
def unrecorded_hand_off_callees(call_node, receiver = call_node.receiver)
|
|
830
|
+
constructed = receiver.is_a?(Prism::CallNode) && receiver.name == :new
|
|
831
|
+
written = constant_name(constructed ? receiver.receiver : receiver)
|
|
832
|
+
return NO_CALLEES if written.nil?
|
|
833
|
+
return compute_unrecorded_hand_off_callees(call_node, receiver, written, constructed) if hand_off_memo_bypassed?
|
|
834
|
+
|
|
835
|
+
memo = unrecorded_hand_off_memo(written)
|
|
836
|
+
key = [constructed, call_node.name]
|
|
837
|
+
unless memo.key?(key)
|
|
838
|
+
return memo[key] = MemoAudit.key_audit("unrecorded hand-off") do
|
|
839
|
+
compute_unrecorded_hand_off_callees(call_node, receiver, written, constructed)
|
|
840
|
+
end
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
MemoAudit.check("unrecorded hand-off", memo[key], written, key) do
|
|
844
|
+
compute_unrecorded_hand_off_callees(call_node, receiver, written, constructed)
|
|
845
|
+
end
|
|
846
|
+
end
|
|
847
|
+
|
|
848
|
+
def compute_unrecorded_hand_off_callees(call_node, receiver, written, constructed)
|
|
849
|
+
found = hand_off_class_names(written).flat_map do |class_name|
|
|
850
|
+
hand_off_callee_in(class_name, constructed, call_node.name)
|
|
851
|
+
end
|
|
852
|
+
hand_off_typing_scopes(written).each do |typing_scope|
|
|
853
|
+
found.concat(receiver_callees_in(typing_scope, call_node, receiver))
|
|
854
|
+
end
|
|
855
|
+
found.size > 1 ? found.uniq { |callee, _, owner| [callee.object_id, owner] } : found
|
|
856
|
+
end
|
|
857
|
+
|
|
858
|
+
# The run's per-discovery memo of {#unrecorded_hand_off_callees}: the answer depends on the receiver's spelling,
|
|
859
|
+
# the class `self` is and its side (a constant's ancestor lookup differs between the class object and an
|
|
860
|
+
# instance), and the called name, not on the def it sits in.
|
|
861
|
+
def unrecorded_hand_off_memo(written)
|
|
862
|
+
table = typing_state.weak_key_map(UNRECORDED_HAND_OFFS)
|
|
863
|
+
self_type = scope.self_type
|
|
864
|
+
by_side = ((table[scope.discovery] ||= {})[self_type.is_a?(Type::Singleton)] ||= {})
|
|
865
|
+
(by_side[enclosing_class_name_for(self_type)] ||= {})[written] ||= {}
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
UNRECORDED_HAND_OFFS = TypingState.memo(
|
|
869
|
+
:callee_unrecorded_hand_offs,
|
|
870
|
+
run_scoped: true, audit: "unrecorded hand-off",
|
|
871
|
+
reason: "keyed by discovery, side, class and spelling, all its answer reads"
|
|
872
|
+
)
|
|
873
|
+
private_constant :UNRECORDED_HAND_OFFS
|
|
874
|
+
|
|
875
|
+
# The scopes {#unrecorded_hand_off_callees} types a receiver written as `written` in. A rooted constant and a
|
|
876
|
+
# top-level def are typed once.
|
|
877
|
+
def hand_off_typing_scopes(written)
|
|
878
|
+
name = enclosing_class_name_for(scope.self_type)
|
|
879
|
+
return [scope.with_lexical_nesting([])] if name.nil?
|
|
880
|
+
return [scope] if written.start_with?("::")
|
|
881
|
+
|
|
882
|
+
parts = name.to_s.split("::")
|
|
883
|
+
chain = parts.each_index.map { |i| parts[0..(parts.size - 1 - i)].join("::") }
|
|
884
|
+
chain.each_index.map { |k| scope.with_lexical_nesting(chain[0..k]) }
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
# `[def, 0, owner]` for `name` called on `class_name` (or on its `new`), or none.
|
|
888
|
+
def hand_off_callee_in(class_name, constructed, name)
|
|
889
|
+
if constructed
|
|
890
|
+
return NO_CALLEES unless scope.singleton_def_through_ancestors(class_name, :new).first.nil?
|
|
891
|
+
|
|
892
|
+
found = scope.user_def_for(class_name, name)
|
|
893
|
+
return found.nil? ? NO_CALLEES : [[found, 0, CalleeOwner.new(Type::Combinator.nominal_of(class_name))]]
|
|
894
|
+
end
|
|
895
|
+
|
|
896
|
+
found, defining = scope.singleton_def_through_ancestors(class_name, name)
|
|
897
|
+
return NO_CALLEES if found.nil?
|
|
898
|
+
|
|
899
|
+
owner = CalleeOwner.new(Type::Combinator.singleton_of(class_name))
|
|
900
|
+
[[found, 0, walked_owner(defining, class_name, owner)]]
|
|
901
|
+
end
|
|
902
|
+
|
|
903
|
+
# The class names a constant written as `written` (`K`, `A::K`, `::K`) can name from the def's class name.
|
|
904
|
+
def hand_off_class_names(written)
|
|
905
|
+
return [written.delete_prefix("::")] if written.start_with?("::")
|
|
906
|
+
|
|
907
|
+
name = enclosing_class_name_for(scope.self_type)
|
|
908
|
+
return [written] if name.nil?
|
|
909
|
+
|
|
910
|
+
parts = name.to_s.split("::")
|
|
911
|
+
parts.each_index.map { |i| "#{parts[0..i].join('::')}::#{written}" } << written
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
# True when a census memo must not be read or written: a dependency recorder is active (it must see each
|
|
915
|
+
# constant read), or this scope carries guard narrowings of constants (`if INST.is_a?(Sub)`), which survive
|
|
916
|
+
# `with_self_type` and change what the constant types as, so the answer is no longer the def's alone. An audit
|
|
917
|
+
# recomputation ({MemoAudit}) bypasses them too, so it reads no stored entry.
|
|
918
|
+
def hand_off_memo_bypassed?
|
|
919
|
+
Analysis::DependencyRecorder.active? || !scope.constant_narrowings.empty? || MemoAudit.suspended?
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
# The name a constant node is written with, or nil when part of it is not a constant (`foo::Bar`).
|
|
923
|
+
def constant_name(node)
|
|
924
|
+
node.full_name if constant_node?(node)
|
|
925
|
+
rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError, Prism::ConstantPathNode::MissingNodesInConstantPathError
|
|
926
|
+
nil
|
|
927
|
+
end
|
|
928
|
+
|
|
929
|
+
# The evaluator a hand-off's {CalleeOwner} stands for, built from this one's scope and kept on it.
|
|
930
|
+
def owner_evaluator(owner)
|
|
931
|
+
(@owner_evaluators ||= {})[owner] ||=
|
|
932
|
+
self.class.new(scope: scope.with_self_type(owner.self_type))
|
|
933
|
+
end
|
|
934
|
+
|
|
935
|
+
# The `[def, offset, owner]` triples a call inside `def_node`'s body hands its parameter to: a self-call
|
|
936
|
+
# ({#resolve_self_callees}, owner nil) or a call on a constant / `Const.new` receiver ({#hand_off_receiver?},
|
|
937
|
+
# {#hand_off_receiver_callees}).
|
|
938
|
+
#
|
|
939
|
+
# Issue #2336 — a call on `itself` is a self-call, and one on `self.class` resolves on the class object `self`
|
|
940
|
+
# types as. A call on any other receiver the callee's body cannot type (a call's result: `new.store(h)`,
|
|
941
|
+
# `instance.helper(h)`; a class variable or a global) resolves by name, as an ivar receiver does. A literal
|
|
942
|
+
# `send` hand-off on any receiver resolves as its target.
|
|
943
|
+
def hand_off_callees(call_node, def_node, depth = 0)
|
|
944
|
+
receiver = call_node.receiver
|
|
945
|
+
return self_hand_off_callees(call_node) if self_dispatch_call?(call_node) || itself_receiver?(receiver)
|
|
946
|
+
|
|
947
|
+
if receiver.is_a?(Prism::LocalVariableReadNode) || receiver.is_a?(Prism::ItLocalVariableReadNode)
|
|
948
|
+
return local_receiver_callees(call_node, def_node, depth)
|
|
949
|
+
end
|
|
950
|
+
return receiver_callees_in(scope, call_node) if class_of_self?(receiver)
|
|
951
|
+
return hand_off_receiver_callees(call_node, def_node) if constant_receiver_form?(receiver)
|
|
952
|
+
|
|
953
|
+
named_hand_off_callees(call_node)
|
|
954
|
+
end
|
|
955
|
+
|
|
956
|
+
# {#named_instance_callees} for `call_node`'s name, or a literal `send` hand-off's target.
|
|
957
|
+
def named_hand_off_callees(call_node)
|
|
958
|
+
return named_instance_callees(call_node.name) unless send_hand_off?(call_node)
|
|
959
|
+
|
|
960
|
+
shifted_callees(named_instance_callees(send_target(call_node)))
|
|
961
|
+
end
|
|
962
|
+
|
|
963
|
+
# `itself` / `self.itself`, which is `self`.
|
|
964
|
+
def itself_receiver?(receiver)
|
|
965
|
+
receiver.is_a?(Prism::CallNode) && receiver.name == :itself && receiver.arguments.nil? &&
|
|
966
|
+
receiver.block.nil? && self_dispatch_call?(receiver)
|
|
967
|
+
end
|
|
968
|
+
|
|
969
|
+
# `self.class`, the class object of `self`.
|
|
970
|
+
def class_of_self?(receiver)
|
|
971
|
+
receiver.is_a?(Prism::CallNode) && receiver.name == :class && receiver.receiver.is_a?(Prism::SelfNode) &&
|
|
972
|
+
receiver.arguments.nil? && receiver.block.nil?
|
|
973
|
+
end
|
|
974
|
+
|
|
975
|
+
# {#resolve_self_callees} for a self-call that is a hand-off inside a def. In a def the evaluator is the top
|
|
976
|
+
# level's (`self` nil), the def a top-level one, and a top-level def is a private method on `Object`: it may be
|
|
977
|
+
# called from any object, so a same-named instance def of the caller's class answers the hand-off before the
|
|
978
|
+
# top-level one. Issue #2515 — the project's same-named instance defs are unioned in as a receiver the callee
|
|
979
|
+
# cannot type is ({#named_instance_callees}): they floor, never narrow.
|
|
980
|
+
def self_hand_off_callees(call_node)
|
|
981
|
+
found = resolve_self_callees(call_node)
|
|
982
|
+
return found unless enclosing_class_name_for(scope.self_type).nil?
|
|
983
|
+
|
|
984
|
+
send_hand_off = send_hand_off?(call_node)
|
|
985
|
+
target = send_hand_off ? call_node.arguments.arguments.first.unescaped.to_sym : call_node.name
|
|
986
|
+
named = named_instance_callees(target)
|
|
987
|
+
return named if named.equal?(CAPPED_HAND_OFF)
|
|
988
|
+
return found if named.empty?
|
|
989
|
+
|
|
990
|
+
named = named.map { |def_node, _offset, owner| [def_node, 1, owner] } if send_hand_off
|
|
991
|
+
UnionCallees.new(found + named)
|
|
992
|
+
end
|
|
993
|
+
|
|
994
|
+
# True for a call on an explicit receiver: one the callee's body can type without its locals (a constant or
|
|
995
|
+
# `Const.new`), a local of the callee (#2515: {#local_receiver_callees}), or (issue #2595 for an ivar, #2336 for
|
|
996
|
+
# any other) one whose type the def's AST does not show, so it resolves by name ({#named_hand_off_callees}).
|
|
997
|
+
def hand_off_receiver?(node) = node.is_a?(Prism::CallNode) && !node.receiver.nil?
|
|
998
|
+
|
|
999
|
+
def constant_receiver_form?(receiver)
|
|
1000
|
+
case receiver
|
|
1001
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode then true
|
|
1002
|
+
when Prism::CallNode then receiver.name == :new && constant_node?(receiver.receiver)
|
|
1003
|
+
else false
|
|
1004
|
+
end
|
|
1005
|
+
end
|
|
1006
|
+
|
|
1007
|
+
# Issue #2515 — the callees of `local.name(p, o)` inside `def_node`, where `local` is a parameter or a local of
|
|
1008
|
+
# the def. A local every assignment of which in the def is a constant receiver form (`Const`, `Const.new`) is
|
|
1009
|
+
# typed inside the callee, as the constant receiver is ({#hand_off_receiver_callees}). Any other receiver (a
|
|
1010
|
+
# parameter, a local assigned anything else, a block-local) has a type that depends on the caller, which is
|
|
1011
|
+
# not a function of the graph node, so the call resolves by name instead ({#named_instance_callees}).
|
|
1012
|
+
def local_receiver_callees(call_node, def_node, depth)
|
|
1013
|
+
receiver = call_node.receiver
|
|
1014
|
+
typed = receiver.is_a?(Prism::LocalVariableReadNode) && receiver.depth == depth
|
|
1015
|
+
forms = typed_local_forms(def_node, receiver.name) if typed
|
|
1016
|
+
forms = nil if forms && local_object_opened?(def_node, call_node)
|
|
1017
|
+
return named_hand_off_callees(call_node) if forms.nil?
|
|
1018
|
+
|
|
1019
|
+
found = forms.flat_map { |form| hand_off_receiver_callees(call_node, def_node, form) }
|
|
1020
|
+
found.size > 1 ? found.uniq { |callee, _, owner| [callee.object_id, owner] } : found
|
|
1021
|
+
end
|
|
1022
|
+
|
|
1023
|
+
# The constant receiver forms every assignment of the local `name` in `def_node` holds, or nil when `name` is a
|
|
1024
|
+
# parameter, is never assigned, or may hold anything else. The whole body is read, including the scopes the
|
|
1025
|
+
# hand-off walk leaves out (a lambda, an `instance_exec` / `class_exec` / `define_method` block, a block that
|
|
1026
|
+
# shadows the name), so a write there counts. The local is untyped when any write is not a constant form (an
|
|
1027
|
+
# op-write, a multiple assignment, `for` or `rescue =>` target, a pattern capture, any other value), when it is
|
|
1028
|
+
# the receiver of `send` / `public_send` / `__send__` / `singleton_class` or is assigned to another local
|
|
1029
|
+
# (`g = h`), and, for every local, when the body calls `binding`, `eval`, `instance_eval` or
|
|
1030
|
+
# `local_variable_set` on any receiver, which can write a local no node names. A local whose object the syntax
|
|
1031
|
+
# gives methods ({#local_object_opened?}) is untyped at that call. A per-def AST fact, memoised as {#def_census}.
|
|
1032
|
+
def typed_local_forms(def_node, name)
|
|
1033
|
+
def_census(:local_forms, def_node) { local_forms_of(def_node) }[name]
|
|
1034
|
+
end
|
|
1035
|
+
|
|
1036
|
+
# True when the file's syntax gives the receiver local's object methods the declaration tables do not show
|
|
1037
|
+
# (`def h.x`, `h.extend(M)`, `h.define_singleton_method`, `h.singleton_class...`, `class << h`, an
|
|
1038
|
+
# `h.instance_eval` that defines): {Analysis::CheckRules::LexicalMethodSites}, asked once per def for every
|
|
1039
|
+
# call on a local.
|
|
1040
|
+
def local_object_opened?(def_node, call_node)
|
|
1041
|
+
def_census(:opened_local_calls, def_node) { opened_local_call_offsets(def_node) }
|
|
1042
|
+
.include?(call_node.location.start_offset)
|
|
1043
|
+
end
|
|
1044
|
+
|
|
1045
|
+
def opened_local_call_offsets(def_node)
|
|
1046
|
+
require_relative "../analysis/check_rules/lexical_method_sites"
|
|
1047
|
+
sites = Analysis::CheckRules::LexicalMethodSites.new(def_node)
|
|
1048
|
+
offsets = Set.new
|
|
1049
|
+
Source::NodeWalker.each(def_node.body) do |node|
|
|
1050
|
+
next unless node.is_a?(Prism::CallNode) && node.receiver.is_a?(Prism::LocalVariableReadNode)
|
|
1051
|
+
|
|
1052
|
+
offsets << node.location.start_offset if sites.singleton_local_def?(node)
|
|
1053
|
+
end
|
|
1054
|
+
offsets.freeze
|
|
1055
|
+
end
|
|
1056
|
+
|
|
1057
|
+
DYNAMIC_LOCAL_CALLS = Set[:binding, :eval, :instance_eval, :local_variable_set].freeze
|
|
1058
|
+
DISPATCHING_CALLS = Set[:send, :public_send, :__send__, :singleton_class].freeze
|
|
1059
|
+
private_constant :DYNAMIC_LOCAL_CALLS, :DISPATCHING_CALLS
|
|
1060
|
+
|
|
1061
|
+
def local_forms_of(def_node)
|
|
1062
|
+
forms = {}
|
|
1063
|
+
tainted = Set.new
|
|
1064
|
+
return NO_FORMS if collect_local_forms(def_node.body, forms, tainted)
|
|
1065
|
+
|
|
1066
|
+
params = def_node.parameters
|
|
1067
|
+
forms.each_value(&:uniq!)
|
|
1068
|
+
forms.delete_if do |name, values|
|
|
1069
|
+
tainted.include?(name) || !values.all? { |value| constant_receiver_form?(value) } ||
|
|
1070
|
+
parameter_named?(params, name)
|
|
1071
|
+
end
|
|
1072
|
+
forms.empty? ? NO_FORMS : forms
|
|
1073
|
+
end
|
|
1074
|
+
|
|
1075
|
+
# Fills `forms` (`{ name => written values }`) and `tainted` from `node` and everything under it, a nested `def`
|
|
1076
|
+
# body excepted (its locals are its own). True when the body may write locals the nodes do not show.
|
|
1077
|
+
def collect_local_forms(node, forms, tainted)
|
|
1078
|
+
return false if node.nil?
|
|
1079
|
+
|
|
1080
|
+
return false if node.is_a?(Prism::DefNode)
|
|
1081
|
+
return true if node.is_a?(Prism::CallNode) && dynamic_local_call?(node, tainted)
|
|
1082
|
+
|
|
1083
|
+
note_local_write(node, forms, tainted)
|
|
1084
|
+
dynamic = false
|
|
1085
|
+
node.rigor_each_child { |child| dynamic = true if collect_local_forms(child, forms, tainted) }
|
|
1086
|
+
dynamic
|
|
1087
|
+
end
|
|
1088
|
+
|
|
1089
|
+
def note_local_write(node, forms, tainted)
|
|
1090
|
+
case node
|
|
1091
|
+
when Prism::LocalVariableWriteNode
|
|
1092
|
+
(forms[node.name] ||= []) << node.value
|
|
1093
|
+
tainted << node.value.name if node.value.is_a?(Prism::LocalVariableReadNode)
|
|
1094
|
+
when Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode,
|
|
1095
|
+
Prism::LocalVariableAndWriteNode, Prism::LocalVariableTargetNode
|
|
1096
|
+
(forms[node.name] ||= []) << node
|
|
1097
|
+
end
|
|
1098
|
+
end
|
|
1099
|
+
|
|
1100
|
+
# True for a `binding` / `eval` / ... call on any receiver (`Kernel.binding` included), which can write a local no
|
|
1101
|
+
# node names. A local receiving a dispatching call (`send`, `singleton_class`, ...) is tainted as a side effect.
|
|
1102
|
+
def dynamic_local_call?(node, tainted)
|
|
1103
|
+
return true if DYNAMIC_LOCAL_CALLS.include?(node.name)
|
|
1104
|
+
|
|
1105
|
+
tainted << node.receiver.name if DISPATCHING_CALLS.include?(node.name) &&
|
|
1106
|
+
node.receiver.is_a?(Prism::LocalVariableReadNode)
|
|
1107
|
+
false
|
|
1108
|
+
end
|
|
1109
|
+
|
|
1110
|
+
NO_FORMS = {}.freeze
|
|
1111
|
+
private_constant :NO_FORMS
|
|
1112
|
+
|
|
1113
|
+
def parameter_named?(params, name)
|
|
1114
|
+
return false if params.nil?
|
|
1115
|
+
|
|
1116
|
+
all = [params.requireds, params.optionals, params.posts, params.keywords,
|
|
1117
|
+
[params.rest, params.keyword_rest, params.block]]
|
|
1118
|
+
all.flatten.compact.any? { |param| param.respond_to?(:name) && param.name == name }
|
|
1119
|
+
end
|
|
1120
|
+
|
|
1121
|
+
# Issue #2515 — the `[def, 0, owner]` triples of every user instance def named `name`, for a hand-off on a
|
|
1122
|
+
# receiver whose type the callee cannot know. It over-approximates (it favours flooring, ADR-5), as
|
|
1123
|
+
# {#unrecorded_hand_off_callees} unions spellings. A def on a class nothing subclasses is walked under that
|
|
1124
|
+
# class. A def on a module, or on a class a project class names as its superclass, is {FLOOR_ALL}: the real
|
|
1125
|
+
# receiver may be an includer or a subclass whose overrides the walk would not see (ADR-127 WD4, as an inherited
|
|
1126
|
+
# def, {#walked_owner}). A name no user def has (a core or gem method) contributes nothing; a name with more
|
|
1127
|
+
# than {NAMED_HAND_OFF_CAP} defs answers {CAPPED_HAND_OFF}, which floors the handed parameter outright.
|
|
1128
|
+
def named_instance_callees(name)
|
|
1129
|
+
owners = instance_def_owners(name)
|
|
1130
|
+
return NO_CALLEES if owners.empty?
|
|
1131
|
+
return CAPPED_HAND_OFF if owners.size > NAMED_HAND_OFF_CAP
|
|
1132
|
+
|
|
1133
|
+
opaque = subclassing_opaque?
|
|
1134
|
+
UnionCallees.new(owners.filter_map do |class_name, open|
|
|
1135
|
+
found = scope.user_def_for(class_name, name)
|
|
1136
|
+
next if found.nil?
|
|
1137
|
+
|
|
1138
|
+
[found, 0, open || opaque ? FLOOR_ALL : CalleeOwner.new(Type::Combinator.nominal_of(class_name))]
|
|
1139
|
+
end)
|
|
1140
|
+
end
|
|
1141
|
+
|
|
1142
|
+
# A resolution by name, whose candidates are guesses at the receiver: every edge it yields is an
|
|
1143
|
+
# over-approximation ({CalleeMutationChain::Edge#approx}), as is any hand-off with more than one candidate.
|
|
1144
|
+
class UnionCallees < Array; end
|
|
1145
|
+
private_constant :UnionCallees
|
|
1146
|
+
|
|
1147
|
+
# The most defs one name may have for {#named_instance_callees} to walk them; more floors the parameter. A cap
|
|
1148
|
+
# on the name key's fan-out that floors, not a cut of the walk, so ADR-127's rejection of out-degree caps does
|
|
1149
|
+
# not apply: the answer stays a function of the graph node.
|
|
1150
|
+
NAMED_HAND_OFF_CAP = 8
|
|
538
1151
|
|
|
539
|
-
|
|
540
|
-
|
|
1152
|
+
# The answer for a name above the cap; {#compute_forwarding_edges} turns it into a floor of each handed
|
|
1153
|
+
# parameter.
|
|
1154
|
+
CAPPED_HAND_OFF = [[nil, 0, nil].freeze].freeze
|
|
1155
|
+
private_constant :NAMED_HAND_OFF_CAP, :CAPPED_HAND_OFF
|
|
541
1156
|
|
|
542
|
-
|
|
1157
|
+
# The `[class, open]` owners of `name` ({Scope#user_instance_def_owners}). A function of the discovery and the
|
|
1158
|
+
# name alone, so its run-scoped memo is keyed by both; the answer reads no constant and no narrowing, so
|
|
1159
|
+
# {#hand_off_memo_bypassed?} does not bypass it. A hit does not reach the scope, so a dependency recorder is told
|
|
1160
|
+
# of the reads here. Built per name, not as a whole index: a run holds a discovery per file, and a whole-index
|
|
1161
|
+
# build per file cost more than the walks it served.
|
|
1162
|
+
def instance_def_owners(name)
|
|
1163
|
+
return scope.user_instance_def_owners(name) if MemoAudit.suspended?
|
|
1164
|
+
|
|
1165
|
+
table = typing_state.weak_key_map(INSTANCE_DEF_INDEX)
|
|
1166
|
+
by_name = (table[scope.discovery] ||= {})
|
|
1167
|
+
unless by_name.key?(name)
|
|
1168
|
+
return by_name[name] = MemoAudit.key_audit("instance def owners") { scope.user_instance_def_owners(name) }
|
|
1169
|
+
end
|
|
1170
|
+
|
|
1171
|
+
record_instance_def_owner_reads(name, by_name[name]) if Analysis::DependencyRecorder.active?
|
|
1172
|
+
MemoAudit.check("instance def owners", by_name[name], name) { scope.user_instance_def_owners(name) }
|
|
1173
|
+
end
|
|
1174
|
+
|
|
1175
|
+
# {Scope#subclassing_opacity}'s flag, memoised beside the owners per discovery; a recorder is told its keys on
|
|
1176
|
+
# every call, hit or not.
|
|
1177
|
+
def subclassing_opaque?
|
|
1178
|
+
return scope.subclassing_opacity.first if MemoAudit.suspended?
|
|
1179
|
+
|
|
1180
|
+
table = typing_state.weak_key_map(INSTANCE_DEF_INDEX)
|
|
1181
|
+
by_name = (table[scope.discovery] ||= {})
|
|
1182
|
+
entry = by_name[OPACITY_ENTRY] ||= MemoAudit.key_audit("subclassing opacity") { scope.subclassing_opacity }
|
|
1183
|
+
Analysis::DependencyRecorder.read_keys(entry.last) if Analysis::DependencyRecorder.active?
|
|
1184
|
+
MemoAudit.check("subclassing opacity", entry, OPACITY_ENTRY) { scope.subclassing_opacity }.first
|
|
1185
|
+
end
|
|
1186
|
+
|
|
1187
|
+
OPACITY_ENTRY = :"<subclassing-opacity>"
|
|
1188
|
+
private_constant :OPACITY_ENTRY
|
|
1189
|
+
|
|
1190
|
+
def record_instance_def_owner_reads(name, owners)
|
|
1191
|
+
Analysis::DependencyRecorder.read_name(:defines, name)
|
|
1192
|
+
owners.map(&:first).each { |owner| Analysis::DependencyRecorder.read_last_segment(:subclass_of, owner) }
|
|
1193
|
+
end
|
|
1194
|
+
|
|
1195
|
+
INSTANCE_DEF_INDEX = TypingState.memo(
|
|
1196
|
+
:callee_instance_def_index,
|
|
1197
|
+
run_scoped: true, audit: ["instance def owners", "subclassing opacity"],
|
|
1198
|
+
reason: "keyed by discovery and name, all its answer reads"
|
|
1199
|
+
)
|
|
1200
|
+
private_constant :INSTANCE_DEF_INDEX
|
|
1201
|
+
|
|
1202
|
+
def constant_node?(node) = node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
|
|
1203
|
+
|
|
1204
|
+
# `census` (a `callee_*_mutated_parameters` name) of `def_node`, read through `owner`, the {CalleeOwner} the
|
|
1205
|
+
# callee's hand-offs resolve in, or this evaluator when the callee is a self-call's.
|
|
1206
|
+
def census_of(owner, census, def_node)
|
|
1207
|
+
return floor_all_census(census, def_node) if owner == FLOOR_ALL
|
|
1208
|
+
return floor_every_census(census) if owner == FLOOR_EVERY
|
|
1209
|
+
|
|
1210
|
+
(owner.nil? ? self : owner_evaluator(owner)).__send__(census, def_node)
|
|
1211
|
+
end
|
|
1212
|
+
|
|
1213
|
+
# The {FLOOR_EVERY} answer: every positional and keyword argument mutated, no hand-offs.
|
|
1214
|
+
def floor_every_census(census)
|
|
1215
|
+
BudgetTrace.hit(BudgetTrace::CALLEE_CHAIN_CAP)
|
|
1216
|
+
census == :forwarding_edges ? NO_FORWARDS : EVERY_ARGUMENT
|
|
1217
|
+
end
|
|
1218
|
+
|
|
1219
|
+
# The {FLOOR_ALL} answer: every content parameter mutated, and (issue #2595) a named rest or keyword rest at
|
|
1220
|
+
# {ANY_SLOT} / {ANY_KEYWORD} ({#forwarding_slots}), no hand-offs.
|
|
1221
|
+
def floor_all_census(census, def_node)
|
|
1222
|
+
BudgetTrace.hit(BudgetTrace::CALLEE_CHAIN_CAP)
|
|
1223
|
+
census == :forwarding_edges ? NO_FORWARDS : forwarding_slots(def_node)
|
|
1224
|
+
end
|
|
1225
|
+
|
|
1226
|
+
# The local-variable reads `call_node` passes to the parameters of `mutated`, a `{ name => slot }` census, the
|
|
1227
|
+
# first `offset` arguments not being the callee's (the `:name` of a `send` hand-off, {#resolve_self_callees}).
|
|
1228
|
+
def arguments_for_slots(call_node, mutated, offset)
|
|
543
1229
|
return NO_ARGUMENT_READS if mutated.empty?
|
|
544
1230
|
|
|
545
1231
|
argument_nodes = call_node.arguments.arguments
|
|
546
|
-
mutated.values.uniq
|
|
547
|
-
|
|
1232
|
+
values = mutated.values.uniq
|
|
1233
|
+
reads = values.filter_map do |slot|
|
|
1234
|
+
next if slot == ANY_SLOT || slot == ANY_KEYWORD || (slot.is_a?(Integer) && (slot + offset).negative?)
|
|
1235
|
+
|
|
1236
|
+
argument = slot.is_a?(Symbol) ? keyword_argument(argument_nodes, slot) : argument_nodes[slot + offset]
|
|
548
1237
|
argument = argument.value if argument.is_a?(Prism::ImplicitNode)
|
|
549
1238
|
argument if argument.is_a?(Prism::LocalVariableReadNode)
|
|
550
1239
|
end
|
|
1240
|
+
reads += rest_arguments(argument_nodes, offset) if values.include?(ANY_SLOT)
|
|
1241
|
+
reads += keyword_rest_arguments(argument_nodes) if values.include?(ANY_KEYWORD)
|
|
1242
|
+
reads.uniq
|
|
1243
|
+
end
|
|
1244
|
+
|
|
1245
|
+
# Every local read passed positionally from `offset` on: what a callee that hands its rest parameter to `super`
|
|
1246
|
+
# may mutate ({#super_forwarded_parameters}).
|
|
1247
|
+
def rest_arguments(argument_nodes, offset)
|
|
1248
|
+
argument_nodes.drop([offset, 0].max).grep(Prism::LocalVariableReadNode)
|
|
1249
|
+
end
|
|
1250
|
+
|
|
1251
|
+
# Every local read passed as a keyword value: what a callee that hands its keyword rest parameter to `super` may
|
|
1252
|
+
# mutate ({#super_forwarded_parameters}).
|
|
1253
|
+
def keyword_rest_arguments(argument_nodes)
|
|
1254
|
+
hash = argument_nodes.last
|
|
1255
|
+
return NO_ARGUMENT_READS unless hash.is_a?(Prism::KeywordHashNode)
|
|
1256
|
+
|
|
1257
|
+
hash.elements.filter_map do |pair|
|
|
1258
|
+
value = pair.value if pair.is_a?(Prism::AssocNode)
|
|
1259
|
+
value = value.value if value.is_a?(Prism::ImplicitNode)
|
|
1260
|
+
value if value.is_a?(Prism::LocalVariableReadNode)
|
|
1261
|
+
end
|
|
551
1262
|
end
|
|
552
1263
|
|
|
1264
|
+
# The slot {#super_forwarded_parameters} gives a keyword rest parameter: every keyword value of the call.
|
|
1265
|
+
ANY_KEYWORD = :__any_keyword__
|
|
1266
|
+
private_constant :ANY_KEYWORD
|
|
1267
|
+
|
|
553
1268
|
# The value `argument_nodes` pass for the keyword `name` (`poke(acc: a)`), or nil.
|
|
554
1269
|
def keyword_argument(argument_nodes, name)
|
|
555
1270
|
hash = argument_nodes.last
|
|
@@ -585,20 +1300,48 @@ module Rigor
|
|
|
585
1300
|
# evaluator's statement and write-back passes ({#with_repeated_or_write_marks}), type every run of a repeating
|
|
586
1301
|
# body from one entry scope, so its slot's gradual type may be what an EARLIER run stored rather than an
|
|
587
1302
|
# absence of evidence.
|
|
1303
|
+
#
|
|
1304
|
+
# On a receiver that is itself untracked (a `Dynamic` / `Top` constituent, the #544 test the narrowing
|
|
1305
|
+
# record takes: a parameter, an unbound ivar), the slot may hold what an earlier call stored, so the reading
|
|
1306
|
+
# keeps the rvalue only as far as its class is known ({#untracked_memo_value}, #2292): read as the fresh
|
|
1307
|
+
# literal, `(h[k] ||= {})[:name].upcase` reported a call on `nil` and `(h[k] ||= 1).upcase` one on `1`. A
|
|
1308
|
+
# tracked receiver keeps the literal, so a block fold over a captured `pool = {}`, whose in-place widening
|
|
1309
|
+
# reads `Hash[Dynamic[top], Dynamic[top]]`, still answers `find { |s| (pool[s] ||= s) == "b" }` as `"b"`.
|
|
588
1310
|
def index_compound_write_value(node)
|
|
589
1311
|
return index_write_stored_type(node, scope) unless node.is_a?(Prism::IndexOrWriteNode)
|
|
590
1312
|
|
|
591
1313
|
current = index_read_type(node, scope)
|
|
592
1314
|
rhs = scope.type_of(node.value, tracer: tracer)
|
|
593
|
-
|
|
1315
|
+
if memoizing_index_read?(node, current, rhs)
|
|
1316
|
+
return rhs if tracked_memo_receiver?(node.receiver)
|
|
1317
|
+
|
|
1318
|
+
memo = untracked_memo_value(rhs)
|
|
1319
|
+
return memo if memo
|
|
1320
|
+
end
|
|
594
1321
|
|
|
595
1322
|
index_write_stored_type(node, scope, current: current, rhs: rhs)
|
|
596
1323
|
end
|
|
597
1324
|
|
|
1325
|
+
# The value of `recv.each_with_object(memo) { |x, acc| … }` given `call_type`, the call's dispatched type
|
|
1326
|
+
# (issue #2411): the memo as the block's content mutations of `acc` leave it ({#each_with_object_return}).
|
|
1327
|
+
# `ExpressionTyper` reads it for every call, so a value position — a method's tail, which
|
|
1328
|
+
# {DefReturnTyper} types through `Scope#type_of` — answers what a statement does; read off the dispatch
|
|
1329
|
+
# alone, `def m = xs.each_with_object({}) { |x, h| h[x] = 1 }` returned the memo argument's `{}`.
|
|
1330
|
+
def each_with_object_value(call_node, call_type)
|
|
1331
|
+
each_with_object_return(call_node, call_type)
|
|
1332
|
+
end
|
|
1333
|
+
|
|
598
1334
|
private
|
|
599
1335
|
|
|
600
1336
|
attr_reader :scope, :tracer
|
|
601
1337
|
|
|
1338
|
+
# ADR-129 WD2 — the calling thread's {TypingState::State}, cached on the evaluator the first time a slot is read.
|
|
1339
|
+
# An evaluator kept across threads must be kept per state instead ({ExpressionTyper#typing_state}). A frame
|
|
1340
|
+
# install whose block's value is the answer calls {TypingState.current} instead: Rigor's own analysis does not
|
|
1341
|
+
# adopt this memoised reader's type at a self-call (#525's self-purity), so the install's answer would read as
|
|
1342
|
+
# `Dynamic` through it.
|
|
1343
|
+
def typing_state = @typing_state ||= TypingState.current
|
|
1344
|
+
|
|
602
1345
|
# Thread the scope through every child statement in declaration order. The body's value is the type of the last
|
|
603
1346
|
# statement (or `Constant[nil]` for an empty body); intermediate statements' types are discarded, but their scope
|
|
604
1347
|
# effects are preserved.
|
|
@@ -608,7 +1351,7 @@ module Rigor
|
|
|
608
1351
|
def eval_statements(node)
|
|
609
1352
|
result_type = Type::Combinator.constant_of(nil)
|
|
610
1353
|
current = scope
|
|
611
|
-
raising =
|
|
1354
|
+
raising = typing_state[RETRY_FRAMES]
|
|
612
1355
|
node.body.each do |stmt|
|
|
613
1356
|
result_type, current = sub_eval(stmt, current)
|
|
614
1357
|
record_raise_points(raising, stmt, current) if raising
|
|
@@ -626,11 +1369,11 @@ module Rigor
|
|
|
626
1369
|
end
|
|
627
1370
|
|
|
628
1371
|
# `name = rvalue` evaluates the rvalue under the entry scope (so earlier assignments in a chained `a = b = expr`
|
|
629
|
-
# propagate left-to-right) and binds `name` to the result type.
|
|
630
|
-
# to a follow-up; for now they degrade to "type the rhs, do not rebind" via the default branch in {#evaluate}.
|
|
1372
|
+
# propagate left-to-right) and binds `name` to the result type.
|
|
631
1373
|
def eval_local_write(node)
|
|
632
1374
|
rhs_type, post_rhs = sub_eval(node.value, scope)
|
|
633
1375
|
bound = bind_local_write(node, rhs_type, post_rhs)
|
|
1376
|
+
bound = bound.with_construction_fact(node.name, :synthesised) if synthesised_write?(node, rhs_type)
|
|
634
1377
|
# Issue #667 — `m = AppConfig::MODE` makes `m` a copy of a constant the reader's file does not
|
|
635
1378
|
# declare, so `m == :production` folding to `true` is the project's configuration and not a logic
|
|
636
1379
|
# error the author can see. Stamped LAST, across every binding path above: an RHS can be both an
|
|
@@ -641,6 +1384,53 @@ module Rigor
|
|
|
641
1384
|
[rhs_type, bound.with_published_constant_mark(:local, node.name)]
|
|
642
1385
|
end
|
|
643
1386
|
|
|
1387
|
+
# Issue #2555 L3 — whether the object a local write binds is one the engine built itself, so the binding
|
|
1388
|
+
# carries the `:synthesised` construction fact ({Scope#construction_fact?}): a collection literal (`[...]`,
|
|
1389
|
+
# `{...}`), or a constructor call on the core class (`Array.new(n, 0)`, `Hash.new(0)`, `Set.new(xs)`,
|
|
1390
|
+
# `Set[1]`) whose value is a carrier of that class. Nothing else is stamped: not a copy of another binding
|
|
1391
|
+
# (`b = a`), a call whose return a signature declares, a parameter, or an instance variable.
|
|
1392
|
+
# The fact is withheld from a local that may escape anywhere in the body ({ConstructionEscape}): a store through
|
|
1393
|
+
# another holder of the object is one no seam sees, and the fact would leave the carrier without the floor that
|
|
1394
|
+
# covered it.
|
|
1395
|
+
def synthesised_write?(node, rhs_type)
|
|
1396
|
+
synthesised_construction?(node.value, rhs_type) &&
|
|
1397
|
+
!ConstructionEscape.escaped?(scope.match_frame&.body, node.name, constructed_carrier_class(rhs_type), scope)
|
|
1398
|
+
end
|
|
1399
|
+
|
|
1400
|
+
def synthesised_construction?(value_node, rhs_type)
|
|
1401
|
+
case value_node
|
|
1402
|
+
when Prism::ArrayNode, Prism::HashNode then true
|
|
1403
|
+
when Prism::CallNode then core_constructor_call?(value_node, rhs_type)
|
|
1404
|
+
else false
|
|
1405
|
+
end
|
|
1406
|
+
end
|
|
1407
|
+
|
|
1408
|
+
CORE_CONSTRUCTOR_NAMES = %i[new []].to_set.freeze
|
|
1409
|
+
private_constant :CORE_CONSTRUCTOR_NAMES
|
|
1410
|
+
|
|
1411
|
+
# The constant receiver must resolve to the core class itself (a project's own `Admin::Set` does not), and the
|
|
1412
|
+
# value must be a carrier of that class.
|
|
1413
|
+
def core_constructor_call?(call, rhs_type)
|
|
1414
|
+
receiver = call.receiver
|
|
1415
|
+
return false unless CORE_CONSTRUCTOR_NAMES.include?(call.name)
|
|
1416
|
+
return false unless receiver.is_a?(Prism::ConstantReadNode) || receiver.is_a?(Prism::ConstantPathNode)
|
|
1417
|
+
|
|
1418
|
+
class_name = constructed_carrier_class(rhs_type)
|
|
1419
|
+
return false if class_name.nil?
|
|
1420
|
+
|
|
1421
|
+
receiver_type = scope.type_of(receiver, tracer: tracer)
|
|
1422
|
+
receiver_type.is_a?(Type::Singleton) && receiver_type.class_name == class_name
|
|
1423
|
+
end
|
|
1424
|
+
|
|
1425
|
+
def constructed_carrier_class(type)
|
|
1426
|
+
case type
|
|
1427
|
+
when Type::Tuple then "Array"
|
|
1428
|
+
when Type::HashShape then "Hash"
|
|
1429
|
+
when Type::Nominal then type.class_name if %w[Array Hash Set].include?(type.class_name)
|
|
1430
|
+
when Type::Constant then "Set" if type.value.is_a?(::Set)
|
|
1431
|
+
end
|
|
1432
|
+
end
|
|
1433
|
+
|
|
644
1434
|
# The binding half of {#eval_local_write}: the three mutually exclusive provenance paths a local write
|
|
645
1435
|
# can take, unchanged from when they were inline.
|
|
646
1436
|
def bind_local_write(node, rhs_type, post_rhs)
|
|
@@ -710,7 +1500,7 @@ module Rigor
|
|
|
710
1500
|
def rhs_origin(value_node, scope_after_rhs, rhs_type)
|
|
711
1501
|
return nil unless rhs_type.is_a?(Type::Dynamic)
|
|
712
1502
|
|
|
713
|
-
scope_after_rhs.
|
|
1503
|
+
scope_after_rhs.dynamic_origin(value_node)
|
|
714
1504
|
end
|
|
715
1505
|
|
|
716
1506
|
# The scope binding the written local with ADR-58's `:local` mark when the write copies a declaration-sourced
|
|
@@ -740,8 +1530,7 @@ module Rigor
|
|
|
740
1530
|
# Slice 7 phase 1 — instance/class/global variable writes. Each handler evaluates the rvalue under the entry scope
|
|
741
1531
|
# and binds the named variable into the post-scope's per-kind binding map. The expression value is the rvalue
|
|
742
1532
|
# type, matching Ruby's semantics. Bindings are method-local: a fresh scope is built at every `def` entry through
|
|
743
|
-
# `build_method_entry_scope
|
|
744
|
-
# lands.
|
|
1533
|
+
# `build_method_entry_scope`; cross-method ivar and cvar state enters it through the class-level seeds.
|
|
745
1534
|
def eval_ivar_write(node)
|
|
746
1535
|
rhs_type, post_rhs = sub_eval(node.value, scope)
|
|
747
1536
|
# Issue #1446 — a write ends a class guard's narrowing of the variable, as it does a global's.
|
|
@@ -980,13 +1769,13 @@ module Rigor
|
|
|
980
1769
|
# narrowing keyed on `(receiver_kind, receiver_name,
|
|
981
1770
|
# literal_key)` when both receiver and key are stable
|
|
982
1771
|
# (see {Inference::IndexedNarrowing}). Unstable shapes
|
|
983
|
-
#
|
|
984
|
-
# `Prism::IndexOrWriteNode` default-branch behaviour.
|
|
1772
|
+
# record no narrowing.
|
|
985
1773
|
#
|
|
986
1774
|
# The expression value is the result type, matching Ruby's semantics: `(x = params[:f] ||= []); x` observes the
|
|
987
1775
|
# post-`||=` value, not the rvalue alone.
|
|
988
1776
|
def eval_index_or_write(node)
|
|
989
|
-
|
|
1777
|
+
operands = index_write_operand_scope(node)
|
|
1778
|
+
_rhs_type, post_rhs = sub_eval(node.value, operands)
|
|
990
1779
|
result_type = index_write_stored_type(node, scope)
|
|
991
1780
|
|
|
992
1781
|
# A narrowing is keyed on ONE literal slot — `a[k]` — but a multi-index `||=` reads and
|
|
@@ -994,8 +1783,7 @@ module Rigor
|
|
|
994
1783
|
# record `a[0]`'s type as the region answer: `a[0, 1] ||= []` would claim `a[0]` non-nil
|
|
995
1784
|
# where the store splices nothing and `a[0]` stays nil at runtime. Decline the record for
|
|
996
1785
|
# any form but the single-index one.
|
|
997
|
-
|
|
998
|
-
address = key_node && IndexedNarrowing.stable_address(node.receiver, key_node)
|
|
1786
|
+
address = recordable_slot_address(node)
|
|
999
1787
|
# Issue #544 — a receiver with an untracked (Dynamic / Top) constituent can hold a caller-supplied
|
|
1000
1788
|
# slot value the `||=` keeps, so the recorded default would invent a fact; decline the record.
|
|
1001
1789
|
address = nil if address && !IndexedNarrowing.fully_tracked_receiver_type?(scope.type_of(node.receiver))
|
|
@@ -1004,7 +1792,9 @@ module Rigor
|
|
|
1004
1792
|
# the reverse order would trade this feature away for the widening. The two are complementary — the shape
|
|
1005
1793
|
# forgets that the collection is still empty, the narrowing remembers that THIS slot is now non-nil.
|
|
1006
1794
|
post = IndexWriteWidening.widen(node: node, current_scope: post,
|
|
1007
|
-
arg_types: index_write_arg_types(node, result_type))
|
|
1795
|
+
arg_types: index_write_arg_types(node, result_type, join_scope: operands))
|
|
1796
|
+
# Issue #1218 — a key that is not a literal (`h[k] ||= v`) may name a slot another `||=` narrowed.
|
|
1797
|
+
post = IndexedNarrowing.invalidate_indexed_write(node, post)
|
|
1008
1798
|
post = post.with_indexed_narrowing(*address, result_type) if address
|
|
1009
1799
|
|
|
1010
1800
|
# The value the expression yields takes the memoizing reading ({#index_compound_write_value}, #1155) for a
|
|
@@ -1029,6 +1819,29 @@ module Rigor
|
|
|
1029
1819
|
erased || stored
|
|
1030
1820
|
end
|
|
1031
1821
|
|
|
1822
|
+
# The memoizing value on an untracked receiver, or nil when it keeps the stored `Dynamic[top] | v`. A `{}` /
|
|
1823
|
+
# `[]` rvalue (a shape, a tuple, a bare `Hash` / `Array`) erases to `Hash[untyped, untyped]` /
|
|
1824
|
+
# `Array[untyped]`, since the table is not empty after the first store. A construction (a nominal,
|
|
1825
|
+
# parameterised or not, or a `Data` / `Struct` instance, possibly with a `nil` / `false` arm) is kept as the
|
|
1826
|
+
# #1155 reading has it. Anything else carries literal precision (`1`, `:a`, an integer range) and yields nil.
|
|
1827
|
+
def untracked_memo_value(rhs)
|
|
1828
|
+
erased = erase_memo_literal(rhs)
|
|
1829
|
+
return erased if erased
|
|
1830
|
+
|
|
1831
|
+
truthy = Narrowing.narrow_truthy(rhs)
|
|
1832
|
+
members = truthy.is_a?(Type::Union) ? truthy.members : [truthy]
|
|
1833
|
+
rhs if members.all? { |member| MEMO_CONSTRUCTIONS.any? { |klass| member.is_a?(klass) } }
|
|
1834
|
+
end
|
|
1835
|
+
|
|
1836
|
+
MEMO_CONSTRUCTIONS = [Type::Nominal, Type::DataInstance, Type::StructInstance, Type::Dynamic, Type::Top].freeze
|
|
1837
|
+
private_constant :MEMO_CONSTRUCTIONS
|
|
1838
|
+
|
|
1839
|
+
def erase_memo_literal(type)
|
|
1840
|
+
return erase_memo_container(type) unless type.is_a?(Type::Nominal)
|
|
1841
|
+
|
|
1842
|
+
erase_memo_container(type) if type.type_args.empty? && %w[Hash Array].include?(type.class_name)
|
|
1843
|
+
end
|
|
1844
|
+
|
|
1032
1845
|
def erase_memo_container(type)
|
|
1033
1846
|
any = Type::Combinator.untyped
|
|
1034
1847
|
case type
|
|
@@ -1047,14 +1860,76 @@ module Rigor
|
|
|
1047
1860
|
# expression, scope untouched — and the receiver never widened. They store through `[]=` exactly as
|
|
1048
1861
|
# `eval_index_or_write` does, so they take the same widening; the stored value is the compound result —
|
|
1049
1862
|
# `falsey(h[k]) | v` for `&&=`, the dispatched `h[k] + v` for `+=` — not the rvalue alone.
|
|
1863
|
+
#
|
|
1864
|
+
# Issue #1218 — the widening carries the receiver's slot narrowings across its rebind
|
|
1865
|
+
# ({MutationRejoin.carry_slot_narrowings}), so a `h[:e] ||= 0` narrowing outlived `h[:e] += 1` and the slot
|
|
1866
|
+
# kept reading `0`. The store drops the narrowing on the slot it overwrote, as a `[]=` call does
|
|
1867
|
+
# ({IndexedNarrowing.invalidate_indexed_write}: every narrowing on the receiver when the index is not one
|
|
1868
|
+
# literal key). A slot that held a narrowing then records the value the write stored, which the read it
|
|
1869
|
+
# performed took from that narrowing; a slot with none records nothing new.
|
|
1050
1870
|
def eval_index_write(node)
|
|
1051
|
-
|
|
1871
|
+
operands = index_write_operand_scope(node)
|
|
1872
|
+
_rhs_type, post_rhs = sub_eval(node.value, operands)
|
|
1052
1873
|
stored = index_write_stored_type(node, scope)
|
|
1053
1874
|
widened = IndexWriteWidening.widen(node: node, current_scope: post_rhs,
|
|
1054
|
-
arg_types: index_write_arg_types(node, stored))
|
|
1875
|
+
arg_types: index_write_arg_types(node, stored, join_scope: operands))
|
|
1876
|
+
widened = IndexedNarrowing.invalidate_indexed_write(node, widened)
|
|
1877
|
+
address = recordable_slot_address(node)
|
|
1878
|
+
widened = widened.with_indexed_narrowing(*address, stored) if address && scope.indexed_narrowing(*address)
|
|
1055
1879
|
[stored, forget_rebound_specials(widened, node)]
|
|
1056
1880
|
end
|
|
1057
1881
|
|
|
1882
|
+
# The `(receiver, key)` slot a compound index write may record its stored value on, or nil. A narrowing is
|
|
1883
|
+
# keyed on ONE literal slot, so a multi-index form, which reads and stores a splice region, records none. The
|
|
1884
|
+
# receiver's `[]=` must leave the stored value at that slot ({IndexedNarrowing.verbatim_slot?}: a `Hash` or
|
|
1885
|
+
# `Array`, and no negative index on an `Array`), and the rvalue must not rebind the receiver variable
|
|
1886
|
+
# ({#rvalue_rebinds_receiver?}), since the store went to the object it named before (issue #1218).
|
|
1887
|
+
def recordable_slot_address(node)
|
|
1888
|
+
key_node = single_index_argument(node)
|
|
1889
|
+
address = key_node && IndexedNarrowing.stable_address(node.receiver, key_node)
|
|
1890
|
+
return nil if address.nil?
|
|
1891
|
+
return nil unless IndexedNarrowing.verbatim_slot?(scope.type_of(node.receiver, tracer: tracer), address.last)
|
|
1892
|
+
return nil if rvalue_rebinds_receiver?(node)
|
|
1893
|
+
|
|
1894
|
+
address
|
|
1895
|
+
end
|
|
1896
|
+
|
|
1897
|
+
# Whether a compound index write's rvalue may rebind its stable receiver variable: it writes the variable
|
|
1898
|
+
# (`h[k] += (h = {}; 1)`, `@h ||= …`, a multiple-assignment target, in a block too:
|
|
1899
|
+
# {OperandEffects.written_variables}), or, for an instance variable, the intervening-call reset past the
|
|
1900
|
+
# rvalue's self calls rebinds it ({InterveningIvarReset.after_operand}). That reset widens only a binding
|
|
1901
|
+
# narrower than the class-ivar seed, and only for a call not proven to leave it alone, so
|
|
1902
|
+
# `@h = { k: nil }; @h[:k] += (reset; 1)` with a `reset` that assigns `@h` is a rebind, and `@t[:k] ||=
|
|
1903
|
+
# compute(1)` on a `@t` the class binds to one shape is not. A store into another slot of the receiver
|
|
1904
|
+
# (`h[:a] ||= (h[:b] ||= [])`) is no rebind either.
|
|
1905
|
+
def rvalue_rebinds_receiver?(node)
|
|
1906
|
+
receiver = node.receiver
|
|
1907
|
+
name = receiver.name
|
|
1908
|
+
return true if OperandEffects.written_variables(node.value).include?(name)
|
|
1909
|
+
return false unless receiver.is_a?(Prism::InstanceVariableReadNode)
|
|
1910
|
+
|
|
1911
|
+
!InterveningIvarReset.after_operand(node.value, scope).ivar(name).equal?(scope.ivar(name))
|
|
1912
|
+
end
|
|
1913
|
+
|
|
1914
|
+
# The scope a compound index write's rvalue runs from: Ruby evaluates the receiver, then the index arguments,
|
|
1915
|
+
# then the rvalue, so a write in either is threaded in that order ({#thread_operand}) before the rvalue. An
|
|
1916
|
+
# index write takes no block-pass argument (a syntax error since Ruby 3.4).
|
|
1917
|
+
#
|
|
1918
|
+
# Issue #1272 — `(h ||= {})[k] ||= v`, `(@h ||= {})[k] ||= v` and `h[(i = 1)] ||= v` left `h`, `@h` and `i`
|
|
1919
|
+
# on their pre-write bindings, so a later `h.fetch(k)` on a nil-initialised `h` reported a call on `nil`. A
|
|
1920
|
+
# receiver or argument holding no write answers the entry scope unchanged.
|
|
1921
|
+
#
|
|
1922
|
+
# The receiver's joinability is read from this scope ({#index_write_arg_types}' `join_scope`), since the
|
|
1923
|
+
# binding the store widens is the one the receiver's write left (`h` is `{}`, not the entry's `nil`). The read,
|
|
1924
|
+
# the stored value and the widening's argument types are still typed from the entry scope (TODO #3100): the
|
|
1925
|
+
# receiver expression's own value (`(h ||= {})` is the hash) answers what its write produced, but an rvalue
|
|
1926
|
+
# reading a variable an index argument writes (`h[(i = 1)] ||= i + 1`) is typed at its pre-write binding.
|
|
1927
|
+
def index_write_operand_scope(node)
|
|
1928
|
+
walk = OperandWalk.new(walk_recorder)
|
|
1929
|
+
after = thread_operand(node.receiver, scope, walk, scope)
|
|
1930
|
+
thread_operand(node.arguments, after, walk, scope)
|
|
1931
|
+
end
|
|
1932
|
+
|
|
1058
1933
|
# `[index_type..., stored_value_type]` for an index-write node, shaped exactly like a `[]=`
|
|
1059
1934
|
# call's argument list so the widening seam can join it the same way (issue #560) — a
|
|
1060
1935
|
# two-index compound write (`a[0, 1] += v`) keeps BOTH index arguments ahead of the stored
|
|
@@ -1068,15 +1943,16 @@ module Rigor
|
|
|
1068
1943
|
# The index arguments are typed, and the receiver's joinability read, in `type_scope`: the
|
|
1069
1944
|
# evaluator's entry scope by default; a `for` index passes its post-collection scope and a
|
|
1070
1945
|
# rescue reference its arm's entry scope, the nearest the engine has to where Ruby evaluates
|
|
1071
|
-
# them (each iteration, the moment of the catch).
|
|
1946
|
+
# them (each iteration, the moment of the catch). A compound index write reads the joinability alone in
|
|
1947
|
+
# `join_scope`, the scope its receiver and index arguments left ({#index_write_operand_scope}).
|
|
1072
1948
|
# There is deliberately NO `rescue` here. `Scope#type_of` is a total query over well-formed Prism input,
|
|
1073
1949
|
# so a raise is an engine bug, and swallowing it would silently downgrade a live seam to "no evidence" —
|
|
1074
1950
|
# the join would quietly stop happening with nothing to show for it. Let it reach the runner's
|
|
1075
1951
|
# internal-error path, where it is visible.
|
|
1076
|
-
def index_write_arg_types(node, stored_type, type_scope: scope)
|
|
1952
|
+
def index_write_arg_types(node, stored_type, type_scope: scope, join_scope: type_scope)
|
|
1077
1953
|
args = node.arguments
|
|
1078
1954
|
return MutationWidening::NO_ARG_TYPES if args.nil? || stored_type.nil?
|
|
1079
|
-
return MutationWidening::NO_ARG_TYPES unless MutationWidening.joinable_receiver?(node.receiver,
|
|
1955
|
+
return MutationWidening::NO_ARG_TYPES unless MutationWidening.joinable_receiver?(node.receiver, join_scope)
|
|
1080
1956
|
|
|
1081
1957
|
list = args.respond_to?(:arguments) ? args.arguments : args
|
|
1082
1958
|
# A splat argument is marked `nil` — its expansion decides the store's arity at
|
|
@@ -1113,6 +1989,28 @@ module Rigor
|
|
|
1113
1989
|
end
|
|
1114
1990
|
end
|
|
1115
1991
|
|
|
1992
|
+
# Whether a memoizing `||=` on `receiver` reads as the fresh rvalue ({#index_compound_write_value}): the
|
|
1993
|
+
# receiver's type passes the #544 test, and the receiver is a local the current body binds, captured into a
|
|
1994
|
+
# block or not, and no parameter of the enclosing method names ({MatchRebinding::Frame#body_local?}), or
|
|
1995
|
+
# an object fresh at every run (`Hash.new`, {RepeatedOrWrites.fresh_object?}) (issue
|
|
1996
|
+
# #2354). An ivar, cvar or global read, a parameter, or any other expression (a call result, a
|
|
1997
|
+
# parenthesised `||=`) is a table whose gradual slot another call, or an earlier run of this one, may have
|
|
1998
|
+
# filled — an ivar seeded `{}` in `initialize`, a parameter declared `Hash[Symbol, untyped]` — so it widens
|
|
1999
|
+
# through {#untracked_memo_value} like an untracked receiver. Without a frame (no method, class or file
|
|
2000
|
+
# body stamped one) a local receiver is treated as body-bound, as before #2354.
|
|
2001
|
+
def tracked_memo_receiver?(receiver)
|
|
2002
|
+
return false unless body_bound_memo_receiver?(receiver)
|
|
2003
|
+
|
|
2004
|
+
IndexedNarrowing.fully_tracked_receiver_type?(scope.type_of(receiver, tracer: tracer))
|
|
2005
|
+
end
|
|
2006
|
+
|
|
2007
|
+
def body_bound_memo_receiver?(receiver)
|
|
2008
|
+
return RepeatedOrWrites.fresh_object?(receiver) unless receiver.is_a?(Prism::LocalVariableReadNode)
|
|
2009
|
+
|
|
2010
|
+
frame = scope.match_frame
|
|
2011
|
+
frame.nil? || frame.body_local?(receiver.name)
|
|
2012
|
+
end
|
|
2013
|
+
|
|
1116
2014
|
# True when {#index_compound_write_value} reads the `||=` `node` as the memoization idiom's rvalue: the slot
|
|
1117
2015
|
# reads wholly gradual, the rvalue can store something truthy, and no block-return pass marked the site.
|
|
1118
2016
|
def memoizing_index_read?(node, current, rhs)
|
|
@@ -1160,29 +2058,113 @@ module Rigor
|
|
|
1160
2058
|
# Both mutator-receiver widenings, against one typing of the call's arguments: the bindings the
|
|
1161
2059
|
# receiver NAMES ({MutationWidening.widen_after_call}) and, when it names none because the
|
|
1162
2060
|
# receiver is an element read into a local, that element's pin inside its container
|
|
1163
|
-
# ({ElementReadWidening.widen_element_read}, issue #643)
|
|
1164
|
-
|
|
2061
|
+
# ({ElementReadWidening.widen_element_read}, issue #643) — and, issue #2499, the String a stdlib
|
|
2062
|
+
# out-buffer call fills ({OutBufferMutation}), which names no receiver of the call at all.
|
|
2063
|
+
#
|
|
2064
|
+
# Issue #2335 — a literal-symbol `send` (`a.send(:push, x)`) widens as the call it makes, and a mutation through a
|
|
2065
|
+
# receiver that may be another local's object floors every local whose object another holder may reach
|
|
2066
|
+
# ({AliasMutationFloor}).
|
|
2067
|
+
def widen_mutated_receivers(node, current_scope)
|
|
2068
|
+
call_node = AliasMutationFloor.literal_send_target(node) || node
|
|
1165
2069
|
arg_types = mutator_arg_types(call_node, current_scope)
|
|
1166
2070
|
widened = MutationWidening.widen_after_call(call_node: call_node, current_scope: current_scope,
|
|
1167
|
-
arg_types: arg_types
|
|
1168
|
-
|
|
2071
|
+
arg_types: arg_types,
|
|
2072
|
+
rewritten_element: rewritten_element(call_node, current_scope))
|
|
2073
|
+
widened = ElementReadWidening.widen_element_read(call_node: call_node, current_scope: widened,
|
|
2074
|
+
arg_types: arg_types)
|
|
2075
|
+
if AliasMutationFloor.candidate?(call_node)
|
|
2076
|
+
widened = AliasMutationFloor.floor_after_call(call_node, widened) do
|
|
2077
|
+
named_receiver_locals(call_node.receiver)
|
|
2078
|
+
end
|
|
2079
|
+
end
|
|
2080
|
+
# Issue #2499 — a stdlib call that fills a String argument (`io.read(n, buf)`, `StringIO.new(s)`).
|
|
2081
|
+
floor_ivar_mutating_callee(node, OutBufferMutation.floor(call_node, widened))
|
|
1169
2082
|
end
|
|
1170
2083
|
|
|
1171
|
-
#
|
|
1172
|
-
def
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
2084
|
+
# Issue #2335 — a call on a local that may hold another local's object (`box = Box.new(a)`) to a project method
|
|
2085
|
+
# that mutates one of its instance variables (`def add(v) = @items << v`) may mutate that object: every local
|
|
2086
|
+
# whose object another holder may reach takes one unknown store ({AliasMutationFloor.floor_after_written_local}).
|
|
2087
|
+
def floor_ivar_mutating_callee(call_node, current_scope)
|
|
2088
|
+
receiver = call_node.receiver
|
|
2089
|
+
return current_scope if MutationWidening::SHAPE_MUTATORS.include?(call_node.name) || send_hand_off?(call_node)
|
|
2090
|
+
return floor_self_ivar_callee(call_node, current_scope) if self_dispatch_call?(call_node)
|
|
2091
|
+
return current_scope unless receiver.is_a?(Prism::LocalVariableReadNode)
|
|
2092
|
+
return current_scope unless callee_bearing?(current_scope.local(receiver.name))
|
|
2093
|
+
return current_scope unless AliasMutationFloor.reaches_other_locals?(current_scope, receiver, call_node)
|
|
2094
|
+
|
|
2095
|
+
callees = receiver_callees_in(scope, call_node)
|
|
2096
|
+
return current_scope unless callees.any? { |def_node, _, _| AliasMutationFloor.mutates_ivar?(def_node) }
|
|
2097
|
+
|
|
2098
|
+
AliasMutationFloor.floor_after_written_local(current_scope, receiver, call_node)
|
|
2099
|
+
end
|
|
2100
|
+
|
|
2101
|
+
# The same for a call on `self` (`push_it(v)`, `self.push_it(v)`): its ivars hold whatever locals were handed to
|
|
2102
|
+
# them (`@items = a`, `a = @q`), so every aliased local holding a literal shape takes one unknown store.
|
|
2103
|
+
def floor_self_ivar_callee(call_node, current_scope)
|
|
2104
|
+
return current_scope unless AliasMutationFloor.holder_call_reaches?(current_scope, call_node)
|
|
2105
|
+
return current_scope unless resolve_self_callees(call_node).any? do |def_node, _, _|
|
|
2106
|
+
AliasMutationFloor.mutates_ivar?(def_node)
|
|
1179
2107
|
end
|
|
1180
2108
|
|
|
1181
|
-
|
|
2109
|
+
AliasMutationFloor.floor_after_holder_call(current_scope, call_node)
|
|
1182
2110
|
end
|
|
1183
2111
|
|
|
1184
|
-
#
|
|
1185
|
-
#
|
|
2112
|
+
# A binding `receiver_callees_in` can resolve a project def on: a class or instance type, or a union of them. Any
|
|
2113
|
+
# other binding resolves none, so the floor skips the resolution for it.
|
|
2114
|
+
def callee_bearing?(type) = type.is_a?(Type::Nominal) || type.is_a?(Type::Singleton) || type.is_a?(Type::Union)
|
|
2115
|
+
|
|
2116
|
+
# The locals a mutator's receiver names ({ReceiverAlias.mutated_reads}), which the named path widens, and the
|
|
2117
|
+
# root of an element read (`c[0].shift`), whose element {ElementReadWidening} widens.
|
|
2118
|
+
def named_receiver_locals(receiver)
|
|
2119
|
+
return nil if receiver.nil?
|
|
2120
|
+
return receiver.name if receiver.is_a?(Prism::LocalVariableReadNode)
|
|
2121
|
+
|
|
2122
|
+
names = ReceiverAlias.mutated_reads(receiver).filter_map do |read|
|
|
2123
|
+
ReceiverAlias.read_name(read) if read.is_a?(Prism::LocalVariableReadNode)
|
|
2124
|
+
end
|
|
2125
|
+
root = ElementReadWidening.element_read_path(receiver)&.first
|
|
2126
|
+
root ? names << root.name : names
|
|
2127
|
+
end
|
|
2128
|
+
|
|
2129
|
+
# Issue #2427 — the element an element rewriter (`map!`, `collect!`, `flatten!`) leaves in its receiver, read
|
|
2130
|
+
# off what the non-bang counterpart answers for the same receiver, block and arguments
|
|
2131
|
+
# ({RewriteMutation::TYPED_COUNTERPARTS}): `a.map!(&:to_i)` stores what `a.map(&:to_i)` returns. `nil` when the
|
|
2132
|
+
# call is no such rewriter, or the counterpart has no single element; the widening then takes
|
|
2133
|
+
# {RewriteMutation}'s gradual arm.
|
|
2134
|
+
#
|
|
2135
|
+
# `nil` too when the receiver may name more than one binding (`(flag ? a : b).map!(&:to_i)`): Ruby rewrites
|
|
2136
|
+
# only the one the branch selects, and typing every candidate gave the other one an element it never holds.
|
|
2137
|
+
#
|
|
2138
|
+
# The block and receiver gates only save typing a counterpart nothing consumes: with no block `map` answers an
|
|
2139
|
+
# Enumerator, which has no element to read, and {RewriteMutation.typed} answers nothing for a receiver the
|
|
2140
|
+
# widening does not rewrite.
|
|
2141
|
+
def rewritten_element(call_node, current_scope)
|
|
2142
|
+
counterpart = RewriteMutation::TYPED_COUNTERPARTS[call_node.name]
|
|
2143
|
+
return nil if counterpart.nil?
|
|
2144
|
+
return nil if counterpart != :flatten && call_node.block.nil? # cost guard
|
|
2145
|
+
return nil unless MutationWidening.joinable_receiver?(call_node.receiver, current_scope) # cost guard
|
|
2146
|
+
return nil unless ReceiverAlias.mutated_reads(call_node.receiver).size == 1
|
|
2147
|
+
|
|
2148
|
+
RewriteMutation.counterpart_element(operand_scope.type_of(call_node.copy(name: counterpart)))
|
|
2149
|
+
end
|
|
2150
|
+
|
|
2151
|
+
# `tr!` / `tr_s!` are typed too: whether they can empty a `non-empty-string` turns on their replacement argument.
|
|
2152
|
+
def mutator_arg_types(call_node, current_scope)
|
|
2153
|
+
name = call_node.name
|
|
2154
|
+
unless ContentJoin::CONTENT_ADDERS.include?(name) || StringMutation::TRANSLATORS.include?(name) ||
|
|
2155
|
+
SetMutation::CONTENT_ADDERS.include?(name)
|
|
2156
|
+
return MutationWidening::NO_ARG_TYPES
|
|
2157
|
+
end
|
|
2158
|
+
unless MutationWidening.joinable_receiver?(call_node.receiver, current_scope) ||
|
|
2159
|
+
ElementReadWidening.joinable_element_read?(call_node.receiver, current_scope)
|
|
2160
|
+
return MutationWidening::NO_ARG_TYPES
|
|
2161
|
+
end
|
|
2162
|
+
|
|
2163
|
+
content_arg_types(call_node, operand_scope, @operand_types)
|
|
2164
|
+
end
|
|
2165
|
+
|
|
2166
|
+
# The index node of an index-write when it holds exactly one index argument — the only form
|
|
2167
|
+
# whose stored value lands on a nameable slot (`a[k]`). Multi-index forms address a splice
|
|
1186
2168
|
# region and answer `nil`.
|
|
1187
2169
|
def single_index_argument(node)
|
|
1188
2170
|
args = node.arguments
|
|
@@ -1250,14 +2232,17 @@ module Rigor
|
|
|
1250
2232
|
[scope.type_of(node, tracer: tracer), forget_rebound_specials(widened, node)]
|
|
1251
2233
|
end
|
|
1252
2234
|
|
|
1253
|
-
# The scope effect of calling the writer `writer` on `receiver` outside a `CallNode`: the receiver widening,
|
|
1254
|
-
#
|
|
2235
|
+
# The scope effect of calling the writer `writer` on `receiver` outside a `CallNode`: the receiver widening, the
|
|
2236
|
+
# drop of every method-chain narrowing rooted at the receiver that `eval_call` makes after any call on it (issue
|
|
2237
|
+
# #2693: `x.r ||= v`, `x.r, y = …` and `for x.r in …` call `r=`), and the receiver-wide drop of recorded
|
|
2238
|
+
# `receiver[key]` narrowings `IndexedNarrowing` makes after a mutator call.
|
|
1255
2239
|
def widen_attribute_write(receiver, writer, current_scope)
|
|
1256
2240
|
widened = MutationWidening.widen_receiver_aliases(receiver, writer, current_scope)
|
|
1257
2241
|
stable = IndexedNarrowing.stable_receiver(receiver)
|
|
1258
|
-
return widened unless stable
|
|
2242
|
+
return widened unless stable
|
|
1259
2243
|
|
|
1260
|
-
widened.
|
|
2244
|
+
widened = widened.without_method_chain_narrowings_for(*stable)
|
|
2245
|
+
IndexedNarrowing.mutator?(writer) ? widened.without_indexed_narrowings_for(*stable) : widened
|
|
1261
2246
|
end
|
|
1262
2247
|
|
|
1263
2248
|
# The attribute targets of a multi-write (`h.default, x = 0, 1`), nested ones included, each widening its
|
|
@@ -1337,11 +2322,7 @@ module Rigor
|
|
|
1337
2322
|
return [Type::Combinator.union(then_type, else_type), then_scope] \
|
|
1338
2323
|
if branch_terminates?(node.subsequent, else_type) && node.statements
|
|
1339
2324
|
|
|
1340
|
-
[
|
|
1341
|
-
Type::Combinator.union(then_type, else_type),
|
|
1342
|
-
UntypedRejoin.apply(join_with_nil_injection(then_scope, else_scope), post_pred,
|
|
1343
|
-
[node.statements, node.subsequent])
|
|
1344
|
-
]
|
|
2325
|
+
if_join(node.statements, node.subsequent, [then_type, then_scope], [else_type, else_scope], post_pred)
|
|
1345
2326
|
end
|
|
1346
2327
|
|
|
1347
2328
|
# `unless pred; t; else; e; end`. Same shape as `if`, but Prism exposes the else-branch as `else_clause` (no elsif
|
|
@@ -1373,11 +2354,22 @@ module Rigor
|
|
|
1373
2354
|
return [Type::Combinator.union(then_type, else_type), then_scope] \
|
|
1374
2355
|
if branch_terminates?(node.else_clause, else_type) && node.statements
|
|
1375
2356
|
|
|
1376
|
-
[
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
2357
|
+
if_join(node.statements, node.else_clause, [then_type, then_scope], [else_type, else_scope], post_pred)
|
|
2358
|
+
end
|
|
2359
|
+
|
|
2360
|
+
# The `[type, post_scope]` an `if` / `unless` with both branches evaluated ends with, once the arms above found no
|
|
2361
|
+
# terminating branch by {#branch_terminates?}.
|
|
2362
|
+
#
|
|
2363
|
+
# Issue #1230 — the then-body (`then_node`) certainly exits beside an else (`else_node`): the only surviving path
|
|
2364
|
+
# is the else that RAN, so the continuation is the else's exit scope, the predicate's other edge PLUS the else's
|
|
2365
|
+
# writes, not a join with the exited body's writes. {#branch_certainly_exits?} is stricter than
|
|
2366
|
+
# {#branch_terminates?}, which the arms above use: a `bot`-typed helper call does not qualify (issue #3110).
|
|
2367
|
+
# Otherwise both exit scopes join with nil-injection.
|
|
2368
|
+
def if_join(then_node, else_node, (then_type, then_scope), (else_type, else_scope), post_pred)
|
|
2369
|
+
type = Type::Combinator.union(then_type, else_type)
|
|
2370
|
+
return [type, else_scope] if else_node && branch_certainly_exits?(then_node)
|
|
2371
|
+
|
|
2372
|
+
[type, UntypedRejoin.apply(join_with_nil_injection(then_scope, else_scope), post_pred, [then_node, else_node])]
|
|
1381
2373
|
end
|
|
1382
2374
|
|
|
1383
2375
|
# Returns the `[type, post_scope]` of the live branch when the predicate is provably truthy / falsey, else nil so
|
|
@@ -1440,7 +2432,7 @@ module Rigor
|
|
|
1440
2432
|
sub_eval(node.statements, scope)
|
|
1441
2433
|
end
|
|
1442
2434
|
|
|
1443
|
-
# `case pred; when ...; when ...; else; end` and the pattern-
|
|
2435
|
+
# `case pred; when ...; when ...; else; end` and the pattern-matching variant. The predicate's post-scope is
|
|
1444
2436
|
# shared with every branch (including the else); branches are evaluated independently and merged with
|
|
1445
2437
|
# nil-injection so half-bound names degrade to `T | nil`.
|
|
1446
2438
|
def eval_case(node)
|
|
@@ -1646,6 +2638,11 @@ module Rigor
|
|
|
1646
2638
|
InterveningIvarReset.may_write?(condition, name, before, run_lambdas: true)
|
|
1647
2639
|
when Prism::LocalVariableReadNode, Prism::LocalVariableWriteNode
|
|
1648
2640
|
false
|
|
2641
|
+
when Prism::GlobalVariableReadNode
|
|
2642
|
+
# Issue #2314 — the question the statement walk asks past a call ({#forget_reached_globals}).
|
|
2643
|
+
body = when_lambda_body(condition)
|
|
2644
|
+
GlobalWriterReach.may_write?(condition, name, before) ||
|
|
2645
|
+
(!body.nil? && GlobalWriterReach.may_write?(body, name, before))
|
|
1649
2646
|
else
|
|
1650
2647
|
body = when_lambda_body(condition)
|
|
1651
2648
|
GuardRebinding.may_rebind?(condition, before) || (!body.nil? && GuardRebinding.may_rebind?(body, before))
|
|
@@ -1757,9 +2754,12 @@ module Rigor
|
|
|
1757
2754
|
# while it ran, put back as this scope binds them ({ErrorInfo.restore}): the control those carry has left every
|
|
1758
2755
|
# rescue clause the construct entered.
|
|
1759
2756
|
def restoring_error_info
|
|
1760
|
-
|
|
2757
|
+
state = typing_state
|
|
2758
|
+
marks = [@next_scope_sink&.size, state[BREAK_SINK]&.size]
|
|
1761
2759
|
type, after = yield
|
|
1762
|
-
|
|
2760
|
+
break_sink = state[BREAK_SINK]
|
|
2761
|
+
state.touch(BREAK_SINK) if break_sink && marks.last && marks.last < break_sink.size
|
|
2762
|
+
[[@next_scope_sink, marks.first], [break_sink, marks.last]].each do |sink, mark|
|
|
1763
2763
|
next if sink.nil? || mark.nil?
|
|
1764
2764
|
|
|
1765
2765
|
(mark...sink.size).each do |index|
|
|
@@ -1782,9 +2782,12 @@ module Rigor
|
|
|
1782
2782
|
# after it, or a rescue clause, may have set `$_` enters with it forgotten; a rescue clause runs after any
|
|
1783
2783
|
# prefix of the body, and so reads it forgotten whenever the body may set it.
|
|
1784
2784
|
entry = edge ? forget_guard_if_rebinds(LastLine.forget_if_set(scope, node), node) : scope
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
2785
|
+
rescue_entry = rescue_entry_scope(node, entry)
|
|
2786
|
+
collector = begin_fact_collector(node, rescue_entry, edge)
|
|
2787
|
+
primary_type, primary_scope = eval_begin_primary_under(node, entry, edge: edge, collector: collector)
|
|
2788
|
+
rescue_chain = collect_rescue_chain_results(node.rescue_clause, rescue_entry,
|
|
2789
|
+
edge: edge, writes: rescue_writes(node, edge),
|
|
2790
|
+
raise_scopes: collector&.raise_scopes)
|
|
1788
2791
|
|
|
1789
2792
|
# B2.1 — retry-edge widening. When a `retry` in the rescue chain targets this `begin`, control re-enters the
|
|
1790
2793
|
# primary body carrying every rebind made before the retry: the arm's (`rescue; tries += 1; retry; end`), and
|
|
@@ -1880,7 +2883,10 @@ module Rigor
|
|
|
1880
2883
|
# none, so an `ensure` clause inside it, nested at any depth, is evaluated once rather than once per pass of
|
|
1881
2884
|
# every enclosing clause holding a jump.
|
|
1882
2885
|
def jump_sinks_installed?
|
|
1883
|
-
|
|
2886
|
+
return true unless @next_scope_sink.nil?
|
|
2887
|
+
|
|
2888
|
+
state = typing_state
|
|
2889
|
+
JUMP_SINKS.any? { |slot| state[slot] }
|
|
1884
2890
|
end
|
|
1885
2891
|
|
|
1886
2892
|
# Whether `node` holds a `return`, `next` or `break` outside a nested `def`.
|
|
@@ -1936,12 +2942,8 @@ module Rigor
|
|
|
1936
2942
|
end
|
|
1937
2943
|
|
|
1938
2944
|
# Runs `block` with no `return`, `next`, `break` or retry-frame sink installed.
|
|
1939
|
-
def without_jump_sinks
|
|
1940
|
-
|
|
1941
|
-
CLAUSE_SINK_KEYS.each { |key| Thread.current[key] = nil }
|
|
1942
|
-
yield
|
|
1943
|
-
ensure
|
|
1944
|
-
CLAUSE_SINK_KEYS.zip(previous).each { |key, value| Thread.current[key] = value } if previous
|
|
2945
|
+
def without_jump_sinks(&)
|
|
2946
|
+
TypingState.current.without(CLAUSE_SINKS, &)
|
|
1945
2947
|
end
|
|
1946
2948
|
|
|
1947
2949
|
# The scope a rescue clause of `node` enters with: it runs after any prefix of the body, so neither a `$_`
|
|
@@ -1950,6 +2952,65 @@ module Rigor
|
|
|
1950
2952
|
forget_guard_if_rebinds(LastLine.forget_if_set(entry, node.statements), node.statements)
|
|
1951
2953
|
end
|
|
1952
2954
|
|
|
2955
|
+
# Issue #2691 — a rescue clause runs after any prefix of the `begin` body raised, so a fact the clause's
|
|
2956
|
+
# entry scope holds for an indexed read (`h[:k]`) or a single-hop chain (`x.r`) survives into it only where
|
|
2957
|
+
# EVERY scope the body can raise from still binds the identical type object. `raise_scopes` is that set —
|
|
2958
|
+
# the post-scope of each statement of the body's frame and the scope it ends with
|
|
2959
|
+
# ({#eval_begin_primary_under}'s collector) — not the fall-through exit alone: `eval_if` and kin discard a
|
|
2960
|
+
# branch that terminates, so a write on `if c; @x.r = nil; raise; end` never reaches the exit scope.
|
|
2961
|
+
# `equal?` keeps only an untouched fact: a re-narrowed or re-joined one is a new object and drops (the
|
|
2962
|
+
# conservative side).
|
|
2963
|
+
#
|
|
2964
|
+
# The residual is a fact the body drops and then re-establishes as the identical (interned) type object:
|
|
2965
|
+
# raise points after the re-establishment keep it, which is correct, and the only unsound window would be a
|
|
2966
|
+
# raise point whose recorded scope still binds the object although the fact was invalidated and not yet
|
|
2967
|
+
# re-established. No producer reaches that today — a rebind or a clause widening answers `Dynamic[top]`,
|
|
2968
|
+
# never the fact's type ({ClauseWidening}), and a widened slot read floors to `Dynamic` rather than
|
|
2969
|
+
# reinstalling the recorded object.
|
|
2970
|
+
#
|
|
2971
|
+
# The filter covers rescue entries only: an `ensure` clause enters from the scope past the `begin` — the
|
|
2972
|
+
# join of the fall-through exits — so it keeps a fact a terminating-branch write voided, the pre-#2691
|
|
2973
|
+
# hole left open because collecting raise points for every `ensure`-only `begin` costs a frame walk.
|
|
2974
|
+
def clause_fact_scope(entry_scope, raise_scopes)
|
|
2975
|
+
return entry_scope if entry_scope.indexed_narrowings.empty? && entry_scope.method_chain_narrowings.empty?
|
|
2976
|
+
|
|
2977
|
+
indexed = entry_scope.indexed_narrowings.reduce(entry_scope) do |acc, (key, type)|
|
|
2978
|
+
if raise_scopes.all? { |point| point.indexed_narrowings[key].equal?(type) }
|
|
2979
|
+
acc
|
|
2980
|
+
else
|
|
2981
|
+
acc.without_indexed_narrowing(key.receiver_kind, key.receiver_name, key.key)
|
|
2982
|
+
end
|
|
2983
|
+
end
|
|
2984
|
+
indexed.method_chain_narrowings.reduce(indexed) do |acc, (key, type)|
|
|
2985
|
+
if raise_scopes.all? { |point| point.method_chain_narrowings[key].equal?(type) }
|
|
2986
|
+
acc
|
|
2987
|
+
else
|
|
2988
|
+
acc.without_method_chain_narrowing(key.receiver_kind, key.receiver_name, key.method_name)
|
|
2989
|
+
end
|
|
2990
|
+
end
|
|
2991
|
+
end
|
|
2992
|
+
|
|
2993
|
+
# Issue #2691 — the {BodyRaiseScopes} {#eval_begin_primary_under} fills while evaluating `node`'s primary
|
|
2994
|
+
# body, or nil when {#clause_fact_scope} has nothing to filter: no rescue clause, no body, or a `clause_entry`
|
|
2995
|
+
# (the scope the chain enters with — `rescue_entry_scope`'s result, or the retry-widened entry under an
|
|
2996
|
+
# edge) holding no indexed or method-chain fact. The frame walk is skipped unless a fact could drop, so a
|
|
2997
|
+
# `begin` whose entry binds none pays nothing.
|
|
2998
|
+
def begin_fact_collector(node, clause_entry, edge)
|
|
2999
|
+
return nil unless node.rescue_clause && node.statements
|
|
3000
|
+
return nil if clause_entry.indexed_narrowings.empty? && clause_entry.method_chain_narrowings.empty?
|
|
3001
|
+
|
|
3002
|
+
frame = edge ? edge.frame : primary_body_frame(node.statements)
|
|
3003
|
+
BodyRaiseScopes.new(frame: frame, raise_scopes: [])
|
|
3004
|
+
end
|
|
3005
|
+
|
|
3006
|
+
# The set {#walk_primary_frame} collects for a primary body — the nodes whose post-statement scopes are the
|
|
3007
|
+
# body's raise points — without the write-name pass a retry edge also needs.
|
|
3008
|
+
def primary_body_frame(statements)
|
|
3009
|
+
frame = Set.new.compare_by_identity
|
|
3010
|
+
walk_primary_frame(statements, true, frame, Set.new)
|
|
3011
|
+
frame
|
|
3012
|
+
end
|
|
3013
|
+
|
|
1953
3014
|
# Rescue arms that never fall through contribute neither a type fragment NOR a scope to the post-begin flow —
|
|
1954
3015
|
# control left the `begin` via that arm. That is an arm ending in `return`, `next`, `break`, `raise`, `throw`,
|
|
1955
3016
|
# `exit`, `abort` or `fail`, and one whose type is `bot` ({#branch_terminates?}): an arm ending in `retry`, or in
|
|
@@ -1968,7 +3029,7 @@ module Rigor
|
|
|
1968
3029
|
def ensure_jump_marks(node)
|
|
1969
3030
|
return nil unless node.ensure_clause
|
|
1970
3031
|
|
|
1971
|
-
[@next_scope_sink&.size,
|
|
3032
|
+
[@next_scope_sink&.size, typing_state[BREAK_SINK]&.size]
|
|
1972
3033
|
end
|
|
1973
3034
|
|
|
1974
3035
|
# A `next` or `break` inside `begin … ensure` leaves only once the `ensure` clause has run, so each jump scope the
|
|
@@ -1978,7 +3039,10 @@ module Rigor
|
|
|
1978
3039
|
def carry_jumps_through_ensure(ensure_clause, marks)
|
|
1979
3040
|
next_mark, break_mark = marks
|
|
1980
3041
|
carry_through_ensure(ensure_clause, @next_scope_sink, next_mark)
|
|
1981
|
-
|
|
3042
|
+
state = typing_state
|
|
3043
|
+
break_sink = state[BREAK_SINK]
|
|
3044
|
+
state.touch(BREAK_SINK) if break_sink && break_mark && break_mark < break_sink.size
|
|
3045
|
+
carry_through_ensure(ensure_clause, break_sink, break_mark)
|
|
1982
3046
|
end
|
|
1983
3047
|
|
|
1984
3048
|
def carry_through_ensure(ensure_clause, sink, mark)
|
|
@@ -1997,13 +3061,23 @@ module Rigor
|
|
|
1997
3061
|
# `edge`, when given, collects every scope the primary body could raise from: the scope after each statement of
|
|
1998
3062
|
# its frame ({#record_raise_points}), and the scope it ends with. The else-clause is not among them: what it
|
|
1999
3063
|
# raises is not rescued here.
|
|
2000
|
-
|
|
3064
|
+
#
|
|
3065
|
+
# `collector`, when given, is a {BodyRaiseScopes} the same recording fills for {#clause_fact_scope} — built
|
|
3066
|
+
# only when the clause entry still holds an indexed or method-chain fact a raise-point write could
|
|
3067
|
+
# invalidate ({#begin_fact_collector}). A retry `edge` collects the same scopes for its own widening; the
|
|
3068
|
+
# collector is separate so each evaluation pass filters against exactly its own raise points.
|
|
3069
|
+
#
|
|
3070
|
+
# Returns `[type, scope]`: the construct's value and exit scope (the else-clause's when present). The scope the
|
|
3071
|
+
# primary body alone ended with is recorded as a raise point before the `else` runs — the `else` runs only when
|
|
3072
|
+
# nothing raised, so its effects are no raise point's.
|
|
3073
|
+
def eval_begin_primary_under(node, entry_scope, edge: nil, collector: nil)
|
|
2001
3074
|
body_type, body_scope =
|
|
2002
3075
|
if node.statements
|
|
2003
|
-
with_raise_frame(edge) { sub_eval(node.statements, entry_scope) }
|
|
3076
|
+
with_raise_frame(edge, collector) { sub_eval(node.statements, entry_scope) }
|
|
2004
3077
|
else
|
|
2005
3078
|
[Type::Combinator.constant_of(nil), entry_scope]
|
|
2006
3079
|
end
|
|
3080
|
+
collector.raise_scopes << body_scope if collector
|
|
2007
3081
|
edge.raise_scopes << body_scope if edge
|
|
2008
3082
|
|
|
2009
3083
|
if node.else_clause
|
|
@@ -2027,6 +3101,13 @@ module Rigor
|
|
|
2027
3101
|
end
|
|
2028
3102
|
private_constant :RetryEdge
|
|
2029
3103
|
|
|
3104
|
+
# Issue #2691 — the raise-point scopes one evaluation of a `begin` primary body collects for
|
|
3105
|
+
# {#clause_fact_scope}: {RetryEdge}'s `frame` / `raise_scopes` pair without the retry bookkeeping, so
|
|
3106
|
+
# {#record_raise_points} records into it exactly as into an edge. `frame` is the body's own frame — shared
|
|
3107
|
+
# with the retry edge when one exists, since both walks collect the same nodes.
|
|
3108
|
+
BodyRaiseScopes = Data.define(:frame, :raise_scopes)
|
|
3109
|
+
private_constant :BodyRaiseScopes
|
|
3110
|
+
|
|
2030
3111
|
# The entry and edge one widening reads, whether it widens to the Nominal envelope or keeps literals, and, per
|
|
2031
3112
|
# name, the rebinds it has already weighed: a statement-by-statement body shares one binding object across every
|
|
2032
3113
|
# scope until the name is rebound, and weighing each copy again is what made a long body quadratic.
|
|
@@ -2035,17 +3116,29 @@ module Rigor
|
|
|
2035
3116
|
end
|
|
2036
3117
|
private_constant :RetryWidening
|
|
2037
3118
|
|
|
2038
|
-
# The thread-local stack of the
|
|
2039
|
-
|
|
2040
|
-
|
|
3119
|
+
# The thread-local stack of the raise-point frames whose primary body is being evaluated, innermost last, or
|
|
3120
|
+
# nil: a {RetryEdge} and/or a {BodyRaiseScopes}. The
|
|
3121
|
+
# frozen stack is the Frame; a statement's raise point recorded into a frame is a Resource touch
|
|
3122
|
+
# ({#record_raise_points}).
|
|
3123
|
+
RETRY_FRAMES = TypingState.frame(
|
|
3124
|
+
:retry_frames,
|
|
3125
|
+
contents: :resource, reason: "installed per retrying body; each raise point records into it",
|
|
3126
|
+
witness: "spec/fixtures/typing_state_witnesses/retry_frames.rb"
|
|
3127
|
+
)
|
|
3128
|
+
private_constant :RETRY_FRAMES
|
|
2041
3129
|
|
|
2042
3130
|
# The thread-local sinks a widened clause pass ({#without_jump_sinks}) leaves uninstalled.
|
|
2043
|
-
|
|
2044
|
-
private_constant :
|
|
3131
|
+
CLAUSE_SINKS = [RETURN_SINK, NEXT_SINK, BREAK_SINK, BREAK_VALUE_SINK, RETRY_FRAMES].freeze
|
|
3132
|
+
private_constant :CLAUSE_SINKS
|
|
2045
3133
|
|
|
2046
3134
|
# The thread-local sinks a `return`, `next` or `break` is collected into.
|
|
2047
|
-
|
|
2048
|
-
private_constant :
|
|
3135
|
+
JUMP_SINKS = (CLAUSE_SINKS - [RETRY_FRAMES]).freeze
|
|
3136
|
+
private_constant :JUMP_SINKS
|
|
3137
|
+
|
|
3138
|
+
# The sinks a caller's construct installs and a callee's body must not record into: a `next`, `break` or raise
|
|
3139
|
+
# point in the callee belongs to the callee's own constructs, never to the caller's block, loop or retrying body.
|
|
3140
|
+
CALLER_SINKS = [NEXT_SINK, BREAK_SINK, BREAK_VALUE_SINK, RETRY_FRAMES].freeze
|
|
3141
|
+
private_constant :CALLER_SINKS
|
|
2049
3142
|
|
|
2050
3143
|
# The retry edge of `node`, or nil when no `retry` in its rescue chain targets it. Allocation-free for a `begin`
|
|
2051
3144
|
# no `retry` targets.
|
|
@@ -2123,8 +3216,11 @@ module Rigor
|
|
|
2123
3216
|
end
|
|
2124
3217
|
|
|
2125
3218
|
def eval_begin_paths(node, entry, edge)
|
|
2126
|
-
|
|
2127
|
-
|
|
3219
|
+
collector = begin_fact_collector(node, entry, edge)
|
|
3220
|
+
primary_type, primary_scope = eval_begin_primary_under(node, entry, edge: edge, collector: collector)
|
|
3221
|
+
[primary_type, primary_scope,
|
|
3222
|
+
collect_rescue_chain_results(node.rescue_clause, entry,
|
|
3223
|
+
edge: edge, raise_scopes: collector&.raise_scopes)]
|
|
2128
3224
|
end
|
|
2129
3225
|
|
|
2130
3226
|
# B2.1 — the entry scope widened by what crosses the retry edge, or nil when nothing does. A local or ivar bound
|
|
@@ -2149,28 +3245,33 @@ module Rigor
|
|
|
2149
3245
|
widened
|
|
2150
3246
|
end
|
|
2151
3247
|
|
|
2152
|
-
# Runs `block` with `
|
|
2153
|
-
|
|
2154
|
-
|
|
3248
|
+
# Runs `block` with `frames` — a retry edge and/or a {BodyRaiseScopes}, nils skipped — on top of the
|
|
3249
|
+
# thread-local stack {#record_raise_points} reads.
|
|
3250
|
+
def with_raise_frame(first, second, &)
|
|
3251
|
+
return yield if first.nil? && second.nil?
|
|
2155
3252
|
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
ensure
|
|
2161
|
-
Thread.current[RETRY_FRAMES_KEY] = previous
|
|
2162
|
-
end
|
|
3253
|
+
frames = first && second ? [first, second] : [first || second]
|
|
3254
|
+
state = TypingState.current
|
|
3255
|
+
previous = state[RETRY_FRAMES]
|
|
3256
|
+
state.with(RETRY_FRAMES, (previous ? previous + frames : frames).freeze, &)
|
|
2163
3257
|
end
|
|
2164
3258
|
|
|
2165
|
-
# The scope after each statement of a
|
|
2166
|
-
# statement's being the one after
|
|
2167
|
-
#
|
|
2168
|
-
#
|
|
2169
|
-
#
|
|
3259
|
+
# The scope after each statement of a primary body's frame is a point the body can raise from, the next
|
|
3260
|
+
# statement's being the one after — into every frame on the stack: a retrying begin's edge, which carries
|
|
3261
|
+
# every rebind a retry can re-enter with, and a {BodyRaiseScopes}, which {#clause_fact_scope} intersects
|
|
3262
|
+
# over. Both see a rebind on a branch that then raises and so never reaches the body's exit scope
|
|
3263
|
+
# (`if bad; tries += 1; raise; end`), and a write threaded into the raising call's own operands
|
|
3264
|
+
# (`raise Retry.new(tries += 1) if flaky?`). A statement of a nested block or lambda body is not in the
|
|
3265
|
+
# frame (a block parameter can shadow the counter); the block's effect
|
|
2170
3266
|
# on this frame shows in the post-scope of the statement holding it. Recording from the evaluator rather than
|
|
2171
3267
|
# `on_enter` keeps a statement reached with the index recorder off (a threaded operand, a loop fixpoint pass).
|
|
2172
3268
|
def record_raise_points(frames, stmt, stmt_scope)
|
|
2173
|
-
frames.each
|
|
3269
|
+
frames.each do |collector|
|
|
3270
|
+
next unless collector.frame.include?(stmt)
|
|
3271
|
+
|
|
3272
|
+
typing_state.touch(RETRY_FRAMES)
|
|
3273
|
+
collector.raise_scopes << stmt_scope
|
|
3274
|
+
end
|
|
2174
3275
|
end
|
|
2175
3276
|
|
|
2176
3277
|
# An `on_enter` that records, besides forwarding to the installed one, the entry scope of each `retry` of `edge`
|
|
@@ -2327,7 +3428,14 @@ module Rigor
|
|
|
2327
3428
|
# Issue #1231 — `writes` widens the scope each clause enters with ({#widen_clause_entry}) before its
|
|
2328
3429
|
# reference is bound, so `rescue => e` still binds `e` to the exception. It is nil under a retry edge, which
|
|
2329
3430
|
# already carries every scope the primary body raises from ({#widen_entry_for_retry}).
|
|
2330
|
-
|
|
3431
|
+
# `raise_scopes`, when given, is every scope the primary body can raise from — the post-scope of each
|
|
3432
|
+
# statement of its frame and the scope it ends with ({#eval_begin_primary_under}'s collector): the rescue
|
|
3433
|
+
# entry keeps only the indexed and method-chain facts every one of them still binds verbatim
|
|
3434
|
+
# ({#clause_fact_scope}).
|
|
3435
|
+
def collect_rescue_chain_results(rescue_node, entry_scope, edge: nil, writes: nil, raise_scopes: nil)
|
|
3436
|
+
return [] if rescue_node.nil?
|
|
3437
|
+
|
|
3438
|
+
entry_scope = clause_fact_scope(entry_scope, raise_scopes) if raise_scopes
|
|
2331
3439
|
on_enter = edge ? retry_scope_recorder(edge) : @on_enter
|
|
2332
3440
|
results = []
|
|
2333
3441
|
current = rescue_node
|
|
@@ -2375,7 +3483,7 @@ module Rigor
|
|
|
2375
3483
|
# True when the clause's scope `after` still binds `$_` to the `Dynamic[top]` the entry gave it and the clause
|
|
2376
3484
|
# holds nothing that may set it ({LastLine.may_set?}).
|
|
2377
3485
|
def last_line_kept?(after, node)
|
|
2378
|
-
after.global(:$_)
|
|
3486
|
+
Type::Combinator.untyped?(after.global(:$_)) && !LastLine.may_set?(node.statements, scope)
|
|
2379
3487
|
end
|
|
2380
3488
|
|
|
2381
3489
|
# Issue #1889 — {#eval_for} with a literal-name `instance_variable_set` in the body joining its value into the
|
|
@@ -2649,15 +3757,9 @@ module Rigor
|
|
|
2649
3757
|
# Installs a fresh thread-local break sink around `yield` (a loop-body evaluation), returning `[collected,
|
|
2650
3758
|
# yield_result]`. Stacks: the previous sink is restored on exit so a nested loop's breaks do not leak to the
|
|
2651
3759
|
# enclosing loop.
|
|
2652
|
-
def collect_break_scopes
|
|
2653
|
-
previous = Thread.current[BREAK_SINK_KEY]
|
|
3760
|
+
def collect_break_scopes(&)
|
|
2654
3761
|
sink = []
|
|
2655
|
-
|
|
2656
|
-
begin
|
|
2657
|
-
result = yield
|
|
2658
|
-
ensure
|
|
2659
|
-
Thread.current[BREAK_SINK_KEY] = previous
|
|
2660
|
-
end
|
|
3762
|
+
result = TypingState.current.with(BREAK_SINK, sink, &)
|
|
2661
3763
|
[sink, result]
|
|
2662
3764
|
end
|
|
2663
3765
|
|
|
@@ -2734,7 +3836,7 @@ module Rigor
|
|
|
2734
3836
|
# from the widened binding reaches it ({#loop_break_arms}); without it the method inferred `true` and a
|
|
2735
3837
|
# caller's test on it folded always-truthy on correct code.
|
|
2736
3838
|
def later_pass_returns?(statements)
|
|
2737
|
-
!
|
|
3839
|
+
!typing_state[RETURN_SINK].nil? && ReturnBarrier.exits_method?(statements)
|
|
2738
3840
|
end
|
|
2739
3841
|
|
|
2740
3842
|
# Joins each `break` arm's body-written local bindings into the loop continuation, so a `break`-path binding the
|
|
@@ -2746,12 +3848,11 @@ module Rigor
|
|
|
2746
3848
|
def join_break_scopes(continuation, breaks, names)
|
|
2747
3849
|
return continuation if breaks.empty? || names.empty?
|
|
2748
3850
|
|
|
2749
|
-
floor = Type::Combinator.untyped
|
|
2750
3851
|
breaks.reduce(continuation) do |cont, break_scope|
|
|
2751
3852
|
names.reduce(cont) do |acc, name|
|
|
2752
3853
|
break_value = CapturedLocals.bound_type(break_scope, name)
|
|
2753
3854
|
current = CapturedLocals.bound_type(acc, name)
|
|
2754
|
-
next acc if break_value.nil? || current
|
|
3855
|
+
next acc if break_value.nil? || Type::Combinator.untyped?(current)
|
|
2755
3856
|
|
|
2756
3857
|
bind_capture(acc, name, current ? Type::Combinator.union(current, break_value) : break_value)
|
|
2757
3858
|
end
|
|
@@ -3068,7 +4169,9 @@ module Rigor
|
|
|
3068
4169
|
types = [coll_type]
|
|
3069
4170
|
name = for_collection_variable(node)
|
|
3070
4171
|
types << CapturedLocals.bound_type(continuation, name) if name
|
|
3071
|
-
deep = DeepErasure.element_mutation?(node.statements, DeepErasure.parameter_names(node.index))
|
|
4172
|
+
deep = DeepErasure.element_mutation?(node.statements, DeepErasure.parameter_names(node.index)) do
|
|
4173
|
+
DeepErasure.holds_set?(coll_type)
|
|
4174
|
+
end
|
|
3072
4175
|
Type::Combinator.union(*types.compact.map { |type| shape_erased_collection(type, deep: deep) })
|
|
3073
4176
|
end
|
|
3074
4177
|
|
|
@@ -3133,14 +4236,14 @@ module Rigor
|
|
|
3133
4236
|
forget_guard_if_rebinds(body_entry, node.statements)
|
|
3134
4237
|
end
|
|
3135
4238
|
|
|
3136
|
-
# `for x in coll` is semantically `coll.each { |x| ... }`.
|
|
3137
|
-
#
|
|
3138
|
-
#
|
|
4239
|
+
# `for x in coll` is semantically `coll.each { |x| ... }`. The structural `collection_element_type` answers first;
|
|
4240
|
+
# only when it is untyped do we ask the method dispatcher for `coll.each`'s expected block parameter types, which
|
|
4241
|
+
# consult RBS and the iterator dispatch table (they know, e.g., that `Hash[K, V]#each` yields `[K, V]` even when
|
|
3139
4242
|
# the receiver is not a literal Hash carrier in our local lattice). When the dispatcher returns nothing (no
|
|
3140
|
-
# signature, unknown receiver)
|
|
4243
|
+
# signature, unknown receiver) the structural answer stands.
|
|
3141
4244
|
def for_iteration_element_type(coll_type)
|
|
3142
4245
|
structural = collection_element_type(coll_type)
|
|
3143
|
-
return structural unless
|
|
4246
|
+
return structural unless Type::Combinator.untyped?(structural)
|
|
3144
4247
|
|
|
3145
4248
|
block_params = MethodDispatcher.expected_block_param_types(
|
|
3146
4249
|
receiver_type: coll_type,
|
|
@@ -3174,13 +4277,17 @@ module Rigor
|
|
|
3174
4277
|
scope.with_ivar(index_node.name, element_type).without_ivar_guard(index_node.name)
|
|
3175
4278
|
when Prism::GlobalVariableTargetNode
|
|
3176
4279
|
scope.with_global(index_node.name, element_type)
|
|
4280
|
+
when Prism::ClassVariableTargetNode
|
|
4281
|
+
scope.with_cvar(index_node.name, element_type)
|
|
3177
4282
|
when Prism::IndexTargetNode
|
|
3178
4283
|
widen_index_target(index_node, element_type, scope, type_scope: scope)
|
|
4284
|
+
when Prism::CallTargetNode
|
|
4285
|
+
widen_attribute_write(index_node.receiver, index_node.name, scope)
|
|
3179
4286
|
when Prism::MultiTargetNode
|
|
3180
4287
|
bound = MultiTargetBinder.bind_marked(index_node, element_type, scope: scope)
|
|
3181
|
-
widen_index_targets(bound, bound.apply_to(scope), type_scope: scope)
|
|
4288
|
+
widen_attribute_targets(index_node, widen_index_targets(bound, bound.apply_to(scope), type_scope: scope))
|
|
3182
4289
|
when Prism::SplatNode
|
|
3183
|
-
bind_for_splat_index(index_node, scope)
|
|
4290
|
+
bind_for_splat_index(index_node, element_type, scope)
|
|
3184
4291
|
else
|
|
3185
4292
|
scope
|
|
3186
4293
|
end
|
|
@@ -3188,18 +4295,34 @@ module Rigor
|
|
|
3188
4295
|
|
|
3189
4296
|
# `for *h[:a] in pairs` — Prism gives a bare splat index as a `SplatNode`, not a `MultiTargetNode`, so the
|
|
3190
4297
|
# binder never sees it. The store is `*h[:a] = element`, an array of the element's `to_ary` parts; the receiver
|
|
3191
|
-
# widens with the binder's floor for a rest it cannot decompose, `Dynamic[top]`.
|
|
3192
|
-
#
|
|
3193
|
-
|
|
4298
|
+
# widens with the binder's floor for a rest it cannot decompose, `Dynamic[top]`. `for *x.r in pairs` calls `r=`
|
|
4299
|
+
# and widens as `for x.r in` does (issue #2693).
|
|
4300
|
+
#
|
|
4301
|
+
# Issue #1217 — a bare `*a` / `*@a` / `*@@a` / `*$a` binds the name to the rest
|
|
4302
|
+
# {MultiTargetBinder.decompose_slots} reads off the element: a `Tuple` element exactly, an `Array[T]` element as
|
|
4303
|
+
# `Array[T]`, and anything else — a union, a value Ruby would wrap as `[element]` — the `Dynamic[top]` floor.
|
|
4304
|
+
# Left unbound, a read after the loop folded on the pre-loop binding.
|
|
4305
|
+
def bind_for_splat_index(splat, element_type, scope)
|
|
3194
4306
|
target = splat.expression
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
4307
|
+
case target
|
|
4308
|
+
when Prism::CallTargetNode
|
|
4309
|
+
widen_attribute_write(target.receiver, target.name, scope)
|
|
4310
|
+
when Prism::IndexTargetNode
|
|
4311
|
+
widen_index_target(target, Type::Combinator.untyped, scope, type_scope: scope)
|
|
4312
|
+
when Prism::LocalVariableTargetNode, Prism::InstanceVariableTargetNode,
|
|
4313
|
+
Prism::ClassVariableTargetNode, Prism::GlobalVariableTargetNode
|
|
4314
|
+
_, rest, = MultiTargetBinder.decompose_slots(
|
|
4315
|
+
element_type, front_count: 0, back_count: 0, rest_present: true, scope: scope
|
|
4316
|
+
)
|
|
4317
|
+
bind_for_index(target, rest, scope)
|
|
4318
|
+
else
|
|
4319
|
+
scope
|
|
4320
|
+
end
|
|
3198
4321
|
end
|
|
3199
4322
|
|
|
3200
4323
|
# Extracts the per-iteration element type from a collection carrier. `Tuple[T1..Tn]` yields the union of its
|
|
3201
4324
|
# elements; `Nominal[Array, [T]]` and `Nominal[Range, [T]]` yield `T`; `Nominal[Hash, [K, V]]` yields `Tuple[K,
|
|
3202
|
-
# V]` (Hash#each yields `[key, value]` pairs); `IntegerRange` yields `Integer`; `
|
|
4325
|
+
# V]` (Hash#each yields `[key, value]` pairs); `IntegerRange` yields `Integer`; a `Range` constant reads the
|
|
3203
4326
|
# literal range's element class. Anything else falls back to `untyped`.
|
|
3204
4327
|
def collection_element_type(type)
|
|
3205
4328
|
case type
|
|
@@ -3500,8 +4623,9 @@ module Rigor
|
|
|
3500
4623
|
|
|
3501
4624
|
# `class << expr; body; end`. When `expr` is `self`, the body defines class methods on the immediate enclosing
|
|
3502
4625
|
# class — the innermost frame flips to `singleton: true` so a nested `def foo` resolves through `singleton_method`
|
|
3503
|
-
# rather than `instance_method`. For
|
|
3504
|
-
# the existing context and accept that nested defs degrade to the `Dynamic[Top]`
|
|
4626
|
+
# rather than `instance_method`. For an expression that is neither `self` nor a constant we cannot statically
|
|
4627
|
+
# resolve the receiver, so we keep the existing context and accept that nested defs degrade to the `Dynamic[Top]`
|
|
4628
|
+
# default.
|
|
3505
4629
|
# Issue #652 — the body INHERITS this evaluator's chain. Ruby pushes the singleton class onto
|
|
3506
4630
|
# `Module.nesting` and leaves the enclosing entries beneath it, so `class << Other` written inside
|
|
3507
4631
|
# `module Admin` reads `Admin::Y`: the enclosing rung is live, and only the singleton rung itself
|
|
@@ -3530,13 +4654,9 @@ module Rigor
|
|
|
3530
4654
|
# false positives in `prism-1.9.0`'s auto-generated `copy` methods alone. A nested `def` is a return barrier:
|
|
3531
4655
|
# its body's `return`s belong to the inner method, not the one currently being inferred. Suspend the return sink
|
|
3532
4656
|
# across the nested body so `eval_return` does not record them into the outer method's return type.
|
|
3533
|
-
|
|
3534
|
-
Thread.current[RETURN_SINK_KEY] = nil
|
|
3535
|
-
begin
|
|
4657
|
+
typing_state.with(RETURN_SINK, nil) do
|
|
3536
4658
|
sub_eval(node.parameters, body_scope, class_context: @class_context) if node.parameters
|
|
3537
4659
|
sub_eval(node.body, body_scope, class_context: @class_context) if node.body
|
|
3538
|
-
ensure
|
|
3539
|
-
Thread.current[RETURN_SINK_KEY] = outer_sink
|
|
3540
4660
|
end
|
|
3541
4661
|
[Type::Combinator.constant_of(node.name), scope]
|
|
3542
4662
|
end
|
|
@@ -3567,11 +4687,9 @@ module Rigor
|
|
|
3567
4687
|
invoked = call_operand_scope(node, walk, scope)
|
|
3568
4688
|
invoked = forget_operand_specials(node, invoked)
|
|
3569
4689
|
operand_types = walk.types(tracer)
|
|
4690
|
+
# The `each_with_object` memo widening (ADR-56 slice C, B3) is part of the call's type, applied by
|
|
4691
|
+
# `ExpressionTyper#call_type_for` ({#each_with_object_value}).
|
|
3570
4692
|
call_type = OperandWalk.type_of(scope, node, tracer, operand_types)
|
|
3571
|
-
# ADR-56 slice C (B3) — `each_with_object(memo) { |x, acc| acc << … }` returns the memo; the engine otherwise
|
|
3572
|
-
# types the call `Dynamic[top]`. Compute the joined memo type from the block's content mutations of the memo
|
|
3573
|
-
# block-param and adopt it as the call's return type.
|
|
3574
|
-
call_type = each_with_object_return(node, call_type)
|
|
3575
4693
|
[call_type, invoke_from(node, invoked, call_type, operand_types)]
|
|
3576
4694
|
end
|
|
3577
4695
|
|
|
@@ -3760,8 +4878,7 @@ module Rigor
|
|
|
3760
4878
|
# B2.2 — intervening method call ivar invalidation. An implicit-self / self-receiver call could mutate any ivar
|
|
3761
4879
|
# of the enclosing class (we cannot prove purity without an effect system). Reset each ivar whose current local
|
|
3762
4880
|
# binding has narrowed below the class-ivar seed back to the seed itself, so a subsequent `if @flag` predicate
|
|
3763
|
-
# observes the seed's union (not the pre-call narrowed value). Always-safe (only widens; no new facts).
|
|
3764
|
-
# [`docs/CURRENT_WORK.md`](../../../docs/CURRENT_WORK.md) § "Flow-folding" — G2 intervening-call case.
|
|
4881
|
+
# observes the seed's union (not the pre-call narrowed value). Always-safe (only widens; no new facts).
|
|
3765
4882
|
post_scope = invalidate_ivars_for_intervening_call(node, post_scope) if statement_call
|
|
3766
4883
|
# Issue #1889 — `instance_variable_set` on `self` writes an ivar no write node shows. It runs after the reset
|
|
3767
4884
|
# above, which would join a literal-name write back into the class seed.
|
|
@@ -3795,9 +4912,32 @@ module Rigor
|
|
|
3795
4912
|
post_scope.forget_guard_narrowings
|
|
3796
4913
|
end
|
|
3797
4914
|
end
|
|
4915
|
+
post_scope = forget_reached_globals(node, post_scope)
|
|
3798
4916
|
forget_rescued_status(LastStatus.after(node, post_scope, scope), node)
|
|
3799
4917
|
end
|
|
3800
4918
|
|
|
4919
|
+
def guard_narrowed_global?(scope, name)
|
|
4920
|
+
scope.guard_narrowed? && scope.guard_records.key?([:global, name])
|
|
4921
|
+
end
|
|
4922
|
+
|
|
4923
|
+
# Issue #2314 — `post_scope` with each global a project method may write rebound to `Dynamic[top]` when the
|
|
4924
|
+
# statement call `node` may reach that method ({GlobalWriterReach.may_write?}): `$g = nil; reset!; $g + 1`,
|
|
4925
|
+
# with `reset!` assigning `$g`, is `2` in Ruby. A global no reachable method writes keeps its binding. A
|
|
4926
|
+
# `case` whose `when` test may rebind a global subject reads it the same way ({#when_test_rebinds_subject?}).
|
|
4927
|
+
# A global a guard narrowed where the operands started keeps the union issue #1429's restore reads instead
|
|
4928
|
+
# (`return unless $sep; puts(reset_sep); $sep.length` reads `$sep` as the union of its seed and `","`).
|
|
4929
|
+
def forget_reached_globals(node, post_scope)
|
|
4930
|
+
names = GlobalWriterReach.candidates(post_scope)
|
|
4931
|
+
return post_scope if names.empty?
|
|
4932
|
+
|
|
4933
|
+
names.reduce(post_scope) do |acc, name|
|
|
4934
|
+
next acc if guard_narrowed_global?(operand_scope, name)
|
|
4935
|
+
next acc unless GlobalWriterReach.may_write?(node, name, operand_scope)
|
|
4936
|
+
|
|
4937
|
+
acc.with_global(name, Type::Combinator.untyped)
|
|
4938
|
+
end
|
|
4939
|
+
end
|
|
4940
|
+
|
|
3801
4941
|
# True when the call may rebind this frame's match globals: it is match-capable itself ({#match_capable_call?}),
|
|
3802
4942
|
# its own block may run a match ({MatchRebinding.block_may_match?} — issue #1358: the block runs in this frame,
|
|
3803
4943
|
# so `items.each { |i| i =~ re }` rebinds the enclosing method's `$~`, while a match inside a called Ruby method
|
|
@@ -3917,7 +5057,8 @@ module Rigor
|
|
|
3917
5057
|
|
|
3918
5058
|
# Returns a scope with each ivar's narrowed local binding widened back to its class-ivar seed value when the call
|
|
3919
5059
|
# is one that could plausibly mutate ivars on the enclosing class (implicit-self or explicit `self.foo`).
|
|
3920
|
-
# External-
|
|
5060
|
+
# External-receiver calls (`obj.method`) cannot reach the caller's ivars; they pass through unchanged, apart from
|
|
5061
|
+
# the literal-block case below.
|
|
3921
5062
|
# Issue #1596 — where the call is proven to write only some ivars ({#self_call_ivar_writes}), only those widen.
|
|
3922
5063
|
#
|
|
3923
5064
|
# Issue #1402 — a proc's call (`blk.call`) is a candidate too, and so is a call on another receiver whose literal
|
|
@@ -3925,11 +5066,19 @@ module Rigor
|
|
|
3925
5066
|
# calls do, marking a binding whose narrowing had ruled `nil` out, since the code they run is far more often
|
|
3926
5067
|
# code that leaves the variable alone. A `yield` or proc call in the call's receiver or arguments
|
|
3927
5068
|
# (`@items << yield`, `puts(yield)`) widens every narrowed ivar ({InterveningIvarReset.after_operands}).
|
|
5069
|
+
#
|
|
5070
|
+
# Issue #2039 — an implicit-self or `self.` call marks on the same rule only when its write set is unproven
|
|
5071
|
+
# and its callee's body shows no possibly-nil write of the ivar (`mark: :unproven`, {DirectIvarWrites}):
|
|
5072
|
+
# `v2?`, an `attr_reader` call or `respond_to?(:x)` forgets the narrowing without making the ivar newly
|
|
5073
|
+
# nil-reportable, while a callee that assigns it where the program shows (`clear_tag`) widens unmarked.
|
|
3928
5074
|
def invalidate_ivars_for_intervening_call(call_node, current_scope)
|
|
3929
5075
|
widened =
|
|
3930
5076
|
if intervening_call_candidate?(call_node)
|
|
3931
|
-
mark =
|
|
3932
|
-
|
|
5077
|
+
mark = InterveningIvarReset.self_call?(call_node) ? :unproven : true
|
|
5078
|
+
direct_writes = -> { DirectIvarWrites.of(call_node, operand_scope) }
|
|
5079
|
+
InterveningIvarReset.widen(current_scope, mark: mark, direct_writes: direct_writes) do
|
|
5080
|
+
self_call_ivar_writes(call_node)
|
|
5081
|
+
end
|
|
3933
5082
|
else
|
|
3934
5083
|
InterveningIvarReset.after_block(call_node, current_scope)
|
|
3935
5084
|
end
|
|
@@ -4119,7 +5268,7 @@ module Rigor
|
|
|
4119
5268
|
# `resolve_call_method`); plugins recognising their own synthetic call shapes (e.g. `T.assert_type!`) have no
|
|
4120
5269
|
# method_def and the parameter facts silently skip — the plugin's own diagnostics_for_file path covers those
|
|
4121
5270
|
# cases. The full plugin-side parameter-targeting story (PHPStan-style Type-Specifying Extensions on
|
|
4122
|
-
# plugin-recognised calls) lives behind a follow-up slice that introduces
|
|
5271
|
+
# plugin-recognised calls) lives behind a follow-up slice that introduces an `:argument_at` target kind.
|
|
4123
5272
|
def apply_plugin_assertions(call_node, current_scope)
|
|
4124
5273
|
registry = current_scope.environment&.plugin_registry
|
|
4125
5274
|
return current_scope if registry.nil? || registry.empty?
|
|
@@ -4274,7 +5423,7 @@ module Rigor
|
|
|
4274
5423
|
def lookup_post_return_arg(call_node, method_def, target_name)
|
|
4275
5424
|
# Plugin-source contributions arrive without an authoritative method_def (the plugin recognised the call shape
|
|
4276
5425
|
# directly). Parameter-targeting falls back to "no narrow" in that case — the wider plugin-side parameter
|
|
4277
|
-
# mapping (`:
|
|
5426
|
+
# mapping (`:argument_at`) is a follow-up slice.
|
|
4278
5427
|
return nil if method_def.nil?
|
|
4279
5428
|
|
|
4280
5429
|
arguments = call_node.arguments&.arguments || []
|
|
@@ -4301,8 +5450,15 @@ module Rigor
|
|
|
4301
5450
|
narrowed = Narrowing.safe_navigation_block_scope(node, scope)
|
|
4302
5451
|
return enter_call_block(node, block) if narrowed.equal?(scope)
|
|
4303
5452
|
|
|
4304
|
-
evaluator_at(narrowed, operand_scope: operand_scope, operand_types: @operand_types)
|
|
4305
|
-
|
|
5453
|
+
narrowed_evaluator = evaluator_at(narrowed, operand_scope: operand_scope, operand_types: @operand_types)
|
|
5454
|
+
narrowed_evaluator.send(:enter_call_block_carrying, node, block, @value_position_block)
|
|
5455
|
+
end
|
|
5456
|
+
|
|
5457
|
+
# The safe-navigation arm of {#evaluate_block_if_present}: the narrowed evaluator enters the block carrying the
|
|
5458
|
+
# value-position mark {#evaluate_block_of} set on this one, which {#block_self_entry} reads.
|
|
5459
|
+
def enter_call_block_carrying(node, block, value_position_block)
|
|
5460
|
+
@value_position_block = value_position_block
|
|
5461
|
+
enter_call_block(node, block)
|
|
4306
5462
|
end
|
|
4307
5463
|
|
|
4308
5464
|
def enter_call_block(node, block)
|
|
@@ -4360,14 +5516,8 @@ module Rigor
|
|
|
4360
5516
|
end
|
|
4361
5517
|
|
|
4362
5518
|
# Runs the block with the method's return sink suspended, for a body whose `return` is not the method's.
|
|
4363
|
-
def without_return_sink
|
|
4364
|
-
|
|
4365
|
-
Thread.current[RETURN_SINK_KEY] = nil
|
|
4366
|
-
begin
|
|
4367
|
-
yield
|
|
4368
|
-
ensure
|
|
4369
|
-
Thread.current[RETURN_SINK_KEY] = outer_sink
|
|
4370
|
-
end
|
|
5519
|
+
def without_return_sink(&)
|
|
5520
|
+
TypingState.current.with(RETURN_SINK, nil, &)
|
|
4371
5521
|
end
|
|
4372
5522
|
|
|
4373
5523
|
# Issue #963 — `define_method(:name) { ... }` in a class body defines an INSTANCE method, and Ruby runs
|
|
@@ -4471,20 +5621,470 @@ module Rigor
|
|
|
4471
5621
|
acc = floor_callee_escaped_args_for_call(receiver, acc)
|
|
4472
5622
|
receiver = receiver.receiver
|
|
4473
5623
|
end
|
|
4474
|
-
|
|
5624
|
+
# Issue #2397 — and every self-call in the call's own non-escaping block.
|
|
5625
|
+
floor_block_callee_captures(node, acc)
|
|
4475
5626
|
end
|
|
4476
5627
|
|
|
4477
5628
|
# Runs for every call of every body it walks, and nearly every call reports no argument, so that case returns
|
|
4478
5629
|
# before `reduce`: `Enumerable#inject` allocates its iteration state even over an empty array.
|
|
4479
|
-
|
|
4480
|
-
|
|
5630
|
+
#
|
|
5631
|
+
# Issue #2296 — two more channels follow the bare-carrier floor, each keeping the argument's element type
|
|
5632
|
+
# ({UnknownStoreWidening.callee_block_floor} / {UnknownStoreWidening.callee_set_floor}): a parameter the callee
|
|
5633
|
+
# mutates inside a block (`2.times { |i| acc[i] = v }`), and a Set argument the callee grows through a Set-only
|
|
5634
|
+
# adder (`s.add(x)`, `s.merge(xs)`), which the bare census leaves out because `Hash#merge` does not mutate.
|
|
5635
|
+
#
|
|
5636
|
+
# Issue #2297 — a call may resolve to more than one def ({#resolve_self_callees}); each channel floors through
|
|
5637
|
+
# every one of them before the next channel runs, so a bare-census mutation in any of them still goes first.
|
|
5638
|
+
#
|
|
5639
|
+
# `climbing`, when given, keeps only the argument reads deeper than it: those that climb out of a block that
|
|
5640
|
+
# many levels in ({#floor_block_callee_captures}).
|
|
5641
|
+
def floor_callee_escaped_args_for_call(node, base_scope, climbing: nil)
|
|
5642
|
+
callees = argument_mutating_callees(node)
|
|
5643
|
+
return base_scope if callees.empty?
|
|
5644
|
+
|
|
5645
|
+
acc = floor_callee_channel(node, callees, :callee_content_mutated_parameters, base_scope, climbing) do |type|
|
|
5646
|
+
content_floor_for(type)
|
|
5647
|
+
end
|
|
5648
|
+
acc = floor_splat_arguments(node, callees, acc)
|
|
5649
|
+
# Each closure below is only worth computing when an argument holds something its floor could widen.
|
|
5650
|
+
if floorable_local_argument?(node, acc) { |type| UnknownStoreWidening.callee_floorable?(type) }
|
|
5651
|
+
acc = floor_callee_channel(node, callees, :callee_block_mutated_parameters, acc, climbing) do |type|
|
|
5652
|
+
UnknownStoreWidening.callee_block_floor(type)
|
|
5653
|
+
end
|
|
5654
|
+
end
|
|
5655
|
+
return acc unless floorable_local_argument?(node, acc) { |type| SetMutation.carrier_in?(type) }
|
|
5656
|
+
|
|
5657
|
+
callees.each do |def_node, offset, owner|
|
|
5658
|
+
acc = floor_arguments(node, census_of(owner, :callee_set_mutated_parameters, def_node), acc, offset,
|
|
5659
|
+
climbing) do |type|
|
|
5660
|
+
UnknownStoreWidening.callee_set_floor(type)
|
|
5661
|
+
end
|
|
5662
|
+
end
|
|
5663
|
+
acc
|
|
5664
|
+
end
|
|
5665
|
+
|
|
5666
|
+
# Every callee's `census` channel floors the arguments it names with the block's floor, made gradual when the
|
|
5667
|
+
# floor is an over-approximation ({#callee_gradual_floor}). Only a String result is split; the Set channel and
|
|
5668
|
+
# the container elements a floor widens (#2647) are not.
|
|
5669
|
+
def floor_callee_channel(node, callees, census, base_scope, climbing)
|
|
5670
|
+
several = callees.size > 1
|
|
5671
|
+
acc = base_scope
|
|
5672
|
+
callees.each do |def_node, offset, owner|
|
|
5673
|
+
acc = floor_arguments(node, census_of(owner, census, def_node), acc, offset, climbing) do |type|
|
|
5674
|
+
callee_gradual_floor(yield(type), def_node, owner, several)
|
|
5675
|
+
end
|
|
5676
|
+
end
|
|
5677
|
+
acc
|
|
5678
|
+
end
|
|
5679
|
+
|
|
5680
|
+
# Issue #2397 — the continuation of a `:non_escaping` block whose body hands a captured outer local to a
|
|
5681
|
+
# self-call whose callee mutates it: `acc = [7, 9]; [1].each { poke(acc) }`. The body's own statement floor
|
|
5682
|
+
# rebinds `acc` only inside the block, and neither write-back reads it there (the rebind write-back follows
|
|
5683
|
+
# local writes, the content join follows mutator calls), so the continuation kept the literal and
|
|
5684
|
+
# `acc[0].upcase` reported on correct code.
|
|
5685
|
+
#
|
|
5686
|
+
# Every call the statement `node` makes before it returns is visited: `node` itself and the calls in its
|
|
5687
|
+
# receiver chain and arguments (`[1].each { poke(acc) }.size`, `foo([1].each { poke(acc) })`), never inside a
|
|
5688
|
+
# block or lambda of them. For each one with a `:non_escaping` block not run on another object
|
|
5689
|
+
# ({#foreign_self_block?}), each self-call in the block floors as at a statement, by position or keyword, its
|
|
5690
|
+
# arguments limited to reads that climb out of the block: a block parameter or a nested block's parameter
|
|
5691
|
+
# shadowing the outer name is not the outer local. The body walk ({#each_node_in_method_scope}) leaves out a
|
|
5692
|
+
# lambda and the block of a nested `instance_eval` / `define_method` / …, whose self-calls are not ours.
|
|
5693
|
+
#
|
|
5694
|
+
# Two readings are conservative, since the floor only forgets: a block that may run zero times floors, as the
|
|
5695
|
+
# escaping path ({#floor_block_body_callee_escaped_args}) does, and the floor applies after the block's
|
|
5696
|
+
# write-back, so a rebind later in the body (`poke(acc); acc = [1, 2]`) is floored too.
|
|
5697
|
+
def floor_block_callee_captures(node, post_scope)
|
|
5698
|
+
acc = post_scope
|
|
5699
|
+
each_node_outside_nested_scopes(node) do |call|
|
|
5700
|
+
next unless call.is_a?(Prism::CallNode) && call.block.is_a?(Prism::BlockNode) && call.block.body
|
|
5701
|
+
|
|
5702
|
+
acc = floor_block_body_callee_captures(call, acc)
|
|
5703
|
+
end
|
|
5704
|
+
acc
|
|
5705
|
+
end
|
|
5706
|
+
|
|
5707
|
+
def floor_block_body_callee_captures(call_node, post_scope)
|
|
5708
|
+
return post_scope unless classify_closure_escape(call_node) == :non_escaping
|
|
5709
|
+
return post_scope if foreign_self_block?(call_node)
|
|
5710
|
+
|
|
5711
|
+
acc = post_scope
|
|
5712
|
+
each_node_in_caller_block(call_node.block.body, 0) do |node, depth|
|
|
5713
|
+
acc = floor_callee_escaped_args_for_call(node, acc, climbing: depth)
|
|
5714
|
+
end
|
|
5715
|
+
acc
|
|
5716
|
+
end
|
|
5717
|
+
|
|
5718
|
+
# Yields every call under `node` with the number of blocks and lambdas it sits in, for
|
|
5719
|
+
# {#floor_block_callee_captures}. A lambda is entered, as the escaping path floors one: it may be called in the
|
|
5720
|
+
# block (`l.call`, `-> { ... }.call`). A nested def is not. Nor is the block of a nested call whose self-calls
|
|
5721
|
+
# are provably not the caller's: one run on another object ({#foreign_block_call?}), a `define_method` /
|
|
5722
|
+
# `define_singleton_method` method body, or a `Class.new` / `Module.new` / `Struct.new` / `Data.define` block.
|
|
5723
|
+
# An `instance_eval` / `class_exec` / … on any other receiver (none, `self`, an instance) is entered.
|
|
5724
|
+
def each_node_in_caller_block(node, depth, &)
|
|
5725
|
+
return if node.nil?
|
|
5726
|
+
|
|
5727
|
+
yield node, depth if node.is_a?(Prism::CallNode)
|
|
5728
|
+
skip_block = node.is_a?(Prism::CallNode) && foreign_block_call?(node)
|
|
5729
|
+
node.rigor_each_child do |child|
|
|
5730
|
+
next if child.is_a?(Prism::DefNode)
|
|
5731
|
+
|
|
5732
|
+
if child.is_a?(Prism::BlockNode) || child.is_a?(Prism::LambdaNode)
|
|
5733
|
+
each_node_in_caller_block(child, depth + 1, &) unless skip_block && child.is_a?(Prism::BlockNode)
|
|
5734
|
+
else
|
|
5735
|
+
each_node_in_caller_block(child, depth, &)
|
|
5736
|
+
end
|
|
5737
|
+
end
|
|
5738
|
+
end
|
|
5739
|
+
|
|
5740
|
+
def foreign_block_call?(call_node)
|
|
5741
|
+
return false unless self_rebinding_block_call?(call_node)
|
|
5742
|
+
return true if %i[define_method define_singleton_method].include?(call_node.name)
|
|
5743
|
+
return true unless AnonymousMetaClass.block_form_receiver(call_node).nil?
|
|
5744
|
+
|
|
5745
|
+
foreign_self_block?(call_node)
|
|
5746
|
+
end
|
|
5747
|
+
|
|
5748
|
+
# Issue #2515 — the floor for a `*x` argument whose `x` is a local holding an array (`args = [parser, options];
|
|
5749
|
+
# Opts.add_format(*args)`). The hash travels inside the array, so the caller's `options` is the array's element,
|
|
5750
|
+
# not an argument, and the per-parameter floor never sees it. An element the callee writes is floored at its place
|
|
5751
|
+
# in the array's type and in every local bound to an equal type (the element's source: only the type is known, so
|
|
5752
|
+
# an equal literal in another local is floored too).
|
|
5753
|
+
#
|
|
5754
|
+
# A known tuple whose splat sits at a fixed position (only plain positional arguments before it) binds element
|
|
5755
|
+
# `j` to the callee's slot `index - offset + j` and floors it when that slot is in the channel's census. Any other
|
|
5756
|
+
# case floors every element when the callee writes any positional parameter: an array that is no longer a tuple
|
|
5757
|
+
# (it was mutated), a splat after another splat, an `offset` that swallows the splat. An element that reaches the
|
|
5758
|
+
# callee's rest or post parameter floors when the callee may write it ({#rest_parameter_written?}). A splat of
|
|
5759
|
+
# anything but a local (a call result, an ivar) is not followed.
|
|
5760
|
+
def floor_splat_arguments(node, callees, base_scope)
|
|
5761
|
+
arguments = node.arguments.arguments
|
|
5762
|
+
return base_scope unless arguments.any?(Prism::SplatNode)
|
|
5763
|
+
|
|
5764
|
+
fixed = true
|
|
5765
|
+
arguments.each_with_index.reduce(base_scope) do |acc, (argument, index)|
|
|
5766
|
+
next acc unless argument.is_a?(Prism::SplatNode)
|
|
5767
|
+
|
|
5768
|
+
read = argument.expression
|
|
5769
|
+
at = fixed ? index : nil
|
|
5770
|
+
fixed = false
|
|
5771
|
+
next acc unless read.is_a?(Prism::LocalVariableReadNode) && acc.locals.key?(read.name)
|
|
5772
|
+
|
|
5773
|
+
floor_splat_local(read.name, at, callees, acc)
|
|
5774
|
+
end
|
|
5775
|
+
end
|
|
5776
|
+
|
|
5777
|
+
# Every callee's channels in turn. The callees are a call's own ({#argument_mutating_callees}), never the nil
|
|
5778
|
+
# sentinel {CAPPED_HAND_OFF}, which only a hand-off inside a def answers ({#rest_hand_off_writes?} reads it).
|
|
5779
|
+
def floor_splat_local(name, at, callees, base_scope)
|
|
5780
|
+
channels = splat_channels
|
|
5781
|
+
origin = base_scope.local(name)
|
|
5782
|
+
callees.reduce(base_scope) do |acc, (def_node, offset, owner)|
|
|
5783
|
+
rest = rest_parameter_written?(def_node, owner)
|
|
5784
|
+
channels.reduce(acc) do |inner, (census, floor)|
|
|
5785
|
+
mutated = census_of(owner, census, def_node)
|
|
5786
|
+
writer = ->(count, position) { splat_written_elements(count, position, offset, def_node, mutated, rest) }
|
|
5787
|
+
floor_splat_arrays(name, at, writer, floor, inner, origin)
|
|
5788
|
+
end
|
|
5789
|
+
end
|
|
5790
|
+
end
|
|
5791
|
+
|
|
5792
|
+
# `base_scope` with the elements of the array local `name` the callee writes floored. The local may be a union
|
|
5793
|
+
# (`cond ? [p, o] : [p, q]`, or an array rebound in a block) and may include nil (`args = [...] if cond`): each
|
|
5794
|
+
# array member floors on its own, and a union of arrays, whose position is unknown, takes the any-slot rule. A
|
|
5795
|
+
# local bound to an equal array type (`args2 = args`) is rebuilt too.
|
|
5796
|
+
def floor_splat_arrays(name, at, writer, floor, base_scope, origin)
|
|
5797
|
+
current = base_scope.local(name)
|
|
5798
|
+
members = current.is_a?(Type::Union) ? current.members : [current]
|
|
5799
|
+
arrays = members.count { |member| splat_elements(member) }
|
|
5800
|
+
return base_scope if arrays.zero?
|
|
5801
|
+
|
|
5802
|
+
origins = splat_origin_members(origin, members)
|
|
5803
|
+
acc = base_scope
|
|
5804
|
+
floored = members.each_with_index.map do |member, index|
|
|
5805
|
+
elements = splat_elements(member)
|
|
5806
|
+
next member if elements.nil?
|
|
5807
|
+
|
|
5808
|
+
position = member.is_a?(Type::Tuple) && arrays == 1 ? at : nil
|
|
5809
|
+
written = writer.call(elements.size, position)
|
|
5810
|
+
next member if written.empty?
|
|
5811
|
+
|
|
5812
|
+
source = splat_elements(origins[index]) || elements
|
|
5813
|
+
rebuilt, acc = floor_array_elements(member, elements, [written, source], floor, acc, name)
|
|
5814
|
+
rebuilt
|
|
5815
|
+
end
|
|
5816
|
+
return acc if floored == members
|
|
5817
|
+
|
|
5818
|
+
rebind_array_aliases(name, current, Type::Combinator.union(*floored), acc)
|
|
5819
|
+
end
|
|
5820
|
+
|
|
5821
|
+
# The members of the array local as it was before the channels ran, aligned with `members`, or `members` itself
|
|
5822
|
+
# when a channel merged or dropped some.
|
|
5823
|
+
def splat_origin_members(origin, members)
|
|
5824
|
+
origin_members = splat_parts(origin)
|
|
5825
|
+
origin_members.size == members.size ? origin_members : members
|
|
5826
|
+
end
|
|
5827
|
+
|
|
5828
|
+
# `scope_in` with `name`, and every local bound to the same array type `before`, rebound to `after`.
|
|
5829
|
+
def rebind_array_aliases(name, before, after, scope_in)
|
|
5830
|
+
aliased = scope_in.locals.reduce(scope_in) do |acc, (other, bound)|
|
|
5831
|
+
other != name && bound == before ? acc.with_mutated_local(other, after) : acc
|
|
5832
|
+
end
|
|
5833
|
+
aliased.with_mutated_local(name, after)
|
|
5834
|
+
end
|
|
5835
|
+
|
|
5836
|
+
def splat_channels
|
|
5837
|
+
[[:callee_content_mutated_parameters, method(:content_floor_for)],
|
|
5838
|
+
[:callee_block_mutated_parameters, UnknownStoreWidening.method(:callee_block_floor)],
|
|
5839
|
+
[:callee_set_mutated_parameters, UnknownStoreWidening.method(:callee_set_floor)]]
|
|
5840
|
+
end
|
|
5841
|
+
|
|
5842
|
+
# The element types of `type` when it is an array a splat spreads, or nil: a tuple's, or a nominal `Array[T]`'s.
|
|
5843
|
+
def splat_elements(type)
|
|
5844
|
+
if type.is_a?(Type::Tuple) then type.elements
|
|
5845
|
+
elsif type.is_a?(Type::Nominal) && type.class_name == "Array" && type.type_args.size == 1 then type.type_args
|
|
5846
|
+
end
|
|
5847
|
+
end
|
|
5848
|
+
|
|
5849
|
+
# The indexes of the `count` array elements the callee writes, for a splat at the call's argument `at` (nil: a
|
|
5850
|
+
# position the call does not show, or an array of unknown length). `rest` is true when the callee may write its
|
|
5851
|
+
# rest parameter ({#rest_parameter_written?}).
|
|
5852
|
+
def splat_written_elements(count, at, offset, def_node, mutated, rest)
|
|
5853
|
+
slots = mutated.values
|
|
5854
|
+
return NO_ELEMENTS unless rest || slots.any? { |slot| slot.is_a?(Integer) || slot == ANY_SLOT }
|
|
5855
|
+
return (0...count).to_a if at.nil? || at < offset || slots.include?(ANY_SLOT)
|
|
5856
|
+
|
|
5857
|
+
named = content_parameter_slots(def_node).each_value.grep(Integer).size
|
|
5858
|
+
(0...count).select do |j|
|
|
5859
|
+
slot = at - offset + j
|
|
5860
|
+
slot >= named ? rest : slots.include?(slot)
|
|
5861
|
+
end
|
|
5862
|
+
end
|
|
5863
|
+
|
|
5864
|
+
NO_ELEMENTS = [].freeze
|
|
5865
|
+
private_constant :NO_ELEMENTS
|
|
5866
|
+
|
|
5867
|
+
# `[array, scope]`: the array type `member` with its `written` elements floored by `floor`, and `scope_in` with
|
|
5868
|
+
# every caller local (other than `name`) the floored elements could be floored too. A union element floors member
|
|
5869
|
+
# by member; every element of the array holding a floored type is rebuilt (`[options, options]`, one slot
|
|
5870
|
+
# written); a local is floored (gradually, {#floor_locals_accepted_by}) when a floored part accepts its type.
|
|
5871
|
+
# Locals are matched against the `source` elements, the array as it was before an earlier channel floored it,
|
|
5872
|
+
# so a floored `Hash[untyped, untyped]` element does not match every hash.
|
|
5873
|
+
def floor_array_elements(member, elements, written_and_source, floor, scope_in, name)
|
|
5874
|
+
written, source = written_and_source
|
|
5875
|
+
acc = scope_in
|
|
5876
|
+
pairs = []
|
|
5877
|
+
written.each do |j|
|
|
5878
|
+
splat_parts(source[j]).each { |part| acc = floor_locals_accepted_by(part, name, floor, acc) }
|
|
5879
|
+
splat_parts(elements[j]).each do |part|
|
|
5880
|
+
replacement = floor.call(part)
|
|
5881
|
+
pairs << [part, replacement] unless replacement.nil? || replacement == part
|
|
5882
|
+
end
|
|
5883
|
+
end
|
|
5884
|
+
return [member, acc] if pairs.empty?
|
|
5885
|
+
|
|
5886
|
+
floored = elements.map { |element| replace_splat_parts(element, pairs) }
|
|
5887
|
+
rebuilt = if member.is_a?(Type::Tuple)
|
|
5888
|
+
Type::Combinator.tuple_of(*floored)
|
|
5889
|
+
else
|
|
5890
|
+
Type::Combinator.nominal_of("Array", type_args: floored)
|
|
5891
|
+
end
|
|
5892
|
+
[rebuilt, acc]
|
|
5893
|
+
end
|
|
5894
|
+
|
|
5895
|
+
def splat_parts(type) = type.is_a?(Type::Union) ? type.members : [type]
|
|
5896
|
+
|
|
5897
|
+
# `element` with the floored parts replaced, gradually ({#gradual_floor}): an element is floored by
|
|
5898
|
+
# over-approximation as often as by a precise slot (a repeated type, an unknown position, a rest read as written),
|
|
5899
|
+
# and one the callee never writes must not start failing against a literal parameter type.
|
|
5900
|
+
def replace_splat_parts(element, pairs)
|
|
5901
|
+
parts = splat_parts(element)
|
|
5902
|
+
replaced = parts.map do |part|
|
|
5903
|
+
replacement = pairs.find { |from, _| from == part }&.last
|
|
5904
|
+
replacement ? gradual_floor(replacement) : part
|
|
5905
|
+
end
|
|
5906
|
+
replaced == parts ? element : Type::Combinator.union(*replaced)
|
|
5907
|
+
end
|
|
5908
|
+
|
|
5909
|
+
# `scope_in` with each carrier local other than `except` whose type `part` accepts (it is a subtype of, or equal
|
|
5910
|
+
# to, the element: `args << options` widens the pushed `{ format: "text" }` to `Hash[Symbol, String]`) floored
|
|
5911
|
+
# with `floor`. Such a local is only a guess at the element's source (the type is all the array records), so its
|
|
5912
|
+
# floor is gradual ({#gradual_floor}): it may forget a fact, never add a finding. A `Dynamic` part matches no
|
|
5913
|
+
# local: it stands for any value and would floor every carrier in scope.
|
|
5914
|
+
def floor_locals_accepted_by(part, except, floor, scope_in)
|
|
5915
|
+
return scope_in if part.is_a?(Type::Dynamic)
|
|
5916
|
+
|
|
5917
|
+
scope_in.locals.reduce(scope_in) do |acc, (other, bound)|
|
|
5918
|
+
next acc if other == except || !UnknownStoreWidening.callee_floorable?(bound)
|
|
5919
|
+
next acc unless Acceptance.accepts(part, bound).yes?
|
|
5920
|
+
|
|
5921
|
+
floored = floor.call(bound)
|
|
5922
|
+
floored.nil? || floored == bound ? acc : acc.with_mutated_local(other, gradual_floor(floored))
|
|
5923
|
+
end
|
|
5924
|
+
end
|
|
5925
|
+
|
|
5926
|
+
# `type` with a bare `String` carrier made gradual (`Dynamic[String]`): a floor to the nominal `String` fails
|
|
5927
|
+
# against a literal parameter type (`("text") -> String`) that the unmutated local satisfied.
|
|
5928
|
+
def gradual_floor(type)
|
|
5929
|
+
return Type::Combinator.union(*type.members.map { |member| gradual_floor(member) }) if type.is_a?(Type::Union)
|
|
5930
|
+
|
|
5931
|
+
string_carrier?(type) ? Type::Combinator.dynamic(type) : type
|
|
5932
|
+
end
|
|
5933
|
+
|
|
5934
|
+
# `floored`, a callee floor (content or block channel) of an argument of a call to `def_node` under `owner`,
|
|
5935
|
+
# made gradual when the floor is an over-approximation ({#over_approximating_callee?}) or the call has several
|
|
5936
|
+
# callees. The one place both channels take the rule.
|
|
5937
|
+
def callee_gradual_floor(floored, def_node, owner, several)
|
|
5938
|
+
# Only a String carrier turns gradual, so the callee is walked only for one (this runs per floored argument).
|
|
5939
|
+
return floored unless string_carrier_in?(floored)
|
|
5940
|
+
|
|
5941
|
+
several || over_approximating_callee?(def_node, owner) ? gradual_floor(floored) : floored
|
|
5942
|
+
end
|
|
5943
|
+
|
|
5944
|
+
def string_carrier_in?(type)
|
|
5945
|
+
type.is_a?(Type::Union) ? type.members.any? { |member| string_carrier?(member) } : string_carrier?(type)
|
|
5946
|
+
end
|
|
5947
|
+
|
|
5948
|
+
# Issue #2515 — true when the floor a call applies to `def_node`'s arguments is an over-approximation, with no
|
|
5949
|
+
# proof that the callee writes the argument it floors: the owner is {FLOOR_ALL} (an inherited def, a
|
|
5950
|
+
# `method_missing`) or {FLOOR_EVERY} (a top-level def that may not be the one called), or the callee hands a
|
|
5951
|
+
# parameter on, through any hop, to one of those, by name or to several candidates. A def read as written that
|
|
5952
|
+
# writes the argument keeps the
|
|
5953
|
+
# precise floor, which is what a later `Integer` parameter or `no_such_method` on the String needs to report.
|
|
5954
|
+
# The walk is bounded by {CalleeMutationChain::MAX_NODES}; past it the answer is true.
|
|
5955
|
+
def over_approximating_callee?(def_node, owner)
|
|
5956
|
+
return true if OVER_APPROXIMATING.include?(owner)
|
|
5957
|
+
|
|
5958
|
+
seen = {}.compare_by_identity
|
|
5959
|
+
queue = [[def_node, owner]]
|
|
5960
|
+
until queue.empty?
|
|
5961
|
+
node, node_owner = queue.shift
|
|
5962
|
+
next if (seen[node] ||= {}).key?(node_owner)
|
|
5963
|
+
|
|
5964
|
+
seen[node][node_owner] = true
|
|
5965
|
+
return true if seen.sum { |_, owners| owners.size } > CalleeMutationChain::MAX_NODES
|
|
5966
|
+
|
|
5967
|
+
census_of(node_owner, :forwarding_edges, node).each do |edge|
|
|
5968
|
+
return true if edge.approx || OVER_APPROXIMATING.include?(edge.owner)
|
|
5969
|
+
|
|
5970
|
+
queue << [edge.callee, edge.owner || node_owner]
|
|
5971
|
+
end
|
|
5972
|
+
end
|
|
5973
|
+
false
|
|
5974
|
+
end
|
|
5975
|
+
|
|
5976
|
+
def string_carrier?(type) = type.is_a?(Type::Nominal) && type.class_name == "String"
|
|
5977
|
+
|
|
5978
|
+
# True when `def_node` has a rest, post or `...` parameter, so a slot no longer names the parameter an element
|
|
5979
|
+
# binds.
|
|
5980
|
+
def variable_arity?(def_node)
|
|
5981
|
+
params = def_node.parameters
|
|
5982
|
+
return false if params.nil?
|
|
5983
|
+
|
|
5984
|
+
!params.rest.nil? || !params.posts.empty? || params.keyword_rest.is_a?(Prism::ForwardingParameterNode)
|
|
5985
|
+
end
|
|
5986
|
+
|
|
5987
|
+
# Issue #2515 — true when a callee's rest parameter (`*args`, `*`, `...`) may be written, so an element a splat
|
|
5988
|
+
# binds to it floors: the body reads it anywhere but as a splat argument of a call (`args.first[:k] = 1`), or
|
|
5989
|
+
# hands it on (`b(*args)`) to a call that resolves to a def which writes a positional parameter or has a rest
|
|
5990
|
+
# parameter of its own that it hands on. A rest handed to a call that resolves to no user def hands off nothing,
|
|
5991
|
+
# as for any hand-off. A callee reached through an inherited lookup ({FLOOR_ALL}) or one that may not be the def
|
|
5992
|
+
# the call runs ({FLOOR_EVERY}) reads as written, as its parameters do: neither owner is a {CalleeOwner}, so its
|
|
5993
|
+
# body's hand-offs cannot be read under it.
|
|
5994
|
+
def rest_parameter_written?(def_node, owner)
|
|
5995
|
+
return true if def_node.nil?
|
|
5996
|
+
return false unless variable_arity?(def_node)
|
|
5997
|
+
return true if [FLOOR_ALL, FLOOR_EVERY].include?(owner)
|
|
5998
|
+
|
|
5999
|
+
other, calls = def_census(:rest_uses, def_node) { rest_uses(def_node) }
|
|
6000
|
+
return true if other
|
|
6001
|
+
|
|
6002
|
+
evaluator = owner.nil? ? self : owner_evaluator(owner)
|
|
6003
|
+
calls.any? { |call_node, depth| evaluator.__send__(:rest_hand_off_writes?, call_node, def_node, depth) }
|
|
6004
|
+
end
|
|
6005
|
+
|
|
6006
|
+
# True when a call that splats a rest parameter hands it to a def that writes a positional parameter or has a
|
|
6007
|
+
# rest or post parameter of its own, read in this evaluator, the one the call's def runs in. A nil callee is
|
|
6008
|
+
# {CAPPED_HAND_OFF}'s sentinel (a name with too many defs) and reads as written.
|
|
6009
|
+
def rest_hand_off_writes?(call_node, def_node, depth)
|
|
6010
|
+
hand_off_callees(call_node, def_node, depth).any? do |callee, _offset, callee_owner|
|
|
6011
|
+
next true if callee.nil? || variable_arity?(callee)
|
|
6012
|
+
|
|
6013
|
+
census = census_of(callee_owner, :callee_content_mutated_parameters, callee)
|
|
6014
|
+
census.value?(ANY_SLOT) || census.each_value.any?(Integer)
|
|
6015
|
+
end
|
|
6016
|
+
end
|
|
6017
|
+
|
|
6018
|
+
# `[other, calls]` for `def_node`'s rest parameter: `other` is true when it is read except as a splat argument
|
|
6019
|
+
# (`*args`, `*`) of a call, `calls` the calls that splat it ({#rest_parameter_written?}). An AST property.
|
|
6020
|
+
def rest_uses(def_node)
|
|
6021
|
+
params = def_node.parameters
|
|
6022
|
+
name = params.rest.is_a?(Prism::RestParameterNode) ? params.rest.name : nil
|
|
6023
|
+
forwarding = params.keyword_rest.is_a?(Prism::ForwardingParameterNode)
|
|
6024
|
+
reads = 0
|
|
6025
|
+
splatted = 0
|
|
6026
|
+
calls = []
|
|
6027
|
+
each_node_in_method_scope(def_node.body, 0) do |node, depth|
|
|
6028
|
+
reads += 1 if name && splat_reads?(node, name, depth)
|
|
6029
|
+
next unless node.is_a?(Prism::CallNode) && node.arguments
|
|
6030
|
+
|
|
6031
|
+
spread = node.arguments.arguments.select { |a| rest_splat?(a, name, forwarding, depth) }
|
|
6032
|
+
next if spread.empty?
|
|
6033
|
+
|
|
6034
|
+
splatted += spread.count { |a| a.is_a?(Prism::SplatNode) && a.expression }
|
|
6035
|
+
calls << [node, depth]
|
|
6036
|
+
end
|
|
6037
|
+
[reads > splatted, calls.freeze].freeze
|
|
6038
|
+
end
|
|
6039
|
+
|
|
6040
|
+
def rest_splat?(argument, name, forwarding, depth)
|
|
6041
|
+
case argument
|
|
6042
|
+
when Prism::ForwardingArgumentsNode then forwarding
|
|
6043
|
+
when Prism::SplatNode
|
|
6044
|
+
argument.expression.nil? ? name.nil? : !name.nil? && splat_reads?(argument.expression, name, depth)
|
|
6045
|
+
else false
|
|
6046
|
+
end
|
|
6047
|
+
end
|
|
6048
|
+
|
|
6049
|
+
def splat_reads?(node, name, depth)
|
|
6050
|
+
node.is_a?(Prism::LocalVariableReadNode) && node.name == name && node.depth == depth
|
|
6051
|
+
end
|
|
6052
|
+
|
|
6053
|
+
# True when a local `node` passes (positionally or as a keyword's value) is bound in `scope` to a type the
|
|
6054
|
+
# block accepts.
|
|
6055
|
+
def floorable_local_argument?(node, scope, &)
|
|
6056
|
+
node.arguments.arguments.any? do |argument|
|
|
6057
|
+
if argument.is_a?(Prism::KeywordHashNode)
|
|
6058
|
+
argument.elements.any? do |pair|
|
|
6059
|
+
pair.is_a?(Prism::AssocNode) && floorable_local?(pair.value, scope, &)
|
|
6060
|
+
end
|
|
6061
|
+
else
|
|
6062
|
+
floorable_local?(argument, scope, &)
|
|
6063
|
+
end
|
|
6064
|
+
end
|
|
6065
|
+
end
|
|
6066
|
+
|
|
6067
|
+
def floorable_local?(node, scope)
|
|
6068
|
+
node = node.value if node.is_a?(Prism::ImplicitNode)
|
|
6069
|
+
node.is_a?(Prism::LocalVariableReadNode) && scope.locals.key?(node.name) && yield(scope.local(node.name))
|
|
6070
|
+
end
|
|
6071
|
+
|
|
6072
|
+
# `base_scope` with each local `node` passes to a parameter of `mutated` rebound to what the block answers for
|
|
6073
|
+
# its binding; a nil answer, or an answer equal to the binding, leaves it. Issue #2335 — an argument that may be
|
|
6074
|
+
# another local's object (a block parameter, a copy) floors every local another holder may reach
|
|
6075
|
+
# ({AliasMutationFloor.floor_after_written_local}), a block-local one included.
|
|
6076
|
+
def floor_arguments(node, mutated, base_scope, offset, climbing = nil)
|
|
6077
|
+
arguments = arguments_for_slots(node, mutated, offset)
|
|
4481
6078
|
return base_scope if arguments.empty?
|
|
4482
6079
|
|
|
4483
6080
|
arguments.reduce(base_scope) do |acc, argument|
|
|
6081
|
+
acc = AliasMutationFloor.floor_after_written_local(acc, argument, node)
|
|
4484
6082
|
next acc unless acc.locals.key?(argument.name)
|
|
6083
|
+
next acc if climbing && argument.depth <= climbing
|
|
4485
6084
|
|
|
4486
|
-
|
|
4487
|
-
floored
|
|
6085
|
+
current = acc.local(argument.name)
|
|
6086
|
+
floored = yield(current)
|
|
6087
|
+
floored.nil? || floored == current ? acc : acc.with_mutated_local(argument.name, floored)
|
|
4488
6088
|
end
|
|
4489
6089
|
end
|
|
4490
6090
|
|
|
@@ -4496,33 +6096,112 @@ module Rigor
|
|
|
4496
6096
|
def callee_content_mutated_parameters(def_node)
|
|
4497
6097
|
cache = (@callee_mutated_param_cache ||= {}.compare_by_identity)
|
|
4498
6098
|
cache[def_node] ||= resolved_census(:close, def_node) do
|
|
4499
|
-
CalleeMutationChain.close(def_node, own:
|
|
6099
|
+
CalleeMutationChain.close(def_node, own: hop_census(:own_content_parameters),
|
|
6100
|
+
edges: hop_census(:forwarding_edges), **callee_chain_store)
|
|
6101
|
+
end
|
|
6102
|
+
end
|
|
6103
|
+
|
|
6104
|
+
# Issue #2515 — `census` as the `(def, owner)` callback {CalleeMutationChain} reads a hop through: `owner` is the
|
|
6105
|
+
# evaluator a cross-class hand-off resolves in, nil for the evaluator the closure started in.
|
|
6106
|
+
def hop_census(census) = ->(def_node, owner) { census_of(owner, census, def_node) }
|
|
6107
|
+
|
|
6108
|
+
# ADR-127 WD2 — the run's successor store for {CalleeMutationChain} keyed by discovery: `def => owner => name =>
|
|
6109
|
+
# [callee, name, owner, ...]`, a graph fact (the nodes a parameter is handed to), never a walk's conclusion. The
|
|
6110
|
+
# edges do not depend on which census a closure reads, so one store serves all three. The nil owner is stored
|
|
6111
|
+
# under the side and class the closure runs in, the same context {#resolved_census} keys by. Bypassed (nothing
|
|
6112
|
+
# read or written) under {#hand_off_memo_bypassed?}, as the memos it sits beside.
|
|
6113
|
+
def callee_chain_store
|
|
6114
|
+
return NO_CHAIN_STORE if hand_off_memo_bypassed?
|
|
6115
|
+
|
|
6116
|
+
table = typing_state.weak_key_map(CHAIN_STORE)
|
|
6117
|
+
self_type = scope.self_type
|
|
6118
|
+
{ store: (table[scope.discovery] ||= {}.compare_by_identity),
|
|
6119
|
+
ctx: [self_type.is_a?(Type::Singleton), enclosing_class_name_for(self_type)] }
|
|
6120
|
+
end
|
|
6121
|
+
|
|
6122
|
+
CHAIN_STORE = TypingState.memo(
|
|
6123
|
+
:callee_chain_successors,
|
|
6124
|
+
run_scoped: true, audit: "successor",
|
|
6125
|
+
reason: "graph facts keyed by discovery, the same whichever census reads them"
|
|
6126
|
+
)
|
|
6127
|
+
NO_CHAIN_STORE = {}.freeze
|
|
6128
|
+
private_constant :CHAIN_STORE, :NO_CHAIN_STORE
|
|
6129
|
+
|
|
6130
|
+
# Issue #2296 — the `{ name => slot }` parameters the callee mutates inside a block in its body
|
|
6131
|
+
# ({#own_block_content_parameters}), closed over hand-offs as {#callee_content_mutated_parameters} is.
|
|
6132
|
+
def callee_block_mutated_parameters(def_node)
|
|
6133
|
+
cache = (@callee_block_param_cache ||= {}.compare_by_identity)
|
|
6134
|
+
cache[def_node] ||= resolved_census(:close_block, def_node) do
|
|
6135
|
+
CalleeMutationChain.close(def_node, own: hop_census(:own_block_content_parameters),
|
|
6136
|
+
edges: hop_census(:forwarding_edges), **callee_chain_store)
|
|
4500
6137
|
end
|
|
4501
6138
|
end
|
|
4502
6139
|
|
|
4503
|
-
# Issue #
|
|
6140
|
+
# Issue #2296 — the `{ name => slot }` parameters the callee grows through a Set-only adder
|
|
6141
|
+
# ({#own_set_content_parameters}), closed over hand-offs the same way.
|
|
6142
|
+
def callee_set_mutated_parameters(def_node)
|
|
6143
|
+
cache = (@callee_set_param_cache ||= {}.compare_by_identity)
|
|
6144
|
+
cache[def_node] ||= resolved_census(:close_set, def_node) do
|
|
6145
|
+
CalleeMutationChain.close(def_node, own: hop_census(:own_set_content_parameters),
|
|
6146
|
+
edges: hop_census(:forwarding_edges), **callee_chain_store)
|
|
6147
|
+
end
|
|
6148
|
+
end
|
|
6149
|
+
|
|
6150
|
+
# Issue #2104 — the per-thread memo of a census that also resolves self-calls ({#resolve_self_callees}), so it
|
|
4504
6151
|
# reads, besides `def_node`'s AST, the class self-calls resolve in and the discovery index whose def table they
|
|
4505
|
-
# resolve through. Those two key it: the index by identity, as it is immutable (one
|
|
4506
|
-
#
|
|
6152
|
+
# resolve through. Those two key it: the index by identity, as it is immutable (one entry per live index, held
|
|
6153
|
+
# weakly: a table dropped with its index), the class by name. A run builds hundreds of
|
|
4507
6154
|
# thousands of evaluators, and a per-evaluator memo alone recomputed each closure and each def's edges about
|
|
4508
|
-
# thirty times over (#2295's cost on the release gate).
|
|
6155
|
+
# thirty times over (#2295's cost on the release gate). Issue #2297 — the side self-calls resolve on
|
|
6156
|
+
# ({#resolve_self_callees}) keys it too: a singleton-side table apart from the instance-side one.
|
|
4509
6157
|
#
|
|
4510
6158
|
# While a dependency recorder is active the memo is bypassed: a resolution records an ADR-46 read in the file
|
|
4511
|
-
# and in each capture window open at the time, and a hit would leave that read out of them.
|
|
4512
|
-
|
|
4513
|
-
|
|
6159
|
+
# and in each capture window open at the time, and a hit would leave that read out of them. It is bypassed, read
|
|
6160
|
+
# and write, while this scope carries guard narrowings of constants ({#hand_off_memo_bypassed?}, #2524): they
|
|
6161
|
+
# survive `with_self_type` and change what a constant in the def's body types as, so the census is no longer
|
|
6162
|
+
# the def's alone, and the first caller's narrowing must not become every later caller's answer.
|
|
6163
|
+
def resolved_census(kind, def_node, &)
|
|
6164
|
+
# An audit recomputation bypasses the memo but still opens the site's declaration (ADR-129 WD1).
|
|
6165
|
+
return MemoAudit.key_audit("resolved census", RESOLVED_CENSUS_KEY_AUDIT, &) if MemoAudit.suspended?
|
|
6166
|
+
return yield if hand_off_memo_bypassed?
|
|
4514
6167
|
|
|
4515
6168
|
discovery = scope.discovery
|
|
4516
|
-
|
|
4517
|
-
slot =
|
|
4518
|
-
|
|
4519
|
-
|
|
6169
|
+
table = typing_state.weak_key_map(RESOLVED_CENSUS)
|
|
6170
|
+
slot = (table[discovery] ||= [{}, {}])
|
|
6171
|
+
self_type = scope.self_type
|
|
6172
|
+
by_class = (slot[self_type.is_a?(Type::Singleton) ? 1 : 0][kind] ||= {})
|
|
6173
|
+
class_name = enclosing_class_name_for(self_type)
|
|
4520
6174
|
by_def = (by_class[class_name] ||= {}.compare_by_identity)
|
|
4521
|
-
by_def.
|
|
6175
|
+
unless by_def.key?(def_node)
|
|
6176
|
+
return by_def[def_node] = MemoAudit.key_audit("resolved census", RESOLVED_CENSUS_KEY_AUDIT, &)
|
|
6177
|
+
end
|
|
6178
|
+
|
|
6179
|
+
MemoAudit.check("resolved census", by_def[def_node], kind, class_name) do
|
|
6180
|
+
MemoAudit.key_audit("resolved census", RESOLVED_CENSUS_KEY_AUDIT, &)
|
|
6181
|
+
end
|
|
4522
6182
|
end
|
|
4523
6183
|
|
|
4524
|
-
|
|
4525
|
-
|
|
6184
|
+
RESOLVED_CENSUS = TypingState.memo(
|
|
6185
|
+
:callee_resolved_census,
|
|
6186
|
+
run_scoped: true, audit: "resolved census",
|
|
6187
|
+
reason: "keyed by discovery, side, kind, class and def, all its answer reads; audited by MemoAudit"
|
|
6188
|
+
)
|
|
6189
|
+
private_constant :RESOLVED_CENSUS
|
|
6190
|
+
|
|
6191
|
+
# ADR-129 WD1's key audit of a resolved-census miss. A census types a callee's receivers ({#receiver_callees_in})
|
|
6192
|
+
# under the frames its caller installed, and keys on none of them: a known gap, #2867.
|
|
6193
|
+
CENSUS_CALLER_FRAMES = %i[
|
|
6194
|
+
threaded_block_body key_presence_walk return_sink next_sink break_sink break_value_sink retry_frames
|
|
6195
|
+
interface_erasure interface_dispatch_off
|
|
6196
|
+
].freeze
|
|
6197
|
+
private_constant :CENSUS_CALLER_FRAMES
|
|
6198
|
+
RESOLVED_CENSUS_KEY_AUDIT = MemoAudit.key_declaration(
|
|
6199
|
+
gaps: CENSUS_CALLER_FRAMES.to_h do |name|
|
|
6200
|
+
[name, "#2867: the census types a callee's receivers under the caller's frames, and its key carries " \
|
|
6201
|
+
"none of them"]
|
|
6202
|
+
end
|
|
6203
|
+
).freeze
|
|
6204
|
+
private_constant :RESOLVED_CENSUS_KEY_AUDIT
|
|
4526
6205
|
|
|
4527
6206
|
# The parameters `def_node` mutates itself, from either channel: those escape-mutated inside a block (which may
|
|
4528
6207
|
# run later / repeatedly) AND those mutated directly in the method body during the call itself
|
|
@@ -4530,18 +6209,269 @@ module Rigor
|
|
|
4530
6209
|
# ({#def_census}) rather than per evaluator: a chain reads its callees' census from every evaluator that floors a
|
|
4531
6210
|
# call into it.
|
|
4532
6211
|
def own_content_parameters(def_node)
|
|
4533
|
-
def_census(:own, def_node)
|
|
6212
|
+
def_census(:own, def_node) do
|
|
6213
|
+
own = escaped_content_parameters(def_node).merge(direct_content_parameters(def_node))
|
|
6214
|
+
written = callee_write_census(def_node).written
|
|
6215
|
+
own = own.merge(written_slots(def_node, written)) unless written.empty?
|
|
6216
|
+
handed = super_forwarded_parameters(def_node)
|
|
6217
|
+
handed.empty? ? own : own.merge(handed)
|
|
6218
|
+
end
|
|
6219
|
+
end
|
|
6220
|
+
|
|
6221
|
+
# Issue #2595 — the parameter writes the content scans above miss: a shape mutator they do not count
|
|
6222
|
+
# (`h.merge!(x)`), a write through an alias (`other = h`, `h.tap { |x| }`, `[h].each { |x| }`, `@x = h`), and
|
|
6223
|
+
# the aliases themselves, which {#forwarded_parameters} follows as hand-offs ({CalleeWriteCensus}). A property
|
|
6224
|
+
# of the def's AST alone, memoised per def node for the thread.
|
|
6225
|
+
def callee_write_census(def_node)
|
|
6226
|
+
def_census(:writes, def_node) do
|
|
6227
|
+
CalleeWriteCensus.scan(def_node.body, census_seeds(def_node),
|
|
6228
|
+
tables: CENSUS_TABLES, rebinds: parameter_rebinds(def_node),
|
|
6229
|
+
returns: returns_counted?(def_node), anonymous: anonymous_rests?(def_node))
|
|
6230
|
+
end
|
|
6231
|
+
end
|
|
6232
|
+
|
|
6233
|
+
# Issue #2336 — the calls whose block is a method body of the class (`define_method(:go) { poke(h) }`): the
|
|
6234
|
+
# hand-off walk leaves it out ({#each_node_in_method_scope}), so the write census reads a call in it that passes
|
|
6235
|
+
# a parameter as written ({CalleeWriteCensus}). A block run on another `self` (`instance_exec`, `Class.new`)
|
|
6236
|
+
# stays out: a self-call there is not a call on the method's receiver.
|
|
6237
|
+
METHOD_BODY_BLOCK_CALLS = %i[define_method define_singleton_method].to_set.freeze
|
|
6238
|
+
private_constant :METHOD_BODY_BLOCK_CALLS
|
|
6239
|
+
|
|
6240
|
+
# Issue #2336 — whether the value a def returns counts as a write of the parameter it is ({CalleeWriteCensus}):
|
|
6241
|
+
# not for `initialize`, whose value `new` drops, nor for a setter (`x=`, `[]=`), whose value an assignment drops.
|
|
6242
|
+
def returns_counted?(def_node)
|
|
6243
|
+
name = def_node.name
|
|
6244
|
+
name != :initialize && name != :[]= && !SETTER_NAME.match?(name)
|
|
6245
|
+
end
|
|
6246
|
+
|
|
6247
|
+
# An attribute writer's name: an identifier followed by `=` (`format=`), not an operator (`==`, `<=`, `===`).
|
|
6248
|
+
SETTER_NAME = /\A[A-Za-z_][A-Za-z0-9_]*=\z/
|
|
6249
|
+
private_constant :SETTER_NAME
|
|
6250
|
+
|
|
6251
|
+
# Issue #2336 — true when `def_node` has an anonymous rest (`*`), keyword rest (`**`) or `...`, whose arguments
|
|
6252
|
+
# an opaque sink may receive ({CalleeWriteCensus::ANONYMOUS_REST}).
|
|
6253
|
+
def anonymous_rests?(def_node)
|
|
6254
|
+
params = def_node.parameters
|
|
6255
|
+
return false if params.nil?
|
|
6256
|
+
|
|
6257
|
+
(params.rest.is_a?(Prism::RestParameterNode) && params.rest.name.nil?) ||
|
|
6258
|
+
(params.keyword_rest.is_a?(Prism::KeywordRestParameterNode) && params.keyword_rest.name.nil?) ||
|
|
6259
|
+
params.keyword_rest.is_a?(Prism::ForwardingParameterNode)
|
|
4534
6260
|
end
|
|
4535
6261
|
|
|
6262
|
+
# The parameters {#callee_write_census} follows: each content parameter holds the caller's argument itself
|
|
6263
|
+
# (`:same`); a named rest (`*args`) or keyword rest (`**kw`) holds the caller's arguments as elements (`:held`).
|
|
6264
|
+
def census_seeds(def_node)
|
|
6265
|
+
seeds = content_parameter_slots(def_node).transform_values { :same }
|
|
6266
|
+
rest_parameter_names(def_node).each_key { |name| seeds[name] = :held }
|
|
6267
|
+
seeds
|
|
6268
|
+
end
|
|
6269
|
+
|
|
6270
|
+
# `{ name => census key }` for a named rest (`*args` reads as {REST_PARAMETER}) and keyword rest (`**kw` reads as
|
|
6271
|
+
# {KEYWORD_REST_PARAMETER}) of `def_node`.
|
|
6272
|
+
def rest_parameter_names(def_node)
|
|
6273
|
+
params = def_node.parameters
|
|
6274
|
+
return NO_PARAMETER_SLOTS if params.nil?
|
|
6275
|
+
|
|
6276
|
+
rest = params.rest
|
|
6277
|
+
keyword_rest = params.keyword_rest
|
|
6278
|
+
names = {}
|
|
6279
|
+
names[rest.name] = REST_PARAMETER if rest.is_a?(Prism::RestParameterNode) && rest.name
|
|
6280
|
+
names[keyword_rest.name] = KEYWORD_REST_PARAMETER if keyword_rest.is_a?(Prism::KeywordRestParameterNode) &&
|
|
6281
|
+
keyword_rest.name
|
|
6282
|
+
names
|
|
6283
|
+
end
|
|
6284
|
+
|
|
6285
|
+
# The own-census entries for the parameter names {#callee_write_census} wrote: a content parameter at its slot,
|
|
6286
|
+
# a rest at {ANY_SLOT} and a keyword rest at {ANY_KEYWORD}, the slots every argument they bind reaches.
|
|
6287
|
+
#
|
|
6288
|
+
# Issue #2336 — an anonymous rest, keyword rest or `...` an opaque sink received
|
|
6289
|
+
# ({CalleeWriteCensus::ANONYMOUS_REST}) is written at its slot too.
|
|
6290
|
+
def written_slots(def_node, written)
|
|
6291
|
+
found = content_parameter_slots(def_node).slice(*written)
|
|
6292
|
+
rest_parameter_names(def_node).each do |name, key|
|
|
6293
|
+
found[key] = key == REST_PARAMETER ? ANY_SLOT : ANY_KEYWORD if written.include?(name)
|
|
6294
|
+
end
|
|
6295
|
+
rests = rest_parameters(def_node)
|
|
6296
|
+
if written.include?(CalleeWriteCensus::ANONYMOUS_REST) && rests.include?(REST_PARAMETER)
|
|
6297
|
+
found[REST_PARAMETER] = ANY_SLOT
|
|
6298
|
+
end
|
|
6299
|
+
if written.include?(CalleeWriteCensus::ANONYMOUS_KEYWORD_REST) && rests.include?(KEYWORD_REST_PARAMETER)
|
|
6300
|
+
found[KEYWORD_REST_PARAMETER] = ANY_KEYWORD
|
|
6301
|
+
end
|
|
6302
|
+
found
|
|
6303
|
+
end
|
|
6304
|
+
|
|
6305
|
+
# Issue #2521 (#2570 review) — the parameters `def_node` hands to the method it overrides: every content
|
|
6306
|
+
# parameter under a zsuper (`super`, which passes them all), and each one an explicit `super(...)` passes as an
|
|
6307
|
+
# argument or keyword value, at the block depth the call sits at. The overridden def is not followed; the
|
|
6308
|
+
# parameter reads as mutated, so a literal handed through an override that forwards it is floored.
|
|
6309
|
+
#
|
|
6310
|
+
# A rest parameter (`*`, `*args`, `...`) handed on whole (a zsuper, `super(*args)`, `super(...)`) reads as
|
|
6311
|
+
# mutated under {REST_PARAMETER}, the name an argument the call binds to the rest parameter reaches
|
|
6312
|
+
# ({#compute_forwarding_edges}), at {ANY_SLOT} ({#arguments_for_slots}).
|
|
6313
|
+
def super_forwarded_parameters(def_node)
|
|
6314
|
+
# A cheap gate first: the walk below runs for the few defs whose source mentions `super` at all.
|
|
6315
|
+
body = def_node.body
|
|
6316
|
+
return NO_PARAMETER_SLOTS if body.nil? || !body.slice.include?("super")
|
|
6317
|
+
|
|
6318
|
+
slots = content_parameter_slots(def_node)
|
|
6319
|
+
rests = rest_parameters(def_node)
|
|
6320
|
+
return NO_PARAMETER_SLOTS if slots.empty? && rests.empty?
|
|
6321
|
+
|
|
6322
|
+
names = super_forwarded_names(def_node, slots, rests)
|
|
6323
|
+
return NO_PARAMETER_SLOTS if names.nil?
|
|
6324
|
+
|
|
6325
|
+
found = slots.slice(*names)
|
|
6326
|
+
found[REST_PARAMETER] = ANY_SLOT if names.include?(REST_PARAMETER)
|
|
6327
|
+
found[KEYWORD_REST_PARAMETER] = ANY_KEYWORD if names.include?(KEYWORD_REST_PARAMETER)
|
|
6328
|
+
found
|
|
6329
|
+
end
|
|
6330
|
+
|
|
6331
|
+
# The parameter names `def_node` hands to `super` ({#super_forwarded_parameters}), or nil when it calls none.
|
|
6332
|
+
# `rests` holds the rest names the def has ({#rest_parameters}).
|
|
6333
|
+
def super_forwarded_names(def_node, slots, rests)
|
|
6334
|
+
names = nil
|
|
6335
|
+
each_node_in_method_scope(def_node.body, 0) do |node, depth|
|
|
6336
|
+
case node
|
|
6337
|
+
when Prism::ForwardingSuperNode
|
|
6338
|
+
(names ||= Set.new).merge(slots.keys.reject { |name| after_rebind?(def_node, name, node) }).merge(rests)
|
|
6339
|
+
when Prism::SuperNode
|
|
6340
|
+
handed = super_argument_parameters(node, slots, depth).reject { |name| after_rebind?(def_node, name, node) }
|
|
6341
|
+
(names ||= Set.new).merge(handed)
|
|
6342
|
+
names.merge(rests & splatted_rests(node))
|
|
6343
|
+
end
|
|
6344
|
+
end
|
|
6345
|
+
names
|
|
6346
|
+
end
|
|
6347
|
+
|
|
6348
|
+
# The names {#super_forwarded_parameters} records a positional and a keyword rest parameter handed to `super`
|
|
6349
|
+
# under; no local can be named with a leading `*`.
|
|
6350
|
+
REST_PARAMETER = :"*rest"
|
|
6351
|
+
KEYWORD_REST_PARAMETER = :"**rest"
|
|
6352
|
+
REST_NAMES = [REST_PARAMETER].freeze
|
|
6353
|
+
KEYWORD_REST_NAMES = [KEYWORD_REST_PARAMETER].freeze
|
|
6354
|
+
private_constant :REST_PARAMETER, :KEYWORD_REST_PARAMETER, :REST_NAMES, :KEYWORD_REST_NAMES
|
|
6355
|
+
|
|
6356
|
+
# The rest names an argument the call binds to no named parameter of `callee` reaches, when `callee` hands that
|
|
6357
|
+
# rest parameter to `super` ({#super_forwarded_parameters}); empty otherwise. `slot` is the call's positional
|
|
6358
|
+
# index, or the keyword.
|
|
6359
|
+
def handed_rest_names(callee, slot)
|
|
6360
|
+
own = own_content_parameters(callee)
|
|
6361
|
+
if slot.is_a?(Symbol) then own.key?(KEYWORD_REST_PARAMETER) ? KEYWORD_REST_NAMES : NO_FORWARDS
|
|
6362
|
+
else own.key?(REST_PARAMETER) ? REST_NAMES : NO_FORWARDS
|
|
6363
|
+
end
|
|
6364
|
+
end
|
|
6365
|
+
|
|
6366
|
+
# The rest parameters `def_node` has, as {REST_PARAMETER} / {KEYWORD_REST_PARAMETER}: `*args` / `*`, `**kw` /
|
|
6367
|
+
# `**`, both for `...`.
|
|
6368
|
+
def rest_parameters(def_node)
|
|
6369
|
+
params = def_node.parameters
|
|
6370
|
+
return NO_FORWARDS if params.nil?
|
|
6371
|
+
|
|
6372
|
+
forwarding = params.keyword_rest.is_a?(Prism::ForwardingParameterNode)
|
|
6373
|
+
rests = []
|
|
6374
|
+
rests << REST_PARAMETER if forwarding || params.rest.is_a?(Prism::RestParameterNode)
|
|
6375
|
+
rests << KEYWORD_REST_PARAMETER if forwarding || params.keyword_rest.is_a?(Prism::KeywordRestParameterNode)
|
|
6376
|
+
rests
|
|
6377
|
+
end
|
|
6378
|
+
|
|
6379
|
+
# The rests an explicit `super(...)` passes on whole: a splat (`*args`, `*`), a double splat (`**kw`, `**`) in its
|
|
6380
|
+
# keywords, or `...` for both.
|
|
6381
|
+
def splatted_rests(node)
|
|
6382
|
+
(node.arguments&.arguments || []).flat_map do |argument|
|
|
6383
|
+
case argument
|
|
6384
|
+
when Prism::SplatNode then REST_NAMES
|
|
6385
|
+
when Prism::ForwardingArgumentsNode then [REST_PARAMETER, KEYWORD_REST_PARAMETER]
|
|
6386
|
+
when Prism::KeywordHashNode
|
|
6387
|
+
argument.elements.any?(Prism::AssocSplatNode) ? KEYWORD_REST_NAMES : NO_FORWARDS
|
|
6388
|
+
else NO_FORWARDS
|
|
6389
|
+
end
|
|
6390
|
+
end
|
|
6391
|
+
end
|
|
6392
|
+
|
|
6393
|
+
def super_argument_parameters(node, slots, depth)
|
|
6394
|
+
arguments = node.arguments&.arguments || []
|
|
6395
|
+
arguments.flat_map do |argument|
|
|
6396
|
+
if argument.is_a?(Prism::KeywordHashNode)
|
|
6397
|
+
argument.elements.filter_map do |pair|
|
|
6398
|
+
parameter_read_name(pair.value, slots, depth) if pair.is_a?(Prism::AssocNode)
|
|
6399
|
+
end
|
|
6400
|
+
else
|
|
6401
|
+
[parameter_read_name(argument, slots, depth)].compact
|
|
6402
|
+
end
|
|
6403
|
+
end
|
|
6404
|
+
end
|
|
6405
|
+
|
|
6406
|
+
# Issue #2296 — the parameters `def_node` content-mutates inside a block of its body, at any nesting: a
|
|
6407
|
+
# mutation whose receiver reads the parameter itself at the block depth it sits at, so a block parameter or
|
|
6408
|
+
# block-local of the same name, and a block-local copy (`t = [acc[i]]; t[0] = v`), do not count. A lambda or
|
|
6409
|
+
# nested def is not entered (its body is not run by the call), nor is a block that runs on another `self`
|
|
6410
|
+
# ({#each_node_in_method_scope}). The walk counts the method body too: a mutation there, or in an escaping
|
|
6411
|
+
# block, is one the bare census ({#own_content_parameters}) also counts, and its floor runs first, leaving this
|
|
6412
|
+
# one nothing to widen. A property of the AST alone, memoised per def node for the thread.
|
|
6413
|
+
def own_block_content_parameters(def_node) = block_and_set_parameters(def_node)[0]
|
|
6414
|
+
|
|
6415
|
+
# Issue #2296 — the parameters `def_node` grows through a Set-only adder ({SET_ONLY_ADDERS}) in its body or a
|
|
6416
|
+
# block in it, read at the depth it sits at as above. The name alone does not say the receiver is a Set
|
|
6417
|
+
# (`Hash#merge` returns a new Hash), so only a Set argument is floored on it
|
|
6418
|
+
# ({UnknownStoreWidening.callee_set_floor}).
|
|
6419
|
+
def own_set_content_parameters(def_node) = block_and_set_parameters(def_node)[1]
|
|
6420
|
+
|
|
6421
|
+
# Both censuses above, `[block, set]`, from one walk of the method scope, memoised per def node for the thread.
|
|
6422
|
+
def block_and_set_parameters(def_node)
|
|
6423
|
+
def_census(:block_and_set, def_node) do
|
|
6424
|
+
slots = content_parameter_slots(def_node)
|
|
6425
|
+
next NO_BLOCK_AND_SET if slots.empty?
|
|
6426
|
+
|
|
6427
|
+
block = nil
|
|
6428
|
+
set = nil
|
|
6429
|
+
each_node_in_method_scope(def_node.body, 0) do |node, depth|
|
|
6430
|
+
name = content_mutation_target(node) { |r| r.depth == depth }.first
|
|
6431
|
+
(block ||= Set.new) << name if counted_mutation?(def_node, slots, name, node)
|
|
6432
|
+
name = set_only_adder_target(node, depth)
|
|
6433
|
+
(set ||= Set.new) << name if counted_mutation?(def_node, slots, name, node)
|
|
6434
|
+
end
|
|
6435
|
+
[block.nil? ? NO_PARAMETER_SLOTS : slots.slice(*block), set.nil? ? NO_PARAMETER_SLOTS : slots.slice(*set)]
|
|
6436
|
+
end
|
|
6437
|
+
end
|
|
6438
|
+
|
|
6439
|
+
# True when `name`, the local the mutation `node` targets, is one of `slots`' parameters and `node` runs before
|
|
6440
|
+
# any rebind of it ({#after_rebind?}).
|
|
6441
|
+
def counted_mutation?(def_node, slots, name, node)
|
|
6442
|
+
!name.nil? && slots.key?(name) && !after_rebind?(def_node, name, node)
|
|
6443
|
+
end
|
|
6444
|
+
|
|
6445
|
+
# The local `node` grows through a Set-only adder when its receiver reads that local at `depth`, else nil.
|
|
6446
|
+
def set_only_adder_target(node, depth)
|
|
6447
|
+
return unless node.is_a?(Prism::CallNode) && SET_ONLY_ADDERS.include?(node.name)
|
|
6448
|
+
|
|
6449
|
+
receiver = node.receiver
|
|
6450
|
+
receiver.name if receiver.is_a?(Prism::LocalVariableReadNode) && receiver.depth == depth
|
|
6451
|
+
end
|
|
6452
|
+
|
|
6453
|
+
NO_PARAMETER_SLOTS = {}.freeze
|
|
6454
|
+
NO_BLOCK_AND_SET = [NO_PARAMETER_SLOTS, NO_PARAMETER_SLOTS].freeze
|
|
6455
|
+
private_constant :NO_PARAMETER_SLOTS, :NO_BLOCK_AND_SET
|
|
6456
|
+
|
|
4536
6457
|
# Issue #2104 — the per-thread memo of a census that reads nothing but `def_node`'s AST, under `kind`.
|
|
4537
|
-
def def_census(kind, def_node)
|
|
4538
|
-
|
|
6458
|
+
def def_census(kind, def_node, &)
|
|
6459
|
+
# An audit recomputation bypasses the memo, so it reads no stored entry (ADR-127 WD3, #3071).
|
|
6460
|
+
return MemoAudit.key_audit("def census", &) if MemoAudit.suspended?
|
|
6461
|
+
|
|
6462
|
+
table = typing_state.weak_key_map(DEF_CENSUS)
|
|
4539
6463
|
entry = (table[def_node] ||= {})
|
|
4540
|
-
|
|
6464
|
+
return entry[kind] = MemoAudit.key_audit("def census", &) unless entry.key?(kind)
|
|
6465
|
+
|
|
6466
|
+
MemoAudit.check("def census", entry[kind], kind, &)
|
|
4541
6467
|
end
|
|
4542
6468
|
|
|
4543
|
-
|
|
4544
|
-
|
|
6469
|
+
DEF_CENSUS = TypingState.memo(
|
|
6470
|
+
:callee_def_census,
|
|
6471
|
+
run_scoped: true, audit: "def census",
|
|
6472
|
+
reason: "keyed by def node, the only input it reads; audited by MemoAudit"
|
|
6473
|
+
)
|
|
6474
|
+
private_constant :DEF_CENSUS
|
|
4545
6475
|
|
|
4546
6476
|
# Issue #2104 — the {CalleeMutationChain::Edge}s along which `def_node` hands one of its parameters, unchanged, to
|
|
4547
6477
|
# a self-call resolving to a user def: a positional argument or a keyword value that reads the parameter itself
|
|
@@ -4553,19 +6483,39 @@ module Rigor
|
|
|
4553
6483
|
cache[def_node] ||= resolved_census(:edges, def_node) { compute_forwarding_edges(def_node) }
|
|
4554
6484
|
end
|
|
4555
6485
|
|
|
6486
|
+
# True for a hand-off answer with more than one candidate, or resolved by name ({UnionCallees}): its edges are
|
|
6487
|
+
# over-approximations ({CalleeMutationChain::Edge#approx}).
|
|
6488
|
+
def approximate_hand_off?(callees) = callees.size > 1 || callees.is_a?(UnionCallees)
|
|
6489
|
+
|
|
6490
|
+
# True for a hand-off answer that reads the handed parameter as written whatever the callee is: a name with too
|
|
6491
|
+
# many defs ({CAPPED_HAND_OFF}) or a top-level def that may not be the one called ({FLOOR_EVERY}).
|
|
6492
|
+
def floors_outright?(callees)
|
|
6493
|
+
callees.equal?(CAPPED_HAND_OFF) || callees.any? { |_callee, _offset, owner| owner == FLOOR_EVERY }
|
|
6494
|
+
end
|
|
6495
|
+
|
|
4556
6496
|
def compute_forwarding_edges(def_node)
|
|
4557
|
-
slots =
|
|
6497
|
+
slots = forwarding_slots(def_node)
|
|
4558
6498
|
edges = []
|
|
4559
|
-
each_forwarding_call(def_node, slots) do |call_node, forwarded|
|
|
4560
|
-
|
|
4561
|
-
|
|
6499
|
+
each_forwarding_call(def_node, slots) do |call_node, forwarded, depth|
|
|
6500
|
+
callees = hand_off_callees(call_node, def_node, depth)
|
|
6501
|
+
if floors_outright?(callees)
|
|
6502
|
+
forwarded.map(&:first).each do |name|
|
|
6503
|
+
edges << CalleeMutationChain::Edge.new(name: name, slot: slots[name], callee: def_node,
|
|
6504
|
+
callee_names: [name], owner: FLOOR_ALL)
|
|
6505
|
+
end
|
|
6506
|
+
next
|
|
6507
|
+
end
|
|
4562
6508
|
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
6509
|
+
callees.each do |callee, offset, owner|
|
|
6510
|
+
callee_slots = content_parameter_slots(callee)
|
|
6511
|
+
forwarded.each do |name, index|
|
|
6512
|
+
names = bound_parameter_names(callee_slots, index.is_a?(Integer) ? index - offset : index)
|
|
6513
|
+
names = handed_rest_names(callee, index) if names.empty? && index != ANY_SLOT
|
|
6514
|
+
next if names.empty?
|
|
4567
6515
|
|
|
4568
|
-
|
|
6516
|
+
edges << CalleeMutationChain::Edge.new(name: name, slot: slots[name], callee: callee, callee_names: names,
|
|
6517
|
+
owner: owner, approx: approximate_hand_off?(callees))
|
|
6518
|
+
end
|
|
4569
6519
|
end
|
|
4570
6520
|
end
|
|
4571
6521
|
edges.empty? ? NO_FORWARDS : edges
|
|
@@ -4581,11 +6531,13 @@ module Rigor
|
|
|
4581
6531
|
|
|
4582
6532
|
def forwarding_calls(def_node, slots)
|
|
4583
6533
|
calls = []
|
|
6534
|
+
context = HandOffContext.new(census: callee_write_census(def_node), seeds: census_seeds(def_node),
|
|
6535
|
+
rests: rest_parameter_names(def_node), rebinds: parameter_rebinds(def_node))
|
|
4584
6536
|
each_node_in_method_scope(def_node.body, 0) do |descendant, depth|
|
|
4585
|
-
next unless self_dispatch_call?(descendant)
|
|
6537
|
+
next unless self_dispatch_call?(descendant) || hand_off_receiver?(descendant)
|
|
4586
6538
|
|
|
4587
|
-
forwarded = forwarded_parameters(descendant, slots, depth)
|
|
4588
|
-
calls << [descendant, forwarded] unless forwarded.empty?
|
|
6539
|
+
forwarded = forwarded_parameters(descendant, slots, depth, context)
|
|
6540
|
+
calls << [descendant, forwarded, depth] unless forwarded.empty?
|
|
4589
6541
|
end
|
|
4590
6542
|
calls.empty? ? NO_FORWARDS : calls
|
|
4591
6543
|
end
|
|
@@ -4596,8 +6548,11 @@ module Rigor
|
|
|
4596
6548
|
# `[[parameter_name, callee_slot], ...]` for the arguments of `call_node` that read one of `slots`' parameters at
|
|
4597
6549
|
# `depth`, the block nesting the call sits at: a read of the method's own parameter there has that depth. An
|
|
4598
6550
|
# argument after a splat or `...` has an index that says nothing about the parameter it binds, so it binds
|
|
4599
|
-
# {ANY_SLOT}, any positional parameter of the callee; one before the first splat keeps its index
|
|
4600
|
-
|
|
6551
|
+
# {ANY_SLOT}, any positional parameter of the callee; one before the first splat keeps its index among the call's
|
|
6552
|
+
# arguments, which {#compute_forwarding_edges} shifts by each callee's offset ({#resolve_self_callees}).
|
|
6553
|
+
# Issue #2595 — an argument that reads an alias of a parameter (`other = h; poke(other)`, `@x = h; poke(@x)`, a
|
|
6554
|
+
# block parameter of `h.tap`) hands on that parameter too (`census`, {#callee_write_census}).
|
|
6555
|
+
def forwarded_parameters(call_node, slots, depth, context = NO_HAND_OFF_CONTEXT)
|
|
4601
6556
|
args = call_node.arguments
|
|
4602
6557
|
return NO_FORWARDS unless args.is_a?(Prism::ArgumentsNode)
|
|
4603
6558
|
|
|
@@ -4605,11 +6560,14 @@ module Rigor
|
|
|
4605
6560
|
forwarded = []
|
|
4606
6561
|
args.arguments.each_with_index do |argument, index|
|
|
4607
6562
|
if argument.is_a?(Prism::KeywordHashNode)
|
|
4608
|
-
forwarded.concat(forwarded_keywords(argument, slots, depth))
|
|
6563
|
+
forwarded.concat(forwarded_keywords(argument, slots, depth, context))
|
|
4609
6564
|
elsif argument.is_a?(Prism::SplatNode) || argument.is_a?(Prism::ForwardingArgumentsNode)
|
|
4610
6565
|
spread = true
|
|
4611
|
-
|
|
4612
|
-
forwarded << [
|
|
6566
|
+
rest = splatted_rest(argument, depth, context)
|
|
6567
|
+
forwarded << [rest, ANY_SLOT] if rest
|
|
6568
|
+
else
|
|
6569
|
+
slot = spread ? ANY_SLOT : index
|
|
6570
|
+
handed_names(argument, slots, depth, context) { |name| forwarded << [name, slot] }
|
|
4613
6571
|
end
|
|
4614
6572
|
end
|
|
4615
6573
|
forwarded
|
|
@@ -4617,25 +6575,93 @@ module Rigor
|
|
|
4617
6575
|
|
|
4618
6576
|
# The `callee_slot` of an argument whose parameter the call does not show (one after a splat).
|
|
4619
6577
|
ANY_SLOT = :__any_positional__
|
|
4620
|
-
|
|
6578
|
+
|
|
6579
|
+
# The census {FLOOR_EVERY} answers: the rest and keyword-rest slots, which {#arguments_for_slots} reads as every
|
|
6580
|
+
# positional argument and every keyword value of the call.
|
|
6581
|
+
EVERY_ARGUMENT = { REST_PARAMETER => ANY_SLOT, KEYWORD_REST_PARAMETER => ANY_KEYWORD }.freeze
|
|
6582
|
+
private_constant :ANY_SLOT, :EVERY_ARGUMENT
|
|
4621
6583
|
|
|
4622
6584
|
# The names of the callee's parameters (`callee_slots`, {#content_parameter_slots}) an argument bound to `slot`
|
|
4623
6585
|
# may reach: the one at that slot, or every positional one for {ANY_SLOT}.
|
|
4624
6586
|
def bound_parameter_names(callee_slots, slot)
|
|
4625
6587
|
if slot == ANY_SLOT
|
|
4626
6588
|
callee_slots.filter_map { |name, at| name if at.is_a?(Integer) }
|
|
6589
|
+
elsif slot == ANY_KEYWORD
|
|
6590
|
+
callee_slots.filter_map { |name, at| name if at.is_a?(Symbol) }
|
|
4627
6591
|
else
|
|
4628
6592
|
callee_slots.filter_map { |name, at| name if at == slot }
|
|
4629
6593
|
end
|
|
4630
6594
|
end
|
|
4631
6595
|
|
|
4632
|
-
def
|
|
4633
|
-
|
|
4634
|
-
|
|
6596
|
+
# Issue #2336 — a double splat of the def's own keyword rest (`poke(**kw)`) hands it on whole, at {ANY_KEYWORD}.
|
|
6597
|
+
def forwarded_keywords(hash_node, slots, depth, context)
|
|
6598
|
+
found = []
|
|
6599
|
+
hash_node.elements.each do |pair|
|
|
6600
|
+
if pair.is_a?(Prism::AssocSplatNode)
|
|
6601
|
+
rest = splatted_keyword_rest(pair, depth, context)
|
|
6602
|
+
found << [rest, ANY_KEYWORD] if rest
|
|
6603
|
+
end
|
|
6604
|
+
next unless pair.is_a?(Prism::AssocNode) && pair.key.is_a?(Prism::SymbolNode)
|
|
6605
|
+
|
|
6606
|
+
keyword = pair.key.unescaped.to_sym
|
|
6607
|
+
handed_names(pair.value, slots, depth, context) { |name| found << [name, keyword] }
|
|
6608
|
+
end
|
|
6609
|
+
found
|
|
6610
|
+
end
|
|
6611
|
+
|
|
6612
|
+
# Yields each parameter `node` may hand on: the one it reads ({#parameter_read_name}) and those it may be through
|
|
6613
|
+
# an alias or an expression ({CalleeWriteCensus.handed_aliases}). An element of a named rest or keyword rest
|
|
6614
|
+
# (`x = args[0]; poke(x)`, `poke(kw[:h])`) hands on that rest, under {REST_PARAMETER} / {KEYWORD_REST_PARAMETER}
|
|
6615
|
+
# ({#forwarding_slots}).
|
|
6616
|
+
#
|
|
6617
|
+
# Issue #2336 — a read of the parameter after the def rebound it to a new object (`acc = acc.dup; poke(acc)`)
|
|
6618
|
+
# hands on the copy, not the caller's argument ({#after_rebind?}).
|
|
6619
|
+
def handed_names(node, slots, depth, context)
|
|
6620
|
+
name = parameter_read_name(node, slots, depth)
|
|
6621
|
+
yield name if name && !CalleeWriteCensus.rebound?(context.rebinds, name, node)
|
|
6622
|
+
node = node.value if node.is_a?(Prism::ImplicitNode)
|
|
6623
|
+
aliases = CalleeWriteCensus.handed_aliases(context.census, context.seeds, node, depth,
|
|
6624
|
+
fresh: FRESH_RESULTS, copiers: CENSUS_COPIERS)
|
|
6625
|
+
aliases.each do |aliased|
|
|
6626
|
+
next if aliased == name
|
|
6627
|
+
|
|
6628
|
+
if slots.key?(aliased) then yield aliased
|
|
6629
|
+
elsif (rest = context.rests[aliased]) then yield rest
|
|
6630
|
+
end
|
|
6631
|
+
end
|
|
6632
|
+
end
|
|
6633
|
+
|
|
6634
|
+
# The keyword-rest key a `**kw` double splat of the def's own named keyword rest hands on whole, or nil.
|
|
6635
|
+
def splatted_keyword_rest(pair, depth, context)
|
|
6636
|
+
read = pair.value
|
|
6637
|
+
return unless read.is_a?(Prism::LocalVariableReadNode) && read.depth == depth
|
|
6638
|
+
|
|
6639
|
+
rest = context.rests[read.name]
|
|
6640
|
+
rest if rest == KEYWORD_REST_PARAMETER
|
|
6641
|
+
end
|
|
6642
|
+
|
|
6643
|
+
# The rest key a `*args` splat of the def's own named rest hands on whole (`poke(*args)`), or nil.
|
|
6644
|
+
def splatted_rest(argument, depth, context)
|
|
6645
|
+
read = argument.is_a?(Prism::SplatNode) ? argument.expression : nil
|
|
6646
|
+
return unless read.is_a?(Prism::LocalVariableReadNode) && read.depth == depth
|
|
6647
|
+
|
|
6648
|
+
rest = context.rests[read.name]
|
|
6649
|
+
rest if rest == REST_PARAMETER
|
|
6650
|
+
end
|
|
6651
|
+
|
|
6652
|
+
# What {#forwarded_parameters} reads a def's hand-offs with: its write census, its census seeds, its named
|
|
6653
|
+
# rests and its parameter rebinds ({#parameter_rebinds}).
|
|
6654
|
+
HandOffContext = Data.define(:census, :seeds, :rests, :rebinds)
|
|
6655
|
+
NO_HAND_OFF_CONTEXT = HandOffContext.new(census: CalleeWriteCensus::EMPTY, seeds: {}.freeze, rests: {}.freeze,
|
|
6656
|
+
rebinds: {}.freeze)
|
|
6657
|
+
private_constant :HandOffContext, :NO_HAND_OFF_CONTEXT
|
|
4635
6658
|
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
6659
|
+
# The slots a hand-off's forwarded name maps to: each content parameter's, and {ANY_SLOT} / {ANY_KEYWORD} for the
|
|
6660
|
+
# rest keys an element of a named rest or keyword rest hands on under ({#handed_names}).
|
|
6661
|
+
def forwarding_slots(def_node)
|
|
6662
|
+
slots = content_parameter_slots(def_node)
|
|
6663
|
+
rest_parameter_names(def_node).each_value { |key| slots[key] = key == REST_PARAMETER ? ANY_SLOT : ANY_KEYWORD }
|
|
6664
|
+
slots
|
|
4639
6665
|
end
|
|
4640
6666
|
|
|
4641
6667
|
# The parameter `node` reads when it is a read of one of `slots`' names at `depth` (a keyword shorthand
|
|
@@ -4647,16 +6673,168 @@ module Rigor
|
|
|
4647
6673
|
node.name if slots.key?(node.name)
|
|
4648
6674
|
end
|
|
4649
6675
|
|
|
4650
|
-
# The
|
|
4651
|
-
# `
|
|
4652
|
-
#
|
|
4653
|
-
def
|
|
4654
|
-
|
|
4655
|
-
|
|
6676
|
+
# The `[def, offset]` pairs a self-dispatch `node` resolves to in the enclosing class, `offset` being how many
|
|
6677
|
+
# leading arguments of `node` are not the def's. Issue #2521 — the instance def is looked up along the class's
|
|
6678
|
+
# ancestors ({Scope#user_def_through_ancestors}), as Ruby dispatches the call: an inherited def is now walked
|
|
6679
|
+
# under the receiver's class, so a self-call inside it to another inherited def must still be found there.
|
|
6680
|
+
#
|
|
6681
|
+
# Issue #2297 — in a singleton-side body (`def self.x`, a `def` in `class << self`), whose `self` is the class
|
|
6682
|
+
# object, a self-call resolves to the singleton def of that name AND to the same-named instance def, so the floor
|
|
6683
|
+
# applies when either mutates. Ruby calls only the singleton side, but the hand-offs that reach an instance def
|
|
6684
|
+
# from there (`new.call(acc)`, `instance.helper(acc)`) and the singleton defs the index does not record
|
|
6685
|
+
# (`module_function :poke`, `define_singleton_method`, `alias_method` in `class << self`) are not followed, and
|
|
6686
|
+
# dropping the instance def floored too little on correct code
|
|
6687
|
+
# (`approval-queue.d/callee-floor-singleton-side.md`).
|
|
6688
|
+
# An instance-side body resolves to instance defs only.
|
|
6689
|
+
#
|
|
6690
|
+
# A `send(:name, ...)` / `public_send(:name, ...)` with a symbol literal resolves as the self-call `name(...)`,
|
|
6691
|
+
# offset 1, on each side that has no def of the `send` / `public_send` name itself; on a side that has one, it is
|
|
6692
|
+
# an ordinary self-call to that def, offset 0.
|
|
6693
|
+
def resolve_self_callees(node)
|
|
6694
|
+
self_type = scope.self_type
|
|
6695
|
+
class_name = enclosing_class_name_for(self_type)
|
|
6696
|
+
return top_level_callees(node, nil) if class_name.nil?
|
|
6697
|
+
|
|
6698
|
+
instance = ancestor_side_callee(node, class_name) do |name|
|
|
6699
|
+
scope.user_def_through_ancestors(class_name, name)
|
|
6700
|
+
end
|
|
6701
|
+
side = self_type.is_a?(Type::Singleton) ? :singleton : :instance
|
|
6702
|
+
singleton = inherited_singleton_callee(node, self_type) if side == :singleton
|
|
6703
|
+
return unanswered_self_callees(node, class_name, side) unless instance || singleton
|
|
6704
|
+
return [instance] if singleton.nil?
|
|
6705
|
+
return [singleton] if instance.nil?
|
|
6706
|
+
return [instance] if singleton.first.equal?(instance.first)
|
|
6707
|
+
|
|
6708
|
+
[singleton, instance]
|
|
6709
|
+
end
|
|
6710
|
+
|
|
6711
|
+
# Issue #2515 — the `[def, offset, owner]` a receiverless call resolves to in the project's top-level defs: a
|
|
6712
|
+
# top-level def is a private method on `Object`, so a call outside any class body, and a call in a class whose
|
|
6713
|
+
# chain does not answer the name, reaches it. `class_name` is nil outside a class (the callee's own hand-offs
|
|
6714
|
+
# resolve in this evaluator, owner nil), else the class and side the chain was searched on. The def is read as
|
|
6715
|
+
# written only when nothing else can answer it ({ToplevelDefCensus.reliable?}, and for a class the chain
|
|
6716
|
+
# {#chain_may_answer?}); otherwise it reads as {FLOOR_EVERY}, every argument written. Nothing when no top-level
|
|
6717
|
+
# def has the name.
|
|
6718
|
+
def top_level_callees(node, class_name, side = nil)
|
|
6719
|
+
target = nil
|
|
6720
|
+
found = side_callee(node) do |name|
|
|
6721
|
+
def_node = scope.top_level_def_for(name)
|
|
6722
|
+
target = name unless def_node.nil?
|
|
6723
|
+
def_node
|
|
6724
|
+
end
|
|
6725
|
+
return NO_CALLEES if found.nil?
|
|
6726
|
+
|
|
6727
|
+
def_node, offset = found
|
|
6728
|
+
floor = !ToplevelDefCensus.reliable?(scope, target) ||
|
|
6729
|
+
(!class_name.nil? && chain_may_answer?(class_name, side, target))
|
|
6730
|
+
[[def_node, offset, if floor
|
|
6731
|
+
FLOOR_EVERY
|
|
6732
|
+
else
|
|
6733
|
+
(class_name.nil? ? nil : TOPLEVEL_OWNER)
|
|
6734
|
+
end]]
|
|
6735
|
+
end
|
|
6736
|
+
|
|
6737
|
+
# A self-call the class's own chain found no def for: the chain's `method_missing` answers it, else the project's
|
|
6738
|
+
# top-level def of that name (Ruby searches `Object` before `method_missing`, so with both the top-level def runs
|
|
6739
|
+
# and the `method_missing` reading is the over-approximation; both are returned and the top-level one floors
|
|
6740
|
+
# every argument).
|
|
6741
|
+
#
|
|
6742
|
+
# Issue #2336 — a name nothing above answers floors every argument when a `define_method` /
|
|
6743
|
+
# `define_singleton_method` / `alias_method` call gives it a body ({#self_ownerless_body?}).
|
|
6744
|
+
def unanswered_self_callees(node, class_name, side)
|
|
6745
|
+
missing = self_method_missing_callees(node, class_name, side)
|
|
6746
|
+
top = top_level_callees(node, class_name, side)
|
|
6747
|
+
if missing.empty? && top.empty?
|
|
6748
|
+
name = send_hand_off?(node) ? send_target(node) : node.name
|
|
6749
|
+
return self_ownerless_body?(name, class_name, side) ? OPAQUE_CALLEES : NO_CALLEES
|
|
6750
|
+
end
|
|
6751
|
+
return top if missing.empty?
|
|
6752
|
+
return missing if top.empty?
|
|
6753
|
+
|
|
6754
|
+
missing + top.map { |def_node, offset, _owner| [def_node, offset, FLOOR_EVERY] }
|
|
6755
|
+
end
|
|
6756
|
+
|
|
6757
|
+
# True when something besides `Object` may answer a receiverless call of `name` in `class_name`'s `side`: the
|
|
6758
|
+
# class is no project class, a definer other than a `def` sits on its chain (`attr_*`, `define_method`, a budget
|
|
6759
|
+
# cut), or an ancestor the project does not declare is unknown to RBS or declares the name there (a
|
|
6760
|
+
# `< ActiveRecord::Base` the environment has no RBS for may define anything). The project's own `def`s were
|
|
6761
|
+
# searched already ({#resolve_self_callees}).
|
|
6762
|
+
def chain_may_answer?(class_name, side, name)
|
|
6763
|
+
return true unless project_class?(class_name)
|
|
6764
|
+
return true if scope.discovered_method_through_ancestors?(class_name, name, side)
|
|
6765
|
+
return true if rbs_declares?(class_name, name) || (side == :singleton && rbs_declares?("Class", name))
|
|
6766
|
+
|
|
6767
|
+
external = scope.external_ancestor_name_candidates(class_name)
|
|
6768
|
+
external += scope.singleton_extends_of(class_name).map { |module_name| [module_name] } if side == :singleton
|
|
6769
|
+
external.any? do |candidates|
|
|
6770
|
+
known = candidates.find { |candidate| Reflection.rbs_class_known?(candidate, scope: scope) }
|
|
6771
|
+
known.nil? ? candidates.none? { |candidate| project_class?(candidate) } : rbs_declares?(known, name)
|
|
6772
|
+
end
|
|
6773
|
+
end
|
|
6774
|
+
|
|
6775
|
+
# A class or module the project declares, even one with no method of its own.
|
|
6776
|
+
def project_class?(name) = scope.known_user_class?(name) || scope.declared_class?(name)
|
|
6777
|
+
|
|
6778
|
+
def rbs_declares?(class_name, name)
|
|
6779
|
+
!Reflection.instance_method_definition(class_name, name, scope: scope).nil? ||
|
|
6780
|
+
!Reflection.singleton_method_definition(class_name, name, scope: scope).nil?
|
|
6781
|
+
end
|
|
6782
|
+
|
|
6783
|
+
# The `[def, offset, owner]` a singleton-side self-call resolves to along the singleton chain: owner nil for a
|
|
6784
|
+
# def of the class itself, {FLOOR_ALL} for one it inherits ({#walked_owner}).
|
|
6785
|
+
def inherited_singleton_callee(node, self_type)
|
|
6786
|
+
class_name = self_type.class_name
|
|
6787
|
+
ancestor_side_callee(node, class_name) do |name|
|
|
6788
|
+
scope.singleton_def_through_ancestors(class_name, name)
|
|
6789
|
+
end
|
|
6790
|
+
end
|
|
6791
|
+
|
|
6792
|
+
# The `[def, offset, owner]` `node` resolves to through a lookup on `class_name` that answers `[def, defining]`
|
|
6793
|
+
# (the block), owner nil, or {FLOOR_ALL} ({#walked_owner}); nil when nothing answers.
|
|
6794
|
+
def ancestor_side_callee(node, class_name)
|
|
6795
|
+
defining = nil
|
|
6796
|
+
found = side_callee(node) do |name|
|
|
6797
|
+
def_node, owner = yield(name)
|
|
6798
|
+
defining = owner unless def_node.nil?
|
|
6799
|
+
def_node
|
|
6800
|
+
end
|
|
6801
|
+
found && [*found, walked_owner(defining, class_name, nil)]
|
|
6802
|
+
end
|
|
6803
|
+
|
|
6804
|
+
# {#method_missing_callees} for a self-call `node` that resolves to nothing; a `send(:name, ...)` hand-off
|
|
6805
|
+
# passes the name as `method_missing`'s own first argument.
|
|
6806
|
+
def self_method_missing_callees(node, class_name, side)
|
|
6807
|
+
if send_hand_off?(node)
|
|
6808
|
+
method_missing_callees(class_name, side, node.arguments.arguments.first.unescaped.to_sym, 1)
|
|
6809
|
+
else
|
|
6810
|
+
method_missing_callees(class_name, side, node.name, 0)
|
|
6811
|
+
end
|
|
6812
|
+
end
|
|
6813
|
+
|
|
6814
|
+
# The `[def, offset]` `node` resolves to on one side, whose def table the block looks a name up in, or nil.
|
|
6815
|
+
def side_callee(node)
|
|
6816
|
+
if send_hand_off?(node)
|
|
6817
|
+
own = yield(node.name)
|
|
6818
|
+
return [own, 0] if own
|
|
6819
|
+
|
|
6820
|
+
target = yield(node.arguments.arguments.first.unescaped.to_sym)
|
|
6821
|
+
return target && [target, 1]
|
|
6822
|
+
end
|
|
6823
|
+
|
|
6824
|
+
found = yield(node.name)
|
|
6825
|
+
found && [found, 0]
|
|
6826
|
+
end
|
|
4656
6827
|
|
|
4657
|
-
|
|
6828
|
+
# True for a `send` / `public_send` / `__send__` whose first argument is a literal target name: a symbol, or
|
|
6829
|
+
# (issue #2336) a string (`send("poke", h)`).
|
|
6830
|
+
def send_hand_off?(node)
|
|
6831
|
+
SEND_HAND_OFFS.include?(node.name) && CalleeWriteCensus.literal_send?(node)
|
|
4658
6832
|
end
|
|
4659
6833
|
|
|
6834
|
+
SEND_HAND_OFFS = %i[send public_send __send__].freeze
|
|
6835
|
+
NO_CALLEES = [].freeze
|
|
6836
|
+
private_constant :SEND_HAND_OFFS, :NO_CALLEES
|
|
6837
|
+
|
|
4660
6838
|
def self_dispatch_call?(node)
|
|
4661
6839
|
return false unless node.is_a?(Prism::CallNode)
|
|
4662
6840
|
|
|
@@ -4664,7 +6842,7 @@ module Rigor
|
|
|
4664
6842
|
end
|
|
4665
6843
|
|
|
4666
6844
|
# The set of `[name, slot]` parameters of `def_node` whose content a block in the body escape-mutates.
|
|
4667
|
-
# Memoised per def node (the body walk is otherwise repeated at every call site). A parameter is "escape-
|
|
6845
|
+
# Memoised per def node (the body walk is otherwise repeated at every call site). A parameter is "escape-mutated"
|
|
4668
6846
|
# when a `param[k] = v` / `param << x` mutation on it appears inside a block whose receiving call is not proven
|
|
4669
6847
|
# non-escaping.
|
|
4670
6848
|
def escaped_content_parameters(def_node)
|
|
@@ -4681,8 +6859,10 @@ module Rigor
|
|
|
4681
6859
|
next unless descendant.is_a?(Prism::CallNode) && descendant.block.is_a?(Prism::BlockNode)
|
|
4682
6860
|
next if syntactically_non_escaping_call?(descendant)
|
|
4683
6861
|
|
|
4684
|
-
collect_content_mutations(descendant.block.body).
|
|
4685
|
-
|
|
6862
|
+
collect_content_mutations(descendant.block.body).each do |name, nodes|
|
|
6863
|
+
next unless positions.key?(name)
|
|
6864
|
+
|
|
6865
|
+
mutated << name if nodes.any? { |node| !after_rebind?(def_node, name, node) }
|
|
4686
6866
|
end
|
|
4687
6867
|
end
|
|
4688
6868
|
positions.slice(*mutated)
|
|
@@ -4694,9 +6874,9 @@ module Rigor
|
|
|
4694
6874
|
# parameter-name read is genuinely at method scope (depth 0) rather than a block-local of the same name that
|
|
4695
6875
|
# merely shadows the parameter — so we never floor a caller argument the callee did not actually mutate.
|
|
4696
6876
|
#
|
|
4697
|
-
# A mutation inside a NON-escaping block (`n.times { |i| acc[i] = v }`) is not counted: the bare-collection
|
|
4698
|
-
#
|
|
4699
|
-
#
|
|
6877
|
+
# A mutation inside a NON-escaping block (`n.times { |i| acc[i] = v }`) is not counted here: the bare-collection
|
|
6878
|
+
# floor this census applies dropped the element type (af3efef33, and again under #2104). Issue #2296 counts it in
|
|
6879
|
+
# {#own_block_content_parameters}, whose floor keeps the element type.
|
|
4700
6880
|
def direct_content_parameters(def_node)
|
|
4701
6881
|
cache = (@direct_param_cache ||= {}.compare_by_identity)
|
|
4702
6882
|
cache[def_node] ||= compute_direct_content_parameters(def_node)
|
|
@@ -4709,11 +6889,110 @@ module Rigor
|
|
|
4709
6889
|
mutated = Set.new
|
|
4710
6890
|
each_node_outside_nested_scopes(def_node.body) do |descendant|
|
|
4711
6891
|
name, = content_mutation_target(descendant) { |r| r.is_a?(Prism::LocalVariableReadNode) && r.depth.zero? }
|
|
4712
|
-
mutated << name if !name.nil? && positions.key?(name)
|
|
6892
|
+
mutated << name if !name.nil? && positions.key?(name) && !after_rebind?(def_node, name, descendant)
|
|
4713
6893
|
end
|
|
4714
6894
|
positions.slice(*mutated)
|
|
4715
6895
|
end
|
|
4716
6896
|
|
|
6897
|
+
# Issue #2296 — true when `node` runs after the def has rebound its parameter `name` to a fresh object
|
|
6898
|
+
# (`acc = acc.dup` as a statement of the method body, ahead of `node`): from there `name` holds another object,
|
|
6899
|
+
# so a mutation of it does not reach the caller's argument. Only an unconditional rebind counts, a write that is
|
|
6900
|
+
# itself a statement of the body, not one under a branch, a loop, a block or `||=`. Its value MUST provably be a
|
|
6901
|
+
# new object ({#fresh_value?}): `acc = acc.to_a` or `s = s.to_s` hands back the same object, so a mutation after
|
|
6902
|
+
# it still reaches the caller. A def whose body has a `rescue` or `ensure` clause never rebinds, since the clause
|
|
6903
|
+
# runs with whichever binding the raise left. And only a node that starts after the write ends counts, so
|
|
6904
|
+
# `acc = poke(acc)` still passes the original.
|
|
6905
|
+
def after_rebind?(def_node, name, node)
|
|
6906
|
+
offset = parameter_rebinds(def_node)[name]
|
|
6907
|
+
!offset.nil? && node.location.start_offset >= offset
|
|
6908
|
+
end
|
|
6909
|
+
|
|
6910
|
+
# `{ name => end_offset }` of the first unconditional fresh rebind of each parameter ({#after_rebind?}),
|
|
6911
|
+
# memoised per def node for the thread.
|
|
6912
|
+
def parameter_rebinds(def_node)
|
|
6913
|
+
def_census(:rebinds, def_node) do
|
|
6914
|
+
slots = content_parameter_slots(def_node)
|
|
6915
|
+
rebinds = nil
|
|
6916
|
+
body_statements(def_node.body).each do |statement|
|
|
6917
|
+
next unless fresh_rebind?(statement, slots)
|
|
6918
|
+
|
|
6919
|
+
(rebinds ||= {})[statement.name] ||= statement.location.end_offset
|
|
6920
|
+
end
|
|
6921
|
+
rebinds || NO_PARAMETER_SLOTS
|
|
6922
|
+
end
|
|
6923
|
+
end
|
|
6924
|
+
|
|
6925
|
+
def fresh_rebind?(statement, slots)
|
|
6926
|
+
case statement
|
|
6927
|
+
when Prism::LocalVariableWriteNode
|
|
6928
|
+
statement.depth.zero? && slots.key?(statement.name) && fresh_value?(statement.value, statement.name)
|
|
6929
|
+
when Prism::LocalVariableOperatorWriteNode
|
|
6930
|
+
statement.depth.zero? && slots.key?(statement.name) &&
|
|
6931
|
+
FRESH_OPERATORS.include?(statement.binary_operator)
|
|
6932
|
+
else false
|
|
6933
|
+
end
|
|
6934
|
+
end
|
|
6935
|
+
|
|
6936
|
+
# A value that is always a new object: a `dup` / `clone`, an Array / Hash / String literal, `new` on a core
|
|
6937
|
+
# collection class ({#core_collection_constant?}), or one of {FRESH_COPIERS} called on the parameter itself. Any
|
|
6938
|
+
# other call may return its receiver.
|
|
6939
|
+
def fresh_value?(value, name)
|
|
6940
|
+
case value
|
|
6941
|
+
when Prism::ArrayNode, Prism::HashNode, Prism::StringNode, Prism::InterpolatedStringNode then true
|
|
6942
|
+
when Prism::CallNode then fresh_call?(value, name)
|
|
6943
|
+
else false
|
|
6944
|
+
end
|
|
6945
|
+
end
|
|
6946
|
+
|
|
6947
|
+
def fresh_call?(call, name)
|
|
6948
|
+
return true if FRESH_DUPLICATORS.include?(call.name)
|
|
6949
|
+
return core_collection_constant?(call.receiver) if call.name == :new
|
|
6950
|
+
|
|
6951
|
+
receiver = call.receiver
|
|
6952
|
+
FRESH_COPIERS.include?(call.name) && receiver.is_a?(Prism::LocalVariableReadNode) && receiver.name == name
|
|
6953
|
+
end
|
|
6954
|
+
|
|
6955
|
+
# `::Array` / `::Hash` / `::String` / `::Set`, written `::`-rooted: the core classes whose `new` always builds a
|
|
6956
|
+
# new object. A project class's `new` may hand back its argument (`Ident.new(acc)`) or wrap it (`Wrap.new(acc)`,
|
|
6957
|
+
# whose `[]=` writes through), so it is no fresh rebind. The census reads the AST alone and cannot resolve a bare
|
|
6958
|
+
# `Array`, which a namespace may shadow (`M::Array`), so only the rooted spelling counts.
|
|
6959
|
+
def core_collection_constant?(node)
|
|
6960
|
+
node.is_a?(Prism::ConstantPathNode) && node.parent.nil? && FRESH_CONSTRUCTED.include?(node.name)
|
|
6961
|
+
end
|
|
6962
|
+
|
|
6963
|
+
FRESH_DUPLICATORS = %i[dup clone].to_set.freeze
|
|
6964
|
+
FRESH_CONSTRUCTED = %i[Array Hash String Set].to_set.freeze
|
|
6965
|
+
FRESH_COPIERS = %i[map collect select reject merge + -].to_set.freeze
|
|
6966
|
+
FRESH_OPERATORS = %i[+ -].to_set.freeze
|
|
6967
|
+
# The calls whose result {#callee_write_census} reads as a new object rather than an alias of the receiver.
|
|
6968
|
+
# The calls {#callee_write_census} reads as a new container of the receiver's elements, on an Array, a Hash and a
|
|
6969
|
+
# String alike: a write to the result misses the receiver, an element of it may be a part. `to_a`, `to_h`
|
|
6970
|
+
# (without a block; `to_h { }` is read as one in {CalleeWriteCensus}), `to_s`, `each_slice`, `each_cons` and
|
|
6971
|
+
# `each_with_index` stay out: each returns its receiver on one of them.
|
|
6972
|
+
CENSUS_COPIERS = %i[
|
|
6973
|
+
slice except compact transform_values transform_keys filter_map group_by partition sort sort_by uniq flatten
|
|
6974
|
+
reverse keys values inspect zip filter flat_map collect_concat take drop
|
|
6975
|
+
].to_set.freeze
|
|
6976
|
+
# The calls whose result {#callee_write_census} reads as a new object holding nothing of the receiver's.
|
|
6977
|
+
FRESH_RESULTS = (FRESH_DUPLICATORS | FRESH_COPIERS | %i[join]).freeze
|
|
6978
|
+
private_constant :FRESH_DUPLICATORS, :FRESH_CONSTRUCTED, :FRESH_COPIERS, :FRESH_OPERATORS, :CENSUS_COPIERS,
|
|
6979
|
+
:FRESH_RESULTS
|
|
6980
|
+
|
|
6981
|
+
# The statements a def body runs in order: a statements list, with a parenthesised list (an endless def's
|
|
6982
|
+
# `= (a; b)`) read in place, or the lone expression. A body with a `rescue` / `ensure` clause (a `BeginNode`)
|
|
6983
|
+
# answers none, so it never rebinds.
|
|
6984
|
+
def body_statements(body)
|
|
6985
|
+
case body
|
|
6986
|
+
when nil, Prism::BeginNode then NO_FORWARDS
|
|
6987
|
+
when Prism::StatementsNode then body.body.flat_map { |statement| body_statements_of(statement) }
|
|
6988
|
+
else [body]
|
|
6989
|
+
end
|
|
6990
|
+
end
|
|
6991
|
+
|
|
6992
|
+
def body_statements_of(statement)
|
|
6993
|
+
statement.is_a?(Prism::ParenthesesNode) ? body_statements(statement.body) : [statement]
|
|
6994
|
+
end
|
|
6995
|
+
|
|
4717
6996
|
# Yields every node reachable from `body` without crossing into a nested block / def / lambda — i.e. the nodes
|
|
4718
6997
|
# that execute in the method's own scope. A local-variable read found here has depth 0 relative to the method, so
|
|
4719
6998
|
# a content mutation whose receiver is a positional-parameter name is a genuine mutation of that parameter.
|
|
@@ -4766,7 +7045,7 @@ module Rigor
|
|
|
4766
7045
|
end
|
|
4767
7046
|
|
|
4768
7047
|
# A receiver-independent over-approximation of `ClosureEscapeAnalyzer`'s non-escaping verdict, used when scanning
|
|
4769
|
-
# a callee body where the block-
|
|
7048
|
+
# a callee body where the block-owning call's receiver TYPE is not available. A call whose method name is a known
|
|
4770
7049
|
# structural iterator (`each` / `map` / `tap` / …) runs its block synchronously and does not retain it, so its
|
|
4771
7050
|
# captured mutations are not a cross-boundary escape. Any other name (`on`, `subscribe`, `define_method`, an
|
|
4772
7051
|
# unknown DSL hook) is treated as escaping — sound, since mis-classifying a truly-non-escaping call only floors an
|
|
@@ -4804,14 +7083,21 @@ module Rigor
|
|
|
4804
7083
|
# True when at least one argument of `node` is a bare local-variable read (positional or keyword value) bound in
|
|
4805
7084
|
# the current scope — a cheap pre-filter so the def resolution / body scan only runs for calls that could actually
|
|
4806
7085
|
# floor something.
|
|
7086
|
+
# A local bound in the current scope, or (issue #2335) one whose object another local may share, which the
|
|
7087
|
+
# alias floor reads a callee's write through ({AliasMutationFloor.reaches_other_locals?}).
|
|
7088
|
+
def passed_local?(read, node)
|
|
7089
|
+
scope.locals.key?(read.name) || AliasMutationFloor.reaches_other_locals?(scope, read, node)
|
|
7090
|
+
end
|
|
7091
|
+
|
|
4807
7092
|
def call_passes_local_argument?(node)
|
|
4808
7093
|
args = node.arguments
|
|
4809
7094
|
return false unless args.respond_to?(:arguments)
|
|
4810
7095
|
|
|
4811
7096
|
args.arguments.any? do |arg|
|
|
4812
7097
|
case arg
|
|
4813
|
-
when Prism::LocalVariableReadNode
|
|
4814
|
-
|
|
7098
|
+
when Prism::LocalVariableReadNode then passed_local?(arg, node)
|
|
7099
|
+
when Prism::SplatNode
|
|
7100
|
+
arg.expression.is_a?(Prism::LocalVariableReadNode) && scope.locals.key?(arg.expression.name)
|
|
4815
7101
|
when Prism::KeywordHashNode
|
|
4816
7102
|
arg.elements.any? do |pair|
|
|
4817
7103
|
next false unless pair.is_a?(Prism::AssocNode)
|
|
@@ -4835,7 +7121,7 @@ module Rigor
|
|
|
4835
7121
|
while receiver.is_a?(Prism::CallNode)
|
|
4836
7122
|
if receiver.block.is_a?(Prism::BlockNode) &&
|
|
4837
7123
|
classify_closure_escape(receiver) != :non_escaping
|
|
4838
|
-
acc = widen_escaping_content_captures(receiver.block, acc)
|
|
7124
|
+
acc = widen_escaping_content_captures(receiver.block, acc, foreign_self: foreign_self_block?(receiver))
|
|
4839
7125
|
end
|
|
4840
7126
|
receiver = receiver.receiver
|
|
4841
7127
|
end
|
|
@@ -4853,7 +7139,14 @@ module Rigor
|
|
|
4853
7139
|
# `Array[Dynamic[top]]`, Hash -> `Hash[untyped, untyped]`, String -> `String`. The seed's element/key/value
|
|
4854
7140
|
# precision is forgotten; only the carrier survives. Read-only captures and locals the block merely rebinds
|
|
4855
7141
|
# (already floored by `drop_captured_narrowing`) are untouched.
|
|
4856
|
-
|
|
7142
|
+
#
|
|
7143
|
+
# An instance variable of this scope's `self` the body mutates in place is floored too (issue #1208): a callback
|
|
7144
|
+
# that runs `@opts[:format] = "json"` later left `@opts[:format]` folding to `"text"`, and so are a class variable
|
|
7145
|
+
# and a global (issue #3096). Each floors member by member ({#member_content_floor}, issue #3095), so a lazily
|
|
7146
|
+
# initialised `@buf = flag ? [1] : nil` keeps its `nil`. An instance variable under a block that runs on another
|
|
7147
|
+
# object is that object's, and so is every one when `foreign_self` says the closure itself does; a class
|
|
7148
|
+
# variable resolves lexically and a global is global, so neither depends on `self`.
|
|
7149
|
+
def widen_escaping_content_captures(block_node, post_scope, foreign_self: false)
|
|
4857
7150
|
body = block_node.body
|
|
4858
7151
|
return post_scope if body.nil?
|
|
4859
7152
|
|
|
@@ -4861,13 +7154,83 @@ module Rigor
|
|
|
4861
7154
|
# `local[k] = v` write, which `collect_content_mutations` cannot see (see below).
|
|
4862
7155
|
post_scope = floor_block_body_callee_escaped_args(body, post_scope)
|
|
4863
7156
|
|
|
4864
|
-
|
|
4865
|
-
|
|
7157
|
+
post_scope = collect_content_mutations(body).keys.reduce(post_scope) do |acc, name|
|
|
7158
|
+
floored = content_floor_for(CapturedLocals.bound_type(acc, name))
|
|
7159
|
+
floored.nil? ? acc : bind_mutated_capture(acc, name, floored)
|
|
7160
|
+
end
|
|
7161
|
+
|
|
7162
|
+
floor_non_local_contents(escaping_non_local_content_mutations(body, post_scope, foreign_self), post_scope)
|
|
7163
|
+
end
|
|
7164
|
+
|
|
7165
|
+
# `scope` with each instance variable, class variable or global in `names` floored member by member.
|
|
7166
|
+
def floor_non_local_contents(names, scope)
|
|
7167
|
+
names.reduce(scope) do |acc, name|
|
|
7168
|
+
bound = CapturedLocals.bound_type(acc, name)
|
|
7169
|
+
floored = member_content_floor(bound)
|
|
7170
|
+
floored.equal?(bound) ? acc : CapturedLocals.bind(acc, name, floored)
|
|
7171
|
+
end
|
|
7172
|
+
end
|
|
7173
|
+
|
|
7174
|
+
# `type` with each member {#content_floor_for} recognises floored to its bare carrier and every other member
|
|
7175
|
+
# kept (issue #3095): `Array[1]?` reads `Array[Dynamic[top]]?` and `3 | [1]` reads `3 | Array[Dynamic[top]]`,
|
|
7176
|
+
# so the floor neither drops the `nil` a callback that never runs leaves nor narrows the binding. `type` itself
|
|
7177
|
+
# when no member floors.
|
|
7178
|
+
def member_content_floor(type)
|
|
7179
|
+
members = type.is_a?(Type::Union) ? type.members : [type]
|
|
7180
|
+
floored = members.map { |member| content_floor_for(member) || member }
|
|
7181
|
+
return type if floored.each_with_index.all? { |member, index| member.equal?(members[index]) }
|
|
7182
|
+
|
|
7183
|
+
Type::Combinator.union(*floored)
|
|
7184
|
+
end
|
|
7185
|
+
|
|
7186
|
+
# The instance variables of `scope`'s `self`, the class variables and the globals an escaping body mutates in
|
|
7187
|
+
# place, on the terms {#capturable_non_local?} reads against `scope` — the continuation the floor writes into.
|
|
7188
|
+
def escaping_non_local_content_mutations(body, scope, foreign_self)
|
|
7189
|
+
names = nil
|
|
7190
|
+
Source::NodeWalker.each_with_ancestors(body) do |node, ancestors|
|
|
7191
|
+
name, content = non_local_shape_mutation(node)
|
|
7192
|
+
next if name.nil?
|
|
7193
|
+
next unless (content && capturable_non_local?(name, ancestors, scope, foreign_self: foreign_self)) ||
|
|
7194
|
+
block_defined_method_capture?(name, ancestors, scope, foreign_self)
|
|
7195
|
+
|
|
7196
|
+
(names ||= []) << name
|
|
7197
|
+
end
|
|
7198
|
+
names ? names.uniq : NO_REWRITES
|
|
7199
|
+
end
|
|
7200
|
+
|
|
7201
|
+
# `[name, content]` when `node` mutates an instance variable, class variable or global in place, else nil.
|
|
7202
|
+
# `content` says a content scan counts the mutation ({CONTENT_MUTATORS}, or an index write). A method the block
|
|
7203
|
+
# defines counts every name the straight-line widening responds to as well ({CALLEE_WRITE_MUTATORS}: `merge!`,
|
|
7204
|
+
# `map!`, `clear`, …), because its floor reads no evidence from the call (issue #3163).
|
|
7205
|
+
def non_local_shape_mutation(node)
|
|
7206
|
+
content = index_write?(node)
|
|
7207
|
+
unless content
|
|
7208
|
+
return nil unless node.is_a?(Prism::CallNode) && CALLEE_WRITE_MUTATORS.include?(node.name)
|
|
7209
|
+
|
|
7210
|
+
content = CONTENT_MUTATORS.include?(node.name)
|
|
7211
|
+
end
|
|
7212
|
+
receiver = node.receiver
|
|
7213
|
+
NON_LOCAL_READ_NODES.include?(receiver.class) ? [receiver.name, content] : nil
|
|
7214
|
+
end
|
|
7215
|
+
|
|
7216
|
+
# Issue #3163 — `post_scope` with every instance variable, class variable and global floored member by member
|
|
7217
|
+
# ({#member_content_floor}) that a method defined in `call_node`'s non-escaping block mutates in place
|
|
7218
|
+
# ({#block_defined_method_capture?}). The block, or anything it calls, may call that method before it returns:
|
|
7219
|
+
# `$buf = []; [1].each { def h; $buf << 1; end; h }` leaves `$buf` as `[1]`, and the content join reads no
|
|
7220
|
+
# evidence from a method body, which it types in no scope of the block's. No call is followed, so a method the
|
|
7221
|
+
# block defines and nothing calls floors too. An escaping block floors the same names
|
|
7222
|
+
# ({#escaping_non_local_content_mutations}).
|
|
7223
|
+
def floor_block_defined_method_mutations(call_node, block, post_scope)
|
|
7224
|
+
foreign = foreign_self_block?(call_node)
|
|
7225
|
+
names = nil
|
|
7226
|
+
Source::NodeWalker.each_with_ancestors(block.body) do |node, ancestors|
|
|
7227
|
+
name, = non_local_shape_mutation(node)
|
|
7228
|
+
# Read against the continuation: a variable the block itself first binds has no pre-call binding.
|
|
7229
|
+
next if name.nil? || !block_defined_method_capture?(name, ancestors, post_scope, foreign)
|
|
4866
7230
|
|
|
4867
|
-
|
|
4868
|
-
floored = content_floor_for(acc.local(name))
|
|
4869
|
-
floored.nil? ? acc : acc.with_mutated_local(name, floored)
|
|
7231
|
+
(names ||= []) << name
|
|
4870
7232
|
end
|
|
7233
|
+
names.nil? ? post_scope : floor_non_local_contents(names.uniq, post_scope)
|
|
4871
7234
|
end
|
|
4872
7235
|
|
|
4873
7236
|
# Inside an ESCAPING block body, a captured outer local can be content- mutated transitively: the body is (or
|
|
@@ -4907,6 +7270,18 @@ module Rigor
|
|
|
4907
7270
|
Type::Combinator.untyped])
|
|
4908
7271
|
elsif arrayish?(type)
|
|
4909
7272
|
Type::Combinator.nominal_of("Array", type_args: [Type::Combinator.untyped])
|
|
7273
|
+
elsif setish?(type)
|
|
7274
|
+
Type::Combinator.nominal_of("Set", type_args: [Type::Combinator.untyped])
|
|
7275
|
+
end
|
|
7276
|
+
end
|
|
7277
|
+
|
|
7278
|
+
# A `Set` carrier, constant-folded or nominal, for the callee floor above (issue #2294).
|
|
7279
|
+
def setish?(type)
|
|
7280
|
+
case type
|
|
7281
|
+
when Type::Constant then SetMutation.constant?(type)
|
|
7282
|
+
when Type::Nominal then type.class_name == "Set"
|
|
7283
|
+
when Type::Union then type.members.any? { |m| setish?(m) }
|
|
7284
|
+
else false
|
|
4910
7285
|
end
|
|
4911
7286
|
end
|
|
4912
7287
|
|
|
@@ -4981,7 +7356,7 @@ module Rigor
|
|
|
4981
7356
|
# continuation holds each rebound capture at `Dynamic[top]` ({#drop_captured_narrowing}), and the pass reads it
|
|
4982
7357
|
# so: `seen = 0; yielder { return :ret if (seen += 1) > 1 }` reaches the `return`.
|
|
4983
7358
|
def collect_repeated_block_returns(call_node, post_scope)
|
|
4984
|
-
return if
|
|
7359
|
+
return if typing_state[RETURN_SINK].nil?
|
|
4985
7360
|
|
|
4986
7361
|
block = call_node.block
|
|
4987
7362
|
return unless repeated_block_return_candidate?(call_node, block)
|
|
@@ -5021,7 +7396,7 @@ module Rigor
|
|
|
5021
7396
|
# ADR-56 slice A. For a `:non_escaping` block, fold the continuation binding of every outer local the body can
|
|
5022
7397
|
# rebind back into `post_scope`. The binding is a capped fixpoint (cap 3) over the block body re-evaluated under
|
|
5023
7398
|
# the running per-name assumption, joined with the pre-call binding (kept as a constituent so the 0-iteration path
|
|
5024
|
-
# — `[].each { … }` — stays sound), value-pinned
|
|
7399
|
+
# — `[].each { … }` — stays sound), value-pinned widened on the final permitted iteration, and floored to
|
|
5025
7400
|
# `Dynamic[top]` on non-convergence (matching `drop_captured_narrowing`).
|
|
5026
7401
|
#
|
|
5027
7402
|
# The instance variables the body rebinds (`CapturedLocals.writes` with `ivars: true`) outlive the call the same
|
|
@@ -5118,16 +7493,16 @@ module Rigor
|
|
|
5118
7493
|
end
|
|
5119
7494
|
end
|
|
5120
7495
|
|
|
5121
|
-
# Binds a name from `CapturedLocals.writes`. An ivar
|
|
5122
|
-
# #286 optimistic mark. A local keeps the plain `with_local`
|
|
5123
|
-
# rebinds no ivar leaves the locals exactly as before.
|
|
7496
|
+
# Binds a name from `CapturedLocals.writes`. An ivar (or any other non-local) goes through
|
|
7497
|
+
# {CapturedLocals.bind}, which keeps an ivar's issue #286 optimistic mark. A local keeps the plain `with_local`
|
|
7498
|
+
# these seams have always used, so a block that rebinds no ivar leaves the locals exactly as before.
|
|
5124
7499
|
#
|
|
5125
7500
|
# Issue #1402 — `optimistic` is a mark a write-back pass's exit held ({#with_capture_exit_marks}); a name that has
|
|
5126
7501
|
# one binds through {CapturedLocals.bind} whatever its kind, which adds it.
|
|
5127
7502
|
def bind_capture(scope, name, type, optimistic: nil)
|
|
5128
7503
|
return CapturedLocals.bind(scope, name, type, optimistic: optimistic) if optimistic
|
|
5129
7504
|
|
|
5130
|
-
CapturedLocals.
|
|
7505
|
+
CapturedLocals.local_name?(name) ? scope.with_local(name, type) : CapturedLocals.bind(scope, name, type)
|
|
5131
7506
|
end
|
|
5132
7507
|
|
|
5133
7508
|
# A block-level `break` ends the CALL, so the binding it leaves with starts no further iteration and is no input
|
|
@@ -5155,9 +7530,8 @@ module Rigor
|
|
|
5155
7530
|
end
|
|
5156
7531
|
return converged if arms.empty?
|
|
5157
7532
|
|
|
5158
|
-
floor = Type::Combinator.untyped
|
|
5159
7533
|
converged.to_h do |name, type|
|
|
5160
|
-
next [name, type] if type
|
|
7534
|
+
next [name, type] if Type::Combinator.untyped?(type)
|
|
5161
7535
|
|
|
5162
7536
|
[name, Type::Combinator.union(type, *arms.filter_map { |arm| CapturedLocals.bound_type(arm, name) })]
|
|
5163
7537
|
end
|
|
@@ -5216,22 +7590,48 @@ module Rigor
|
|
|
5216
7590
|
block = call_node.block
|
|
5217
7591
|
return post_scope unless block.is_a?(Prism::BlockNode)
|
|
5218
7592
|
return post_scope unless classify_closure_escape(call_node) == :non_escaping
|
|
7593
|
+
return post_scope if block.body.nil?
|
|
5219
7594
|
|
|
5220
|
-
|
|
5221
|
-
|
|
7595
|
+
post_scope = join_block_content_captures(call_node, block, post_scope, seed_scope)
|
|
7596
|
+
# Issue #3163 — a method the block defines may run during the block, so its in-place mutations of an instance
|
|
7597
|
+
# variable, class variable or global floor that binding. The join reads no evidence from a method body.
|
|
7598
|
+
floor_block_defined_method_mutations(call_node, block, post_scope)
|
|
7599
|
+
end
|
|
5222
7600
|
|
|
7601
|
+
# The join {#content_writeback_block_captures} documents, for a non-escaping `block` with a body.
|
|
7602
|
+
def join_block_content_captures(call_node, block, post_scope, seed_scope)
|
|
7603
|
+
body = block.body
|
|
5223
7604
|
shadows = {}.compare_by_identity
|
|
5224
|
-
mutations = captured_content_mutations(block, shadows)
|
|
7605
|
+
mutations = captured_content_mutations(block, shadows, call_node)
|
|
5225
7606
|
return post_scope if mutations.empty?
|
|
5226
7607
|
|
|
5227
7608
|
seeds = mutations.to_h do |name, _calls|
|
|
5228
|
-
|
|
7609
|
+
seed = CapturedLocals.bound_type(seed_scope, name)
|
|
7610
|
+
[name, lookup_mutated_seed(body, name, seed) { |depth, nesting| depth > nesting }]
|
|
5229
7611
|
end
|
|
5230
|
-
shadow_rebound_reads(block, mutations, seeds, shadows)
|
|
7612
|
+
shadow_rebound_reads(block, mutations, seeds, shadows, call_node)
|
|
5231
7613
|
joined = join_content_to_fixpoint(mutations, seeds, build_block_entry_scope(call_node, block), shadows)
|
|
5232
7614
|
rewrites = local_rewrites(block.body) { |receiver, ancestors| receiver.depth > scope_nesting(ancestors) }
|
|
5233
7615
|
joined.reduce(post_scope) do |acc, (name, type)|
|
|
5234
|
-
acc
|
|
7616
|
+
bind_mutated_capture(acc, name, rewritten_capture(type, seeds[name], rewrites.fetch(name, NO_REWRITES)))
|
|
7617
|
+
end
|
|
7618
|
+
end
|
|
7619
|
+
|
|
7620
|
+
# `scope` with a capture the block mutated in place bound to its joined or floored content: a local through
|
|
7621
|
+
# `Scope#with_mutated_local`, which keeps the marks a source-level write drops, and an instance variable, class
|
|
7622
|
+
# variable or global through {CapturedLocals.bind}, which keeps an ivar's optimistic nil-freeness mark. The
|
|
7623
|
+
# binding still names the same object.
|
|
7624
|
+
def bind_mutated_capture(scope, name, type)
|
|
7625
|
+
CapturedLocals.local_name?(name) ? scope.with_mutated_local(name, type) : CapturedLocals.bind(scope, name, type)
|
|
7626
|
+
end
|
|
7627
|
+
|
|
7628
|
+
# `scope` with a content seam's working binding for `name` — its join so far, or `Dynamic[top]` for a read the
|
|
7629
|
+
# body rebinds — whatever kind of variable it is.
|
|
7630
|
+
def bind_capture_slot(scope, name, type)
|
|
7631
|
+
case CapturedLocals.variable_kind(name)
|
|
7632
|
+
when :local then scope.with_local(name, type)
|
|
7633
|
+
when :ivar then scope.with_ivar(name, type)
|
|
7634
|
+
else CapturedLocals.bind(scope, name, type)
|
|
5235
7635
|
end
|
|
5236
7636
|
end
|
|
5237
7637
|
|
|
@@ -5240,19 +7640,29 @@ module Rigor
|
|
|
5240
7640
|
|
|
5241
7641
|
# The {RewriteMutation} names `root` calls on each local its block admits — `a.map!(&:to_s)` beside an `a << x`.
|
|
5242
7642
|
# The receiver test is the one the caller's content-mutation walk applies.
|
|
5243
|
-
def local_rewrites(root)
|
|
7643
|
+
def local_rewrites(root, &)
|
|
5244
7644
|
rewrites = {}
|
|
5245
7645
|
Source::NodeWalker.each_with_ancestors(root) do |node, ancestors|
|
|
5246
7646
|
next unless node.is_a?(Prism::CallNode) && RewriteMutation.rewriter?(node.name)
|
|
5247
7647
|
|
|
5248
7648
|
receiver = node.receiver
|
|
5249
|
-
next unless
|
|
7649
|
+
next unless rewritten_capture_receiver?(receiver, ancestors, &)
|
|
5250
7650
|
|
|
5251
7651
|
(rewrites[receiver.name] ||= []) << node.name
|
|
5252
7652
|
end
|
|
5253
7653
|
rewrites
|
|
5254
7654
|
end
|
|
5255
7655
|
|
|
7656
|
+
# A local the caller's test admits, or an instance variable, class variable or global outside a body that makes
|
|
7657
|
+
# it another variable ({#under_scope_body?}): the names the content walks can collect.
|
|
7658
|
+
def rewritten_capture_receiver?(receiver, ancestors)
|
|
7659
|
+
case receiver
|
|
7660
|
+
when Prism::LocalVariableReadNode then yield(receiver, ancestors)
|
|
7661
|
+
when *NON_LOCAL_READ_NODES then !under_scope_body?(ancestors, CapturedLocals.variable_kind(receiver.name))
|
|
7662
|
+
else false
|
|
7663
|
+
end
|
|
7664
|
+
end
|
|
7665
|
+
|
|
5256
7666
|
# The slice-C join (and the loop seam's) rebuilds a collection from its SEED, so the rewrite the body's own
|
|
5257
7667
|
# widening applied is gone from it: `a = [1]; [0].each { a.map!(&:to_s); a << "x" }` read `Array["x" | 1]`, and
|
|
5258
7668
|
# `a[0] == "1"` folded always-falsey. Each rewrite the body makes on the local is re-applied here, on the seam's
|
|
@@ -5279,10 +7689,14 @@ module Rigor
|
|
|
5279
7689
|
# - joined with the block-entry typing, it still stores exit values no store reads;
|
|
5280
7690
|
# - one more walk of the body to the store inherits every gap in the engine's in-body flow.
|
|
5281
7691
|
#
|
|
7692
|
+
# An instance variable the body rebinds is one too (issue #1235): `@total += x; out << @total` stored the
|
|
7693
|
+
# pre-call `@total`, and so are a class variable and a global (issue #3096). Each counts on the terms the content
|
|
7694
|
+
# walk takes one ({#capturable_non_local?}).
|
|
7695
|
+
#
|
|
5282
7696
|
# See ADR-56 WD2.13.
|
|
5283
|
-
def shadow_rebound_reads(block, sites, seeds, shadows)
|
|
7697
|
+
def shadow_rebound_reads(block, sites, seeds, shadows, call_node)
|
|
5284
7698
|
entry_names = scope.locals.keys | CapturedLocals.introduced_locals(block).to_a
|
|
5285
|
-
written = scope_local_writes(block) & entry_names
|
|
7699
|
+
written = (scope_local_writes(block) & entry_names) | rebound_capturable_non_locals(block, call_node)
|
|
5286
7700
|
return if written.empty?
|
|
5287
7701
|
|
|
5288
7702
|
sites.each do |name, nodes|
|
|
@@ -5296,6 +7710,23 @@ module Rigor
|
|
|
5296
7710
|
end
|
|
5297
7711
|
end
|
|
5298
7712
|
|
|
7713
|
+
# The instance variables, class variables and globals the block body writes that {#capturable_non_local?}
|
|
7714
|
+
# accepts; no instance variable for a block that runs on another object. Every self-rebinding call classifies
|
|
7715
|
+
# `:unknown` today, so neither content seam that calls this meets one; the guard keeps the answer right should
|
|
7716
|
+
# the escape analysis learn to call one non-escaping.
|
|
7717
|
+
def rebound_capturable_non_locals(block, call_node)
|
|
7718
|
+
return NO_REWRITES if block.body.nil?
|
|
7719
|
+
|
|
7720
|
+
foreign = foreign_self_block?(call_node)
|
|
7721
|
+
names = []
|
|
7722
|
+
Source::NodeWalker.each_with_ancestors(block.body) do |node, ancestors|
|
|
7723
|
+
next unless CapturedLocals::NON_LOCAL_WRITE_NODES.include?(node.class)
|
|
7724
|
+
|
|
7725
|
+
names << node.name if capturable_non_local?(node.name, ancestors, scope, foreign_self: foreign)
|
|
7726
|
+
end
|
|
7727
|
+
names.uniq
|
|
7728
|
+
end
|
|
7729
|
+
|
|
5299
7730
|
# The locals a store reads to build what it stores. An Array index write's index arguments are left out, and so
|
|
5300
7731
|
# is any name they read: the join classifies the store as an element or a splice from the index's type, and a
|
|
5301
7732
|
# `Dynamic` index reads as both, so `grid[i] = [x, x]` would join `x` itself as a member of `grid` beside the
|
|
@@ -5334,6 +7765,29 @@ module Rigor
|
|
|
5334
7765
|
SCOPE_BODY_NODES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
5335
7766
|
private_constant :SCOPE_BODY_NODES
|
|
5336
7767
|
|
|
7768
|
+
CLASS_BODY_NODES = [Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
7769
|
+
private_constant :CLASS_BODY_NODES
|
|
7770
|
+
|
|
7771
|
+
# The bodies under a block body where a non-local of each kind is no longer the one the block's caller sees, or
|
|
7772
|
+
# does not run while the block does. A `def` body runs only when called. A class or module body is a class of
|
|
7773
|
+
# its own for an instance variable and a class variable, but runs at once and reads the one global. `class <<
|
|
7774
|
+
# self` keeps the enclosing class's class variables, so only an instance variable leaves there.
|
|
7775
|
+
NON_LOCAL_SCOPE_BODY_NODES = {
|
|
7776
|
+
ivar: SCOPE_BODY_NODES,
|
|
7777
|
+
cvar: [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode].freeze,
|
|
7778
|
+
global: [Prism::DefNode].freeze
|
|
7779
|
+
}.freeze
|
|
7780
|
+
private_constant :NON_LOCAL_SCOPE_BODY_NODES
|
|
7781
|
+
|
|
7782
|
+
# True when a non-local of `kind` (`:ivar`, `:cvar` or `:global`) at `ancestors` under a block body sits in a body
|
|
7783
|
+
# the block defines that makes it another variable or does not run during the block
|
|
7784
|
+
# ({NON_LOCAL_SCOPE_BODY_NODES}): for an instance variable, any method, class or module body, where `self` is that
|
|
7785
|
+
# body's own.
|
|
7786
|
+
def under_scope_body?(ancestors, kind = :ivar)
|
|
7787
|
+
bodies = NON_LOCAL_SCOPE_BODY_NODES.fetch(kind)
|
|
7788
|
+
ancestors.any? { |a| bodies.any? { |k| a.is_a?(k) } }
|
|
7789
|
+
end
|
|
7790
|
+
|
|
5337
7791
|
# True when the local `node` names lives in the scope the walk started in or an outer one.
|
|
5338
7792
|
def same_scope_local?(node, ancestors)
|
|
5339
7793
|
return false if ancestors.any? { |ancestor| SCOPE_BODY_NODES.any? { |klass| ancestor.is_a?(klass) } }
|
|
@@ -5350,16 +7804,37 @@ module Rigor
|
|
|
5350
7804
|
].freeze
|
|
5351
7805
|
private_constant :LOCAL_READ_NODES
|
|
5352
7806
|
|
|
5353
|
-
#
|
|
5354
|
-
|
|
7807
|
+
# The same for an instance variable, a class variable and a global.
|
|
7808
|
+
NON_LOCAL_VALUE_READ_NODES = Set[
|
|
7809
|
+
Prism::InstanceVariableReadNode,
|
|
7810
|
+
Prism::InstanceVariableOperatorWriteNode,
|
|
7811
|
+
Prism::InstanceVariableOrWriteNode,
|
|
7812
|
+
Prism::InstanceVariableAndWriteNode,
|
|
7813
|
+
Prism::ClassVariableReadNode,
|
|
7814
|
+
Prism::ClassVariableOperatorWriteNode,
|
|
7815
|
+
Prism::ClassVariableOrWriteNode,
|
|
7816
|
+
Prism::ClassVariableAndWriteNode,
|
|
7817
|
+
Prism::GlobalVariableReadNode,
|
|
7818
|
+
Prism::GlobalVariableOperatorWriteNode,
|
|
7819
|
+
Prism::GlobalVariableOrWriteNode,
|
|
7820
|
+
Prism::GlobalVariableAndWriteNode
|
|
7821
|
+
].freeze
|
|
7822
|
+
private_constant :NON_LOCAL_VALUE_READ_NODES
|
|
7823
|
+
|
|
7824
|
+
# Every local `node` reads from the scope it sits in, and every instance variable, class variable or global
|
|
7825
|
+
# outside a nested method or class body: a read inside an inner block of a name that block introduces is a
|
|
7826
|
+
# different variable. The caller intersects the answer with the names the body writes, which
|
|
7827
|
+
# {#rebound_capturable_non_locals} has already filtered.
|
|
5355
7828
|
def local_reads(node)
|
|
5356
7829
|
return [] if node.nil?
|
|
5357
7830
|
|
|
5358
7831
|
names = []
|
|
5359
7832
|
Source::NodeWalker.each_with_ancestors(node) do |n, ancestors|
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
7833
|
+
if LOCAL_READ_NODES.any? { |klass| n.is_a?(klass) }
|
|
7834
|
+
names << n.name if same_scope_local?(n, ancestors)
|
|
7835
|
+
elsif NON_LOCAL_VALUE_READ_NODES.include?(n.class)
|
|
7836
|
+
names << n.name unless under_scope_body?(ancestors, CapturedLocals.variable_kind(n.name))
|
|
7837
|
+
end
|
|
5363
7838
|
end
|
|
5364
7839
|
names.uniq
|
|
5365
7840
|
end
|
|
@@ -5445,7 +7920,7 @@ module Rigor
|
|
|
5445
7920
|
|
|
5446
7921
|
def bind_content_joins(scope, kinds, seeds, evidence)
|
|
5447
7922
|
kinds.reduce(scope) do |acc, (name, kind)|
|
|
5448
|
-
acc
|
|
7923
|
+
bind_capture_slot(acc, name, content_entry_binding(seeds[name], kind, name, evidence))
|
|
5449
7924
|
end
|
|
5450
7925
|
end
|
|
5451
7926
|
|
|
@@ -5478,10 +7953,23 @@ module Rigor
|
|
|
5478
7953
|
#
|
|
5479
7954
|
# Each site nested in an inner block or lambda is recorded in `shadows` with the names those blocks bind
|
|
5480
7955
|
# themselves (see {#join_content_to_fixpoint}).
|
|
5481
|
-
|
|
7956
|
+
#
|
|
7957
|
+
# Issue #1208 — an instance variable is a capture too: every block made in a method shares its `self`, so `@out
|
|
7958
|
+
# << x` in the body grows the method's `@out` exactly as `out << x` grows `out`. It counts on the terms
|
|
7959
|
+
# {CapturedLocals.writes} takes a rebound one: bound at the call site and not on its ADR-58 declaration seed
|
|
7960
|
+
# ({CapturedLocals.rebindable_ivar?}), outside a nested method or class body, and not in a block that runs on
|
|
7961
|
+
# another object ({CapturedLocals.under_foreign_self_block?}, or `call_node`'s own block).
|
|
7962
|
+
#
|
|
7963
|
+
# Issue #3096 — a class variable and a global are captures on the same terms, less the foreign-self one: Ruby
|
|
7964
|
+
# resolves a class variable lexically, and a global is the same one whatever `self` is.
|
|
7965
|
+
def captured_content_mutations(block, shadows, call_node)
|
|
5482
7966
|
mutations = Hash.new { |h, k| h[k] = [] }
|
|
7967
|
+
# Every self-rebinding call classifies `:unknown` today, so this seam does not meet one; the guard keeps the
|
|
7968
|
+
# ivar arm right should the escape analysis learn to call one non-escaping.
|
|
7969
|
+
foreign = foreign_self_block?(call_node)
|
|
5483
7970
|
Source::NodeWalker.each_with_ancestors(block.body) do |node, ancestors|
|
|
5484
7971
|
name, site = content_mutation_target(node) { |receiver| receiver.depth > scope_nesting(ancestors) }
|
|
7972
|
+
name, site = non_local_content_mutation_target(node, ancestors, foreign) if name.nil?
|
|
5485
7973
|
next if name.nil?
|
|
5486
7974
|
|
|
5487
7975
|
mutations[name] << site
|
|
@@ -5490,6 +7978,89 @@ module Rigor
|
|
|
5490
7978
|
mutations
|
|
5491
7979
|
end
|
|
5492
7980
|
|
|
7981
|
+
# `[name, node]` when `node` mutates the content of an instance variable of this scope's `self`, a class
|
|
7982
|
+
# variable or a global that the content seams may join ({#captured_content_mutations}), else
|
|
7983
|
+
# {NO_CONTENT_MUTATION}. `foreign_self` says the seam's block runs on another object.
|
|
7984
|
+
def non_local_content_mutation_target(node, ancestors, foreign_self)
|
|
7985
|
+
is_call_mutator = node.is_a?(Prism::CallNode) && CONTENT_MUTATORS.include?(node.name)
|
|
7986
|
+
return NO_CONTENT_MUTATION unless is_call_mutator || index_write?(node)
|
|
7987
|
+
|
|
7988
|
+
receiver = node.receiver
|
|
7989
|
+
return NO_CONTENT_MUTATION unless NON_LOCAL_READ_NODES.include?(receiver.class)
|
|
7990
|
+
return NO_CONTENT_MUTATION unless capturable_non_local?(receiver.name, ancestors, scope, foreign_self:)
|
|
7991
|
+
return NO_CONTENT_MUTATION unless collection_only?(CapturedLocals.bound_type(scope, receiver.name))
|
|
7992
|
+
|
|
7993
|
+
[receiver.name, node]
|
|
7994
|
+
end
|
|
7995
|
+
|
|
7996
|
+
# The receiver reads of a variable the content seams take besides a local.
|
|
7997
|
+
NON_LOCAL_READ_NODES = Set[
|
|
7998
|
+
Prism::InstanceVariableReadNode, Prism::ClassVariableReadNode, Prism::GlobalVariableReadNode
|
|
7999
|
+
].freeze
|
|
8000
|
+
private_constant :NON_LOCAL_READ_NODES
|
|
8001
|
+
|
|
8002
|
+
# True when every member of `type` is an Array, a Hash or a String carrier. The non-escaping join takes an
|
|
8003
|
+
# instance variable only then: it drops a seed's `nil` after a guarded mutation (#1219), so `@buf = flag ? [1] :
|
|
8004
|
+
# nil; [1].each { @buf << 2 if @buf }` would lose the `nil` the runtime keeps when the block never runs it. The
|
|
8005
|
+
# local spelling has the gap; an instance variable, the common lazily-initialised spelling, keeps its pre-call
|
|
8006
|
+
# binding until it closes. The escaping floor takes every instance variable ({#member_content_floor}).
|
|
8007
|
+
def collection_only?(type)
|
|
8008
|
+
return false if type.nil?
|
|
8009
|
+
return type.members.all? { |member| collection_only?(member) } if type.is_a?(Type::Union)
|
|
8010
|
+
|
|
8011
|
+
arrayish?(type) || hashish?(type) || UnknownStoreWidening.carrier_class(type) == "String"
|
|
8012
|
+
end
|
|
8013
|
+
|
|
8014
|
+
# True when the non-local `name`, read or written at `ancestors` under a block body, is one the content seams
|
|
8015
|
+
# take against `in_scope`: bound there ({CapturedLocals.rebindable_non_local?}, which keeps an instance
|
|
8016
|
+
# variable on its declaration seed out) and outside a body the block defines that makes it another variable or
|
|
8017
|
+
# runs later ({#under_scope_body?}).
|
|
8018
|
+
#
|
|
8019
|
+
# - An instance variable must be `self`'s: not under a block that runs on another object, nor in a seam whose
|
|
8020
|
+
# block does (`foreign_self`), nor in any method, class or module body.
|
|
8021
|
+
# - A class variable resolves lexically and a global is global, so `self` does not matter to either (issue
|
|
8022
|
+
# #3096). A `def` body runs only when called, so neither counts there. A class or module body runs at once:
|
|
8023
|
+
# it reads its own class variables but the one global, so a global counts there. `class << self` keeps the
|
|
8024
|
+
# enclosing class's class variables, so a class variable counts there.
|
|
8025
|
+
# - A global Ruby's own signatures declare (`$stdout`, `$;`, `$0`, the frame-local `$~` and `$_`, …) is left
|
|
8026
|
+
# alone: the interpreter owns it, and some of them read through a getter (`$0` answers a frozen copy).
|
|
8027
|
+
def capturable_non_local?(name, ancestors, in_scope, foreign_self: false)
|
|
8028
|
+
return false if under_scope_body?(ancestors, CapturedLocals.variable_kind(name))
|
|
8029
|
+
|
|
8030
|
+
non_local_of_scope?(name, ancestors, in_scope, foreign_self)
|
|
8031
|
+
end
|
|
8032
|
+
|
|
8033
|
+
# Issue #3163 — true when the non-local `name`, mutated at `ancestors` under a block body, sits in a method the
|
|
8034
|
+
# block defines and is one the content floor takes against `in_scope`. {#capturable_non_local?} leaves a `def`
|
|
8035
|
+
# body out because it runs only when called, but the block may call it. The terms are that method's, less the
|
|
8036
|
+
# `def` boundary:
|
|
8037
|
+
#
|
|
8038
|
+
# - A class or module body or a `class << self` between the block and the mutation still leaves every kind out:
|
|
8039
|
+
# a method defined there is another class's, and the walks do not ask who calls it.
|
|
8040
|
+
# - An instance variable in a method defined on an explicit receiver other than `self` (`def other.m`) is that
|
|
8041
|
+
# object's. A plain `def` counts wherever it is written, so one in a class body's block, whose instance
|
|
8042
|
+
# variables are an instance's and not the class's, floors the class's too.
|
|
8043
|
+
def block_defined_method_capture?(name, ancestors, in_scope, foreign_self)
|
|
8044
|
+
defs = ancestors.grep(Prism::DefNode)
|
|
8045
|
+
return false if defs.empty? || ancestors.any? { |a| CLASS_BODY_NODES.any? { |k| a.is_a?(k) } }
|
|
8046
|
+
|
|
8047
|
+
foreign_def = defs.any? { |d| !d.receiver.nil? && !d.receiver.is_a?(Prism::SelfNode) }
|
|
8048
|
+
return false if foreign_def && CapturedLocals.variable_kind(name) == :ivar
|
|
8049
|
+
|
|
8050
|
+
non_local_of_scope?(name, ancestors, in_scope, foreign_self)
|
|
8051
|
+
end
|
|
8052
|
+
|
|
8053
|
+
# The half of {#capturable_non_local?} that does not read the bodies between the block and the non-local.
|
|
8054
|
+
def non_local_of_scope?(name, ancestors, in_scope, foreign_self)
|
|
8055
|
+
case CapturedLocals.variable_kind(name)
|
|
8056
|
+
when :ivar
|
|
8057
|
+
return false if foreign_self || CapturedLocals.under_foreign_self_block?(ancestors, in_scope)
|
|
8058
|
+
when :global
|
|
8059
|
+
return false if in_scope.environment.global_for_name(name, builtin: true)
|
|
8060
|
+
end
|
|
8061
|
+
CapturedLocals.rebindable_non_local?(in_scope, name)
|
|
8062
|
+
end
|
|
8063
|
+
|
|
5493
8064
|
def scope_nesting(ancestors)
|
|
5494
8065
|
ancestors.count { |ancestor| scope_nesting_node?(ancestor) }
|
|
5495
8066
|
end
|
|
@@ -5510,7 +8081,7 @@ module Rigor
|
|
|
5510
8081
|
names = shadows[site]
|
|
5511
8082
|
return scope if names.nil?
|
|
5512
8083
|
|
|
5513
|
-
names.reduce(scope) { |acc, name| acc
|
|
8084
|
+
names.reduce(scope) { |acc, name| bind_capture_slot(acc, name, Type::Combinator.untyped) }
|
|
5514
8085
|
end
|
|
5515
8086
|
|
|
5516
8087
|
# `base` binds every fixed name to its join; the moving names are rebound on each pass.
|
|
@@ -5522,7 +8093,7 @@ module Rigor
|
|
|
5522
8093
|
widen: Type::Combinator.method(:widen_value_pinned),
|
|
5523
8094
|
evaluate_body: lambda do |assumption|
|
|
5524
8095
|
pass_entry = kinds.reduce(base) do |acc, (name, kind)|
|
|
5525
|
-
acc
|
|
8096
|
+
bind_capture_slot(acc, name, content_carrier_under(seeds[name], kind, name, assumption))
|
|
5526
8097
|
end
|
|
5527
8098
|
content_evidence(sites, kinds, pass_entry, shadows)
|
|
5528
8099
|
end
|
|
@@ -5633,15 +8204,23 @@ module Rigor
|
|
|
5633
8204
|
type.nil? || type.is_a?(Type::Bot) ? [] : [type]
|
|
5634
8205
|
end
|
|
5635
8206
|
|
|
8207
|
+
# A joined name is a local, an instance variable (#1208), a class variable or a global (#3096), so a store
|
|
8208
|
+
# reading any of them moves.
|
|
8209
|
+
CAPTURE_READ_NODES = Set[
|
|
8210
|
+
Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode, Prism::ClassVariableReadNode,
|
|
8211
|
+
Prism::GlobalVariableReadNode
|
|
8212
|
+
].freeze
|
|
8213
|
+
private_constant :CAPTURE_READ_NODES
|
|
8214
|
+
|
|
5636
8215
|
def store_arguments_read?(node, names)
|
|
5637
8216
|
arguments = node.arguments
|
|
5638
8217
|
return false if arguments.nil?
|
|
5639
8218
|
|
|
5640
|
-
Source::NodeWalker.each(arguments).any? { |n|
|
|
8219
|
+
Source::NodeWalker.each(arguments).any? { |n| CAPTURE_READ_NODES.include?(n.class) && names.include?(n.name) }
|
|
5641
8220
|
end
|
|
5642
8221
|
|
|
5643
8222
|
# ADR-56 slice C (B3). For `recv.each_with_object(memo) { |x, acc| … }` the return is the memo object after the
|
|
5644
|
-
# block has mutated it through the `acc` alias. Compute the joined memo type the same way captured-
|
|
8223
|
+
# block has mutated it through the `acc` alias. Compute the joined memo type the same way captured-local content
|
|
5645
8224
|
# mutations are joined: pre-state = the memo argument's type, added evidence = the content-mutator args on the
|
|
5646
8225
|
# memo block param. Returns `call_type` unchanged for any other call, a missing block, or a memo whose pre-state
|
|
5647
8226
|
# is not a collection.
|
|
@@ -5660,33 +8239,250 @@ module Rigor
|
|
|
5660
8239
|
body = block.body
|
|
5661
8240
|
return call_type if body.nil?
|
|
5662
8241
|
|
|
8242
|
+
memo_after_block(call_node, call_type, memo_param, body)
|
|
8243
|
+
end
|
|
8244
|
+
|
|
8245
|
+
# {#each_with_object_return}'s answer once the call is known to bind a memo parameter in a block with a body.
|
|
8246
|
+
def memo_after_block(call_node, call_type, memo_param, body)
|
|
8247
|
+
memo_arg = call_node.arguments.arguments.first
|
|
8248
|
+
memo_type = scope.type_of(memo_arg, tracer: tracer)
|
|
8249
|
+
pre_state = lookup_mutated_seed(body, memo_param, memo_type, MEMO_SEED_MUTATORS) do |depth, nesting|
|
|
8250
|
+
depth == nesting
|
|
8251
|
+
end
|
|
8252
|
+
reach = memo_scan_reach(body, memo_param)
|
|
8253
|
+
return content_floor_for(pre_state) || call_type if reach == :escape
|
|
8254
|
+
|
|
5663
8255
|
# The memo alias is a block-local (depth 0) — collect content mutations on it directly rather than via the
|
|
5664
8256
|
# captured-local walk.
|
|
5665
8257
|
shadows = {}.compare_by_identity
|
|
5666
8258
|
calls = body_content_mutations_on(body, memo_param, shadows)
|
|
5667
|
-
|
|
8259
|
+
memo = if calls.empty?
|
|
8260
|
+
# Issue #2411 — a memo the block only reshapes (`m.merge!(s)`, `m.delete(k)`) answers its widened seed.
|
|
8261
|
+
pre_state.equal?(memo_type) ? call_type : pre_state
|
|
8262
|
+
else
|
|
8263
|
+
join_memo_content(call_node, memo_param, calls, pre_state, shadows) || call_type
|
|
8264
|
+
end
|
|
8265
|
+
memo = floor_memo_callee_escapes(body, memo_param, memo)
|
|
8266
|
+
reach == :values ? memo_value_floor(memo) : memo
|
|
8267
|
+
end
|
|
8268
|
+
|
|
8269
|
+
# Issue #2411 — `memo` with its values or elements floored to `Dynamic[top]` and a Hash's keys kept: a reader's
|
|
8270
|
+
# value is an element the block may then mutate where no scan sees it (`h[x] << "s"`, `v = h[:k]; v << "s"`,
|
|
8271
|
+
# `h[:k][:n] = "s"`), so the join's element types are not what the memo holds. A memo whose every value or
|
|
8272
|
+
# element is of a class no method mutates ({MEMO_IMMUTABLE_CLASSES}) keeps its join: `m[k] = m[k] + 1` reads an
|
|
8273
|
+
# Integer, which nothing in the block can change.
|
|
8274
|
+
def memo_value_floor(type)
|
|
8275
|
+
return type if memo_contents_immutable?(type)
|
|
5668
8276
|
|
|
5669
|
-
|
|
5670
|
-
|
|
8277
|
+
case type
|
|
8278
|
+
when Type::Union then Type::Combinator.union(*type.members.map { |member| memo_value_floor(member) })
|
|
8279
|
+
when Type::Nominal
|
|
8280
|
+
return content_floor_for(type) || type unless type.class_name == "Hash" && type.type_args.size == 2
|
|
8281
|
+
|
|
8282
|
+
Type::Combinator.nominal_of("Hash", type_args: [type.type_args.first, Type::Combinator.untyped])
|
|
8283
|
+
else content_floor_for(type) || type
|
|
8284
|
+
end
|
|
8285
|
+
end
|
|
8286
|
+
|
|
8287
|
+
# The classes whose instances no method mutates.
|
|
8288
|
+
MEMO_IMMUTABLE_CLASSES = Set[
|
|
8289
|
+
"Integer", "Float", "Symbol", "NilClass", "TrueClass", "FalseClass", "Rational", "Complex"
|
|
8290
|
+
].freeze
|
|
8291
|
+
private_constant :MEMO_IMMUTABLE_CLASSES
|
|
8292
|
+
|
|
8293
|
+
# Whether every value of a Hash memo, or every element of an Array memo, is of a {MEMO_IMMUTABLE_CLASSES} class.
|
|
8294
|
+
def memo_contents_immutable?(type)
|
|
8295
|
+
case type
|
|
8296
|
+
when Type::Union then type.members.all? { |member| memo_contents_immutable?(member) }
|
|
8297
|
+
when Type::Nominal
|
|
8298
|
+
slot = type.class_name == "Hash" ? 1 : 0
|
|
8299
|
+
%w[Hash Array].include?(type.class_name) && !type.type_args[slot].nil? &&
|
|
8300
|
+
memo_immutable_value?(type.type_args[slot])
|
|
8301
|
+
when Type::Tuple then type.elements.all? { |element| memo_immutable_value?(element) }
|
|
8302
|
+
when Type::HashShape then type.pairs.values.all? { |value| memo_immutable_value?(value) }
|
|
8303
|
+
else false
|
|
8304
|
+
end
|
|
8305
|
+
end
|
|
8306
|
+
|
|
8307
|
+
def memo_immutable_value?(type)
|
|
8308
|
+
case type
|
|
8309
|
+
when Type::Union then type.members.all? { |member| memo_immutable_value?(member) }
|
|
8310
|
+
when Type::Constant then MEMO_IMMUTABLE_CLASSES.include?(type.value.class.name)
|
|
8311
|
+
when Type::Nominal then MEMO_IMMUTABLE_CLASSES.include?(type.class_name)
|
|
8312
|
+
when Type::IntegerRange, Type::FloatRange then true
|
|
8313
|
+
else false
|
|
8314
|
+
end
|
|
8315
|
+
end
|
|
8316
|
+
|
|
8317
|
+
# Issue #2411 — whether the block can change the memo where neither the join nor the callee floor sees it, so
|
|
8318
|
+
# the memo answers its content floor ({#content_floor_for}) instead of the join. Read as an expression, the
|
|
8319
|
+
# join's answer reaches every use of the call, and one that misses a store reports on correct code:
|
|
8320
|
+
# `|x, h| g = h; g["s"] = "v"; h[:a] = 2` joined `Hash[:a, 2]`, and `.fetch("s").upcase` read `2`. Every read
|
|
8321
|
+
# or write of the memo's name in its own scope ({#scope_nesting}) must be one the scan models
|
|
8322
|
+
# ({#memo_read_modelled?}); a rebinding write (`h = {}`) is not, since the stores after it reach another object.
|
|
8323
|
+
# Answers `:escape` for such a read or write, `:values` when the only unmodelled reads are readers whose value
|
|
8324
|
+
# is used ({#memo_value_floor}), and nil when every read is modelled.
|
|
8325
|
+
def memo_scan_reach(body, memo_param)
|
|
8326
|
+
reach = nil
|
|
8327
|
+
Source::NodeWalker.each_with_ancestors(body) do |node, ancestors|
|
|
8328
|
+
next unless MEMO_NAME_NODES.any? { |klass| node.is_a?(klass) } && node.name == memo_param
|
|
8329
|
+
next unless node.depth == scope_nesting(ancestors)
|
|
8330
|
+
|
|
8331
|
+
modelled = node.is_a?(Prism::LocalVariableReadNode) && memo_read_modelled?(node, ancestors)
|
|
8332
|
+
return :escape unless modelled
|
|
8333
|
+
|
|
8334
|
+
reach = :values if modelled == :values
|
|
8335
|
+
end
|
|
8336
|
+
reach
|
|
8337
|
+
end
|
|
8338
|
+
|
|
8339
|
+
# The nodes that read or bind a local by name. `LocalVariableTargetNode` covers a multi-assign and a `rescue =>`.
|
|
8340
|
+
MEMO_NAME_NODES = [
|
|
8341
|
+
Prism::LocalVariableReadNode, Prism::LocalVariableWriteNode, Prism::LocalVariableTargetNode,
|
|
8342
|
+
Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode, Prism::LocalVariableAndWriteNode
|
|
8343
|
+
].freeze
|
|
8344
|
+
private_constant :MEMO_NAME_NODES
|
|
8345
|
+
|
|
8346
|
+
# The memo reads that cannot hand the object to anything the scan does not see. A call on the memo is one when
|
|
8347
|
+
# it takes no block (`tap`, `each` and `then` yield it) and the scan knows the method: one whose value never is
|
|
8348
|
+
# the memo ({MEMO_VALUE_READERS}), or a mutator the join or the seed widening reads ({CONTENT_MUTATORS},
|
|
8349
|
+
# {MEMO_SEED_MUTATORS}) whose value, the memo for `h << x` and `h.merge!(s)`, is discarded
|
|
8350
|
+
# ({#memo_value_discarded?}). Any other name is not: `h.send(:store, :t, "t")` stores what no scan sees. An index
|
|
8351
|
+
# compound write on it is one. So is an argument of a self-call that resolves to a user def, whose census the
|
|
8352
|
+
# callee floor reads ({#floor_memo_callee_escapes}), when the call's value is discarded: `keep(h)["s"] = "v"`
|
|
8353
|
+
# stores through the value. Any other read — `g = h`, `[h]`, `Helper.fill(h, x)`, an unresolved self-call — is
|
|
8354
|
+
# not.
|
|
8355
|
+
def memo_read_modelled?(read, ancestors)
|
|
8356
|
+
parent = ancestors.last
|
|
8357
|
+
if parent.respond_to?(:receiver) && parent.receiver.equal?(read)
|
|
8358
|
+
return true if INDEX_WRITE_NODES.any? { |klass| parent.is_a?(klass) }
|
|
8359
|
+
return false unless parent.is_a?(Prism::CallNode) && parent.block.nil?
|
|
8360
|
+
|
|
8361
|
+
return memo_receiver_call_modelled?(parent, ancestors[0...-1])
|
|
8362
|
+
end
|
|
8363
|
+
memo_self_call_argument?(ancestors)
|
|
8364
|
+
end
|
|
8365
|
+
|
|
8366
|
+
# True, false, or `:values` for a reader whose value is used: the value may be an element the block mutates.
|
|
8367
|
+
# A mutator given a splat or `**` argument is not modelled: the join reads each argument by position, and
|
|
8368
|
+
# `h.store(*kv)` stored a pair it never saw.
|
|
8369
|
+
def memo_receiver_call_modelled?(call, ancestors)
|
|
8370
|
+
mutator = CONTENT_MUTATORS.include?(call.name) || MEMO_SEED_MUTATORS.include?(call.name)
|
|
8371
|
+
return false if mutator && memo_splat_arguments?(call)
|
|
8372
|
+
return memo_value_discarded?(call, ancestors) || :values if MEMO_VALUE_READERS.include?(call.name)
|
|
8373
|
+
return false unless mutator
|
|
8374
|
+
|
|
8375
|
+
memo_value_discarded?(call, ancestors)
|
|
8376
|
+
end
|
|
8377
|
+
|
|
8378
|
+
def memo_splat_arguments?(call)
|
|
8379
|
+
(call.arguments&.arguments || []).any? do |argument|
|
|
8380
|
+
argument.is_a?(Prism::SplatNode) ||
|
|
8381
|
+
(argument.is_a?(Prism::KeywordHashNode) && argument.elements.any?(Prism::AssocSplatNode))
|
|
8382
|
+
end
|
|
8383
|
+
end
|
|
8384
|
+
|
|
8385
|
+
# Methods of Hash and Array whose value is never the receiver itself, so using it hands nothing on.
|
|
8386
|
+
MEMO_VALUE_READERS = Set[
|
|
8387
|
+
:[], :[]=, :store, :fetch, :dig, :key?, :has_key?, :include?, :member?, :key, :value?, :has_value?,
|
|
8388
|
+
:size, :length, :count, :empty?, :any?, :none?, :keys, :values, :values_at, :fetch_values, :first, :last,
|
|
8389
|
+
:delete, :pop, :shift, :to_s, :inspect, :hash, :==, :!=, :eql?, :equal?, :frozen?, :nil?, :is_a?,
|
|
8390
|
+
:kind_of?, :instance_of?, :respond_to?, :class, :join, :sum, :min, :max, :dup, :clone
|
|
8391
|
+
].freeze
|
|
8392
|
+
private_constant :MEMO_VALUE_READERS
|
|
8393
|
+
|
|
8394
|
+
# Iterators whose block's value the call never reads.
|
|
8395
|
+
MEMO_BLOCK_VALUE_DROPPING = Set[
|
|
8396
|
+
:each, :each_with_index, :each_pair, :each_key, :each_value, :each_index, :reverse_each, :each_slice,
|
|
8397
|
+
:each_cons, :each_entry, :each_char, :each_line, :each_byte, :times, :upto, :downto, :step, :loop
|
|
8398
|
+
].freeze
|
|
8399
|
+
private_constant :MEMO_BLOCK_VALUE_DROPPING
|
|
8400
|
+
|
|
8401
|
+
# Whether `node`'s value is dropped: a statement that is not its sequence's last, or the last one of a sequence
|
|
8402
|
+
# whose own value is dropped, up through `if` / `unless` arms, `begin` / `rescue` bodies and parentheses, to the
|
|
8403
|
+
# `each_with_object` block body, whose value the call never answers, or to the body of a nested block whose
|
|
8404
|
+
# call never reads its value ({MEMO_BLOCK_VALUE_DROPPING}: `xs.each { |y| acc << y }`). Any other block's value
|
|
8405
|
+
# may be the memo handed on (`map { acc << y }`).
|
|
8406
|
+
def memo_value_discarded?(node, ancestors)
|
|
8407
|
+
return true if ancestors.empty?
|
|
8408
|
+
|
|
8409
|
+
parent = ancestors.last
|
|
8410
|
+
up = ancestors[0...-1]
|
|
8411
|
+
case parent
|
|
8412
|
+
when Prism::StatementsNode
|
|
8413
|
+
return true unless parent.body.last.equal?(node)
|
|
8414
|
+
|
|
8415
|
+
block = up.last
|
|
8416
|
+
return memo_value_discarded?(parent, up) unless block.is_a?(Prism::BlockNode)
|
|
8417
|
+
|
|
8418
|
+
up[-2].is_a?(Prism::CallNode) && MEMO_BLOCK_VALUE_DROPPING.include?(up[-2].name)
|
|
8419
|
+
when Prism::IfNode, Prism::UnlessNode
|
|
8420
|
+
!parent.predicate.equal?(node) && memo_value_discarded?(parent, up)
|
|
8421
|
+
when Prism::ElseNode, Prism::BeginNode, Prism::RescueNode, Prism::ParenthesesNode
|
|
8422
|
+
memo_value_discarded?(parent, up)
|
|
8423
|
+
when Prism::EnsureNode then true
|
|
8424
|
+
else false
|
|
8425
|
+
end
|
|
8426
|
+
end
|
|
8427
|
+
|
|
8428
|
+
# Whether the read under `ancestors` is a positional or keyword argument of a self-call resolving to a user def.
|
|
8429
|
+
# Its value must be discarded: the callee may return the memo (`def keep(h) = h`) or wrap it.
|
|
8430
|
+
def memo_self_call_argument?(ancestors)
|
|
8431
|
+
depth = ancestors.size - 1
|
|
8432
|
+
depth -= 2 if ancestors[depth].is_a?(Prism::AssocNode) && ancestors[depth - 1].is_a?(Prism::KeywordHashNode)
|
|
8433
|
+
call = ancestors[depth - 1]
|
|
8434
|
+
return false unless ancestors[depth].is_a?(Prism::ArgumentsNode) && call.is_a?(Prism::CallNode)
|
|
8435
|
+
return false unless self_dispatch_call?(call) && memo_value_discarded?(call, ancestors[0...(depth - 1)])
|
|
8436
|
+
|
|
8437
|
+
!resolve_self_callees(call).empty?
|
|
8438
|
+
end
|
|
8439
|
+
|
|
8440
|
+
# Issue #2411 — `memo` floored as a captured local is when the block hands it to a self-call whose callee
|
|
8441
|
+
# mutates it ({#floor_callee_escaped_args_for_call}, the walk {#floor_block_body_callee_captures} makes): the
|
|
8442
|
+
# join reads only the stores in the block, so `|x, out| record(out, x); out[[x, :e]] = 1` joined
|
|
8443
|
+
# `Hash[[Integer, :e], 1]` while `record` stored other keys, and a memo the block only passed on kept its `{}`.
|
|
8444
|
+
# Every read of the memo's name counts, a nested block's parameter shadowing it included: the floor only forgets.
|
|
8445
|
+
# The walk runs on an evaluator whose scope binds the memo, since the floor reads only locals its scope binds.
|
|
8446
|
+
def floor_memo_callee_escapes(body, memo_param, memo)
|
|
8447
|
+
StatementEvaluator.new(scope: scope.with_local(memo_param, memo), tracer: tracer)
|
|
8448
|
+
.callee_floored_local(body, memo_param)
|
|
8449
|
+
end
|
|
8450
|
+
|
|
8451
|
+
protected
|
|
8452
|
+
|
|
8453
|
+
# `name` as the self-calls in `body` leave it, each flooring the arguments its callee mutates
|
|
8454
|
+
# ({#floor_memo_callee_escapes}).
|
|
8455
|
+
def callee_floored_local(body, name)
|
|
8456
|
+
floored = scope
|
|
8457
|
+
each_node_in_caller_block(body, 0) do |node, _depth|
|
|
8458
|
+
floored = floor_callee_escaped_args_for_call(node, floored)
|
|
5671
8459
|
end
|
|
5672
|
-
|
|
5673
|
-
joined || call_type
|
|
8460
|
+
floored.local(name)
|
|
5674
8461
|
end
|
|
5675
8462
|
|
|
8463
|
+
private
|
|
8464
|
+
|
|
5676
8465
|
# `seed` as the {HashLookupMutation} calls `body` makes on `name` leave it. They add no content, so the join
|
|
5677
8466
|
# never sees them as sites, and a seed read before `widen_after_block` is still the closed shape whose known
|
|
5678
8467
|
# values answer every missing key: `b = { a: 1 }; [1].each { b.default = 0; b[:c] = 2 }` read `b[:zz]` as
|
|
5679
8468
|
# `1 | 2`, and so did an `each_with_object({})` memo given a default beside its stores, and a `while` body. The
|
|
5680
8469
|
# block receives a read's `depth` and its enclosing block count, and says whether the read is the variable
|
|
5681
|
-
# `seed` describes. A `def` opens a scope of its own, so nothing under one is.
|
|
5682
|
-
|
|
8470
|
+
# `seed` describes. A `def` opens a scope of its own, so nothing under one is. `mutators` widens the call names
|
|
8471
|
+
# read; the `each_with_object` memo passes {MEMO_SEED_MUTATORS}.
|
|
8472
|
+
def lookup_mutated_seed(body, name, seed, mutators = HashLookupMutation::MUTATORS)
|
|
5683
8473
|
Source::NodeWalker.each_with_ancestors(body) do |node, ancestors|
|
|
5684
|
-
next unless node.is_a?(Prism::CallNode) &&
|
|
8474
|
+
next unless node.is_a?(Prism::CallNode) && mutators.include?(node.name)
|
|
5685
8475
|
next if ancestors.any?(Prism::DefNode)
|
|
5686
8476
|
|
|
5687
8477
|
receiver = node.receiver
|
|
5688
|
-
|
|
5689
|
-
|
|
8478
|
+
case receiver
|
|
8479
|
+
when Prism::LocalVariableReadNode
|
|
8480
|
+
next unless receiver.name == name && yield(receiver.depth, scope_nesting(ancestors))
|
|
8481
|
+
when Prism::InstanceVariableReadNode, Prism::ClassVariableReadNode, Prism::GlobalVariableReadNode
|
|
8482
|
+
next unless receiver.name == name && capturable_non_local?(name, ancestors, scope)
|
|
8483
|
+
else
|
|
8484
|
+
next
|
|
8485
|
+
end
|
|
5690
8486
|
|
|
5691
8487
|
seed = MutationWidening.widen_for_mutator(seed, node.name) || seed
|
|
5692
8488
|
end
|
|
@@ -5699,11 +8495,11 @@ module Rigor
|
|
|
5699
8495
|
# write.
|
|
5700
8496
|
def join_memo_content(call_node, memo_param, calls, pre_state, shadows)
|
|
5701
8497
|
block = call_node.block
|
|
5702
|
-
captured = captured_content_mutations(block, shadows)
|
|
5703
|
-
seeds = captured.keys.to_h { |name| [name,
|
|
8498
|
+
captured = captured_content_mutations(block, shadows, call_node)
|
|
8499
|
+
seeds = captured.keys.to_h { |name| [name, CapturedLocals.bound_type(scope, name)] }
|
|
5704
8500
|
seeds[memo_param] = pre_state
|
|
5705
8501
|
sites = captured.merge(memo_param => calls)
|
|
5706
|
-
shadow_rebound_reads(block, sites, seeds, shadows)
|
|
8502
|
+
shadow_rebound_reads(block, sites, seeds, shadows, call_node)
|
|
5707
8503
|
join_content_to_fixpoint(sites, seeds, build_block_entry_scope(call_node, block), shadows)[memo_param]
|
|
5708
8504
|
end
|
|
5709
8505
|
|
|
@@ -5723,21 +8519,19 @@ module Rigor
|
|
|
5723
8519
|
second.respond_to?(:name) ? second.name : nil
|
|
5724
8520
|
end
|
|
5725
8521
|
|
|
5726
|
-
# Content
|
|
5727
|
-
#
|
|
5728
|
-
#
|
|
8522
|
+
# Content mutations of the block-local `var_name` within `body` — directly, or from a nested block that reaches
|
|
8523
|
+
# it by exactly the scopes it is nested in. A nested block's own parameter of the same name is a different
|
|
8524
|
+
# variable and does not count. The sites are the ones {#captured_content_mutations} reads for a captured local
|
|
8525
|
+
# ({#content_mutation_target}), the index compound writes included: scanning calls alone, `h[x] ||= []` was no
|
|
8526
|
+
# store, and the memo kept its `{}`.
|
|
5729
8527
|
def body_content_mutations_on(body, var_name, shadows)
|
|
5730
8528
|
calls = []
|
|
5731
8529
|
Source::NodeWalker.each_with_ancestors(body) do |descendant, ancestors|
|
|
5732
|
-
|
|
5733
|
-
next unless
|
|
5734
|
-
|
|
5735
|
-
receiver = descendant.receiver
|
|
5736
|
-
next unless receiver.is_a?(Prism::LocalVariableReadNode)
|
|
5737
|
-
next unless receiver.name == var_name && receiver.depth == scope_nesting(ancestors)
|
|
8530
|
+
name, site = content_mutation_target(descendant) { |receiver| receiver.depth == scope_nesting(ancestors) }
|
|
8531
|
+
next unless name == var_name
|
|
5738
8532
|
|
|
5739
|
-
calls <<
|
|
5740
|
-
record_shadows(shadows,
|
|
8533
|
+
calls << site
|
|
8534
|
+
record_shadows(shadows, site, ancestors)
|
|
5741
8535
|
end
|
|
5742
8536
|
calls
|
|
5743
8537
|
end
|
|
@@ -5877,6 +8671,29 @@ module Rigor
|
|
|
5877
8671
|
MutationWidening::HASH_MUTATORS)).freeze
|
|
5878
8672
|
private_constant :CONTENT_MUTATORS
|
|
5879
8673
|
|
|
8674
|
+
# The memo's seed widens on every shape mutator its scan does not read evidence from, as a captured local's
|
|
8675
|
+
# binding does on the block seam ({MutationWidening.widen_after_block}): the {HashLookupMutation} calls, and the
|
|
8676
|
+
# rewriters and removers (`merge!`, `update`, `delete`, `replace`, …) whose added content no join can read. Read
|
|
8677
|
+
# off the memo argument alone, `xs.each_with_object({}) { |s, m| m.merge!(s) }` answered the empty `{}`.
|
|
8678
|
+
MEMO_SEED_MUTATORS = (MutationWidening::SHAPE_MUTATORS - CONTENT_MUTATORS).freeze
|
|
8679
|
+
private_constant :MEMO_SEED_MUTATORS
|
|
8680
|
+
|
|
8681
|
+
# Issue #2296 — the Set adders {CONTENT_MUTATORS} leaves out (`add`, `add?`, `merge`): a scan cannot see the
|
|
8682
|
+
# receiver's class, and `merge` on a Hash returns a new Hash, so the callee census keeps them apart and floors
|
|
8683
|
+
# only a Set argument on them.
|
|
8684
|
+
SET_ONLY_ADDERS = (SetMutation::CONTENT_ADDERS - CONTENT_MUTATORS).freeze
|
|
8685
|
+
private_constant :SET_ONLY_ADDERS
|
|
8686
|
+
|
|
8687
|
+
# Issue #2595 — every name the callee write census ({#callee_write_census}) counts as a write: the shape mutators
|
|
8688
|
+
# the widening reads ({MutationWidening::SHAPE_MUTATORS}) and the content scans' own names. A Set-only adder
|
|
8689
|
+
# stays out: `merge` on a Hash returns a new Hash.
|
|
8690
|
+
CALLEE_WRITE_MUTATORS = (MutationWidening::SHAPE_MUTATORS | CONTENT_MUTATORS).freeze
|
|
8691
|
+
# The tables {#callee_write_census} scans with.
|
|
8692
|
+
CENSUS_TABLES = CalleeWriteCensus::Tables.new(mutators: CALLEE_WRITE_MUTATORS, handled: CONTENT_MUTATORS,
|
|
8693
|
+
fresh: FRESH_RESULTS, copiers: CENSUS_COPIERS,
|
|
8694
|
+
foreign_calls: METHOD_BODY_BLOCK_CALLS)
|
|
8695
|
+
private_constant :CALLEE_WRITE_MUTATORS, :CENSUS_TABLES
|
|
8696
|
+
|
|
5880
8697
|
# The shared "not a content mutation" answer. This predicate runs on every node of every block, loop and
|
|
5881
8698
|
# method body it censuses (~950k calls on the lib self-check) and almost always declines, so a fresh
|
|
5882
8699
|
# `[nil, nil]` per decline was one of the largest allocation sites in the evaluator.
|
|
@@ -6239,7 +9056,8 @@ module Rigor
|
|
|
6239
9056
|
# ({#record_closure_escape_if_any}) and a lambda literal ({#eval_lambda}) — so they cannot drift.
|
|
6240
9057
|
def escaping_closure_captures(closure_node, post_scope, foreign_self: false)
|
|
6241
9058
|
widen_escaping_content_captures(closure_node,
|
|
6242
|
-
drop_captured_narrowing(closure_node, post_scope, foreign_self: foreign_self)
|
|
9059
|
+
drop_captured_narrowing(closure_node, post_scope, foreign_self: foreign_self),
|
|
9060
|
+
foreign_self: foreign_self)
|
|
6243
9061
|
end
|
|
6244
9062
|
|
|
6245
9063
|
# Issue #1402 — whether `call_node`'s block runs on an object that is provably not `self`
|
|
@@ -6249,6 +9067,30 @@ module Rigor
|
|
|
6249
9067
|
InterveningIvarReset.foreign_self_block?(call_node, operand_scope)
|
|
6250
9068
|
end
|
|
6251
9069
|
|
|
9070
|
+
# The scope a block body's `self` marks start from: the statement form's opaque `self` (issue #316), or the
|
|
9071
|
+
# walk's unknown-`self` mark alone (issue #1717) for the value-position block {#evaluate_block_of} enters and for
|
|
9072
|
+
# every block entered beneath it. A scope carrying the unknown-`self` mark without the opaque one comes only from
|
|
9073
|
+
# the index walk ({Scope#with_block_self_unknown}), so it is how a nested block knows it sits under one: the
|
|
9074
|
+
# opaque mark, which declines a cross-file top-level `def`, stays off the whole value-position subtree.
|
|
9075
|
+
#
|
|
9076
|
+
# Issue #2861 — a block whose call provably runs it under the caller's `self` ({SelfPreservingBlock}:
|
|
9077
|
+
# `rows.map { … }`) enters with no opaque mark the caller's scope does not already carry
|
|
9078
|
+
# ({Scope#entering_self_preserving_block}).
|
|
9079
|
+
def block_self_entry(block_node, call_node = nil)
|
|
9080
|
+
walked = block_node.equal?(@value_position_block) || scope.walked_block_self_unknown?
|
|
9081
|
+
return scope.with_block_self_unknown(true) if walked
|
|
9082
|
+
return scope.entering_self_preserving_block if self_preserving_block?(call_node)
|
|
9083
|
+
|
|
9084
|
+
scope.entering_opaque_block
|
|
9085
|
+
end
|
|
9086
|
+
|
|
9087
|
+
def self_preserving_block?(call_node)
|
|
9088
|
+
return false if call_node.nil? || call_node.receiver.nil?
|
|
9089
|
+
|
|
9090
|
+
SelfPreservingBlock.call?(call_node, explicit_receiver_type(call_node), scope,
|
|
9091
|
+
classification: classify_closure_escape(call_node))
|
|
9092
|
+
end
|
|
9093
|
+
|
|
6252
9094
|
# Builds the entry scope for a block body. The block sees the outer scope's locals (Ruby's lexical scoping rule)
|
|
6253
9095
|
# and adds bindings for every named block parameter on top. Parameter types come from the receiving method's RBS
|
|
6254
9096
|
# signature when one is available; the rest default to `Dynamic[Top]`.
|
|
@@ -6263,7 +9105,8 @@ module Rigor
|
|
|
6263
9105
|
# yielding method, not the lexical context, decides what `self` is, and Rigor does not track it.
|
|
6264
9106
|
# Issue #1358 — a body that may run a match reads the match globals an earlier iteration may have rebound
|
|
6265
9107
|
# ({MatchRebinding.block_entry}).
|
|
6266
|
-
entry = MatchRebinding.block_entry(
|
|
9108
|
+
entry = MatchRebinding.block_entry(block_self_entry(block_node, call_node), block_node, call_node)
|
|
9109
|
+
entry = entry.entering_block_without_gradual_marks(block_node)
|
|
6267
9110
|
scope_with_params = BlockParameterBinder.new(expected_param_types: expected).bind_onto(block_node, entry)
|
|
6268
9111
|
# ADR-16 Tier A — a plugin `block_as_methods:` entry that matches `(receiver, name)` narrows the
|
|
6269
9112
|
# body's `self` to the object the DSL `instance_eval`s the block on (`params` on
|
|
@@ -6399,7 +9242,8 @@ module Rigor
|
|
|
6399
9242
|
# def's contract; it binds its parameters as an undeclared method does.
|
|
6400
9243
|
class_path: @class_context.last&.refinement ? nil : current_class_path,
|
|
6401
9244
|
singleton: singleton,
|
|
6402
|
-
source_path: scope.source_path
|
|
9245
|
+
source_path: scope.source_path,
|
|
9246
|
+
scope: scope
|
|
6403
9247
|
)
|
|
6404
9248
|
bindings = binder.bind(def_node)
|
|
6405
9249
|
# ADR-67 WD3 — override an undeclared parameter with its call-site inferred type (precision-additive; an
|
|
@@ -6484,7 +9328,7 @@ module Rigor
|
|
|
6484
9328
|
# True for the `Dynamic[Top]` carrier `MethodParameterBinder` leaves on an undeclared parameter — the only
|
|
6485
9329
|
# bindings ADR-67 WD3 overrides.
|
|
6486
9330
|
def untyped_binding?(type)
|
|
6487
|
-
|
|
9331
|
+
Type::Combinator.untyped?(type)
|
|
6488
9332
|
end
|
|
6489
9333
|
|
|
6490
9334
|
def seed_instance_ivars(body_scope, singleton:)
|
|
@@ -6696,8 +9540,13 @@ module Rigor
|
|
|
6696
9540
|
# nested body, so this handler only ever appends a return that genuinely exits the method currently being
|
|
6697
9541
|
# inferred.
|
|
6698
9542
|
def eval_return(node)
|
|
6699
|
-
|
|
6700
|
-
sink
|
|
9543
|
+
state = typing_state
|
|
9544
|
+
sink = state[RETURN_SINK]
|
|
9545
|
+
if sink
|
|
9546
|
+
type = jump_value_type(node)
|
|
9547
|
+
state.touch(RETURN_SINK)
|
|
9548
|
+
sink.record(node, type, state[PROVISIONAL_RETURNS])
|
|
9549
|
+
end
|
|
6701
9550
|
[Type::Combinator.bot, scope]
|
|
6702
9551
|
end
|
|
6703
9552
|
|
|
@@ -6711,8 +9560,13 @@ module Rigor
|
|
|
6711
9560
|
# dead arm), so the join is flow-sensitive for free: `[1, 2].map { |x| next nil if x.nil?; x.to_s }` keeps its
|
|
6712
9561
|
# exact per-element answer.
|
|
6713
9562
|
def eval_next(node)
|
|
6714
|
-
|
|
6715
|
-
sink
|
|
9563
|
+
state = typing_state
|
|
9564
|
+
sink = state[NEXT_SINK]
|
|
9565
|
+
if sink
|
|
9566
|
+
type = jump_value_type(node)
|
|
9567
|
+
state.touch(NEXT_SINK)
|
|
9568
|
+
sink << [node, type]
|
|
9569
|
+
end
|
|
6716
9570
|
@next_scope_sink << [node, jump_scope(node)] if @next_scope_sink
|
|
6717
9571
|
[Type::Combinator.bot, scope]
|
|
6718
9572
|
end
|
|
@@ -6738,10 +9592,19 @@ module Rigor
|
|
|
6738
9592
|
# break-value sink for `ExpressionTyper#call_dispatch_type_for` to union in. Both sinks are optional and
|
|
6739
9593
|
# independent — a loop body collects scopes while an enclosing call collects values from the same walk.
|
|
6740
9594
|
def eval_break(node)
|
|
6741
|
-
|
|
6742
|
-
sink
|
|
6743
|
-
|
|
6744
|
-
|
|
9595
|
+
state = typing_state
|
|
9596
|
+
sink = state[BREAK_SINK]
|
|
9597
|
+
if sink
|
|
9598
|
+
jump = jump_scope(node)
|
|
9599
|
+
state.touch(BREAK_SINK)
|
|
9600
|
+
sink << [node, jump]
|
|
9601
|
+
end
|
|
9602
|
+
value_sink = state[BREAK_VALUE_SINK]
|
|
9603
|
+
if value_sink
|
|
9604
|
+
type = jump_value_type(node)
|
|
9605
|
+
state.touch(BREAK_VALUE_SINK)
|
|
9606
|
+
value_sink << [node, type]
|
|
9607
|
+
end
|
|
6745
9608
|
[Type::Combinator.bot, scope]
|
|
6746
9609
|
end
|
|
6747
9610
|
|
|
@@ -6847,6 +9710,39 @@ module Rigor
|
|
|
6847
9710
|
branch_type.is_a?(Type::Bot)
|
|
6848
9711
|
end
|
|
6849
9712
|
|
|
9713
|
+
# Issue #1230 — a strict, syntactic-only exit test for the then-exits-beside-an-else arm ({#if_join}). The
|
|
9714
|
+
# branch's last statement is a `return` / `next` / `break`; a receiver-less `raise` / `fail` /
|
|
9715
|
+
# `exit` / `exit!` / `abort` / `throw` whose name no project file defines on any class or module, at the top level
|
|
9716
|
+
# or by `define_method` ({Scope#project_method_name?}); or an if / unless / case with an else whose every arm
|
|
9717
|
+
# certainly exits. A `redo` / `retry` does not: it re-runs a loop body or a `begin` that reads the branch's
|
|
9718
|
+
# writes. Unlike {#branch_terminates?} it reads no type, so a `bot`-typed helper call does not qualify:
|
|
9719
|
+
# the typer types a call to an overridable or Kernel-shadowing project method `bot` (issue #3110).
|
|
9720
|
+
def branch_certainly_exits?(node)
|
|
9721
|
+
case node
|
|
9722
|
+
when Prism::ReturnNode, Prism::NextNode, Prism::BreakNode then true
|
|
9723
|
+
when Prism::CallNode then certain_exit_call?(node)
|
|
9724
|
+
when Prism::StatementsNode then branch_certainly_exits?(node.body.last)
|
|
9725
|
+
when Prism::ParenthesesNode then branch_certainly_exits?(node.body)
|
|
9726
|
+
when Prism::ElseNode then branch_certainly_exits?(node.statements)
|
|
9727
|
+
when Prism::IfNode, Prism::UnlessNode
|
|
9728
|
+
branch_certainly_exits?(node.statements) && branch_certainly_exits?(node_else_branch(node))
|
|
9729
|
+
when Prism::CaseNode, Prism::CaseMatchNode then case_certainly_exits?(node)
|
|
9730
|
+
else false
|
|
9731
|
+
end
|
|
9732
|
+
end
|
|
9733
|
+
|
|
9734
|
+
def certain_exit_call?(node)
|
|
9735
|
+
node.receiver.nil? && CERTAIN_EXIT_NAMES.include?(node.name) && !scope.project_method_name?(node.name)
|
|
9736
|
+
end
|
|
9737
|
+
|
|
9738
|
+
def case_certainly_exits?(node)
|
|
9739
|
+
!node.else_clause.nil? && branch_certainly_exits?(node.else_clause) &&
|
|
9740
|
+
node.conditions.all? { |arm| branch_certainly_exits?(arm.statements) }
|
|
9741
|
+
end
|
|
9742
|
+
|
|
9743
|
+
CERTAIN_EXIT_NAMES = %i[raise fail exit exit! abort throw].freeze
|
|
9744
|
+
private_constant :CERTAIN_EXIT_NAMES
|
|
9745
|
+
|
|
6850
9746
|
def eval_branch_or_nil(branch_node, branch_scope, on_enter: @on_enter)
|
|
6851
9747
|
return [Type::Combinator.constant_of(nil), branch_scope] if branch_node.nil?
|
|
6852
9748
|
|
|
@@ -6899,6 +9795,8 @@ module Rigor
|
|
|
6899
9795
|
scope.with_ivar(ref.name, exception_type).without_ivar_guard(ref.name)
|
|
6900
9796
|
when Prism::GlobalVariableTargetNode
|
|
6901
9797
|
scope.with_global(ref.name, exception_type)
|
|
9798
|
+
when Prism::ClassVariableTargetNode
|
|
9799
|
+
scope.with_cvar(ref.name, exception_type)
|
|
6902
9800
|
when Prism::IndexTargetNode
|
|
6903
9801
|
widen_index_target(ref, exception_type, scope, type_scope: scope)
|
|
6904
9802
|
else
|
|
@@ -6906,20 +9804,11 @@ module Rigor
|
|
|
6906
9804
|
end
|
|
6907
9805
|
end
|
|
6908
9806
|
|
|
6909
|
-
# Derives the exception instance type for a `RescueNode
|
|
6910
|
-
#
|
|
6911
|
-
#
|
|
9807
|
+
# Derives the exception instance type for a `RescueNode` ({ErrorInfo.rescue_exception_type}): `StandardError` for
|
|
9808
|
+
# a bare `rescue`, else the union of the named classes' instances, with `Dynamic[top]` for one that does not
|
|
9809
|
+
# resolve to a `Singleton`.
|
|
6912
9810
|
def rescue_exception_type(rescue_node, scope)
|
|
6913
|
-
|
|
6914
|
-
if exceptions.empty?
|
|
6915
|
-
Type::Combinator.nominal_of("StandardError")
|
|
6916
|
-
else
|
|
6917
|
-
types = exceptions.map do |exc_node|
|
|
6918
|
-
singleton_type, = sub_eval(exc_node, scope)
|
|
6919
|
-
singleton_to_nominal(singleton_type)
|
|
6920
|
-
end
|
|
6921
|
-
Type::Combinator.union(*types)
|
|
6922
|
-
end
|
|
9811
|
+
ErrorInfo.rescue_exception_type(rescue_node) { |exception_node| sub_eval(exception_node, scope).first }
|
|
6923
9812
|
end
|
|
6924
9813
|
|
|
6925
9814
|
# --------------------------------------------------------------- `case/in` pattern variable binding helpers
|
|
@@ -6931,7 +9820,27 @@ module Rigor
|
|
|
6931
9820
|
# `deconstruct` / `deconstruct_keys` dispatch reads for its freshness gate.
|
|
6932
9821
|
def apply_in_pattern_bindings(subject_type, subject_node, pattern, scope)
|
|
6933
9822
|
bindings = collect_in_pattern_bindings(subject_type, pattern, scope, subject_node: subject_node)
|
|
6934
|
-
bindings.reduce(scope) { |s, (name, type)| s.with_local(name, type) }
|
|
9823
|
+
bound = bindings.reduce(scope) { |s, (name, type)| s.with_local(name, type) }
|
|
9824
|
+
marked = subject_node.is_a?(Prism::LocalVariableReadNode) && scope.gradual_narrowing?(subject_node.name)
|
|
9825
|
+
mark_gradual_class_capture(subject_type, pattern, bound, marked: marked)
|
|
9826
|
+
end
|
|
9827
|
+
|
|
9828
|
+
# Issue #2656 — `in C => y` over a `Dynamic` subject binds `y` to `C`'s `Nominal`, as a class guard's edge binds
|
|
9829
|
+
# the subject, and marks it as that edge marks a local ({Narrowing.mark_gradual_local}). Only a capture of
|
|
9830
|
+
# the whole subject by a class or an alternation of classes is marked.
|
|
9831
|
+
def mark_gradual_class_capture(subject_type, pattern, scope, marked: false)
|
|
9832
|
+
return scope unless pattern.is_a?(Prism::CapturePatternNode) && class_capture_value?(pattern.value)
|
|
9833
|
+
|
|
9834
|
+
name = pattern.target.name
|
|
9835
|
+
Narrowing.mark_gradual_local(scope, name, subject_type, scope.local(name), marked: marked)
|
|
9836
|
+
end
|
|
9837
|
+
|
|
9838
|
+
def class_capture_value?(value)
|
|
9839
|
+
case value
|
|
9840
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode then true
|
|
9841
|
+
when Prism::AlternationPatternNode then class_capture_value?(value.left) && class_capture_value?(value.right)
|
|
9842
|
+
else false
|
|
9843
|
+
end
|
|
6935
9844
|
end
|
|
6936
9845
|
|
|
6937
9846
|
# Returns an array of `[Symbol, Rigor::Type]` pairs for every variable captured by `pattern`, typed against the
|
|
@@ -7311,7 +10220,8 @@ module Rigor
|
|
|
7311
10220
|
floor = Type::Combinator.untyped
|
|
7312
10221
|
walks.first.map do |name, _type|
|
|
7313
10222
|
types = walks.map { |walk| walk.assoc(name)&.last }
|
|
7314
|
-
|
|
10223
|
+
floored = types.any? { |type| type.nil? || Type::Combinator.untyped?(type) }
|
|
10224
|
+
[name, floored ? floor : Type::Combinator.union(*types)]
|
|
7315
10225
|
end
|
|
7316
10226
|
end
|
|
7317
10227
|
|