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
|
@@ -10,6 +10,9 @@ require_relative "../source/constant_path"
|
|
|
10
10
|
require_relative "../source/node_children"
|
|
11
11
|
require_relative "../source/node_walker"
|
|
12
12
|
require_relative "../analysis/self_call_resolution_recorder"
|
|
13
|
+
require_relative "../run_scoped_slots"
|
|
14
|
+
require_relative "../typing_state"
|
|
15
|
+
require_relative "../type_of_memo"
|
|
13
16
|
require_relative "block_call_timing"
|
|
14
17
|
require_relative "block_repetition"
|
|
15
18
|
require_relative "project_yielder"
|
|
@@ -19,6 +22,7 @@ require_relative "body_fixpoint"
|
|
|
19
22
|
require_relative "budget_trace"
|
|
20
23
|
require_relative "captured_locals"
|
|
21
24
|
require_relative "closure_escape_analyzer"
|
|
25
|
+
require_relative "self_preserving_block"
|
|
22
26
|
require_relative "receiver_blind_block"
|
|
23
27
|
require_relative "def_node_resolver"
|
|
24
28
|
require_relative "dynamic_origin"
|
|
@@ -40,6 +44,7 @@ require_relative "mutation_widening"
|
|
|
40
44
|
require_relative "narrowing"
|
|
41
45
|
require_relative "optimistic_origin"
|
|
42
46
|
require_relative "receiver_alias"
|
|
47
|
+
require_relative "recursion_rebinding"
|
|
43
48
|
require_relative "repeated_or_writes"
|
|
44
49
|
require_relative "singleton_object_constant"
|
|
45
50
|
require_relative "refined_dispatch"
|
|
@@ -259,17 +264,36 @@ module Rigor
|
|
|
259
264
|
@typing_node = typing_node
|
|
260
265
|
end
|
|
261
266
|
|
|
267
|
+
# ADR-129 WD2 — the calling thread's {TypingState::State}, cached on the typer the first time a slot is read, so
|
|
268
|
+
# a later read is an ivar and an array index rather than a `Thread.current` lookup. A typer lives for one query on
|
|
269
|
+
# one thread; an object kept across threads must not keep one (see {Analysis::IncrementalSession}'s effects
|
|
270
|
+
# evaluator), or it would write into a state whose thread's {RunScopedSlots.release} has already run. A frame
|
|
271
|
+
# install whose block's value is the answer calls {TypingState.current} instead: Rigor's own analysis does not
|
|
272
|
+
# adopt this memoised reader's type at a self-call (#525's self-purity), so the install's answer would read as
|
|
273
|
+
# `Dynamic` through it.
|
|
274
|
+
def typing_state = @typing_state ||= TypingState.current
|
|
275
|
+
|
|
262
276
|
def type_of(node)
|
|
263
277
|
previous = @typing_node
|
|
264
278
|
@typing_node = node
|
|
279
|
+
# ADR-129 G1′: under the audit, the Frame declarations are asserted around every `type_of`.
|
|
280
|
+
audited = MemoAudit.enabled?
|
|
281
|
+
MemoAudit.enter_type_of if audited
|
|
265
282
|
result = if FlowTracer.active?
|
|
266
283
|
FlowTracer.trace_node(node) { untraced_type_of(node) }
|
|
284
|
+
elsif TypeOfMemo.enabled? && @tracer.nil? && @operand_types.nil?
|
|
285
|
+
# ADR-129 stage 3 — the shadow memo (G1); shut under a tracer and for a typer with instance state.
|
|
286
|
+
state = typing_state
|
|
287
|
+
TypeOfMemo.shadow(state, scope, node, callee: !state.stack(INFERENCE_GUARD).empty?) do
|
|
288
|
+
untraced_type_of(node)
|
|
289
|
+
end
|
|
267
290
|
else
|
|
268
291
|
untraced_type_of(node)
|
|
269
292
|
end
|
|
270
293
|
result
|
|
271
294
|
ensure
|
|
272
295
|
@typing_node = previous
|
|
296
|
+
MemoAudit.leave_type_of if audited
|
|
273
297
|
end
|
|
274
298
|
|
|
275
299
|
def untraced_type_of(node)
|
|
@@ -298,8 +322,10 @@ module Rigor
|
|
|
298
322
|
# unchanged before skipping the callee's symbol dependents. nil for an abstract / bodyless def, or when
|
|
299
323
|
# the memo refuses a transient result — the caller treats either as "not provably stable" (keeps the
|
|
300
324
|
# dependents), the conservative direction.
|
|
301
|
-
|
|
302
|
-
|
|
325
|
+
#
|
|
326
|
+
# Issue #1237 — `positionals_unknown:` re-drives a key a splat call observed, binding it the way that call did.
|
|
327
|
+
def return_type_for(def_node, receiver, arg_types, positionals_unknown: false)
|
|
328
|
+
infer_user_method_return(def_node, receiver, arg_types, positionals_unknown: positionals_unknown)
|
|
303
329
|
end
|
|
304
330
|
|
|
305
331
|
# The `receiver[args]` read a compound index write performs before it stores (`c[k] += v` reads `c[k]`),
|
|
@@ -316,7 +342,7 @@ module Rigor
|
|
|
316
342
|
# `name` anyway. Minting a `Prism::CallNode` for the read would hand plugins a node that is not in the
|
|
317
343
|
# tree, through a constructor whose field list changed inside the `prism` range the gemspec accepts.
|
|
318
344
|
def implicit_index_read_type(node, receiver, arg_types)
|
|
319
|
-
return splat_index_read_type(node, receiver, arg_types) if
|
|
345
|
+
return splat_index_read_type(node, receiver, arg_types) if splat_arguments?(node)
|
|
320
346
|
|
|
321
347
|
try_overriding_def_dispatch(node, receiver, arg_types, method_name: :[]) ||
|
|
322
348
|
index_read_dispatch(node, receiver, arg_types) ||
|
|
@@ -341,32 +367,60 @@ module Rigor
|
|
|
341
367
|
recorded = self.class.recorded_inject_accumulator(block, scope)
|
|
342
368
|
return recorded if recorded
|
|
343
369
|
|
|
344
|
-
|
|
370
|
+
# ADR-129 WD1 — a read-through Table's miss: writing another Table from it touches.
|
|
371
|
+
typing_state.memo_miss(INJECT_ACCUMULATOR) { try_block_inject_fold(call_node, receiver, arg_types) }
|
|
345
372
|
self.class.recorded_inject_accumulator(block, scope)
|
|
346
373
|
end
|
|
347
374
|
|
|
348
|
-
|
|
349
|
-
|
|
375
|
+
# ADR-129 WD1 — a Table, and a read-through cache of the computation's own value: a record read back answers
|
|
376
|
+
# what typing the call again from the same scope would compute.
|
|
377
|
+
INJECT_ACCUMULATOR = TypingState.table(
|
|
378
|
+
:inject_accumulator,
|
|
379
|
+
run_scoped: true, read_through: true, entries: TypingState::WeakKeyMapEntries,
|
|
380
|
+
reason: "keyed by block node; a read-through cache of the fold that the call's own typing computes"
|
|
381
|
+
)
|
|
382
|
+
private_constant :INJECT_ACCUMULATOR
|
|
350
383
|
|
|
351
384
|
# The accumulator type {#record_inject_accumulator} stored for `block` when the call was typed from `scope`
|
|
352
|
-
# itself (by identity), else nil.
|
|
385
|
+
# itself (by identity) in the current ADR-55 summary generation (#2767, {LoopValueRecord.generation}), else nil.
|
|
353
386
|
def self.recorded_inject_accumulator(block, scope)
|
|
354
|
-
|
|
355
|
-
slot
|
|
387
|
+
state = TypingState.current
|
|
388
|
+
slot = state[INJECT_ACCUMULATOR]&.[](block)
|
|
389
|
+
state.log_read(INJECT_ACCUMULATOR, block, slot || TypingState::ABSENT)
|
|
390
|
+
slot && slot[0].equal?(scope) && slot[2] == LoopValueRecord.generation ? slot[1] : nil
|
|
356
391
|
end
|
|
357
392
|
|
|
358
393
|
# ADR-89 WD2 — the current run's return memo bucket as `{ def_node => [MemoEntry, …] }` (only entries
|
|
359
394
|
# that carry a call descriptor, i.e. every stored entry). Read by the incremental session right after a
|
|
360
395
|
# recording run to harvest each analyzed callee's observed call keys → return descriptors. Returns an
|
|
361
|
-
# empty hash when no bucket exists (a run that memoised nothing). Class method: the bucket lives
|
|
362
|
-
#
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
396
|
+
# empty hash when no bucket exists (a run that memoised nothing). Class method: the bucket lives in the thread's
|
|
397
|
+
# {TypingState}, independent of any one `ExpressionTyper` instance.
|
|
398
|
+
#
|
|
399
|
+
# `bucket` defaults to the calling thread's current one; `Runner#run` keeps the bucket its run filled
|
|
400
|
+
# ({.current_return_memo}) and passes it, since the run releases the thread's slot when it ends.
|
|
401
|
+
#
|
|
402
|
+
# Issue #3160 — the entries are what the memo STORED. A def one of whose calls it refused to store was observed
|
|
403
|
+
# under a key no entry lists; {.return_memo_refused?} says which.
|
|
404
|
+
def self.harvest_return_memo(bucket = current_return_memo)
|
|
405
|
+
return {} if bucket.nil?
|
|
406
|
+
|
|
407
|
+
bucket.transform_values { |per_def| per_def.except(RETURN_MEMO_REFUSED).values }
|
|
408
|
+
end
|
|
366
409
|
|
|
367
|
-
|
|
410
|
+
# Issue #3160 — whether `bucket` served a result of `def_node` that it refused to store (ADR-84 WD3). That
|
|
411
|
+
# result reached its caller, so the def's stored entries are not every key it was observed under.
|
|
412
|
+
def self.return_memo_refused?(bucket, def_node)
|
|
413
|
+
bucket&.[](def_node)&.key?(RETURN_MEMO_REFUSED) || false
|
|
368
414
|
end
|
|
369
415
|
|
|
416
|
+
# Issue #3160 — the per-def key under which {#consult_and_store_return_memo} notes that it served a result it
|
|
417
|
+
# did not store. No call descriptor is a Symbol, so it never meets a real key.
|
|
418
|
+
RETURN_MEMO_REFUSED = :refused
|
|
419
|
+
private_constant :RETURN_MEMO_REFUSED
|
|
420
|
+
|
|
421
|
+
# The calling thread's return memo bucket (`{ def_node => { descriptor => MemoEntry } }`), or nil.
|
|
422
|
+
def self.current_return_memo = TypingState.current[RETURN_MEMO]&.last
|
|
423
|
+
|
|
370
424
|
# Issue #720 — the value the block passed by the call site currently being re-typed evaluates to, or
|
|
371
425
|
# `nil` outside such a frame. Read by {#type_of_yield}.
|
|
372
426
|
#
|
|
@@ -375,22 +429,19 @@ module Rigor
|
|
|
375
429
|
# inference rebuilds freely. It is installed unconditionally — with `nil` — at every user-method
|
|
376
430
|
# inference frame, so a callee reached WITHOUT a block from inside a yielding body cannot read its
|
|
377
431
|
# caller's block.
|
|
378
|
-
|
|
379
|
-
|
|
432
|
+
YIELD_VALUE = TypingState.frame(
|
|
433
|
+
:yield_value_type, reason: "the caller's block value, installed per frame",
|
|
434
|
+
witness: "spec/fixtures/typing_state_witnesses/yield_value_type.rb"
|
|
435
|
+
)
|
|
436
|
+
private_constant :YIELD_VALUE
|
|
380
437
|
|
|
381
438
|
def self.current_yield_value_type
|
|
382
|
-
|
|
439
|
+
TypingState.current[YIELD_VALUE]
|
|
383
440
|
end
|
|
384
441
|
|
|
385
442
|
# Runs `block` with `type` installed as the frame's yield value, restoring the previous frame on exit.
|
|
386
|
-
def self.with_yield_value_type(type)
|
|
387
|
-
|
|
388
|
-
Thread.current[YIELD_VALUE_KEY] = type
|
|
389
|
-
begin
|
|
390
|
-
yield
|
|
391
|
-
ensure
|
|
392
|
-
Thread.current[YIELD_VALUE_KEY] = previous
|
|
393
|
-
end
|
|
443
|
+
def self.with_yield_value_type(type, &)
|
|
444
|
+
TypingState.current.with(YIELD_VALUE, type, &)
|
|
394
445
|
end
|
|
395
446
|
|
|
396
447
|
# Issue #1125 — the argument types of the call currently being re-typed into the body being walked,
|
|
@@ -398,37 +449,48 @@ module Rigor
|
|
|
398
449
|
# `m` itself was called with, so {#call_arg_types} expands the forwarding node to THIS list instead of
|
|
399
450
|
# the `Dynamic[top]` a bare `Prism::ForwardingArgumentsNode` types as.
|
|
400
451
|
#
|
|
401
|
-
# A thread-local for the same reason the yield value is (see {
|
|
452
|
+
# A thread-local for the same reason the yield value is (see {YIELD_VALUE}): `...` names the
|
|
402
453
|
# frame's caller, not a binding, and the body is walked through scopes the inference rebuilds freely.
|
|
403
454
|
# It is installed unconditionally — with nil — at every user-method inference frame, so a body reached
|
|
404
455
|
# without `...` cannot read its caller's argument list. Because each frame installs its OWN list, a
|
|
405
456
|
# chain `a(...) -> b(...) -> c(...)` threads through: `b`'s frame holds the types `a` expanded and
|
|
406
457
|
# `c`'s holds `b`'s. The list is the callee's call-site `arg_types`, which is what keeps the ADR-84
|
|
407
458
|
# return memo's `(def_node, receiver, arg_types)` key complete for a forwarding def.
|
|
408
|
-
|
|
409
|
-
|
|
459
|
+
FORWARDED_ARGS = TypingState.frame(:forwarded_call_arg_types,
|
|
460
|
+
reason: "the caller's frozen argument list, installed per frame",
|
|
461
|
+
witness: "spec/fixtures/typing_state_witnesses/forwarded_call_arg_types.rb")
|
|
462
|
+
private_constant :FORWARDED_ARGS
|
|
410
463
|
|
|
411
464
|
def self.current_forwarded_arg_types
|
|
412
|
-
|
|
465
|
+
TypingState.current[FORWARDED_ARGS]
|
|
413
466
|
end
|
|
414
467
|
|
|
415
|
-
# Runs `block` with `types` installed as the frame's forwarded argument list, restoring the previous
|
|
468
|
+
# Runs `block` with `types` (frozen) installed as the frame's forwarded argument list, restoring the previous
|
|
416
469
|
# frame on exit.
|
|
417
|
-
def self.with_forwarded_arg_types(types, &
|
|
418
|
-
|
|
419
|
-
Thread.current[FORWARDED_ARGS_KEY] = types
|
|
420
|
-
begin
|
|
421
|
-
block.call
|
|
422
|
-
ensure
|
|
423
|
-
Thread.current[FORWARDED_ARGS_KEY] = previous
|
|
424
|
-
end
|
|
470
|
+
def self.with_forwarded_arg_types(types, &)
|
|
471
|
+
TypingState.current.with(FORWARDED_ARGS, types, &)
|
|
425
472
|
end
|
|
426
473
|
|
|
427
474
|
# The two call-site channels a user-method body may read from its caller — the block a `yield`
|
|
428
475
|
# reaches ({#with_yield_value_type}) and the argument list `...` re-supplies
|
|
429
476
|
# ({#with_forwarded_arg_types}) — installed together, both even when nil, for exactly one frame.
|
|
477
|
+
#
|
|
478
|
+
# The frame also clears the reflective-ivar repeat depth ({ReflectiveIvarWrite.in_repeating_body}): it says
|
|
479
|
+
# the CALLER's loop body is walked once for many runs, while the callee's body starts from its own scope and
|
|
480
|
+
# reads its own write. The depth only widens and the return memo's key has no term for it, so a body first
|
|
481
|
+
# reached from a `for` body stored a wider return that every later caller was served. It clears the caller's
|
|
482
|
+
# jump sinks and retry frames too ({StatementEvaluator.outside_caller_sinks}): a `next`, `break` or raise point
|
|
483
|
+
# in the callee's body is never the caller's, and a recursive re-walk of the caller's own block otherwise
|
|
484
|
+
# recorded the callee's arms into it. The block-body
|
|
485
|
+
# threading suppression is the same kind of caller flag and is NOT cleared here (issue #2782): it is what
|
|
486
|
+
# keeps a fold from nesting through callee frames, and clearing it re-walked unstorable frames
|
|
487
|
+
# exponentially, while keying the memo on it cost a fifth of `lib`'s allocations.
|
|
430
488
|
def self.with_call_site_frame(yield_type, forwarded_arg_types, &)
|
|
431
|
-
|
|
489
|
+
ReflectiveIvarWrite.outside_repeating_body do
|
|
490
|
+
StatementEvaluator.outside_caller_sinks do
|
|
491
|
+
TypingState.current.with_pair(YIELD_VALUE, yield_type, FORWARDED_ARGS, forwarded_arg_types, &)
|
|
492
|
+
end
|
|
493
|
+
end
|
|
432
494
|
end
|
|
433
495
|
|
|
434
496
|
private
|
|
@@ -1052,7 +1114,7 @@ module Rigor
|
|
|
1052
1114
|
# optimistic-carrier decline) and one terminating-arm rule, and cannot drift apart again. Only the
|
|
1053
1115
|
# value is read; the post-scope belongs to whichever walk owns the statement.
|
|
1054
1116
|
def type_of_conditional(node)
|
|
1055
|
-
LoopValueRecord.fetch(node, scope) || scope.evaluate(node, tracer: tracer).first
|
|
1117
|
+
LoopValueRecord.fetch(node, scope) || LoopValueRecord.miss { scope.evaluate(node, tracer: tracer).first }
|
|
1056
1118
|
end
|
|
1057
1119
|
|
|
1058
1120
|
def type_of_else(node)
|
|
@@ -1100,7 +1162,6 @@ module Rigor
|
|
|
1100
1162
|
conditions = when_node.respond_to?(:conditions) ? when_node.conditions : []
|
|
1101
1163
|
body_scope, next_scope = Narrowing.case_when_scopes(node.predicate, conditions, clause_scope)
|
|
1102
1164
|
certainty = case_when_branch_certainty(subject_type, when_node)
|
|
1103
|
-
# :no — drop the branch
|
|
1104
1165
|
candidates << case_arm_type(when_node, body_scope) unless certainty == :no
|
|
1105
1166
|
if certainty == :yes
|
|
1106
1167
|
reached_yes = true
|
|
@@ -1284,7 +1345,7 @@ module Rigor
|
|
|
1284
1345
|
def type_of_loop(node)
|
|
1285
1346
|
return Type::Combinator.constant_of(nil) if break_free_condition_loop?(node)
|
|
1286
1347
|
|
|
1287
|
-
LoopValueRecord.fetch(node, scope) || scope.evaluate(node, tracer: tracer).first
|
|
1348
|
+
LoopValueRecord.fetch(node, scope) || LoopValueRecord.miss { scope.evaluate(node, tracer: tracer).first }
|
|
1288
1349
|
end
|
|
1289
1350
|
|
|
1290
1351
|
def break_free_condition_loop?(node)
|
|
@@ -1321,7 +1382,7 @@ module Rigor
|
|
|
1321
1382
|
# to bare `Nominal[Range]` when no endpoint contributes a typable shape.
|
|
1322
1383
|
def nominal_range_for_endpoints(left_node, right_node)
|
|
1323
1384
|
endpoints = [left_node, right_node].compact.map { |n| range_endpoint_element_type(n) }
|
|
1324
|
-
endpoints.reject! { |t|
|
|
1385
|
+
endpoints.reject! { |t| Type::Combinator.untyped?(t) }
|
|
1325
1386
|
return Type::Combinator.nominal_of("Range") if endpoints.empty?
|
|
1326
1387
|
|
|
1327
1388
|
Type::Combinator.nominal_of("Range", type_args: [Type::Combinator.union(*endpoints)])
|
|
@@ -1519,9 +1580,8 @@ module Rigor
|
|
|
1519
1580
|
|
|
1520
1581
|
# Stable single-hop chain narrowing — `receiver.method` after an `is_a?` / `kind_of?` / `instance_of?`
|
|
1521
1582
|
# predicate established the narrowing on the dominated edge. The call MUST be no-arg + no-block +
|
|
1522
|
-
# rooted at a local-var / ivar read; everything else falls through to the standard dispatcher.
|
|
1523
|
-
#
|
|
1524
|
-
# single-hop scope.
|
|
1583
|
+
# rooted at a local-var / ivar read; everything else falls through to the standard dispatcher. The
|
|
1584
|
+
# single hop is Law-of-Demeter-justified.
|
|
1525
1585
|
def method_chain_narrowing_for(node)
|
|
1526
1586
|
return nil unless node.is_a?(Prism::CallNode)
|
|
1527
1587
|
return nil unless node.block.nil?
|
|
@@ -1558,13 +1618,20 @@ module Rigor
|
|
|
1558
1618
|
# engine walks can enumerate: a plugin's `dynamic_return` answer and the ADR-16 synthetic-method
|
|
1559
1619
|
# index. They are members of `self` exactly as an `attr_reader` is, so a top-level `def` must not
|
|
1560
1620
|
# bind ahead of them either.
|
|
1621
|
+
#
|
|
1622
|
+
# Issue #2861 — a bind the #316 gate declines answers `Dynamic[Top]`, never the RBS method of the same name: the
|
|
1623
|
+
# block's real `self` reaches either a same-named method of its own or the top-level `def`, which shadows
|
|
1624
|
+
# `Kernel#format` and every other `Object` / `Kernel` method on the way. Falling through typed
|
|
1625
|
+
# `format(r).bit_length` against `Kernel#format`'s `String` on code that runs the project's `def format`.
|
|
1561
1626
|
def try_local_def_dispatch(node, receiver, arg_types, block_type = nil)
|
|
1562
|
-
|
|
1563
|
-
|
|
1627
|
+
return nil unless node.receiver.nil?
|
|
1628
|
+
|
|
1629
|
+
local_def = scope.bindable_top_level_def_for(node.name)
|
|
1630
|
+
return declined_top_level_def_answer(node) if local_def.nil?
|
|
1564
1631
|
return nil if self_type_answers?(node.name)
|
|
1565
1632
|
return nil if plugin_member_answers?(node, receiver)
|
|
1566
1633
|
|
|
1567
|
-
local_inference = infer_top_level_user_method(local_def, receiver, arg_types, block_type)
|
|
1634
|
+
local_inference = infer_top_level_user_method(local_def, receiver, node, arg_types, block_type)
|
|
1568
1635
|
return local_inference if local_inference
|
|
1569
1636
|
|
|
1570
1637
|
# The local def matches by name but the inference was disqualified — the parameter shape is too
|
|
@@ -1576,6 +1643,16 @@ module Rigor
|
|
|
1576
1643
|
dynamic_top
|
|
1577
1644
|
end
|
|
1578
1645
|
|
|
1646
|
+
# Issue #2861 — the answer for an implicit-self call whose top-level `def` the #316 gate declined: nil (dispatch
|
|
1647
|
+
# as before) when the project defines no top-level `def` of the name, else `Dynamic[Top]`. The gate declines only
|
|
1648
|
+
# where `self_type` is nil, so no member of `self` ({#self_type_answers?}, {#plugin_member_answers?}) can answer.
|
|
1649
|
+
def declined_top_level_def_answer(node)
|
|
1650
|
+
return nil unless scope.opaque_block_self? && scope.self_type.nil?
|
|
1651
|
+
return nil if scope.top_level_def_for(node.name).nil?
|
|
1652
|
+
|
|
1653
|
+
dynamic_top
|
|
1654
|
+
end
|
|
1655
|
+
|
|
1579
1656
|
# ADR-110 WD1 / #856 — the receiver's own `def` outranks a declaration that only an ANCESTOR carries.
|
|
1580
1657
|
#
|
|
1581
1658
|
# RBS resolves through the fully built method table, so `RbsDispatch` answers a subclass call with a
|
|
@@ -1708,8 +1785,15 @@ module Rigor
|
|
|
1708
1785
|
return true if scope.discovered_method_through_ancestors?(class_name, method_name, :instance)
|
|
1709
1786
|
return true if meta_member?(class_name, method_name)
|
|
1710
1787
|
return true if resolve_user_def_through_ancestors(class_name, method_name)
|
|
1788
|
+
return true if rbs_ancestor_answers?(class_name, method_name)
|
|
1789
|
+
|
|
1790
|
+
# Issue #2968 — a class on the chain defines methods under names no table holds (`attr_accessor(*FLAGS)`, a
|
|
1791
|
+
# splatted `Struct.new(*FIELDS)`), so the name may be one of them, ahead of the top-level `def`.
|
|
1792
|
+
return true if Scope::DiscoveryIndex.open_names_on_chain?(scope, class_name, :instance)
|
|
1711
1793
|
|
|
1712
|
-
|
|
1794
|
+
# Issue #2994 — a class on the chain mixes in a module no table names (`include(*MODS)`, `include helper`), so
|
|
1795
|
+
# the name may be that module's, ahead of the top-level `def`.
|
|
1796
|
+
Scope::DiscoveryIndex.unnamed_mixin_on_chain?(scope, class_name)
|
|
1713
1797
|
end
|
|
1714
1798
|
|
|
1715
1799
|
# The RBS arm of {#instance_self_answers?}. A project class is usually absent from the RBS
|
|
@@ -1736,7 +1820,13 @@ module Rigor
|
|
|
1736
1820
|
return true if scope.discovered_method_through_ancestors?(class_name, method_name, :singleton)
|
|
1737
1821
|
return true unless scope.singleton_def_through_ancestors(class_name, method_name).first.nil?
|
|
1738
1822
|
|
|
1739
|
-
|
|
1823
|
+
declared = safe_rbs_method_definition(class_name, method_name, :singleton)
|
|
1824
|
+
return true if rbs_declared_on_class?(declared, class_name)
|
|
1825
|
+
|
|
1826
|
+
# Issue #2994 — a class object on the chain that a module no table names may extend through a hook (the
|
|
1827
|
+
# `:include_hook` open mark of a class with `include(*MODS)`), a class that `extend(*EMODS)`s, or an extended
|
|
1828
|
+
# module whose own module set is unknown, may answer any name ahead of the top-level `def`.
|
|
1829
|
+
Scope::ResolutionChain.for(scope, class_name.to_s, :singleton, :methods).unnamed_mixin?
|
|
1740
1830
|
end
|
|
1741
1831
|
|
|
1742
1832
|
# A `Struct.new(:a, :b)` / `Data.define(:a, :b)` member accessor. The layouts are a discovery table of
|
|
@@ -1771,7 +1861,164 @@ module Rigor
|
|
|
1771
1861
|
def call_type_for(node)
|
|
1772
1862
|
return safe_navigation_call_type(node) if node.safe_navigation?
|
|
1773
1863
|
|
|
1774
|
-
attribute_write_value(node, call_dispatch_type_for(node))
|
|
1864
|
+
memo_object_value(node, class_test_find_value(node, attribute_write_value(node, call_dispatch_type_for(node))))
|
|
1865
|
+
end
|
|
1866
|
+
|
|
1867
|
+
CLASS_TEST_FINDERS = Set[:find, :detect, :rfind].freeze
|
|
1868
|
+
CLASS_TEST_PREDICATES = Set[:is_a?, :kind_of?, :instance_of?].freeze
|
|
1869
|
+
private_constant :CLASS_TEST_FINDERS, :CLASS_TEST_PREDICATES
|
|
1870
|
+
|
|
1871
|
+
# Issue #2719 — `find` / `detect` / `rfind` whose block is one class test on its one parameter
|
|
1872
|
+
# (`ancestors.rfind { |n| n.is_a?(Prism::DefNode) }`) returns an element that passed the test, or `nil`. The
|
|
1873
|
+
# dispatched `E?` is narrowed by that test, through the same {Narrowing} rule an `if n.is_a?(C)` edge takes, and
|
|
1874
|
+
# keeps its `nil`. Only an answer that already carries `nil` is narrowed, so an untyped receiver's `Dynamic`
|
|
1875
|
+
# stays as it was; a test that leaves no element (`Bot`, a class the element type is disjoint from) keeps the
|
|
1876
|
+
# dispatched answer too, since `nil` alone would be a verdict that the guard never matches. Any other block
|
|
1877
|
+
# shape, an `ifnone` argument, and a block with more than one parameter or statement keep today's answer.
|
|
1878
|
+
def class_test_find_value(node, result)
|
|
1879
|
+
param = class_test_find_parameter(node)
|
|
1880
|
+
return result if param.nil?
|
|
1881
|
+
return result unless result.is_a?(Type::Union) && result.members.any? { |m| nil_constant?(m) }
|
|
1882
|
+
return result unless element_or_nil?(node, result)
|
|
1883
|
+
# A refinement in effect may replace the finder with a body whose answer only happens to read as an element
|
|
1884
|
+
# or nil (`refine(Array) { def find = [2.5, 1, nil].sample }`), so the call keeps its answer.
|
|
1885
|
+
return result unless scope.in_effect_refinements(node).empty?
|
|
1886
|
+
|
|
1887
|
+
truthy, = Narrowing.predicate_scopes(node.block.body.body.first, scope.with_local(param, result))
|
|
1888
|
+
narrowed = truthy.local(param)
|
|
1889
|
+
return result if narrowed.nil?
|
|
1890
|
+
|
|
1891
|
+
non_nil = Narrowing.narrow_non_nil(narrowed)
|
|
1892
|
+
return result if non_nil.is_a?(Type::Bot) || introduces_dynamic?(non_nil, result)
|
|
1893
|
+
|
|
1894
|
+
Type::Combinator.union(non_nil, Type::Combinator.constant_of(nil))
|
|
1895
|
+
end
|
|
1896
|
+
|
|
1897
|
+
def nil_constant?(type) = type.is_a?(Type::Constant) && type.value.nil?
|
|
1898
|
+
|
|
1899
|
+
# True when the dispatched answer is exactly the receiver's element type or nil: the call really answers "an
|
|
1900
|
+
# element the block accepted, or nil". Whatever else wins the dispatch (a refinement, a reopened `Array` or
|
|
1901
|
+
# `Enumerable`, a gem's override) and answers anything else keeps its answer, and so does a receiver whose
|
|
1902
|
+
# element type cannot be read.
|
|
1903
|
+
def element_or_nil?(node, result)
|
|
1904
|
+
type = type_of(node.receiver)
|
|
1905
|
+
members = type.is_a?(Type::Union) ? type.members : [type]
|
|
1906
|
+
elements = members.map { |member| MethodDispatcher::IteratorDispatch.element_type_of(member) }
|
|
1907
|
+
return false if elements.include?(nil)
|
|
1908
|
+
|
|
1909
|
+
result == Type::Combinator.union(*elements, Type::Combinator.constant_of(nil))
|
|
1910
|
+
end
|
|
1911
|
+
|
|
1912
|
+
# A narrowing that cannot order an element's class against the tested one answers `Dynamic[top]` for it (a
|
|
1913
|
+
# project class with no RBS, typically); that is the test's "cannot tell", not a narrower element, so the
|
|
1914
|
+
# dispatched answer stands.
|
|
1915
|
+
def introduces_dynamic?(narrowed, result)
|
|
1916
|
+
members = narrowed.is_a?(Type::Union) ? narrowed.members : [narrowed]
|
|
1917
|
+
members.any? { |member| member.is_a?(Type::Dynamic) && !result.members.include?(member) }
|
|
1918
|
+
end
|
|
1919
|
+
|
|
1920
|
+
# The block parameter's name when `node` is a block-only `find` / `detect` / `rfind` whose block takes exactly
|
|
1921
|
+
# one plain parameter and whose body is one `is_a?` / `kind_of?` / `instance_of?` call on that parameter with a
|
|
1922
|
+
# single constant argument; nil otherwise.
|
|
1923
|
+
def class_test_find_parameter(node)
|
|
1924
|
+
# `detect(ifnone)` answers `ifnone.call` when nothing matches, which is not an element. A guard: today the
|
|
1925
|
+
# dispatched answer of an argument-bearing `detect` never carries the `nil` the narrowing needs, so the
|
|
1926
|
+
# call would already keep its answer.
|
|
1927
|
+
return nil unless CLASS_TEST_FINDERS.include?(node.name) && node.arguments.nil?
|
|
1928
|
+
|
|
1929
|
+
block = node.block
|
|
1930
|
+
return nil unless block.is_a?(Prism::BlockNode)
|
|
1931
|
+
|
|
1932
|
+
name = single_block_parameter(block)
|
|
1933
|
+
return nil if name.nil?
|
|
1934
|
+
|
|
1935
|
+
body = block.body
|
|
1936
|
+
return nil unless body.is_a?(Prism::StatementsNode) && body.body.size == 1
|
|
1937
|
+
|
|
1938
|
+
return nil unless class_test_on?(body.body.first, name)
|
|
1939
|
+
|
|
1940
|
+
core_finder_call?(node) ? name : nil
|
|
1941
|
+
end
|
|
1942
|
+
|
|
1943
|
+
# The owners whose `find` / `detect` / `rfind` return an element the block accepted: core `Array` and
|
|
1944
|
+
# `Enumerable` (`Hash`, `Set` and `Range` take `Enumerable`'s).
|
|
1945
|
+
CORE_FINDER_OWNERS = %w[Array Enumerable].freeze
|
|
1946
|
+
private_constant :CORE_FINDER_OWNERS
|
|
1947
|
+
|
|
1948
|
+
# True when every member of the call's receiver dispatches `node.name` to core `Array` or `Enumerable`: the
|
|
1949
|
+
# method's RBS owner is one of {CORE_FINDER_OWNERS} and the project defines no method by that name on the
|
|
1950
|
+
# class or an ancestor. A project or gem `find` may return anything its block did not test
|
|
1951
|
+
# (`def find = yield(1) ? 2.0 : nil`), so the name alone is not enough. {#element_or_nil?} closes the spellings
|
|
1952
|
+
# this cannot see (a refinement, a reopened module the discovery walk does not reach).
|
|
1953
|
+
def core_finder_call?(node)
|
|
1954
|
+
receiver = node.receiver
|
|
1955
|
+
return false if receiver.nil?
|
|
1956
|
+
|
|
1957
|
+
type = type_of(receiver)
|
|
1958
|
+
members = type.is_a?(Type::Union) ? type.members : [type]
|
|
1959
|
+
members.all? { |member| core_finder_member?(member, node.name) }
|
|
1960
|
+
end
|
|
1961
|
+
|
|
1962
|
+
def core_finder_member?(member, method_name)
|
|
1963
|
+
class_name =
|
|
1964
|
+
case member
|
|
1965
|
+
when Type::Nominal then member.class_name
|
|
1966
|
+
when Type::Tuple then "Array"
|
|
1967
|
+
when Type::HashShape then "Hash"
|
|
1968
|
+
end
|
|
1969
|
+
return false if class_name.nil?
|
|
1970
|
+
return false if scope.discovered_method_through_ancestors?(class_name, method_name, :instance)
|
|
1971
|
+
|
|
1972
|
+
defined_in = safe_rbs_method_definition(class_name, method_name, :instance)&.defined_in
|
|
1973
|
+
return false if defined_in.nil?
|
|
1974
|
+
|
|
1975
|
+
owner = defined_in.to_s.delete_prefix("::")
|
|
1976
|
+
return false unless CORE_FINDER_OWNERS.include?(owner)
|
|
1977
|
+
|
|
1978
|
+
# A reopened core owner (`module Enumerable; def find = 2.5; end`) replaces the method for every includer,
|
|
1979
|
+
# which the ancestor walk above does not reach for a class whose ancestry RBS alone records (`Set`).
|
|
1980
|
+
!scope.discovered_method?(owner, method_name, :instance)
|
|
1981
|
+
end
|
|
1982
|
+
|
|
1983
|
+
def single_block_parameter(block)
|
|
1984
|
+
params = block.parameters
|
|
1985
|
+
return nil unless params.is_a?(Prism::BlockParametersNode) && params.locals.empty?
|
|
1986
|
+
|
|
1987
|
+
list = params.parameters
|
|
1988
|
+
return nil if list.nil? || list.requireds.size != 1
|
|
1989
|
+
return nil unless list.optionals.empty? && list.rest.nil? && list.posts.empty? &&
|
|
1990
|
+
list.keywords.empty? && list.keyword_rest.nil? && list.block.nil?
|
|
1991
|
+
|
|
1992
|
+
required = list.requireds.first
|
|
1993
|
+
required.is_a?(Prism::RequiredParameterNode) ? required.name : nil
|
|
1994
|
+
end
|
|
1995
|
+
|
|
1996
|
+
def class_test_on?(test, name)
|
|
1997
|
+
return false unless test.is_a?(Prism::CallNode) && CLASS_TEST_PREDICATES.include?(test.name)
|
|
1998
|
+
return false if test.block || test.safe_navigation?
|
|
1999
|
+
|
|
2000
|
+
receiver = test.receiver
|
|
2001
|
+
return false unless receiver.is_a?(Prism::LocalVariableReadNode) && receiver.name == name
|
|
2002
|
+
|
|
2003
|
+
args = test.arguments&.arguments
|
|
2004
|
+
!args.nil? && args.size == 1 &&
|
|
2005
|
+
(args.first.is_a?(Prism::ConstantReadNode) || args.first.is_a?(Prism::ConstantPathNode))
|
|
2006
|
+
end
|
|
2007
|
+
|
|
2008
|
+
# Issue #2411 — `recv.each_with_object(memo) { |x, acc| acc[k] = v }` returns the memo as the block's
|
|
2009
|
+
# stores leave it. The statement evaluator owns that join ({StatementEvaluator#each_with_object_value}),
|
|
2010
|
+
# and reading it here gives every value position the statement's answer: a method's tail is typed through
|
|
2011
|
+
# this typer, and read off the dispatch alone it was the memo argument's literal `{}` / `[]`. A `break` ends
|
|
2012
|
+
# the call with its own value, so the block's break arms ({#call_break_arm_types}) join the memo: the join
|
|
2013
|
+
# alone read `break :stop if x == 2; a << x` as `Array[1 | 2]`, where Ruby answers `:stop`.
|
|
2014
|
+
def memo_object_value(node, result)
|
|
2015
|
+
return result unless node.name == :each_with_object && node.block.is_a?(Prism::BlockNode)
|
|
2016
|
+
|
|
2017
|
+
memo = StatementEvaluator.new(scope: scope, tracer: tracer).each_with_object_value(node, result)
|
|
2018
|
+
return memo if memo.equal?(result)
|
|
2019
|
+
|
|
2020
|
+
breaks = call_break_arm_types(node)
|
|
2021
|
+
breaks.empty? ? memo : Type::Combinator.union(memo, *breaks)
|
|
1775
2022
|
end
|
|
1776
2023
|
|
|
1777
2024
|
# The RHS-value override for plain attribute / index writes (#520); a non-write call keeps the
|
|
@@ -1918,7 +2165,7 @@ module Rigor
|
|
|
1918
2165
|
return type unless type.is_a?(Type::Constant) && [true, false].include?(type.value)
|
|
1919
2166
|
# The carrier read itself keeps its declared answer even when that is a literal `true` /
|
|
1920
2167
|
# `false` — the mark says its nil-freeness is a bet, not that the value's class widened.
|
|
1921
|
-
return type if scope.
|
|
2168
|
+
return type if scope.optimistic_origin(node)
|
|
1922
2169
|
return type if OptimisticOrigin.resolve(node, scope).nil?
|
|
1923
2170
|
|
|
1924
2171
|
# Widen only toward what a miss really answers: a safe-navigation call answers `nil`, and
|
|
@@ -2048,7 +2295,7 @@ module Rigor
|
|
|
2048
2295
|
return dynamic_top if name && refined_name_for?(receiver, name, list)
|
|
2049
2296
|
|
|
2050
2297
|
block_name = symbol_block_argument(node)
|
|
2051
|
-
dynamic_top if block_name && refined_anywhere?(block_name, list)
|
|
2298
|
+
dynamic_top if block_name && RefinedDispatch.refined_anywhere?(scope, block_name, list)
|
|
2052
2299
|
end
|
|
2053
2300
|
|
|
2054
2301
|
REFINED_NAMING_CALLS = Set[:send, :__send__, :public_send, :respond_to?, :method, :public_method].freeze
|
|
@@ -2084,17 +2331,6 @@ module Rigor
|
|
|
2084
2331
|
end
|
|
2085
2332
|
end
|
|
2086
2333
|
|
|
2087
|
-
# ADR-121 WD7 — a names-wildcard row counts as a refinement of every name.
|
|
2088
|
-
def refined_anywhere?(name, list)
|
|
2089
|
-
refinements = scope.discovered_refinements
|
|
2090
|
-
wildcard = Scope::DiscoveryIndex::REFINEMENT_WILDCARD
|
|
2091
|
-
return true if list.include?(InEffectRefinements::UNKNOWN) &&
|
|
2092
|
-
refinements.any? { |_refined, methods| methods.key?(name) || methods.key?(wildcard) }
|
|
2093
|
-
|
|
2094
|
-
!RefinedDispatch.targets(refinements, name, list).nil? ||
|
|
2095
|
-
!RefinedDispatch.targets(refinements, wildcard, list).nil?
|
|
2096
|
-
end
|
|
2097
|
-
|
|
2098
2334
|
# ADR-121 WD7 — does the call's lexical list (a plugin-declared module is the plugin's declaration and never
|
|
2099
2335
|
# opaque) hold an opaque module? `list` is the full list, which is the lexical one when nothing is declared.
|
|
2100
2336
|
def opaque_refinement_span?(node, list)
|
|
@@ -2132,7 +2368,7 @@ module Rigor
|
|
|
2132
2368
|
return nil if winner.nil?
|
|
2133
2369
|
return dynamic_top if winner == RefinedDispatch::UNKNOWN
|
|
2134
2370
|
|
|
2135
|
-
refined_body_return(winner, node
|
|
2371
|
+
refined_body_return(winner, node, Type::Combinator.nominal_of(class_name), arg_types, block_type) ||
|
|
2136
2372
|
dynamic_top
|
|
2137
2373
|
end
|
|
2138
2374
|
|
|
@@ -2151,15 +2387,17 @@ module Rigor
|
|
|
2151
2387
|
#
|
|
2152
2388
|
# A body another file wrote is re-typed with that file's in-effect refinements at the `def` (its own module
|
|
2153
2389
|
# among them), so a sibling override it calls answers from its refine body too.
|
|
2154
|
-
def refined_body_return(winner,
|
|
2390
|
+
def refined_body_return(winner, call_node, self_type, arg_types, block_type)
|
|
2155
2391
|
def_node, foreign =
|
|
2156
|
-
InEffectRefinements.refinement_def_with_query(scope, winner.module_name, winner.refined_class,
|
|
2392
|
+
InEffectRefinements.refinement_def_with_query(scope, winner.module_name, winner.refined_class, call_node.name)
|
|
2157
2393
|
return nil if def_node.nil?
|
|
2158
2394
|
|
|
2159
2395
|
refinements = foreign&.at(def_node.location.start_offset) do |name|
|
|
2160
2396
|
InEffectRefinements.activated_modules(scope, name)
|
|
2161
2397
|
end
|
|
2162
|
-
|
|
2398
|
+
arg_types, positionals_unknown = positional_correspondence(call_node, arg_types)
|
|
2399
|
+
infer_user_method_return(def_node, self_type, arg_types, yield_type: block_type, refinements: refinements,
|
|
2400
|
+
positionals_unknown: positionals_unknown)
|
|
2163
2401
|
rescue StandardError
|
|
2164
2402
|
nil
|
|
2165
2403
|
end
|
|
@@ -2179,6 +2417,151 @@ module Rigor
|
|
|
2179
2417
|
end
|
|
2180
2418
|
end
|
|
2181
2419
|
|
|
2420
|
+
# Issue #1237 — whether the call's argument list leaves the positional correspondence unknown: it holds a
|
|
2421
|
+
# `*splat`, or a `...` with no forwarding frame to expand it from. `call_arg_types` carries either as ONE
|
|
2422
|
+
# argument, so a body re-typed from that list bound it to one parameter: `def count(*keys) = keys.size`
|
|
2423
|
+
# called as `r.count(*[0, 1])` folded to `1` and drew an always-falsey `== 2`. A Tuple splat of known
|
|
2424
|
+
# arity is not expanded either.
|
|
2425
|
+
def splat_arguments?(node)
|
|
2426
|
+
arguments = node.arguments&.arguments
|
|
2427
|
+
return false if arguments.nil?
|
|
2428
|
+
|
|
2429
|
+
arguments.any?(Prism::SplatNode) ||
|
|
2430
|
+
(forwards_arguments?(arguments) && ExpressionTyper.current_forwarded_arg_types.nil?)
|
|
2431
|
+
end
|
|
2432
|
+
|
|
2433
|
+
# The argument list and the `positionals_unknown:` flag a body tier re-types a user `def` with. Under
|
|
2434
|
+
# {#splat_arguments?} the body is still re-typed, so a body that always raises still answers `bot` and a
|
|
2435
|
+
# return that does not read its arguments is kept, but with no positional correspondence
|
|
2436
|
+
# ({#bind_params_without_correspondence}): the list is the call's keyword shape alone. Only a trailing
|
|
2437
|
+
# keyword-hash argument the call writes and {#call_arg_type} types as a `HashShape` supplies one. Any
|
|
2438
|
+
# other call leaves the keywords unknown (an open empty shape), because a splatted array's last element
|
|
2439
|
+
# is passed as keywords when it is a `ruby2_keywords` hash.
|
|
2440
|
+
#
|
|
2441
|
+
# Issue #3157 — a call whose every splat {#expand_tuple_splats} can expand is bound as the equivalent
|
|
2442
|
+
# positional call. Issue #3099 — a literal `**{}` passes no argument and is dropped. Issue #3191 — any other
|
|
2443
|
+
# trailing keyword hash holding a double splat is bound with no positional correspondence unless it is typed
|
|
2444
|
+
# as a non-empty closed shape ({#unknown_double_splat?}).
|
|
2445
|
+
def positional_correspondence(call_node, arg_types)
|
|
2446
|
+
arguments = call_node.arguments&.arguments
|
|
2447
|
+
unless splat_arguments?(call_node)
|
|
2448
|
+
return [arg_types[0...-1], false] if literal_empty_double_splat?(arguments&.last)
|
|
2449
|
+
return [[UNKNOWN_KEYWORDS], true] if unknown_double_splat?(arguments&.last, arg_types.last)
|
|
2450
|
+
|
|
2451
|
+
return [arg_types, false]
|
|
2452
|
+
end
|
|
2453
|
+
|
|
2454
|
+
expanded = expand_tuple_splats(arguments, arg_types)
|
|
2455
|
+
return [expanded, false] if expanded
|
|
2456
|
+
|
|
2457
|
+
last = arguments.last
|
|
2458
|
+
keywords = arg_types.last if last.is_a?(Prism::KeywordHashNode) && keyword_shape?(arg_types.last)
|
|
2459
|
+
[[keywords || UNKNOWN_KEYWORDS], true]
|
|
2460
|
+
end
|
|
2461
|
+
|
|
2462
|
+
UNKNOWN_KEYWORDS = Type::Combinator.hash_shape_of({}, extra_keys: :open)
|
|
2463
|
+
private_constant :UNKNOWN_KEYWORDS
|
|
2464
|
+
|
|
2465
|
+
# Issue #3099 — whether `argument` is a keyword hash built only of literal empty double splats (`**{}`),
|
|
2466
|
+
# which Ruby passes as no argument at all.
|
|
2467
|
+
def literal_empty_double_splat?(argument)
|
|
2468
|
+
argument.is_a?(Prism::KeywordHashNode) &&
|
|
2469
|
+
argument.elements.all? do |element|
|
|
2470
|
+
element.is_a?(Prism::AssocSplatNode) && element.value.is_a?(Prism::HashNode) &&
|
|
2471
|
+
element.value.elements.empty?
|
|
2472
|
+
end
|
|
2473
|
+
end
|
|
2474
|
+
|
|
2475
|
+
# Issues #3099 / #3191 — whether `argument` is a keyword hash holding a double splat whose type (`type`) is
|
|
2476
|
+
# not a non-empty closed shape. Ruby passes such a hash as a trailing positional `Hash` to a method that
|
|
2477
|
+
# declares no keywords when it is non-empty, and passes nothing when it is empty, so the call does not say
|
|
2478
|
+
# how many positionals the callee receives. An empty closed shape (`**h`, `h = {}`) may be a stale read of
|
|
2479
|
+
# a hash a callee filled; an open or shapeless one (`**opts`), a mixed `k: 1, **h`, two splats `**h, **h`
|
|
2480
|
+
# and `**nil` are not typed as a shape at all ({#double_splat_hash_shape}). Bound as given, each left a
|
|
2481
|
+
# positional `Hash`: `def count(*keys) = keys.size` called `r.count(1, 2, **opts)` folded to `3`.
|
|
2482
|
+
def unknown_double_splat?(argument, type)
|
|
2483
|
+
argument.is_a?(Prism::KeywordHashNode) && argument.elements.any?(Prism::AssocSplatNode) &&
|
|
2484
|
+
!keyword_shape?(type)
|
|
2485
|
+
end
|
|
2486
|
+
|
|
2487
|
+
# Issue #3157 — the argument list with each `*splat` replaced by its elements, or nil to keep the
|
|
2488
|
+
# no-correspondence binding. It expands only when every splat's operand is an array literal, so its arity is
|
|
2489
|
+
# syntactic, and every element is {#provably_not_hash?}: a splatted array's last element is passed as
|
|
2490
|
+
# keywords when it is a `ruby2_keywords` hash. A splat of a local or any other expression is not expanded,
|
|
2491
|
+
# because a `Tuple` read there may be a stale fold of an array that has since grown or shrunk. A trailing
|
|
2492
|
+
# keyword hash must be a literal `**{}` (dropped) or a non-empty shape; an open, mixed or empty non-literal
|
|
2493
|
+
# one leaves the keywords unknown. A list holding `...` is not expanded: its argument types do not line up
|
|
2494
|
+
# with its argument nodes. The splat's own entry in `arg_types` is `Dynamic[top]` ({#type_of_non_value}),
|
|
2495
|
+
# so the literal's elements are typed here.
|
|
2496
|
+
#
|
|
2497
|
+
# Only a call outside every re-typed body frame expands (the main walk's call sites). Inside a frame a rest
|
|
2498
|
+
# parameter holds the caller's arguments, so a chain `m1(*a) = m2(*[a, 1]) + m2(*[a, a])` handed each level
|
|
2499
|
+
# a deeper element type and walked every combination of every level
|
|
2500
|
+
# (spec/rigor/inference/splat_chain_growth_spec.rb).
|
|
2501
|
+
def expand_tuple_splats(arguments, arg_types)
|
|
2502
|
+
return nil if forwards_arguments?(arguments)
|
|
2503
|
+
return nil unless typing_state.stack(INFERENCE_GUARD).empty?
|
|
2504
|
+
|
|
2505
|
+
last = arguments.last
|
|
2506
|
+
trailing_keywords = last.is_a?(Prism::KeywordHashNode)
|
|
2507
|
+
return nil if trailing_keywords && !literal_empty_double_splat?(last) && !keyword_shape?(arg_types.last)
|
|
2508
|
+
|
|
2509
|
+
expanded = []
|
|
2510
|
+
arguments.each_with_index do |argument, index|
|
|
2511
|
+
next if argument.equal?(last) && trailing_keywords && literal_empty_double_splat?(last)
|
|
2512
|
+
next expanded << arg_types[index] unless argument.is_a?(Prism::SplatNode)
|
|
2513
|
+
|
|
2514
|
+
elements = literal_splat_elements(argument)
|
|
2515
|
+
return nil if elements.nil?
|
|
2516
|
+
|
|
2517
|
+
expanded.concat(elements)
|
|
2518
|
+
end
|
|
2519
|
+
expanded
|
|
2520
|
+
end
|
|
2521
|
+
|
|
2522
|
+
# Whether a trailing keyword-hash argument's type is a shape with at least one pair. {#call_arg_type} types a
|
|
2523
|
+
# keyword hash as a shape only when it is closed.
|
|
2524
|
+
def keyword_shape?(type)
|
|
2525
|
+
type.is_a?(Type::HashShape) && !type.pairs.empty?
|
|
2526
|
+
end
|
|
2527
|
+
|
|
2528
|
+
# The element types of `*[a, b]`, each typed on its own, or nil unless the operand is an array literal whose
|
|
2529
|
+
# every element is {#provably_not_hash?}. A splat element (`*[*t]`) types `Dynamic[top]`
|
|
2530
|
+
# ({#type_of_non_value}), so it declines too.
|
|
2531
|
+
def literal_splat_elements(splat)
|
|
2532
|
+
literal = unparenthesized(splat.expression)
|
|
2533
|
+
return nil unless literal.is_a?(Prism::ArrayNode)
|
|
2534
|
+
|
|
2535
|
+
types = literal.elements.map { |element| type_of(element) }
|
|
2536
|
+
types if types.all? { |type| provably_not_hash?(type) }
|
|
2537
|
+
end
|
|
2538
|
+
|
|
2539
|
+
# Classes no instance of which is a `Hash`. A `Nominal` of any other class may be a `Hash` subclass.
|
|
2540
|
+
NON_HASH_CLASSES = %w[
|
|
2541
|
+
Integer Float Rational Complex String Symbol Array Range Regexp NilClass TrueClass FalseClass Proc
|
|
2542
|
+
].to_set.freeze
|
|
2543
|
+
private_constant :NON_HASH_CLASSES
|
|
2544
|
+
|
|
2545
|
+
# `*([1, 2])`'s array literal, or the node itself when it is not a parenthesised single expression.
|
|
2546
|
+
def unparenthesized(node)
|
|
2547
|
+
return node unless node.is_a?(Prism::ParenthesesNode) && node.body.is_a?(Prism::StatementsNode)
|
|
2548
|
+
|
|
2549
|
+
statements = node.body.body
|
|
2550
|
+
statements.size == 1 ? statements.first : node
|
|
2551
|
+
end
|
|
2552
|
+
|
|
2553
|
+
# Whether no value of `type` is a `Hash`. Anything this does not recognise answers false. A `Constant` never
|
|
2554
|
+
# carries a `Hash` (it holds scalars only).
|
|
2555
|
+
def provably_not_hash?(type)
|
|
2556
|
+
case type
|
|
2557
|
+
when Type::Constant, Type::Tuple, Type::IntegerRange then true
|
|
2558
|
+
when Type::Nominal then NON_HASH_CLASSES.include?(type.class_name)
|
|
2559
|
+
when Type::Union then type.members.all? { |member| provably_not_hash?(member) }
|
|
2560
|
+
when Type::Refined, Type::Difference then provably_not_hash?(type.base)
|
|
2561
|
+
else false
|
|
2562
|
+
end
|
|
2563
|
+
end
|
|
2564
|
+
|
|
2182
2565
|
# The post-dispatch tiers for a call `MethodDispatcher` could not answer, in their historical
|
|
2183
2566
|
# order; extracted from {#call_dispatch_type_for} whole.
|
|
2184
2567
|
def dispatch_miss_result(node, receiver, arg_types, block_type = nil)
|
|
@@ -2388,16 +2771,16 @@ module Rigor
|
|
|
2388
2771
|
#
|
|
2389
2772
|
# - the receiver is not `Nominal[T]` for some T;
|
|
2390
2773
|
# - no def_node is recorded for that class/method (the receiver is foreign or has only an RBS sig);
|
|
2391
|
-
# - the def has no body, or has a parameter shape we cannot bind from the call's positional args
|
|
2392
|
-
# - the inference is already in progress for this (class, method, signature) tuple — recursion safety
|
|
2393
|
-
# net.
|
|
2774
|
+
# - the def has no body, or has a parameter shape we cannot bind from the call's positional args.
|
|
2394
2775
|
# v0.0.3 A — re-types a top-level (or DSL-block-nested) `def` discovered by `ScopeIndexer` under the
|
|
2395
2776
|
# `TOP_LEVEL_DEF_KEY` sentinel. Mirrors the `infer_user_method_return` shape but uses the current
|
|
2396
2777
|
# `scope.self_type` (or implicit `Object`) as the receiver carrier so the body's own self is
|
|
2397
2778
|
# consistent with the call site's. Returns nil when the parameter shape disqualifies the def, when the
|
|
2398
|
-
# body is empty, or when
|
|
2399
|
-
def infer_top_level_user_method(def_node, receiver, arg_types, block_type = nil)
|
|
2400
|
-
|
|
2779
|
+
# body is empty, or when the inference raises.
|
|
2780
|
+
def infer_top_level_user_method(def_node, receiver, call_node, arg_types, block_type = nil)
|
|
2781
|
+
arg_types, positionals_unknown = positional_correspondence(call_node, arg_types)
|
|
2782
|
+
infer_user_method_return(def_node, receiver, arg_types, yield_type: block_type,
|
|
2783
|
+
positionals_unknown: positionals_unknown)
|
|
2401
2784
|
rescue StandardError
|
|
2402
2785
|
nil
|
|
2403
2786
|
end
|
|
@@ -2442,13 +2825,16 @@ module Rigor
|
|
|
2442
2825
|
|
|
2443
2826
|
# True when `type` is a concrete value — a `Type::Constant` or a `Type::Tuple` whose elements are
|
|
2444
2827
|
# (recursively) all value-pinned. ADR-55 slice 1: a value-pinned self-call result is adopted even
|
|
2445
|
-
# inside a class/method body (where WD3 otherwise keeps non-`Bot` returns as `Dynamic[top]`). A
|
|
2446
|
-
#
|
|
2447
|
-
#
|
|
2828
|
+
# inside a class/method body (where WD3 otherwise keeps non-`Bot` returns as `Dynamic[top]`). A concrete
|
|
2829
|
+
# value at a call site is strictly more precise and can never enable an undefined-method or argument-type
|
|
2830
|
+
# false positive — it is FP-neutral by construction.
|
|
2448
2831
|
def fully_value_pinned?(type)
|
|
2449
2832
|
case type
|
|
2450
2833
|
when Type::Constant then true
|
|
2451
2834
|
when Type::Tuple then type.elements.all? { |element| fully_value_pinned?(element) }
|
|
2835
|
+
when Type::HashShape
|
|
2836
|
+
# Issue #3115 — the shape {#pinned_shape_descriptor} keys an unroll on is as pinned as a result.
|
|
2837
|
+
type.closed? && type.optional_keys.empty? && type.pairs.each_value.all? { |value| fully_value_pinned?(value) }
|
|
2452
2838
|
else false
|
|
2453
2839
|
end
|
|
2454
2840
|
end
|
|
@@ -2535,9 +2921,10 @@ module Rigor
|
|
|
2535
2921
|
def_node, owner, raising = resolve_typing_def_with_owner(receiver.class_name, method_name)
|
|
2536
2922
|
return every_definer_raises_type(raising, receiver, call_node, arg_types, block_type) if def_node.nil?
|
|
2537
2923
|
|
|
2924
|
+
arg_types, positionals_unknown = positional_correspondence(call_node, arg_types)
|
|
2538
2925
|
result = infer_user_method_return(def_node, receiver, arg_types,
|
|
2539
2926
|
self_fold_safe: fold_safe_call_receiver?(call_node),
|
|
2540
|
-
yield_type: block_type)
|
|
2927
|
+
yield_type: block_type, positionals_unknown: positionals_unknown)
|
|
2541
2928
|
return result if result.nil?
|
|
2542
2929
|
|
|
2543
2930
|
degrade_if_overridable(result, owner, method_name, :instance)
|
|
@@ -2619,7 +3006,9 @@ module Rigor
|
|
|
2619
3006
|
# proven to be the method Ruby calls, so the call falls through to `Dynamic`.
|
|
2620
3007
|
return nil if scope.discovery.contested?(:discovered_singleton_def_nodes, [owner, method_name.to_sym])
|
|
2621
3008
|
|
|
2622
|
-
|
|
3009
|
+
arg_types, positionals_unknown = positional_correspondence(call_node, arg_types)
|
|
3010
|
+
result = infer_user_method_return(def_node, receiver, arg_types, yield_type: block_type,
|
|
3011
|
+
positionals_unknown: positionals_unknown)
|
|
2623
3012
|
return result if result.nil?
|
|
2624
3013
|
|
|
2625
3014
|
degrade_if_overridable(result, owner, method_name, :singleton)
|
|
@@ -2641,12 +3030,240 @@ module Rigor
|
|
|
2641
3030
|
cache = (by_singleton[scope.discovered_singleton_def_nodes] ||= {})
|
|
2642
3031
|
table = (cache[class_name.to_s] ||= {})
|
|
2643
3032
|
key = method_name.to_sym
|
|
2644
|
-
|
|
3033
|
+
if table.key?(key)
|
|
3034
|
+
return MemoAudit.check("class graph singleton def", table[key], class_name, key) do
|
|
3035
|
+
compute_singleton_def_with_owner(class_name, method_name)
|
|
3036
|
+
end
|
|
3037
|
+
end
|
|
3038
|
+
|
|
3039
|
+
table[key] = MemoAudit.key_audit("class graph singleton def") do
|
|
3040
|
+
compute_singleton_def_with_owner(class_name, method_name)
|
|
3041
|
+
end
|
|
3042
|
+
end
|
|
2645
3043
|
|
|
3044
|
+
def compute_singleton_def_with_owner(class_name, method_name)
|
|
2646
3045
|
found = scope.singleton_def_through_ancestors(class_name, method_name)
|
|
2647
3046
|
owner = found.last
|
|
2648
3047
|
found = [nil, nil] if owner && SingletonExternalAhead.withhold?(scope, class_name, method_name, owner)
|
|
2649
|
-
|
|
3048
|
+
found = [nil, nil] if found.last && dynamic_singleton_definer_ahead?(class_name, method_name, found.last)
|
|
3049
|
+
found = [nil, nil] if found.last && singleton_hook_order_split?(class_name, method_name)
|
|
3050
|
+
found = [nil, nil] if found.last && singleton_mixin_split?(class_name, method_name, found)
|
|
3051
|
+
found = [nil, nil] if found.last && unseen_definer_split?(class_name, method_name, found)
|
|
3052
|
+
found
|
|
3053
|
+
end
|
|
3054
|
+
|
|
3055
|
+
# A definer no def table holds that Ruby may call instead of `found`: a hook-placed one, or a foreign `def C.x`.
|
|
3056
|
+
def unseen_definer_split?(class_name, method_name, found)
|
|
3057
|
+
hidden_hook_split?(class_name, method_name, found.last) || foreign_definer_behind_copy?(method_name, found)
|
|
3058
|
+
end
|
|
3059
|
+
|
|
3060
|
+
# Issue #3155 — a `def C.x` outside C's body (in the extended module, another class or the file) defines C's
|
|
3061
|
+
# own `x`, which no def table holds, only its object-singleton mark. A fold copy found on C may then be a
|
|
3062
|
+
# body Ruby does not call, so the typing read withholds.
|
|
3063
|
+
def foreign_definer_behind_copy?(method_name, found)
|
|
3064
|
+
node, owner = found
|
|
3065
|
+
return false unless singleton_copy_head(owner, node) != owner
|
|
3066
|
+
|
|
3067
|
+
SingletonHookDecline.foreign_singleton_definer?(scope, owner, method_name)
|
|
3068
|
+
end
|
|
3069
|
+
|
|
3070
|
+
# The hook-placed definers a found singleton `def` may not be the one Ruby calls through.
|
|
3071
|
+
def hidden_hook_split?(class_name, method_name, owner)
|
|
3072
|
+
hidden_hook_definer?(class_name, method_name, owner) || hook_block_rival?(class_name, method_name, owner)
|
|
3073
|
+
end
|
|
3074
|
+
|
|
3075
|
+
# Issue #3005 — a class object's own `def self.x` and a `def self.x` a concern's `included` block defines on it
|
|
3076
|
+
# replace each other in the order the `def` and the `include` run, which the tables do not hold, so the typing
|
|
3077
|
+
# read withholds when the answer is a class object's own `def` and its level's hook-block module defines the
|
|
3078
|
+
# name too.
|
|
3079
|
+
def hook_block_rival?(class_name, method_name, owner)
|
|
3080
|
+
chain = Scope::ResolutionChain.for(scope, class_name.to_s, :singleton, :methods)
|
|
3081
|
+
index = chain.entries.index { |entry| !entry.external? && entry.side == :singleton && entry.name == owner }
|
|
3082
|
+
!index.nil? && chain.hook_block_rival?(scope, index, method_name)
|
|
3083
|
+
end
|
|
3084
|
+
|
|
3085
|
+
# Issue #1592 — a module a hook extends the class with may define the name private (`class_methods do private;
|
|
3086
|
+
# def x`), which Ruby refuses on an explicit receiver. The visibility rule does not reach that module, so the
|
|
3087
|
+
# typing read withholds rather than answer a call Ruby raises on. A module the class `extend`s itself is left to
|
|
3088
|
+
# that rule.
|
|
3089
|
+
def hidden_hook_definer?(class_name, method_name, owner)
|
|
3090
|
+
visibility = scope.discovered_method_visibility(owner, method_name)
|
|
3091
|
+
return false if visibility.nil? || visibility == :public
|
|
3092
|
+
|
|
3093
|
+
chain = Scope::ResolutionChain.for(scope, class_name.to_s, :singleton, :methods)
|
|
3094
|
+
return false unless chain.entries.any? { |entry| entry.name == owner && entry.side == :instance }
|
|
3095
|
+
|
|
3096
|
+
scope.singleton_extends_of(class_name).none? do |raw|
|
|
3097
|
+
written = raw.delete_prefix("::")
|
|
3098
|
+
owner == written || owner.end_with?("::#{written}")
|
|
3099
|
+
end
|
|
3100
|
+
end
|
|
3101
|
+
|
|
3102
|
+
# Issue #1592 — whether to withhold `method_name`'s class-side answer: a class object on the chain has
|
|
3103
|
+
# hook-extended modules whose order against its own `extend`s is not a fact, and the orders give different
|
|
3104
|
+
# definers (the retro world's against the chain's, or, with no retro world, the class objects' alone against
|
|
3105
|
+
# the chain's). A `def` the extends fold copied onto a class object is read where Ruby finds it, at the extended
|
|
3106
|
+
# module's own entry, since the copy sits ahead of every module in both orders.
|
|
3107
|
+
def singleton_hook_order_split?(class_name, method_name)
|
|
3108
|
+
chain = Scope::ResolutionChain.for(scope, class_name.to_s, :singleton, :methods)
|
|
3109
|
+
return false unless chain.hook_order_marked?
|
|
3110
|
+
|
|
3111
|
+
answer = positioned_singleton_definer(chain, method_name)
|
|
3112
|
+
retro = chain.retro_world
|
|
3113
|
+
return answer != positioned_singleton_definer(retro, method_name) if retro
|
|
3114
|
+
|
|
3115
|
+
answer != positioned_singleton_definer(chain, method_name, copies: true)
|
|
3116
|
+
end
|
|
3117
|
+
|
|
3118
|
+
# Issues #2998 and #3020 — whether a singleton-side mixin of a class object on the chain may answer ahead of
|
|
3119
|
+
# `found`: a named prepend master's order does not place, or a module no table names.
|
|
3120
|
+
def singleton_mixin_split?(class_name, method_name, found)
|
|
3121
|
+
singleton_prepend_split?(class_name, method_name, found) ||
|
|
3122
|
+
unnamed_singleton_mixin_split?(class_name, method_name)
|
|
3123
|
+
end
|
|
3124
|
+
|
|
3125
|
+
# Issue #2998 — whether to withhold the class-side answer `found` on a chain whose class objects have
|
|
3126
|
+
# singleton-side prepends of their own: master's order, which a chain that does not stand answers, places no
|
|
3127
|
+
# such prepend, so its definer may not be the one Ruby's order reaches first. A `def` the extends fold copied
|
|
3128
|
+
# onto a class object is compared as its module's. A definer from a prepend region whose order is not a fact
|
|
3129
|
+
# is withheld whichever order answers.
|
|
3130
|
+
def singleton_prepend_split?(class_name, method_name, found)
|
|
3131
|
+
chain = Scope::ResolutionChain.for(scope, class_name.to_s, :singleton, :methods)
|
|
3132
|
+
return false unless chain.singleton_prepend_marked?
|
|
3133
|
+
|
|
3134
|
+
answer = positioned_singleton_definer(chain, method_name)
|
|
3135
|
+
return true if unordered_prepend_definer?(chain, answer)
|
|
3136
|
+
|
|
3137
|
+
node, owner = found
|
|
3138
|
+
head = scope.singleton_def_for(owner, method_name).equal?(node) ? singleton_copy_head(owner, node) : owner
|
|
3139
|
+
head != answer
|
|
3140
|
+
end
|
|
3141
|
+
|
|
3142
|
+
# Whether `answer` is a module in the prepend region of a class object on the chain whose prepends have no known
|
|
3143
|
+
# order: the entries between the start of that class's level and its own entry.
|
|
3144
|
+
def unordered_prepend_definer?(chain, answer)
|
|
3145
|
+
chain.prepend_order_nodes.any? do |node|
|
|
3146
|
+
level = chain.level_classes.index(node)
|
|
3147
|
+
next true if level.nil?
|
|
3148
|
+
|
|
3149
|
+
start = chain.level_starts[level]
|
|
3150
|
+
own = chain.entries.index { |entry| entry.side == :singleton && entry.name == node }
|
|
3151
|
+
next true if own.nil?
|
|
3152
|
+
|
|
3153
|
+
chain.entries[start...own].any? { |entry| entry.name == answer }
|
|
3154
|
+
end
|
|
3155
|
+
end
|
|
3156
|
+
|
|
3157
|
+
# Issue #3020 — whether to withhold the class-side answer on a chain whose class objects mix a module no table
|
|
3158
|
+
# names into their singleton side, which may define the name. A prepend (`class << self; prepend(*MODS)`) sits
|
|
3159
|
+
# ahead of the class object's own `def self.`, so a definer at or behind that class object's level is withheld;
|
|
3160
|
+
# an `extend(*MODS)` or a `class << self` `include(*MODS)` sits right behind it, so a definer behind the class
|
|
3161
|
+
# object's own entry is withheld and its own `def self.` still answers.
|
|
3162
|
+
def unnamed_singleton_mixin_split?(class_name, method_name)
|
|
3163
|
+
chain = Scope::ResolutionChain.for(scope, class_name.to_s, :singleton, :methods)
|
|
3164
|
+
prepends = chain.unnamed_prepend_nodes
|
|
3165
|
+
extends = chain.unnamed_extend_nodes
|
|
3166
|
+
return false if prepends.empty? && extends.empty?
|
|
3167
|
+
|
|
3168
|
+
answer = positioned_singleton_definer(chain, method_name)
|
|
3169
|
+
prepends.any? { |node| !answered_ahead?(chain, answer, level_start_of(chain, node)) } ||
|
|
3170
|
+
extends.any? { |node| !answered_ahead?(chain, answer, own_singleton_index(chain, node)&.succ) }
|
|
3171
|
+
end
|
|
3172
|
+
|
|
3173
|
+
# Whether `answer` owns an entry before `stop` (nil when the position is unknown: never). A nil `answer` (no class
|
|
3174
|
+
# object's own def) owns none.
|
|
3175
|
+
def answered_ahead?(chain, answer, stop)
|
|
3176
|
+
!stop.nil? && chain.entries[0...stop].any? { |entry| entry.name == answer }
|
|
3177
|
+
end
|
|
3178
|
+
|
|
3179
|
+
def level_start_of(chain, node)
|
|
3180
|
+
level = chain.level_classes.index(node)
|
|
3181
|
+
level && chain.level_starts[level]
|
|
3182
|
+
end
|
|
3183
|
+
|
|
3184
|
+
def own_singleton_index(chain, node)
|
|
3185
|
+
chain.entries.index { |entry| entry.side == :singleton && entry.name == node }
|
|
3186
|
+
end
|
|
3187
|
+
|
|
3188
|
+
# The owner of the first `def` of the name along the chain: a fold copy is skipped, or with `copies:` read as
|
|
3189
|
+
# its module's, on the class objects alone (the answer master gives).
|
|
3190
|
+
def positioned_singleton_definer(chain, method_name, copies: false)
|
|
3191
|
+
chain.search(scope, side: copies ? :singleton : nil) do |entry|
|
|
3192
|
+
next if entry.external?
|
|
3193
|
+
|
|
3194
|
+
name = entry.name
|
|
3195
|
+
if entry.side == :singleton
|
|
3196
|
+
node = scope.singleton_def_for(name, method_name)
|
|
3197
|
+
next nil if node.nil?
|
|
3198
|
+
|
|
3199
|
+
head = singleton_copy_head(name, node)
|
|
3200
|
+
copies ? head : (name if head == name)
|
|
3201
|
+
elsif scope.user_def_for(name, method_name)
|
|
3202
|
+
name
|
|
3203
|
+
end
|
|
3204
|
+
end
|
|
3205
|
+
end
|
|
3206
|
+
|
|
3207
|
+
# The module a receiverless singleton `def` was written in when the extends fold copied it onto `owner`, else
|
|
3208
|
+
# `owner`.
|
|
3209
|
+
def singleton_copy_head(owner, node)
|
|
3210
|
+
head = node.receiver.nil? ? scope.discovery.recorded_def_nesting(node)&.first : nil
|
|
3211
|
+
head.nil? ? owner : head
|
|
3212
|
+
end
|
|
3213
|
+
|
|
3214
|
+
SINGLETON_WILDCARD_SLOT = %i[singleton *].freeze
|
|
3215
|
+
private_constant :SINGLETON_WILDCARD_SLOT
|
|
3216
|
+
|
|
3217
|
+
# Issue #2284 — a project entry nearer than the `def`'s owner on the singleton chain may define the name
|
|
3218
|
+
# without a `def`, which the def tables do not hold: `class S < B; define_singleton_method(:x) { "s" }; end`
|
|
3219
|
+
# calls S's block, not `B.x`. The farther `def` is then withheld and the call types `Dynamic`. A class's own
|
|
3220
|
+
# singleton side is read from the visibility-contest census: `define_singleton_method(:x)`,
|
|
3221
|
+
# `singleton_class.define_method(:x)` / `attr_*` and a computed name contest the entry's slot whether or not a
|
|
3222
|
+
# `def` is recorded (#1984, #2013). An `extend`ed module's side is read from `discovered_methods`, which holds
|
|
3223
|
+
# its `attr_*` and `define_method` rows, and the module's `def self.extended` hook may define any name on the
|
|
3224
|
+
# extender, so it withholds too.
|
|
3225
|
+
#
|
|
3226
|
+
# An `inherited` hook anywhere on the chain past the receiver's own entry runs when the receiver's class is
|
|
3227
|
+
# created and may define any singleton method on it (`def self.inherited(s) = s.define_singleton_method(:x)`),
|
|
3228
|
+
# so a `def` the receiver only inherits is withheld then as well: a class's `def self.inherited`, or an
|
|
3229
|
+
# `inherited` an extended module defines.
|
|
3230
|
+
def dynamic_singleton_definer_ahead?(class_name, method_name, owner)
|
|
3231
|
+
name = method_name.to_sym
|
|
3232
|
+
chain = Scope::ResolutionChain.for(scope, class_name.to_s, :singleton, :methods)
|
|
3233
|
+
return true if class_name.to_s != owner.to_s && inherited_hook_on_chain?(chain, class_name.to_s)
|
|
3234
|
+
|
|
3235
|
+
chain.entries.each do |entry|
|
|
3236
|
+
next if entry.external?
|
|
3237
|
+
return false if entry.name == owner.to_s
|
|
3238
|
+
return true if entry_defines_without_def?(entry, name)
|
|
3239
|
+
# Issue #1592 — a hook of a module the class includes may have extended it with a module no table names.
|
|
3240
|
+
return true if entry.side == :singleton && chain.hook_open_nodes.include?(entry.name)
|
|
3241
|
+
end
|
|
3242
|
+
false
|
|
3243
|
+
end
|
|
3244
|
+
|
|
3245
|
+
def inherited_hook_on_chain?(chain, receiver_name)
|
|
3246
|
+
chain.entries.any? do |entry|
|
|
3247
|
+
next false if entry.external? || entry.name == receiver_name
|
|
3248
|
+
|
|
3249
|
+
if entry.side == :singleton
|
|
3250
|
+
!scope.singleton_def_for(entry.name, :inherited).nil?
|
|
3251
|
+
else
|
|
3252
|
+
scope.discovered_method?(entry.name, :inherited, :instance)
|
|
3253
|
+
end
|
|
3254
|
+
end
|
|
3255
|
+
end
|
|
3256
|
+
|
|
3257
|
+
def entry_defines_without_def?(entry, name)
|
|
3258
|
+
unless entry.side == :singleton
|
|
3259
|
+
return scope.discovered_method?(entry.name, name, :instance) ||
|
|
3260
|
+
!scope.singleton_def_for(entry.name, :extended).nil?
|
|
3261
|
+
end
|
|
3262
|
+
|
|
3263
|
+
discovery = scope.discovery
|
|
3264
|
+
# No edge of its own: `singleton_def_through_ancestors`' chain search filed every entry ahead of the owner.
|
|
3265
|
+
discovery.contested?(:discovered_method_visibilities, [entry.name, [:singleton, name]]) ||
|
|
3266
|
+
discovery.contested?(:discovered_method_visibilities, [entry.name, SINGLETON_WILDCARD_SLOT])
|
|
2650
3267
|
end
|
|
2651
3268
|
|
|
2652
3269
|
# The project-side singleton-method band: a `Foo.bar` call resolved against a `class << …` / `def self.…`
|
|
@@ -2672,7 +3289,9 @@ module Rigor
|
|
|
2672
3289
|
def_node = SingletonObjectConstant.def_node_for(call_node, receiver, method_name, scope)
|
|
2673
3290
|
return nil if def_node.nil?
|
|
2674
3291
|
|
|
2675
|
-
|
|
3292
|
+
arg_types, positionals_unknown = positional_correspondence(call_node, arg_types)
|
|
3293
|
+
infer_user_method_return(def_node, receiver, arg_types, yield_type: block_type,
|
|
3294
|
+
positionals_unknown: positionals_unknown)
|
|
2676
3295
|
rescue StandardError
|
|
2677
3296
|
nil
|
|
2678
3297
|
end
|
|
@@ -2684,8 +3303,14 @@ module Rigor
|
|
|
2684
3303
|
# external entries this lookup passes over. Cross-file: the chain is built from the tables the runner
|
|
2685
3304
|
# seeds from the project-wide pre-pass, cycle-guarded and capped at `Scope::ANCESTOR_WALK_LIMIT`.
|
|
2686
3305
|
|
|
2687
|
-
|
|
2688
|
-
|
|
3306
|
+
CLASS_GRAPH_CACHE = TypingState.memo(
|
|
3307
|
+
:class_graph_cache,
|
|
3308
|
+
run_scoped: true,
|
|
3309
|
+
audit: ["class graph user def", "class graph typing def", "class graph singleton def", "class graph yields",
|
|
3310
|
+
"class graph self pure"],
|
|
3311
|
+
reason: "pure functions of the frozen discovery index it keys on"
|
|
3312
|
+
)
|
|
3313
|
+
private_constant :CLASS_GRAPH_CACHE
|
|
2689
3314
|
|
|
2690
3315
|
# Memo for the static class-graph resolvers below. They are pure functions of the scope's *frozen*
|
|
2691
3316
|
# discovery tables — `user_def_for` / `superclass_of` / `includes_of` and the ancestor-name resolver
|
|
@@ -2703,8 +3328,8 @@ module Rigor
|
|
|
2703
3328
|
#
|
|
2704
3329
|
# ONE slot, replaced rather than accumulated — the same shape, and now the same key, as
|
|
2705
3330
|
# {MethodDispatcher::RbsDispatch}'s `core_stdlib_memo`. `ExpressionTyper` is rebuilt per
|
|
2706
|
-
# `Scope#type_of`, so the slot lives
|
|
2707
|
-
# file's discovery with the project pre-pass, so every analysed file arrives with a fresh index: an
|
|
3331
|
+
# `Scope#type_of`, so the slot lives in the thread's {TypingState} rather than on `self`. A `Scope` merges
|
|
3332
|
+
# its file's discovery with the project pre-pass, so every analysed file arrives with a fresh index: an
|
|
2708
3333
|
# identity-keyed *store* grew one bucket per file and, because the keys ARE the tables, pinned every
|
|
2709
3334
|
# file's whole discovery index for the length of the run. What that bought, measured over
|
|
2710
3335
|
# `lib`+`plugins`: 552 buckets holding 1,834 entries between them, and 58 of 80,106 calls answered
|
|
@@ -2714,23 +3339,32 @@ module Rigor
|
|
|
2714
3339
|
# because one `equal?` is cheaper than three identity-hash lookups.
|
|
2715
3340
|
#
|
|
2716
3341
|
# Steady-state cost is one `equal?` check and zero allocation; the array and the buckets are
|
|
2717
|
-
# allocated once per index. (Pool mode forks per worker, so the
|
|
3342
|
+
# allocated once per index. (Pool mode forks per worker, so the per-thread slot is
|
|
2718
3343
|
# process-local and never crosses a project boundary — and note that the parent's reported
|
|
2719
3344
|
# `Memory peak` therefore does not see a worker's share of this at all.)
|
|
3345
|
+
#
|
|
3346
|
+
# ADR-129 WD1 — every bucket's hit goes through `MemoAudit.check`; an audit recomputation gets fresh buckets that
|
|
3347
|
+
# nothing stores, so it reads no entry and leaves none.
|
|
2720
3348
|
def class_graph_buckets
|
|
3349
|
+
return fresh_class_graph_buckets if MemoAudit.enabled? && MemoAudit.suspended?
|
|
3350
|
+
|
|
2721
3351
|
discovery = scope.discovery
|
|
2722
|
-
|
|
3352
|
+
state = typing_state
|
|
3353
|
+
slot = state[CLASS_GRAPH_CACHE]
|
|
2723
3354
|
unless slot && slot[0].equal?(discovery)
|
|
2724
3355
|
# `self_pure` is issue #525's grant scan (identity-keyed by def node); it belongs here because it
|
|
2725
3356
|
# is a pure function of the same frozen index — the sibling resolver it walks reads nothing else.
|
|
2726
3357
|
# `singleton_def` is added lazily by {#singleton_def_through_ancestors}'s caller.
|
|
2727
|
-
slot = [discovery,
|
|
2728
|
-
|
|
2729
|
-
Thread.current[CLASS_GRAPH_CACHE_KEY] = slot
|
|
3358
|
+
slot = [discovery, fresh_class_graph_buckets]
|
|
3359
|
+
state[CLASS_GRAPH_CACHE] = slot
|
|
2730
3360
|
end
|
|
2731
3361
|
slot[1]
|
|
2732
3362
|
end
|
|
2733
3363
|
|
|
3364
|
+
def fresh_class_graph_buckets
|
|
3365
|
+
{ user_def: {}, typing_def: {}, self_pure: {}.compare_by_identity, yields: {}.compare_by_identity }
|
|
3366
|
+
end
|
|
3367
|
+
|
|
2734
3368
|
# ADR-119 C2-b1 — the TYPING read of {#resolve_user_def_with_owner}: the same `[def_node, owner]` pair, but
|
|
2735
3369
|
# answered through {DefinerResolution} so a chain that does not stand for the name (a conditional definer,
|
|
2736
3370
|
# a conditional include, a fork, an external ancestor that may answer first) is `[nil, nil]` and the call
|
|
@@ -2745,17 +3379,26 @@ module Rigor
|
|
|
2745
3379
|
cache = class_graph_buckets[:typing_def]
|
|
2746
3380
|
table = (cache[class_name.to_s] ||= {})
|
|
2747
3381
|
key = method_name.to_sym
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
case DefinerResolution.resolve(scope, class_name, method_name, :instance, question: :definer)
|
|
2752
|
-
in DefinerResolution::Known(answer: [node, owner])
|
|
2753
|
-
[node, owner]
|
|
2754
|
-
in DefinerResolution::UNKNOWN
|
|
2755
|
-
[nil, nil, every_project_definer(class_name, method_name)]
|
|
2756
|
-
in DefinerResolution::ABSENT
|
|
2757
|
-
[nil, nil]
|
|
3382
|
+
if table.key?(key)
|
|
3383
|
+
return MemoAudit.check("class graph typing def", table[key], class_name, key) do
|
|
3384
|
+
compute_typing_def_with_owner(class_name, method_name)
|
|
2758
3385
|
end
|
|
3386
|
+
end
|
|
3387
|
+
|
|
3388
|
+
table[key] = MemoAudit.key_audit("class graph typing def") do
|
|
3389
|
+
compute_typing_def_with_owner(class_name, method_name)
|
|
3390
|
+
end
|
|
3391
|
+
end
|
|
3392
|
+
|
|
3393
|
+
def compute_typing_def_with_owner(class_name, method_name)
|
|
3394
|
+
case DefinerResolution.resolve(scope, class_name, method_name, :instance, question: :definer)
|
|
3395
|
+
in DefinerResolution::Known(answer: [node, owner])
|
|
3396
|
+
[node, owner]
|
|
3397
|
+
in DefinerResolution::UNKNOWN
|
|
3398
|
+
[nil, nil, every_project_definer(class_name, method_name)]
|
|
3399
|
+
in DefinerResolution::ABSENT
|
|
3400
|
+
[nil, nil]
|
|
3401
|
+
end
|
|
2759
3402
|
end
|
|
2760
3403
|
|
|
2761
3404
|
# ADR-119 C2-b1 errata (the `bot` exception) — an UNKNOWN chain does not say WHICH definer Ruby calls, but
|
|
@@ -2769,9 +3412,11 @@ module Rigor
|
|
|
2769
3412
|
return nil if defs.nil?
|
|
2770
3413
|
|
|
2771
3414
|
self_fold_safe = fold_safe_call_receiver?(call_node)
|
|
3415
|
+
arg_types, positionals_unknown = positional_correspondence(call_node, arg_types)
|
|
2772
3416
|
defs.each do |def_node|
|
|
2773
3417
|
result = infer_user_method_return(def_node, receiver, arg_types,
|
|
2774
|
-
self_fold_safe: self_fold_safe, yield_type: block_type
|
|
3418
|
+
self_fold_safe: self_fold_safe, yield_type: block_type,
|
|
3419
|
+
positionals_unknown: positionals_unknown)
|
|
2775
3420
|
return nil unless result.is_a?(Type::Bot)
|
|
2776
3421
|
end
|
|
2777
3422
|
Type::Combinator.bot
|
|
@@ -2846,9 +3491,15 @@ module Rigor
|
|
|
2846
3491
|
cache = class_graph_buckets[:user_def]
|
|
2847
3492
|
table = (cache[class_name.to_s] ||= {})
|
|
2848
3493
|
key = method_name.to_sym
|
|
2849
|
-
|
|
3494
|
+
if table.key?(key)
|
|
3495
|
+
return MemoAudit.check("class graph user def", table[key], class_name, key) do
|
|
3496
|
+
compute_user_def_with_owner(class_name, method_name)
|
|
3497
|
+
end
|
|
3498
|
+
end
|
|
2850
3499
|
|
|
2851
|
-
table[key] =
|
|
3500
|
+
table[key] = MemoAudit.key_audit("class graph user def") do
|
|
3501
|
+
compute_user_def_with_owner(class_name, method_name)
|
|
3502
|
+
end
|
|
2852
3503
|
end
|
|
2853
3504
|
|
|
2854
3505
|
# ADR-24 slice 2 — the walk itself lives on `Scope` ({Scope#user_def_through_ancestors}); it reads
|
|
@@ -2885,8 +3536,12 @@ module Rigor
|
|
|
2885
3536
|
dynamic_top
|
|
2886
3537
|
end
|
|
2887
3538
|
|
|
2888
|
-
|
|
2889
|
-
|
|
3539
|
+
OVERRIDE_GATE_CACHE = TypingState.memo(
|
|
3540
|
+
:overridable_method_gate,
|
|
3541
|
+
run_scoped: true, audit: "overridable method gate",
|
|
3542
|
+
reason: "pure functions of the frozen discovery index it keys on"
|
|
3543
|
+
)
|
|
3544
|
+
private_constant :OVERRIDE_GATE_CACHE
|
|
2890
3545
|
|
|
2891
3546
|
# Memo for {#overridden_in_project?}, nested `kind → owner → method_name`. The predicate is a pure
|
|
2892
3547
|
# function of the scope's frozen discovery tables, so that index's *identity* is what says whether a
|
|
@@ -2916,10 +3571,11 @@ module Rigor
|
|
|
2916
3571
|
# keyed and bounded differently for exactly that reason.
|
|
2917
3572
|
def override_gate_buckets
|
|
2918
3573
|
discovery = scope.discovery
|
|
2919
|
-
|
|
3574
|
+
state = typing_state
|
|
3575
|
+
slot = state[OVERRIDE_GATE_CACHE]
|
|
2920
3576
|
unless slot && slot[0].equal?(discovery)
|
|
2921
3577
|
slot = [discovery, { instance: {}, singleton: {} }]
|
|
2922
|
-
|
|
3578
|
+
state[OVERRIDE_GATE_CACHE] = slot
|
|
2923
3579
|
end
|
|
2924
3580
|
slot[1]
|
|
2925
3581
|
end
|
|
@@ -2930,10 +3586,18 @@ module Rigor
|
|
|
2930
3586
|
# reopen of `owner` itself is NOT an override (monkey-patch reopen shares the owner identity). Memoized
|
|
2931
3587
|
# per `(owner, method_name, kind)`.
|
|
2932
3588
|
def overridden_in_project?(owner, method_name, kind)
|
|
3589
|
+
return compute_overridden_in_project?(owner, method_name, kind) if MemoAudit.enabled? && MemoAudit.suspended?
|
|
3590
|
+
|
|
2933
3591
|
by_owner = (override_gate_buckets[kind][owner] ||= {})
|
|
2934
|
-
|
|
3592
|
+
if by_owner.key?(method_name)
|
|
3593
|
+
return MemoAudit.check("overridable method gate", by_owner[method_name], owner, method_name) do
|
|
3594
|
+
compute_overridden_in_project?(owner, method_name, kind)
|
|
3595
|
+
end
|
|
3596
|
+
end
|
|
2935
3597
|
|
|
2936
|
-
by_owner[method_name] =
|
|
3598
|
+
by_owner[method_name] = MemoAudit.key_audit("overridable method gate") do
|
|
3599
|
+
compute_overridden_in_project?(owner, method_name, kind)
|
|
3600
|
+
end
|
|
2937
3601
|
end
|
|
2938
3602
|
|
|
2939
3603
|
def compute_overridden_in_project?(owner, method_name, kind)
|
|
@@ -2958,16 +3622,23 @@ module Rigor
|
|
|
2958
3622
|
EMPTY_REDEFINERS = [].freeze
|
|
2959
3623
|
private_constant :EMPTY_REDEFINERS
|
|
2960
3624
|
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
#
|
|
2969
|
-
|
|
2970
|
-
|
|
3625
|
+
METHOD_DEFINERS_INDEX = TypingState.memo(
|
|
3626
|
+
:method_definers_index,
|
|
3627
|
+
run_scoped: true, audit: "method definers index",
|
|
3628
|
+
reason: "pure functions of the def tables it keys on"
|
|
3629
|
+
)
|
|
3630
|
+
private_constant :METHOD_DEFINERS_INDEX
|
|
3631
|
+
|
|
3632
|
+
# The second way. Two flat slots rather than one slot holding an array of ways: a `slot ||= […]` seeds the
|
|
3633
|
+
# store with an array LITERAL, which folds to a tuple whose elements are pinned, and the engine then reads the
|
|
3634
|
+
# `way[0].equal?(…)` guards below as always-falsey (`make check` fires `flow.always-truthy-condition` on this
|
|
3635
|
+
# very file). Two slots read as two plain reads and keep the guards analysable.
|
|
3636
|
+
METHOD_DEFINERS_INDEX_ALT = TypingState.memo(
|
|
3637
|
+
:method_definers_index_alt,
|
|
3638
|
+
run_scoped: true, audit: "method definers index alt",
|
|
3639
|
+
reason: "pure functions of the def tables it keys on"
|
|
3640
|
+
)
|
|
3641
|
+
private_constant :METHOD_DEFINERS_INDEX_ALT
|
|
2971
3642
|
|
|
2972
3643
|
# `method_name (Symbol) → [owner names]` inverted index over the instance / singleton def tables,
|
|
2973
3644
|
# valid for exactly as long as the table it inverts: a new analysis generation, or a `Scope` that swaps
|
|
@@ -3000,36 +3671,60 @@ module Rigor
|
|
|
3000
3671
|
# the singleton index.
|
|
3001
3672
|
#
|
|
3002
3673
|
# DELIBERATELY not keyed on the whole {Scope::DiscoveryIndex}, unlike {#class_graph_buckets} and
|
|
3003
|
-
# {#override_gate_buckets} above. {
|
|
3004
|
-
# nothing else, so the def tables are already the complete key, and the index
|
|
3674
|
+
# {#override_gate_buckets} above. {DefinersIndex.for} reads the ONE table it is handed, and the seed that
|
|
3675
|
+
# table was merged over, and nothing else, so the def tables are already the complete key, and the index
|
|
3676
|
+
# object is a strictly
|
|
3005
3677
|
# narrower one: the project-seed scope carries a fresh index per file while its def tables stay the
|
|
3006
3678
|
# same object. Over the 200 synthetic files above, an index-keyed memo misses all 400 times where a
|
|
3007
3679
|
# def-table-keyed one misses 201 — it would rebuild the seed's 934-entry table once per file and hand
|
|
3008
3680
|
# back the +22.6 % this bound exists to avoid. Check that number before widening this key to match
|
|
3009
3681
|
# its siblings.
|
|
3682
|
+
#
|
|
3683
|
+
# ADR-129 WD1 — a hit in either way is audited against an index built with every memo bypassed, compared in
|
|
3684
|
+
# {DefinersIndex.canonical} form (a `Layered` index and a whole one hold the same owners, in any order).
|
|
3010
3685
|
def method_definers_index(kind)
|
|
3011
|
-
def_nodes = scope.discovered_def_nodes
|
|
3012
|
-
singleton_def_nodes = scope.discovered_singleton_def_nodes
|
|
3013
|
-
way = Thread.current[METHOD_DEFINERS_INDEX_KEY]
|
|
3014
|
-
unless way && way[0].equal?(def_nodes) && way[1].equal?(singleton_def_nodes)
|
|
3015
|
-
alt = Thread.current[METHOD_DEFINERS_INDEX_ALT_KEY]
|
|
3016
|
-
alt = nil unless alt && alt[0].equal?(def_nodes) && alt[1].equal?(singleton_def_nodes)
|
|
3017
|
-
Thread.current[METHOD_DEFINERS_INDEX_ALT_KEY] = way
|
|
3018
|
-
way = alt || [def_nodes, singleton_def_nodes, nil, nil]
|
|
3019
|
-
Thread.current[METHOD_DEFINERS_INDEX_KEY] = way
|
|
3020
|
-
end
|
|
3021
3686
|
singleton = kind == :singleton
|
|
3022
|
-
|
|
3687
|
+
table = singleton ? scope.discovered_singleton_def_nodes : scope.discovered_def_nodes
|
|
3688
|
+
return DefinersIndex.for(table) if MemoAudit.enabled? && MemoAudit.suspended?
|
|
3689
|
+
|
|
3690
|
+
state = typing_state
|
|
3691
|
+
primary = current_definers_way?(state[METHOD_DEFINERS_INDEX])
|
|
3692
|
+
way = primary ? state[METHOD_DEFINERS_INDEX] : rotate_definers_ways(state)
|
|
3693
|
+
position = singleton ? 3 : 2
|
|
3694
|
+
index = way[position]
|
|
3695
|
+
return way[position] = MemoAudit.key_audit("method definers index") { DefinersIndex.for(table) } if index.nil?
|
|
3696
|
+
|
|
3697
|
+
audit_definers_index(primary, index, kind, table) if MemoAudit.enabled?
|
|
3698
|
+
index
|
|
3699
|
+
end
|
|
3700
|
+
|
|
3701
|
+
# Whether `way` was built over this scope's two def tables.
|
|
3702
|
+
def current_definers_way?(way)
|
|
3703
|
+
!way.nil? && way[0].equal?(scope.discovered_def_nodes) && way[1].equal?(scope.discovered_singleton_def_nodes)
|
|
3023
3704
|
end
|
|
3024
3705
|
|
|
3025
|
-
def
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3706
|
+
# Makes the alternate way current when it was built over this scope's def tables, or a fresh way otherwise; the
|
|
3707
|
+
# way it replaces becomes the alternate.
|
|
3708
|
+
def rotate_definers_ways(state)
|
|
3709
|
+
alt = state[METHOD_DEFINERS_INDEX_ALT]
|
|
3710
|
+
alt = nil unless current_definers_way?(alt)
|
|
3711
|
+
state[METHOD_DEFINERS_INDEX_ALT] = state[METHOD_DEFINERS_INDEX]
|
|
3712
|
+
state[METHOD_DEFINERS_INDEX] =
|
|
3713
|
+
alt || [scope.discovered_def_nodes, scope.discovered_singleton_def_nodes, nil, nil]
|
|
3714
|
+
end
|
|
3029
3715
|
|
|
3030
|
-
|
|
3716
|
+
# A hit in the current way (`primary`) or the alternate one, checked in {DefinersIndex.canonical} form.
|
|
3717
|
+
def audit_definers_index(primary, index, kind, table)
|
|
3718
|
+
canonical = DefinersIndex.canonical(index)
|
|
3719
|
+
if primary
|
|
3720
|
+
MemoAudit.check("method definers index", canonical, kind) do
|
|
3721
|
+
DefinersIndex.canonical(DefinersIndex.for(table))
|
|
3722
|
+
end
|
|
3723
|
+
else
|
|
3724
|
+
MemoAudit.check("method definers index alt", canonical, kind) do
|
|
3725
|
+
DefinersIndex.canonical(DefinersIndex.for(table))
|
|
3726
|
+
end
|
|
3031
3727
|
end
|
|
3032
|
-
index
|
|
3033
3728
|
end
|
|
3034
3729
|
|
|
3035
3730
|
# True when `candidate`'s ancestor chain (superclasses + included/prepended modules) reaches `owner` —
|
|
@@ -3042,18 +3737,30 @@ module Rigor
|
|
|
3042
3737
|
!chain.search(scope) { |entry| entry.name == owner }.nil?
|
|
3043
3738
|
end
|
|
3044
3739
|
|
|
3045
|
-
|
|
3046
|
-
|
|
3740
|
+
# ADR-129 WD3 — an id-determined Resource: {TypingState::State#push} and {TypingState::State#pop} each take a
|
|
3741
|
+
# fresh id, so the stack's contents are fixed while the id is and a read of it refuses nothing.
|
|
3742
|
+
INFERENCE_GUARD = TypingState.resource(
|
|
3743
|
+
:user_method_inference_stack,
|
|
3744
|
+
read_exemption: :id_determined,
|
|
3745
|
+
reason: "the ADR-55 recursion guard, pushed and popped in place by each inference frame"
|
|
3746
|
+
)
|
|
3747
|
+
private_constant :INFERENCE_GUARD
|
|
3047
3748
|
|
|
3048
|
-
|
|
3049
|
-
|
|
3749
|
+
INFERENCE_UNROLL_FUEL = TypingState.resource(
|
|
3750
|
+
:user_method_unroll_fuel,
|
|
3751
|
+
reason: "ADR-55 unroll fuel, consumed in place per outermost entry"
|
|
3752
|
+
)
|
|
3753
|
+
private_constant :INFERENCE_UNROLL_FUEL
|
|
3050
3754
|
|
|
3051
3755
|
# ADR-55 slice 2 — thread-local fixpoint return-summary table, keyed by the plain `(receiver, method)`
|
|
3052
3756
|
# signature (NOT the value-extended signature: extended frames from slice 1 share the same summary).
|
|
3053
3757
|
# Each entry is `{ assumption:, consulted: }` where `assumption` is the current Kleene iterate (seeded
|
|
3054
3758
|
# `bot`) and `consulted` flips true when an in-cycle re-entry returns it.
|
|
3055
|
-
|
|
3056
|
-
|
|
3759
|
+
INFERENCE_SUMMARY = TypingState.resource(
|
|
3760
|
+
:user_method_return_summary,
|
|
3761
|
+
reason: "ADR-55 Kleene summaries and their `:consulted` flag, rewritten in place"
|
|
3762
|
+
)
|
|
3763
|
+
private_constant :INFERENCE_SUMMARY
|
|
3057
3764
|
|
|
3058
3765
|
# Per-thread append-only log of the seed depths of every in-flight summary `consult_summary` read
|
|
3059
3766
|
# (ADR-55 slice 2 mutual-recursion soundness fix, 2026-06-12). Each fixpoint owner records the guard
|
|
@@ -3064,8 +3771,11 @@ module Rigor
|
|
|
3064
3771
|
# `untyped` rather than fold one branch's seed into a "final" constant. Own-signature consults log
|
|
3065
3772
|
# depth == own depth, and a nested fixpoint that completes within the evaluation logs depths > own
|
|
3066
3773
|
# depth; neither is foreign. Cleared with the summary table when the guard stack drains to empty.
|
|
3067
|
-
|
|
3068
|
-
|
|
3774
|
+
SUMMARY_CONSULT_DEPTHS = TypingState.resource(
|
|
3775
|
+
:user_method_summary_consult_depths,
|
|
3776
|
+
reason: "ADR-55 consult log, appended in place and read as a delta"
|
|
3777
|
+
)
|
|
3778
|
+
private_constant :SUMMARY_CONSULT_DEPTHS
|
|
3069
3779
|
|
|
3070
3780
|
# ADR-57 follow-up — run-scoped memo for resolved user-method return types. The ADR-57 gate-open made
|
|
3071
3781
|
# every resolved in-body self-call adopt the callee's inferred return, which re-types the callee body
|
|
@@ -3088,7 +3798,7 @@ module Rigor
|
|
|
3088
3798
|
# post-hoc (ADR-84 WD3): candidacy only requires that the plain signature not already be on the
|
|
3089
3799
|
# recursion guard stack (a frame inside its own cycle returns a Kleene iterate by construction), and
|
|
3090
3800
|
# the store gate brackets the compute over the transient-event depth log
|
|
3091
|
-
# (
|
|
3801
|
+
# (TRANSIENT_EVENT_DEPTHS), refusing only when an event referenced a frame BELOW the bracket's
|
|
3092
3802
|
# entry depth — proof the ancestor context (an in-flight ancestor summary, the shared unroll fuel, a
|
|
3093
3803
|
# possibly-ancestor-caused clamp) influenced the result. Events at-or-above the entry depth are the
|
|
3094
3804
|
# compute's own deterministic machinery — a recursive method's own converged fixpoint stores from a
|
|
@@ -3110,11 +3820,70 @@ module Rigor
|
|
|
3110
3820
|
# `[receiver, *args]` descriptor tuple; a callee may appear under at most two node identities per run
|
|
3111
3821
|
# (the project-index parse every OTHER file resolves through, plus the defining file's own analysis
|
|
3112
3822
|
# parse). Only the current generation's bucket is retained (single slot) — a rolled-over run's entries
|
|
3113
|
-
# become garbage instead of accumulating across LSP runs
|
|
3114
|
-
#
|
|
3115
|
-
# so
|
|
3116
|
-
|
|
3117
|
-
|
|
3823
|
+
# become garbage instead of accumulating across LSP runs, and the run that filled the slot releases it
|
|
3824
|
+
# when it ends ({RunScopedSlots}; `Runner#run` keeps its bucket for `#return_summaries`).
|
|
3825
|
+
# `ExpressionTyper` is rebuilt per `Scope#type_of`, so the store lives in the thread's {TypingState}; fork-pool
|
|
3826
|
+
# workers are separate processes, so it never crosses a project boundary (ADR-84 WD5).
|
|
3827
|
+
#
|
|
3828
|
+
# ADR-129 WD1 — a Table, not a Memo: its WD3 serves a standalone result where a fresh compute inside a cycle
|
|
3829
|
+
# would be tainted, and its WD6 serves a stored `untyped`, so it is not extensional. Its reads and stores are
|
|
3830
|
+
# journaled ({#log_return_memo}).
|
|
3831
|
+
# ADR-129 WD3 — the return memo's entry protocol. The slot holds `[generation, bucket]` ({#return_memo_bucket}),
|
|
3832
|
+
# and a journal record keys an entry on `[def_node, memo_key]`. A replay writes into the bucket the slot holds: a
|
|
3833
|
+
# `type_of` memo entry lives no longer than its file's analysis, inside one run generation, so a slot with no
|
|
3834
|
+
# bucket means the entry outlived its run, which is a defect to surface rather than a bucket to invent.
|
|
3835
|
+
module ReturnMemoEntries
|
|
3836
|
+
module_function
|
|
3837
|
+
|
|
3838
|
+
def entry(slot_value, key)
|
|
3839
|
+
per_def = slot_value&.[](1)&.[](key[0])
|
|
3840
|
+
per_def.nil? ? TypingState::ABSENT : per_def.fetch(key[1], TypingState::ABSENT)
|
|
3841
|
+
end
|
|
3842
|
+
|
|
3843
|
+
def put(slot_value, key, value)
|
|
3844
|
+
(slot_value[1][key[0]] ||= {})[key[1]] = value
|
|
3845
|
+
end
|
|
3846
|
+
|
|
3847
|
+
def delete(slot_value, key)
|
|
3848
|
+
per_def = slot_value && slot_value[1][key[0]]
|
|
3849
|
+
per_def&.delete(key[1])
|
|
3850
|
+
end
|
|
3851
|
+
|
|
3852
|
+
def container(state, slot)
|
|
3853
|
+
state[slot] || raise(ArgumentError, "no return memo bucket to replay into: its entry outlived its run")
|
|
3854
|
+
end
|
|
3855
|
+
end
|
|
3856
|
+
|
|
3857
|
+
RETURN_MEMO = TypingState.table(
|
|
3858
|
+
:user_method_return_memo,
|
|
3859
|
+
run_scoped: true, computed: "return memo", entries: ReturnMemoEntries,
|
|
3860
|
+
reason: "ADR-84 return memo; not extensional (its WD3 and WD6), so its reads and stores are journaled"
|
|
3861
|
+
)
|
|
3862
|
+
private_constant :RETURN_MEMO
|
|
3863
|
+
|
|
3864
|
+
# ADR-129 WD1's key audit of a return-memo miss. The key carries the yield value ({YIELD_VALUE}, read through
|
|
3865
|
+
# `key_frame`). {.with_call_site_frame} installs the forwarded-argument list, derived from `arg_types`, repeat
|
|
3866
|
+
# depth zero and none of the caller's jump sinks or retry frames before the key is built, and
|
|
3867
|
+
# {#infer_user_method_return} installs the key-presence walk off, so the computation reads those forms. The
|
|
3868
|
+
# bucket keys on the run generation ({#return_memo_bucket}), within which ADR-130's run-level flags are fixed, so
|
|
3869
|
+
# the key carries them. Two Frames are read without a key term, each a known gap: the block-body threading
|
|
3870
|
+
# suppression (#2782) and Acceptance's unread-shape refusal (#2866).
|
|
3871
|
+
RETURN_MEMO_KEY_AUDIT = MemoAudit.key_declaration(
|
|
3872
|
+
forced: %i[forwarded_call_arg_types reflective_ivar_write_repeating key_presence_walk next_sink break_sink
|
|
3873
|
+
break_value_sink retry_frames],
|
|
3874
|
+
carried: {
|
|
3875
|
+
interface_erasure: "installed around a whole run; the bucket keys on the run generation",
|
|
3876
|
+
interface_dispatch_off: "installed around a whole run; the bucket keys on the run generation"
|
|
3877
|
+
},
|
|
3878
|
+
gaps: {
|
|
3879
|
+
threaded_block_body: "#2782: the return memo reads the block-body threading suppression without keying on " \
|
|
3880
|
+
"it; keying on it cost a fifth of lib's allocations, and clearing it per callee " \
|
|
3881
|
+
"frame re-walked unstorable frames exponentially",
|
|
3882
|
+
acceptance_unread_shape_refused: "#2866: a callee body typed inside Acceptance.refusing_unread " \
|
|
3883
|
+
"reads the caller's refusal, and the return memo does not key on it"
|
|
3884
|
+
}
|
|
3885
|
+
).freeze
|
|
3886
|
+
private_constant :RETURN_MEMO_KEY_AUDIT
|
|
3118
3887
|
|
|
3119
3888
|
# ADR-84 WD2 — one memo entry: the final inferred return plus, when the entry was computed under
|
|
3120
3889
|
# ADR-46 dependency recording, the frozen `Analysis::DependencyRecorder::ReadSet` its body walk
|
|
@@ -3126,7 +3895,10 @@ module Rigor
|
|
|
3126
3895
|
# when every return is unchanged, skips its symbol dependents. Carrying two frozen type refs is
|
|
3127
3896
|
# negligible on the memo-store path (already in scope) and never enters memo equality (entries are
|
|
3128
3897
|
# keyed by `memo_key`, retrieved by `.result` / `.read_set`).
|
|
3129
|
-
|
|
3898
|
+
#
|
|
3899
|
+
# Issue #1237 — `positionals_unknown` marks an entry a splat call stored ({#positional_correspondence}): the
|
|
3900
|
+
# session re-drives its key the same way ({#return_type_for}).
|
|
3901
|
+
MemoEntry = Data.define(:result, :read_set, :receiver, :arg_types, :positionals_unknown)
|
|
3130
3902
|
private_constant :MemoEntry
|
|
3131
3903
|
|
|
3132
3904
|
# ADR-84 WD3 — thread-local transient-machinery event log, appended by `note_transient_fallback` at
|
|
@@ -3182,8 +3954,11 @@ module Rigor
|
|
|
3182
3954
|
# sense above — position 0 is always a sound (conservative) choice; spec/rigor/inference/
|
|
3183
3955
|
# return_memo_taint_spec.rb pins this (no raw `BudgetTrace.hit` on the recursion categories outside
|
|
3184
3956
|
# the helper).
|
|
3185
|
-
|
|
3186
|
-
|
|
3957
|
+
TRANSIENT_EVENT_DEPTHS = TypingState.resource(
|
|
3958
|
+
:user_method_transient_event_depths,
|
|
3959
|
+
reason: "ADR-84 WD3 event log, appended in place and read as a delta"
|
|
3960
|
+
)
|
|
3961
|
+
private_constant :TRANSIENT_EVENT_DEPTHS
|
|
3187
3962
|
|
|
3188
3963
|
# Per-inference recursion context threaded through the guard / fixpoint helpers (ADR-55 slice 2).
|
|
3189
3964
|
# Bundles the call descriptor (`receiver`, `arg_types`, `plain_signature`), the thread-local summary
|
|
@@ -3191,7 +3966,7 @@ module Rigor
|
|
|
3191
3966
|
# carried separately (it is the body owner, not call context).
|
|
3192
3967
|
RecursionContext = Data.define(
|
|
3193
3968
|
:receiver, :arg_types, :plain_signature, :summaries, :would_have_been_guarded,
|
|
3194
|
-
:self_fold_safe
|
|
3969
|
+
:self_fold_safe, :positionals_unknown
|
|
3195
3970
|
)
|
|
3196
3971
|
private_constant :RecursionContext
|
|
3197
3972
|
|
|
@@ -3231,24 +4006,27 @@ module Rigor
|
|
|
3231
4006
|
# The return memo and the recursion guard do not key on them, which holds because such a node comes only from
|
|
3232
4007
|
# `DefNodeResolver.refinement_query`'s own parse and always carries that file's one list; a caller that types
|
|
3233
4008
|
# the same node under another list must add it to the key.
|
|
4009
|
+
#
|
|
4010
|
+
# Issue #1237 — `positionals_unknown:` re-types the body with no positional correspondence
|
|
4011
|
+
# ({#positional_correspondence}): `arg_types` is then the call's keyword shape alone. Such a frame reads and
|
|
4012
|
+
# stores the return memo under a key of its own ({#return_memo_key}), which the incremental session re-drives
|
|
4013
|
+
# with the same flag ({#return_type_for}), and installs no `...` forwarding list.
|
|
3234
4014
|
def infer_user_method_return(def_node, receiver, arg_types, self_fold_safe: false, yield_type: nil,
|
|
3235
|
-
refinements: nil)
|
|
4015
|
+
refinements: nil, positionals_unknown: false)
|
|
3236
4016
|
return nil if def_node.body.nil?
|
|
3237
|
-
unless KeyPresenceGuard.active?
|
|
3238
|
-
return infer_user_method_return_unguarded(def_node, receiver, arg_types, self_fold_safe, yield_type,
|
|
3239
|
-
refinements)
|
|
3240
|
-
end
|
|
3241
4017
|
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
4018
|
+
call = [def_node, receiver, arg_types, self_fold_safe, yield_type, refinements, positionals_unknown]
|
|
4019
|
+
return infer_user_method_return_unguarded(*call) unless KeyPresenceGuard.forcing_off?
|
|
4020
|
+
|
|
4021
|
+
KeyPresenceGuard.without_guards { infer_user_method_return_unguarded(*call) }
|
|
3245
4022
|
end
|
|
3246
4023
|
|
|
3247
4024
|
def infer_user_method_return_unguarded(def_node, receiver, arg_types, self_fold_safe, yield_type,
|
|
3248
|
-
refinements
|
|
4025
|
+
refinements, positionals_unknown)
|
|
3249
4026
|
yield_type = nil unless yield_type && body_yields?(def_node)
|
|
3250
4027
|
body_scope = build_user_method_body_scope(def_node, receiver, arg_types,
|
|
3251
|
-
self_fold_safe: self_fold_safe
|
|
4028
|
+
self_fold_safe: self_fold_safe,
|
|
4029
|
+
positionals_unknown: positionals_unknown)
|
|
3252
4030
|
return nil if body_scope.nil?
|
|
3253
4031
|
|
|
3254
4032
|
body_scope = body_scope.with_declared_refinements(refinements) if refinements
|
|
@@ -3261,8 +4039,7 @@ module Rigor
|
|
|
3261
4039
|
# resolving during the main walk. `describe(:short)` keeps non-Nominal receivers (the implicit
|
|
3262
4040
|
# `Object` carrier for top-level / DSL-block defs) printable.
|
|
3263
4041
|
plain_signature = [receiver.describe(:short), def_node.name]
|
|
3264
|
-
stack = (
|
|
3265
|
-
summaries = (Thread.current[INFERENCE_SUMMARY_KEY] ||= {})
|
|
4042
|
+
stack = typing_state.stack(INFERENCE_GUARD)
|
|
3266
4043
|
|
|
3267
4044
|
# WD0 (RIGOR_BUDGET_TRACE) — one entry into user-method return inference. No-op when disabled.
|
|
3268
4045
|
BudgetTrace.hit(BudgetTrace::MEMO_ENTRIES)
|
|
@@ -3285,11 +4062,12 @@ module Rigor
|
|
|
3285
4062
|
# below reads it back from here rather than taking a second parameter, so key and frame cannot
|
|
3286
4063
|
# disagree). It is installed even when nil, which is what stops a blockless callee reached from
|
|
3287
4064
|
# inside a yielding body from inheriting the outer caller's block.
|
|
3288
|
-
|
|
4065
|
+
forwarded = positionals_unknown ? nil : forwarded_frame_types(def_node, arg_types)
|
|
4066
|
+
ExpressionTyper.with_call_site_frame(yield_type, forwarded) do
|
|
3289
4067
|
unless memo_candidate?(stack, plain_signature)
|
|
3290
4068
|
trace_memo_refusal(stack, plain_signature)
|
|
3291
|
-
next compute_user_method_return(def_node, body_scope, stack,
|
|
3292
|
-
|
|
4069
|
+
next compute_user_method_return(def_node, body_scope, stack, receiver, arg_types, plain_signature,
|
|
4070
|
+
positionals_unknown: positionals_unknown)
|
|
3293
4071
|
end
|
|
3294
4072
|
|
|
3295
4073
|
# INVARIANT (ADR-46 recording soundness, ADR-84 WD2) — the deep cross-file dependency edges (the
|
|
@@ -3306,8 +4084,8 @@ module Rigor
|
|
|
3306
4084
|
# (ActiveStorage video_analyzer.rb's subtree, 0.43s -> >90s; PR #79) and stays rejected. Pinned by
|
|
3307
4085
|
# spec/rigor/inference/return_memo_recording_spec.rb (cross-file replay completeness) and
|
|
3308
4086
|
# dependency_recorder_spec.rb's transitive deep-edge example.
|
|
3309
|
-
consult_and_store_return_memo(def_node, body_scope, stack,
|
|
3310
|
-
|
|
4087
|
+
consult_and_store_return_memo(def_node, body_scope, stack, receiver, arg_types, plain_signature,
|
|
4088
|
+
positionals_unknown: positionals_unknown)
|
|
3311
4089
|
end
|
|
3312
4090
|
end
|
|
3313
4091
|
|
|
@@ -3328,9 +4106,15 @@ module Rigor
|
|
|
3328
4106
|
# replaced with the analysis generation cannot outlive the nodes it holds.
|
|
3329
4107
|
def body_yields?(def_node)
|
|
3330
4108
|
cache = class_graph_buckets[:yields]
|
|
3331
|
-
|
|
4109
|
+
if cache.key?(def_node)
|
|
4110
|
+
return MemoAudit.check("class graph yields", cache[def_node], def_node.name) do
|
|
4111
|
+
yield_reachable?(def_node.body, def_node.parameters&.block&.name)
|
|
4112
|
+
end
|
|
4113
|
+
end
|
|
3332
4114
|
|
|
3333
|
-
cache[def_node] =
|
|
4115
|
+
cache[def_node] = MemoAudit.key_audit("class graph yields") do
|
|
4116
|
+
yield_reachable?(def_node.body, def_node.parameters&.block&.name)
|
|
4117
|
+
end
|
|
3334
4118
|
end
|
|
3335
4119
|
|
|
3336
4120
|
def yield_reachable?(node, block_name)
|
|
@@ -3361,7 +4145,7 @@ module Rigor
|
|
|
3361
4145
|
# The candidate-frame memo path: consult the current run generation's bucket, and on a miss compute
|
|
3362
4146
|
# and store a FINAL result. Reached only when `memo_candidate?` held (see `infer_user_method_return`).
|
|
3363
4147
|
# The store gate (ADR-84 WD3): a result is stored when no transient-machinery event during the
|
|
3364
|
-
# bracket referenced a stack frame BELOW the bracket's entry depth (see
|
|
4148
|
+
# bracket referenced a stack frame BELOW the bracket's entry depth (see TRANSIENT_EVENT_DEPTHS).
|
|
3365
4149
|
# A separate ADR-55 fixpoint-consult bracket counter used to sit in front of this test; it is gone
|
|
3366
4150
|
# (issue #875) because the event log already subsumes it — the sole `consult_summary` call site is
|
|
3367
4151
|
# the in-cycle guard hit, which logs its event at the consulted owner's position first, so a consult
|
|
@@ -3375,8 +4159,11 @@ module Rigor
|
|
|
3375
4159
|
# is already `Dynamic[top]` is stored anyway — see `top_result?`. A hit under ADR-46 recording
|
|
3376
4160
|
# replays the entry's captured read-set into the current consumer (see the INVARIANT comment at the
|
|
3377
4161
|
# call site).
|
|
3378
|
-
|
|
3379
|
-
|
|
4162
|
+
#
|
|
4163
|
+
# ADR-129 WD3 — the ADR-55 summaries are not read here: a hit neither reads nor writes them, and a miss's
|
|
4164
|
+
# compute takes them ({#return_summaries}).
|
|
4165
|
+
def consult_and_store_return_memo(def_node, body_scope, stack, receiver, arg_types, plain_signature,
|
|
4166
|
+
positionals_unknown: false)
|
|
3380
4167
|
per_def = (return_memo_bucket[def_node] ||= {})
|
|
3381
4168
|
# Issue #525 — the `:self` fold-safety grant is a THIRD call-site-varying dimension: the same
|
|
3382
4169
|
# `(receiver, arg_types)` body returns a folded member type when the caller's receiver expression
|
|
@@ -3387,11 +4174,8 @@ module Rigor
|
|
|
3387
4174
|
# passed in for the same reason: a yielding callee's return depends on the block THIS call site
|
|
3388
4175
|
# hands it, so two call sites passing differently-typed blocks must not share an entry. It is nil
|
|
3389
4176
|
# for every def that cannot reach a `yield`, which is where the memo's mass is.
|
|
3390
|
-
memo_key =
|
|
3391
|
-
|
|
3392
|
-
body_scope.struct_fold_safe?(:self),
|
|
3393
|
-
ExpressionTyper.current_yield_value_type&.describe(:short)]
|
|
3394
|
-
if (entry = per_def[memo_key])
|
|
4177
|
+
memo_key = return_memo_key(body_scope, receiver, arg_types, positionals_unknown: positionals_unknown)
|
|
4178
|
+
if (entry = recall_return_memo(per_def, def_node, memo_key))
|
|
3395
4179
|
BudgetTrace.hit(BudgetTrace::MEMO_HITS)
|
|
3396
4180
|
Analysis::DependencyRecorder.replay(entry.read_set) if Analysis::DependencyRecorder.active?
|
|
3397
4181
|
return entry.result
|
|
@@ -3401,20 +4185,65 @@ module Rigor
|
|
|
3401
4185
|
|
|
3402
4186
|
entry_depth = stack.size
|
|
3403
4187
|
event_mark = transient_event_mark
|
|
3404
|
-
result, read_set =
|
|
3405
|
-
|
|
4188
|
+
result, read_set = MemoAudit.key_audit("return memo", RETURN_MEMO_KEY_AUDIT, memo_key) do
|
|
4189
|
+
compute_with_read_capture(def_node, body_scope, stack, receiver, arg_types, plain_signature,
|
|
4190
|
+
positionals_unknown)
|
|
4191
|
+
end
|
|
3406
4192
|
|
|
3407
4193
|
if context_tainted?(event_mark, entry_depth) && !top_result?(result)
|
|
3408
4194
|
BudgetTrace.hit(BudgetTrace::MEMO_REFUSE_TRANSIENT)
|
|
4195
|
+
per_def[RETURN_MEMO_REFUSED] = true
|
|
3409
4196
|
else
|
|
3410
|
-
per_def
|
|
3411
|
-
|
|
4197
|
+
store_return_memo(per_def, def_node, memo_key,
|
|
4198
|
+
MemoEntry.new(result: result, read_set: read_set, receiver: receiver, arg_types: arg_types,
|
|
4199
|
+
positionals_unknown: positionals_unknown))
|
|
3412
4200
|
end
|
|
3413
4201
|
result
|
|
3414
4202
|
end
|
|
3415
4203
|
|
|
4204
|
+
# The return memo's key: the call descriptor, the body scope's `:self` grant, and the yield-value Frame, read as a
|
|
4205
|
+
# key term (ADR-129 WD1's key audit). Issue #1237 — a frame with no positional correspondence keys one term more,
|
|
4206
|
+
# {POSITIONALS_UNKNOWN_KEY}, so it never shares an entry with a positional call whose list reads the same; every
|
|
4207
|
+
# other key is as it was.
|
|
4208
|
+
def return_memo_key(body_scope, receiver, arg_types, positionals_unknown: false)
|
|
4209
|
+
MemoAudit.key do
|
|
4210
|
+
key = [receiver.describe(:short),
|
|
4211
|
+
arg_types.map { |type| type.describe(:short) },
|
|
4212
|
+
body_scope.struct_fold_safe?(:self),
|
|
4213
|
+
typing_state.key_frame(YIELD_VALUE)&.describe(:short)]
|
|
4214
|
+
positionals_unknown ? key << POSITIONALS_UNKNOWN_KEY : key
|
|
4215
|
+
end
|
|
4216
|
+
end
|
|
4217
|
+
|
|
4218
|
+
POSITIONALS_UNKNOWN_KEY = :positionals_unknown
|
|
4219
|
+
private_constant :POSITIONALS_UNKNOWN_KEY
|
|
4220
|
+
|
|
4221
|
+
# The entry `per_def` holds for `memo_key`, or nil, journaled as a Table read (ADR-129 WD3).
|
|
4222
|
+
def recall_return_memo(per_def, def_node, memo_key)
|
|
4223
|
+
entry = per_def[memo_key]
|
|
4224
|
+
log_return_memo(:read, def_node, memo_key, entry || TypingState::ABSENT)
|
|
4225
|
+
entry
|
|
4226
|
+
end
|
|
4227
|
+
|
|
4228
|
+
# Stores `entry` for `memo_key`, journaled as a Table write (ADR-129 WD3), logged before the store so a priors
|
|
4229
|
+
# window notes the entry it replaces.
|
|
4230
|
+
def store_return_memo(per_def, def_node, memo_key, entry)
|
|
4231
|
+
log_return_memo(:write, def_node, memo_key, entry)
|
|
4232
|
+
per_def[memo_key] = entry
|
|
4233
|
+
end
|
|
4234
|
+
|
|
4235
|
+
# ADR-129 WD3 — journals a return-memo read or store under `[def_node, memo_key]`. The composite key is built
|
|
4236
|
+
# only while a capture is open, so a run with the `type_of` memo off allocates nothing here.
|
|
4237
|
+
def log_return_memo(kind, def_node, memo_key, value)
|
|
4238
|
+
state = typing_state
|
|
4239
|
+
return unless state.capturing?
|
|
4240
|
+
|
|
4241
|
+
key = [def_node, memo_key].freeze
|
|
4242
|
+
kind == :read ? state.log_read(RETURN_MEMO, key, value) : state.log_write(RETURN_MEMO, key, value)
|
|
4243
|
+
end
|
|
4244
|
+
|
|
3416
4245
|
def transient_event_mark
|
|
3417
|
-
|
|
4246
|
+
typing_state.read(TRANSIENT_EVENT_DEPTHS)&.size || 0
|
|
3418
4247
|
end
|
|
3419
4248
|
|
|
3420
4249
|
# ADR-84 WD3 — true when a transient-machinery event logged during the bracket (entries past
|
|
@@ -3426,7 +4255,7 @@ module Rigor
|
|
|
3426
4255
|
# outermost entry, so the suffix slice allocated an array per candidate compute — a term quadratic in
|
|
3427
4256
|
# the event count, and the `ary_ensure_room_for_push` / GC-sweep profile issue #870 sampled.
|
|
3428
4257
|
def context_tainted?(event_mark, entry_depth)
|
|
3429
|
-
log =
|
|
4258
|
+
log = typing_state.read(TRANSIENT_EVENT_DEPTHS)
|
|
3430
4259
|
return false if log.nil? || log.size <= event_mark
|
|
3431
4260
|
|
|
3432
4261
|
index = event_mark
|
|
@@ -3458,31 +4287,29 @@ module Rigor
|
|
|
3458
4287
|
# happen in practice is the byte-identical `--format json` runs over `lib`, `plugins/*/lib`,
|
|
3459
4288
|
# `examples/*/lib` and the survey projects recorded on issue #872.
|
|
3460
4289
|
def top_result?(result)
|
|
3461
|
-
|
|
4290
|
+
Type::Combinator.untyped?(result)
|
|
3462
4291
|
end
|
|
3463
4292
|
|
|
3464
4293
|
# ADR-84 WD2 — the memo-miss compute, wrapped in a `DependencyRecorder.capture` window when ADR-46
|
|
3465
4294
|
# recording is active so the entry can carry its replayable read-set. Returns `[result, read_set]`
|
|
3466
4295
|
# (`read_set` nil when not recording).
|
|
3467
|
-
def compute_with_read_capture(def_node, body_scope, stack,
|
|
3468
|
-
|
|
4296
|
+
def compute_with_read_capture(def_node, body_scope, stack, receiver, arg_types, plain_signature,
|
|
4297
|
+
positionals_unknown)
|
|
4298
|
+
call = [def_node, body_scope, stack, receiver, arg_types, plain_signature]
|
|
3469
4299
|
unless Analysis::DependencyRecorder.active?
|
|
3470
|
-
return [compute_user_method_return(
|
|
3471
|
-
receiver, arg_types, plain_signature), nil]
|
|
4300
|
+
return [compute_user_method_return(*call, positionals_unknown: positionals_unknown), nil]
|
|
3472
4301
|
end
|
|
3473
4302
|
|
|
3474
|
-
Analysis::DependencyRecorder.capture
|
|
3475
|
-
compute_user_method_return(def_node, body_scope, stack, summaries,
|
|
3476
|
-
receiver, arg_types, plain_signature)
|
|
3477
|
-
end
|
|
4303
|
+
Analysis::DependencyRecorder.capture { compute_user_method_return(*call, positionals_unknown: positionals_unknown) }
|
|
3478
4304
|
end
|
|
3479
4305
|
|
|
3480
4306
|
# The ADR-55 recursion-guard + value-unroll + fixpoint body of user-method return inference, factored
|
|
3481
4307
|
# out so `infer_user_method_return` is a thin memo wrapper (the memo is the ADR-57 follow-up; this is
|
|
3482
4308
|
# unchanged from pre-memo behaviour).
|
|
3483
|
-
def compute_user_method_return(def_node, body_scope, stack,
|
|
3484
|
-
|
|
4309
|
+
def compute_user_method_return(def_node, body_scope, stack, receiver, arg_types, plain_signature,
|
|
4310
|
+
positionals_unknown: false)
|
|
3485
4311
|
trace_body_eval(plain_signature)
|
|
4312
|
+
summaries = return_summaries
|
|
3486
4313
|
# ADR-55 slice 1: when every bound argument is value-pinned, extend the guard key with a stable
|
|
3487
4314
|
# descriptor of the argument *values* so distinct constant frames may recurse (e.g. `factorial(5)`
|
|
3488
4315
|
# folds to `Constant[120]`). Distinct constant frames are bounded by `RECURSION_UNROLL_FUEL` per
|
|
@@ -3502,7 +4329,11 @@ module Rigor
|
|
|
3502
4329
|
# `bot`) instead of bare `untyped`. The fixpoint loop below seeds the entry on the outermost frame;
|
|
3503
4330
|
# if a re-entry beats it here the entry already exists. The WD4 composition: slice 1's clamp/fuel
|
|
3504
4331
|
# fallbacks also route here when a summary is active.
|
|
3505
|
-
|
|
4332
|
+
# Issue #3162 — the summary is read only where its frame covers the re-entry's own parameter
|
|
4333
|
+
# bindings and block. A value-keyed re-entry is tested like any other: the unroll frame it
|
|
4334
|
+
# re-enters holds its constants, but the summary it reads is the outermost frame's.
|
|
4335
|
+
floor = uncovered_reentry_floor(summaries[plain_signature], def_node, body_scope, positionals_unknown)
|
|
4336
|
+
return floor || consult_summary(summaries, plain_signature)
|
|
3506
4337
|
end
|
|
3507
4338
|
|
|
3508
4339
|
# ADR-55 WD1 clamp (governing rule): the constant-arg unroll may only ever surface a fully
|
|
@@ -3522,11 +4353,16 @@ module Rigor
|
|
|
3522
4353
|
# Issue #525 — read off the built scope so the bot-collapse retry rebuilds a widened scope with
|
|
3523
4354
|
# the SAME `:self` polarity the first attempt used; a retry that silently dropped the grant would
|
|
3524
4355
|
# return a different type for the same call.
|
|
3525
|
-
self_fold_safe: body_scope.struct_fold_safe?(:self)
|
|
4356
|
+
self_fold_safe: body_scope.struct_fold_safe?(:self),
|
|
4357
|
+
positionals_unknown: positionals_unknown
|
|
3526
4358
|
)
|
|
3527
4359
|
evaluate_guarded_user_method_body(def_node, body_scope, stack, signature, context)
|
|
3528
4360
|
end
|
|
3529
4361
|
|
|
4362
|
+
# ADR-55 — the thread's in-flight fixpoint summaries, created on first use. Taken only by a compute, never on a
|
|
4363
|
+
# return-memo hit (ADR-129 WD3), and cleared when the guard stack drains.
|
|
4364
|
+
def return_summaries = typing_state.acquire(INFERENCE_SUMMARY) { {} }
|
|
4365
|
+
|
|
3530
4366
|
# Issue #1125 — the argument list a `def m(...)` body's `...` re-supplies, or nil for a def without the
|
|
3531
4367
|
# forwarding parameter (whose body cannot contain a `...` call at all) and for a call that does not even
|
|
3532
4368
|
# satisfy `m`'s own required positionals (that call raises at runtime; the body is not worth re-typing).
|
|
@@ -3545,8 +4381,9 @@ module Rigor
|
|
|
3545
4381
|
kw_shape = positional.pop if takes_keywords?(params) && positional.last.is_a?(Type::HashShape)
|
|
3546
4382
|
return nil if positional.size < params.requireds.size
|
|
3547
4383
|
|
|
4384
|
+
# Frozen: a Frame value does not change while it is installed (ADR-129 WD1).
|
|
3548
4385
|
tail = positional[(params.requireds.size + params.optionals.size)..] || []
|
|
3549
|
-
kw_shape ? tail + [kw_shape] : tail
|
|
4386
|
+
(kw_shape ? tail + [kw_shape] : tail).freeze
|
|
3550
4387
|
end
|
|
3551
4388
|
|
|
3552
4389
|
# True when this frame's result is a candidate for the return memo: the one structural precondition,
|
|
@@ -3567,13 +4404,14 @@ module Rigor
|
|
|
3567
4404
|
# `def_nodes` table identity for runner-less scopes (single-file probes) — the pre-WD2 per-file
|
|
3568
4405
|
# scope. A generation mismatch drops the previous bucket whole, so a re-run in one process (LSP,
|
|
3569
4406
|
# ADR-62 warm loop) can neither hit stale entries nor accumulate them. The bucket maps `def_node`
|
|
3570
|
-
# (object identity, held strongly — see
|
|
4407
|
+
# (object identity, held strongly — see RETURN_MEMO) to its per-descriptor entries.
|
|
3571
4408
|
def return_memo_bucket
|
|
3572
4409
|
generation = scope.run_generation || scope.discovered_def_nodes
|
|
3573
|
-
|
|
4410
|
+
state = typing_state
|
|
4411
|
+
slot = state[RETURN_MEMO]
|
|
3574
4412
|
unless slot && slot[0].equal?(generation)
|
|
3575
4413
|
slot = [generation, {}.compare_by_identity]
|
|
3576
|
-
|
|
4414
|
+
state[RETURN_MEMO] = slot
|
|
3577
4415
|
end
|
|
3578
4416
|
slot[1]
|
|
3579
4417
|
end
|
|
@@ -3623,10 +4461,10 @@ module Rigor
|
|
|
3623
4461
|
# referenced frame's stack position to the load-bearing thread-local event log (regardless of
|
|
3624
4462
|
# RIGOR_BUDGET_TRACE) and forwards the category to `BudgetTrace`. The taint spec pins that no
|
|
3625
4463
|
# recursion-machinery `BudgetTrace.hit` exists outside this helper, so a future fallback cannot
|
|
3626
|
-
# silently join unlogged (see the audit table at
|
|
4464
|
+
# silently join unlogged (see the audit table at TRANSIENT_EVENT_DEPTHS; `context_depth` 0 is
|
|
3627
4465
|
# always a sound conservative choice).
|
|
3628
4466
|
def note_transient_fallback(category, context_depth)
|
|
3629
|
-
(
|
|
4467
|
+
typing_state.append(TRANSIENT_EVENT_DEPTHS, context_depth)
|
|
3630
4468
|
BudgetTrace.hit(category)
|
|
3631
4469
|
end
|
|
3632
4470
|
|
|
@@ -3638,25 +4476,53 @@ module Rigor
|
|
|
3638
4476
|
# The outermost frame for this plain signature owns the summary entry and runs the fixpoint loop.
|
|
3639
4477
|
# ADR-55 WD2.
|
|
3640
4478
|
outermost = stack.none? { |frame| plain_part(frame) == context.plain_signature }
|
|
3641
|
-
stack
|
|
4479
|
+
first_unpinned = first_unpinned_frame?(stack, signature, context)
|
|
4480
|
+
state = typing_state
|
|
4481
|
+
state.push(INFERENCE_GUARD, signature)
|
|
3642
4482
|
begin
|
|
3643
4483
|
if outermost
|
|
3644
4484
|
fixpoint_user_method_return(def_node, body_scope, context)
|
|
4485
|
+
elsif first_unpinned
|
|
4486
|
+
nested_fixpoint_user_method_return(def_node, body_scope, context)
|
|
3645
4487
|
else
|
|
3646
4488
|
type, = evaluate_body_with_returns(body_scope, def_node.body)
|
|
3647
4489
|
clamp_unroll_result(type, context.would_have_been_guarded)
|
|
3648
4490
|
end
|
|
3649
4491
|
ensure
|
|
3650
|
-
|
|
4492
|
+
state.pop(INFERENCE_GUARD)
|
|
3651
4493
|
if stack.empty?
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
4494
|
+
state.write(INFERENCE_UNROLL_FUEL, nil)
|
|
4495
|
+
state.write(INFERENCE_SUMMARY, nil)
|
|
4496
|
+
state.write(SUMMARY_CONSULT_DEPTHS, nil)
|
|
4497
|
+
state.write(TRANSIENT_EVENT_DEPTHS, nil)
|
|
3656
4498
|
end
|
|
3657
4499
|
end
|
|
3658
4500
|
end
|
|
3659
4501
|
|
|
4502
|
+
# Issue #3162 — whether this frame is the first of its signature with an unpinned argument: every
|
|
4503
|
+
# frame of the signature below it is a constant-argument unroll frame, and the recursion has now
|
|
4504
|
+
# passed something that is no constant (`rec(k - 1, *rest)` out of `rec(3, 1)`). The outermost
|
|
4505
|
+
# frame's summary is the return of a body typed for its constants, which does not cover this
|
|
4506
|
+
# frame's re-entries, so this frame owns a summary of its own. A frame that is all constants but
|
|
4507
|
+
# out of unroll fuel is not one: its re-entries keep reading the outermost frame's summary, whose
|
|
4508
|
+
# `bot` sends the outermost frame to the widened retry. Asked before the frame is pushed.
|
|
4509
|
+
def first_unpinned_frame?(stack, signature, context)
|
|
4510
|
+
plain_signature = context.plain_signature
|
|
4511
|
+
signature.equal?(plain_signature) && stack.none?(plain_signature) &&
|
|
4512
|
+
constant_argument_value_key(context.arg_types).nil?
|
|
4513
|
+
end
|
|
4514
|
+
|
|
4515
|
+
# The fixpoint of a {#first_unpinned_frame?} frame, run over its own summary entry; the outermost
|
|
4516
|
+
# frame's entry is put back afterwards.
|
|
4517
|
+
def nested_fixpoint_user_method_return(def_node, body_scope, context)
|
|
4518
|
+
summaries = context.summaries
|
|
4519
|
+
outer = summaries[context.plain_signature]
|
|
4520
|
+
fixpoint_user_method_return(def_node, body_scope, context)
|
|
4521
|
+
ensure
|
|
4522
|
+
summaries[context.plain_signature] = outer
|
|
4523
|
+
LoopValueRecord.advance_generation
|
|
4524
|
+
end
|
|
4525
|
+
|
|
3660
4526
|
# Evaluates a method body and joins the value types of every explicit `return value` reached during
|
|
3661
4527
|
# the walk with the body's tail type.
|
|
3662
4528
|
#
|
|
@@ -3685,14 +4551,11 @@ module Rigor
|
|
|
3685
4551
|
def fixpoint_user_method_return(def_node, body_scope, context, widened: false)
|
|
3686
4552
|
plain_signature = context.plain_signature
|
|
3687
4553
|
summaries = context.summaries
|
|
3688
|
-
depth = seed_fixpoint_summary(summaries, plain_signature)
|
|
3689
|
-
consult_depths = (Thread.current[SUMMARY_CONSULT_DEPTHS_KEY] ||= [])
|
|
4554
|
+
depth = seed_fixpoint_summary(summaries, plain_signature, body_scope, context.receiver)
|
|
3690
4555
|
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
type, = evaluate_body_with_returns(body_scope, def_node.body)
|
|
3695
|
-
computed = clamp_unroll_result(type, context.would_have_been_guarded)
|
|
4556
|
+
iteration = 0
|
|
4557
|
+
while iteration < RECURSION_FIXPOINT_CAP
|
|
4558
|
+
computed, entangled = evaluate_fixpoint_iterate(def_node, body_scope, context, depth)
|
|
3696
4559
|
|
|
3697
4560
|
# Cross-signature mutual recursion (ADR-55 soundness fix, 2026-06-12): the evaluation consulted an
|
|
3698
4561
|
# ANCESTOR signature's in-flight summary (seed depth shallower than this frame's), so `computed`
|
|
@@ -3700,9 +4563,7 @@ module Rigor
|
|
|
3700
4563
|
# `Constant[false]`. The per-signature iteration below cannot converge such an entangled pair (each
|
|
3701
4564
|
# side's iterate is conditioned on the other's unfinished assumption), so degrade this frame to the
|
|
3702
4565
|
# sound `untyped` floor instead of surfacing a one-sided value.
|
|
3703
|
-
|
|
3704
|
-
return degrade_entangled_fixpoint(summaries, plain_signature)
|
|
3705
|
-
end
|
|
4566
|
+
return degrade_entangled_fixpoint(summaries, plain_signature) if entangled
|
|
3706
4567
|
|
|
3707
4568
|
# The summary was never consulted — the method did not recurse on this evaluation, so there is no
|
|
3708
4569
|
# fixpoint to chase. Return the computed type directly (pre-fixpoint behaviour for non-recursive
|
|
@@ -3720,26 +4581,153 @@ module Rigor
|
|
|
3720
4581
|
return resolved unless resolved.nil?
|
|
3721
4582
|
end
|
|
3722
4583
|
|
|
4584
|
+
# Issue #3162 — a re-entry read the summary for arguments this frame's parameters do not cover
|
|
4585
|
+
# (`acc(n - 1, k + 1)` against `k = 1`), so `computed` holds this frame's base case as that
|
|
4586
|
+
# call's answer. The evaluation is discarded and the body typed again with such a re-entry
|
|
4587
|
+
# answering `Dynamic[top]`; it is not a Kleene step.
|
|
4588
|
+
next if floored_uncovered_reentries?(summaries[plain_signature])
|
|
4589
|
+
|
|
3723
4590
|
converged = fixpoint_step(summaries, plain_signature, computed, iteration)
|
|
3724
4591
|
return converged if converged
|
|
4592
|
+
|
|
4593
|
+
iteration += 1
|
|
3725
4594
|
end
|
|
3726
4595
|
|
|
3727
4596
|
# Out of iterations and still unstable. The collapse is the loop's own tail rather than a branch
|
|
3728
|
-
# inside `fixpoint_step`:
|
|
3729
|
-
# tail-less loop reads as a path that returns an Integer summary (the `def.return-type-mismatch`
|
|
3730
|
-
# on `return_type_for` this shape retired), and the loop, not the step, owns its cap.
|
|
4597
|
+
# inside `fixpoint_step`: the loop, not the step, owns its cap.
|
|
3731
4598
|
collapse_fixpoint_cap(summaries, plain_signature)
|
|
3732
4599
|
end
|
|
3733
4600
|
|
|
4601
|
+
# One body evaluation of the fixpoint loop, as `[computed, entangled]`: the clamped return, and
|
|
4602
|
+
# whether the evaluation consulted an ancestor signature's in-flight summary (one seeded shallower
|
|
4603
|
+
# than `depth`). Clears the summary's per-evaluation record first.
|
|
4604
|
+
def evaluate_fixpoint_iterate(def_node, body_scope, context, depth)
|
|
4605
|
+
entry = context.summaries[context.plain_signature]
|
|
4606
|
+
entry[:consulted] = false
|
|
4607
|
+
entry[:uncovered] = false
|
|
4608
|
+
consult_depths = typing_state.acquire(SUMMARY_CONSULT_DEPTHS) { [] }
|
|
4609
|
+
consult_mark = consult_depths.size
|
|
4610
|
+
type, = evaluate_body_with_returns(body_scope, def_node.body)
|
|
4611
|
+
computed = clamp_unroll_result(type, context.would_have_been_guarded)
|
|
4612
|
+
[computed, consult_depths[consult_mark..].any? { |consulted| consulted < depth }]
|
|
4613
|
+
end
|
|
4614
|
+
|
|
4615
|
+
# Switches the summary to its floor after an evaluation in which an uncovered re-entry read it, and
|
|
4616
|
+
# says whether it did. Once on the floor such a re-entry never reads the summary again, so the
|
|
4617
|
+
# switch happens at most once per fixpoint.
|
|
4618
|
+
#
|
|
4619
|
+
# A `bot` iterate has been to the widened retry first. When that retry found no exit either
|
|
4620
|
+
# (`spin`), the entry here is the retry's own, which noted nothing, and the answer stays `bot`.
|
|
4621
|
+
def floored_uncovered_reentries?(entry)
|
|
4622
|
+
return false unless entry[:uncovered] && !entry[:floor]
|
|
4623
|
+
|
|
4624
|
+
entry[:floor] = true
|
|
4625
|
+
LoopValueRecord.advance_generation
|
|
4626
|
+
true
|
|
4627
|
+
end
|
|
4628
|
+
|
|
4629
|
+
# Issue #3162 — the answer of a re-entry the summary's frame does not cover, or nil for one that
|
|
4630
|
+
# reads the summary. The uncovered re-entry is noted on the entry; until the fixpoint owner has
|
|
4631
|
+
# switched the summary to its floor ({#floored_uncovered_reentries?}) it still reads the summary,
|
|
4632
|
+
# so a body whose iterate is `bot` reaches the widened retry as before. On the floor it answers
|
|
4633
|
+
# `untyped`, the pre-slice-2 observable for an in-cycle call.
|
|
4634
|
+
def uncovered_reentry_floor(entry, def_node, body_scope, positionals_unknown)
|
|
4635
|
+
return nil if entry.nil?
|
|
4636
|
+
return nil if summary_covers_reentry?(entry, def_node, body_scope.locals, positionals_unknown)
|
|
4637
|
+
|
|
4638
|
+
entry[:uncovered] = true
|
|
4639
|
+
return nil unless entry[:floor]
|
|
4640
|
+
|
|
4641
|
+
scope.record_dynamic_origin(@typing_node, DynamicOrigin::ANALYZER_BUDGET_CUTOFF) if @typing_node
|
|
4642
|
+
Type::Combinator.untyped
|
|
4643
|
+
end
|
|
4644
|
+
|
|
4645
|
+
# Issue #3162 — whether the summary's frame covers a re-entry's parameter bindings (`locals`, as the
|
|
4646
|
+
# guard site built them): every parameter of the frame holds a type that includes what the re-entry
|
|
4647
|
+
# bound it to. The summary is the return of the frame's body under the frame's bindings, so it is
|
|
4648
|
+
# the re-entry's return only then; a parameter passed through unchanged is the same type object.
|
|
4649
|
+
#
|
|
4650
|
+
# A re-entry through a call-site `*splat` has no positional correspondence, and its binder holds
|
|
4651
|
+
# `Dynamic[top]` for every positional. There the def's own calls are read instead
|
|
4652
|
+
# ({RecursionRebinding}): a positional every one of them passes through unchanged (`*rest` as
|
|
4653
|
+
# `*rest`) is covered, and any other is not.
|
|
4654
|
+
#
|
|
4655
|
+
# The block is part of the frame too: `yield` answers the frame's block ({#reentry_block_covered?}).
|
|
4656
|
+
def summary_covers_reentry?(entry, def_node, locals, positionals_unknown)
|
|
4657
|
+
return false unless reentry_block_covered?(entry, def_node)
|
|
4658
|
+
|
|
4659
|
+
kept = (entry[:kept] ||= kept_positional_parameters(def_node, entry[:receiver])) if positionals_unknown
|
|
4660
|
+
frame = entry[:locals]
|
|
4661
|
+
locals.all? do |name, argument|
|
|
4662
|
+
bound = frame[name]
|
|
4663
|
+
bound.nil? || kept&.include?(name) || binding_covers?(bound, argument)
|
|
4664
|
+
end
|
|
4665
|
+
end
|
|
4666
|
+
|
|
4667
|
+
# Whether the re-entry runs with a block the summary's frame covers. A re-entry with no block, or
|
|
4668
|
+
# one that passes the def's own block parameter on (`&blk`, or the anonymous `&`), does; `&blk` is
|
|
4669
|
+
# the frame's block only while the body never assigns `blk` and no block or lambda in it declares
|
|
4670
|
+
# the name ({RecursionRebinding}'s write and block-local scan, as for a positional). One that
|
|
4671
|
+
# passes another `&expression` does not. A block literal does when the value it yields is covered
|
|
4672
|
+
# by the value the frame's block yields, which is nil on both sides for a def that never `yield`s.
|
|
4673
|
+
# The re-entry's call is the node being typed; with none to read, only the yield values are compared.
|
|
4674
|
+
def reentry_block_covered?(entry, def_node)
|
|
4675
|
+
block = @typing_node.block if @typing_node.is_a?(Prism::CallNode)
|
|
4676
|
+
return true if @typing_node.is_a?(Prism::CallNode) && block.nil?
|
|
4677
|
+
return own_block_passed?(def_node, block) if block.is_a?(Prism::BlockArgumentNode)
|
|
4678
|
+
|
|
4679
|
+
yielded = ExpressionTyper.current_yield_value_type
|
|
4680
|
+
return true if yielded.nil?
|
|
4681
|
+
|
|
4682
|
+
frame_yield = entry[:yield]
|
|
4683
|
+
!frame_yield.nil? && binding_covers?(frame_yield, yielded)
|
|
4684
|
+
end
|
|
4685
|
+
|
|
4686
|
+
def own_block_passed?(def_node, block)
|
|
4687
|
+
params = def_node.parameters
|
|
4688
|
+
own = params.block if params.is_a?(Prism::ParametersNode)
|
|
4689
|
+
return false if own.nil?
|
|
4690
|
+
|
|
4691
|
+
expression = block.expression
|
|
4692
|
+
return own.name.nil? if expression.nil?
|
|
4693
|
+
return false unless expression.is_a?(Prism::LocalVariableReadNode) && expression.name == own.name
|
|
4694
|
+
|
|
4695
|
+
written = RecursionRebinding.rebound_parameters(def_node, Set.new)
|
|
4696
|
+
written != RecursionRebinding::ALL && !written.include?(own.name)
|
|
4697
|
+
end
|
|
4698
|
+
|
|
4699
|
+
# `2` is covered by `Integer` though their union is spelled `2 | Integer`, so the argument is also
|
|
4700
|
+
# tried with its value pins widened.
|
|
4701
|
+
def binding_covers?(bound, argument)
|
|
4702
|
+
return true if argument.equal?(bound) || Type::Combinator.untyped?(bound) || argument == bound
|
|
4703
|
+
return false if Type::Combinator.untyped?(argument)
|
|
4704
|
+
return true if Type::Combinator.union(bound, argument) == bound
|
|
4705
|
+
|
|
4706
|
+
Type::Combinator.union(bound, widen_bound_argument(argument)) == bound
|
|
4707
|
+
end
|
|
4708
|
+
|
|
4709
|
+
def kept_positional_parameters(def_node, receiver)
|
|
4710
|
+
params = def_node.parameters
|
|
4711
|
+
rebound = recursion_rebound_parameters(def_node, receiver)
|
|
4712
|
+
return [] if rebound == RecursionRebinding::ALL || !params.is_a?(Prism::ParametersNode)
|
|
4713
|
+
|
|
4714
|
+
positionals = params.requireds + params.optionals + params.posts + [params.rest]
|
|
4715
|
+
positionals.filter_map { |param| param.name if param.respond_to?(:name) && param.name } - rebound.to_a
|
|
4716
|
+
end
|
|
4717
|
+
|
|
3734
4718
|
# Seeds the thread-local summary entry for a fixpoint owner: the `bot` Kleene seed plus the
|
|
3735
4719
|
# guard-stack depth at seed time (the frame for this signature is already pushed), which
|
|
3736
4720
|
# `consult_summary` logs so nested fixpoints can detect a foreign in-flight (ancestor) consult.
|
|
3737
4721
|
# Returns the seed depth. ADR-55 slice 2.
|
|
3738
|
-
def seed_fixpoint_summary(summaries, plain_signature)
|
|
3739
|
-
depth =
|
|
4722
|
+
def seed_fixpoint_summary(summaries, plain_signature, body_scope, receiver)
|
|
4723
|
+
depth = typing_state[INFERENCE_GUARD]&.size || 0
|
|
3740
4724
|
summaries[plain_signature] = {
|
|
3741
|
-
assumption: Type::Combinator.bot, consulted: false, depth: depth
|
|
4725
|
+
assumption: Type::Combinator.bot, consulted: false, depth: depth,
|
|
4726
|
+
# Issue #3162 — the frame the summary is the return of, for {#summary_covers_reentry?}.
|
|
4727
|
+
locals: body_scope.locals, yield: ExpressionTyper.current_yield_value_type, receiver: receiver,
|
|
4728
|
+
uncovered: false, floor: false
|
|
3742
4729
|
}
|
|
4730
|
+
LoopValueRecord.advance_generation
|
|
3743
4731
|
depth
|
|
3744
4732
|
end
|
|
3745
4733
|
|
|
@@ -3752,13 +4740,14 @@ module Rigor
|
|
|
3752
4740
|
note_transient_fallback(BudgetTrace::RECURSION_GUARD, own_guard_frame_position)
|
|
3753
4741
|
scope.record_dynamic_origin(@typing_node, DynamicOrigin::ANALYZER_BUDGET_CUTOFF) if @typing_node
|
|
3754
4742
|
summaries[plain_signature][:assumption] = Type::Combinator.untyped
|
|
4743
|
+
LoopValueRecord.advance_generation
|
|
3755
4744
|
Type::Combinator.untyped
|
|
3756
4745
|
end
|
|
3757
4746
|
|
|
3758
4747
|
# The stack position of the currently-evaluating owner frame (the guard stack's top) — the
|
|
3759
4748
|
# self-referential `context_depth` for events that are deterministic per owner (ADR-84 WD3).
|
|
3760
4749
|
def own_guard_frame_position
|
|
3761
|
-
size =
|
|
4750
|
+
size = typing_state[INFERENCE_GUARD]&.size || 0
|
|
3762
4751
|
size.positive? ? size - 1 : 0
|
|
3763
4752
|
end
|
|
3764
4753
|
|
|
@@ -3779,6 +4768,7 @@ module Rigor
|
|
|
3779
4768
|
return candidate if joined == assumption
|
|
3780
4769
|
|
|
3781
4770
|
summaries[plain_signature][:assumption] = joined
|
|
4771
|
+
LoopValueRecord.advance_generation
|
|
3782
4772
|
nil
|
|
3783
4773
|
end
|
|
3784
4774
|
|
|
@@ -3790,17 +4780,67 @@ module Rigor
|
|
|
3790
4780
|
note_transient_fallback(BudgetTrace::RECURSION_FIXPOINT_CAP, own_guard_frame_position)
|
|
3791
4781
|
scope.record_dynamic_origin(@typing_node, DynamicOrigin::ANALYZER_BUDGET_CUTOFF) if @typing_node
|
|
3792
4782
|
summaries[plain_signature][:assumption] = Type::Combinator.untyped
|
|
4783
|
+
LoopValueRecord.advance_generation
|
|
3793
4784
|
Type::Combinator.untyped
|
|
3794
4785
|
end
|
|
3795
4786
|
|
|
3796
4787
|
# Rebuilds the user-method body scope with every bound positional parameter widened to its nominal
|
|
3797
4788
|
# base (`1 | 2 | 3` → `Integer`, `Constant[:x]` → `Symbol`). Used by the bot-collapse retry in
|
|
3798
4789
|
# `fixpoint_user_method_return`: call-site argument narrowing can prune a recursive method's base
|
|
3799
|
-
# case, and widening restores the declared-type view under which the base case is reachable.
|
|
3800
|
-
#
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
4790
|
+
# case, and widening restores the declared-type view under which the base case is reachable. A
|
|
4791
|
+
# parameter the def's own recursive calls rebind is bound `Dynamic[top]` instead ({RecursionRebinding}).
|
|
4792
|
+
# Returns `nil` when the parameter shape is not inferable (mirrors `build_user_method_body_scope`).
|
|
4793
|
+
def widened_user_method_body_scope(def_node, receiver, arg_types, self_fold_safe: false,
|
|
4794
|
+
positionals_unknown: false)
|
|
4795
|
+
widened_args = arg_types.map { |arg_type| widen_bound_argument(arg_type) }
|
|
4796
|
+
build_user_method_body_scope(def_node, receiver, widened_args,
|
|
4797
|
+
self_fold_safe: self_fold_safe, positionals_unknown: positionals_unknown,
|
|
4798
|
+
rebound: recursion_rebound_parameters(def_node, receiver))
|
|
4799
|
+
end
|
|
4800
|
+
|
|
4801
|
+
# Issue #3115 — {RecursionRebinding.rebound_parameters} for `def_node`, told the def's other names: a name
|
|
4802
|
+
# the body calls is an alias when `receiver`'s class resolves it to this same def (`alias al2 al1` makes
|
|
4803
|
+
# `al2(n - 1, !f)` a recursive call). A receiver whose class cannot be asked rebinds every parameter.
|
|
4804
|
+
def recursion_rebound_parameters(def_node, receiver)
|
|
4805
|
+
names = Set[def_node.name]
|
|
4806
|
+
RecursionRebinding.called_names(def_node.body).each do |name|
|
|
4807
|
+
next if name == def_node.name
|
|
4808
|
+
|
|
4809
|
+
resolved = recursion_def_for(receiver, name)
|
|
4810
|
+
return RecursionRebinding::ALL if resolved == :unknown
|
|
4811
|
+
|
|
4812
|
+
names << name if resolved.equal?(def_node)
|
|
4813
|
+
end
|
|
4814
|
+
RecursionRebinding.rebound_parameters(def_node, names)
|
|
4815
|
+
end
|
|
4816
|
+
|
|
4817
|
+
def recursion_def_for(receiver, method_name)
|
|
4818
|
+
return scope.singleton_def_for(receiver.class_name, method_name) if receiver.is_a?(Type::Singleton)
|
|
4819
|
+
return :unknown unless user_inference_receiver?(receiver)
|
|
4820
|
+
|
|
4821
|
+
resolve_user_def_through_ancestors(receiver.class_name, method_name)
|
|
4822
|
+
end
|
|
4823
|
+
|
|
4824
|
+
# Issue #3115 — one argument of the widened retry. A keyword argument reaches the binder inside the call's
|
|
4825
|
+
# trailing `HashShape`, which {#widen_value_pinned} leaves as it is (a shape is not a value pin), so a
|
|
4826
|
+
# keyword parameter kept its call-site pin through the retry and the base case stayed pruned: the body
|
|
4827
|
+
# then converged on the `bot` seed. A shape carrier keeps its keys, policy and arity, and the values it
|
|
4828
|
+
# holds widen at every depth: a `HashShape`'s values, a `Tuple`'s elements and a `Union`'s members.
|
|
4829
|
+
def widen_bound_argument(arg_type)
|
|
4830
|
+
case arg_type
|
|
4831
|
+
when Type::HashShape
|
|
4832
|
+
Type::HashShape.new(
|
|
4833
|
+
arg_type.pairs.transform_values { |value| widen_bound_argument(value) },
|
|
4834
|
+
required_keys: arg_type.required_keys, optional_keys: arg_type.optional_keys,
|
|
4835
|
+
read_only_keys: arg_type.read_only_keys, extra_keys: arg_type.extra_keys
|
|
4836
|
+
)
|
|
4837
|
+
when Type::Tuple
|
|
4838
|
+
Type::Combinator.tuple_of(*arg_type.elements.map { |element| widen_bound_argument(element) })
|
|
4839
|
+
when Type::Union
|
|
4840
|
+
Type::Combinator.union(*arg_type.members.map { |member| widen_bound_argument(member) })
|
|
4841
|
+
else
|
|
4842
|
+
widen_value_pinned(arg_type)
|
|
4843
|
+
end
|
|
3804
4844
|
end
|
|
3805
4845
|
|
|
3806
4846
|
# ADR-55 slice 2 bot-collapse resolution (2026-06-11). Called when a fixpoint iteration computed `bot`
|
|
@@ -3821,7 +4861,8 @@ module Rigor
|
|
|
3821
4861
|
def resolve_bot_collapse(def_node, context, widened:)
|
|
3822
4862
|
unless widened
|
|
3823
4863
|
widened_scope = widened_user_method_body_scope(def_node, context.receiver, context.arg_types,
|
|
3824
|
-
self_fold_safe: context.self_fold_safe
|
|
4864
|
+
self_fold_safe: context.self_fold_safe,
|
|
4865
|
+
positionals_unknown: context.positionals_unknown)
|
|
3825
4866
|
return fixpoint_user_method_return(def_node, widened_scope, context, widened: true) unless widened_scope.nil?
|
|
3826
4867
|
end
|
|
3827
4868
|
|
|
@@ -3863,7 +4904,7 @@ module Rigor
|
|
|
3863
4904
|
return Type::Combinator.untyped if entry.nil?
|
|
3864
4905
|
|
|
3865
4906
|
entry[:consulted] = true
|
|
3866
|
-
(
|
|
4907
|
+
typing_state.append(SUMMARY_CONSULT_DEPTHS, entry[:depth])
|
|
3867
4908
|
entry[:assumption]
|
|
3868
4909
|
end
|
|
3869
4910
|
|
|
@@ -3898,14 +4939,15 @@ module Rigor
|
|
|
3898
4939
|
# Consumes one unit from the thread-local unroll-fuel counter and returns the units that were
|
|
3899
4940
|
# available *before* this consumption (so a positive return means the extended value-key may be used).
|
|
3900
4941
|
# Fuel is per-outermost inference entry: at the top level (empty guard stack) it seeds to
|
|
3901
|
-
# `RECURSION_UNROLL_FUEL`, and the `ensure` in `
|
|
4942
|
+
# `RECURSION_UNROLL_FUEL`, and the `ensure` in `evaluate_guarded_user_method_body` clears it once the stack
|
|
3902
4943
|
# drains back to empty. On exhaustion (return 0) it records a `RECURSION_UNROLL_FUEL` hit so the caller
|
|
3903
4944
|
# keeps the plain `(receiver, method)` signature — today's behaviour.
|
|
3904
4945
|
def unroll_fuel_remaining(stack)
|
|
3905
|
-
|
|
4946
|
+
state = typing_state
|
|
4947
|
+
remaining = state.read(INFERENCE_UNROLL_FUEL)
|
|
3906
4948
|
remaining = RECURSION_UNROLL_FUEL if remaining.nil? || stack.empty?
|
|
3907
4949
|
if remaining.positive?
|
|
3908
|
-
|
|
4950
|
+
state.write(INFERENCE_UNROLL_FUEL, remaining - 1)
|
|
3909
4951
|
else
|
|
3910
4952
|
# ADR-84 WD3 — position 0: fuel is one shared resource seeded at the stack bottom, so its
|
|
3911
4953
|
# exhaustion is context-dependent for every nested bracket.
|
|
@@ -3954,14 +4996,33 @@ module Rigor
|
|
|
3954
4996
|
total += descriptor.last
|
|
3955
4997
|
end
|
|
3956
4998
|
[["t", parts], total]
|
|
4999
|
+
when Type::HashShape then pinned_shape_descriptor(arg)
|
|
5000
|
+
end
|
|
5001
|
+
end
|
|
5002
|
+
|
|
5003
|
+
# Issue #3115 — a call's keyword arguments arrive as one closed `HashShape`, so `m(n: 2)` is as
|
|
5004
|
+
# value-pinned as `m(2)` when every entry is. An open shape, or one with an optional key, does not name
|
|
5005
|
+
# one argument list and stays on the type-keyed path.
|
|
5006
|
+
def pinned_shape_descriptor(shape)
|
|
5007
|
+
return nil unless shape.closed? && shape.optional_keys.empty?
|
|
5008
|
+
|
|
5009
|
+
parts = []
|
|
5010
|
+
total = 1
|
|
5011
|
+
shape.pairs.each do |key, value|
|
|
5012
|
+
descriptor = pinned_value_descriptor(value)
|
|
5013
|
+
return nil if descriptor.nil?
|
|
5014
|
+
|
|
5015
|
+
parts << [key.inspect, descriptor.first]
|
|
5016
|
+
total += descriptor.last
|
|
3957
5017
|
end
|
|
5018
|
+
[["h", parts], total]
|
|
3958
5019
|
end
|
|
3959
5020
|
|
|
3960
5021
|
# Builds the body scope for a user-defined instance method call: a fresh `Scope` with `self_type` set
|
|
3961
5022
|
# to the receiver's nominal type, the project-wide accumulators inherited (so the body sees the same
|
|
3962
5023
|
# `discovered_classes` / `class_ivars` / etc. the caller does), and the call's `arg_types` bound to
|
|
3963
5024
|
# the parameters. Returns nil when the binding is truly ambiguous (arity mismatch, trailing `post`
|
|
3964
|
-
# params
|
|
5025
|
+
# params).
|
|
3965
5026
|
#
|
|
3966
5027
|
# Issue #524 — the binder is per-PARAMETER, not per-signature. The first iteration declined any def
|
|
3967
5028
|
# with an optional / rest / keyword / block parameter outright, so ONE `options = {}` default zeroed
|
|
@@ -3978,11 +5039,25 @@ module Rigor
|
|
|
3978
5039
|
# through it. Record a per-file write kind in the census instead, as {ScopeIndexer::CONSTANT_MEMO} does
|
|
3979
5040
|
# (#1276). Setting `source_path` here was rejected: plugins read it, and the return memo would become
|
|
3980
5041
|
# caller-dependent.
|
|
3981
|
-
|
|
5042
|
+
#
|
|
5043
|
+
# Issue #3115 — `rebound:` is the widened retry's set ({#widened_user_method_body_scope}): the parameters
|
|
5044
|
+
# the def's own recursive calls rebind ({RecursionRebinding}), each bound `Dynamic[top]` whether the call
|
|
5045
|
+
# wrote the slot or a default filled it. The retry types the body for every call the recursion makes,
|
|
5046
|
+
# and such a slot holds whatever a recursive call passes (`tog(n: n - 1, f: !f)` against `f: false`).
|
|
5047
|
+
def build_user_method_body_scope(def_node, receiver, arg_types, self_fold_safe: false,
|
|
5048
|
+
positionals_unknown: false, rebound: nil)
|
|
3982
5049
|
params = def_node.parameters
|
|
3983
|
-
locals =
|
|
5050
|
+
locals =
|
|
5051
|
+
if positionals_unknown
|
|
5052
|
+
bind_params_without_correspondence(params, arg_types.last)
|
|
5053
|
+
else
|
|
5054
|
+
bind_params_from_call_types(params, arg_types)
|
|
5055
|
+
end
|
|
3984
5056
|
return nil if locals.nil?
|
|
3985
5057
|
|
|
5058
|
+
rebound = locals.keys if rebound == RecursionRebinding::ALL
|
|
5059
|
+
rebound&.each { |name| locals[name] = dynamic_top if locals.key?(name) }
|
|
5060
|
+
|
|
3986
5061
|
# Construct the body scope in a SINGLE Scope allocation (plus the issue #1358 frame it carries, whose
|
|
3987
5062
|
# scans run only on demand) — the previous `Scope.empty.with_*.with_*…` chain
|
|
3988
5063
|
# allocated a fresh frozen Scope per field, run per user-method-call inference (ADR-44). The
|
|
@@ -4009,7 +5084,7 @@ module Rigor
|
|
|
4009
5084
|
body_scope.with_repeated_or_writes(RepeatedOrWrites.straight_line_sites(def_node.body, body_scope))
|
|
4010
5085
|
end
|
|
4011
5086
|
|
|
4012
|
-
# Issue #681 / #707 — the
|
|
5087
|
+
# Issue #681 / #707 — the typer's answer to "what `Module.nesting` was recorded for this
|
|
4013
5088
|
# def node". `Scope::DiscoveryIndex#discovered_def_nestings` is the owner; the resolver memo is that
|
|
4014
5089
|
# table's REHYDRATION for the one population it cannot hold — a def served from an ADR-85 seed bundle,
|
|
4015
5090
|
# whose node {Inference::DefNodeResolver} mints from its own parse, so no table keyed by the identities
|
|
@@ -4024,7 +5099,7 @@ module Rigor
|
|
|
4024
5099
|
# run and an `--incremental` recheck answering the same constant for the same body; the rehydration half
|
|
4025
5100
|
# is populated only inside `DefNodeResolver.with_run`, and outside it this falls back to the peel.
|
|
4026
5101
|
def recorded_def_nesting(def_node)
|
|
4027
|
-
scope.discovery.
|
|
5102
|
+
scope.discovery.recorded_def_nesting(def_node)
|
|
4028
5103
|
end
|
|
4029
5104
|
|
|
4030
5105
|
# The body scope's fold-safe set: the body's own struct locals, plus issue #525's `:self` sentinel
|
|
@@ -4047,9 +5122,15 @@ module Rigor
|
|
|
4047
5122
|
def self_fold_safe_grant?(def_node, receiver)
|
|
4048
5123
|
per_def = (class_graph_buckets[:self_pure][def_node] ||= {})
|
|
4049
5124
|
key = [receiver.class_name, receiver.member_names]
|
|
4050
|
-
|
|
5125
|
+
if per_def.key?(key)
|
|
5126
|
+
return MemoAudit.check("class graph self pure", per_def[key], def_node.name, key) do
|
|
5127
|
+
self_fold_safe_body?(def_node.body, receiver, [def_node])
|
|
5128
|
+
end
|
|
5129
|
+
end
|
|
4051
5130
|
|
|
4052
|
-
per_def[key] =
|
|
5131
|
+
per_def[key] = MemoAudit.key_audit("class graph self pure") do
|
|
5132
|
+
self_fold_safe_body?(def_node.body, receiver, [def_node])
|
|
5133
|
+
end
|
|
4053
5134
|
end
|
|
4054
5135
|
|
|
4055
5136
|
# Whether `body`'s every use of `self` is a pure read, resolving each unrecognised self-call against
|
|
@@ -4084,6 +5165,11 @@ module Rigor
|
|
|
4084
5165
|
locals = bind_positional_params(params, positional)
|
|
4085
5166
|
return nil if locals.nil?
|
|
4086
5167
|
|
|
5168
|
+
bind_keyword_and_block_params(params, kw_shape, locals)
|
|
5169
|
+
end
|
|
5170
|
+
|
|
5171
|
+
# The keyword, `**rest` and block parameters' bindings, added to `locals`, which it returns.
|
|
5172
|
+
def bind_keyword_and_block_params(params, kw_shape, locals)
|
|
4087
5173
|
bind_keyword_params(params, kw_shape, locals)
|
|
4088
5174
|
rest_name = keyword_rest_name(params)
|
|
4089
5175
|
locals[rest_name.to_sym] = keyword_rest_type(params, kw_shape) if rest_name
|
|
@@ -4091,6 +5177,25 @@ module Rigor
|
|
|
4091
5177
|
locals
|
|
4092
5178
|
end
|
|
4093
5179
|
|
|
5180
|
+
# Issue #1237 — the locals table for a call whose positional correspondence is unknown (a `*splat` at the
|
|
5181
|
+
# call site). Every positional parameter (required, optional, trailing) binds `Dynamic[top]` and the rest
|
|
5182
|
+
# parameter an `Array[untyped]`; the keyword parameters bind from `keywords` as {#bind_keyword_params} does
|
|
5183
|
+
# for any call, so an open shape leaves them `Dynamic[top]`. An optional's default is not used: the splat
|
|
5184
|
+
# may supply the slot.
|
|
5185
|
+
def bind_params_without_correspondence(params, keywords)
|
|
5186
|
+
return {} if params.nil?
|
|
5187
|
+
return nil unless params.is_a?(Prism::ParametersNode)
|
|
5188
|
+
|
|
5189
|
+
locals = {}
|
|
5190
|
+
(params.requireds + params.posts).each { |param| bind_positional_param(locals, param, dynamic_top) }
|
|
5191
|
+
params.optionals.each { |param| locals[param.name.to_sym] = dynamic_top }
|
|
5192
|
+
rest = params.rest
|
|
5193
|
+
if rest.respond_to?(:name) && rest.name
|
|
5194
|
+
locals[rest.name.to_sym] = Type::Combinator.nominal_of("Array", type_args: [dynamic_top])
|
|
5195
|
+
end
|
|
5196
|
+
bind_keyword_and_block_params(params, keywords, locals)
|
|
5197
|
+
end
|
|
5198
|
+
|
|
4094
5199
|
# Trailing required positionals after a rest (`def f(a, *m, z)`) shift the correspondence and stay
|
|
4095
5200
|
# declined — correspondence, not width, is the issue. Issue #1125: `def m(...)`'s forwarding slot is no
|
|
4096
5201
|
# longer a decline. Its NAMED parameters (a leading `def m(a, ...)` required) bind exactly as before;
|
|
@@ -4236,7 +5341,7 @@ module Rigor
|
|
|
4236
5341
|
# now carry what the callee needs:
|
|
4237
5342
|
#
|
|
4238
5343
|
# - `f(...)` inside `def m(...)` re-supplies the arguments `m` was called with, read from the frame
|
|
4239
|
-
# {#infer_user_method_return} installed ({
|
|
5344
|
+
# {#infer_user_method_return} installed ({FORWARDED_ARGS}) — so it expands to a whole LIST, not
|
|
4240
5345
|
# one type, which is why the map became a flat_map.
|
|
4241
5346
|
# - a keyword hash built ENTIRELY from a double splat (`f(**h)`) is the shape of `h`, so `f(**h)`
|
|
4242
5347
|
# binds the same parameters the literal form `f(a: 1, b: 2)` does.
|
|
@@ -4332,7 +5437,7 @@ module Rigor
|
|
|
4332
5437
|
)
|
|
4333
5438
|
block_return_for(
|
|
4334
5439
|
block_arg, expected,
|
|
4335
|
-
call_node: call_node,
|
|
5440
|
+
call_node: call_node, receiver_type: receiver_type,
|
|
4336
5441
|
narrowing: block_body_self_narrowing(call_node, receiver_type),
|
|
4337
5442
|
repeats: !BLOCK_VALUE_DISCARDING.include?(call_node.name) && block_may_repeat?(call_node, receiver_type)
|
|
4338
5443
|
)
|
|
@@ -4353,13 +5458,15 @@ module Rigor
|
|
|
4353
5458
|
|
|
4354
5459
|
# Whether the call may run its block more than once, so a later run reads a captured binding an earlier
|
|
4355
5460
|
# run rebound — the premise of laying the #587 (b) binding ({#captured_block_bindings}) under the block.
|
|
4356
|
-
# {BlockRepetition.may_repeat?} holds the rule, which the statement pass shares (issue #1412).
|
|
4357
|
-
#
|
|
4358
|
-
#
|
|
4359
|
-
#
|
|
5461
|
+
# {BlockRepetition.may_repeat?} holds the rule, which the statement pass shares (issue #1412). A project callee
|
|
5462
|
+
# whose body may run the block more than once on one path ({ProjectYielder.block_value_may_repeat?}) also
|
|
5463
|
+
# counts: a `yield` in a loop or a core iterator's block (`xs.each { |x| v = yield(x) }`, issue #2303), or a
|
|
5464
|
+
# hand-off (`g(x, &)`, issue #2301), whose target may run it many times, so a captured local the block rebinds
|
|
5465
|
+
# must not read its entry binding. A callee that runs it once per path (`f ? yield(1) : yield(2)`,
|
|
5466
|
+
# `then { yield }`) keeps the single-run value.
|
|
4360
5467
|
def block_may_repeat?(call_node, receiver_type)
|
|
4361
5468
|
BlockRepetition.may_repeat?(method_name: call_node.name, receiver_type: receiver_type, scope: scope) ||
|
|
4362
|
-
ProjectYielder.
|
|
5469
|
+
ProjectYielder.block_value_may_repeat?(call_node, receiver_type, scope)
|
|
4363
5470
|
end
|
|
4364
5471
|
|
|
4365
5472
|
EMPTY_BREAK_ARMS = [].freeze
|
|
@@ -4403,7 +5510,7 @@ module Rigor
|
|
|
4403
5510
|
param_types = break_arm_param_types(call_node, receiver)
|
|
4404
5511
|
block_scope = block_entry_scope(
|
|
4405
5512
|
block_node, param_types,
|
|
4406
|
-
call_node: call_node,
|
|
5513
|
+
call_node: call_node, receiver_type: receiver,
|
|
4407
5514
|
narrowing: block_body_self_narrowing(call_node, receiver),
|
|
4408
5515
|
captured: repeating_captured_bindings(block_node, param_types, block_may_repeat?(call_node, receiver))
|
|
4409
5516
|
)
|
|
@@ -4446,12 +5553,13 @@ module Rigor
|
|
|
4446
5553
|
)
|
|
4447
5554
|
end
|
|
4448
5555
|
|
|
4449
|
-
def block_return_for(block_arg, expected, call_node: nil, narrowing: nil, repeats: false)
|
|
5556
|
+
def block_return_for(block_arg, expected, call_node: nil, narrowing: nil, repeats: false, receiver_type: nil)
|
|
4450
5557
|
case block_arg
|
|
4451
5558
|
when Prism::BlockNode
|
|
4452
5559
|
captured = repeating_captured_bindings(block_arg, expected, repeats)
|
|
4453
5560
|
entry = block_entry_scope(
|
|
4454
|
-
block_arg, expected,
|
|
5561
|
+
block_arg, expected,
|
|
5562
|
+
call_node: call_node, narrowing: narrowing, captured: captured, receiver_type: receiver_type
|
|
4455
5563
|
)
|
|
4456
5564
|
type_block_body(block_arg, entry, captured: captured)
|
|
4457
5565
|
when Prism::BlockArgumentNode
|
|
@@ -4479,8 +5587,16 @@ module Rigor
|
|
|
4479
5587
|
# Issue #316 — mirrors `StatementEvaluator#build_block_entry_scope`: the block body's `self` is the
|
|
4480
5588
|
# yielding method's business, so the return-typing pass must see the same unmodelled-self mark. Issue #1358
|
|
4481
5589
|
# — and the same match-global view ({MatchRebinding.block_entry}), which reads the owning `call_node`.
|
|
4482
|
-
|
|
4483
|
-
|
|
5590
|
+
#
|
|
5591
|
+
# Issue #2861 — and the same self-preserving entry ({SelfPreservingBlock}), read off `receiver_type`.
|
|
5592
|
+
def block_entry_scope(block_node, expected, call_node: nil, narrowing: nil, captured: nil, receiver_type: nil)
|
|
5593
|
+
self_entry = if SelfPreservingBlock.call?(call_node, receiver_type, scope)
|
|
5594
|
+
scope.entering_self_preserving_block
|
|
5595
|
+
else
|
|
5596
|
+
scope.entering_opaque_block
|
|
5597
|
+
end
|
|
5598
|
+
entry = MatchRebinding.block_entry(self_entry, block_node, call_node)
|
|
5599
|
+
entry = entry.entering_block_without_gradual_marks(block_node)
|
|
4484
5600
|
entry = captured.lay(entry) if captured
|
|
4485
5601
|
block_scope = BlockParameterBinder.new(expected_param_types: expected).bind_onto(block_node, entry)
|
|
4486
5602
|
return block_scope unless narrowing
|
|
@@ -4750,23 +5866,20 @@ module Rigor
|
|
|
4750
5866
|
# Suppression flag for {#threaded_block_body_type} — set while a fold is running (so a fold never nests)
|
|
4751
5867
|
# and while the per-element Tuple fold is over its arity cap. Thread-local because block typing is
|
|
4752
5868
|
# re-entrant within one thread and the fork-pool workers each own their own.
|
|
4753
|
-
|
|
4754
|
-
|
|
5869
|
+
THREADED_BLOCK_BODY = TypingState.frame(
|
|
5870
|
+
:threaded_block_body, reason: "a suppression flag installed per fold",
|
|
5871
|
+
witness: "spec/fixtures/typing_state_witnesses/threaded_block_body.rb"
|
|
5872
|
+
)
|
|
5873
|
+
private_constant :THREADED_BLOCK_BODY
|
|
4755
5874
|
|
|
4756
5875
|
def block_body_threading_suppressed?
|
|
4757
|
-
|
|
5876
|
+
typing_state[THREADED_BLOCK_BODY] ? true : false
|
|
4758
5877
|
end
|
|
4759
5878
|
|
|
4760
5879
|
# Runs the block with the scope-threading fold suppressed, restoring the previous state (not clearing
|
|
4761
5880
|
# it) on the way out, so nesting a suppressed region inside another cannot re-enable threading.
|
|
4762
|
-
def without_block_body_threading
|
|
4763
|
-
|
|
4764
|
-
Thread.current[THREADED_BLOCK_BODY_KEY] = true
|
|
4765
|
-
begin
|
|
4766
|
-
yield
|
|
4767
|
-
ensure
|
|
4768
|
-
Thread.current[THREADED_BLOCK_BODY_KEY] = previous
|
|
4769
|
-
end
|
|
5881
|
+
def without_block_body_threading(&)
|
|
5882
|
+
TypingState.current.with(THREADED_BLOCK_BODY, true, &)
|
|
4770
5883
|
end
|
|
4771
5884
|
|
|
4772
5885
|
# Every variable-write form whose name `StatementEvaluator` threads into the following statement's
|
|
@@ -4961,11 +6074,13 @@ module Rigor
|
|
|
4961
6074
|
# position matches. `r = [1, 2].find(-> { 0 }) { |e| e > 5 }` is `0` at runtime; the walk answered `nil`,
|
|
4962
6075
|
# and `r + 1` then reported a nil receiver on correct code. The other supported methods take no argument.
|
|
4963
6076
|
PER_ELEMENT_TUPLE_METHODS = Set[
|
|
4964
|
-
:map, :collect, :filter_map, :flat_map,
|
|
4965
|
-
:select, :filter, :reject,
|
|
4966
|
-
:find, :detect, :find_index, :index
|
|
6077
|
+
:map, :collect, :filter_map, :flat_map, :collect_concat,
|
|
6078
|
+
:select, :filter, :find_all, :reject, :take_while, :drop_while,
|
|
6079
|
+
:partition, :count, :find, :detect, :rfind, :find_index, :index, :rindex
|
|
4967
6080
|
].freeze
|
|
4968
|
-
|
|
6081
|
+
ARRAY_ONLY_PER_ELEMENT_METHODS = Set[:index, :rindex, :rfind].freeze
|
|
6082
|
+
REVERSE_PER_ELEMENT_METHODS = Set[:rfind, :rindex].freeze
|
|
6083
|
+
private_constant :PER_ELEMENT_TUPLE_METHODS, :ARRAY_ONLY_PER_ELEMENT_METHODS, :REVERSE_PER_ELEMENT_METHODS
|
|
4969
6084
|
|
|
4970
6085
|
HASH_SHAPE_TRANSFORM_METHODS = Set[
|
|
4971
6086
|
:transform_keys, :transform_keys!,
|
|
@@ -4984,23 +6099,35 @@ module Rigor
|
|
|
4984
6099
|
private_constant :PER_ELEMENT_RANGE_LIMIT
|
|
4985
6100
|
|
|
4986
6101
|
def try_per_element_block_fold(call_node, receiver_type)
|
|
4987
|
-
|
|
6102
|
+
name = call_node.name
|
|
6103
|
+
return nil unless PER_ELEMENT_TUPLE_METHODS.include?(name)
|
|
6104
|
+
return nil if ARRAY_ONLY_PER_ELEMENT_METHODS.include?(name) && !receiver_type.is_a?(Type::Tuple)
|
|
4988
6105
|
return nil unless call_node.arguments.nil?
|
|
4989
6106
|
|
|
4990
6107
|
element_types = per_element_elements_of(receiver_type)
|
|
4991
6108
|
return nil if element_types.nil? || element_types.empty?
|
|
4992
6109
|
|
|
4993
|
-
per_position =
|
|
6110
|
+
per_position = evaluate_per_element_positions(call_node.block, element_types, name)
|
|
6111
|
+
return nil if per_position.nil?
|
|
6112
|
+
|
|
6113
|
+
assemble_per_element_result(name, per_position, element_types) ||
|
|
6114
|
+
undecided_fold_floor(name, element_types, receiver_type)
|
|
6115
|
+
end
|
|
6116
|
+
|
|
6117
|
+
def evaluate_per_element_positions(block, element_types, method_name)
|
|
6118
|
+
reverse = REVERSE_PER_ELEMENT_METHODS.include?(method_name)
|
|
6119
|
+
eval_elements = reverse ? element_types.reverse : element_types
|
|
6120
|
+
per_position = per_element_block_results(block, eval_elements)
|
|
4994
6121
|
return nil if per_position.nil? || per_position.any?(&:nil?)
|
|
4995
6122
|
|
|
4996
|
-
|
|
4997
|
-
undecided_fold_floor(call_node.name, element_types, receiver_type)
|
|
6123
|
+
reverse ? per_position.reverse : per_position
|
|
4998
6124
|
end
|
|
4999
6125
|
|
|
5000
|
-
# The honest answer for the find family (`find` / `detect` / `find_index` / `index`
|
|
5001
|
-
# (`select` / `filter` / `
|
|
5002
|
-
#
|
|
5003
|
-
#
|
|
6126
|
+
# The honest answer for the find family (`find` / `detect` / `rfind` / `find_index` / `index` / `rindex`),
|
|
6127
|
+
# the filter family (`select` / `filter` / `find_all` / `reject` / `take_while` / `drop_while`), and `count`
|
|
6128
|
+
# when this fold walked every position and the assembler still could not decide — which happens for
|
|
6129
|
+
# exactly one reason: some position's predicate is not a `Constant`, so "the matching one" or "the ones
|
|
6130
|
+
# kept" is not a static fact.
|
|
5004
6131
|
#
|
|
5005
6132
|
# Falling through to the dispatcher was WRONG for this family, and issue #617 residue (1) is the bill:
|
|
5006
6133
|
# `seen = 0; [1, 2].find do |e| seen += 1; seen == 2 end` answered `nil` where the runtime answers `2`.
|
|
@@ -5010,30 +6137,34 @@ module Rigor
|
|
|
5010
6137
|
# typed the predicate per position and saw that it does not fold. Answering here is what keeps that
|
|
5011
6138
|
# knowledge from being thrown away in favour of a worse-informed tier.
|
|
5012
6139
|
#
|
|
5013
|
-
# The floor is what `find` can return and no tighter: one of the receiver's own elements, or
|
|
5014
|
-
# no element matches. Value pinning survives because it is still true of every candidate —
|
|
5015
|
-
# back an element, it does not compute one. `find_index` / `index` answer a
|
|
5016
|
-
# `Integer?` is their floor.
|
|
5017
|
-
#
|
|
5018
|
-
# `select` / `filter` / `reject` take a floor for the same
|
|
5019
|
-
# wider: `BlockFolding`'s filter folds DO answer on a `Constant`
|
|
5020
|
-
# them one, so `seen = 0; [1, 2].select do |e| seen += 1; seen == 2
|
|
5021
|
-
# where Ruby answers `[2]`. Nested inside a threaded body, where a
|
|
5022
|
-
# ({#tail_only_body_floor}), `[[], []].select do |a| a << w; a.any? end`
|
|
5023
|
-
# Array of the receiver's own elements: which of them survive is
|
|
5024
|
-
# A range receiver's elements are widened first
|
|
6140
|
+
# The floor is what `find` / `rfind` can return and no tighter: one of the receiver's own elements, or
|
|
6141
|
+
# `nil` when no element matches. Value pinning survives because it is still true of every candidate —
|
|
6142
|
+
# `find` hands back an element, it does not compute one. `find_index` / `index` / `rindex` answer a
|
|
6143
|
+
# position in the receiver, so `Integer?` is their floor.
|
|
6144
|
+
#
|
|
6145
|
+
# `select` / `filter` / `find_all` / `reject` / `take_while` / `drop_while` take a floor for the same
|
|
6146
|
+
# reason. Their fall-through is not merely wider: `BlockFolding`'s filter folds DO answer on a `Constant`
|
|
6147
|
+
# block, and the entry-scope pin hands them one, so `seen = 0; [1, 2].select do |e| seen += 1; seen == 2
|
|
6148
|
+
# end` answered a provably-empty `[]` where Ruby answers `[2]`. Nested inside a threaded body, where a
|
|
6149
|
+
# position can be typed tail-only ({#tail_only_body_floor}), `[[], []].select do |a| a << w; a.any? end`
|
|
6150
|
+
# did the same. Their floor is an Array of the receiver's own elements: which of them survive is
|
|
6151
|
+
# undecided, and what they are is not. A range receiver's elements are widened first
|
|
6152
|
+
# ({#filter_family_floor}). `count` likewise floors to `Integer` so an undecided block never falls through
|
|
6153
|
+
# to `BlockFolding`'s entry-scope `0` or `size`.
|
|
5025
6154
|
#
|
|
5026
6155
|
# Nothing else in {PER_ELEMENT_TUPLE_METHODS} takes a floor. `map`'s assembler cannot decline, and
|
|
5027
|
-
# `BlockFolding` never folds `filter_map` / `flat_map`, so their fall-through is the RBS
|
|
6156
|
+
# `BlockFolding` never folds `filter_map` / `flat_map` / `partition`, so their fall-through is the RBS
|
|
5028
6157
|
# projection.
|
|
5029
6158
|
def undecided_fold_floor(method_name, element_types, receiver_type)
|
|
5030
6159
|
case method_name
|
|
5031
|
-
when :find, :detect
|
|
6160
|
+
when :find, :detect, :rfind
|
|
5032
6161
|
Type::Combinator.union(*element_types, Type::Combinator.constant_of(nil))
|
|
5033
|
-
when :select, :filter, :reject
|
|
6162
|
+
when :select, :filter, :find_all, :reject, :take_while, :drop_while
|
|
5034
6163
|
filter_family_floor(element_types, receiver_type)
|
|
5035
|
-
when :find_index, :index
|
|
6164
|
+
when :find_index, :index, :rindex
|
|
5036
6165
|
Type::Combinator.union(Type::Combinator.nominal_of("Integer"), Type::Combinator.constant_of(nil))
|
|
6166
|
+
when :count
|
|
6167
|
+
Type::Combinator.nominal_of("Integer")
|
|
5037
6168
|
end
|
|
5038
6169
|
end
|
|
5039
6170
|
|
|
@@ -5569,10 +6700,15 @@ module Rigor
|
|
|
5569
6700
|
end
|
|
5570
6701
|
|
|
5571
6702
|
# Records `type` as the accumulator `block` receives when typed from this typer's scope
|
|
5572
|
-
# ({.recorded_inject_accumulator}).
|
|
5573
|
-
# the
|
|
6703
|
+
# ({.recorded_inject_accumulator}). Prism nodes compare by identity. The keys are weak but the values strong,
|
|
6704
|
+
# and a recorded scope reaches the parse tree that holds the key, so a record never goes on its own (issue
|
|
6705
|
+
# #2412): the map is a run-scoped slot ({TypingState.release_run_scoped}), emptied when the run that filled it
|
|
6706
|
+
# ends.
|
|
5574
6707
|
def record_inject_accumulator(block, type)
|
|
5575
|
-
|
|
6708
|
+
record = [scope, type, LoopValueRecord.generation].freeze
|
|
6709
|
+
state = typing_state
|
|
6710
|
+
state.log_write(INJECT_ACCUMULATOR, block, record)
|
|
6711
|
+
state.weak_key_map(INJECT_ACCUMULATOR)[block] = record
|
|
5576
6712
|
end
|
|
5577
6713
|
|
|
5578
6714
|
# Issue #1332 — the #587 (b) captured-block binding ({#captured_block_bindings}) both inject arms lay under
|
|
@@ -5687,18 +6823,44 @@ module Rigor
|
|
|
5687
6823
|
result
|
|
5688
6824
|
end
|
|
5689
6825
|
|
|
6826
|
+
PER_ELEMENT_ASSEMBLERS = {
|
|
6827
|
+
map: :assemble_map_result,
|
|
6828
|
+
collect: :assemble_map_result,
|
|
6829
|
+
filter_map: :assemble_filter_map_result,
|
|
6830
|
+
flat_map: :assemble_flat_map_result,
|
|
6831
|
+
collect_concat: :assemble_flat_map_result,
|
|
6832
|
+
select: :assemble_select_result,
|
|
6833
|
+
filter: :assemble_select_result,
|
|
6834
|
+
find_all: :assemble_select_result,
|
|
6835
|
+
reject: :assemble_reject_result,
|
|
6836
|
+
take_while: :assemble_take_while_result,
|
|
6837
|
+
drop_while: :assemble_drop_while_result,
|
|
6838
|
+
partition: :assemble_partition_result,
|
|
6839
|
+
count: :assemble_count_result,
|
|
6840
|
+
find: :assemble_find_result,
|
|
6841
|
+
detect: :assemble_find_result,
|
|
6842
|
+
rfind: :assemble_rfind_result,
|
|
6843
|
+
find_index: :assemble_find_index_result,
|
|
6844
|
+
index: :assemble_find_index_result,
|
|
6845
|
+
rindex: :assemble_rindex_result
|
|
6846
|
+
}.freeze
|
|
6847
|
+
private_constant :PER_ELEMENT_ASSEMBLERS
|
|
6848
|
+
|
|
5690
6849
|
def assemble_per_element_result(method_name, per_position, element_types)
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
6850
|
+
handler = PER_ELEMENT_ASSEMBLERS[method_name]
|
|
6851
|
+
send(handler, per_position, element_types) if handler
|
|
6852
|
+
end
|
|
6853
|
+
|
|
6854
|
+
def assemble_map_result(per_position, _element_types)
|
|
6855
|
+
Type::Combinator.tuple_of(*per_position)
|
|
6856
|
+
end
|
|
6857
|
+
|
|
6858
|
+
def assemble_select_result(per_position, element_types)
|
|
6859
|
+
assemble_filter_result(per_position, element_types, keep_on_truthy: true)
|
|
6860
|
+
end
|
|
6861
|
+
|
|
6862
|
+
def assemble_reject_result(per_position, element_types)
|
|
6863
|
+
assemble_filter_result(per_position, element_types, keep_on_truthy: false)
|
|
5702
6864
|
end
|
|
5703
6865
|
|
|
5704
6866
|
# `select` / `filter` / `reject`: keeps each receiver element whose per-position predicate result
|
|
@@ -5718,10 +6880,51 @@ module Rigor
|
|
|
5718
6880
|
Type::Combinator.tuple_of(*kept)
|
|
5719
6881
|
end
|
|
5720
6882
|
|
|
6883
|
+
# `take_while` / `drop_while`: stops at the first falsey `Constant` position when every preceding
|
|
6884
|
+
# position is a truthy `Constant` (or takes/drops all elements when every position is a truthy
|
|
6885
|
+
# `Constant`). Declines when an undecided position precedes the stopping point.
|
|
6886
|
+
def assemble_take_while_result(per_position, element_types)
|
|
6887
|
+
cutoff = leading_truthy_cutoff(per_position)
|
|
6888
|
+
return nil if cutoff.nil?
|
|
6889
|
+
|
|
6890
|
+
Type::Combinator.tuple_of(*element_types[0...cutoff])
|
|
6891
|
+
end
|
|
6892
|
+
|
|
6893
|
+
def assemble_drop_while_result(per_position, element_types)
|
|
6894
|
+
cutoff = leading_truthy_cutoff(per_position)
|
|
6895
|
+
return nil if cutoff.nil?
|
|
6896
|
+
|
|
6897
|
+
Type::Combinator.tuple_of(*element_types[cutoff..])
|
|
6898
|
+
end
|
|
6899
|
+
|
|
6900
|
+
def leading_truthy_cutoff(per_position)
|
|
6901
|
+
first_falsey_index = per_position.index { |type| falsey_constant?(type) }
|
|
6902
|
+
return (per_position.all?(Type::Constant) ? per_position.size : nil) if first_falsey_index.nil?
|
|
6903
|
+
return nil unless per_position[0...first_falsey_index].all?(Type::Constant)
|
|
6904
|
+
|
|
6905
|
+
first_falsey_index
|
|
6906
|
+
end
|
|
6907
|
+
|
|
6908
|
+
# `partition`: returns `Tuple[truthy_tuple, falsey_tuple]` when every position folds to a `Constant`.
|
|
6909
|
+
def assemble_partition_result(per_position, element_types)
|
|
6910
|
+
return nil unless per_position.all?(Type::Constant)
|
|
6911
|
+
|
|
6912
|
+
truthy = assemble_filter_result(per_position, element_types, keep_on_truthy: true)
|
|
6913
|
+
falsey = assemble_filter_result(per_position, element_types, keep_on_truthy: false)
|
|
6914
|
+
Type::Combinator.tuple_of(truthy, falsey)
|
|
6915
|
+
end
|
|
6916
|
+
|
|
6917
|
+
# `count`: returns `Constant[Integer]` of the truthy positions when every position folds to a `Constant`.
|
|
6918
|
+
def assemble_count_result(per_position, _element_types)
|
|
6919
|
+
return nil unless per_position.all?(Type::Constant)
|
|
6920
|
+
|
|
6921
|
+
Type::Combinator.constant_of(per_position.count { |type| truthy_constant?(type) })
|
|
6922
|
+
end
|
|
6923
|
+
|
|
5721
6924
|
# `filter_map` folds tightly only when every per-position result is a `Constant`: positions whose
|
|
5722
6925
|
# value is `nil` or `false` drop, the rest survive in declaration order. When any position is
|
|
5723
6926
|
# non-Constant the dispatcher declines (returns nil) so the RBS tier widens to `Array[U]`.
|
|
5724
|
-
def assemble_filter_map_result(per_position)
|
|
6927
|
+
def assemble_filter_map_result(per_position, _element_types = nil)
|
|
5725
6928
|
return nil unless per_position.all?(Type::Constant)
|
|
5726
6929
|
|
|
5727
6930
|
kept = per_position.reject { |type| type.value.nil? || type.value == false }
|
|
@@ -5736,7 +6939,7 @@ module Rigor
|
|
|
5736
6939
|
#
|
|
5737
6940
|
# `Type::Constant` only ever holds non-Array scalars (the carrier rejects Array literals), so a single
|
|
5738
6941
|
# `Constant` safely contributes itself as a single Tuple element.
|
|
5739
|
-
def assemble_flat_map_result(per_position)
|
|
6942
|
+
def assemble_flat_map_result(per_position, _element_types = nil)
|
|
5740
6943
|
flattened = per_position.flat_map { |type| flat_map_contribution(type) }
|
|
5741
6944
|
return nil if flattened.nil? || flattened.any?(&:nil?)
|
|
5742
6945
|
|
|
@@ -5766,15 +6969,32 @@ module Rigor
|
|
|
5766
6969
|
Type::Combinator.union(*candidates.map { |index| element_types[index] })
|
|
5767
6970
|
end
|
|
5768
6971
|
|
|
6972
|
+
# `rfind`: reverse of `find` — returns the last receiver element whose block result is Ruby-truthy,
|
|
6973
|
+
# joined with any undecided positions to its right.
|
|
6974
|
+
def assemble_rfind_result(per_position, element_types)
|
|
6975
|
+
candidates = last_match_candidates(per_position)
|
|
6976
|
+
return candidates if candidates.nil? || candidates.equal?(NO_MATCH)
|
|
6977
|
+
|
|
6978
|
+
Type::Combinator.union(*candidates.map { |index| element_types[index] })
|
|
6979
|
+
end
|
|
6980
|
+
|
|
5769
6981
|
# `find_index` / `index`: returns the index of the first truthy position, or `Constant[nil]` when
|
|
5770
6982
|
# nothing matches, on {#assemble_find_result}'s terms.
|
|
5771
|
-
def assemble_find_index_result(per_position)
|
|
6983
|
+
def assemble_find_index_result(per_position, _element_types = nil)
|
|
5772
6984
|
candidates = first_match_candidates(per_position)
|
|
5773
6985
|
return candidates if candidates.nil? || candidates.equal?(NO_MATCH)
|
|
5774
6986
|
|
|
5775
6987
|
Type::Combinator.union(*candidates.map { |index| Type::Combinator.constant_of(index) })
|
|
5776
6988
|
end
|
|
5777
6989
|
|
|
6990
|
+
# `rindex`: returns the index of the last truthy position, or `Constant[nil]` when nothing matches.
|
|
6991
|
+
def assemble_rindex_result(per_position, _element_types = nil)
|
|
6992
|
+
candidates = last_match_candidates(per_position)
|
|
6993
|
+
return candidates if candidates.nil? || candidates.equal?(NO_MATCH)
|
|
6994
|
+
|
|
6995
|
+
Type::Combinator.union(*candidates.map { |index| Type::Combinator.constant_of(index) })
|
|
6996
|
+
end
|
|
6997
|
+
|
|
5778
6998
|
NO_MATCH = Type::Combinator.constant_of(nil)
|
|
5779
6999
|
private_constant :NO_MATCH
|
|
5780
7000
|
|
|
@@ -5790,8 +7010,27 @@ module Rigor
|
|
|
5790
7010
|
undecided + [first_truthy_index]
|
|
5791
7011
|
end
|
|
5792
7012
|
|
|
7013
|
+
# The positions `rfind` / `rindex` may stop at — the last decisive truthy one and every undecided one
|
|
7014
|
+
# after it — `NO_MATCH` when every position folds falsey, or nil when an undecided position has no
|
|
7015
|
+
# truthy one before it.
|
|
7016
|
+
def last_match_candidates(per_position)
|
|
7017
|
+
last_truthy_index = per_position.rindex { |type| truthy_constant?(type) }
|
|
7018
|
+
if last_truthy_index.nil?
|
|
7019
|
+
return per_position.all?(Type::Constant) ? NO_MATCH : nil
|
|
7020
|
+
end
|
|
7021
|
+
|
|
7022
|
+
undecided = ((last_truthy_index + 1)...per_position.size).reject do |index|
|
|
7023
|
+
per_position[index].is_a?(Type::Constant)
|
|
7024
|
+
end
|
|
7025
|
+
[last_truthy_index] + undecided
|
|
7026
|
+
end
|
|
7027
|
+
|
|
5793
7028
|
def truthy_constant?(type)
|
|
5794
|
-
type.is_a?(Type::Constant) && type.value && type.value != false
|
|
7029
|
+
type.is_a?(Type::Constant) && !type.value.nil? && type.value != false
|
|
7030
|
+
end
|
|
7031
|
+
|
|
7032
|
+
def falsey_constant?(type)
|
|
7033
|
+
type.is_a?(Type::Constant) && (type.value.nil? || type.value == false)
|
|
5795
7034
|
end
|
|
5796
7035
|
|
|
5797
7036
|
# Per-pair block fold for `HashShape#transform_keys` and `HashShape#transform_values` (and their bang
|