rigortype 0.3.8 → 0.4.0
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 +2 -2
- data/data/capability_roles/capability_roles.rbs +36 -0
- data/data/core_overlay/enumerable.rbs +51 -0
- data/data/core_overlay/enumerator.rbs +84 -0
- data/data/core_overlay/hash_rbs3.rbs +41 -0
- data/data/core_overlay/process.rbs +40 -0
- data/data/core_overlay/string_io.rbs +33 -0
- data/data/effects/core.yml +3 -3
- data/data/gem_overlay/activesupport/core_ext.rbs +236 -17
- data/docs/handbook/02-everyday-types.md +17 -14
- data/docs/handbook/03-narrowing.md +132 -14
- data/docs/handbook/04-tuples-and-shapes.md +8 -6
- data/docs/handbook/05-methods-and-blocks.md +1 -1
- data/docs/handbook/06-classes.md +2 -2
- data/docs/handbook/07-rbs-and-extended.md +19 -1
- data/docs/handbook/10-sorbet.md +9 -10
- data/docs/handbook/11-sig-gen.md +525 -13
- data/docs/handbook/README.md +1 -1
- data/docs/handbook/appendix-elixir.md +2 -2
- data/docs/handbook/appendix-go.md +2 -2
- data/docs/handbook/appendix-java-csharp.md +2 -2
- data/docs/handbook/appendix-mypy.md +2 -2
- data/docs/handbook/appendix-phpstan.md +1 -1
- data/docs/handbook/appendix-rust.md +1 -1
- data/docs/handbook/appendix-type-theory.md +4 -4
- data/docs/handbook/appendix-typescript.md +1 -1
- data/docs/llms.txt +2 -0
- data/docs/manual/02-cli-reference.md +95 -6
- data/docs/manual/03-configuration.md +18 -0
- data/docs/manual/04-diagnostics.md +15 -0
- data/docs/manual/07-plugins.md +15 -5
- data/docs/manual/08-skills.md +23 -2
- data/docs/manual/10-mcp-server.md +3 -2
- data/docs/manual/11-ci.md +9 -0
- data/docs/manual/15-type-protection-coverage.md +8 -0
- data/docs/manual/16-rbs-extended-annotations.md +46 -8
- data/docs/manual/18-removing-dead-code.md +10 -8
- data/docs/manual/19-effect-labels.md +10 -0
- data/docs/manual/plugins/README.md +7 -0
- data/docs/manual/plugins/rigor-actioncable.md +8 -1
- data/docs/manual/plugins/rigor-actionmailer.md +7 -0
- data/docs/manual/plugins/rigor-actionpack.md +261 -0
- data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
- data/docs/manual/plugins/rigor-activejob.md +7 -0
- data/docs/manual/plugins/rigor-activerecord.md +138 -4
- data/docs/manual/plugins/rigor-activestorage.md +8 -1
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +43 -7
- data/docs/manual/plugins/rigor-grape.md +106 -0
- data/docs/manual/plugins/rigor-graphql.md +23 -2
- data/docs/manual/plugins/rigor-pundit.md +8 -1
- data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
- data/docs/manual/plugins/rigor-rails-routes.md +7 -0
- data/docs/manual/plugins/rigor-rbs-inline.md +229 -1
- data/docs/manual/plugins/rigor-sidekiq.md +8 -1
- data/docs/manual/plugins/rigor-sorbet.md +21 -5
- data/exe/rigor +19 -4
- data/lib/rigor/analysis/baseline.rb +3 -3
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +1 -5
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +10 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +6 -4
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +15 -6
- data/lib/rigor/analysis/check_rules/rule_ids.rb +21 -6
- data/lib/rigor/analysis/check_rules/rule_walk.rb +41 -5
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +1 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
- data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +38 -4
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +0 -1
- data/lib/rigor/analysis/check_rules.rb +718 -79
- data/lib/rigor/analysis/crash_signature.rb +8 -24
- data/lib/rigor/analysis/dependency_recorder.rb +23 -0
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +0 -2
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +0 -2
- data/lib/rigor/analysis/dependency_source_inference/index.rb +5 -5
- data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +1 -2
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
- data/lib/rigor/analysis/effects_cache_probe.rb +3 -4
- data/lib/rigor/analysis/erb_template_detector.rb +1 -2
- data/lib/rigor/analysis/fact_store.rb +15 -1
- data/lib/rigor/analysis/incremental.rb +42 -0
- data/lib/rigor/analysis/incremental_session.rb +117 -26
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +0 -1
- data/lib/rigor/analysis/project_scan.rb +11 -1
- data/lib/rigor/analysis/reachability/graph.rb +3 -5
- data/lib/rigor/analysis/reachability/plugin_roots.rb +5 -5
- data/lib/rigor/analysis/reachability/project_files.rb +2 -2
- data/lib/rigor/analysis/reachability/scan.rb +5 -5
- data/lib/rigor/analysis/reachability/scan_cache.rb +3 -2
- data/lib/rigor/analysis/reachability/signature_scan.rb +2 -2
- data/lib/rigor/analysis/result.rb +1 -3
- data/lib/rigor/analysis/rule_catalog.rb +131 -1
- data/lib/rigor/analysis/run_cache_key.rb +21 -12
- data/lib/rigor/analysis/run_cache_probe.rb +3 -5
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -8
- data/lib/rigor/analysis/runner/declaration_position.rb +1 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +340 -33
- data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +2 -3
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +18 -14
- data/lib/rigor/analysis/runner/pool_coordinator.rb +357 -97
- data/lib/rigor/analysis/runner/project_pre_passes.rb +38 -13
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +407 -51
- data/lib/rigor/analysis/template_unit_collector.rb +303 -0
- data/lib/rigor/analysis/template_unit_paths.rb +91 -0
- data/lib/rigor/analysis/template_unit_positions.rb +293 -0
- data/lib/rigor/analysis/template_units.rb +399 -0
- data/lib/rigor/analysis/worker_session.rb +62 -20
- data/lib/rigor/bleeding_edge.rb +10 -24
- data/lib/rigor/builtins/hkt_builtins.rb +3 -2
- data/lib/rigor/builtins/imported_refinements.rb +159 -21
- data/lib/rigor/builtins/predefined_constant_refinements.rb +5 -7
- data/lib/rigor/builtins/regex_refinement.rb +21 -13
- data/lib/rigor/builtins/static_return_refinements.rb +7 -6
- data/lib/rigor/cache/annotation_location.rb +2 -4
- data/lib/rigor/cache/descriptor.rb +63 -20
- data/lib/rigor/cache/engine_source.rb +33 -5
- data/lib/rigor/cache/file_digest.rb +9 -1
- data/lib/rigor/cache/incremental_snapshot.rb +72 -6
- data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +0 -3
- data/lib/rigor/cache/rbs_class_type_param_names.rb +0 -3
- data/lib/rigor/cache/rbs_constant_table.rb +0 -3
- data/lib/rigor/cache/rbs_descriptor.rb +112 -14
- data/lib/rigor/cache/rbs_environment.rb +8 -4
- data/lib/rigor/cache/rbs_known_class_names.rb +0 -3
- data/lib/rigor/cache/store.rb +76 -32
- data/lib/rigor/ci_detector.rb +1 -0
- data/lib/rigor/cli/annotate_command.rb +5 -6
- data/lib/rigor/cli/check_command.rb +57 -10
- data/lib/rigor/cli/check_invocation.rb +6 -11
- data/lib/rigor/cli/check_runner_factory.rb +1 -5
- data/lib/rigor/cli/coverage_command.rb +1 -1
- data/lib/rigor/cli/coverage_mutation.rb +12 -3
- data/lib/rigor/cli/coverage_scan.rb +1 -5
- data/lib/rigor/cli/diff_command.rb +1 -1
- data/lib/rigor/cli/doc_links.rb +4 -4
- data/lib/rigor/cli/docs_command.rb +6 -6
- data/lib/rigor/cli/doctor_command.rb +85 -38
- data/lib/rigor/cli/effects_command.rb +2 -2
- data/lib/rigor/cli/effects_diff_renderer.rb +3 -3
- data/lib/rigor/cli/effects_snapshot_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +34 -1
- data/lib/rigor/cli/fused_protection_report.rb +7 -1
- data/lib/rigor/cli/lsp_command.rb +1 -1
- data/lib/rigor/cli/mcp_command.rb +1 -1
- data/lib/rigor/cli/measurement_integrity_warning.rb +4 -5
- data/lib/rigor/cli/mutation_fork_scan.rb +6 -6
- data/lib/rigor/cli/mutation_protection_report.rb +7 -1
- data/lib/rigor/cli/plugin_command.rb +4 -4
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/prism_colorizer.rb +1 -2
- data/lib/rigor/cli/protection_fork_scan.rb +6 -6
- data/lib/rigor/cli/show_bleedingedge_command.rb +1 -1
- data/lib/rigor/cli/sig_gen_command.rb +212 -31
- data/lib/rigor/cli/skill_command.rb +2 -2
- data/lib/rigor/cli/skill_deep_probe.rb +4 -4
- data/lib/rigor/cli/skill_describe.rb +35 -35
- data/lib/rigor/cli/trace_command.rb +4 -4
- data/lib/rigor/cli/trace_renderer.rb +5 -6
- data/lib/rigor/cli/triage_command.rb +1 -1
- data/lib/rigor/cli/type_of_command.rb +29 -11
- data/lib/rigor/cli/type_of_renderer.rb +20 -9
- data/lib/rigor/cli/type_of_template_probe.rb +189 -0
- data/lib/rigor/cli/type_scan_command.rb +4 -4
- data/lib/rigor/cli/unused_command.rb +11 -3
- data/lib/rigor/cli/upgrade_command.rb +1 -1
- data/lib/rigor/cli.rb +89 -9
- data/lib/rigor/config_audit.rb +30 -5
- data/lib/rigor/configuration/severity_profile.rb +25 -9
- data/lib/rigor/configuration.rb +109 -7
- data/lib/rigor/effects/ancestry_recorder.rb +191 -0
- data/lib/rigor/effects/attribution.rb +12 -5
- data/lib/rigor/effects/callee_rule.rb +368 -0
- data/lib/rigor/effects/catalog.rb +7 -4
- data/lib/rigor/effects/collector.rb +11 -5
- data/lib/rigor/effects/config_envelopes.rb +15 -10
- data/lib/rigor/effects/definition_context.rb +179 -0
- data/lib/rigor/effects/definition_lines.rb +25 -6
- data/lib/rigor/effects/effect_table.rb +11 -4
- data/lib/rigor/effects/entry_points.rb +0 -2
- data/lib/rigor/effects/envelope_check.rb +9 -9
- data/lib/rigor/effects/envelope_index.rb +20 -8
- data/lib/rigor/effects/file_collection.rb +59 -5
- data/lib/rigor/effects/framework_units.rb +5 -7
- data/lib/rigor/effects/identity.rb +20 -6
- data/lib/rigor/effects/inline_anchor.rb +7 -7
- data/lib/rigor/effects/label_intent.rb +3 -4
- data/lib/rigor/effects/liskov_check.rb +8 -8
- data/lib/rigor/effects/local_ownership.rb +38 -12
- data/lib/rigor/effects/method_key.rb +22 -1
- data/lib/rigor/effects/mutation_classifier.rb +23 -12
- data/lib/rigor/effects/plugin_facts.rb +47 -37
- data/lib/rigor/effects/propagator.rb +297 -19
- data/lib/rigor/effects/registry.rb +1 -1
- data/lib/rigor/effects/scanner.rb +122 -73
- data/lib/rigor/effects/signature_sources.rb +4 -6
- data/lib/rigor/effects/snapshot.rb +12 -11
- data/lib/rigor/effects/snapshot_diff.rb +1 -1
- data/lib/rigor/effects/summary.rb +27 -4
- data/lib/rigor/effects/unit_scan.rb +393 -38
- data/lib/rigor/effects/unknown_label_check.rb +3 -8
- data/lib/rigor/effects/unknown_label_report.rb +3 -4
- data/lib/rigor/effects/visibility.rb +101 -0
- data/lib/rigor/environment/bundle_sig_discovery.rb +6 -6
- data/lib/rigor/environment/class_registry.rb +3 -1
- data/lib/rigor/environment/failure_slot.rb +2 -2
- data/lib/rigor/environment/installed_gem_set.rb +85 -0
- data/lib/rigor/environment/lockfile_resolver.rb +68 -4
- data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
- data/lib/rigor/environment/member_consistency.rb +298 -0
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -4
- data/lib/rigor/environment/rbs_collection_discovery.rb +5 -5
- data/lib/rigor/environment/rbs_coverage_report.rb +5 -5
- data/lib/rigor/environment/rbs_hierarchy.rb +3 -1
- data/lib/rigor/environment/rbs_loader.rb +674 -79
- data/lib/rigor/environment.rb +174 -24
- data/lib/rigor/flow_contribution/merger.rb +0 -2
- data/lib/rigor/flow_contribution.rb +11 -13
- data/lib/rigor/hashing/xxh3.rb +264 -0
- data/lib/rigor/inference/acceptance.rb +292 -17
- data/lib/rigor/inference/block_auto_splat.rb +216 -0
- data/lib/rigor/inference/block_call_timing.rb +338 -0
- data/lib/rigor/inference/block_parameter_binder.rb +75 -30
- data/lib/rigor/inference/block_repetition.rb +71 -0
- data/lib/rigor/inference/body_fixpoint.rb +7 -6
- data/lib/rigor/inference/budget_trace.rb +3 -6
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
- data/lib/rigor/inference/captured_locals.rb +389 -18
- data/lib/rigor/inference/closure_escape_analyzer.rb +159 -17
- data/lib/rigor/inference/content_join.rb +200 -27
- data/lib/rigor/inference/coverage_scanner.rb +4 -6
- data/lib/rigor/inference/def_return_typer.rb +11 -7
- data/lib/rigor/inference/define_method_block_self.rb +64 -0
- data/lib/rigor/inference/dynamic_origin.rb +1 -1
- data/lib/rigor/inference/element_read_widening.rb +180 -0
- data/lib/rigor/inference/error_info.rb +196 -0
- data/lib/rigor/inference/expression_typer.rb +2124 -468
- data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
- data/lib/rigor/inference/fork_map.rb +5 -7
- data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
- data/lib/rigor/inference/global_write_census.rb +239 -0
- data/lib/rigor/inference/guard_rebinding.rb +447 -0
- data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
- data/lib/rigor/inference/hkt_reducer.rb +2 -3
- data/lib/rigor/inference/hkt_registry.rb +4 -7
- data/lib/rigor/inference/index_write_widening.rb +16 -7
- data/lib/rigor/inference/indexed_narrowing.rb +61 -9
- data/lib/rigor/inference/jump_targets.rb +82 -0
- data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
- data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
- data/lib/rigor/inference/last_line.rb +340 -0
- data/lib/rigor/inference/last_status.rb +144 -0
- data/lib/rigor/inference/macro_block_self_type.rb +168 -21
- data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
- data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
- data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
- data/lib/rigor/inference/match_rebinding.rb +392 -0
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +87 -33
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +305 -19
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/json_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -1
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +41 -3
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -5
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +165 -111
- data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
- data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
- data/lib/rigor/inference/method_dispatcher/random_folding.rb +82 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +1046 -69
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +25 -4
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +2 -8
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +129 -54
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +1 -2
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -6
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +85 -4
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +1 -2
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher.rb +225 -25
- data/lib/rigor/inference/method_parameter_binder.rb +12 -9
- data/lib/rigor/inference/multi_target_binder.rb +341 -55
- data/lib/rigor/inference/mutation_rejoin.rb +171 -0
- data/lib/rigor/inference/mutation_widening.rb +156 -85
- data/lib/rigor/inference/narrowing.rb +835 -162
- data/lib/rigor/inference/operand_effects.rb +167 -0
- data/lib/rigor/inference/operand_walk.rb +88 -0
- data/lib/rigor/inference/optimistic_origin.rb +153 -16
- data/lib/rigor/inference/origin_lookup.rb +2 -3
- data/lib/rigor/inference/parameter_inference_collector.rb +7 -7
- data/lib/rigor/inference/pre_eval_constants.rb +6 -5
- data/lib/rigor/inference/precision_scanner.rb +3 -4
- data/lib/rigor/inference/project_method_ownership.rb +136 -0
- data/lib/rigor/inference/project_patched_methods.rb +9 -4
- data/lib/rigor/inference/project_patched_scanner.rb +10 -6
- data/lib/rigor/inference/protection_scanner.rb +1 -2
- data/lib/rigor/inference/range_constant.rb +57 -0
- data/lib/rigor/inference/rbs_type_translator.rb +49 -11
- data/lib/rigor/inference/receiver_alias.rb +93 -4
- data/lib/rigor/inference/receiver_blind_block.rb +219 -0
- data/lib/rigor/inference/refinement_mutation.rb +76 -0
- data/lib/rigor/inference/repeated_or_writes.rb +463 -0
- data/lib/rigor/inference/return_barrier.rb +54 -0
- data/lib/rigor/inference/rewrite_mutation.rb +120 -0
- data/lib/rigor/inference/scope_indexer.rb +5228 -627
- data/lib/rigor/inference/statement_evaluator.rb +3452 -595
- data/lib/rigor/inference/stored_block_call.rb +54 -0
- data/lib/rigor/inference/string_mutation.rb +97 -0
- data/lib/rigor/inference/struct_fold_safety.rb +6 -7
- data/lib/rigor/inference/synthetic_method_index.rb +1 -2
- data/lib/rigor/inference/synthetic_method_scanner.rb +4 -6
- data/lib/rigor/inference/unknown_store_widening.rb +200 -0
- data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
- data/lib/rigor/inference/version_guard.rb +28 -21
- data/lib/rigor/inference/void_origin.rb +3 -3
- data/lib/rigor/inference/void_tail_summary.rb +2 -4
- data/lib/rigor/language_server/buffer_table.rb +4 -4
- data/lib/rigor/language_server/completion_provider.rb +1 -1
- data/lib/rigor/language_server/debouncer.rb +0 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +7 -6
- data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
- data/lib/rigor/language_server/folding_range_provider.rb +1 -1
- data/lib/rigor/language_server/hover_provider.rb +1 -1
- data/lib/rigor/language_server/hover_renderer.rb +4 -4
- data/lib/rigor/language_server/incremental_sync.rb +6 -6
- data/lib/rigor/language_server/project_context.rb +7 -8
- data/lib/rigor/language_server/publish_batcher.rb +2 -2
- data/lib/rigor/language_server/selection_range_provider.rb +2 -2
- data/lib/rigor/language_server/server.rb +9 -9
- data/lib/rigor/language_server/signature_help_provider.rb +1 -1
- data/lib/rigor/language_server/uri.rb +1 -1
- data/lib/rigor/mcp/server.rb +2 -1
- data/lib/rigor/plugin/base.rb +258 -26
- data/lib/rigor/plugin/box_probe.rb +91 -0
- data/lib/rigor/plugin/bundled_catalog.rb +167 -0
- data/lib/rigor/plugin/effect_attribution.rb +65 -11
- data/lib/rigor/plugin/effect_entry_points.rb +3 -3
- data/lib/rigor/plugin/fact_store.rb +6 -6
- data/lib/rigor/plugin/io_boundary.rb +93 -8
- data/lib/rigor/plugin/isolation.rb +29 -6
- data/lib/rigor/plugin/loader.rb +33 -13
- data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
- data/lib/rigor/plugin/macro/heredoc_template.rb +1 -1
- data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
- data/lib/rigor/plugin/manifest.rb +94 -11
- data/lib/rigor/plugin/registry.rb +76 -12
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +21 -4
- data/lib/rigor/plugin/template_unit.rb +196 -0
- data/lib/rigor/plugin/trust_policy.rb +2 -4
- data/lib/rigor/plugin/type_node_resolver.rb +3 -3
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/plugin_gap_advisory.rb +96 -0
- data/lib/rigor/project_environment.rb +138 -0
- data/lib/rigor/protection/analysis_guard.rb +93 -14
- data/lib/rigor/protection/closure_kill_oracle.rb +67 -23
- data/lib/rigor/protection/dependency_closure.rb +5 -8
- data/lib/rigor/protection/diagnostic_oracle.rb +2 -2
- data/lib/rigor/protection/discovery_seed.rb +14 -12
- data/lib/rigor/protection/kill_signature.rb +1 -4
- data/lib/rigor/protection/measurement_integrity.rb +1 -3
- data/lib/rigor/protection/mutation_cache.rb +7 -11
- data/lib/rigor/protection/mutation_scanner.rb +12 -15
- data/lib/rigor/protection/mutator.rb +2 -1
- data/lib/rigor/protection/test_suite_oracle.rb +5 -5
- data/lib/rigor/rbs_extended/conformance_checker.rb +4 -3
- data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
- data/lib/rigor/rbs_extended/reporter.rb +36 -10
- data/lib/rigor/rbs_extended.rb +88 -21
- data/lib/rigor/reflection/constant_ancestors.rb +97 -0
- data/lib/rigor/reflection/constant_path.rb +139 -0
- data/lib/rigor/reflection.rb +93 -110
- data/lib/rigor/runtime/jit.rb +6 -8
- data/lib/rigor/scope/discovery_index.rb +165 -2
- data/lib/rigor/scope.rb +1041 -32
- data/lib/rigor/sig_gen/alias_index.rb +289 -0
- data/lib/rigor/sig_gen/classification.rb +22 -5
- data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
- data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
- data/lib/rigor/sig_gen/generator.rb +673 -53
- data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
- data/lib/rigor/sig_gen/layout_index.rb +3 -4
- data/lib/rigor/sig_gen/meta_class_shape.rb +3 -5
- data/lib/rigor/sig_gen/method_candidate.rb +47 -3
- data/lib/rigor/sig_gen/observation_collector.rb +18 -16
- data/lib/rigor/sig_gen/path_mapper.rb +4 -6
- data/lib/rigor/sig_gen/rbs_validity.rb +4 -4
- data/lib/rigor/sig_gen/renderer.rb +128 -14
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +154 -0
- data/lib/rigor/sig_gen/superclass_spelling.rb +27 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +1 -3
- data/lib/rigor/sig_gen/write_result.rb +19 -3
- data/lib/rigor/sig_gen/writer.rb +189 -41
- data/lib/rigor/sig_gen.rb +3 -0
- data/lib/rigor/signature_path_audit.rb +153 -6
- data/lib/rigor/source/literals.rb +0 -23
- data/lib/rigor/source/node_children.rb +0 -2
- data/lib/rigor/source/node_locator.rb +5 -11
- data/lib/rigor/source/node_walker.rb +0 -6
- data/lib/rigor/source/parameter_envelope.rb +72 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/triage/catalogue.rb +1 -3
- data/lib/rigor/triage.rb +3 -5
- data/lib/rigor/type/accepts_result.rb +24 -6
- data/lib/rigor/type/combinator.rb +144 -5
- data/lib/rigor/type/data_class.rb +2 -2
- data/lib/rigor/type/data_instance.rb +4 -4
- data/lib/rigor/type/difference.rb +1 -0
- data/lib/rigor/type/float_range.rb +128 -0
- data/lib/rigor/type/hash_shape.rb +6 -6
- data/lib/rigor/type/integer_range.rb +13 -8
- data/lib/rigor/type/nominal.rb +8 -2
- data/lib/rigor/type/refined.rb +4 -3
- data/lib/rigor/type/struct_class.rb +3 -3
- data/lib/rigor/type/struct_instance.rb +4 -4
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_node/generic.rb +1 -1
- data/lib/rigor/type_node/range_literal.rb +25 -0
- data/lib/rigor/type_node/resolver_chain.rb +1 -1
- data/lib/rigor/type_node.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +0 -4
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +0 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +0 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +10 -5
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +0 -4
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +3 -5
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +1 -4
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +14 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +32 -30
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +0 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +264 -3
- data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
- data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
- data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
- data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +0 -4
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +2 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +0 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +0 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +10 -5
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +383 -7
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +1 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +1 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +322 -41
- data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +26 -14
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -2
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +290 -19
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +3 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +9 -7
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +7 -3
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +1 -0
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +8 -6
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +11 -7
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -11
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +9 -4
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +2 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +0 -5
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +0 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +0 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +8 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +36 -4
- data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
- data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
- data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
- data/plugins/rigor-grape/sig/grape.rbs +263 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +8 -6
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +78 -5
- data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +1 -0
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -1
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -3
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +0 -5
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -2
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +0 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +0 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +12 -7
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +0 -12
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +1 -3
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +0 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +36 -37
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +5 -5
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -10
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +5 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +3 -2
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +2 -2
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +2 -2
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +9 -10
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +2 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +10 -17
- data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +1 -0
- data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +13 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +324 -18
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +0 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +0 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +2 -3
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +0 -3
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +1 -0
- data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +1 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +1 -9
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +1 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +0 -4
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +1 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +2 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +0 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +0 -2
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +10 -5
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +2 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +1 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +3 -9
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +4 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +1 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +3 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +51 -6
- data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -0
- data/sig/rigor/analysis/baseline.rbs +69 -7
- data/sig/rigor/analysis/fact_store.rbs +1 -1
- data/sig/rigor/analysis/project_scan.rbs +74 -0
- data/sig/rigor/analysis/reachability/scan_cache.rbs +12 -0
- data/sig/rigor/cache.rbs +2 -3
- data/sig/rigor/effects/config_envelopes.rbs +100 -0
- data/sig/rigor/effects/effect_table.rbs +60 -0
- data/sig/rigor/effects/envelope.rbs +97 -0
- data/sig/rigor/effects/envelope_index.rbs +33 -0
- data/sig/rigor/effects/file_collection.rbs +81 -0
- data/sig/rigor/effects/label.rbs +26 -0
- data/sig/rigor/effects/label_set.rbs +46 -0
- data/sig/rigor/effects/method_key.rbs +24 -0
- data/sig/rigor/effects/origin.rbs +50 -0
- data/sig/rigor/effects/plugin_facts.rbs +141 -0
- data/sig/rigor/effects/registry.rbs +68 -0
- data/sig/rigor/effects/summary.rbs +50 -0
- data/sig/rigor/effects/taint_cause.rbs +12 -0
- data/sig/rigor/environment.rbs +17 -10
- data/sig/rigor/inference/optimistic_origin.rbs +10 -0
- data/sig/rigor/inference.rbs +9 -14
- data/sig/rigor/plugin/additional_initializer.rbs +36 -0
- data/sig/rigor/plugin/base.rbs +30 -7
- data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
- data/sig/rigor/plugin/effect_attribution.rbs +66 -0
- data/sig/rigor/plugin/effect_edge.rbs +33 -0
- data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
- data/sig/rigor/plugin/io_boundary.rbs +4 -0
- data/sig/rigor/plugin/loader.rbs +3 -3
- data/sig/rigor/plugin/manifest.rbs +51 -11
- data/sig/rigor/plugin/protocol_contract.rbs +68 -0
- data/sig/rigor/plugin/registry.rbs +62 -1
- data/sig/rigor/plugin.rbs +1 -1
- data/sig/rigor/rbs_extended.rbs +1 -1
- data/sig/rigor/reflection.rbs +9 -6
- data/sig/rigor/scope.rbs +148 -12
- data/sig/rigor/sig_gen/skip_reason_catalog.rbs +21 -0
- data/sig/rigor/source.rbs +4 -4
- data/sig/rigor/testing.rbs +10 -4
- data/sig/rigor/type.rbs +33 -1
- data/sig/rigor.rbs +39 -20
- data/skills/rigor-ask/SKILL.md +16 -11
- data/skills/rigor-baseline-reduce/SKILL.md +4 -6
- data/skills/rigor-ci-setup/SKILL.md +17 -21
- data/skills/rigor-doctor/SKILL.md +24 -22
- data/skills/rigor-doctor/references/01-checks.md +97 -33
- data/skills/rigor-editor-setup/SKILL.md +6 -4
- data/skills/rigor-mcp-setup/SKILL.md +5 -4
- data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
- data/skills/rigor-next-steps/SKILL.md +4 -2
- data/skills/rigor-plugin-author/SKILL.md +19 -23
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +25 -27
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
- data/skills/rigor-plugin-review/SKILL.md +6 -4
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
- data/skills/rigor-plugin-tune/SKILL.md +4 -2
- data/skills/rigor-project-init/SKILL.md +15 -10
- data/skills/rigor-project-init/references/01-detect.md +13 -9
- data/skills/rigor-project-init/references/02-configure.md +43 -14
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
- data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
- data/skills/rigor-project-init/references/06-agent-contract.md +67 -0
- data/skills/rigor-protection-uplift/SKILL.md +4 -6
- data/skills/rigor-rbs-setup/SKILL.md +4 -2
- data/skills/rigor-type-oracle/SKILL.md +226 -0
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +265 -0
- data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
- data/skills/rigor-type-oracle/references/03-gap-protocol.md +125 -0
- data/skills/rigor-unused-adjudicate/SKILL.md +12 -7
- data/skills/rigor-upgrade/SKILL.md +13 -8
- metadata +133 -3
- data/lib/rigor/cli/probe_environment.rb +0 -85
- data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
|
@@ -9,26 +9,40 @@ require_relative "../source/constant_path"
|
|
|
9
9
|
require_relative "../source/node_children"
|
|
10
10
|
require_relative "../source/node_walker"
|
|
11
11
|
require_relative "../analysis/self_call_resolution_recorder"
|
|
12
|
+
require_relative "block_call_timing"
|
|
13
|
+
require_relative "block_repetition"
|
|
12
14
|
require_relative "block_parameter_binder"
|
|
13
15
|
require_relative "method_parameter_binder"
|
|
14
16
|
require_relative "body_fixpoint"
|
|
15
17
|
require_relative "budget_trace"
|
|
16
18
|
require_relative "captured_locals"
|
|
19
|
+
require_relative "closure_escape_analyzer"
|
|
20
|
+
require_relative "receiver_blind_block"
|
|
17
21
|
require_relative "def_node_resolver"
|
|
18
22
|
require_relative "dynamic_origin"
|
|
23
|
+
require_relative "error_info"
|
|
24
|
+
require_relative "external_ancestor_resolution"
|
|
19
25
|
require_relative "origin_lookup"
|
|
20
26
|
require_relative "../effects/collector"
|
|
21
27
|
require_relative "fallback"
|
|
22
28
|
require_relative "flow_tracer"
|
|
29
|
+
require_relative "index_write_widening"
|
|
23
30
|
require_relative "indexed_narrowing"
|
|
31
|
+
require_relative "jump_targets"
|
|
32
|
+
require_relative "define_method_block_self"
|
|
24
33
|
require_relative "macro_block_self_type"
|
|
34
|
+
require_relative "match_rebinding"
|
|
25
35
|
require_relative "method_dispatcher"
|
|
26
36
|
require_relative "mutation_widening"
|
|
27
37
|
require_relative "narrowing"
|
|
38
|
+
require_relative "optimistic_origin"
|
|
28
39
|
require_relative "receiver_alias"
|
|
40
|
+
require_relative "repeated_or_writes"
|
|
29
41
|
require_relative "singleton_object_constant"
|
|
30
|
-
require_relative "
|
|
42
|
+
require_relative "stored_block_call"
|
|
31
43
|
require_relative "struct_fold_safety"
|
|
44
|
+
require_relative "unknown_store_widening"
|
|
45
|
+
require_relative "unthreaded_rebinds"
|
|
32
46
|
require_relative "version_guard"
|
|
33
47
|
|
|
34
48
|
module Rigor
|
|
@@ -99,36 +113,36 @@ module Rigor
|
|
|
99
113
|
Prism::ConstantPathNode => :type_of_constant_path,
|
|
100
114
|
Prism::ConstantWriteNode => :type_of_assignment_write,
|
|
101
115
|
Prism::ConstantPathWriteNode => :type_of_assignment_write,
|
|
102
|
-
Prism::ConstantOperatorWriteNode => :
|
|
103
|
-
Prism::ConstantOrWriteNode => :
|
|
104
|
-
Prism::ConstantAndWriteNode => :
|
|
105
|
-
Prism::ConstantPathOperatorWriteNode => :
|
|
106
|
-
Prism::ConstantPathOrWriteNode => :
|
|
107
|
-
Prism::ConstantPathAndWriteNode => :
|
|
116
|
+
Prism::ConstantOperatorWriteNode => :type_of_compound_constant_write,
|
|
117
|
+
Prism::ConstantOrWriteNode => :type_of_compound_constant_write,
|
|
118
|
+
Prism::ConstantAndWriteNode => :type_of_compound_constant_write,
|
|
119
|
+
Prism::ConstantPathOperatorWriteNode => :type_of_compound_constant_write,
|
|
120
|
+
Prism::ConstantPathOrWriteNode => :type_of_compound_constant_write,
|
|
121
|
+
Prism::ConstantPathAndWriteNode => :type_of_compound_constant_write,
|
|
108
122
|
# Self and instance/class/global variables
|
|
109
123
|
Prism::SelfNode => :type_of_self_node,
|
|
110
124
|
Prism::InstanceVariableReadNode => :type_of_instance_variable_read,
|
|
111
125
|
Prism::InstanceVariableWriteNode => :type_of_assignment_write,
|
|
112
|
-
Prism::InstanceVariableOperatorWriteNode => :
|
|
113
|
-
Prism::InstanceVariableOrWriteNode => :
|
|
114
|
-
Prism::InstanceVariableAndWriteNode => :
|
|
126
|
+
Prism::InstanceVariableOperatorWriteNode => :type_of_compound_variable_write,
|
|
127
|
+
Prism::InstanceVariableOrWriteNode => :type_of_compound_variable_write,
|
|
128
|
+
Prism::InstanceVariableAndWriteNode => :type_of_compound_variable_write,
|
|
115
129
|
Prism::ClassVariableReadNode => :type_of_class_variable_read,
|
|
116
130
|
Prism::ClassVariableWriteNode => :type_of_assignment_write,
|
|
117
|
-
Prism::ClassVariableOperatorWriteNode => :
|
|
118
|
-
Prism::ClassVariableOrWriteNode => :
|
|
119
|
-
Prism::ClassVariableAndWriteNode => :
|
|
131
|
+
Prism::ClassVariableOperatorWriteNode => :type_of_compound_variable_write,
|
|
132
|
+
Prism::ClassVariableOrWriteNode => :type_of_compound_variable_write,
|
|
133
|
+
Prism::ClassVariableAndWriteNode => :type_of_compound_variable_write,
|
|
120
134
|
Prism::GlobalVariableReadNode => :type_of_global_variable_read,
|
|
121
135
|
Prism::GlobalVariableWriteNode => :type_of_assignment_write,
|
|
122
|
-
Prism::GlobalVariableOperatorWriteNode => :
|
|
123
|
-
Prism::GlobalVariableOrWriteNode => :
|
|
124
|
-
Prism::GlobalVariableAndWriteNode => :
|
|
136
|
+
Prism::GlobalVariableOperatorWriteNode => :type_of_compound_variable_write,
|
|
137
|
+
Prism::GlobalVariableOrWriteNode => :type_of_compound_variable_write,
|
|
138
|
+
Prism::GlobalVariableAndWriteNode => :type_of_compound_variable_write,
|
|
125
139
|
# Compound writes that share the `.value` rvalue accessor
|
|
126
|
-
Prism::LocalVariableOperatorWriteNode => :
|
|
127
|
-
Prism::LocalVariableOrWriteNode => :
|
|
128
|
-
Prism::LocalVariableAndWriteNode => :
|
|
129
|
-
Prism::IndexOperatorWriteNode => :
|
|
130
|
-
Prism::IndexOrWriteNode => :
|
|
131
|
-
Prism::IndexAndWriteNode => :
|
|
140
|
+
Prism::LocalVariableOperatorWriteNode => :type_of_compound_variable_write,
|
|
141
|
+
Prism::LocalVariableOrWriteNode => :type_of_compound_variable_write,
|
|
142
|
+
Prism::LocalVariableAndWriteNode => :type_of_compound_variable_write,
|
|
143
|
+
Prism::IndexOperatorWriteNode => :type_of_index_compound_write,
|
|
144
|
+
Prism::IndexOrWriteNode => :type_of_index_compound_write,
|
|
145
|
+
Prism::IndexAndWriteNode => :type_of_index_compound_write,
|
|
132
146
|
Prism::MultiWriteNode => :type_of_assignment_write,
|
|
133
147
|
# LHS-only target nodes (destructuring assignment, pattern matching, `for x in xs`, block parameter
|
|
134
148
|
# `|a, (b, c)|`). They have no value to extract — the type-of pass acknowledges the node class so the
|
|
@@ -183,8 +197,8 @@ module Rigor
|
|
|
183
197
|
Prism::SplatNode => :type_of_non_value,
|
|
184
198
|
# Control flow (Slice 3 phase 1): branch types are unioned, jumps
|
|
185
199
|
# type as Bot, loops type as Constant[nil].
|
|
186
|
-
Prism::IfNode => :
|
|
187
|
-
Prism::UnlessNode => :
|
|
200
|
+
Prism::IfNode => :type_of_conditional,
|
|
201
|
+
Prism::UnlessNode => :type_of_conditional,
|
|
188
202
|
Prism::ElseNode => :type_of_else,
|
|
189
203
|
Prism::AndNode => :type_of_and_or,
|
|
190
204
|
Prism::OrNode => :type_of_and_or,
|
|
@@ -201,7 +215,7 @@ module Rigor
|
|
|
201
215
|
Prism::NextNode => :type_of_jump,
|
|
202
216
|
Prism::RetryNode => :type_of_jump,
|
|
203
217
|
Prism::RedoNode => :type_of_jump,
|
|
204
|
-
Prism::YieldNode => :
|
|
218
|
+
Prism::YieldNode => :type_of_yield,
|
|
205
219
|
Prism::SuperNode => :type_of_dynamic_top,
|
|
206
220
|
Prism::ForwardingArgumentsNode => :type_of_non_value,
|
|
207
221
|
Prism::WhileNode => :type_of_loop,
|
|
@@ -229,10 +243,15 @@ module Rigor
|
|
|
229
243
|
NO_STATIC_HASH_KEY = Object.new.freeze
|
|
230
244
|
private_constant :NO_STATIC_HASH_KEY
|
|
231
245
|
|
|
232
|
-
|
|
246
|
+
# `operand_types` is the identity-comparing `Prism::Node => Rigor::Type` table of the later operands a
|
|
247
|
+
# threading `StatementEvaluator` root typed from the scope the earlier operands left (issue #1256), or nil.
|
|
248
|
+
# It is consulted only by this typer's own descent: a node it holds is the same node typed from its own
|
|
249
|
+
# entry scope, so answering it here is what typing it there would answer.
|
|
250
|
+
def initialize(scope:, tracer: nil, operand_types: nil, typing_node: nil)
|
|
233
251
|
@scope = scope
|
|
234
252
|
@tracer = tracer
|
|
235
|
-
@
|
|
253
|
+
@operand_types = operand_types
|
|
254
|
+
@typing_node = typing_node
|
|
236
255
|
end
|
|
237
256
|
|
|
238
257
|
def type_of(node)
|
|
@@ -256,6 +275,9 @@ module Rigor
|
|
|
256
275
|
declared = scope.declared_types[node]
|
|
257
276
|
return declared if declared
|
|
258
277
|
|
|
278
|
+
threaded = @operand_types&.[](node)
|
|
279
|
+
return threaded if threaded
|
|
280
|
+
|
|
259
281
|
return type_of_virtual(node) if node.is_a?(AST::Node)
|
|
260
282
|
|
|
261
283
|
handler = PRISM_DISPATCH[node.class]
|
|
@@ -275,6 +297,28 @@ module Rigor
|
|
|
275
297
|
infer_user_method_return(def_node, receiver, arg_types)
|
|
276
298
|
end
|
|
277
299
|
|
|
300
|
+
# The `receiver[args]` read a compound index write performs before it stores (`c[k] += v` reads `c[k]`),
|
|
301
|
+
# or nil when no tier answers. No `Prism::CallNode` for that read exists in the tree, so the write node
|
|
302
|
+
# itself stands in as the call context: it carries the same `receiver` / `arguments` / `block` a
|
|
303
|
+
# plain `c[k]` call does, which is what the context-reading tiers consult. With it, the read reaches
|
|
304
|
+
# the tiers a plain `c[k]` takes, in the same order — the own-`def` override check, the dispatcher's
|
|
305
|
+
# node- and scope-gated tiers, then the project `def` inference a dispatch miss falls to — so a project
|
|
306
|
+
# `[]` with no signature answers from its body instead of the whole write degrading to `Dynamic[top]`.
|
|
307
|
+
#
|
|
308
|
+
# A plugin `dynamic_return` rule written for `[]` still does not answer this read. Its `methods:` gate
|
|
309
|
+
# matches `call_node.name`, which an index write does not have, and the shipped rules decline any node
|
|
310
|
+
# that is not a `Prism::CallNode`, by an explicit check or through the rescue around a rule that reads
|
|
311
|
+
# `name` anyway. Minting a `Prism::CallNode` for the read would hand plugins a node that is not in the
|
|
312
|
+
# tree, through a constructor whose field list changed inside the `prism` range the gemspec accepts.
|
|
313
|
+
def implicit_index_read_type(node, receiver, arg_types)
|
|
314
|
+
return splat_index_read_type(node, receiver, arg_types) if node.arguments&.arguments&.any?(Prism::SplatNode)
|
|
315
|
+
|
|
316
|
+
try_overriding_def_dispatch(node, receiver, arg_types, method_name: :[]) ||
|
|
317
|
+
index_read_dispatch(node, receiver, arg_types) ||
|
|
318
|
+
try_user_method_inference(receiver, node, arg_types, method_name: :[]) ||
|
|
319
|
+
try_project_singleton_inference(receiver, node, arg_types, method_name: :[])
|
|
320
|
+
end
|
|
321
|
+
|
|
278
322
|
# ADR-89 WD2 — the current run's return memo bucket as `{ def_node => [MemoEntry, …] }` (only entries
|
|
279
323
|
# that carry a call descriptor, i.e. every stored entry). Read by the incremental session right after a
|
|
280
324
|
# recording run to harvest each analyzed callee's observed call keys → return descriptors. Returns an
|
|
@@ -287,6 +331,70 @@ module Rigor
|
|
|
287
331
|
slot[1].transform_values(&:values)
|
|
288
332
|
end
|
|
289
333
|
|
|
334
|
+
# Issue #720 — the value the block passed by the call site currently being re-typed evaluates to, or
|
|
335
|
+
# `nil` outside such a frame. Read by {#type_of_yield}.
|
|
336
|
+
#
|
|
337
|
+
# A thread-local rather than a `Scope` field for the same reason the `return` / `next` / `break` sinks
|
|
338
|
+
# are: `yield` names the frame's caller, not a binding, and the body is walked through scopes the
|
|
339
|
+
# inference rebuilds freely. It is installed unconditionally — with `nil` — at every user-method
|
|
340
|
+
# inference frame, so a callee reached WITHOUT a block from inside a yielding body cannot read its
|
|
341
|
+
# caller's block.
|
|
342
|
+
YIELD_VALUE_KEY = :__rigor_yield_value_type__
|
|
343
|
+
private_constant :YIELD_VALUE_KEY
|
|
344
|
+
|
|
345
|
+
def self.current_yield_value_type
|
|
346
|
+
Thread.current[YIELD_VALUE_KEY]
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Runs `block` with `type` installed as the frame's yield value, restoring the previous frame on exit.
|
|
350
|
+
def self.with_yield_value_type(type)
|
|
351
|
+
previous = Thread.current[YIELD_VALUE_KEY]
|
|
352
|
+
Thread.current[YIELD_VALUE_KEY] = type
|
|
353
|
+
begin
|
|
354
|
+
yield
|
|
355
|
+
ensure
|
|
356
|
+
Thread.current[YIELD_VALUE_KEY] = previous
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Issue #1125 — the argument types of the call currently being re-typed into the body being walked,
|
|
361
|
+
# or nil outside such a frame. A `f(...)` call inside `def m(...)` re-supplies exactly the arguments
|
|
362
|
+
# `m` itself was called with, so {#call_arg_types} expands the forwarding node to THIS list instead of
|
|
363
|
+
# the `Dynamic[top]` a bare `Prism::ForwardingArgumentsNode` types as.
|
|
364
|
+
#
|
|
365
|
+
# A thread-local for the same reason the yield value is (see {YIELD_VALUE_KEY}): `...` names the
|
|
366
|
+
# frame's caller, not a binding, and the body is walked through scopes the inference rebuilds freely.
|
|
367
|
+
# It is installed unconditionally — with nil — at every user-method inference frame, so a body reached
|
|
368
|
+
# without `...` cannot read its caller's argument list. Because each frame installs its OWN list, a
|
|
369
|
+
# chain `a(...) -> b(...) -> c(...)` threads through: `b`'s frame holds the types `a` expanded and
|
|
370
|
+
# `c`'s holds `b`'s. The list is the callee's call-site `arg_types`, which is what keeps the ADR-84
|
|
371
|
+
# return memo's `(def_node, receiver, arg_types)` key complete for a forwarding def.
|
|
372
|
+
FORWARDED_ARGS_KEY = :__rigor_forwarded_call_arg_types__
|
|
373
|
+
private_constant :FORWARDED_ARGS_KEY
|
|
374
|
+
|
|
375
|
+
def self.current_forwarded_arg_types
|
|
376
|
+
Thread.current[FORWARDED_ARGS_KEY]
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Runs `block` with `types` installed as the frame's forwarded argument list, restoring the previous
|
|
380
|
+
# frame on exit.
|
|
381
|
+
def self.with_forwarded_arg_types(types, &block)
|
|
382
|
+
previous = Thread.current[FORWARDED_ARGS_KEY]
|
|
383
|
+
Thread.current[FORWARDED_ARGS_KEY] = types
|
|
384
|
+
begin
|
|
385
|
+
block.call
|
|
386
|
+
ensure
|
|
387
|
+
Thread.current[FORWARDED_ARGS_KEY] = previous
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# The two call-site channels a user-method body may read from its caller — the block a `yield`
|
|
392
|
+
# reaches ({#with_yield_value_type}) and the argument list `...` re-supplies
|
|
393
|
+
# ({#with_forwarded_arg_types}) — installed together, both even when nil, for exactly one frame.
|
|
394
|
+
def self.with_call_site_frame(yield_type, forwarded_arg_types, &)
|
|
395
|
+
with_yield_value_type(yield_type) { with_forwarded_arg_types(forwarded_arg_types, &) }
|
|
396
|
+
end
|
|
397
|
+
|
|
290
398
|
private
|
|
291
399
|
|
|
292
400
|
attr_reader :scope, :tracer
|
|
@@ -318,6 +426,182 @@ module Rigor
|
|
|
318
426
|
type_of(node.value)
|
|
319
427
|
end
|
|
320
428
|
|
|
429
|
+
# `v += 1` / `@v ||= x` / `$v &&= x` as an EXPRESSION. The value of a compound write is the value it
|
|
430
|
+
# stores, which is a function of the variable's CURRENT binding — not of the rvalue alone. Typing it as
|
|
431
|
+
# `type_of(node.value)` answered `1` for `total += 1` whatever `total` held.
|
|
432
|
+
#
|
|
433
|
+
# `StatementEvaluator#compound_eval` already computes the right answer on the straight-line path, so the
|
|
434
|
+
# gap only showed where a body is typed WITHOUT that evaluator: the block-return pass types a
|
|
435
|
+
# single-statement body by expression alone, and issue #617 residue (3) is the live consequence —
|
|
436
|
+
# `total = 0; r = [1, 2].map { total += 1 }` folded to `[1, 1]`, and `r.last == 1` fired a false
|
|
437
|
+
# `flow.always-truthy-condition` on a program where it is `2`. Under the per-element fold the captured
|
|
438
|
+
# local is already bound to its converged `Integer` (issue #587 (b)); reading that binding here is what
|
|
439
|
+
# turns the stale pin into the honest `Integer`.
|
|
440
|
+
#
|
|
441
|
+
# The algebra is `StatementEvaluator#compound_result_type`'s, deliberately: the two must not disagree
|
|
442
|
+
# about what a compound write evaluates to. An unbound target is `Dynamic[top]` — the same fallback the
|
|
443
|
+
# evaluator takes — and an operator the receiver does not answer widens to `Dynamic[top]` rather than
|
|
444
|
+
# inventing the rvalue.
|
|
445
|
+
#
|
|
446
|
+
# Constant targets have {#type_of_compound_constant_write}, over the same algebra. Index targets have their
|
|
447
|
+
# own handler, {#type_of_index_compound_write}.
|
|
448
|
+
def type_of_compound_variable_write(node)
|
|
449
|
+
compound_write_value(node, compound_write_current_binding(node), type_of(node.value))
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# `H ||= 0` / `Conf::LIMIT += 1` as an EXPRESSION: the value it stores, by
|
|
453
|
+
# {#type_of_compound_variable_write}'s algebra. Typed as the rvalue alone, `H = { x: 1 }; v = (H ||= 0);
|
|
454
|
+
# v[:x]` reported `Integer#[]` on a program whose `v` is `H`, and `F.transform_values { |e| F ||= 0 }`
|
|
455
|
+
# folded every value to `0`.
|
|
456
|
+
#
|
|
457
|
+
# A constant has no scope binding, so its current binding is what a plain read of the same spelling
|
|
458
|
+
# resolves to at the write site — the lexical ladder, the in-source table of plain writes, then RBS —
|
|
459
|
+
# less the caller-derived rungs a top-level body keeps for reads Ruby raises on. The memoization idiom
|
|
460
|
+
# (`def registry = REGISTRY ||= {}`, legal where a plain `REGISTRY = {}` is a dynamic constant
|
|
461
|
+
# assignment) writes a constant nothing else binds, so an unresolved target keeps the rvalue reading
|
|
462
|
+
# exactly as an unbound variable does — unless a write other than a memo binds it
|
|
463
|
+
# ({#written_constant_binding}). A path whose base renders no static name (`klass::X`, `self::X`) names
|
|
464
|
+
# no binding the resolver can look up, and reads as unbound too, on the same condition.
|
|
465
|
+
def type_of_compound_constant_write(node)
|
|
466
|
+
rhs = type_of(node.value)
|
|
467
|
+
compound_write_value(node, compound_write_constant_binding(node), rhs)
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def compound_write_value(node, current, rhs)
|
|
471
|
+
case node
|
|
472
|
+
when Prism::LocalVariableOrWriteNode, Prism::InstanceVariableOrWriteNode,
|
|
473
|
+
Prism::ClassVariableOrWriteNode, Prism::GlobalVariableOrWriteNode,
|
|
474
|
+
Prism::ConstantOrWriteNode, Prism::ConstantPathOrWriteNode
|
|
475
|
+
# An UNBOUND target is the memoization idiom (`def self.default = @default ||= new`): nothing
|
|
476
|
+
# has written the variable on any path the analyzer saw, so the stored value is the rvalue.
|
|
477
|
+
# Reading it as `Dynamic[top] | rhs` would skip every memoized singleton in `sig-gen`
|
|
478
|
+
# (ADR-5 optimism; three `.default` readers went `sig.skipped.untyped-return` without this).
|
|
479
|
+
#
|
|
480
|
+
# The index rule's exception applies: an rvalue with no truthy part makes the write a guard, not a
|
|
481
|
+
# memo. `@settings ||= raise "boot first"` returns only when something the analyzer did not see set
|
|
482
|
+
# `@settings`, and `@verbose ||= false` answers `true` once one did, so the value is that unseen
|
|
483
|
+
# binding — `Dynamic[top]`, the statement evaluator's reading of an unbound target — never the
|
|
484
|
+
# rvalue's `bot` or `false` alone. A local is no exception: the scope a loop body is typed from does
|
|
485
|
+
# not carry what the previous iteration stored.
|
|
486
|
+
if current.nil?
|
|
487
|
+
return rhs unless Narrowing.narrow_truthy(rhs).is_a?(Type::Bot)
|
|
488
|
+
|
|
489
|
+
current = dynamic_top
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
Type::Combinator.union(Narrowing.narrow_truthy(current), rhs)
|
|
493
|
+
when Prism::LocalVariableAndWriteNode, Prism::InstanceVariableAndWriteNode,
|
|
494
|
+
Prism::ClassVariableAndWriteNode, Prism::GlobalVariableAndWriteNode,
|
|
495
|
+
Prism::ConstantAndWriteNode, Prism::ConstantPathAndWriteNode
|
|
496
|
+
# `&&=` is no memo: an unset target returns its own `nil` without evaluating the rvalue (a constant or
|
|
497
|
+
# class variable raises instead). Beside the rvalue, an UNBOUND target's value is its own falsey value —
|
|
498
|
+
# `nil` when unset, or a falsey value a write the analyzer did not see stored — which
|
|
499
|
+
# `narrow_falsey(Dynamic[top])` carries as `Dynamic[top]`, the statement evaluator's reading. The index
|
|
500
|
+
# rule says the same of `h[k] &&= v`. Read as the rvalue, `if (@x &&= 1)` folded always-truthy on a
|
|
501
|
+
# program that takes the else arm.
|
|
502
|
+
Type::Combinator.union(Narrowing.narrow_falsey(current || dynamic_top), rhs)
|
|
503
|
+
else
|
|
504
|
+
compound_operator_result(current || dynamic_top, rhs, node.binary_operator)
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# `h[k] += v` / `h[k] ||= v` / `h[k] &&= v` as an EXPRESSION. Like a variable compound write, its value is
|
|
509
|
+
# what it stores through `[]=` — the dispatched `h[k] + v`, `truthy(h[k]) | v`, `falsey(h[k]) | v` — which
|
|
510
|
+
# reads the slot's current type, recorded indexed narrowing included. Typed as the rvalue alone it answered
|
|
511
|
+
# `1` for `counts[w] += 1`, so `r = words.map { |w| counts[w] += 1 }` pinned every position to `1` and
|
|
512
|
+
# `r.last == 1` drew a false `flow.always-truthy-condition`.
|
|
513
|
+
#
|
|
514
|
+
# The statement evaluator already owned that algebra for the straight-line write and the `[]=` widening
|
|
515
|
+
# join, so this reads its answer rather than keeping a second copy. It asks for the value alone, not a
|
|
516
|
+
# whole `evaluate`: the widening and the narrowing record are scope effects a value position discards,
|
|
517
|
+
# and the memoizing `@cache[k] ||= build(k)` tail is common enough not to pay for them.
|
|
518
|
+
#
|
|
519
|
+
# One exception carries over, narrowed, from {#type_of_compound_variable_write}: a memoizing `||=` whose
|
|
520
|
+
# slot the analyzer has no evidence about reads as the rvalue. The evaluator's value method owns it,
|
|
521
|
+
# because it is decided on the `[]` read the evaluator performs.
|
|
522
|
+
def type_of_index_compound_write(node)
|
|
523
|
+
StatementEvaluator.new(scope: scope, tracer: tracer).index_compound_write_value(node)
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
def compound_write_current_binding(node)
|
|
527
|
+
case node
|
|
528
|
+
when Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode,
|
|
529
|
+
Prism::LocalVariableAndWriteNode then scope.local(node.name)
|
|
530
|
+
when Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableOrWriteNode,
|
|
531
|
+
Prism::InstanceVariableAndWriteNode then scope.ivar(node.name)
|
|
532
|
+
when Prism::ClassVariableOperatorWriteNode, Prism::ClassVariableOrWriteNode,
|
|
533
|
+
Prism::ClassVariableAndWriteNode then scope.cvar(node.name)
|
|
534
|
+
else scope.global(node.name)
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def compound_write_constant_binding(node)
|
|
539
|
+
case node
|
|
540
|
+
when Prism::ConstantOperatorWriteNode, Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode
|
|
541
|
+
full_name = node.name.to_s
|
|
542
|
+
rooted = false
|
|
543
|
+
else
|
|
544
|
+
full_name = Source::ConstantPath.qualified_name_or_nil(node.target)
|
|
545
|
+
return unnamed_path_binding(node.target) if full_name.nil?
|
|
546
|
+
|
|
547
|
+
rooted = Source::ConstantPath.rooted?(node.target)
|
|
548
|
+
end
|
|
549
|
+
resolve_constant_name(full_name, rooted: rooted, caller_derived: false) ||
|
|
550
|
+
written_constant_binding(full_name, rooted)
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
# A constant the plain read cannot resolve may still be bound: another file writes a value that never
|
|
554
|
+
# published (`H = { x: 1 }`), the compound write withdrew one by being the second writer the census
|
|
555
|
+
# counts, or this file writes it in a form its own table does not carry (`A, B = …`, `A = B = …`). The
|
|
556
|
+
# value is one the analyzer does not have, so the binding is `Dynamic[top]` rather than the memo's
|
|
557
|
+
# unbound reading. The one write that does not bind is a memo `||=` no other file shares, so the
|
|
558
|
+
# memoization idiom keeps that reading however often its file repeats it; memos of the name in two files
|
|
559
|
+
# bind, since either may load first and set what the other reads.
|
|
560
|
+
#
|
|
561
|
+
# Which name the write reads is the ladder's to decide, not the spelling's: the same resolution runs
|
|
562
|
+
# again with only the census's binding names in the in-source table, so `Other::REGISTRY` elsewhere
|
|
563
|
+
# leaves a top-level `REGISTRY ||= {}` its memo reading. Two census spellings the ladder cannot reach
|
|
564
|
+
# still bind: a write through a base nothing names (`k::X = 1`), which may have written any of them,
|
|
565
|
+
# and a path the census keeps as written ([#690](https://github.com/rigortype/rigor/issues/690)), so
|
|
566
|
+
# `Foo::BAR = …` inside `module M` binds `M::Foo::BAR ||= 0`.
|
|
567
|
+
def written_constant_binding(full_name, rooted)
|
|
568
|
+
names = scope.bound_constant_names(full_name)
|
|
569
|
+
return nil if names.empty?
|
|
570
|
+
return dynamic_top if names.any? { |name| unladdered_census_name?(name, full_name) }
|
|
571
|
+
|
|
572
|
+
written = names.to_h { |name| [name, dynamic_top] }
|
|
573
|
+
probe = scope.with_discovery(scope.discovery.with(in_source_constants: written))
|
|
574
|
+
Reflection.resolve_constant_type(full_name, scope: probe, rooted: rooted, caller_derived: false)
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def unladdered_census_name?(name, full_name)
|
|
578
|
+
return true if name.start_with?(ScopeIndexer::DYNAMIC_TARGET_PREFIX)
|
|
579
|
+
|
|
580
|
+
name.include?("::") && full_name.end_with?("::#{name}")
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
# `self::X ||= v` / `klass::X ||= v` name no constant the resolver can look up, so any binding write
|
|
584
|
+
# that shares the last segment may be the one the base reaches. The answer now depends on other files'
|
|
585
|
+
# writes, and this path never reaches `Reflection.resolve_constant_type`, which records the
|
|
586
|
+
# `constant:<segment>` edge for every other form — so it records the edge itself, or an incremental
|
|
587
|
+
# run keeps serving the reading from before another file's write of the segment appeared.
|
|
588
|
+
def unnamed_path_binding(target)
|
|
589
|
+
segment = target.name&.to_s
|
|
590
|
+
return nil if segment.nil?
|
|
591
|
+
|
|
592
|
+
Analysis::DependencyRecorder.read_name(:constant, segment) if Analysis::DependencyRecorder.active?
|
|
593
|
+
dynamic_top unless scope.bound_constant_names(segment).empty?
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
def compound_operator_result(current, rhs, operator)
|
|
597
|
+
MethodDispatcher.dispatch(
|
|
598
|
+
receiver_type: current,
|
|
599
|
+
method_name: operator.to_sym,
|
|
600
|
+
arg_types: [rhs],
|
|
601
|
+
environment: scope.environment
|
|
602
|
+
) || dynamic_top
|
|
603
|
+
end
|
|
604
|
+
|
|
321
605
|
# Slice 7 phase 1 — instance/class/global variable reads. Each lookup returns the type currently bound
|
|
322
606
|
# in the surrounding scope's per-kind binding map (populated by `StatementEvaluator` write handlers
|
|
323
607
|
# within the same method body), falling through to `Dynamic[Top]` when no binding is recorded.
|
|
@@ -372,6 +656,19 @@ module Rigor
|
|
|
372
656
|
dynamic_top
|
|
373
657
|
end
|
|
374
658
|
|
|
659
|
+
# Issue #720 — `yield` evaluates the block the CALLER passed, so its value is that block's value. The
|
|
660
|
+
# caller is only known when this body is being re-typed on behalf of one call site
|
|
661
|
+
# ({#infer_user_method_return} installs the frame); on the main walk over a `def` no caller exists and
|
|
662
|
+
# the answer stays `Dynamic[top]`.
|
|
663
|
+
#
|
|
664
|
+
# Nothing about the yield's arguments is checked here. The block's return type was computed at the call
|
|
665
|
+
# site under the parameter bindings the call implies, so it already answers "what does this block
|
|
666
|
+
# evaluate to for this caller"; a `yield` that passes different arguments than the block's parameters
|
|
667
|
+
# accept is a separate (arity) question this frame does not decide.
|
|
668
|
+
def type_of_yield(_node)
|
|
669
|
+
ExpressionTyper.current_yield_value_type || dynamic_top
|
|
670
|
+
end
|
|
671
|
+
|
|
375
672
|
# `defined?(expr)` returns `String | nil` per Ruby semantics — a description of the expression's
|
|
376
673
|
# category (`"local-variable"`, `"method"`, ...) when defined, or `nil` when not. The argument is not
|
|
377
674
|
# evaluated (it is statically inspected by the runtime), so the typer does not recurse into it.
|
|
@@ -428,7 +725,17 @@ module Rigor
|
|
|
428
725
|
# `Inference::FallbackTracer` from inside `Rigor::CLI::Foo` resolves to
|
|
429
726
|
# `Rigor::Inference::FallbackTracer`.
|
|
430
727
|
def type_of_constant_read(node)
|
|
431
|
-
|
|
728
|
+
guard_narrowed_constant(node) ||
|
|
729
|
+
resolve_constant_name(node.name.to_s) || unresolved_constant_fallback(node, node.name.to_s)
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
# Issue #1429 — the type a guard narrowed this constant reference to on the edge being typed
|
|
733
|
+
# (`STDOUT.is_a?(StringIO) ? STDOUT.string : nil`), or nil. Keyed by spelling ({Narrowing.constant_key}).
|
|
734
|
+
def guard_narrowed_constant(node)
|
|
735
|
+
return nil if scope.constant_narrowings.empty?
|
|
736
|
+
|
|
737
|
+
key = Narrowing.constant_key(node)
|
|
738
|
+
key && scope.constant_narrowing(key)
|
|
432
739
|
end
|
|
433
740
|
|
|
434
741
|
# A leading `::` (`::Rails`, `::Rails::Application`) is Ruby's escape hatch out of the lexical ladder:
|
|
@@ -436,6 +743,9 @@ module Rigor
|
|
|
436
743
|
# deliberately un-rooted (the discovery tables are keyed that way), so the marker rides alongside it
|
|
437
744
|
# into the resolver (#614).
|
|
438
745
|
def type_of_constant_path(node)
|
|
746
|
+
narrowed = guard_narrowed_constant(node)
|
|
747
|
+
return narrowed if narrowed
|
|
748
|
+
|
|
439
749
|
full_name = Source::ConstantPath.qualified_name_or_nil(node)
|
|
440
750
|
return fallback_for(node, family: :prism) if full_name.nil?
|
|
441
751
|
|
|
@@ -504,16 +814,17 @@ module Rigor
|
|
|
504
814
|
# in-source value, RBS constant, across the peeled `::` prefix candidates) is reused by
|
|
505
815
|
# `Inference::Narrowing`'s `Constant[Regexp]` match-operand recognition. Returns the matched
|
|
506
816
|
# `Rigor::Type` or nil; the caller decides whether to fall back.
|
|
507
|
-
def resolve_constant_name(name, rooted: false)
|
|
508
|
-
Reflection.resolve_constant_type(name, scope: scope, rooted: rooted)
|
|
817
|
+
def resolve_constant_name(name, rooted: false, caller_derived: true)
|
|
818
|
+
Reflection.resolve_constant_type(name, scope: scope, rooted: rooted, caller_derived: caller_derived)
|
|
509
819
|
end
|
|
510
820
|
|
|
511
821
|
# Slice 5 phase 1 upgrades hash literals to `HashShape{...}` when every entry is a static `AssocNode`
|
|
512
822
|
# whose key is a value-pinned scalar literal — Symbol, plain String, Integer, Float, `true`, `false`,
|
|
513
823
|
# or `nil` (covering `{ a: 1, "b" => 2 }` and `{ 1 => 2, 1.0 => 4 }` alike) — falling back to the
|
|
514
824
|
# generic `Hash[K, V]` form otherwise. Splatted entries (`{ **other }`) and dynamic keys widen to the
|
|
515
|
-
# underlying `Hash[K, V]` form by unioning the types each entry exposes
|
|
516
|
-
#
|
|
825
|
+
# underlying `Hash[K, V]` form by unioning the types each entry exposes — a splat exposes the `[K, V]`
|
|
826
|
+
# of the hash it copies plus a `Dynamic[top]` arm (see {#hash_splat_pair}), so every entry contributes a
|
|
827
|
+
# pair. A splat never keeps a shape, even over an exact closed one.
|
|
517
828
|
def type_of_hash(node)
|
|
518
829
|
elements = node.respond_to?(:elements) ? node.elements : []
|
|
519
830
|
# v0.0.7 — `{}` resolves to the empty `HashShape{}` carrier rather than `Nominal[Hash]`, mirroring the
|
|
@@ -525,8 +836,6 @@ module Rigor
|
|
|
525
836
|
return shape if shape
|
|
526
837
|
|
|
527
838
|
keys, values = generic_hash_pairs_for(elements)
|
|
528
|
-
return Type::Combinator.nominal_of(Hash) if keys.empty? || values.empty?
|
|
529
|
-
|
|
530
839
|
Type::Combinator.nominal_of(
|
|
531
840
|
Hash,
|
|
532
841
|
type_args: [Type::Combinator.union(*keys), Type::Combinator.union(*values)]
|
|
@@ -580,14 +889,64 @@ module Rigor
|
|
|
580
889
|
keys = []
|
|
581
890
|
values = []
|
|
582
891
|
elements.each do |entry|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
892
|
+
if entry.is_a?(Prism::AssocNode)
|
|
893
|
+
keys << type_of(entry.key)
|
|
894
|
+
values << type_of(entry.value)
|
|
895
|
+
else
|
|
896
|
+
key, value = hash_splat_pair(entry)
|
|
897
|
+
keys << key
|
|
898
|
+
values << value
|
|
899
|
+
end
|
|
587
900
|
end
|
|
588
901
|
[keys, values]
|
|
589
902
|
end
|
|
590
903
|
|
|
904
|
+
# The `[K, V]` a `**splat` entry adds to the literal: what the analysis can read of the hash it copies, each
|
|
905
|
+
# side joined with `Dynamic[top]`. Ruby inserts every pair of that hash, so leaving the entry out typed
|
|
906
|
+
# `o = { a: :z }; { **o, b: :y }` as `Hash[:b, :y]` and folded `h[:a] == :z` on correct code.
|
|
907
|
+
#
|
|
908
|
+
# The `Dynamic[top]` arm is there even when the copy is read exactly. The literal builds a new hash that no
|
|
909
|
+
# declaration describes, while `MutationRejoin` regrows a `Hash[K, V]` after `[]=` / `merge!` only when it
|
|
910
|
+
# already carries a gradual arm, because it reads a precise one as a declared claim. Without the arm
|
|
911
|
+
# `h = { **o }; h[:b] = 2; h[:b] == 2` would fold always-falsey and `h[:e] = "s"; h[:e].upcase` would fire
|
|
912
|
+
# `call.undefined-method`, where the `Hash` a splat-only literal used to type as had stayed quiet. The same
|
|
913
|
+
# arm stands in for what the analysis cannot read: an anonymous `**`, an untyped value, a nominal other than
|
|
914
|
+
# `Hash`, an open shape's unlisted entries, and a hash filled through an alias the engine does not track,
|
|
915
|
+
# which still reads `{}`.
|
|
916
|
+
def hash_splat_pair(entry)
|
|
917
|
+
untyped = Type::Combinator.untyped
|
|
918
|
+
copied = entry.value && splatted_hash_pair(type_of(entry.value))
|
|
919
|
+
return [untyped, untyped] if copied.nil?
|
|
920
|
+
|
|
921
|
+
copied.map { |side| Type::Combinator.union(side, untyped) }
|
|
922
|
+
end
|
|
923
|
+
|
|
924
|
+
# The `[K, V]` the analysis can read of a splatted value, or nil when it reads nothing: a shape's keys and
|
|
925
|
+
# values, a `Hash[K, V]`'s type arguments (through a difference over one, `non-empty-hash[K, V]`), and the
|
|
926
|
+
# join over a union's members. `nil` reads as nothing because `**nil` splats nothing (Ruby 3.4+; earlier
|
|
927
|
+
# Rubies raise). A `Hash` subclass is not read: its own type arguments, if any, are not `Hash`'s `K` and `V`.
|
|
928
|
+
def splatted_hash_pair(type)
|
|
929
|
+
case type
|
|
930
|
+
when Type::HashShape then splatted_shape_pair(type)
|
|
931
|
+
when Type::Nominal then type.class_name == "Hash" && type.type_args.size == 2 ? type.type_args : nil
|
|
932
|
+
when Type::Difference then splatted_hash_pair(type.base)
|
|
933
|
+
when Type::Union then joined_hash_pair(type.members.filter_map { |member| splatted_hash_pair(member) })
|
|
934
|
+
end
|
|
935
|
+
end
|
|
936
|
+
|
|
937
|
+
def splatted_shape_pair(shape)
|
|
938
|
+
return nil if shape.pairs.empty?
|
|
939
|
+
|
|
940
|
+
[Type::Combinator.union(*shape.pairs.keys.map { |k| Type::Combinator.constant_of(k) }),
|
|
941
|
+
Type::Combinator.union(*shape.pairs.values)]
|
|
942
|
+
end
|
|
943
|
+
|
|
944
|
+
def joined_hash_pair(pairs)
|
|
945
|
+
return nil if pairs.empty?
|
|
946
|
+
|
|
947
|
+
[Type::Combinator.union(*pairs.map(&:first)), Type::Combinator.union(*pairs.map(&:last))]
|
|
948
|
+
end
|
|
949
|
+
|
|
591
950
|
# An interpolated string `"#{a}b#{c}"` is `literal-string` when every part contributes literal-bearing
|
|
592
951
|
# material: plain text segments are literal by construction, embedded expressions count when their type
|
|
593
952
|
# is itself literal-string-compatible (a `Constant<String>`, the `literal-string` carrier, an
|
|
@@ -642,132 +1001,29 @@ module Rigor
|
|
|
642
1001
|
Type::Combinator.constant_of(nil)
|
|
643
1002
|
end
|
|
644
1003
|
|
|
645
|
-
# `if
|
|
646
|
-
#
|
|
647
|
-
#
|
|
648
|
-
#
|
|
649
|
-
#
|
|
650
|
-
#
|
|
651
|
-
#
|
|
652
|
-
#
|
|
653
|
-
#
|
|
654
|
-
def
|
|
655
|
-
|
|
656
|
-
else_type = if_else_type(node.subsequent)
|
|
657
|
-
elide_or_union(node.predicate, then_type, else_type)
|
|
658
|
-
end
|
|
659
|
-
|
|
660
|
-
# `unless c; t; else; e; end`. Prism uses `else_clause` here (no `elsif` chain). Branch-elision logic
|
|
661
|
-
# mirrors `type_of_if`, inverted: a truthy predicate selects the else branch.
|
|
662
|
-
def type_of_unless(node)
|
|
663
|
-
then_type = statements_or_nil(node.statements)
|
|
664
|
-
else_type = if_else_type(node.else_clause)
|
|
665
|
-
elide_or_union(node.predicate, else_type, then_type)
|
|
666
|
-
end
|
|
667
|
-
|
|
668
|
-
# Issue #286 — the effective optimistic-nil-free cause of an expression. {OptimisticOrigin.resolve} owns
|
|
669
|
-
# the judgment, shared verbatim with `StatementEvaluator#optimistic_origin_for` and the
|
|
670
|
-
# `flow.always-truthy-condition` collector.
|
|
671
|
-
def optimistic_origin_for(node)
|
|
672
|
-
OptimisticOrigin.resolve(node, scope)
|
|
673
|
-
end
|
|
674
|
-
|
|
675
|
-
def if_else_type(subsequent)
|
|
676
|
-
return Type::Combinator.constant_of(nil) if subsequent.nil?
|
|
677
|
-
|
|
678
|
-
type_of(subsequent)
|
|
679
|
-
end
|
|
680
|
-
|
|
681
|
-
# Routes the predicate's typed value through branch elision. `live_when_truthy` and `live_when_falsey`
|
|
682
|
-
# are the branch types selected by the predicate's polarity; the names match `IfNode` semantics
|
|
683
|
-
# directly and invert at the `type_of_unless` call site.
|
|
684
|
-
def elide_or_union(predicate, live_when_truthy, live_when_falsey)
|
|
685
|
-
case constant_predicate_polarity(predicate)
|
|
686
|
-
when :truthy then live_when_truthy
|
|
687
|
-
when :falsey then live_when_falsey
|
|
688
|
-
else Type::Combinator.union(live_when_truthy, live_when_falsey)
|
|
689
|
-
end
|
|
690
|
-
end
|
|
691
|
-
|
|
692
|
-
# Returns `:truthy`, `:falsey`, or `nil` for an arbitrary predicate expression under three-valued logic.
|
|
693
|
-
# {Narrowing.predicate_certainty} owns the judgment (the same one `StatementEvaluator#live_branch_for_if`
|
|
694
|
-
# reads on the scope side): `Nominal[Integer]` (always truthy in Ruby), `Constant[nil]`, and
|
|
695
|
-
# `Constant[false]` fold one branch; `Union[true, false]`, `Dynamic[T]`, and `Top` keep both branches live.
|
|
696
|
-
def constant_predicate_polarity(predicate)
|
|
697
|
-
return nil if predicate.nil?
|
|
698
|
-
|
|
699
|
-
# ADR-47 WD5 — a decidable version guard (#627) answers first, exactly as it does on the scope side
|
|
700
|
-
# in `StatementEvaluator#branch_certainty`. Both readers ask the same pure function of the AST, so
|
|
701
|
-
# the expression form (`RUBY_VERSION >= "3.1" ? a : b`) and the statement form cannot disagree about
|
|
702
|
-
# which arm survives. The verdict rests on literals, so the ADR-101 optimistic-carrier decline below
|
|
703
|
-
# — which guards an RBS-derived judgment — does not apply to it.
|
|
704
|
-
guard = VersionGuard.verdict(predicate)
|
|
705
|
-
return guard if guard
|
|
706
|
-
# ADR-101 — decline on an optimistically nil-free carrier; see
|
|
707
|
-
# `StatementEvaluator#optimistic_carrier?` for why the gate is here and not in `Narrowing`.
|
|
708
|
-
return nil unless optimistic_origin_for(predicate).nil?
|
|
709
|
-
|
|
710
|
-
Narrowing.predicate_certainty(type_of(predicate))
|
|
1004
|
+
# `if` / `unless` in value position — the ternary `c ? a : b`, a modifier `(a unless c)`, or a
|
|
1005
|
+
# conditional written as an argument, a receiver, or a literal element. Issue #1003: this handler used
|
|
1006
|
+
# to type both arms in the receiver scope and union them, so the predicate's narrowing reached a
|
|
1007
|
+
# statement-position conditional (through `StatementEvaluator#eval_if`) and never a value-position
|
|
1008
|
+
# one: `f.finite? ? f : 0.0` read `0.0 | Float` while the `if` statement bound `0.0 | finite-float`.
|
|
1009
|
+
# It delegates to the statement evaluator instead of re-deriving the edges, so the two positions share
|
|
1010
|
+
# one narrowing application, one branch elision (ADR-47 WD5 version guards, the ADR-101
|
|
1011
|
+
# optimistic-carrier decline) and one terminating-arm rule, and cannot drift apart again. Only the
|
|
1012
|
+
# value is read; the post-scope belongs to whichever walk owns the statement.
|
|
1013
|
+
def type_of_conditional(node)
|
|
1014
|
+
scope.evaluate(node, tracer: tracer).first
|
|
711
1015
|
end
|
|
712
1016
|
|
|
713
1017
|
def type_of_else(node)
|
|
714
1018
|
statements_or_nil(node.statements)
|
|
715
1019
|
end
|
|
716
1020
|
|
|
717
|
-
# `a && b`
|
|
718
|
-
#
|
|
719
|
-
#
|
|
720
|
-
#
|
|
721
|
-
# through, so the result is one operand's type instead of a union. Otherwise the union-of-both-operands
|
|
722
|
-
# fallback is preserved.
|
|
1021
|
+
# `a && b` / `a || b` in value position. Issue #1016: this handler used to type both operands in the
|
|
1022
|
+
# receiver scope, so `x.finite? && x` read `Float | false` while the statement form bound
|
|
1023
|
+
# `false | finite-float`. Like `type_of_conditional`, it delegates to the statement evaluator, which owns
|
|
1024
|
+
# the RHS narrowing, the constant short-circuit and its issue #313 optimistic-carrier decline.
|
|
723
1025
|
def type_of_and_or(node)
|
|
724
|
-
|
|
725
|
-
polarity = left_operand_polarity(node.left, left_type)
|
|
726
|
-
return short_circuit_for(node, left_type, polarity) if polarity
|
|
727
|
-
|
|
728
|
-
# The left operand only flows through on the edge that short-circuits: `a || b` yields `a` solely
|
|
729
|
-
# when `a` is truthy, so its falsey constituents (`nil` / `false`) can never be the value of the
|
|
730
|
-
# OrNode (they hand off to `b`); `a && b` yields `a` solely when `a` is falsey. Narrow the surviving
|
|
731
|
-
# left edge before the union so `s || full` (with `s : String?`) types `String | <full>` rather than
|
|
732
|
-
# re-admitting the stripped `nil`. Mirrors `StatementEvaluator#eval_and_or`'s `skipped_type`.
|
|
733
|
-
surviving_left =
|
|
734
|
-
if node.is_a?(Prism::AndNode)
|
|
735
|
-
Narrowing.narrow_falsey(left_type)
|
|
736
|
-
else
|
|
737
|
-
Narrowing.narrow_truthy(left_type)
|
|
738
|
-
end
|
|
739
|
-
Type::Combinator.union(surviving_left, type_of(node.right))
|
|
740
|
-
end
|
|
741
|
-
|
|
742
|
-
def short_circuit_for(node, left_type, polarity)
|
|
743
|
-
and_node = node.is_a?(Prism::AndNode)
|
|
744
|
-
if polarity == :truthy
|
|
745
|
-
and_node ? type_of(node.right) : left_type
|
|
746
|
-
else
|
|
747
|
-
and_node ? left_type : type_of(node.right)
|
|
748
|
-
end
|
|
749
|
-
end
|
|
750
|
-
|
|
751
|
-
# Issue #313 — the node-aware wrapper the `&&` / `||` short-circuit reads. The spec's exclusion binds
|
|
752
|
-
# this gate as much as it binds `flow.always-truthy-condition`, and a `Constant`-only gate is not by
|
|
753
|
-
# itself enough to honour it: a literal hash whose values share one type reads as a lone `Constant`
|
|
754
|
-
# (`UNIFORM[key]` → `Constant[1]`), so the gate would judge the left operand of `UNIFORM[key] || key`
|
|
755
|
-
# provably truthy and discard the author's fallback — the counter-example the spec names verbatim.
|
|
756
|
-
# Declining returns the union of both operands, which is what `StatementEvaluator#eval_and_or` produces
|
|
757
|
-
# anyway, so the two `&&` / `||` typers stay in agreement.
|
|
758
|
-
def left_operand_polarity(left_node, left_type)
|
|
759
|
-
return nil unless optimistic_origin_for(left_node).nil?
|
|
760
|
-
|
|
761
|
-
constant_value_polarity(left_type)
|
|
762
|
-
end
|
|
763
|
-
|
|
764
|
-
# Returns `:truthy` / `:falsey` for a `Type::Constant`, nil otherwise. Mirrors
|
|
765
|
-
# `constant_predicate_polarity` but operates on a typed value (already-type-of'd) rather than a Prism
|
|
766
|
-
# node, so the same predicate analysis can be reused in both contexts.
|
|
767
|
-
def constant_value_polarity(type)
|
|
768
|
-
return nil unless type.is_a?(Type::Constant)
|
|
769
|
-
|
|
770
|
-
type.value ? :truthy : :falsey
|
|
1026
|
+
scope.evaluate(node, tracer: tracer).first
|
|
771
1027
|
end
|
|
772
1028
|
|
|
773
1029
|
# Three-valued evaluation of `case predicate when pattern` dispatch. For each `when` clause we ask:
|
|
@@ -785,29 +1041,41 @@ module Rigor
|
|
|
785
1041
|
# The `case ... in` pattern-matching form (`CaseMatchNode`) and the predicate-less form (`case; when
|
|
786
1042
|
# c1; ...`) bypass the `===` analysis: pattern matching has richer semantics, and a predicate-less
|
|
787
1043
|
# `case` reduces to a `if c1; ...; elsif c2` chain that statement-level narrowing already handles.
|
|
1044
|
+
# Issue #1429 — each arm is typed under the subject's clause narrowing (`Narrowing.case_when_scopes`), the
|
|
1045
|
+
# scope the statement evaluator runs the arm in: `when Symbol then n` on `n: Integer | Symbol` answers
|
|
1046
|
+
# `Symbol`, and the `else` arm reads the subject every earlier clause has ruled out.
|
|
788
1047
|
def type_of_case(node)
|
|
789
1048
|
return type_of_case_simple_union(node) if node.is_a?(Prism::CaseMatchNode) || node.predicate.nil?
|
|
790
1049
|
|
|
791
1050
|
subject_type = type_of(node.predicate)
|
|
792
1051
|
candidates = []
|
|
793
1052
|
reached_yes = false
|
|
1053
|
+
clause_scope = scope
|
|
794
1054
|
|
|
795
1055
|
node.conditions.each do |when_node|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
1056
|
+
conditions = when_node.respond_to?(:conditions) ? when_node.conditions : []
|
|
1057
|
+
body_scope, next_scope = Narrowing.case_when_scopes(node.predicate, conditions, clause_scope)
|
|
1058
|
+
certainty = case_when_branch_certainty(subject_type, when_node)
|
|
1059
|
+
# :no — drop the branch
|
|
1060
|
+
candidates << case_arm_type(when_node, body_scope) unless certainty == :no
|
|
1061
|
+
if certainty == :yes
|
|
799
1062
|
reached_yes = true
|
|
800
1063
|
break
|
|
801
|
-
when :maybe
|
|
802
|
-
candidates << type_of(when_node)
|
|
803
|
-
# :no — drop the branch
|
|
804
1064
|
end
|
|
1065
|
+
clause_scope = next_scope
|
|
805
1066
|
end
|
|
806
1067
|
|
|
807
|
-
candidates <<
|
|
1068
|
+
candidates << case_arm_type(node.else_clause, clause_scope) unless reached_yes
|
|
808
1069
|
Type::Combinator.union(*candidates)
|
|
809
1070
|
end
|
|
810
1071
|
|
|
1072
|
+
# The value of one `case` arm (`nil` for an absent `else`), typed under `arm_scope`.
|
|
1073
|
+
def case_arm_type(arm, arm_scope)
|
|
1074
|
+
return Type::Combinator.constant_of(nil) if arm.nil?
|
|
1075
|
+
|
|
1076
|
+
arm_scope.equal?(scope) ? type_of(arm) : arm_scope.type_of(arm, tracer: tracer)
|
|
1077
|
+
end
|
|
1078
|
+
|
|
811
1079
|
def type_of_case_simple_union(node)
|
|
812
1080
|
branch_types = node.conditions.map { |branch| type_of(branch) }
|
|
813
1081
|
Type::Combinator.union(*branch_types, type_of_case_else(node))
|
|
@@ -843,9 +1111,21 @@ module Rigor
|
|
|
843
1111
|
#
|
|
844
1112
|
# Other pattern shapes (Range, Regexp, custom `===`) stay `:maybe` — the existing union fallback
|
|
845
1113
|
# handles them.
|
|
1114
|
+
#
|
|
1115
|
+
# The class reference is resolved through {Narrowing.lexical_class_name}, the walk the narrowing
|
|
1116
|
+
# side of the same `case` already uses (#655). Matching on the as-written spelling instead made a
|
|
1117
|
+
# SHADOWED name confidently wrong rather than merely unresolved: `when Random` inside
|
|
1118
|
+
# `Bar::Nested` names `Bar::Nested::Random`, the environment answered the core `::Random` the
|
|
1119
|
+
# spelling matches at top level, and a subject that is a core `Random` then made the arm certain
|
|
1120
|
+
# — dropping the `else` Ruby actually takes and typing the expression as a branch that never
|
|
1121
|
+
# runs. The documented argument that an unresolvable pattern can only lose certainty holds for an
|
|
1122
|
+
# unresolvable name and not for a shadowed one.
|
|
846
1123
|
def case_when_pattern_certainty(subject_type, pattern_node)
|
|
847
|
-
class_name =
|
|
848
|
-
|
|
1124
|
+
class_name = Narrowing.lexical_class_name(pattern_node, scope)
|
|
1125
|
+
if class_name
|
|
1126
|
+
return Narrowing.class_pattern_certainty(subject_type, class_name, environment: scope.environment,
|
|
1127
|
+
scope: scope)
|
|
1128
|
+
end
|
|
849
1129
|
|
|
850
1130
|
literal = literal_pattern_value(pattern_node)
|
|
851
1131
|
return Narrowing.value_pattern_certainty(subject_type, literal[:value]) if literal
|
|
@@ -891,24 +1171,46 @@ module Rigor
|
|
|
891
1171
|
Type::Combinator.union(primary_type, *rescue_types)
|
|
892
1172
|
end
|
|
893
1173
|
|
|
1174
|
+
# Issue #1360 — the evaluator binds `$!` in a rescue clause it enters; a clause typed here, such as one in a
|
|
1175
|
+
# `begin` passed as an argument, reads `$!`, `$@` and `$?` unbound rather than an enclosing clause's.
|
|
894
1176
|
def rescue_chain_types(rescue_node)
|
|
1177
|
+
arm_typer = typer_under(rescue_arm_scope)
|
|
895
1178
|
types = []
|
|
896
1179
|
current = rescue_node
|
|
897
1180
|
while current
|
|
898
|
-
types << statements_or_nil
|
|
1181
|
+
types << arm_typer.send(:statements_or_nil, current.statements)
|
|
899
1182
|
current = current.subsequent
|
|
900
1183
|
end
|
|
901
1184
|
types
|
|
902
1185
|
end
|
|
903
1186
|
|
|
904
1187
|
def type_of_rescue(node)
|
|
905
|
-
statements_or_nil
|
|
1188
|
+
typer_under(rescue_arm_scope).send(:statements_or_nil, node.statements)
|
|
906
1189
|
end
|
|
907
1190
|
|
|
1191
|
+
def rescue_arm_scope = scope.forget_error_info.forget_last_status
|
|
1192
|
+
|
|
908
1193
|
# `expr rescue fallback` is RescueModifierNode in Prism. The result is `expr`'s type when no exception
|
|
909
|
-
# is raised and `fallback`'s type otherwise; both paths are reachable, so the result is their union.
|
|
1194
|
+
# is raised and `fallback`'s type otherwise; both paths are reachable, so the result is their union. Issue #1360
|
|
1195
|
+
# — `fallback` runs with `$!` and `$@` bound to the `StandardError` it rescued ({ErrorInfo.modifier_entry}).
|
|
910
1196
|
def type_of_rescue_modifier(node)
|
|
911
|
-
|
|
1197
|
+
fallback = node.rescue_expression
|
|
1198
|
+
fallback_type =
|
|
1199
|
+
if ErrorInfo.read_in?(fallback)
|
|
1200
|
+
typer_under(ErrorInfo.modifier_entry(scope, fallback)).type_of(fallback)
|
|
1201
|
+
else
|
|
1202
|
+
type_of(fallback)
|
|
1203
|
+
end
|
|
1204
|
+
Type::Combinator.union(type_of(node.expression), fallback_type)
|
|
1205
|
+
end
|
|
1206
|
+
|
|
1207
|
+
# A typer that shares this one's tracer and operand types but reads `other_scope`.
|
|
1208
|
+
def typer_under(other_scope)
|
|
1209
|
+
return self if other_scope.equal?(scope)
|
|
1210
|
+
|
|
1211
|
+
ExpressionTyper.new(
|
|
1212
|
+
scope: other_scope, tracer: tracer, operand_types: @operand_types, typing_node: @typing_node
|
|
1213
|
+
)
|
|
912
1214
|
end
|
|
913
1215
|
|
|
914
1216
|
def type_of_ensure(node)
|
|
@@ -963,6 +1265,8 @@ module Rigor
|
|
|
963
1265
|
Type::Combinator.nominal_of(value.class.name)
|
|
964
1266
|
when Type::IntegerRange
|
|
965
1267
|
Type::Combinator.nominal_of("Integer")
|
|
1268
|
+
when Type::FloatRange
|
|
1269
|
+
Type::Combinator.nominal_of("Float")
|
|
966
1270
|
else
|
|
967
1271
|
type
|
|
968
1272
|
end
|
|
@@ -1177,12 +1481,18 @@ module Rigor
|
|
|
1177
1481
|
# and fired `undefined method 'upcase' for nil` on correct code. The candidate is still looked up
|
|
1178
1482
|
# first — that lookup is a hash probe and owns the ADR-46 cross-file dependency edge — and
|
|
1179
1483
|
# {#self_type_answers?} then vetoes the bind for a name the enclosing class answers itself.
|
|
1180
|
-
|
|
1484
|
+
#
|
|
1485
|
+
# Issue #963 item 2 — {#plugin_member_answers?} is the second veto, for the members no source the
|
|
1486
|
+
# engine walks can enumerate: a plugin's `dynamic_return` answer and the ADR-16 synthetic-method
|
|
1487
|
+
# index. They are members of `self` exactly as an `attr_reader` is, so a top-level `def` must not
|
|
1488
|
+
# bind ahead of them either.
|
|
1489
|
+
def try_local_def_dispatch(node, receiver, arg_types, block_type = nil)
|
|
1181
1490
|
local_def = node.receiver.nil? ? scope.bindable_top_level_def_for(node.name) : nil
|
|
1182
1491
|
return nil unless local_def
|
|
1183
1492
|
return nil if self_type_answers?(node.name)
|
|
1493
|
+
return nil if plugin_member_answers?(node, receiver)
|
|
1184
1494
|
|
|
1185
|
-
local_inference = infer_top_level_user_method(local_def, receiver, arg_types)
|
|
1495
|
+
local_inference = infer_top_level_user_method(local_def, receiver, arg_types, block_type)
|
|
1186
1496
|
return local_inference if local_inference
|
|
1187
1497
|
|
|
1188
1498
|
# The local def matches by name but the inference was disqualified — the parameter shape is too
|
|
@@ -1194,6 +1504,82 @@ module Rigor
|
|
|
1194
1504
|
dynamic_top
|
|
1195
1505
|
end
|
|
1196
1506
|
|
|
1507
|
+
# ADR-110 WD1 / #856 — the receiver's own `def` outranks a declaration that only an ANCESTOR carries.
|
|
1508
|
+
#
|
|
1509
|
+
# RBS resolves through the fully built method table, so `RbsDispatch` answers a subclass call with a
|
|
1510
|
+
# signature written about the base. redmine's `FieldFormat::Base#target_class` honestly declares
|
|
1511
|
+
# `-> nil`; `RecordList` overrides it with a real lookup, and every call on a `RecordList` receiver
|
|
1512
|
+
# typed as `nil` and fired `undefined method ... for nil` on the subclass's own working code (#744).
|
|
1513
|
+
# A signature is authoritative for a receiver only where someone wrote it about that receiver;
|
|
1514
|
+
# reaching it through an ancestor is a lookup convenience, not an act of authorship.
|
|
1515
|
+
#
|
|
1516
|
+
# This is {#try_local_def_dispatch}'s shape one level up — there the enclosing class shadows a
|
|
1517
|
+
# top-level `def`, here the receiver's class shadows an ancestor's declaration — and it answers the
|
|
1518
|
+
# same way: re-type the override's body, and when that is not possible answer `Dynamic[Top]` rather
|
|
1519
|
+
# than let the inherited declaration speak for a method it does not describe.
|
|
1520
|
+
#
|
|
1521
|
+
# Three conditions, and the third is the blast radius. Clause A of the ADR (an own source `def`, no
|
|
1522
|
+
# own declaration) does NOT by itself stay clear of [ADR-43]: `class Foo; def each; end` inheriting
|
|
1523
|
+
# `Enumerable#each` satisfies it, and disqualifying bundled declarations that way is the blanket fix
|
|
1524
|
+
# ADR-43 rejected — the same reason {#instance_self_answers?}'s RBS arm is own-class only. So the
|
|
1525
|
+
# ancestor whose declaration is being disqualified must itself be PROJECT-declared: a project sidecar
|
|
1526
|
+
# describes the source under analysis, while a bundled signature describes a class the project does
|
|
1527
|
+
# not own, where a project `def` is a monkey-patch and [ADR-17] owns the question.
|
|
1528
|
+
# `project_declared_class?` fail-softs to false, so an unattributable environment changes nothing.
|
|
1529
|
+
def try_overriding_def_dispatch(node, receiver, arg_types, method_name: node.name)
|
|
1530
|
+
return nil unless overriding_own_def?(receiver, method_name)
|
|
1531
|
+
|
|
1532
|
+
try_user_method_inference(receiver, node, arg_types, method_name: method_name) || dynamic_top
|
|
1533
|
+
end
|
|
1534
|
+
|
|
1535
|
+
# The three conditions above: an own source `def` of `method_name` on the receiver's class, which has no
|
|
1536
|
+
# declaration of its own but inherits one a project-declared ancestor wrote.
|
|
1537
|
+
def overriding_own_def?(receiver, method_name)
|
|
1538
|
+
return false unless user_inference_receiver?(receiver)
|
|
1539
|
+
|
|
1540
|
+
class_name = receiver.class_name
|
|
1541
|
+
return false if class_name.nil?
|
|
1542
|
+
# `Scope#user_def_for`, not `discovered_method?`: the cross-file table deliberately withholds a
|
|
1543
|
+
# plain instance `def` under the ADR-17 monkey-patch contract, and this gate must see one.
|
|
1544
|
+
return false if scope.user_def_for(class_name, method_name).nil?
|
|
1545
|
+
|
|
1546
|
+
definition = safe_rbs_method_definition(class_name, method_name, :instance)
|
|
1547
|
+
return false if definition.nil?
|
|
1548
|
+
return false if rbs_declared_on_class?(definition, class_name)
|
|
1549
|
+
|
|
1550
|
+
project_declared_owner?(definition)
|
|
1551
|
+
end
|
|
1552
|
+
|
|
1553
|
+
# {#implicit_index_read_type}'s dispatcher tier, with the write node as the call context.
|
|
1554
|
+
def index_read_dispatch(node, receiver, arg_types)
|
|
1555
|
+
MethodDispatcher.dispatch(
|
|
1556
|
+
receiver_type: receiver, method_name: :[], arg_types: arg_types,
|
|
1557
|
+
environment: scope.environment, call_node: node, scope: scope
|
|
1558
|
+
)
|
|
1559
|
+
end
|
|
1560
|
+
|
|
1561
|
+
# A splat index (`c[*keys] += v`) leaves the read's arity to the splat's expansion, and the one untyped
|
|
1562
|
+
# argument standing in for it would bind positionally to a body written for several: `def [](*keys) =
|
|
1563
|
+
# keys.size` folded the read of `r[*[0, 1]] += 1` to `1` and drew an always-falsey `n == 3` on a program
|
|
1564
|
+
# that takes that branch. So the body tiers decline under a splat, as `try_literal_send` does, and only
|
|
1565
|
+
# the signature-driven dispatch answers. An own `def` overriding an inherited declaration still outranks
|
|
1566
|
+
# that declaration; with its body unreadable, it answers `Dynamic[top]`.
|
|
1567
|
+
def splat_index_read_type(node, receiver, arg_types)
|
|
1568
|
+
return dynamic_top if overriding_own_def?(receiver, :[])
|
|
1569
|
+
|
|
1570
|
+
index_read_dispatch(node, receiver, arg_types)
|
|
1571
|
+
end
|
|
1572
|
+
|
|
1573
|
+
# Whether the class an inherited declaration was written about is one the project declares itself.
|
|
1574
|
+
def project_declared_owner?(definition)
|
|
1575
|
+
defined_in = definition.defined_in
|
|
1576
|
+
return false if defined_in.nil?
|
|
1577
|
+
|
|
1578
|
+
Rigor::Reflection.project_declared_class?(defined_in.to_s, scope: scope)
|
|
1579
|
+
rescue StandardError
|
|
1580
|
+
false
|
|
1581
|
+
end
|
|
1582
|
+
|
|
1197
1583
|
# Issue #618 — whether the call's enclosing `self` already answers `method_name`. Only a `self` whose
|
|
1198
1584
|
# class is KNOWN participates: at genuine top level, and inside a block whose `self` is unmodelled,
|
|
1199
1585
|
# `scope.self_type` is nil, the predicate is false, and the historical top-level binding stands — that
|
|
@@ -1212,30 +1598,71 @@ module Rigor
|
|
|
1212
1598
|
end
|
|
1213
1599
|
end
|
|
1214
1600
|
|
|
1215
|
-
#
|
|
1216
|
-
#
|
|
1217
|
-
#
|
|
1218
|
-
#
|
|
1601
|
+
# Issue #963 item 2 — the plugin arm of the veto: whether a plugin answers `node.name` on the call's
|
|
1602
|
+
# own `self`. Asked of `MethodDispatcher.plugin_member_answers?`, which puts the question to the same
|
|
1603
|
+
# two tiers dispatch would consult (the gated `dynamic_return` walk and the ADR-16 synthetic-method
|
|
1604
|
+
# index), so the veto carries no plugin knowledge of its own.
|
|
1605
|
+
#
|
|
1606
|
+
# The confidence gate is #618's, unchanged and re-stated here rather than inherited: only a `self`
|
|
1607
|
+
# whose class is KNOWN participates. At genuine top level, and inside a block whose `self` is
|
|
1608
|
+
# unmodelled, `scope.self_type` is nil, `receiver` is the synthetic `Object` / `Dynamic[Top]` stand-in
|
|
1609
|
+
# that {#call_receiver_type_for} substitutes, and asking a plugin about THAT receiver would be asking
|
|
1610
|
+
# about a `self` the engine has not modelled — #316's / #319's territory, which this stays out of.
|
|
1611
|
+
def plugin_member_answers?(node, receiver)
|
|
1612
|
+
return false if scope.self_type.nil?
|
|
1613
|
+
|
|
1614
|
+
MethodDispatcher.plugin_member_answers?(
|
|
1615
|
+
call_node: node, scope: scope, receiver_type: receiver, method_name: node.name
|
|
1616
|
+
)
|
|
1617
|
+
end
|
|
1618
|
+
|
|
1619
|
+
# The instance side of {#self_type_answers?}: the discovered methods (`def`, `attr_*`,
|
|
1620
|
+
# `define_method`, `alias`) of the class or any project ancestor, its `Struct.new` / `Data.define`
|
|
1621
|
+
# member accessors, a `def` reached through its project ancestors (superclass chain and included
|
|
1622
|
+
# modules), and an RBS method declared on an owner that PRECEDES `::Object` in the MRO.
|
|
1219
1623
|
#
|
|
1220
|
-
#
|
|
1221
|
-
#
|
|
1222
|
-
#
|
|
1223
|
-
#
|
|
1624
|
+
# Issue #633 — the discovery arm walks ancestors because inheritance does not distinguish how a
|
|
1625
|
+
# method got there: a superclass `attr_accessor :stext` and an included module's `attr_reader` bind
|
|
1626
|
+
# ahead of a top-level `def stext` exactly as an inherited `def` does, and only the latter had a walk.
|
|
1627
|
+
#
|
|
1628
|
+
# The RBS arm's cut-off is `::Object`, not the own class. A top-level `def` IS a private `Object`
|
|
1629
|
+
# instance method, the last link of every MRO, so any declared owner that comes before `::Object`
|
|
1630
|
+
# wins at runtime — `Exception#message` for a `StandardError` subclass, `Array#first`,
|
|
1631
|
+
# `Comparable#clamp`. What the cut-off keeps out is the tier v0.0.3 A exists for and #316 / #319
|
|
1632
|
+
# refined: a name owned by `Object` or `Kernel` themselves (`inspect`, `format`) is at or after the
|
|
1633
|
+
# top-level `def`'s own rung, so the historical binding still stands there.
|
|
1224
1634
|
def instance_self_answers?(class_name, method_name)
|
|
1225
1635
|
return false if class_name.nil?
|
|
1226
|
-
return true if scope.
|
|
1636
|
+
return true if scope.discovered_method_through_ancestors?(class_name, method_name, :instance)
|
|
1227
1637
|
return true if meta_member?(class_name, method_name)
|
|
1228
1638
|
return true if resolve_user_def_through_ancestors(class_name, method_name)
|
|
1229
1639
|
|
|
1230
|
-
|
|
1640
|
+
rbs_ancestor_answers?(class_name, method_name)
|
|
1641
|
+
end
|
|
1642
|
+
|
|
1643
|
+
# The RBS arm of {#instance_self_answers?}. A project class is usually absent from the RBS
|
|
1644
|
+
# environment, so the declaration that decides the question is written about an ancestor the project
|
|
1645
|
+
# does not declare (`StandardError`, `Array`, `Comparable`) — each is asked on its own terms, and
|
|
1646
|
+
# each of them is itself before `::Object` in the reader's MRO by construction.
|
|
1647
|
+
#
|
|
1648
|
+
# Issue #527 slice 0 — the walk itself lives in {ExternalAncestorResolution}, which reports WHICH
|
|
1649
|
+
# declaration answered because the dispatch side needs to dispatch there. This tier only needs to
|
|
1650
|
+
# know that one did, so it reads the presence of an answer. Dependency recording stays ON: this
|
|
1651
|
+
# arm genuinely read the ancestor's declaration to decide the binding.
|
|
1652
|
+
def rbs_ancestor_answers?(class_name, method_name)
|
|
1653
|
+
!ExternalAncestorResolution.resolve(class_name, method_name, :instance, scope: scope).nil?
|
|
1231
1654
|
end
|
|
1232
1655
|
|
|
1233
1656
|
# The singleton side: a class-body `self` is `Singleton[Foo]`, where an implicit-self call reaches
|
|
1234
1657
|
# `Foo`'s own class methods before `Object`'s private top-level `def`.
|
|
1658
|
+
#
|
|
1659
|
+
# Issue #633 — both discovery arms follow the superclass chain, because a subclass inherits its
|
|
1660
|
+
# parent's class methods; `Scope` narrows the singleton walk to superclasses for the same reason
|
|
1661
|
+
# (an `include`d module's `def self.x` is not callable on the includer).
|
|
1235
1662
|
def singleton_self_answers?(class_name, method_name)
|
|
1236
1663
|
return false if class_name.nil?
|
|
1237
|
-
return true if scope.
|
|
1238
|
-
return true unless scope.
|
|
1664
|
+
return true if scope.discovered_method_through_ancestors?(class_name, method_name, :singleton)
|
|
1665
|
+
return true unless scope.singleton_def_through_ancestors(class_name, method_name).first.nil?
|
|
1239
1666
|
|
|
1240
1667
|
rbs_declared_on_class?(safe_rbs_method_definition(class_name, method_name, :singleton), class_name)
|
|
1241
1668
|
end
|
|
@@ -1250,26 +1677,16 @@ module Rigor
|
|
|
1250
1677
|
!members.nil? && members.include?(method_name.to_sym)
|
|
1251
1678
|
end
|
|
1252
1679
|
|
|
1680
|
+
# Issue #527 slice 0 — the RBS lookup, its `rescue`, and the `::Object` MRO cut-off moved to
|
|
1681
|
+
# {ExternalAncestorResolution}, which the dispatch side reads too. These stay as this class's
|
|
1682
|
+
# spelling of them, because three other tiers here (`try_overriding_def_dispatch`,
|
|
1683
|
+
# {#singleton_self_answers?}) ask the own-class question without the ancestor walk.
|
|
1253
1684
|
def safe_rbs_method_definition(class_name, method_name, kind)
|
|
1254
|
-
|
|
1255
|
-
Rigor::Reflection.singleton_method_definition(class_name, method_name, scope: scope)
|
|
1256
|
-
else
|
|
1257
|
-
Rigor::Reflection.instance_method_definition(class_name, method_name, scope: scope)
|
|
1258
|
-
end
|
|
1259
|
-
rescue StandardError
|
|
1260
|
-
nil
|
|
1685
|
+
ExternalAncestorResolution.method_definition(class_name, method_name, kind, scope: scope)
|
|
1261
1686
|
end
|
|
1262
1687
|
|
|
1263
|
-
# True when the RBS declaration found for the name sits on `class_name` itself rather than on an
|
|
1264
|
-
# ancestor; mirrors `CheckRules#defined_on?` and `SigGen::Generator#declared_on_class_itself?`.
|
|
1265
1688
|
def rbs_declared_on_class?(definition, class_name)
|
|
1266
|
-
|
|
1267
|
-
return false unless definition.respond_to?(:defined_in)
|
|
1268
|
-
|
|
1269
|
-
defined_in = definition.defined_in
|
|
1270
|
-
return false if defined_in.nil?
|
|
1271
|
-
|
|
1272
|
-
defined_in.to_s.delete_prefix("::") == class_name.to_s.delete_prefix("::")
|
|
1689
|
+
ExternalAncestorResolution.declared_on_class?(definition, class_name)
|
|
1273
1690
|
end
|
|
1274
1691
|
|
|
1275
1692
|
# Issue #520 — Ruby defines the value of an attribute / index assignment (`x.attr = v`, `h[k] = v`)
|
|
@@ -1376,7 +1793,97 @@ module Rigor
|
|
|
1376
1793
|
# `receiver_override` substitutes the receiver type for the whole pipeline (folds, dispatch, the
|
|
1377
1794
|
# inference tiers) without re-reading the receiver node — the safe-navigation path (#518) dispatches
|
|
1378
1795
|
# on the nil-stripped fragment, and the optional-receiver retry (#519) re-runs the pipeline on it.
|
|
1796
|
+
# Issue #853: `break value` terminates the yielding CALL and is that call's value, so the answer every
|
|
1797
|
+
# tier below produces — an RBS return, a precision fold, an inter-procedural inference — describes only
|
|
1798
|
+
# the path where the block never breaks. The call's value is that result unioned with every `break` arm
|
|
1799
|
+
# the block can reach. Modelling it here rather than inside each fold is what keeps the folds honest:
|
|
1800
|
+
# `ops.all? { |o| break false unless o; true }` still folds the no-break path to `Constant[true]`, and
|
|
1801
|
+
# the union with the `false` arm makes the call `bool` — no `flow.always-truthy-condition` on a program
|
|
1802
|
+
# that really can answer false.
|
|
1803
|
+
#
|
|
1804
|
+
# Issue #1095: the callee's result is kept because an arbitrary callee may return without ever running
|
|
1805
|
+
# the block. A catalogued exactly-once yielder ({BlockCallTiming}) cannot, so when its block's normal
|
|
1806
|
+
# completion is unreachable the callee's result is too, and the call is its `break` arms alone — `bot`
|
|
1807
|
+
# when every path raises or returns instead.
|
|
1808
|
+
#
|
|
1809
|
+
# Issue #1107: `Kernel#loop` declares `-> bot` but returns normally, with `StopIteration#result`, when its
|
|
1810
|
+
# block raises `StopIteration`. Unless the body provably cannot ({BlockCallTiming.loop_may_complete?}),
|
|
1811
|
+
# that normal return is `untyped` — the result's declared type — so `loop { e.next }` is not `bot` and
|
|
1812
|
+
# `loop { x = e.next; break x if x }` keeps its arm beside it.
|
|
1379
1813
|
def call_dispatch_type_for(node, receiver_override: nil)
|
|
1814
|
+
result = loop_completion_type(node, call_result_type_for(node, receiver_override: receiver_override))
|
|
1815
|
+
arms = call_break_arm_types(node, receiver_override: receiver_override)
|
|
1816
|
+
if exactly_once_block_never_completes?(node, receiver_override)
|
|
1817
|
+
return arms.empty? ? Type::Combinator.bot : Type::Combinator.union(*arms)
|
|
1818
|
+
end
|
|
1819
|
+
|
|
1820
|
+
combined = arms.empty? ? result : Type::Combinator.union(result, *arms)
|
|
1821
|
+
widen_optimistic_predicate_constant(node, combined)
|
|
1822
|
+
end
|
|
1823
|
+
|
|
1824
|
+
# Issue #1172 — the nil-collapsing predicates (`nil?`, `!`, `x == nil`, …) answer a `Constant` that
|
|
1825
|
+
# encodes the receiver's nil-freeness. When that nil-freeness is only *optimistic* — an
|
|
1826
|
+
# `%a{implicitly-returns-nil}` read `RbsDispatch` deliberately reads past — the constant is a bet,
|
|
1827
|
+
# not a fact, and unlike the in-scope `OptimisticOrigin` mark a folded `Constant[false]` survives
|
|
1828
|
+
# into the enclosing method's return summary. A caller's `helper(...) ? a : b` then sees a
|
|
1829
|
+
# proof-shaped constant and reports `flow.always-truthy-condition` on a live guard (the mark cannot
|
|
1830
|
+
# follow a value across the method boundary, so {OptimisticOrigin.resolve} at the call site finds
|
|
1831
|
+
# nothing). Widen the predicate's constant answer to `bool` when the call derives a mark: the three
|
|
1832
|
+
# certainty consumers already decline on the mark inside this scope, and the widened return keeps
|
|
1833
|
+
# the same judgment from leaking out as a folded return type. The carrier's own type is unchanged —
|
|
1834
|
+
# `h[k]` still reads nil-free, per the spec's MUST NOT.
|
|
1835
|
+
def widen_optimistic_predicate_constant(node, type)
|
|
1836
|
+
return type unless type.is_a?(Type::Constant) && [true, false].include?(type.value)
|
|
1837
|
+
# The carrier read itself keeps its declared answer even when that is a literal `true` /
|
|
1838
|
+
# `false` — the mark says its nil-freeness is a bet, not that the value's class widened.
|
|
1839
|
+
return type if scope.optimistic_origins[node]
|
|
1840
|
+
return type if OptimisticOrigin.resolve(node, scope).nil?
|
|
1841
|
+
|
|
1842
|
+
# Widen only toward what a miss really answers: a safe-navigation call answers `nil`, and
|
|
1843
|
+
# `!recv&.empty?` answers `true` either way, so widening either would invent the other boolean.
|
|
1844
|
+
miss = OptimisticOrigin.miss_answer(node, scope)
|
|
1845
|
+
return type unless miss.equal?(OptimisticOrigin::UNKNOWN_MISS) || miss == !type.value
|
|
1846
|
+
|
|
1847
|
+
Type::Combinator.union(Type::Combinator.constant_of(true), Type::Combinator.constant_of(false))
|
|
1848
|
+
end
|
|
1849
|
+
|
|
1850
|
+
def loop_completion_type(node, result)
|
|
1851
|
+
return result unless result.is_a?(Type::Bot) && BlockCallTiming.loop_may_complete?(node)
|
|
1852
|
+
|
|
1853
|
+
scope.record_dynamic_origin(node, DynamicOrigin::EXPLICIT_UNTYPED)
|
|
1854
|
+
Type::Combinator.untyped
|
|
1855
|
+
end
|
|
1856
|
+
|
|
1857
|
+
# Whether `node` calls a catalogued exactly-once yielder ({BlockCallTiming}) with a literal block that can
|
|
1858
|
+
# never complete normally. Two proofs must BOTH hold. The syntactic one
|
|
1859
|
+
# ({BlockCallTiming.never_completes_normally?}) says every path ends in a jump or a non-returning Kernel
|
|
1860
|
+
# call. The block-return pass must also answer exactly `bot`: a reachable `next` joins its value instead
|
|
1861
|
+
# (#841), so `tap { next "s" }` completes and keeps the receiver, and a nil-bearing or `Dynamic` value or a
|
|
1862
|
+
# failed pass (`nil`) keeps the #853 union. The pass alone is not enough, because it also answers `bot`
|
|
1863
|
+
# for a body whose last call merely DECLARES `-> bot` — a project method's signature, or `loop` before
|
|
1864
|
+
# #1107 widened it ({#loop_completion_type}); `loop { e.next }` returns normally once `e` is drained, and
|
|
1865
|
+
# trusting it made correct code report an always-falsey condition.
|
|
1866
|
+
#
|
|
1867
|
+
# The pre-gates run cheapest-first because the block is re-typed here: the name, a `Prism::BlockNode` (a
|
|
1868
|
+
# `&blk` / `&:sym` block-pass carries no body to prove anything about), no arguments (none of the three
|
|
1869
|
+
# takes one), the syntactic walk, then the resolved-owner check.
|
|
1870
|
+
def exactly_once_block_never_completes?(node, receiver_override)
|
|
1871
|
+
return false unless BlockCallTiming.candidate_name?(node.name)
|
|
1872
|
+
|
|
1873
|
+
block_node = node.block
|
|
1874
|
+
return false unless block_node.is_a?(Prism::BlockNode) && block_node.body
|
|
1875
|
+
return false if node.arguments
|
|
1876
|
+
return false unless BlockCallTiming.never_completes_normally?(block_node.body, scope)
|
|
1877
|
+
|
|
1878
|
+
receiver = receiver_override || call_receiver_type_for(node)
|
|
1879
|
+
return false unless BlockCallTiming.exactly_once_call?(
|
|
1880
|
+
receiver_type: receiver, method_name: node.name, scope: scope
|
|
1881
|
+
)
|
|
1882
|
+
|
|
1883
|
+
block_return_type_for(node, receiver, []).is_a?(Type::Bot)
|
|
1884
|
+
end
|
|
1885
|
+
|
|
1886
|
+
def call_result_type_for(node, receiver_override: nil)
|
|
1380
1887
|
narrowed = indexed_narrowing_for(node)
|
|
1381
1888
|
return narrowed if narrowed
|
|
1382
1889
|
|
|
@@ -1392,7 +1899,7 @@ module Rigor
|
|
|
1392
1899
|
literal_send = try_literal_send(node, receiver)
|
|
1393
1900
|
return literal_send if literal_send
|
|
1394
1901
|
|
|
1395
|
-
local_def_result = try_local_def_dispatch(node, receiver, arg_types)
|
|
1902
|
+
local_def_result = try_local_def_dispatch(node, receiver, arg_types, block_type)
|
|
1396
1903
|
return local_def_result if local_def_result
|
|
1397
1904
|
|
|
1398
1905
|
# v0.0.6 phase 2 — per-element block fold for Tuple receivers. When `[a, b, c].map { |x| f(x) }` and
|
|
@@ -1403,6 +1910,9 @@ module Rigor
|
|
|
1403
1910
|
block_fold = try_receiver_block_folds(node, receiver, arg_types)
|
|
1404
1911
|
return block_fold if block_fold
|
|
1405
1912
|
|
|
1913
|
+
overriding_def = try_overriding_def_dispatch(node, receiver, arg_types)
|
|
1914
|
+
return overriding_def if overriding_def
|
|
1915
|
+
|
|
1406
1916
|
result = MethodDispatcher.dispatch(
|
|
1407
1917
|
receiver_type: receiver,
|
|
1408
1918
|
method_name: node.name,
|
|
@@ -1414,16 +1924,16 @@ module Rigor
|
|
|
1414
1924
|
)
|
|
1415
1925
|
return result if result
|
|
1416
1926
|
|
|
1417
|
-
dispatch_miss_result(node, receiver, arg_types)
|
|
1927
|
+
dispatch_miss_result(node, receiver, arg_types, block_type)
|
|
1418
1928
|
end
|
|
1419
1929
|
|
|
1420
1930
|
# The post-dispatch tiers for a call `MethodDispatcher` could not answer, in their historical
|
|
1421
1931
|
# order; extracted from {#call_dispatch_type_for} whole.
|
|
1422
|
-
def dispatch_miss_result(node, receiver, arg_types)
|
|
1932
|
+
def dispatch_miss_result(node, receiver, arg_types, block_type = nil)
|
|
1423
1933
|
# v0.0.2 #5 — inter-procedural inference for user-defined methods. When dispatch misses but the
|
|
1424
1934
|
# receiver is a user class with a `def` body, re-type the body with the call's argument types bound
|
|
1425
1935
|
# and return the body's last-expression type.
|
|
1426
|
-
user_inference = try_user_method_inference(receiver, node, arg_types)
|
|
1936
|
+
user_inference = try_user_method_inference(receiver, node, arg_types, block_type: block_type)
|
|
1427
1937
|
return user_inference if user_inference
|
|
1428
1938
|
|
|
1429
1939
|
# Module-singleton call resolution (ADR-57 follow-up) — when the receiver is `Singleton[Foo]` (a
|
|
@@ -1431,7 +1941,7 @@ module Rigor
|
|
|
1431
1941
|
# `module_function` body, re-type that body with the call args bound. Sits after the RBS dispatch
|
|
1432
1942
|
# tier, so foreign / RBS-known singletons (`Math.sqrt`) keep their catalog answer; only
|
|
1433
1943
|
# project-defined singleton methods reach here.
|
|
1434
|
-
singleton_inference = try_project_singleton_inference(receiver, node, arg_types)
|
|
1944
|
+
singleton_inference = try_project_singleton_inference(receiver, node, arg_types, block_type: block_type)
|
|
1435
1945
|
return singleton_inference if singleton_inference
|
|
1436
1946
|
|
|
1437
1947
|
# Dynamic-origin propagation: when the receiver is Dynamic[T] and no positive rule resolves the call,
|
|
@@ -1634,8 +2144,8 @@ module Rigor
|
|
|
1634
2144
|
# `scope.self_type` (or implicit `Object`) as the receiver carrier so the body's own self is
|
|
1635
2145
|
# consistent with the call site's. Returns nil when the parameter shape disqualifies the def, when the
|
|
1636
2146
|
# body is empty, or when a recursion cycle is detected.
|
|
1637
|
-
def infer_top_level_user_method(def_node, receiver, arg_types)
|
|
1638
|
-
infer_user_method_return(def_node, receiver, arg_types)
|
|
2147
|
+
def infer_top_level_user_method(def_node, receiver, arg_types, block_type = nil)
|
|
2148
|
+
infer_user_method_return(def_node, receiver, arg_types, yield_type: block_type)
|
|
1639
2149
|
rescue StandardError
|
|
1640
2150
|
nil
|
|
1641
2151
|
end
|
|
@@ -1706,6 +2216,9 @@ module Rigor
|
|
|
1706
2216
|
# The three receiver-shaped block folds, in their historical order (extracted whole from
|
|
1707
2217
|
# `call_dispatch_type_for` for method-length budget).
|
|
1708
2218
|
def try_receiver_block_folds(node, receiver, arg_types)
|
|
2219
|
+
rebound = rebound_operand_typer(node)
|
|
2220
|
+
return rebound.send(:try_receiver_block_folds, node, receiver, arg_types) if rebound
|
|
2221
|
+
|
|
1709
2222
|
per_element = try_per_element_block_fold(node, receiver)
|
|
1710
2223
|
return per_element if per_element
|
|
1711
2224
|
|
|
@@ -1715,6 +2228,25 @@ module Rigor
|
|
|
1715
2228
|
try_hash_shape_block_fold(node, receiver)
|
|
1716
2229
|
end
|
|
1717
2230
|
|
|
2231
|
+
# Issue #1365 — Ruby runs a call's receiver chain and arguments before the method yields, so when they may
|
|
2232
|
+
# rebind the match globals ({MatchRebinding.operands_may_rebind?}: `[u.index(/(q)/)].map { $1 }`) every
|
|
2233
|
+
# pass that types the call's block — the block-return pass, its captured-local fixpoint and the receiver
|
|
2234
|
+
# folds — types it under a typer whose scope has forgotten them, as {MatchRebinding.block_entry} does for
|
|
2235
|
+
# the call's block and `StatementEvaluator#forget_operand_specials` for the statement. `$_` is forgotten on
|
|
2236
|
+
# the same terms when they may set it ({LastLine.operands_may_set?}, issue #1359). nil otherwise.
|
|
2237
|
+
def rebound_operand_typer(call_node)
|
|
2238
|
+
return nil if call_node.block.nil?
|
|
2239
|
+
|
|
2240
|
+
rebound = scope
|
|
2241
|
+
if rebound.match_globals_bound? && MatchRebinding.operands_may_rebind?(call_node, scope)
|
|
2242
|
+
rebound = rebound.forget_match_globals
|
|
2243
|
+
end
|
|
2244
|
+
rebound = rebound.forget_last_line if rebound.last_line_bound? && LastLine.operands_may_set?(call_node, scope)
|
|
2245
|
+
return nil if rebound.equal?(scope)
|
|
2246
|
+
|
|
2247
|
+
ExpressionTyper.new(scope: rebound, tracer: tracer, operand_types: @operand_types, typing_node: @typing_node)
|
|
2248
|
+
end
|
|
2249
|
+
|
|
1718
2250
|
# Issue #533 — `x.send(:selector, args)` with a LITERAL symbol is statically `x.selector(args)`:
|
|
1719
2251
|
# the private-boundary idiom (protobuf's `send(:get_file_descriptor)` at 84 sites) resolves through
|
|
1720
2252
|
# the same dispatch + project-inference tiers as the direct call would. `send` legitimately crosses
|
|
@@ -1745,14 +2277,15 @@ module Rigor
|
|
|
1745
2277
|
try_project_singleton_inference(receiver, node, inner_args, method_name: inner_name)
|
|
1746
2278
|
end
|
|
1747
2279
|
|
|
1748
|
-
def try_user_method_inference(receiver, call_node, arg_types, method_name: call_node.name)
|
|
2280
|
+
def try_user_method_inference(receiver, call_node, arg_types, method_name: call_node.name, block_type: nil)
|
|
1749
2281
|
return nil unless user_inference_receiver?(receiver)
|
|
1750
2282
|
|
|
1751
2283
|
def_node, owner = resolve_user_def_with_owner(receiver.class_name, method_name)
|
|
1752
2284
|
return nil if def_node.nil?
|
|
1753
2285
|
|
|
1754
2286
|
result = infer_user_method_return(def_node, receiver, arg_types,
|
|
1755
|
-
self_fold_safe: fold_safe_call_receiver?(call_node, receiver)
|
|
2287
|
+
self_fold_safe: fold_safe_call_receiver?(call_node, receiver),
|
|
2288
|
+
yield_type: block_type)
|
|
1756
2289
|
return result if result.nil?
|
|
1757
2290
|
|
|
1758
2291
|
degrade_if_overridable(result, owner, method_name, :instance)
|
|
@@ -1823,13 +2356,14 @@ module Rigor
|
|
|
1823
2356
|
# The OWNER — not the receiver class — is what the overridable gate keys on, exactly as the instance
|
|
1824
2357
|
# side does: adopting `Base`'s literal return is unsound when a subclass redefines the method, and
|
|
1825
2358
|
# after this change the resolved body is routinely not the receiver's own.
|
|
1826
|
-
def try_singleton_method_inference(receiver, call_node, arg_types, method_name: call_node.name
|
|
2359
|
+
def try_singleton_method_inference(receiver, call_node, arg_types, method_name: call_node.name,
|
|
2360
|
+
block_type: nil)
|
|
1827
2361
|
return nil unless receiver.is_a?(Type::Singleton)
|
|
1828
2362
|
|
|
1829
2363
|
def_node, owner = resolve_singleton_def_with_owner(receiver.class_name, method_name)
|
|
1830
2364
|
return nil if def_node.nil?
|
|
1831
2365
|
|
|
1832
|
-
result = infer_user_method_return(def_node, receiver, arg_types)
|
|
2366
|
+
result = infer_user_method_return(def_node, receiver, arg_types, yield_type: block_type)
|
|
1833
2367
|
return result if result.nil?
|
|
1834
2368
|
|
|
1835
2369
|
degrade_if_overridable(result, owner, method_name, :singleton)
|
|
@@ -1857,9 +2391,11 @@ module Rigor
|
|
|
1857
2391
|
# body the project itself wrote. Which of the two tiers applies is decided by the receiver carrier —
|
|
1858
2392
|
# `Singleton[Foo]` when the constant names a class or module, `Nominal[…]` when it holds an ordinary
|
|
1859
2393
|
# object (#320) — so the two are mutually exclusive and consulting both is one resolution attempt.
|
|
1860
|
-
def try_project_singleton_inference(receiver, call_node, arg_types, method_name: call_node.name
|
|
1861
|
-
|
|
1862
|
-
|
|
2394
|
+
def try_project_singleton_inference(receiver, call_node, arg_types, method_name: call_node.name,
|
|
2395
|
+
block_type: nil)
|
|
2396
|
+
kwargs = { method_name: method_name, block_type: block_type }
|
|
2397
|
+
try_singleton_method_inference(receiver, call_node, arg_types, **kwargs) ||
|
|
2398
|
+
try_singleton_object_constant_inference(receiver, call_node, arg_types, **kwargs)
|
|
1863
2399
|
end
|
|
1864
2400
|
|
|
1865
2401
|
# #320 — resolves a call whose receiver is a constant holding an ordinary object with a `class << Const`
|
|
@@ -1867,13 +2403,14 @@ module Rigor
|
|
|
1867
2403
|
# that object, so the receiver carrier is passed through unchanged. Own-constant only, and only for a
|
|
1868
2404
|
# name the project actually recorded — a miss degrades to today's `Dynamic[top]`, never a false
|
|
1869
2405
|
# resolution. `Singleton` receivers never reach here: {#try_singleton_method_inference} owns them.
|
|
1870
|
-
def try_singleton_object_constant_inference(receiver, call_node, arg_types, method_name: call_node.name
|
|
2406
|
+
def try_singleton_object_constant_inference(receiver, call_node, arg_types, method_name: call_node.name,
|
|
2407
|
+
block_type: nil)
|
|
1871
2408
|
return nil unless receiver.is_a?(Type::Nominal)
|
|
1872
2409
|
|
|
1873
2410
|
def_node = SingletonObjectConstant.def_node_for(call_node, receiver, method_name, scope)
|
|
1874
2411
|
return nil if def_node.nil?
|
|
1875
2412
|
|
|
1876
|
-
infer_user_method_return(def_node, receiver, arg_types)
|
|
2413
|
+
infer_user_method_return(def_node, receiver, arg_types, yield_type: block_type)
|
|
1877
2414
|
rescue StandardError
|
|
1878
2415
|
nil
|
|
1879
2416
|
end
|
|
@@ -1889,25 +2426,49 @@ module Rigor
|
|
|
1889
2426
|
CLASS_GRAPH_CACHE_KEY = :__rigor_class_graph_cache__
|
|
1890
2427
|
private_constant :CLASS_GRAPH_CACHE_KEY
|
|
1891
2428
|
|
|
1892
|
-
#
|
|
1893
|
-
#
|
|
1894
|
-
#
|
|
1895
|
-
#
|
|
1896
|
-
#
|
|
1897
|
-
#
|
|
1898
|
-
#
|
|
1899
|
-
#
|
|
1900
|
-
#
|
|
1901
|
-
#
|
|
1902
|
-
#
|
|
2429
|
+
# Memo for the static class-graph resolvers below. They are pure functions of the scope's *frozen*
|
|
2430
|
+
# discovery tables — `user_def_for` / `superclass_of` / `includes_of` and the ancestor-name resolver
|
|
2431
|
+
# read nothing else, and never touch the current scope's locals or narrowings — so a result computed
|
|
2432
|
+
# for one `(class, method)` is valid for every `Scope` carrying the same {DiscoveryIndex}.
|
|
2433
|
+
#
|
|
2434
|
+
# The key is that index's identity, NOT the three tables the walk is usually described by. Five
|
|
2435
|
+
# tables are in play: `discovered_def_nodes` / `discovered_superclasses` / `discovered_includes`,
|
|
2436
|
+
# plus `discovered_header_nestings` and `discovered_methods`, which {Scope#ancestor_name_candidates}
|
|
2437
|
+
# and {Scope#known_user_class?} read when resolving an ancestor name to a project class. Keying on
|
|
2438
|
+
# the trio alone would let a `discovery.with(discovered_methods: …)` that happens to share the trio
|
|
2439
|
+
# serve a name resolution computed against the OLD table. The index that owns all five is one
|
|
2440
|
+
# object, so keying on it is both cheaper to check and correct by construction — and stays correct
|
|
2441
|
+
# when a sixth table joins the walk.
|
|
2442
|
+
#
|
|
2443
|
+
# ONE slot, replaced rather than accumulated — the same shape, and now the same key, as
|
|
2444
|
+
# {MethodDispatcher::RbsDispatch}'s `core_stdlib_memo`. `ExpressionTyper` is rebuilt per
|
|
2445
|
+
# `Scope#type_of`, so the slot lives on `Thread.current` rather than on `self`. A `Scope` merges its
|
|
2446
|
+
# file's discovery with the project pre-pass, so every analysed file arrives with a fresh index: an
|
|
2447
|
+
# identity-keyed *store* grew one bucket per file and, because the keys ARE the tables, pinned every
|
|
2448
|
+
# file's whole discovery index for the length of the run. What that bought, measured over
|
|
2449
|
+
# `lib`+`plugins`: 552 buckets holding 1,834 entries between them, and 58 of 80,106 calls answered
|
|
2450
|
+
# across files — 149 of the 701 slot switches returned to a bucket seen before, all of them in the
|
|
2451
|
+
# seed phases that revisit the project index. One slot gives those 58 answers up and allocates 706
|
|
2452
|
+
# buckets instead of 552, and still allocates FEWER objects over `lib` than the store did (−368),
|
|
2453
|
+
# because one `equal?` is cheaper than three identity-hash lookups.
|
|
2454
|
+
#
|
|
2455
|
+
# Steady-state cost is one `equal?` check and zero allocation; the array and the buckets are
|
|
2456
|
+
# allocated once per index. (Pool mode forks per worker, so the `Thread.current` slot is
|
|
2457
|
+
# process-local and never crosses a project boundary — and note that the parent's reported
|
|
2458
|
+
# `Memory peak` therefore does not see a worker's share of this at all.)
|
|
1903
2459
|
def class_graph_buckets
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
2460
|
+
discovery = scope.discovery
|
|
2461
|
+
slot = Thread.current[CLASS_GRAPH_CACHE_KEY]
|
|
2462
|
+
unless slot && slot[0].equal?(discovery)
|
|
2463
|
+
# `self_pure` is issue #525's grant scan (identity-keyed by def node); it belongs here because it
|
|
2464
|
+
# is a pure function of the same frozen index — the sibling resolver it walks reads nothing else.
|
|
2465
|
+
# `singleton_def` is added lazily by {#singleton_def_through_ancestors}'s caller.
|
|
2466
|
+
slot = [discovery,
|
|
2467
|
+
{ name: {}, user_def: {}, self_pure: {}.compare_by_identity,
|
|
2468
|
+
yields: {}.compare_by_identity }]
|
|
2469
|
+
Thread.current[CLASS_GRAPH_CACHE_KEY] = slot
|
|
2470
|
+
end
|
|
2471
|
+
slot[1]
|
|
1911
2472
|
end
|
|
1912
2473
|
|
|
1913
2474
|
def resolve_user_def_through_ancestors(class_name, method_name)
|
|
@@ -1966,16 +2527,40 @@ module Rigor
|
|
|
1966
2527
|
OVERRIDE_GATE_CACHE_KEY = :__rigor_overridable_method_gate__
|
|
1967
2528
|
private_constant :OVERRIDE_GATE_CACHE_KEY
|
|
1968
2529
|
|
|
1969
|
-
#
|
|
1970
|
-
# the frozen discovery
|
|
1971
|
-
#
|
|
1972
|
-
#
|
|
1973
|
-
#
|
|
2530
|
+
# Memo for {#overridden_in_project?}, nested `kind → owner → method_name`. The predicate is a pure
|
|
2531
|
+
# function of the scope's frozen discovery tables, so that index's *identity* is what says whether a
|
|
2532
|
+
# bucket still applies. Nesting under `kind` and `owner` avoids allocating a composite cache key on
|
|
2533
|
+
# the hot path (the gate runs on every adopted self-call return), so a steady-state hit is one
|
|
2534
|
+
# `equal?` check and three keyed hash reads with zero allocation.
|
|
2535
|
+
#
|
|
2536
|
+
# The key is the whole {Scope::DiscoveryIndex}, NOT the `discovered_def_nodes` /
|
|
2537
|
+
# `discovered_superclasses` / `discovered_includes` trio the walk is usually described by — the same
|
|
2538
|
+
# key, for the same reason, as {#class_graph_buckets}. {#related_to_owner?} reaches
|
|
2539
|
+
# {Scope#ancestor_name_candidates} and {Scope#known_user_class?}, which read
|
|
2540
|
+
# `discovered_header_nestings` and `discovered_methods` as well, so a trio key would let an index
|
|
2541
|
+
# that swapped only one of those serve an answer computed against the old table. Over `lib`+`plugins`
|
|
2542
|
+
# the two keys switch the same 113 times, so correctness here is free.
|
|
2543
|
+
#
|
|
2544
|
+
# ONE slot, replaced rather than accumulated — the same shape as
|
|
2545
|
+
# {MethodDispatcher::RbsDispatch}'s `core_stdlib_memo`. A `Scope` merges its file's discovery with the
|
|
2546
|
+
# project pre-pass, so every analysed file gets a fresh index: an identity-keyed *store* grew one
|
|
2547
|
+
# bucket per file and, because the key IS the index, pinned every file's whole discovery index for the
|
|
2548
|
+
# length of the run — 109 live buckets over `lib`+`plugins`, one per distinct trio at every key level,
|
|
2549
|
+
# holding 418 answers between them (211 instance, 207 singleton).
|
|
2550
|
+
#
|
|
2551
|
+
# What the store bought for that was one bucket's worth of cross-file reuse, not 109: only the
|
|
2552
|
+
# project-seed scope recurs, because {Analysis::Runner} types each file's pre-passes under it before
|
|
2553
|
+
# {ScopeIndexer} merges the file's own discovery in. A bucket is two small hashes, so giving that up
|
|
2554
|
+
# is free — unlike {#method_definers_index} below, whose bucket costs a full table scan and which is
|
|
2555
|
+
# keyed and bounded differently for exactly that reason.
|
|
1974
2556
|
def override_gate_buckets
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
2557
|
+
discovery = scope.discovery
|
|
2558
|
+
slot = Thread.current[OVERRIDE_GATE_CACHE_KEY]
|
|
2559
|
+
unless slot && slot[0].equal?(discovery)
|
|
2560
|
+
slot = [discovery, { instance: {}, singleton: {} }]
|
|
2561
|
+
Thread.current[OVERRIDE_GATE_CACHE_KEY] = slot
|
|
2562
|
+
end
|
|
2563
|
+
slot[1]
|
|
1979
2564
|
end
|
|
1980
2565
|
|
|
1981
2566
|
# True when some discovered project class/module — distinct from `owner` — redefines `(method_name,
|
|
@@ -2015,14 +2600,65 @@ module Rigor
|
|
|
2015
2600
|
METHOD_DEFINERS_INDEX_KEY = :__rigor_method_definers_index__
|
|
2016
2601
|
private_constant :METHOD_DEFINERS_INDEX_KEY
|
|
2017
2602
|
|
|
2018
|
-
#
|
|
2019
|
-
#
|
|
2020
|
-
#
|
|
2021
|
-
#
|
|
2603
|
+
# The second way. Two flat thread slots rather than one slot holding an array of ways: a
|
|
2604
|
+
# `Thread.current[KEY] ||= […]` seeds the store with an array LITERAL, which folds to a tuple whose
|
|
2605
|
+
# elements are pinned, and the engine then reads the `way[0].equal?(…)` guards below as always-falsey
|
|
2606
|
+
# (`make check` fires `flow.always-truthy-condition` on this very file). Two keys read as two plain
|
|
2607
|
+
# `Thread.current` reads and keep the guards analysable.
|
|
2608
|
+
METHOD_DEFINERS_INDEX_ALT_KEY = :__rigor_method_definers_index_alt__
|
|
2609
|
+
private_constant :METHOD_DEFINERS_INDEX_ALT_KEY
|
|
2610
|
+
|
|
2611
|
+
# `method_name (Symbol) → [owner names]` inverted index over the instance / singleton def tables,
|
|
2612
|
+
# valid for exactly as long as the table it inverts: a new analysis generation, or a `Scope` that swaps
|
|
2613
|
+
# its index through {Scope#with_discovery}, needs a fresh one. The toplevel sentinel is excluded — a
|
|
2614
|
+
# toplevel `def` has no class ancestry and so can never be an override.
|
|
2615
|
+
#
|
|
2616
|
+
# Bounded to TWO slots, replaced rather than accumulated. This memo is the costliest member of the
|
|
2617
|
+
# per-file-store family, because it does not hold a handful of resolved answers but a whole inverted
|
|
2618
|
+
# index built over a file's merged def table: an identity-keyed *store* held 135 of them over
|
|
2619
|
+
# `lib`+`plugins` — 744,431 index rows, 50.0 MB in the index hashes and their owner-name arrays alone,
|
|
2620
|
+
# on top of the def tables the keys pinned.
|
|
2621
|
+
#
|
|
2622
|
+
# Two, and not the one slot the cheaper memos in this family use, because the tables ALTERNATE.
|
|
2623
|
+
# {Analysis::Runner} types each file's pre-passes under the project-seed scope and its main pass under
|
|
2624
|
+
# the file's merged tables. Where a file reaches this gate from BOTH phases the request sequence is
|
|
2625
|
+
# `seed, file_1, seed, file_2, …`, and a single slot evicts the seed's index once per file and
|
|
2626
|
+
# rebuilds it on the next — a full scan of the seed's whole def table, every file. 200 synthetic
|
|
2627
|
+
# files shaped that way measured +22.6 % allocations against the store, which a bounded slot must not
|
|
2628
|
+
# cost. Two ways — a most-recently-used slot and one alternate, swapped on a hit in the alternate —
|
|
2629
|
+
# cover that period-2 shape exactly: the same 200 files rebuild 201 times, matching the store.
|
|
2630
|
+
#
|
|
2631
|
+
# Wider rotations are not covered, and do not need to be. On `lib`+`plugins` only 6 of the 109 gating
|
|
2632
|
+
# files consult the seed's table, so its uses are separated by many other files' tables and fall out
|
|
2633
|
+
# of both ways: five rebuilds of a 700-entry table remain, about 4 ms. Retention is bounded at two
|
|
2634
|
+
# generations either way, which is what the store failed to do.
|
|
2635
|
+
#
|
|
2636
|
+
# A way is `[def_nodes, singleton_def_nodes, instance_index, singleton_index]`, so it keys on BOTH def
|
|
2637
|
+
# tables and a gate call that alternates instance and singleton kinds within one file does not rebuild
|
|
2638
|
+
# either. Each index is still built lazily: a run that never asks a singleton question never pays for
|
|
2639
|
+
# the singleton index.
|
|
2640
|
+
#
|
|
2641
|
+
# DELIBERATELY not keyed on the whole {Scope::DiscoveryIndex}, unlike {#class_graph_buckets} and
|
|
2642
|
+
# {#override_gate_buckets} above. {#build_method_definers_index} reads the ONE table it is handed and
|
|
2643
|
+
# nothing else, so the def tables are already the complete key, and the index object is a strictly
|
|
2644
|
+
# narrower one: the project-seed scope carries a fresh index per file while its def tables stay the
|
|
2645
|
+
# same object. Over the 200 synthetic files above, an index-keyed memo misses all 400 times where a
|
|
2646
|
+
# def-table-keyed one misses 201 — it would rebuild the seed's 934-entry table once per file and hand
|
|
2647
|
+
# back the +22.6 % this bound exists to avoid. Check that number before widening this key to match
|
|
2648
|
+
# its siblings.
|
|
2022
2649
|
def method_definers_index(kind)
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2650
|
+
def_nodes = scope.discovered_def_nodes
|
|
2651
|
+
singleton_def_nodes = scope.discovered_singleton_def_nodes
|
|
2652
|
+
way = Thread.current[METHOD_DEFINERS_INDEX_KEY]
|
|
2653
|
+
unless way && way[0].equal?(def_nodes) && way[1].equal?(singleton_def_nodes)
|
|
2654
|
+
alt = Thread.current[METHOD_DEFINERS_INDEX_ALT_KEY]
|
|
2655
|
+
alt = nil unless alt && alt[0].equal?(def_nodes) && alt[1].equal?(singleton_def_nodes)
|
|
2656
|
+
Thread.current[METHOD_DEFINERS_INDEX_ALT_KEY] = way
|
|
2657
|
+
way = alt || [def_nodes, singleton_def_nodes, nil, nil]
|
|
2658
|
+
Thread.current[METHOD_DEFINERS_INDEX_KEY] = way
|
|
2659
|
+
end
|
|
2660
|
+
singleton = kind == :singleton
|
|
2661
|
+
way[singleton ? 3 : 2] ||= build_method_definers_index(singleton ? singleton_def_nodes : def_nodes)
|
|
2026
2662
|
end
|
|
2027
2663
|
|
|
2028
2664
|
def build_method_definers_index(table)
|
|
@@ -2078,14 +2714,6 @@ module Rigor
|
|
|
2078
2714
|
INFERENCE_SUMMARY_KEY = :__rigor_user_method_return_summary__
|
|
2079
2715
|
private_constant :INFERENCE_SUMMARY_KEY
|
|
2080
2716
|
|
|
2081
|
-
# Monotonic per-thread counter, bumped once each time `consult_summary` actually reads an in-flight
|
|
2082
|
-
# fixpoint assumption (ADR-55 slice 2). A method return computed across an interval in which this
|
|
2083
|
-
# counter does NOT move depended on no transient Kleene iterate, so it is FINAL and safe to memoise —
|
|
2084
|
-
# even when the `summaries` table is non-empty because some unrelated outermost frame merely *seeded*
|
|
2085
|
-
# (but never consulted) its own entry. See `infer_user_method_return`'s post-hoc memo gate.
|
|
2086
|
-
SUMMARY_CONSULT_COUNTER_KEY = :__rigor_user_method_summary_consults__
|
|
2087
|
-
private_constant :SUMMARY_CONSULT_COUNTER_KEY
|
|
2088
|
-
|
|
2089
2717
|
# Per-thread append-only log of the seed depths of every in-flight summary `consult_summary` read
|
|
2090
2718
|
# (ADR-55 slice 2 mutual-recursion soundness fix, 2026-06-12). Each fixpoint owner records the guard
|
|
2091
2719
|
# stack size at seed time on its entry (`depth:`); a consult appends the consulted entry's depth here.
|
|
@@ -2248,9 +2876,15 @@ module Rigor
|
|
|
2248
2876
|
# the body both qualify. It does NOT need its own memo-key slot here — the bit is observable on the
|
|
2249
2877
|
# built `body_scope`, which is where every downstream consumer (the memo key, the recursion context)
|
|
2250
2878
|
# reads it from, so the two can never disagree.
|
|
2251
|
-
|
|
2879
|
+
#
|
|
2880
|
+
# `yield_type` (issue #720) is the OTHER call-site-varying input, and unlike `self_fold_safe` it is
|
|
2881
|
+
# not observable on the body scope, so it does need its own memo-key slot. It is dropped for a def
|
|
2882
|
+
# that cannot reach a `yield` — nearly all of them — which keeps the key shape constant for the
|
|
2883
|
+
# methods the memo actually carries and confines the extra dimension to yielding callees.
|
|
2884
|
+
def infer_user_method_return(def_node, receiver, arg_types, self_fold_safe: false, yield_type: nil)
|
|
2252
2885
|
return nil if def_node.body.nil?
|
|
2253
2886
|
|
|
2887
|
+
yield_type = nil unless yield_type && body_yields?(def_node)
|
|
2254
2888
|
body_scope = build_user_method_body_scope(def_node, receiver, arg_types,
|
|
2255
2889
|
self_fold_safe: self_fold_safe)
|
|
2256
2890
|
return nil if body_scope.nil?
|
|
@@ -2283,42 +2917,87 @@ module Rigor
|
|
|
2283
2917
|
# outermost method and thereafter the old `summaries.empty?` gate disabled the memo for every nested
|
|
2284
2918
|
# call, re-walking the shared sub-readers combinatorially (~932k body evaluations for ~20 tiny
|
|
2285
2919
|
# methods). The computation itself lives in `compute_user_method_return`.
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2920
|
+
# Issue #720 — the frame the body's `yield`s read, installed for the whole inference (the memo key
|
|
2921
|
+
# below reads it back from here rather than taking a second parameter, so key and frame cannot
|
|
2922
|
+
# disagree). It is installed even when nil, which is what stops a blockless callee reached from
|
|
2923
|
+
# inside a yielding body from inheriting the outer caller's block.
|
|
2924
|
+
ExpressionTyper.with_call_site_frame(yield_type, forwarded_frame_types(def_node, arg_types)) do
|
|
2925
|
+
unless memo_candidate?(stack, plain_signature)
|
|
2926
|
+
trace_memo_refusal(stack, plain_signature)
|
|
2927
|
+
next compute_user_method_return(def_node, body_scope, stack, summaries,
|
|
2289
2928
|
receiver, arg_types, plain_signature)
|
|
2929
|
+
end
|
|
2930
|
+
|
|
2931
|
+
# INVARIANT (ADR-46 recording soundness, ADR-84 WD2) — the deep cross-file dependency edges (the
|
|
2932
|
+
# reads a callee body's dispatches perform through the instrumented `Scope` accessors) are recorded,
|
|
2933
|
+
# per consumer, only as a side effect of evaluating that body, and a memo hit serves the return
|
|
2934
|
+
# WITHOUT re-evaluating it. Since the bucket is run-scoped, hits CROSS consumer-file boundaries, so
|
|
2935
|
+
# every cross-file hit is PAIRED WITH CACHE-AND-REPLAY of the callee's read-set: under recording,
|
|
2936
|
+
# the first evaluation of a key captures the recorder events of its body walk
|
|
2937
|
+
# (`DependencyRecorder.capture` — observe-and-forward, so the first consumer's own record is
|
|
2938
|
+
# untouched) onto the entry, and a hit replays that set into the current consumer's accumulator
|
|
2939
|
+
# (`DependencyRecorder.replay`, which re-applies the per-consumer self-read filter). A memo hit is
|
|
2940
|
+
# thereby edge-equivalent to a fresh body evaluation for EVERY consumer. The naive alternative —
|
|
2941
|
+
# bypassing the memo while the recorder is active — measured >200x wall on analyzer-shaped files
|
|
2942
|
+
# (ActiveStorage video_analyzer.rb's subtree, 0.43s -> >90s; PR #79) and stays rejected. Pinned by
|
|
2943
|
+
# spec/rigor/inference/return_memo_recording_spec.rb (cross-file replay completeness) and
|
|
2944
|
+
# dependency_recorder_spec.rb's transitive deep-edge example.
|
|
2945
|
+
consult_and_store_return_memo(def_node, body_scope, stack, summaries,
|
|
2946
|
+
receiver, arg_types, plain_signature)
|
|
2290
2947
|
end
|
|
2948
|
+
end
|
|
2949
|
+
|
|
2950
|
+
# Issue #720 — the constructs that own a `yield` written inside them, so a scan for "can this def's
|
|
2951
|
+
# body reach a `yield` that names THIS def's block?" must stop at them. A nested `def` / `class` /
|
|
2952
|
+
# `module` / `class << self` starts a new method-block binding; a block or a lambda does NOT — `yield`
|
|
2953
|
+
# inside `[1, 2].each { yield }` still calls the enclosing method's block, which is precisely the
|
|
2954
|
+
# `each`-wrapper idiom this fix has to keep seeing.
|
|
2955
|
+
YIELD_BOUNDARY_NODES = Set[
|
|
2956
|
+
Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode
|
|
2957
|
+
].freeze
|
|
2958
|
+
private_constant :YIELD_BOUNDARY_NODES
|
|
2959
|
+
|
|
2960
|
+
# Whether `def_node`'s body can reach a `yield`, memoised by def-node identity: it gates the
|
|
2961
|
+
# per-call-site memo dimension above and would otherwise be re-walked once per call site of every
|
|
2962
|
+
# user method. It rides in {#class_graph_buckets} for LIFETIME, not for correctness — a pure
|
|
2963
|
+
# function of the AST depends on none of the tables that bucket keys on, but a store that is
|
|
2964
|
+
# replaced with the analysis generation cannot outlive the nodes it holds.
|
|
2965
|
+
def body_yields?(def_node)
|
|
2966
|
+
cache = class_graph_buckets[:yields]
|
|
2967
|
+
return cache[def_node] if cache.key?(def_node)
|
|
2968
|
+
|
|
2969
|
+
cache[def_node] = yield_reachable?(def_node.body)
|
|
2970
|
+
end
|
|
2291
2971
|
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
# bypassing the memo while the recorder is active — measured >200x wall on analyzer-shaped files
|
|
2303
|
-
# (ActiveStorage video_analyzer.rb's subtree, 0.43s -> >90s; PR #79) and stays rejected. Pinned by
|
|
2304
|
-
# spec/rigor/inference/return_memo_recording_spec.rb (cross-file replay completeness) and
|
|
2305
|
-
# dependency_recorder_spec.rb's transitive deep-edge example.
|
|
2306
|
-
consult_and_store_return_memo(def_node, body_scope, stack, summaries,
|
|
2307
|
-
receiver, arg_types, plain_signature)
|
|
2972
|
+
def yield_reachable?(node)
|
|
2973
|
+
return false if node.nil?
|
|
2974
|
+
return true if node.is_a?(Prism::YieldNode)
|
|
2975
|
+
|
|
2976
|
+
node.rigor_each_child do |child|
|
|
2977
|
+
next if YIELD_BOUNDARY_NODES.include?(child.class)
|
|
2978
|
+
|
|
2979
|
+
return true if yield_reachable?(child)
|
|
2980
|
+
end
|
|
2981
|
+
false
|
|
2308
2982
|
end
|
|
2309
2983
|
|
|
2310
2984
|
# The candidate-frame memo path: consult the current run generation's bucket, and on a miss compute
|
|
2311
2985
|
# and store a FINAL result. Reached only when `memo_candidate?` held (see `infer_user_method_return`).
|
|
2312
|
-
# The store gate (ADR-84 WD3): a result is stored when
|
|
2313
|
-
#
|
|
2314
|
-
#
|
|
2315
|
-
#
|
|
2316
|
-
# guard hit,
|
|
2317
|
-
#
|
|
2318
|
-
#
|
|
2319
|
-
#
|
|
2320
|
-
#
|
|
2321
|
-
#
|
|
2986
|
+
# The store gate (ADR-84 WD3): a result is stored when no transient-machinery event during the
|
|
2987
|
+
# bracket referenced a stack frame BELOW the bracket's entry depth (see TRANSIENT_EVENT_DEPTHS_KEY).
|
|
2988
|
+
# A separate ADR-55 fixpoint-consult bracket counter used to sit in front of this test; it is gone
|
|
2989
|
+
# (issue #875) because the event log already subsumes it — the sole `consult_summary` call site is
|
|
2990
|
+
# the in-cycle guard hit, which logs its event at the consulted owner's position first, so a consult
|
|
2991
|
+
# that could taint a bracket is a below-entry event by construction. Below-entry events — an
|
|
2992
|
+
# ancestor's in-flight Kleene iterate read by a guard hit, a shared-fuel exhaustion, a
|
|
2993
|
+
# possibly-ancestor-caused WD1 clamp — mean the ancestor context influenced `result`, which a
|
|
2994
|
+
# standalone recompute would not reproduce; at-or-above-entry events are the compute's own
|
|
2995
|
+
# deterministic machinery (its own converged fixpoint, sub-cycles that opened and closed inside the
|
|
2996
|
+
# bracket) and do not block the store. A top-of-stack compute (entry depth 0) is standalone by
|
|
2997
|
+
# construction. ADR-84 WD6 adds one exemption on top: a tainted result that
|
|
2998
|
+
# is already `Dynamic[top]` is stored anyway — see `top_result?`. A hit under ADR-46 recording
|
|
2999
|
+
# replays the entry's captured read-set into the current consumer (see the INVARIANT comment at the
|
|
3000
|
+
# call site).
|
|
2322
3001
|
def consult_and_store_return_memo(def_node, body_scope, stack, summaries,
|
|
2323
3002
|
receiver, arg_types, plain_signature)
|
|
2324
3003
|
per_def = (return_memo_bucket[def_node] ||= {})
|
|
@@ -2327,9 +3006,14 @@ module Rigor
|
|
|
2327
3006
|
# was foldable and `Dynamic[top]` when it was not. Without it in the key the first call site to
|
|
2328
3007
|
# reach a def would poison every later one with the other polarity. It is read off the body scope
|
|
2329
3008
|
# rather than passed in, so the key cannot drift from the scope that produced the result.
|
|
3009
|
+
# Issue #720 — the FOURTH such dimension, and it is read off the installed frame rather than
|
|
3010
|
+
# passed in for the same reason: a yielding callee's return depends on the block THIS call site
|
|
3011
|
+
# hands it, so two call sites passing differently-typed blocks must not share an entry. It is nil
|
|
3012
|
+
# for every def that cannot reach a `yield`, which is where the memo's mass is.
|
|
2330
3013
|
memo_key = [receiver.describe(:short),
|
|
2331
3014
|
arg_types.map { |type| type.describe(:short) },
|
|
2332
|
-
body_scope.struct_fold_safe?(:self)
|
|
3015
|
+
body_scope.struct_fold_safe?(:self),
|
|
3016
|
+
ExpressionTyper.current_yield_value_type&.describe(:short)]
|
|
2333
3017
|
if (entry = per_def[memo_key])
|
|
2334
3018
|
BudgetTrace.hit(BudgetTrace::MEMO_HITS)
|
|
2335
3019
|
Analysis::DependencyRecorder.replay(entry.read_set) if Analysis::DependencyRecorder.active?
|
|
@@ -2340,13 +3024,10 @@ module Rigor
|
|
|
2340
3024
|
|
|
2341
3025
|
entry_depth = stack.size
|
|
2342
3026
|
event_mark = transient_event_mark
|
|
2343
|
-
consults_before = summary_consult_count
|
|
2344
3027
|
result, read_set = compute_with_read_capture(def_node, body_scope, stack, summaries,
|
|
2345
3028
|
receiver, arg_types, plain_signature)
|
|
2346
3029
|
|
|
2347
|
-
if
|
|
2348
|
-
BudgetTrace.hit(BudgetTrace::MEMO_REFUSE_CONSULT_TAINTED)
|
|
2349
|
-
elsif context_tainted?(event_mark, entry_depth)
|
|
3030
|
+
if context_tainted?(event_mark, entry_depth) && !top_result?(result)
|
|
2350
3031
|
BudgetTrace.hit(BudgetTrace::MEMO_REFUSE_TRANSIENT)
|
|
2351
3032
|
else
|
|
2352
3033
|
per_def[memo_key] = MemoEntry.new(result: result, read_set: read_set,
|
|
@@ -2359,19 +3040,48 @@ module Rigor
|
|
|
2359
3040
|
Thread.current[TRANSIENT_EVENT_DEPTHS_KEY]&.size || 0
|
|
2360
3041
|
end
|
|
2361
3042
|
|
|
2362
|
-
def summary_consult_count
|
|
2363
|
-
Thread.current[SUMMARY_CONSULT_COUNTER_KEY] || 0
|
|
2364
|
-
end
|
|
2365
|
-
|
|
2366
3043
|
# ADR-84 WD3 — true when a transient-machinery event logged during the bracket (entries past
|
|
2367
3044
|
# `event_mark`) referenced a frame below `entry_depth`. The log is cleared when the guard stack drains
|
|
2368
3045
|
# (only possible mid-bracket for a top-of-stack compute, whose events are deterministic anyway), so a
|
|
2369
3046
|
# missing / shorter log reads as untainted.
|
|
3047
|
+
#
|
|
3048
|
+
# Scanned by index rather than over `log[event_mark..]`: the log is append-only for the whole
|
|
3049
|
+
# outermost entry, so the suffix slice allocated an array per candidate compute — a term quadratic in
|
|
3050
|
+
# the event count, and the `ary_ensure_room_for_push` / GC-sweep profile issue #870 sampled.
|
|
2370
3051
|
def context_tainted?(event_mark, entry_depth)
|
|
2371
3052
|
log = Thread.current[TRANSIENT_EVENT_DEPTHS_KEY]
|
|
2372
3053
|
return false if log.nil? || log.size <= event_mark
|
|
2373
3054
|
|
|
2374
|
-
|
|
3055
|
+
index = event_mark
|
|
3056
|
+
size = log.size
|
|
3057
|
+
while index < size
|
|
3058
|
+
return true if log[index] < entry_depth
|
|
3059
|
+
|
|
3060
|
+
index += 1
|
|
3061
|
+
end
|
|
3062
|
+
false
|
|
3063
|
+
end
|
|
3064
|
+
|
|
3065
|
+
# ADR-84 WD6 (issue #872) — the top-result exemption to the WD3 store gate.
|
|
3066
|
+
#
|
|
3067
|
+
# WD3 refuses a store whenever the bracket saw a below-entry transient event, because such a result
|
|
3068
|
+
# embeds an ancestor's in-flight state and a standalone recompute need not reproduce it. Inside a
|
|
3069
|
+
# strongly connected component of mutually recursive methods that holds for nearly every frame — each
|
|
3070
|
+
# nested compute re-enters a signature already on the guard stack and the guard site logs at the
|
|
3071
|
+
# ancestor's shallower position — so nothing in the component is ever memoised and every call edge
|
|
3072
|
+
# re-walks its whole callee subtree. Issue #870's fixture measured 18,412 of 18,436 memo misses
|
|
3073
|
+
# computing a result and discarding it, and rufo 0.18.2's `formatter.rb` did not finish in 25 minutes.
|
|
3074
|
+
#
|
|
3075
|
+
# The exemption: when the computed result is `Dynamic[top]` the entry carries no context-dependent
|
|
3076
|
+
# information to serve. `untyped` is the lattice top and the ADR-5 degradation floor — the only way a
|
|
3077
|
+
# standalone recompute can differ is by being MORE precise, so serving the stored top can never
|
|
3078
|
+
# surface a type the ancestor context invented, and can never raise a diagnostic that a fresh
|
|
3079
|
+
# evaluation would not (nothing is reported off `untyped`). The cost is a possible missed diagnostic
|
|
3080
|
+
# where the untainted recompute would have been precise; the corpus evidence that this does not
|
|
3081
|
+
# happen in practice is the byte-identical `--format json` runs over `lib`, `plugins/*/lib`,
|
|
3082
|
+
# `examples/*/lib` and the survey projects recorded on issue #872.
|
|
3083
|
+
def top_result?(result)
|
|
3084
|
+
result.equal?(Type::Combinator.untyped)
|
|
2375
3085
|
end
|
|
2376
3086
|
|
|
2377
3087
|
# ADR-84 WD2 — the memo-miss compute, wrapped in a `DependencyRecorder.capture` window when ADR-46
|
|
@@ -2440,14 +3150,36 @@ module Rigor
|
|
|
2440
3150
|
evaluate_guarded_user_method_body(def_node, body_scope, stack, signature, context)
|
|
2441
3151
|
end
|
|
2442
3152
|
|
|
3153
|
+
# Issue #1125 — the argument list a `def m(...)` body's `...` re-supplies, or nil for a def without the
|
|
3154
|
+
# forwarding parameter (whose body cannot contain a `...` call at all) and for a call that does not even
|
|
3155
|
+
# satisfy `m`'s own required positionals (that call raises at runtime; the body is not worth re-typing).
|
|
3156
|
+
#
|
|
3157
|
+
# Ruby allows `...` only beside leading positional parameters (`def m(a, ...)`, `def m(a = 1, ...)`) — a
|
|
3158
|
+
# keyword / rest / block parameter next to it is a syntax error — so the tail is the call's own argument
|
|
3159
|
+
# list MINUS the leading positionals those named parameters consume, keeping a trailing keyword shape
|
|
3160
|
+
# (which `takes_keywords?` reads as the keyword tail rather than as a positional). The callee's own
|
|
3161
|
+
# call-site `arg_types` are what remains.
|
|
3162
|
+
def forwarded_frame_types(def_node, arg_types)
|
|
3163
|
+
params = def_node.parameters
|
|
3164
|
+
return nil unless params.is_a?(Prism::ParametersNode)
|
|
3165
|
+
return nil unless params.keyword_rest.is_a?(Prism::ForwardingParameterNode)
|
|
3166
|
+
|
|
3167
|
+
positional = arg_types.dup
|
|
3168
|
+
kw_shape = positional.pop if takes_keywords?(params) && positional.last.is_a?(Type::HashShape)
|
|
3169
|
+
return nil if positional.size < params.requireds.size
|
|
3170
|
+
|
|
3171
|
+
tail = positional[(params.requireds.size + params.optionals.size)..] || []
|
|
3172
|
+
kw_shape ? tail + [kw_shape] : tail
|
|
3173
|
+
end
|
|
3174
|
+
|
|
2443
3175
|
# True when this frame's result is a candidate for the return memo: the one structural precondition,
|
|
2444
3176
|
# stable across the body walk, that is necessary (but not sufficient) for a FINAL result — this plain
|
|
2445
3177
|
# signature must not itself be on the recursion guard stack (else we are inside its own cycle,
|
|
2446
3178
|
# returning a Kleene iterate by construction). Sufficiency is decided post-hoc in
|
|
2447
|
-
# `consult_and_store_return_memo` by the
|
|
2448
|
-
#
|
|
2449
|
-
#
|
|
2450
|
-
#
|
|
3179
|
+
# `consult_and_store_return_memo` by the ADR-84 WD3 transient-machinery event bracket — so unlike
|
|
3180
|
+
# the prior form this deliberately does NOT refuse while a constant-arg unroll is in flight: a
|
|
3181
|
+
# nested frame whose compute finishes without a single transient event ran exactly as it would
|
|
3182
|
+
# standalone (fuel consumption without exhaustion is invisible), and
|
|
2451
3183
|
# the blanket exclusion refused 82% of mail's body evaluations for such final results (ADR-84).
|
|
2452
3184
|
def memo_candidate?(stack, plain_signature)
|
|
2453
3185
|
stack.none? { |frame| plain_part(frame) == plain_signature }
|
|
@@ -2578,7 +3310,6 @@ module Rigor
|
|
|
2578
3310
|
summaries = context.summaries
|
|
2579
3311
|
depth = seed_fixpoint_summary(summaries, plain_signature)
|
|
2580
3312
|
consult_depths = (Thread.current[SUMMARY_CONSULT_DEPTHS_KEY] ||= [])
|
|
2581
|
-
computed = nil
|
|
2582
3313
|
|
|
2583
3314
|
RECURSION_FIXPOINT_CAP.times do |iteration|
|
|
2584
3315
|
summaries[plain_signature][:consulted] = false
|
|
@@ -2612,9 +3343,15 @@ module Rigor
|
|
|
2612
3343
|
return resolved unless resolved.nil?
|
|
2613
3344
|
end
|
|
2614
3345
|
|
|
2615
|
-
|
|
2616
|
-
return
|
|
3346
|
+
converged = fixpoint_step(summaries, plain_signature, computed, iteration)
|
|
3347
|
+
return converged if converged
|
|
2617
3348
|
end
|
|
3349
|
+
|
|
3350
|
+
# Out of iterations and still unstable. The collapse is the loop's own tail rather than a branch
|
|
3351
|
+
# inside `fixpoint_step`: `Integer#times` yields its receiver when no iteration returns, so a
|
|
3352
|
+
# tail-less loop reads as a path that returns an Integer summary (the `def.return-type-mismatch`
|
|
3353
|
+
# on `return_type_for` this shape retired), and the loop, not the step, owns its cap.
|
|
3354
|
+
collapse_fixpoint_cap(summaries, plain_signature)
|
|
2618
3355
|
end
|
|
2619
3356
|
|
|
2620
3357
|
# Seeds the thread-local summary entry for a fixpoint owner: the `bot` Kleene seed plus the
|
|
@@ -2650,8 +3387,10 @@ module Rigor
|
|
|
2650
3387
|
|
|
2651
3388
|
# One Kleene-iteration step of the fixpoint loop. Joins `computed` into the running assumption
|
|
2652
3389
|
# (widening value-pinned constituents on the final permitted iteration to force convergence) and
|
|
2653
|
-
#
|
|
2654
|
-
#
|
|
3390
|
+
# returns the converged type, or `nil` — no final type yet — having advanced the stored assumption
|
|
3391
|
+
# so the caller's next body evaluation reads the wider iterate. The step never decides that the
|
|
3392
|
+
# loop is over: whether another evaluation follows is the caller's cap, and a `nil` on the final
|
|
3393
|
+
# iteration lands in `collapse_fixpoint_cap`. ADR-55 WD2.
|
|
2655
3394
|
def fixpoint_step(summaries, plain_signature, computed, iteration)
|
|
2656
3395
|
assumption = summaries[plain_signature][:assumption]
|
|
2657
3396
|
last_iteration = iteration == RECURSION_FIXPOINT_CAP - 1
|
|
@@ -2662,17 +3401,19 @@ module Rigor
|
|
|
2662
3401
|
# (joining it back changes nothing).
|
|
2663
3402
|
return candidate if joined == assumption
|
|
2664
3403
|
|
|
2665
|
-
if last_iteration
|
|
2666
|
-
# Out of iterations and still unstable — collapse to today's widening behaviour. ADR-84 WD3: the
|
|
2667
|
-
# cap is a per-owner constant, so the event references the owner's own frame.
|
|
2668
|
-
note_transient_fallback(BudgetTrace::RECURSION_FIXPOINT_CAP, own_guard_frame_position)
|
|
2669
|
-
scope.record_dynamic_origin(@typing_node, DynamicOrigin::ANALYZER_BUDGET_CUTOFF) if @typing_node
|
|
2670
|
-
summaries[plain_signature][:assumption] = Type::Combinator.untyped
|
|
2671
|
-
return Type::Combinator.untyped
|
|
2672
|
-
end
|
|
2673
|
-
|
|
2674
3404
|
summaries[plain_signature][:assumption] = joined
|
|
2675
|
-
|
|
3405
|
+
nil
|
|
3406
|
+
end
|
|
3407
|
+
|
|
3408
|
+
# The capped collapse: `RECURSION_FIXPOINT_CAP` evaluations advanced the assumption without
|
|
3409
|
+
# converging, so the summary degrades to today's widening behaviour, `untyped`, parked in the
|
|
3410
|
+
# assumption for any consumer that still reads it. ADR-84 WD3: the cap is a per-owner constant, so
|
|
3411
|
+
# the event references the owner's own frame.
|
|
3412
|
+
def collapse_fixpoint_cap(summaries, plain_signature)
|
|
3413
|
+
note_transient_fallback(BudgetTrace::RECURSION_FIXPOINT_CAP, own_guard_frame_position)
|
|
3414
|
+
scope.record_dynamic_origin(@typing_node, DynamicOrigin::ANALYZER_BUDGET_CUTOFF) if @typing_node
|
|
3415
|
+
summaries[plain_signature][:assumption] = Type::Combinator.untyped
|
|
3416
|
+
Type::Combinator.untyped
|
|
2676
3417
|
end
|
|
2677
3418
|
|
|
2678
3419
|
# Rebuilds the user-method body scope with every bound positional parameter widened to its nominal
|
|
@@ -2859,7 +3600,8 @@ module Rigor
|
|
|
2859
3600
|
locals = bind_params_from_call_types(params, arg_types)
|
|
2860
3601
|
return nil if locals.nil?
|
|
2861
3602
|
|
|
2862
|
-
# Construct the body scope in a SINGLE allocation
|
|
3603
|
+
# Construct the body scope in a SINGLE Scope allocation (plus the issue #1358 frame it carries, whose
|
|
3604
|
+
# scans run only on demand) — the previous `Scope.empty.with_*.with_*…` chain
|
|
2863
3605
|
# allocated a fresh frozen Scope per field, run per user-method-call inference (ADR-44). The
|
|
2864
3606
|
# discovery index is inherited whole by reference (ADR-53 Track A); the hand-copied per-field list
|
|
2865
3607
|
# this replaces had silently dropped `data_member_layouts` and `discovered_method_visibilities`.
|
|
@@ -2877,7 +3619,9 @@ module Rigor
|
|
|
2877
3619
|
lexical_nesting: recorded_def_nesting(def_node),
|
|
2878
3620
|
discovery: scope.discovery,
|
|
2879
3621
|
struct_fold_safe_locals: body_fold_safe_locals(def_node, receiver, self_fold_safe),
|
|
2880
|
-
dynamic_origins: scope.dynamic_origins
|
|
3622
|
+
dynamic_origins: scope.dynamic_origins,
|
|
3623
|
+
# Issue #1358 — the callee runs in a frame of its own ({Scope#with_match_frame}).
|
|
3624
|
+
match_frame: MatchRebinding::Frame.new(def_node.body, def_node.parameters)
|
|
2881
3625
|
)
|
|
2882
3626
|
end
|
|
2883
3627
|
|
|
@@ -2957,17 +3701,35 @@ module Rigor
|
|
|
2957
3701
|
return nil if locals.nil?
|
|
2958
3702
|
|
|
2959
3703
|
bind_keyword_params(params, kw_shape, locals)
|
|
2960
|
-
|
|
3704
|
+
rest_name = keyword_rest_name(params)
|
|
3705
|
+
locals[rest_name.to_sym] = keyword_rest_type(params, kw_shape) if rest_name
|
|
2961
3706
|
locals[params.block.name.to_sym] = dynamic_top if params.block&.name
|
|
2962
3707
|
locals
|
|
2963
3708
|
end
|
|
2964
3709
|
|
|
2965
|
-
# Trailing required positionals after a rest (`def f(a, *m, z)`) shift the correspondence
|
|
2966
|
-
#
|
|
3710
|
+
# Trailing required positionals after a rest (`def f(a, *m, z)`) shift the correspondence and stay
|
|
3711
|
+
# declined — correspondence, not width, is the issue. Issue #1125: `def m(...)`'s forwarding slot is no
|
|
3712
|
+
# longer a decline. Its NAMED parameters (a leading `def m(a, ...)` required) bind exactly as before;
|
|
3713
|
+
# the forwarded tail is not a binding, so nothing is bound for it — the body's own `f(...)` reads the
|
|
3714
|
+
# frame's argument list instead ({#forwarded_argument_types}).
|
|
2967
3715
|
def bindable_param_shape?(params)
|
|
2968
|
-
params.is_a?(Prism::ParametersNode) &&
|
|
2969
|
-
|
|
2970
|
-
|
|
3716
|
+
params.is_a?(Prism::ParametersNode) && params.posts.empty?
|
|
3717
|
+
end
|
|
3718
|
+
|
|
3719
|
+
# Whether the def's trailing slot is `...` rather than a named `*rest` / `**rest`. Such a def accepts
|
|
3720
|
+
# an unbounded positional tail it does not name, so it behaves as a rest for the positional
|
|
3721
|
+
# correspondence and binds no local.
|
|
3722
|
+
def forwarding_params?(params)
|
|
3723
|
+
params.keyword_rest.is_a?(Prism::ForwardingParameterNode)
|
|
3724
|
+
end
|
|
3725
|
+
|
|
3726
|
+
# A `**rest` parameter's name, or nil when the slot is `def m(...)`'s forwarding parameter —
|
|
3727
|
+
# `Prism::ForwardingParameterNode` carries no name at all.
|
|
3728
|
+
def keyword_rest_name(params)
|
|
3729
|
+
rest = params.keyword_rest
|
|
3730
|
+
return nil unless rest.respond_to?(:name)
|
|
3731
|
+
|
|
3732
|
+
rest.name
|
|
2971
3733
|
end
|
|
2972
3734
|
|
|
2973
3735
|
def takes_keywords?(params)
|
|
@@ -2978,7 +3740,9 @@ module Rigor
|
|
|
2978
3740
|
requireds = params.requireds
|
|
2979
3741
|
optionals = params.optionals
|
|
2980
3742
|
return nil if positional.size < requireds.size
|
|
2981
|
-
|
|
3743
|
+
|
|
3744
|
+
extra = positional.size - requireds.size - optionals.size
|
|
3745
|
+
return nil if extra.positive? && params.rest.nil? && !forwarding_params?(params)
|
|
2982
3746
|
|
|
2983
3747
|
locals = {}
|
|
2984
3748
|
requireds.each_with_index { |param, index| bind_positional_param(locals, param, positional[index]) }
|
|
@@ -3022,6 +3786,23 @@ module Rigor
|
|
|
3022
3786
|
param.respond_to?(:value) && param.value ? literal_default_type(param.value) : dynamic_top
|
|
3023
3787
|
end
|
|
3024
3788
|
|
|
3789
|
+
# Issue #1125 — `**rest` collects the keyword-shape entries no named keyword parameter consumed, as a
|
|
3790
|
+
# closed `HashShape` of their value types. The pre-#1125 `Dynamic[top]` is kept whenever there is
|
|
3791
|
+
# nothing left to collect: no keyword shape at all, an open shape (unknown extras — every remaining-key
|
|
3792
|
+
# answer would be a guess), or a shape whose every pair a named parameter consumed. That last case is
|
|
3793
|
+
# what leaves the literal form's existing binding untouched (`target(a: 1, b: 2)` against
|
|
3794
|
+
# `def target(a:, b:, **rest)` still binds `rest` to `Dynamic[top]`), while a caller that DOES leave
|
|
3795
|
+
# keys over — the only shape where the parameter holds something to report — gets them typed.
|
|
3796
|
+
def keyword_rest_type(params, kw_shape)
|
|
3797
|
+
return dynamic_top if kw_shape.nil? || kw_shape.open?
|
|
3798
|
+
|
|
3799
|
+
consumed = params.keywords.map { |param| param.name.to_s.delete_suffix(":").to_sym }
|
|
3800
|
+
leftovers = kw_shape.pairs.except(*consumed)
|
|
3801
|
+
return dynamic_top if leftovers.empty?
|
|
3802
|
+
|
|
3803
|
+
Type::Combinator.hash_shape_of(leftovers)
|
|
3804
|
+
end
|
|
3805
|
+
|
|
3025
3806
|
# A default expression contributes its type only when it is lexically scope-free — a scalar literal
|
|
3026
3807
|
# or an EMPTY collection literal (`options = {}` is the dominant Rails idiom). Anything that could
|
|
3027
3808
|
# read the def's own lexical scope binds `Dynamic` instead of being mis-typed in the caller's scope.
|
|
@@ -3066,11 +3847,60 @@ module Rigor
|
|
|
3066
3847
|
scope.environment.nominal_for_name("Object") || dynamic_top
|
|
3067
3848
|
end
|
|
3068
3849
|
|
|
3850
|
+
# Issue #1125 — two call-site argument shapes that used to reach every consumer (the parameter
|
|
3851
|
+
# binder, RBS dispatch, the block-parameter reader) as an opaque `Dynamic[top]` / bare `Nominal[Hash]`
|
|
3852
|
+
# now carry what the callee needs:
|
|
3853
|
+
#
|
|
3854
|
+
# - `f(...)` inside `def m(...)` re-supplies the arguments `m` was called with, read from the frame
|
|
3855
|
+
# {#infer_user_method_return} installed ({FORWARDED_ARGS_KEY}) — so it expands to a whole LIST, not
|
|
3856
|
+
# one type, which is why the map became a flat_map.
|
|
3857
|
+
# - a keyword hash built ENTIRELY from a double splat (`f(**h)`) is the shape of `h`, so `f(**h)`
|
|
3858
|
+
# binds the same parameters the literal form `f(a: 1, b: 2)` does.
|
|
3859
|
+
#
|
|
3860
|
+
# Both are precision-only. A `...` outside a forwarding frame (unreachable in valid Ruby) and a double
|
|
3861
|
+
# splat of anything but a Symbol-keyed closed `HashShape` — a shapeless `Hash[Symbol, V]`, an opaque
|
|
3862
|
+
# value, a mixed hash — fall back to exactly the pre-#1125 answer.
|
|
3069
3863
|
def call_arg_types(node)
|
|
3070
3864
|
arguments_node = node.arguments
|
|
3071
3865
|
return [] if arguments_node.nil?
|
|
3072
3866
|
|
|
3073
|
-
arguments_node.arguments.
|
|
3867
|
+
arguments_node.arguments.flat_map do |argument|
|
|
3868
|
+
forwarded_argument_types(argument) || [call_arg_type(argument)]
|
|
3869
|
+
end
|
|
3870
|
+
end
|
|
3871
|
+
|
|
3872
|
+
# The frame's argument list when `argument` is `...`, else nil so the caller types it normally.
|
|
3873
|
+
def forwarded_argument_types(argument)
|
|
3874
|
+
return nil unless argument.is_a?(Prism::ForwardingArgumentsNode)
|
|
3875
|
+
|
|
3876
|
+
ExpressionTyper.current_forwarded_arg_types
|
|
3877
|
+
end
|
|
3878
|
+
|
|
3879
|
+
# The type a single call argument contributes; a keyword-hash argument first offers the `HashShape` it
|
|
3880
|
+
# stands for, and otherwise keeps its own type.
|
|
3881
|
+
def call_arg_type(argument)
|
|
3882
|
+
return type_of(argument) unless argument.is_a?(Prism::KeywordHashNode)
|
|
3883
|
+
|
|
3884
|
+
double_splat_hash_shape(argument) || type_of(argument)
|
|
3885
|
+
end
|
|
3886
|
+
|
|
3887
|
+
# The `HashShape` a `f(**h)` keyword-hash argument stands for, or nil to keep the argument's own type.
|
|
3888
|
+
# Declines a MIXED hash (`f(a: 1, **h)` — merging the literal pairs with the splatted shape is a second
|
|
3889
|
+
# shape algebra this slice does not need), an OPEN shape (unknown extras make every missing-keyword
|
|
3890
|
+
# answer a guess rather than a read), a shape with a non-Symbol key (Ruby itself rejects those as
|
|
3891
|
+
# keywords), and a splat of anything that is not a shape at all.
|
|
3892
|
+
def double_splat_hash_shape(node)
|
|
3893
|
+
elements = node.elements
|
|
3894
|
+
return nil unless elements.size == 1
|
|
3895
|
+
|
|
3896
|
+
splat = elements.first
|
|
3897
|
+
return nil unless splat.is_a?(Prism::AssocSplatNode) && splat.value
|
|
3898
|
+
|
|
3899
|
+
type = type_of(splat.value)
|
|
3900
|
+
return nil unless type.is_a?(Type::HashShape)
|
|
3901
|
+
return nil unless type.closed? && type.pairs.each_key.all?(Symbol)
|
|
3902
|
+
|
|
3903
|
+
type
|
|
3074
3904
|
end
|
|
3075
3905
|
|
|
3076
3906
|
# When the call carries a `Prism::BlockNode`, build the block's entry scope (outer locals plus
|
|
@@ -3092,39 +3922,167 @@ module Rigor
|
|
|
3092
3922
|
return nil if block_arg.nil?
|
|
3093
3923
|
return nil if receiver_type.nil?
|
|
3094
3924
|
|
|
3925
|
+
rebound = rebound_operand_typer(call_node)
|
|
3926
|
+
return rebound.send(:block_return_type_for, call_node, receiver_type, arg_types) if rebound
|
|
3927
|
+
|
|
3095
3928
|
expected = MethodDispatcher.expected_block_param_types(
|
|
3096
3929
|
receiver_type: receiver_type,
|
|
3097
3930
|
method_name: call_node.name,
|
|
3098
3931
|
arg_types: arg_types,
|
|
3099
|
-
environment: scope.environment
|
|
3932
|
+
environment: scope.environment,
|
|
3933
|
+
scope: scope
|
|
3100
3934
|
)
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3935
|
+
block_return_for(
|
|
3936
|
+
block_arg, expected,
|
|
3937
|
+
call_node: call_node,
|
|
3938
|
+
narrowed_self_type: block_body_self_narrowing(call_node, receiver_type),
|
|
3939
|
+
repeats: !BLOCK_VALUE_DISCARDING.include?(call_node.name) && block_may_repeat?(call_node, receiver_type)
|
|
3106
3940
|
)
|
|
3107
|
-
block_return_for(block_arg, expected, narrowed_self_type: narrowed_self)
|
|
3108
3941
|
rescue StandardError
|
|
3109
3942
|
nil
|
|
3110
3943
|
end
|
|
3111
3944
|
|
|
3112
|
-
|
|
3945
|
+
# The catalogued iterators that discard their block's value — it selects the block-bearing overload and
|
|
3946
|
+
# nothing more — so the value pass lays no captured binding under their block: the fixpoint would buy a
|
|
3947
|
+
# type nothing reads, and `sum = 0; xs.each { |x| sum += x }` would pay it on top of the ADR-56
|
|
3948
|
+
# write-back's own. The #853 break-arm scan still lays it, since a `break` value is the call's.
|
|
3949
|
+
BLOCK_VALUE_DISCARDING = Set[
|
|
3950
|
+
:each, :each_with_index, :each_with_object, :each_pair, :each_key, :each_value, :each_index,
|
|
3951
|
+
:reverse_each, :each_entry, :each_slice, :each_cons, :each_char, :each_byte, :each_line,
|
|
3952
|
+
:each_codepoint, :times, :upto, :downto, :step
|
|
3953
|
+
].freeze
|
|
3954
|
+
private_constant :BLOCK_VALUE_DISCARDING
|
|
3955
|
+
|
|
3956
|
+
# Whether the call may run its block more than once, so a later run reads a captured binding an earlier
|
|
3957
|
+
# run rebound — the premise of laying the #587 (b) binding ({#captured_block_bindings}) under the block.
|
|
3958
|
+
# {BlockRepetition.may_repeat?} holds the rule, which the statement pass shares (issue #1412).
|
|
3959
|
+
def block_may_repeat?(call_node, receiver_type)
|
|
3960
|
+
BlockRepetition.may_repeat?(method_name: call_node.name, receiver_type: receiver_type, scope: scope)
|
|
3961
|
+
end
|
|
3962
|
+
|
|
3963
|
+
EMPTY_BREAK_ARMS = [].freeze
|
|
3964
|
+
private_constant :EMPTY_BREAK_ARMS
|
|
3965
|
+
|
|
3966
|
+
# The types the `break`s that terminate THIS call carry out of it, for {#call_dispatch_type_for} to union
|
|
3967
|
+
# with the result the dispatch tiers computed (issue #853).
|
|
3968
|
+
#
|
|
3969
|
+
# The syntactic scan runs first and answers "none" for every call whose block cannot reach a block-level
|
|
3970
|
+
# `break` — nearly all of them — so only that small minority pays the body evaluation. A `break` under a
|
|
3971
|
+
# nested block, lambda, `def`, or loop targets THAT construct instead; {JUMP_BOUNDARY_NODES} stops the
|
|
3972
|
+
# scan there, and the identity filter drops the ones the sink still collects because the nested
|
|
3973
|
+
# construct is walked under the same installation. `break` with no argument carries nil, so the call
|
|
3974
|
+
# becomes optional — which is what Ruby does. A call that only stores its block ({StoredBlockCall}: `lambda`,
|
|
3975
|
+
# `define_method`, `Thread.new`, …) never runs it, so no arm of that block is the call's value.
|
|
3976
|
+
#
|
|
3977
|
+
# A failure yields no arms rather than propagating, matching {#block_return_type_for}: a call typed
|
|
3978
|
+
# without its break arms is the pre-#853 answer, while a raise here would take out the whole call.
|
|
3979
|
+
def call_break_arm_types(node, receiver_override: nil)
|
|
3980
|
+
block_node = node.block
|
|
3981
|
+
return EMPTY_BREAK_ARMS unless block_node.is_a?(Prism::BlockNode)
|
|
3982
|
+
return EMPTY_BREAK_ARMS if StoredBlockCall.stores_block?(node)
|
|
3983
|
+
|
|
3984
|
+
body = block_node.body
|
|
3985
|
+
return EMPTY_BREAK_ARMS if body.nil? || !block_level_jump?(body, Prism::BreakNode)
|
|
3986
|
+
|
|
3987
|
+
collect_break_arm_types(node, block_node, body, receiver_override)
|
|
3988
|
+
rescue StandardError
|
|
3989
|
+
EMPTY_BREAK_ARMS
|
|
3990
|
+
end
|
|
3991
|
+
|
|
3992
|
+
# Evaluates the block body once under a `break`-value sink, in the same entry scope the block-return pass
|
|
3993
|
+
# uses, so each arm is typed in the scope that actually reaches it — a `break v` after `v = "s"`
|
|
3994
|
+
# contributes `"s"`, not the entry binding — and an arm on a branch the analysis proved dead is never
|
|
3995
|
+
# reached at all.
|
|
3996
|
+
def collect_break_arm_types(call_node, block_node, body, receiver_override)
|
|
3997
|
+
targets = block_level_jump_nodes(body, Prism::BreakNode)
|
|
3998
|
+
receiver = receiver_override || call_receiver_type_for(call_node)
|
|
3999
|
+
return EMPTY_BREAK_ARMS if receiver.nil?
|
|
4000
|
+
|
|
4001
|
+
param_types = break_arm_param_types(call_node, receiver)
|
|
4002
|
+
block_scope = block_entry_scope(
|
|
4003
|
+
block_node, param_types,
|
|
4004
|
+
call_node: call_node,
|
|
4005
|
+
narrowed_self_type: block_body_self_narrowing(call_node, receiver),
|
|
4006
|
+
captured: repeating_captured_bindings(block_node, param_types, block_may_repeat?(call_node, receiver))
|
|
4007
|
+
)
|
|
4008
|
+
_result, collected = StatementEvaluator.with_break_value_sink do
|
|
4009
|
+
without_block_body_threading { block_scope.evaluate(body) }
|
|
4010
|
+
end
|
|
4011
|
+
collected.filter_map { |jump, type| type if targets.key?(jump) }
|
|
4012
|
+
end
|
|
4013
|
+
|
|
4014
|
+
# The block body's narrowed `self_type`, or `nil` to leave the scope contract unchanged.
|
|
4015
|
+
#
|
|
4016
|
+
# ADR-16 Tier A: a registered plugin's `block_as_methods` entry matching `(receiver_type,
|
|
4017
|
+
# call_node.name)` narrows to the receiver class's instance type.
|
|
4018
|
+
#
|
|
4019
|
+
# Issue #963: `define_method(:name) { ... }` installs its block as an instance method and runs it with
|
|
4020
|
+
# `self` bound to the receiving instance, so the block body's `self` is the INSTANCE side of a class body's
|
|
4021
|
+
# `Singleton[X]`. Both block-entry paths narrow it, and both decline on the same `class << ...` bodies,
|
|
4022
|
+
# because the whole distinction rides on `Scope#singleton_class_body?` rather than on the statement
|
|
4023
|
+
# evaluator's frame stack. This pass therefore cannot compute a carrier the evaluator disagrees with — it
|
|
4024
|
+
# matters wherever the block's value is observable, e.g. a project-declared generic `define_method`
|
|
4025
|
+
# signature that returns the block's own type.
|
|
4026
|
+
def block_body_self_narrowing(call_node, receiver_type)
|
|
4027
|
+
MacroBlockSelfType.narrow_self_type_for(
|
|
4028
|
+
scope: scope, call_node: call_node, receiver_type: receiver_type
|
|
4029
|
+
) || DefineMethodBlockSelf.narrow_self_type_for(scope: scope, call_node: call_node)
|
|
4030
|
+
end
|
|
4031
|
+
|
|
4032
|
+
def break_arm_param_types(call_node, receiver)
|
|
4033
|
+
MethodDispatcher.expected_block_param_types(
|
|
4034
|
+
receiver_type: receiver,
|
|
4035
|
+
method_name: call_node.name,
|
|
4036
|
+
arg_types: call_arg_types(call_node),
|
|
4037
|
+
environment: scope.environment,
|
|
4038
|
+
scope: scope
|
|
4039
|
+
)
|
|
4040
|
+
end
|
|
4041
|
+
|
|
4042
|
+
def block_return_for(block_arg, expected, call_node: nil, narrowed_self_type: nil, repeats: false)
|
|
3113
4043
|
case block_arg
|
|
3114
4044
|
when Prism::BlockNode
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
end
|
|
3121
|
-
block_scope = block_scope.with_self_type(narrowed_self_type) if narrowed_self_type
|
|
3122
|
-
type_block_body(block_arg, block_scope)
|
|
4045
|
+
captured = repeating_captured_bindings(block_arg, expected, repeats)
|
|
4046
|
+
entry = block_entry_scope(
|
|
4047
|
+
block_arg, expected, call_node: call_node, narrowed_self_type: narrowed_self_type, captured: captured
|
|
4048
|
+
)
|
|
4049
|
+
type_block_body(block_arg, entry, captured: captured)
|
|
3123
4050
|
when Prism::BlockArgumentNode
|
|
3124
4051
|
symbol_block_return_type(block_arg, expected)
|
|
3125
4052
|
end
|
|
3126
4053
|
end
|
|
3127
4054
|
|
|
4055
|
+
# The scope a block body is typed under: the surrounding scope plus the parameter bindings the receiving
|
|
4056
|
+
# method's signature implies, with the #587 (b) `captured` binding ({#repeating_captured_bindings}) laid
|
|
4057
|
+
# between the two, so the body reads what a captured local, instance variable, class variable or global
|
|
4058
|
+
# holds on ANY run rather than the first.
|
|
4059
|
+
#
|
|
4060
|
+
# Issue #316 — mirrors `StatementEvaluator#build_block_entry_scope`: the block body's `self` is the
|
|
4061
|
+
# yielding method's business, so the return-typing pass must see the same unmodelled-self mark. Issue #1358
|
|
4062
|
+
# — and the same match-global view ({MatchRebinding.block_entry}), which reads the owning `call_node`.
|
|
4063
|
+
def block_entry_scope(block_node, expected, call_node: nil, narrowed_self_type: nil, captured: nil)
|
|
4064
|
+
entry = MatchRebinding.block_entry(scope.entering_opaque_block, block_node, call_node)
|
|
4065
|
+
entry = captured.lay(entry) if captured
|
|
4066
|
+
block_scope = BlockParameterBinder.new(expected_param_types: expected).bind_onto(block_node, entry)
|
|
4067
|
+
return block_scope unless narrowed_self_type
|
|
4068
|
+
|
|
4069
|
+
block_scope.with_self_type(narrowed_self_type)
|
|
4070
|
+
end
|
|
4071
|
+
|
|
4072
|
+
# The #587 (b) captured binding when the call may run the block more than once ({#block_may_repeat?}), and
|
|
4073
|
+
# nil — no binding — for a block it runs at most once, whose captures no earlier run can have moved.
|
|
4074
|
+
def repeating_captured_bindings(block_node, expected, repeats)
|
|
4075
|
+
repeats ? generic_captured_bindings(block_node, expected) : nil
|
|
4076
|
+
end
|
|
4077
|
+
|
|
4078
|
+
# The #587 (b) captured binding for the generic pass — or nil, which keeps the entry scope, when computing
|
|
4079
|
+
# it fails: a raise here would otherwise take out the whole block-return pass.
|
|
4080
|
+
def generic_captured_bindings(block_node, expected)
|
|
4081
|
+
captured_block_bindings(block_node, expected)
|
|
4082
|
+
rescue StandardError
|
|
4083
|
+
nil
|
|
4084
|
+
end
|
|
4085
|
+
|
|
3128
4086
|
# `&:symbol` desugars to a one-arg Proc that dispatches `symbol` against its argument. When the param
|
|
3129
4087
|
# type is known and the resulting inner dispatch is precise, this returns the precise carrier;
|
|
3130
4088
|
# otherwise it returns `Dynamic[Top]` (still non-nil) so the outer dispatcher selects the
|
|
@@ -3157,13 +4115,170 @@ module Rigor
|
|
|
3157
4115
|
# — which threads scope statement by statement through `StatementEvaluator` — held `42` for the very
|
|
3158
4116
|
# same node. {#threaded_block_body_type} closes that gap by reusing the main pass's evaluator; a decline
|
|
3159
4117
|
# keeps the tail-only answer verbatim.
|
|
3160
|
-
|
|
4118
|
+
#
|
|
4119
|
+
# Issue #841: the fall-through tail is only ONE of the block's exits. Every `next value` that leaves this
|
|
4120
|
+
# block makes `value` the block's result for that invocation, so the arms join into the value type the
|
|
4121
|
+
# same way {#evaluate_body_with_returns} joins a method's early `return`s with its tail. Without the
|
|
4122
|
+
# join, `ops.all? { |o| next false unless o; true }` read as `Constant[true]` and
|
|
4123
|
+
# {MethodDispatcher::BlockFolding} folded the call to always-truthy on a program that really can answer
|
|
4124
|
+
# false — a warning on correct code.
|
|
4125
|
+
#
|
|
4126
|
+
# `captured` is the #587 (b) binding `block_scope` was laid over, if any; the names it answers are left to it
|
|
4127
|
+
# ({#tail_only_block_body_type}).
|
|
4128
|
+
def type_block_body(block_node, block_scope, captured: nil)
|
|
3161
4129
|
body = block_node.body
|
|
3162
4130
|
return Type::Combinator.constant_of(nil) if body.nil?
|
|
3163
4131
|
|
|
3164
|
-
|
|
4132
|
+
arms = block_level_next_arms(body)
|
|
4133
|
+
return block_body_type_joining_nexts(body, block_scope, arms) if arms
|
|
4134
|
+
|
|
4135
|
+
threaded_block_body_type(body, block_scope) || tail_only_block_body_type(body, block_scope, captured)
|
|
4136
|
+
end
|
|
4137
|
+
|
|
4138
|
+
# The tail typed in the entry scope — except under {#block_body_threading_suppressed?}, where a tail that
|
|
4139
|
+
# reads what its own prefix changed would get the ENTRY binding back, a stale answer rather than a wider
|
|
4140
|
+
# one. There the tail is typed over {#prefix_answered_scope}, which re-answers exactly those names without
|
|
4141
|
+
# evaluating the prefix. The threading's other declines keep the plain tail-only answer: a body with no
|
|
4142
|
+
# prefix, or whose tail ignores it, is not stale, while a `rescue` body and a prefix that can `break`
|
|
4143
|
+
# ({JUMP_NODES}) are typed tail-only with or without the suppression, and neither path re-answers them.
|
|
4144
|
+
#
|
|
4145
|
+
# A failure in the re-answer falls back to the plain tail-only answer, for the reason
|
|
4146
|
+
# {#threaded_block_body_type} gives: a raise reaching `block_return_type_for` would report "no block".
|
|
4147
|
+
def tail_only_block_body_type(body, block_scope, captured)
|
|
4148
|
+
return block_scope.type_of(body) unless block_body_threading_suppressed? && body.is_a?(Prism::StatementsNode)
|
|
4149
|
+
|
|
4150
|
+
answered =
|
|
4151
|
+
begin
|
|
4152
|
+
prefix_answered_scope(body.body, block_scope, captured)
|
|
4153
|
+
rescue StandardError
|
|
4154
|
+
block_scope
|
|
4155
|
+
end
|
|
4156
|
+
answered.type_of(body)
|
|
4157
|
+
end
|
|
4158
|
+
|
|
4159
|
+
# `block_scope` with every name the tail reads and the prefix changed ({#tail_dependent_body_names}) bound to
|
|
4160
|
+
# what the prefix can have left in it, answered without evaluating the prefix — which is the cost the
|
|
4161
|
+
# suppression refuses — on the terms the per-element fold's captured binding answers a capture under the same
|
|
4162
|
+
# suppression:
|
|
4163
|
+
#
|
|
4164
|
+
# - a name the prefix REBINDS reads `Dynamic[top]` ({#captured_floor}'s answer). `i += w; i` inside a block
|
|
4165
|
+
# the call runs once read `i` at its entry `0`, and `k == 0` then fired always-truthy on a `k` Ruby holds
|
|
4166
|
+
# as `1`.
|
|
4167
|
+
# - a name the prefix only MUTATES IN PLACE reads its unknown-store widening ({UnknownStoreWidening.widen}),
|
|
4168
|
+
# the binding {#stored_capture_bindings} lays: `|_k, a| a << w; a` over `{ x: [], y: [] }` read `a` as its
|
|
4169
|
+
# entry `[]` and the call as `Array[[]]`; it now reads `Array[Array[Dynamic[top]]]`.
|
|
4170
|
+
#
|
|
4171
|
+
# That is a floor per NAME, not per block, so the structure around a floored name survives (`e = e.to_s; [e, w]`
|
|
4172
|
+
# keeps its Tuple), and it is limited to the names tail-only answers stale. A name the entry scope does not bind —
|
|
4173
|
+
# a body-local, or an instance variable, class variable or global nothing bound yet — already reads
|
|
4174
|
+
# `Dynamic[top]`. An instance variable on its ADR-58 class-wide seed takes no floor for a rebind, since the seed
|
|
4175
|
+
# is the union of every write in the class, this prefix's included, though its mutation sites still widen it
|
|
4176
|
+
# ({#class_seeded_ivar?}). A name whose widening declines keeps its entry binding, because the threaded body
|
|
4177
|
+
# would have kept it as well: `s = String.new; … { s << "x"; s }` is `String` either way, and a precise nominal
|
|
4178
|
+
# `Array[String]` is a claim the widening may not grow on either path. And a name the #587 (b) `captured` binding
|
|
4179
|
+
# answers is left to it, as {#unanswered_tail_dependency?} leaves it: the per-element fold computes that binding
|
|
4180
|
+
# before it suppresses the threading above its cap, so it can hold the fixpoint's converged `Integer` for
|
|
4181
|
+
# `total += e; total`, which a floor here would throw away.
|
|
4182
|
+
def prefix_answered_scope(statements, block_scope, captured)
|
|
4183
|
+
return block_scope if statements.size < 2
|
|
4184
|
+
|
|
4185
|
+
names = tail_dependent_body_names(statements) - (captured&.names || EMPTY_NAME_SET)
|
|
4186
|
+
return block_scope if names.empty?
|
|
4187
|
+
|
|
4188
|
+
rebound, sites = prefix_changes(statements)
|
|
4189
|
+
names.reduce(block_scope) do |acc, name|
|
|
4190
|
+
answer = prefix_left_binding(block_scope, name, rebound, sites)
|
|
4191
|
+
answer ? CapturedLocals.bind(acc, name, answer) : acc
|
|
4192
|
+
end
|
|
4193
|
+
end
|
|
4194
|
+
|
|
4195
|
+
def prefix_left_binding(block_scope, name, rebound, sites)
|
|
4196
|
+
entry = CapturedLocals.bound_type(block_scope, name)
|
|
4197
|
+
return nil if entry.nil?
|
|
4198
|
+
|
|
4199
|
+
return Type::Combinator.untyped if rebound.include?(name) && !class_seeded_ivar?(block_scope, name)
|
|
4200
|
+
|
|
4201
|
+
widened = UnknownStoreWidening.widen(entry, sites.fetch(name, NO_MUTATION_SITES))
|
|
4202
|
+
widened == entry ? nil : widened
|
|
4203
|
+
end
|
|
4204
|
+
|
|
4205
|
+
# An instance variable still on its ADR-58 class-wide seed: the union of every WRITE in the class, so a rebind
|
|
4206
|
+
# in the prefix is already in it and needs no floor. An in-place mutation is no write — `@out << w.to_s` leaves
|
|
4207
|
+
# the seed `"k"` while the object holds `"k1"` — so the mutation sites still widen the seed, as they widen any
|
|
4208
|
+
# other entry binding.
|
|
4209
|
+
def class_seeded_ivar?(block_scope, name)
|
|
4210
|
+
CapturedLocals.ivar_name?(name) && block_scope.declaration_sourced?(:ivar, name)
|
|
4211
|
+
end
|
|
4212
|
+
|
|
4213
|
+
NO_MUTATION_SITES = [].freeze
|
|
4214
|
+
private_constant :NO_MUTATION_SITES
|
|
4215
|
+
|
|
4216
|
+
# The prefix's changes split the way {#prefix_left_binding} answers them: the names a write node rebinds,
|
|
4217
|
+
# and each in-place mutation site filed under every variable its receiver can evaluate to. The walk and both
|
|
4218
|
+
# predicates are {#prefix_statement_jump_free?}'s, so a name {#tail_dependent_body_names} reports is always
|
|
4219
|
+
# filed here under one of the two. Only a suppressed tail-only body with a dependent tail pays it.
|
|
4220
|
+
def prefix_changes(statements)
|
|
4221
|
+
rebound = Set.new
|
|
4222
|
+
sites = {}
|
|
4223
|
+
statements[0...-1].each do |statement|
|
|
4224
|
+
Source::NodeWalker.each_with_ancestors(statement) do |node, ancestors|
|
|
4225
|
+
rebound << node.name if VARIABLE_WRITE_NODES.include?(node.class)
|
|
4226
|
+
next unless in_place_mutation?(node)
|
|
4227
|
+
|
|
4228
|
+
nested = ancestors.any? { |ancestor| CLOSURE_NODES.include?(ancestor.class) }
|
|
4229
|
+
each_mutated_name(node, nested) { |name| (sites[name] ||= []) << node }
|
|
4230
|
+
end
|
|
4231
|
+
end
|
|
4232
|
+
[rebound, sites]
|
|
4233
|
+
end
|
|
4234
|
+
|
|
4235
|
+
# Evaluates the body once under a `next` sink and joins the arms that leave THIS block with the
|
|
4236
|
+
# fall-through tail the same evaluation produced. Both halves come from one `StatementEvaluator` run, so
|
|
4237
|
+
# each arm is typed in the scope that actually reaches it — a `next y` after `y = "s"` contributes
|
|
4238
|
+
# `"s"`, not the entry scope's stale binding.
|
|
4239
|
+
#
|
|
4240
|
+
# This run ignores the {#block_body_threading_suppressed?} flag that gates the precision fold: that flag
|
|
4241
|
+
# bounds the cost of an OPTIONAL widening, while skipping the join re-opens a false-positive class. It
|
|
4242
|
+
# still SETS the flag for the duration, so nothing folds below it and the cost stays one evaluation per
|
|
4243
|
+
# block-level `next` — a shape that is a small minority of blocks.
|
|
4244
|
+
#
|
|
4245
|
+
# A failure falls back to the tail-only answer rather than propagating, for the same reason
|
|
4246
|
+
# {#threaded_block_body_type} does: the enclosing `block_return_type_for` rescue would otherwise report
|
|
4247
|
+
# "no block" to the dispatcher, a far larger regression than a wide block return.
|
|
4248
|
+
def block_body_type_joining_nexts(body, block_scope, arms)
|
|
4249
|
+
(fall_through, _exit_scope), collected = StatementEvaluator.with_next_sink do
|
|
4250
|
+
without_block_body_threading { block_scope.evaluate(body) }
|
|
4251
|
+
end
|
|
4252
|
+
joined = collected.filter_map { |node, type| type if arms.key?(node) }
|
|
4253
|
+
joined.empty? ? fall_through : Type::Combinator.union(fall_through, *joined)
|
|
4254
|
+
rescue StandardError
|
|
4255
|
+
block_scope.type_of(body)
|
|
4256
|
+
end
|
|
4257
|
+
|
|
4258
|
+
# The identity-keyed set of `next` nodes whose value is THIS block's value — reachable from the body
|
|
4259
|
+
# without crossing a {JUMP_BOUNDARY_NODES} construct that retargets them — or nil for "do not join".
|
|
4260
|
+
#
|
|
4261
|
+
# nil means "do not join": a body with no block-level `next` keeps today's path exactly, and pays only
|
|
4262
|
+
# the allocation-free scan that answers so. A co-resident block-level `break` used to decline here as
|
|
4263
|
+
# well, because its value was dropped and joining the `next` arms would have dressed a still-incomplete
|
|
4264
|
+
# answer as a complete one; since issue #853 the `break` arms are unioned into the yielding call
|
|
4265
|
+
# ({#call_break_arm_types}), so the two exits are modelled at their own levels and neither blocks the
|
|
4266
|
+
# other.
|
|
4267
|
+
def block_level_next_arms(body)
|
|
4268
|
+
return nil unless block_level_jump?(body, Prism::NextNode)
|
|
4269
|
+
|
|
4270
|
+
block_level_jump_nodes(body, Prism::NextNode)
|
|
3165
4271
|
end
|
|
3166
4272
|
|
|
4273
|
+
# True when a `klass` jump is reachable from `node` without crossing a construct that retargets it
|
|
4274
|
+
# ({JumpTargets.any?}, allocation-free and early-exiting: this is the scan every block body pays).
|
|
4275
|
+
def block_level_jump?(node, klass) = JumpTargets.any?(node, klass)
|
|
4276
|
+
|
|
4277
|
+
# The identity-keyed set of `klass` jumps that target THIS block ({JumpTargets.of}). The sinks in
|
|
4278
|
+
# `StatementEvaluator` also collect jumps from nested blocks / loops / defs evaluated under the same
|
|
4279
|
+
# installation, so the consumer filters against this set by node identity.
|
|
4280
|
+
def block_level_jump_nodes(body, klass) = JumpTargets.of(body, klass)
|
|
4281
|
+
|
|
3167
4282
|
# Re-typing the whole body would be wrong to do unconditionally: this path runs for EVERY block-bearing
|
|
3168
4283
|
# call, and the statements ahead of the tail are pure cost whenever the tail does not depend on them.
|
|
3169
4284
|
# Three declines keep that cost where the defect actually is, each falling back to the tail-only path:
|
|
@@ -3177,8 +4292,11 @@ module Rigor
|
|
|
3177
4292
|
# - the fold is not re-entrant. `StatementEvaluator#eval_call` already evaluates each nested block body
|
|
3178
4293
|
# once, plus up to three more times under the ADR-56 `BodyFixpoint` when the block rebinds a captured
|
|
3179
4294
|
# local, so a fold nested inside a fold would multiply that work per block-nesting level. Inside a
|
|
3180
|
-
# threaded body a nested block-bearing call reverts to the tail-only path
|
|
3181
|
-
#
|
|
4295
|
+
# threaded body a nested block-bearing call reverts to the tail-only path. That is a wider answer only
|
|
4296
|
+
# while the nested tail ignores its own prefix: a tail reading a parameter or captured local the prefix
|
|
4297
|
+
# mutated gets the ENTRY binding back, so the per-element and per-pair folds floor that shape
|
|
4298
|
+
# ({#tail_only_body_floor}), and the generic block-return pass re-answers each stale name on its own
|
|
4299
|
+
# ({#prefix_answered_scope}).
|
|
3182
4300
|
#
|
|
3183
4301
|
# ADR-56 interaction: the fold cannot double-apply or fight the captured-local write-back. That
|
|
3184
4302
|
# write-back is `StatementEvaluator#write_back_block_captures`, computed from the CALLER's scope into
|
|
@@ -3242,32 +4360,40 @@ module Rigor
|
|
|
3242
4360
|
private_constant :VARIABLE_WRITE_NODES
|
|
3243
4361
|
|
|
3244
4362
|
# Every node that OBSERVES a variable binding: the plain reads plus the compound writes, which read
|
|
3245
|
-
# their target before rebinding it (`v += 1` in the tail depends on an earlier `v = 0`).
|
|
4363
|
+
# their target before rebinding it (`v += 1` in the tail depends on an earlier `v = 0`). An `it` read
|
|
4364
|
+
# observes the local `:it` ({ReceiverAlias.read_name}); it has no `name` of its own.
|
|
3246
4365
|
VARIABLE_READ_NODES = (
|
|
3247
4366
|
VARIABLE_WRITE_NODES | [
|
|
3248
|
-
Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode,
|
|
4367
|
+
Prism::LocalVariableReadNode, Prism::ItLocalVariableReadNode, Prism::InstanceVariableReadNode,
|
|
3249
4368
|
Prism::ClassVariableReadNode, Prism::GlobalVariableReadNode
|
|
3250
4369
|
]
|
|
3251
4370
|
).freeze
|
|
3252
4371
|
private_constant :VARIABLE_READ_NODES
|
|
3253
4372
|
|
|
3254
4373
|
# A `next` / `break` that leaves THIS block carries a value the fold cannot see: `evaluate(body).first`
|
|
3255
|
-
# is the fall-through value only
|
|
3256
|
-
#
|
|
3257
|
-
#
|
|
3258
|
-
#
|
|
4374
|
+
# is the fall-through value only.
|
|
4375
|
+
#
|
|
4376
|
+
# The `next` entry no longer fires: {#type_block_body} routes such a body past this scan entirely, since
|
|
4377
|
+
# issue #841 joins those arms into the block's value. It is kept because the question this predicate
|
|
4378
|
+
# answers ("can this prefix escape with a value the fall-through misses?") is the caller's premise
|
|
4379
|
+
# rather than the caller's filter restated.
|
|
4380
|
+
#
|
|
4381
|
+
# The `break` entry still fires, and since issue #853 it is conservative rather than load-bearing: the
|
|
4382
|
+
# arms are unioned into the CALL ({#call_break_arm_types}), so the missing value is no longer missing.
|
|
4383
|
+
# What the decline still costs is the threaded tail — `m.synchronize do break 5 if flag; v = 42; v end`
|
|
4384
|
+
# answers `5 | Dynamic[top]` where threading would reach `5 | 42`. Lifting it widens no answer and
|
|
4385
|
+
# invents no fact, but it moves the type of every block that carries a `break`, so it is left to a
|
|
4386
|
+
# change that can measure that.
|
|
3259
4387
|
JUMP_NODES = Set[Prism::NextNode, Prism::BreakNode].freeze
|
|
3260
4388
|
private_constant :JUMP_NODES
|
|
3261
4389
|
|
|
3262
4390
|
# Constructs that RETARGET a `next` / `break` nested inside them, so a jump below one of these says
|
|
3263
|
-
# nothing about our block's value
|
|
3264
|
-
# is the jump's own block (`do xs.each { next 1 }; v = 42; v end` threads
|
|
3265
|
-
# ends the inner iteration); a loop consumes both forms (`while … next 5 …
|
|
3266
|
-
# a `DefNode` body is a different method entirely.
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
Prism::WhileNode, Prism::UntilNode, Prism::ForNode
|
|
3270
|
-
].freeze
|
|
4391
|
+
# nothing about our block's value — it neither triggers the decline nor joins as an arm. A nested
|
|
4392
|
+
# `BlockNode` / `LambdaNode` is the jump's own block (`do xs.each { next 1 }; v = 42; v end` threads
|
|
4393
|
+
# soundly — the inner `next` ends the inner iteration); a loop consumes both forms (`while … next 5 …
|
|
4394
|
+
# end` continues the loop); a `DefNode` body is a different method entirely. The set is
|
|
4395
|
+
# {JumpTargets::BOUNDARY_NODES}, shared with `StatementEvaluator`'s loop and block joins.
|
|
4396
|
+
JUMP_BOUNDARY_NODES = JumpTargets::BOUNDARY_NODES
|
|
3271
4397
|
private_constant :JUMP_BOUNDARY_NODES
|
|
3272
4398
|
|
|
3273
4399
|
# True when the tail statement observes a variable name one of the earlier statements binds OR mutates
|
|
@@ -3282,9 +4408,13 @@ module Rigor
|
|
|
3282
4408
|
# hands downstream rules a provably-empty array. Threading is the fix, not a cost: `StatementEvaluator`
|
|
3283
4409
|
# runs `MutationWidening.widen_after_call` on the `push`, so the threaded tail reads the widened
|
|
3284
4410
|
# `Array[…]`. A call therefore contributes every variable its receiver can evaluate to
|
|
3285
|
-
# ({ReceiverAlias.
|
|
3286
|
-
# is one the widening responds to ({MutationWidening::SHAPE_MUTATORS}); keying on the widening's own
|
|
3287
|
-
# tables is what keeps "the scan says thread" and "threading changes something" the
|
|
4411
|
+
# ({ReceiverAlias.mutated_reads} — the ternary-selected receiver of issue #277 included) whenever its
|
|
4412
|
+
# name is one the widening responds to ({MutationWidening::SHAPE_MUTATORS}); keying on the widening's own
|
|
4413
|
+
# tables and receiver answer is what keeps "the scan says thread" and "threading changes something" the
|
|
4414
|
+
# same predicate. The index-write nodes ({INDEX_WRITE_NODES}) store through `[]=` without being a call,
|
|
4415
|
+
# so a name-keyed scan missed them and `h[:a] += 1; h[:a]` kept the literal's `0`; they contribute their
|
|
4416
|
+
# receiver the same way. A scan that read only local and instance-variable receivers missed `$g << w; $g`
|
|
4417
|
+
# and `it << w; it` the same way, while the tail kept the entry `"k"` / `[]`.
|
|
3288
4418
|
#
|
|
3289
4419
|
# Cost is two walks of the body, the second only when the first found a write and no jump — the same
|
|
3290
4420
|
# order of cost `StatementEvaluator`'s own per-call captured-write scan already pays, and far below
|
|
@@ -3293,53 +4423,115 @@ module Rigor
|
|
|
3293
4423
|
# so `[1].each { v = 5 }` really does bind the outer `v`, and `[1].each { outer << 1 }` really does
|
|
3294
4424
|
# mutate the outer `outer`), while a jump counts only above the nearest {JUMP_BOUNDARY_NODES} boundary.
|
|
3295
4425
|
def tail_depends_on_body_binding?(statements)
|
|
3296
|
-
written =
|
|
3297
|
-
|
|
3298
|
-
return false unless prefix_statement_jump_free?(statement, written, false)
|
|
3299
|
-
end
|
|
3300
|
-
return false if written.empty?
|
|
4426
|
+
written = prefix_written_names(statements)
|
|
4427
|
+
return false if written.nil?
|
|
3301
4428
|
|
|
3302
4429
|
Source::NodeWalker.each(statements.last) do |node|
|
|
3303
|
-
return true if VARIABLE_READ_NODES.include?(node.class) && written.include?(node
|
|
4430
|
+
return true if VARIABLE_READ_NODES.include?(node.class) && written.include?(ReceiverAlias.read_name(node))
|
|
3304
4431
|
end
|
|
3305
4432
|
false
|
|
3306
4433
|
end
|
|
3307
4434
|
|
|
4435
|
+
# WHICH names the predicate above answers YES on — every name the tail observes that the prefix binds
|
|
4436
|
+
# or mutates in place. Only the tail-only floor ({#unanswered_tail_dependency?}) needs them, which is
|
|
4437
|
+
# why the predicate is not written over this method: the predicate runs for every multi-statement block
|
|
4438
|
+
# body and short-circuits on the first hit, the floor runs only for a walk typed tail-only. Sharing
|
|
4439
|
+
# {#prefix_written_names} is what keeps the two from drifting about what the prefix binds.
|
|
4440
|
+
def tail_dependent_body_names(statements)
|
|
4441
|
+
written = prefix_written_names(statements)
|
|
4442
|
+
return EMPTY_NAME_SET if written.nil?
|
|
4443
|
+
|
|
4444
|
+
Source::NodeWalker.each(statements.last).filter_map do |node|
|
|
4445
|
+
next unless VARIABLE_READ_NODES.include?(node.class)
|
|
4446
|
+
|
|
4447
|
+
name = ReceiverAlias.read_name(node)
|
|
4448
|
+
name if written.include?(name)
|
|
4449
|
+
end.to_set
|
|
4450
|
+
end
|
|
4451
|
+
|
|
4452
|
+
# The names the prefix binds or mutates in place, or `nil` when the scan declines — a prefix that can
|
|
4453
|
+
# jump out of the block with a value the fall-through misses, or one that binds nothing at all.
|
|
4454
|
+
def prefix_written_names(statements)
|
|
4455
|
+
written = Set.new
|
|
4456
|
+
statements[0...-1].each do |statement|
|
|
4457
|
+
return nil unless prefix_statement_jump_free?(statement, written, false, false)
|
|
4458
|
+
end
|
|
4459
|
+
return nil if written.empty?
|
|
4460
|
+
|
|
4461
|
+
written
|
|
4462
|
+
end
|
|
4463
|
+
|
|
4464
|
+
EMPTY_NAME_SET = Set.new.freeze
|
|
4465
|
+
private_constant :EMPTY_NAME_SET
|
|
4466
|
+
|
|
3308
4467
|
# True when `node` cannot jump out of the block with a value, collecting into `written` the names it
|
|
3309
|
-
# binds (a variable-write node) or mutates in place (a {MutationWidening::SHAPE_MUTATORS} call
|
|
3310
|
-
# every variable its receiver can evaluate to) on the way down.
|
|
3311
|
-
# has passed a boundary
|
|
4468
|
+
# binds (a variable-write node) or mutates in place (a {MutationWidening::SHAPE_MUTATORS} call or an
|
|
4469
|
+
# {INDEX_WRITE_NODES} store, through every variable its receiver can evaluate to) on the way down.
|
|
4470
|
+
# `retargeted` is true once the descent has passed a boundary, and `nested` once it has passed a block or
|
|
4471
|
+
# lambda ({CLOSURE_NODES}), whose `it` is not the body's.
|
|
3312
4472
|
#
|
|
3313
4473
|
# A `Prism::DefinedNode`'s operand is never evaluated, so it is not descended into — the same rule
|
|
3314
4474
|
# {Source::NodeWalker} applies, for the same reason: neither a write nor a jump under `defined?` runs.
|
|
3315
|
-
def prefix_statement_jump_free?(node, written, retargeted)
|
|
4475
|
+
def prefix_statement_jump_free?(node, written, retargeted, nested)
|
|
3316
4476
|
return false if !retargeted && JUMP_NODES.include?(node.class)
|
|
3317
4477
|
|
|
3318
4478
|
written << node.name if VARIABLE_WRITE_NODES.include?(node.class)
|
|
3319
|
-
|
|
4479
|
+
each_mutated_name(node, nested) { |name| written << name } if in_place_mutation?(node)
|
|
3320
4480
|
return true if node.is_a?(Prism::DefinedNode)
|
|
3321
4481
|
|
|
3322
4482
|
child_retargeted = retargeted || JUMP_BOUNDARY_NODES.include?(node.class)
|
|
4483
|
+
child_nested = nested || CLOSURE_NODES.include?(node.class)
|
|
3323
4484
|
node.rigor_each_child do |child|
|
|
3324
|
-
return false unless prefix_statement_jump_free?(child, written, child_retargeted)
|
|
4485
|
+
return false unless prefix_statement_jump_free?(child, written, child_retargeted, child_nested)
|
|
3325
4486
|
end
|
|
3326
4487
|
true
|
|
3327
4488
|
end
|
|
3328
4489
|
|
|
3329
|
-
|
|
3330
|
-
|
|
4490
|
+
# The forms that store through `[]=` without being a `[]=` call ({IndexWriteWidening::CONTENT_WRITE_NODE_CLASSES}).
|
|
4491
|
+
# `StatementEvaluator` widens the three compound writes in straight-line code, and its captured-local
|
|
4492
|
+
# write-back widens all four when a nested block stores through one. A straight-line index TARGET widens
|
|
4493
|
+
# too, at the multi-assign, `for` index or `rescue =>` reference that owns it (`eval_multi_write`,
|
|
4494
|
+
# `bind_for_index`, `bind_rescue_reference`).
|
|
4495
|
+
INDEX_WRITE_NODES = Set.new(IndexWriteWidening::CONTENT_WRITE_NODE_CLASSES).freeze
|
|
4496
|
+
private_constant :INDEX_WRITE_NODES
|
|
4497
|
+
|
|
4498
|
+
def in_place_mutation?(node)
|
|
4499
|
+
return MutationWidening::SHAPE_MUTATORS.include?(node.name) if node.is_a?(Prism::CallNode)
|
|
4500
|
+
|
|
4501
|
+
INDEX_WRITE_NODES.include?(node.class)
|
|
4502
|
+
end
|
|
4503
|
+
|
|
4504
|
+
# Yields the name of every variable the in-place mutation `node` changes: its receiver's
|
|
4505
|
+
# {ReceiverAlias.mutated_reads}, the answer the straight-line widening the threaded body runs reads too, so a
|
|
4506
|
+
# global or class variable counts (`$g << w; $g`) as well as a local, an instance variable and the `it`
|
|
4507
|
+
# parameter. An `it` read `nested` under a block or lambda inside the prefix is that closure's own parameter,
|
|
4508
|
+
# never the body's `it`, so it names nothing here: `[[]].each { it << w }; it` leaves the body's `it` as it was.
|
|
4509
|
+
def each_mutated_name(node, nested)
|
|
4510
|
+
ReceiverAlias.mutated_reads(node.receiver).each do |read|
|
|
4511
|
+
next if nested && read.is_a?(Prism::ItLocalVariableReadNode)
|
|
3331
4512
|
|
|
3332
|
-
|
|
4513
|
+
yield ReceiverAlias.read_name(read)
|
|
4514
|
+
end
|
|
3333
4515
|
end
|
|
3334
4516
|
|
|
4517
|
+
# The closures whose `it` is their own: a nested block or lambda always binds `it` to its own parameter.
|
|
4518
|
+
CLOSURE_NODES = Set[Prism::BlockNode, Prism::LambdaNode].freeze
|
|
4519
|
+
private_constant :CLOSURE_NODES
|
|
4520
|
+
|
|
3335
4521
|
# v0.0.6 phase 2 — per-element block fold for Tuple receivers under `:map` / `:collect`. Walks every
|
|
3336
4522
|
# Tuple position, binds the block parameter to that element's type, and re-types the block body. The
|
|
3337
4523
|
# per-position results are assembled into `Tuple[U_1..U_n]`, strictly tighter than the RBS-projected
|
|
3338
4524
|
# `Array[union]`.
|
|
3339
4525
|
#
|
|
3340
4526
|
# Declines (returns nil) when the receiver is not a `Tuple` with at least one element, when the call
|
|
3341
|
-
# has no `Prism::BlockNode`, when the method is outside the supported set, when
|
|
3342
|
-
# mid-loop, or when the block has no body. The decline path leaves the
|
|
4527
|
+
# has no `Prism::BlockNode`, when the method is outside the supported set, when the call carries an
|
|
4528
|
+
# argument, when block typing raises mid-loop, or when the block has no body. The decline path leaves the
|
|
4529
|
+
# dispatch chain untouched.
|
|
4530
|
+
#
|
|
4531
|
+
# The walk reads only the block, so an argument declines: `index(value)` / `find_index(value)` search by
|
|
4532
|
+
# `==` without running the block, and `find(ifnone)` / `detect(ifnone)` answer `ifnone.call` when no
|
|
4533
|
+
# position matches. `r = [1, 2].find(-> { 0 }) { |e| e > 5 }` is `0` at runtime; the walk answered `nil`,
|
|
4534
|
+
# and `r + 1` then reported a nil receiver on correct code. The other supported methods take no argument.
|
|
3343
4535
|
PER_ELEMENT_TUPLE_METHODS = Set[
|
|
3344
4536
|
:map, :collect, :filter_map, :flat_map,
|
|
3345
4537
|
:select, :filter, :reject,
|
|
@@ -3353,6 +4545,9 @@ module Rigor
|
|
|
3353
4545
|
].freeze
|
|
3354
4546
|
private_constant :HASH_SHAPE_TRANSFORM_METHODS
|
|
3355
4547
|
|
|
4548
|
+
IN_PLACE_HASH_SHAPE_TRANSFORMS = Set[:transform_keys!, :transform_values!].freeze
|
|
4549
|
+
private_constant :IN_PLACE_HASH_SHAPE_TRANSFORMS
|
|
4550
|
+
|
|
3356
4551
|
# Cardinality cap for per-element block fold over finite-bound `Constant<Range>` receivers. Walking
|
|
3357
4552
|
# `(1..1_000_000).map { … }` element-wise would balloon block-typing cost and explode the resulting
|
|
3358
4553
|
# Tuple, so only short ranges expand into per-position folds. Larger ranges decline so the RBS tier
|
|
@@ -3362,7 +4557,7 @@ module Rigor
|
|
|
3362
4557
|
|
|
3363
4558
|
def try_per_element_block_fold(call_node, receiver_type)
|
|
3364
4559
|
return nil unless PER_ELEMENT_TUPLE_METHODS.include?(call_node.name)
|
|
3365
|
-
return nil
|
|
4560
|
+
return nil unless call_node.arguments.nil?
|
|
3366
4561
|
|
|
3367
4562
|
element_types = per_element_elements_of(receiver_type)
|
|
3368
4563
|
return nil if element_types.nil? || element_types.empty?
|
|
@@ -3370,7 +4565,59 @@ module Rigor
|
|
|
3370
4565
|
per_position = per_element_block_results(call_node.block, element_types)
|
|
3371
4566
|
return nil if per_position.nil? || per_position.any?(&:nil?)
|
|
3372
4567
|
|
|
3373
|
-
assemble_per_element_result(call_node.name, per_position, element_types)
|
|
4568
|
+
assemble_per_element_result(call_node.name, per_position, element_types) ||
|
|
4569
|
+
undecided_fold_floor(call_node.name, element_types, receiver_type)
|
|
4570
|
+
end
|
|
4571
|
+
|
|
4572
|
+
# The honest answer for the find family (`find` / `detect` / `find_index` / `index`) and the filter family
|
|
4573
|
+
# (`select` / `filter` / `reject`) when this fold walked every position and the assembler still could not
|
|
4574
|
+
# decide — which happens for exactly one reason: some position's predicate is not a `Constant`, so "the
|
|
4575
|
+
# first matching one" or "the ones kept" is not a static fact.
|
|
4576
|
+
#
|
|
4577
|
+
# Falling through to the dispatcher was WRONG for this family, and issue #617 residue (1) is the bill:
|
|
4578
|
+
# `seen = 0; [1, 2].find do |e| seen += 1; seen == 2 end` answered `nil` where the runtime answers `2`.
|
|
4579
|
+
# The dispatcher's `BlockFolding` reads ONE block-return type, typed from the call's ENTRY scope, so a
|
|
4580
|
+
# predicate over a rebound capture pins the first iteration (`Constant[false]`) and
|
|
4581
|
+
# `FALSEY_BLOCK_NIL_METHODS` short-circuits the whole call to `nil`. This walk already knows better: it
|
|
4582
|
+
# typed the predicate per position and saw that it does not fold. Answering here is what keeps that
|
|
4583
|
+
# knowledge from being thrown away in favour of a worse-informed tier.
|
|
4584
|
+
#
|
|
4585
|
+
# The floor is what `find` can return and no tighter: one of the receiver's own elements, or `nil` when
|
|
4586
|
+
# no element matches. Value pinning survives because it is still true of every candidate — `find` hands
|
|
4587
|
+
# back an element, it does not compute one. `find_index` / `index` answer a position in the receiver, so
|
|
4588
|
+
# `Integer?` is their floor.
|
|
4589
|
+
#
|
|
4590
|
+
# `select` / `filter` / `reject` take a floor for the same reason. Their fall-through is not merely
|
|
4591
|
+
# wider: `BlockFolding`'s filter folds DO answer on a `Constant` block, and the entry-scope pin hands
|
|
4592
|
+
# them one, so `seen = 0; [1, 2].select do |e| seen += 1; seen == 2 end` answered a provably-empty `[]`
|
|
4593
|
+
# where Ruby answers `[2]`. Nested inside a threaded body, where a position can be typed tail-only
|
|
4594
|
+
# ({#tail_only_body_floor}), `[[], []].select do |a| a << w; a.any? end` did the same. Their floor is an
|
|
4595
|
+
# Array of the receiver's own elements: which of them survive is undecided, and what they are is not.
|
|
4596
|
+
# A range receiver's elements are widened first ({#filter_family_floor}).
|
|
4597
|
+
#
|
|
4598
|
+
# Nothing else in {PER_ELEMENT_TUPLE_METHODS} takes a floor. `map`'s assembler cannot decline, and
|
|
4599
|
+
# `BlockFolding` never folds `filter_map` / `flat_map`, so their fall-through is the RBS `Array[U]`
|
|
4600
|
+
# projection.
|
|
4601
|
+
def undecided_fold_floor(method_name, element_types, receiver_type)
|
|
4602
|
+
case method_name
|
|
4603
|
+
when :find, :detect
|
|
4604
|
+
Type::Combinator.union(*element_types, Type::Combinator.constant_of(nil))
|
|
4605
|
+
when :select, :filter, :reject
|
|
4606
|
+
filter_family_floor(element_types, receiver_type)
|
|
4607
|
+
when :find_index, :index
|
|
4608
|
+
Type::Combinator.union(Type::Combinator.nominal_of("Integer"), Type::Combinator.constant_of(nil))
|
|
4609
|
+
end
|
|
4610
|
+
end
|
|
4611
|
+
|
|
4612
|
+
# A `Constant<Range>` receiver's elements are values this walk enumerated, not ones the program wrote, and
|
|
4613
|
+
# the RBS answer the floor replaces carried no pin. Kept, the pins would make `(1..4).filter { … }` an
|
|
4614
|
+
# `Array[1 | 2 | 3 | 4]` that a later `q << 9` cannot widen (#580 leaves a precise Array without a
|
|
4615
|
+
# gradual arm alone), so `q.last == 9` would fold always-falsey on correct code. A Tuple receiver's pins
|
|
4616
|
+
# are the literal's own and stay, as the RBS projection kept them.
|
|
4617
|
+
def filter_family_floor(element_types, receiver_type)
|
|
4618
|
+
element = Type::Combinator.union(*element_types)
|
|
4619
|
+
element = Type::Combinator.widen_value_pinned(element) if receiver_type.is_a?(Type::Constant)
|
|
4620
|
+
Type::Combinator.nominal_of("Array", type_args: [element])
|
|
3374
4621
|
end
|
|
3375
4622
|
|
|
3376
4623
|
# Evaluates the call's block once per receiver element. Two block shapes are supported:
|
|
@@ -3405,13 +4652,86 @@ module Rigor
|
|
|
3405
4652
|
def per_element_body_results(block, element_types)
|
|
3406
4653
|
captured = per_element_captured_bindings(block, element_types)
|
|
3407
4654
|
results = lambda do
|
|
3408
|
-
element_types.map
|
|
4655
|
+
element_types.each_with_index.map do |element_type, position|
|
|
4656
|
+
type_block_body_with_param(block, [element_type], captured: captured, position: position)
|
|
4657
|
+
end
|
|
3409
4658
|
end
|
|
3410
|
-
return results.call
|
|
4659
|
+
return results.call unless tail_only_walk?(element_types)
|
|
4660
|
+
return tail_only_body_floor(element_types) if unanswered_tail_dependency?(block, captured)
|
|
3411
4661
|
|
|
3412
4662
|
without_block_body_threading(&results)
|
|
3413
4663
|
end
|
|
3414
4664
|
|
|
4665
|
+
# A position is typed tail-only in two cases: above {PER_ELEMENT_THREADING_LIMIT}, where this walk
|
|
4666
|
+
# suppresses the threading itself, and anywhere the walk runs nested inside a body some other pass is
|
|
4667
|
+
# already evaluating whole. {#threaded_block_body_type}, the `next` join
|
|
4668
|
+
# ({#block_body_type_joining_nexts}), the break-arm collection ({#collect_break_arm_types}), the
|
|
4669
|
+
# captured-local fixpoint ({#captured_exit_bindings}) and an enclosing walk of this fold above the cap
|
|
4670
|
+
# all suppress the threading while they run, so a fold never re-enters. The second case has no arity in
|
|
4671
|
+
# it: `[[], []].map do |a| a << w; a end` inside a threaded `m.synchronize do w = v; … end` is typed
|
|
4672
|
+
# tail-only at two positions.
|
|
4673
|
+
def tail_only_walk?(element_types)
|
|
4674
|
+
element_types.size > PER_ELEMENT_THREADING_LIMIT || block_body_threading_suppressed?
|
|
4675
|
+
end
|
|
4676
|
+
|
|
4677
|
+
# Wherever a position is typed tail-only ({#tail_only_walk?}), a tail that reads what the body's own
|
|
4678
|
+
# prefix wrote or mutated does not get a wider answer. It gets the ENTRY binding, which the prefix has
|
|
4679
|
+
# already falsified.
|
|
4680
|
+
#
|
|
4681
|
+
# #584's cliff comment promised `Dynamic[top]` above the cap, and that held for a body-LOCAL: `[1, …,
|
|
4682
|
+
# 9].map do v = e; v end` has no entry binding for `v`, so tail-only lands on `Dynamic[top]` by itself,
|
|
4683
|
+
# which is why a body-local alone does not call for the floor. A mutated PARAMETER has one, and issue
|
|
4684
|
+
# #617 residue (2) is what it buys: `([[]] * 9).map do |a| a << 1; a end` answered nine stale `[]`, a
|
|
4685
|
+
# provably-empty array at every position of a result whose slots all hold the one array the nine `<<`
|
|
4686
|
+
# filled. Nested under the suppression the same body did it at two positions, and a `transform_values`
|
|
4687
|
+
# over `{ x: [], y: [] }` did it at every pair ({#tail_only_pairs_floored?}). Flooring every position
|
|
4688
|
+
# restores the promise for the positions; the assembler still decides the call from them, and a method
|
|
4689
|
+
# whose fall-through could read a pin again takes its own floor ({#undecided_fold_floor}). What the cap
|
|
4690
|
+
# and the suppression refuse to pay is the per-position body evaluation, and refusing to pay it means
|
|
4691
|
+
# declining to know, not answering the pre-state.
|
|
4692
|
+
#
|
|
4693
|
+
# The names come from the scan the threading gate uses ({#tail_depends_on_body_binding?}), so "would
|
|
4694
|
+
# threading have changed this tail" and "is tail-only untrustworthy here" stay one question. A body
|
|
4695
|
+
# {#unanswered_tail_dependency?} answers false for keeps its exact fold: every single-statement block,
|
|
4696
|
+
# and every multi-statement block whose tail ignores its prefix, reads only its own body-locals, or
|
|
4697
|
+
# leaves through a `next` (the last is evaluated whole, never tail-only).
|
|
4698
|
+
def tail_only_body_floor(element_types)
|
|
4699
|
+
Array.new(element_types.size) { Type::Combinator.untyped }
|
|
4700
|
+
end
|
|
4701
|
+
|
|
4702
|
+
# True when the tail reads something the prefix changed that NOTHING has re-answered for this walk.
|
|
4703
|
+
#
|
|
4704
|
+
# `captured` is the #587 (b) any-iteration binding of every outer local and instance variable the block
|
|
4705
|
+
# rebinds or mutates in place ({#per_element_captured_bindings}), and its cost is independent of the
|
|
4706
|
+
# arity, so it keeps working above the cap: `total = 0; [1, …, 9].map do total += e; total end` reads
|
|
4707
|
+
# `total` as the fixpoint's `Integer` at every position and needs no floor, and `out = []; … do out << e;
|
|
4708
|
+
# out.size end` reads `out` as the widened `Array[Dynamic[top]]`, whose content carries a gradual arm and
|
|
4709
|
+
# whose arity is open. Under the nesting suppression a rebound name takes the escaping-block floor name by
|
|
4710
|
+
# name, so `[total, e]` keeps its `Tuple` rather than collapsing whole, and the in-place widening, which
|
|
4711
|
+
# evaluates no body, applies as it does anywhere. What a tail-only walk withholds is the per-position body
|
|
4712
|
+
# evaluation, so the names left unanswered are the ones neither binding covers — a mutated block
|
|
4713
|
+
# PARAMETER (issue #617 residue (2)'s `|a| a << 1; a`), and a captured local whose in-place widening
|
|
4714
|
+
# declined, which is left out of `captured` for exactly this reason.
|
|
4715
|
+
#
|
|
4716
|
+
# Two tails are answered without it. A name with no ENTRY binding — a body-local such as `key = k.to_s;
|
|
4717
|
+
# [key, n + w]` — reads as `Dynamic[top]` under tail-only by itself, which is sound, so flooring the
|
|
4718
|
+
# whole position would only erase the structure around it. Instance and global variables always count,
|
|
4719
|
+
# since the scope may still hold a pre-state for them. And a body with a block-level `next` is never
|
|
4720
|
+
# typed tail-only at all: {#block_body_type_joining_nexts} evaluates the whole body whatever the
|
|
4721
|
+
# suppression says.
|
|
4722
|
+
def unanswered_tail_dependency?(block, captured)
|
|
4723
|
+
body = block.body
|
|
4724
|
+
return false unless body.is_a?(Prism::StatementsNode)
|
|
4725
|
+
return false if body.body.size < 2
|
|
4726
|
+
return false if block_level_jump?(body, Prism::NextNode)
|
|
4727
|
+
|
|
4728
|
+
names = tail_dependent_body_names(body.body) - (captured&.names || EMPTY_NAME_SET)
|
|
4729
|
+
return false if names.empty?
|
|
4730
|
+
|
|
4731
|
+
entry = BlockParameterBinder.new.bind_onto(block, scope)
|
|
4732
|
+
names.any? { |name| name.start_with?("@", "$") || !entry.local(name).nil? }
|
|
4733
|
+
end
|
|
4734
|
+
|
|
3415
4735
|
# Issue #587 (b) — first-iteration pinning. Every position of this fold is typed from the SAME entry
|
|
3416
4736
|
# scope, so a body that rebinds a captured outer local answers the FIRST iteration's value at every
|
|
3417
4737
|
# position: `total = 0; [1, 2].map do total += 1; total end` folded to `[1, 1]` (runtime `[1, 2]`), and
|
|
@@ -3424,12 +4744,12 @@ module Rigor
|
|
|
3424
4744
|
# position's entry scope, so a position answers what the local can be in ANY iteration
|
|
3425
4745
|
# (`[Integer, Integer]`), never what it was in the first.
|
|
3426
4746
|
#
|
|
3427
|
-
# Only the
|
|
3428
|
-
# keeps its exact fold (`[5, 5]`, `[42, 42]`), and a predicate that ignores the rebound
|
|
3429
|
-
# decides (`select do seen += 1; e > 1 end` still folds to `[2]`); a blanket decline would
|
|
3430
|
-
# three for nothing. The fixpoint binds the block parameter to the union of the elements,
|
|
3431
|
-
# is independent of the arity — which is why the per-element threading cap is NOT a reason to
|
|
3432
|
-
# ninth element keeps `Integer` where it would otherwise keep the stale `0`.
|
|
4747
|
+
# Only the names the body changes move. A position whose tail reads an untouched captured local or a
|
|
4748
|
+
# block-local keeps its exact fold (`[5, 5]`, `[42, 42]`), and a predicate that ignores the rebound
|
|
4749
|
+
# counter still decides (`select do seen += 1; e > 1 end` still folds to `[2]`); a blanket decline would
|
|
4750
|
+
# have lost all three for nothing. The fixpoint binds the block parameter to the union of the elements,
|
|
4751
|
+
# so its cost is independent of the arity — which is why the per-element threading cap is NOT a reason to
|
|
4752
|
+
# floor: a ninth element keeps `Integer` where it would otherwise keep the stale `0`.
|
|
3433
4753
|
#
|
|
3434
4754
|
# Under threading suppression — this fold nested inside another threaded body — the fixpoint's body
|
|
3435
4755
|
# evaluations are exactly the re-entrant cost the suppression exists to refuse, so the names take the
|
|
@@ -3437,42 +4757,256 @@ module Rigor
|
|
|
3437
4757
|
# fixpoint takes the same floor rather than the seed — a seed that reaches a position is the pin this
|
|
3438
4758
|
# exists to remove.
|
|
3439
4759
|
#
|
|
3440
|
-
#
|
|
4760
|
+
# An instance variable pins the same way — the block shares the caller's `self`, so `@t = 0; [1,
|
|
4761
|
+
# 2].map { @t += 1 }` folded to `[1, 1]` too — and so do a class variable and a global. Each takes the
|
|
4762
|
+
# same treatment under every rule above: the ones the body rebinds ({CapturedLocals.writes} with
|
|
4763
|
+
# `non_locals: true`, which also counts `self.w = …` as a rebind of `@w`) join the name set. They keep
|
|
4764
|
+
# their sigil, so the one map cannot confuse `@t` with a local `t`, and the arity-cap floor
|
|
4765
|
+
# ({#unanswered_tail_dependency?}), which compares names sigil-and-all, counts a rebound ivar as answered.
|
|
4766
|
+
#
|
|
4767
|
+
# A name the body rebinds where the fixpoint's pass cannot see it ({UnthreadedRebinds}) takes the floor
|
|
4768
|
+
# outright; see {#converged_captured_bindings}. The optimistic nil-freeness mark a pass's exit binding
|
|
4769
|
+
# carries is kept with the converged type ({CapturedLocals::Bindings}).
|
|
4770
|
+
#
|
|
4771
|
+
# The same pin has a CONTENT half the rebind set cannot see: a captured receiver the body mutates in place
|
|
4772
|
+
# is never rebound, so `h = { a: 0 }; [:a, :a].map { |k| h[k] = h[k] + 1 }` read `h` at its entry
|
|
4773
|
+
# contents at every position and folded to `[1, 1]` (runtime `[1, 2]`). Each such local
|
|
4774
|
+
# ({CapturedLocals.content_mutations}) is bound as if every mutation site in the body had already stored
|
|
4775
|
+
# an unknown value ({UnknownStoreWidening.widen}): `Hash[Dynamic[top] | Symbol, Dynamic[top] |
|
|
4776
|
+
# Integer]`, an open arity with a gradual arm on every content parameter a storing site touches. A local
|
|
4777
|
+
# whose widening declines (a precise nominal) gets no binding at all, so it keeps today's answer. The
|
|
4778
|
+
# binding evaluates no body, so it applies under threading suppression too; the rebind fixpoint runs
|
|
4779
|
+
# over it, and a local the body both rebinds and mutates takes the same widening over its converged
|
|
4780
|
+
# type — the rebind can bring a fresh literal back, which the next iteration then mutates. A local mutated
|
|
4781
|
+
# through an element read (`a[0] << e`) or passed to a self-call whose callee content-mutates that
|
|
4782
|
+
# parameter (`add_to(a, e)`) takes it too, widened as straight-line code widens it after such a site. An
|
|
4783
|
+
# unmutated captured local keeps its exact binding (`h = { a: 0 }; [:a, :a].map { |k| h[k] }` still folds
|
|
4784
|
+
# to `[0, 0]`). An instance variable the body mutates in place takes the same binding
|
|
4785
|
+
# ({CapturedLocals.content_mutations} with `non_locals: true`, on the rebind set's terms): once the #587
|
|
4786
|
+
# (a) gate threads an index write, `@cache[:first] ||= e` pins the same way. So do a class variable and a
|
|
4787
|
+
# global.
|
|
4788
|
+
#
|
|
4789
|
+
# The generic block-return pass lays the same binding ({#block_entry_scope}), with the fixpoint's block
|
|
4790
|
+
# parameters bound to the signature's instead of to the elements' union.
|
|
4791
|
+
#
|
|
4792
|
+
# The binding also marks the index `||=` sites whose slot an earlier position may have filled
|
|
4793
|
+
# ({RepeatedOrWrites}), so the memoizing `||=` reading (`StatementEvaluator#index_compound_write_value`)
|
|
4794
|
+
# does not take such a slot's lone `Dynamic` for "no evidence about the slot" and answer one position's
|
|
4795
|
+
# rvalue: `cache = {}; [1, 2].find { |e| (cache[:first] ||= e) == 2 }` answered `2 == 2` at the second
|
|
4796
|
+
# position and folded `find` to `2` where Ruby, keeping the first iteration's `1`, answers `nil`.
|
|
4797
|
+
#
|
|
4798
|
+
# Returns `nil` (no binding to apply) for the overwhelmingly common body that rebinds and mutates nothing
|
|
4799
|
+
# captured and holds no such site.
|
|
3441
4800
|
def per_element_captured_bindings(block, element_types)
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
4801
|
+
captured_block_bindings(block, [Type::Combinator.union(*element_types)], element_types: element_types)
|
|
4802
|
+
end
|
|
4803
|
+
|
|
4804
|
+
# The #587 (b) binding for `block`, with the fixpoint's block parameters bound to `param_types`.
|
|
4805
|
+
# `element_types` is the per-element fold's positions, which {RepeatedOrWrites} reads; nil for the generic
|
|
4806
|
+
# block-return pass.
|
|
4807
|
+
def captured_block_bindings(block, param_types, element_types: nil)
|
|
4808
|
+
stores = CapturedLocals.content_mutations(block, scope, non_locals: true)
|
|
4809
|
+
names = CapturedLocals.writes(block, scope, non_locals: true)
|
|
4810
|
+
repeated = RepeatedOrWrites.sites(block, stores, scope, element_types: element_types)
|
|
4811
|
+
return nil if stores.empty? && names.empty? && repeated.empty?
|
|
4812
|
+
|
|
4813
|
+
stored = stored_capture_bindings(stores)
|
|
4814
|
+
types = stored.dup
|
|
4815
|
+
marks = {}
|
|
4816
|
+
unless names.empty?
|
|
4817
|
+
rebound_capture_bindings(block, names, param_types, stored, marks).each do |name, converged|
|
|
4818
|
+
types[name] = stores.key?(name) ? UnknownStoreWidening.widen(converged, stores[name]) : converged
|
|
4819
|
+
end
|
|
4820
|
+
end
|
|
4821
|
+
return nil if types.empty? && repeated.empty?
|
|
3445
4822
|
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
4823
|
+
CapturedLocals::Bindings.new(types: types, marks: marks, repeated: repeated)
|
|
4824
|
+
end
|
|
4825
|
+
|
|
4826
|
+
# Only a binding the widening MOVED is recorded. One it declined (a precise nominal, a refinement under
|
|
4827
|
+
# `sort!`) is still the entry binding, which says nothing about later iterations; recording it would make the
|
|
4828
|
+
# arity-cap floor ({#unanswered_tail_dependency?}) treat the name as answered and type the tail from that
|
|
4829
|
+
# stale binding. Left out, the name keeps the entry binding below the cap and
|
|
4830
|
+
# the floor above it, which is the fold's answer without this pass.
|
|
4831
|
+
def stored_capture_bindings(stores)
|
|
4832
|
+
stores.each_with_object({}) do |(name, sites), bindings|
|
|
4833
|
+
seed = CapturedLocals.bound_type(scope, name)
|
|
4834
|
+
next if seed.nil?
|
|
4835
|
+
|
|
4836
|
+
widened = UnknownStoreWidening.widen(seed, sites)
|
|
4837
|
+
bindings[name] = widened unless widened == seed
|
|
3450
4838
|
end
|
|
3451
4839
|
end
|
|
3452
4840
|
|
|
4841
|
+
# `stored` is laid under every fixpoint pass, so the rebind converges over the widened contents rather
|
|
4842
|
+
# than the entry ones. `marks` collects the optimistic nil-freeness mark a pass's exit binding carries.
|
|
4843
|
+
def rebound_capture_bindings(block, names, param_types, stored, marks)
|
|
4844
|
+
return captured_floor(names) if block_body_threading_suppressed?
|
|
4845
|
+
|
|
4846
|
+
converged_captured_bindings(block, names, param_types, stored, marks)
|
|
4847
|
+
rescue StandardError
|
|
4848
|
+
captured_floor(names)
|
|
4849
|
+
end
|
|
4850
|
+
|
|
3453
4851
|
def captured_floor(names)
|
|
3454
4852
|
names.to_h { |name| [name, Type::Combinator.untyped] }
|
|
3455
4853
|
end
|
|
3456
4854
|
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
4855
|
+
# A name the body rebinds somewhere the pass's exit scope never sees ({UnthreadedRebinds}) takes the floor
|
|
4856
|
+
# without running the fixpoint, and is bound to it under every pass, so a name that reads it converges over
|
|
4857
|
+
# the floor rather than over the pin: `t = s` after an unthreaded `s` rebind would otherwise converge on
|
|
4858
|
+
# the first iteration's `s`. The unmoved-pin test below still runs over every other name.
|
|
4859
|
+
def converged_captured_bindings(block, names, param_types, stored, marks)
|
|
4860
|
+
floored = UnthreadedRebinds.names(block, names)
|
|
4861
|
+
moving = names.reject { |name| floored.include?(name) }
|
|
4862
|
+
# Issue #1358 — each pass is an iteration, so it reads the match globals the block entry does.
|
|
4863
|
+
base = stored.reduce(MatchRebinding.block_entry(scope, block)) do |acc, (name, type)|
|
|
4864
|
+
CapturedLocals.bind(acc, name, type)
|
|
4865
|
+
end
|
|
4866
|
+
base = floored.reduce(base) { |acc, name| CapturedLocals.bind(acc, name, Type::Combinator.untyped) }
|
|
4867
|
+
seeds = moving.to_h { |name| [name, CapturedLocals.bound_type(base, name)] }
|
|
4868
|
+
moved = {}
|
|
4869
|
+
converged = BodyFixpoint.converge(
|
|
4870
|
+
names: moving,
|
|
4871
|
+
seed_bindings: seeds,
|
|
3462
4872
|
widen: Type::Combinator.method(:widen_value_pinned),
|
|
3463
|
-
evaluate_body:
|
|
4873
|
+
evaluate_body: lambda do |bindings|
|
|
4874
|
+
captured_exit_bindings(block, param_types, base, bindings, moved, marks)
|
|
4875
|
+
end
|
|
3464
4876
|
)
|
|
4877
|
+
unmoved_pins_floored(converged, seeds, moved).merge(captured_floor(floored))
|
|
4878
|
+
end
|
|
4879
|
+
|
|
4880
|
+
# A name the write scan says this block REBINDS, whose fixpoint came back on exactly its value-pinned
|
|
4881
|
+
# seed, is floored rather than believed.
|
|
4882
|
+
#
|
|
4883
|
+
# The fixpoint reads each pass's exit binding out of `StatementEvaluator`, and that evaluator does not
|
|
4884
|
+
# thread a write from every position: a rebind nested where it types a pure value leaves the exit scope
|
|
4885
|
+
# holding the entry binding, so the fixpoint converges on the seed and every position of the fold answers
|
|
4886
|
+
# the first iteration again. That is issue #617 residue (1)'s one-liner, `seen = 0; [1, 2].find { |e|
|
|
4887
|
+
# (seen += 1) == 2 }` answering `nil` where Ruby answers `2`: the pinned `Constant[0]` made both
|
|
4888
|
+
# predicates `Constant[false]`, and `find` short-circuits on a provably-falsey block. The evaluator has
|
|
4889
|
+
# threaded a call's receiver since #1223, but a `when` condition or a `yield` argument is still such a
|
|
4890
|
+
# position.
|
|
4891
|
+
#
|
|
4892
|
+
# An unmoved pin cannot be distinguished from a write that genuinely restores its own entry value
|
|
4893
|
+
# (`x = 5; xs.each { x = 5 }`), so the floor gives that shape up too. It is the far cheaper side: a
|
|
4894
|
+
# value-pinned seed the block rebinds is the exact pre-state this fold exists to stop trusting, and
|
|
4895
|
+
# `Dynamic[top]` is the same escaping-block floor {#captured_floor} already uses. Seeds that carry no
|
|
4896
|
+
# pinning are left alone — there is no first-iteration value or shape in them to remove, and widening a
|
|
4897
|
+
# `Nominal` here would only lose a class for nothing.
|
|
4898
|
+
#
|
|
4899
|
+
# A second route reaches this test with nothing hidden from the evaluator. The fixpoint stops after
|
|
4900
|
+
# {BodyFixpoint::CAP} passes, so a rebind a counter guards past the third iteration (`row = [v, v] if
|
|
4901
|
+
# count > 3`) runs in none of them, and the widen on the capped pass erases a `Constant`'s value but leaves
|
|
4902
|
+
# a `Tuple` / `HashShape` as it is. `row = []` therefore came back on its seed, and `sizes.last == 2`
|
|
4903
|
+
# folded always-falsey on a runtime `2`, so a shape carrier counts as pinned ({#value_pinned?}). For a
|
|
4904
|
+
# shape the unmoved test only asks whether the seed already covers the converged type, so any rebind the
|
|
4905
|
+
# seed covers is floored with it, not only one restoring the entry value: `qr = n.divmod(3)` rebound to
|
|
4906
|
+
# another `divmod` reads `Dynamic[top]` too.
|
|
4907
|
+
#
|
|
4908
|
+
# The pin test reads a binding as a whole and nothing else. A `0 | Integer` seed is floored too, although a
|
|
4909
|
+
# threaded `x += 1` only joins back into it: an unthreaded write storing another class (`log(x = nil)`)
|
|
4910
|
+
# converges on the same seed, and only the floor keeps `x.nil?` from folding to `false`. Nor does a pass
|
|
4911
|
+
# whose exit binding moved prove the rebind was threaded — a narrowing (`next false unless x`) or a
|
|
4912
|
+
# threaded prefix (`x ||= 0`) moves it while `(x += 1) == 2` stays unthreaded.
|
|
4913
|
+
#
|
|
4914
|
+
# "Unmoved" compares against the fixpoint's own seed, but "pinned" is asked of the CALL-SITE binding. For a
|
|
4915
|
+
# name the body also mutates in place the seed is already the in-place widening of that binding
|
|
4916
|
+
# ({#stored_capture_bindings}), and the widening erases exactly the pin this test looks for: `s = +"ab"`
|
|
4917
|
+
# seeds `String`, not `"ab"`. Asking the seed would let a nested rebind the evaluator cannot see (`(s &&=
|
|
4918
|
+
# s.to_sym)` inside an expression) converge on `String` and be believed, where the local really holds a
|
|
4919
|
+
# Symbol from the second iteration on. For every other name the two bindings are the same. The price is
|
|
4920
|
+
# the trade the paragraph above already makes: a name the body mutates and VISIBLY rebinds to the widened
|
|
4921
|
+
# class (`t << "c"; t = t.strip`) converges on that seed as well, and is floored with the hidden case.
|
|
4922
|
+
#
|
|
4923
|
+
# A statement rebind does not buy that case back, because converging on the widened seed is not evidence
|
|
4924
|
+
# that the rebind was all there was. The capped pass above runs no rebind a counter guards past the third
|
|
4925
|
+
# iteration (`s = s.dup; n += 1; s = [e] if n > 3`), and the scans see only write nodes in the body, so a
|
|
4926
|
+
# lambda defined outside it (`close = -> { cur = nil }; … close.call`) or `binding.local_variable_set`
|
|
4927
|
+
# rebinds the local where no pass looks. Both converge on the seed next to a statement rebind, and
|
|
4928
|
+
# believing `String` there reports `undefined method` or an always-falsey `cur.nil?` on correct code.
|
|
4929
|
+
#
|
|
4930
|
+
# A pass's exit binding also joins every block-level `next` ({StatementEvaluator#evaluate_invocation}), and
|
|
4931
|
+
# a rebind on a `next` arm moves the converged binding off its seed while an unthreaded write on the
|
|
4932
|
+
# fall-through stays invisible: `if c; seen = 100; next false; end; (seen += 1) == 2` converged on `0 |
|
|
4933
|
+
# 100` and folded `find` to `nil`. So `moved` ({#captured_exit_bindings}) records which path moved each
|
|
4934
|
+
# name. One the FALL-THROUGH moved is believed; one only a `next` arm moved is unmoved for this test, and
|
|
4935
|
+
# floored with the hidden case; one neither moved is judged against its seed as before, so a block without
|
|
4936
|
+
# a block-level `next` is judged exactly as it always was.
|
|
4937
|
+
#
|
|
4938
|
+
# {UnthreadedRebinds} floors every name it can show is rebound out of the pass's sight before this test
|
|
4939
|
+
# runs, whatever the fixpoint converged to — the threaded `||=` that moves the fall-through beside an
|
|
4940
|
+
# unthreaded `(seen += 1) == 2` included — so this test only ever sees a name whose rebinds the scan found
|
|
4941
|
+
# threaded. It stays as the backstop for what the scan cannot see, and so the `0 | Integer` seed above
|
|
4942
|
+
# keeps its floor: skipping it for a scan-clean name would trade that backstop for precision.
|
|
4943
|
+
def unmoved_pins_floored(converged, seeds, moved)
|
|
4944
|
+
converged.to_h do |name, type|
|
|
4945
|
+
unmoved =
|
|
4946
|
+
case moved[name]
|
|
4947
|
+
when :fall_through then false
|
|
4948
|
+
when :jump then true
|
|
4949
|
+
else type == seeds[name]
|
|
4950
|
+
end
|
|
4951
|
+
next [name, type] unless unmoved && value_pinned?(CapturedLocals.bound_type(scope, name))
|
|
4952
|
+
|
|
4953
|
+
[name, Type::Combinator.untyped]
|
|
4954
|
+
end
|
|
4955
|
+
end
|
|
4956
|
+
|
|
4957
|
+
# A binding carries a pin when widening its values changes it, or when it is a `Tuple` / `HashShape` —
|
|
4958
|
+
# alone or as one member of a union. A shape carrier is pinned whatever its elements are: its arity and
|
|
4959
|
+
# key set are one assignment's, and the fixpoint's final widen erases a value but never a shape.
|
|
4960
|
+
def value_pinned?(type)
|
|
4961
|
+
case type
|
|
4962
|
+
when nil then false
|
|
4963
|
+
when Type::Tuple, Type::HashShape then true
|
|
4964
|
+
when Type::Union then type.members.any? { |member| value_pinned?(member) }
|
|
4965
|
+
else Type::Combinator.widen_value_pinned(type) != type
|
|
4966
|
+
end
|
|
3465
4967
|
end
|
|
3466
4968
|
|
|
3467
4969
|
# One fixpoint pass: the body evaluated from `bindings` with the block parameters bound over them (the
|
|
3468
|
-
# same layering as {#type_block_body_with_param}), returning the per-name exit binding
|
|
3469
|
-
#
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
4970
|
+
# same layering as {#type_block_body_with_param}), returning the per-name exit binding — the invocation's
|
|
4971
|
+
# exit, which joins every block-level `next` ({StatementEvaluator#evaluate_invocation}), so a rebind on a
|
|
4972
|
+
# jumping path reaches the fixpoint. `moved` records, per name, whether this pass's FALL-THROUGH moved it
|
|
4973
|
+
# off its entry (`:fall_through`) or only the joined `next` arms did (`:jump`), for {#unmoved_pins_floored}.
|
|
4974
|
+
# An exit mark is recorded into `marks` and carried into later passes, since the next iteration enters on
|
|
4975
|
+
# that binding. Threading is suppressed for the pass, as it is for every full body evaluation the
|
|
4976
|
+
# block-return pass runs.
|
|
4977
|
+
def captured_exit_bindings(block, param_types, base, bindings, moved, marks)
|
|
4978
|
+
entry = bindings.reduce(base) do |acc, (name, type)|
|
|
4979
|
+
CapturedLocals.bind(acc, name, type, optimistic: marks[name])
|
|
4980
|
+
end
|
|
4981
|
+
entry = BlockParameterBinder.new(expected_param_types: param_types).bind_onto(block, entry)
|
|
4982
|
+
_type, fall_through, exit_scope = without_block_body_threading do
|
|
4983
|
+
StatementEvaluator.new(scope: entry).evaluate_invocation(block)
|
|
4984
|
+
end
|
|
4985
|
+
bindings.to_h do |name, entry_type|
|
|
4986
|
+
exit_type = CapturedLocals.bound_type(exit_scope, name)
|
|
4987
|
+
record_capture_move(moved, name, entry_type, fall_through, exit_type)
|
|
4988
|
+
mark = CapturedLocals.optimistic_mark(exit_scope, name)
|
|
4989
|
+
marks[name] ||= mark if mark
|
|
4990
|
+
[name, exit_type]
|
|
4991
|
+
end
|
|
4992
|
+
end
|
|
4993
|
+
|
|
4994
|
+
# Which path of one pass moved `name` off its `entry_type`: the fall-through outranks a `next` arm, and a
|
|
4995
|
+
# name the fall-through moved in any pass stays moved.
|
|
4996
|
+
def record_capture_move(moved, name, entry_type, fall_through, exit_type)
|
|
4997
|
+
if moved_off?(entry_type, CapturedLocals.bound_type(fall_through, name))
|
|
4998
|
+
moved[name] = :fall_through
|
|
4999
|
+
elsif moved_off?(entry_type, exit_type)
|
|
5000
|
+
moved[name] ||= :jump
|
|
5001
|
+
end
|
|
5002
|
+
end
|
|
5003
|
+
|
|
5004
|
+
# True when `after` is not already contained in `before` — the same stability test `BodyFixpoint` applies.
|
|
5005
|
+
def moved_off?(before, after)
|
|
5006
|
+
return false if after.nil?
|
|
5007
|
+
return true if before.nil?
|
|
5008
|
+
|
|
5009
|
+
Type::Combinator.union(before, after) != before
|
|
3476
5010
|
end
|
|
3477
5011
|
|
|
3478
5012
|
def per_element_symbol_results(block_arg, element_types)
|
|
@@ -3501,7 +5035,7 @@ module Rigor
|
|
|
3501
5035
|
# `Constant<a..b>` → [Constant[a], …, Constant[b]]
|
|
3502
5036
|
# everything else → nil
|
|
3503
5037
|
#
|
|
3504
|
-
# Note: `Type::IntegerRange` is the bounded-Integer carrier (`
|
|
5038
|
+
# Note: `Type::IntegerRange` is the bounded-Integer carrier (`Integer[a..b]` represents "an Integer between
|
|
3505
5039
|
# a and b"), not a Range value. Calls like `.map` / `.find` on an `IntegerRange` receiver would resolve
|
|
3506
5040
|
# to `Integer#map` / `Integer#find` — neither exists — so IntegerRange does NOT participate in this
|
|
3507
5041
|
# fold.
|
|
@@ -3541,7 +5075,7 @@ module Rigor
|
|
|
3541
5075
|
#
|
|
3542
5076
|
# Part 1 (soundness): the accumulator of a block-form fold must reach a fixpoint over an unknown
|
|
3543
5077
|
# number of iterations — the RBS tier's generic `(S) { (S, E) -> S } -> S` binds `S` from a SINGLE
|
|
3544
|
-
# block pass (acc=seed, elem=element-join), so `(1..5).inject(1) { |a, i| a * i }` types `
|
|
5078
|
+
# block pass (acc=seed, elem=element-join), so `(1..5).inject(1) { |a, i| a * i }` types `Integer[1..5]`
|
|
3545
5079
|
# while the runtime is 120 (out of range — unsound). We iterate the accumulator type to a capped
|
|
3546
5080
|
# fixpoint (ADR-55/56 `BodyFixpoint`) so the multiply converges to `Integer`, never a value-bounded
|
|
3547
5081
|
# interval the runtime escapes.
|
|
@@ -3557,8 +5091,6 @@ module Rigor
|
|
|
3557
5091
|
# tier. Captured-local write-back (ADR-56) runs at the statement level independent of this return-type
|
|
3558
5092
|
# computation, so a block that both accumulates and mutates captured state keeps its write-back
|
|
3559
5093
|
# regardless of which arm answers here.
|
|
3560
|
-
#
|
|
3561
|
-
# @return [Rigor::Type, nil]
|
|
3562
5094
|
def try_block_inject_fold(call_node, receiver, arg_types)
|
|
3563
5095
|
return nil unless INJECT_METHODS.include?(call_node.name)
|
|
3564
5096
|
|
|
@@ -3577,7 +5109,7 @@ module Rigor
|
|
|
3577
5109
|
# Splits the positional args into the optional seed. A Symbol final arg (`inject(seed, :*)`) is the
|
|
3578
5110
|
# no-block Symbol form and never reaches here (the block guard already failed for it).
|
|
3579
5111
|
#
|
|
3580
|
-
# @return
|
|
5112
|
+
# @return `[seed, has_seed]`
|
|
3581
5113
|
def inject_seed(arg_types)
|
|
3582
5114
|
case arg_types.size
|
|
3583
5115
|
when 0 then [nil, false]
|
|
@@ -3637,7 +5169,7 @@ module Rigor
|
|
|
3637
5169
|
# member is folded; without a seed the first member seeds the memo and the rest are folded. The
|
|
3638
5170
|
# accumulator is carried as a `Constant` type (so the block body sees a value-pinned param).
|
|
3639
5171
|
#
|
|
3640
|
-
# @return
|
|
5172
|
+
# @return `[acc, rest]`
|
|
3641
5173
|
def inject_constant_start(members, seed, has_seed)
|
|
3642
5174
|
if has_seed
|
|
3643
5175
|
return [nil, []] unless seed.is_a?(Type::Constant) && inject_foldable?(seed.value)
|
|
@@ -3698,15 +5230,6 @@ module Rigor
|
|
|
3698
5230
|
converged[:__inject_acc__] || seed_acc
|
|
3699
5231
|
end
|
|
3700
5232
|
|
|
3701
|
-
# `index(value)` and `find_index(value)` carry a positional argument and search by `==` rather than
|
|
3702
|
-
# running the block. Decline so the RBS tier owns those forms.
|
|
3703
|
-
def find_family_with_args?(call_node)
|
|
3704
|
-
return false unless %i[find_index index].include?(call_node.name)
|
|
3705
|
-
|
|
3706
|
-
args = call_node.arguments
|
|
3707
|
-
!args.nil? && !args.arguments.empty?
|
|
3708
|
-
end
|
|
3709
|
-
|
|
3710
5233
|
def assemble_per_element_result(method_name, per_position, element_types)
|
|
3711
5234
|
case method_name
|
|
3712
5235
|
when :map, :collect then Type::Combinator.tuple_of(*per_position)
|
|
@@ -3774,28 +5297,40 @@ module Rigor
|
|
|
3774
5297
|
# `find` / `detect`: returns the first receiver element whose block result is Ruby-truthy, or `nil`
|
|
3775
5298
|
# when no position folds to truthy.
|
|
3776
5299
|
#
|
|
3777
|
-
#
|
|
3778
|
-
#
|
|
3779
|
-
#
|
|
3780
|
-
# the answer is `Constant[nil]`.
|
|
5300
|
+
# When the first decisive truthy position is found, the answer is the corresponding receiver element,
|
|
5301
|
+
# joined with the element of every earlier position whose result is not a `Type::Constant`: such a
|
|
5302
|
+
# position may match first, but the search ends at the truthy one whatever it answers, so the call
|
|
5303
|
+
# never returns `nil`. When every position folds to falsey, the answer is `Constant[nil]`. With no
|
|
5304
|
+
# decisive truthy position and some undecided one, it declines ({#undecided_fold_floor}).
|
|
3781
5305
|
def assemble_find_result(per_position, element_types)
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
first_truthy_index = per_position.index { |type| truthy_constant?(type) }
|
|
3785
|
-
return Type::Combinator.constant_of(nil) if first_truthy_index.nil?
|
|
5306
|
+
candidates = first_match_candidates(per_position)
|
|
5307
|
+
return candidates if candidates.nil? || candidates.equal?(NO_MATCH)
|
|
3786
5308
|
|
|
3787
|
-
element_types[
|
|
5309
|
+
Type::Combinator.union(*candidates.map { |index| element_types[index] })
|
|
3788
5310
|
end
|
|
3789
5311
|
|
|
3790
5312
|
# `find_index` / `index`: returns the index of the first truthy position, or `Constant[nil]` when
|
|
3791
|
-
# nothing matches.
|
|
5313
|
+
# nothing matches, on {#assemble_find_result}'s terms.
|
|
3792
5314
|
def assemble_find_index_result(per_position)
|
|
3793
|
-
|
|
5315
|
+
candidates = first_match_candidates(per_position)
|
|
5316
|
+
return candidates if candidates.nil? || candidates.equal?(NO_MATCH)
|
|
3794
5317
|
|
|
5318
|
+
Type::Combinator.union(*candidates.map { |index| Type::Combinator.constant_of(index) })
|
|
5319
|
+
end
|
|
5320
|
+
|
|
5321
|
+
NO_MATCH = Type::Combinator.constant_of(nil)
|
|
5322
|
+
private_constant :NO_MATCH
|
|
5323
|
+
|
|
5324
|
+
# The positions `find` may stop at — the first decisive truthy one and every undecided one before it —
|
|
5325
|
+
# `NO_MATCH` when every position folds falsey, or nil when an undecided position has no truthy one after it.
|
|
5326
|
+
def first_match_candidates(per_position)
|
|
3795
5327
|
first_truthy_index = per_position.index { |type| truthy_constant?(type) }
|
|
3796
|
-
|
|
5328
|
+
if first_truthy_index.nil?
|
|
5329
|
+
return per_position.all?(Type::Constant) ? NO_MATCH : nil
|
|
5330
|
+
end
|
|
3797
5331
|
|
|
3798
|
-
|
|
5332
|
+
undecided = (0...first_truthy_index).reject { |index| per_position[index].is_a?(Type::Constant) }
|
|
5333
|
+
undecided + [first_truthy_index]
|
|
3799
5334
|
end
|
|
3800
5335
|
|
|
3801
5336
|
def truthy_constant?(type)
|
|
@@ -3816,28 +5351,47 @@ module Rigor
|
|
|
3816
5351
|
# `Constant[Symbol | String]` — otherwise the tier declines (the new key cannot be used as a static
|
|
3817
5352
|
# HashShape index). Collisions (two old keys mapping to the same new key) also decline.
|
|
3818
5353
|
#
|
|
5354
|
+
# Two runtime behaviours fall outside this model, and both decline:
|
|
5355
|
+
#
|
|
5356
|
+
# - A call with arguments. `transform_keys(mapping)` renames the keys `mapping` names and yields only
|
|
5357
|
+
# the rest to the block, and the fold never reads the mapping. `transform_values` takes none.
|
|
5358
|
+
# - An in-place form whose block could see the receiver ({ReceiverBlindBlock}). The bang forms rewrite
|
|
5359
|
+
# the receiver pair by pair, so the block can see pairs it has already rewritten, while the fold types
|
|
5360
|
+
# every pair against the pre-call shape. The non-bang forms build a new hash and keep the exact fold.
|
|
5361
|
+
#
|
|
3819
5362
|
# Returns `nil` on any decline so the dispatcher falls through to `RbsDispatch` and gets the widened
|
|
3820
5363
|
# `Hash[K, V]` answer.
|
|
3821
5364
|
def try_hash_shape_block_fold(call_node, receiver_type)
|
|
3822
|
-
return nil unless
|
|
3823
|
-
return nil unless receiver_type.is_a?(Type::HashShape)
|
|
3824
|
-
return nil unless receiver_type.closed?
|
|
3825
|
-
return nil unless receiver_type.optional_keys.empty?
|
|
5365
|
+
return nil unless hash_shape_fold_applicable?(call_node, receiver_type)
|
|
3826
5366
|
|
|
3827
5367
|
block_arg = call_node.block
|
|
3828
|
-
return nil if block_arg
|
|
5368
|
+
return nil if in_place_block_sees_receiver?(call_node.name, block_arg)
|
|
3829
5369
|
|
|
3830
5370
|
if %i[transform_values transform_values!].include?(call_node.name)
|
|
3831
5371
|
fold_hash_shape_transform_values(receiver_type, block_arg)
|
|
3832
5372
|
else
|
|
3833
|
-
fold_hash_shape_transform_keys(receiver_type, block_arg)
|
|
5373
|
+
fold_hash_shape_transform_keys(receiver_type, block_arg, in_place: call_node.name == :transform_keys!)
|
|
3834
5374
|
end
|
|
3835
5375
|
end
|
|
3836
5376
|
|
|
5377
|
+
def hash_shape_fold_applicable?(call_node, receiver_type)
|
|
5378
|
+
HASH_SHAPE_TRANSFORM_METHODS.include?(call_node.name) &&
|
|
5379
|
+
receiver_type.is_a?(Type::HashShape) && receiver_type.closed? && receiver_type.optional_keys.empty? &&
|
|
5380
|
+
call_node.arguments.nil? && !call_node.block.nil?
|
|
5381
|
+
end
|
|
5382
|
+
|
|
5383
|
+
def in_place_block_sees_receiver?(method_name, block_arg)
|
|
5384
|
+
IN_PLACE_HASH_SHAPE_TRANSFORMS.include?(method_name) &&
|
|
5385
|
+
block_arg.is_a?(Prism::BlockNode) && !ReceiverBlindBlock.blind?(block_arg, scope)
|
|
5386
|
+
end
|
|
5387
|
+
|
|
3837
5388
|
def fold_hash_shape_transform_values(shape, block_arg)
|
|
5389
|
+
captured = hash_block_captured_bindings(block_arg, shape.pairs.values)
|
|
5390
|
+
return hash_shape_values_floor(shape) if tail_only_pairs_floored?(shape, block_arg, captured)
|
|
5391
|
+
|
|
3838
5392
|
new_pairs = {}
|
|
3839
|
-
shape.pairs.
|
|
3840
|
-
new_value = apply_hash_block(block_arg, value)
|
|
5393
|
+
shape.pairs.each_with_index do |(key, value), position|
|
|
5394
|
+
new_value = apply_hash_block(block_arg, value, captured: captured, position: position)
|
|
3841
5395
|
return nil if new_value.nil?
|
|
3842
5396
|
|
|
3843
5397
|
new_pairs[key] = new_value
|
|
@@ -3845,28 +5399,125 @@ module Rigor
|
|
|
3845
5399
|
Type::Combinator.hash_shape_of(new_pairs)
|
|
3846
5400
|
end
|
|
3847
5401
|
|
|
3848
|
-
def fold_hash_shape_transform_keys(shape, block_arg)
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
new_key_type = apply_hash_block(block_arg, key_type)
|
|
3853
|
-
return nil unless new_key_type.is_a?(Type::Constant)
|
|
5402
|
+
def fold_hash_shape_transform_keys(shape, block_arg, in_place: false)
|
|
5403
|
+
key_types = shape.pairs.keys.map { |key| Type::Combinator.constant_of(key) }
|
|
5404
|
+
captured = hash_block_captured_bindings(block_arg, key_types)
|
|
5405
|
+
return hash_keys_floor(shape) if tail_only_pairs_floored?(shape, block_arg, captured)
|
|
3854
5406
|
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
5407
|
+
new_pairs = {}
|
|
5408
|
+
key_types.zip(shape.pairs.values).each_with_index do |(key_type, value), position|
|
|
5409
|
+
typed_key = apply_hash_block(block_arg, key_type, captured: captured, position: position)
|
|
5410
|
+
new_key = new_shape_key(typed_key, new_pairs)
|
|
5411
|
+
return undecided_keys_floor(shape, block_arg, key_types, captured) if new_key.nil?
|
|
3858
5412
|
|
|
3859
5413
|
new_pairs[new_key] = value
|
|
3860
5414
|
end
|
|
3861
|
-
Type::Combinator.hash_shape_of(new_pairs)
|
|
5415
|
+
Type::Combinator.hash_shape_of(in_place ? in_place_key_order(shape.pairs, new_pairs) : new_pairs)
|
|
5416
|
+
end
|
|
5417
|
+
|
|
5418
|
+
# `transform_keys!` rewrites the receiver in place, pair by pair, so its pairs end in a different order from
|
|
5419
|
+
# the new hash `transform_keys` builds. For each old pair, CRuby's `rb_hash_transform_keys_bang` deletes the
|
|
5420
|
+
# old key unless an earlier pair already produced it as a new key, then stores the new key: in place when the
|
|
5421
|
+
# new key is an old key not yet reached, appended otherwise. `{ a: 1, b: 2, c: 3 }.transform_keys! { |k| k ==
|
|
5422
|
+
# :b ? :c : (k == :c ? :b : k) }` is `{ c: 2, a: 1, b: 3 }`, not `{ a: 1, c: 2, b: 3 }`, and `r.keys.first ==
|
|
5423
|
+
# :a` folded always-truthy on the wrong order. Replaying those steps over a Ruby `Hash`, which orders its keys
|
|
5424
|
+
# the same way, gives the same order. `new_pairs` is collision-free and in the old pairs' order, so every
|
|
5425
|
+
# old key is either deleted or overwritten, and no old value survives.
|
|
5426
|
+
def in_place_key_order(old_pairs, new_pairs)
|
|
5427
|
+
produced = {}
|
|
5428
|
+
old_pairs.keys.zip(new_pairs).each_with_object(old_pairs.dup) do |(old_key, (new_key, value)), result|
|
|
5429
|
+
result.delete(old_key) unless produced.key?(old_key)
|
|
5430
|
+
result[new_key] = value
|
|
5431
|
+
produced[new_key] = true
|
|
5432
|
+
end
|
|
5433
|
+
end
|
|
5434
|
+
|
|
5435
|
+
# The key a pair's block result can index the new `HashShape` with — a `Constant` Symbol or String that no
|
|
5436
|
+
# earlier pair already took — or nil.
|
|
5437
|
+
def new_shape_key(new_key_type, new_pairs)
|
|
5438
|
+
return nil unless new_key_type.is_a?(Type::Constant)
|
|
5439
|
+
|
|
5440
|
+
new_key = new_key_type.value
|
|
5441
|
+
return nil unless new_key.is_a?(Symbol) || new_key.is_a?(String)
|
|
5442
|
+
|
|
5443
|
+
new_pairs.key?(new_key) ? nil : new_key
|
|
5444
|
+
end
|
|
5445
|
+
|
|
5446
|
+
# A key fold that cannot build a `HashShape` declines to the dispatcher — except under the suppression, where
|
|
5447
|
+
# the dispatcher's block-return pass is typed tail-only too and read a captured value the body mutates in
|
|
5448
|
+
# place at its entry contents: `buf = +"k"; … { a: 1 }.transform_keys { |k| buf << w.to_s; buf }` answered
|
|
5449
|
+
# `Hash["k", 1]` for `{ "k1" => 1 }`, though the pairs themselves were typed over the #587 (b) binding that
|
|
5450
|
+
# answers `buf`. There the fold answers from the keys it typed, `Hash[union(<new keys>), union(<values>)]`,
|
|
5451
|
+
# and takes the plain floor ({#hash_keys_floor}) when a pair could not be typed at all. The pairs are
|
|
5452
|
+
# re-typed for it, which only a suppressed, undecided key fold pays. That pass now re-answers such a name
|
|
5453
|
+
# itself ({#prefix_answered_scope}), so a decline would no longer read `"k"`; the fold still answers from
|
|
5454
|
+
# the pairs it has typed.
|
|
5455
|
+
def undecided_keys_floor(shape, block_arg, key_types, captured)
|
|
5456
|
+
return nil unless block_body_threading_suppressed?
|
|
5457
|
+
|
|
5458
|
+
new_key_types = key_types.each_with_index.map do |key_type, position|
|
|
5459
|
+
apply_hash_block(block_arg, key_type, captured: captured, position: position)
|
|
5460
|
+
end
|
|
5461
|
+
return hash_keys_floor(shape) if new_key_types.any?(&:nil?)
|
|
5462
|
+
|
|
5463
|
+
Type::Combinator.nominal_of(
|
|
5464
|
+
"Hash", type_args: [Type::Combinator.union(*new_key_types), Type::Combinator.union(*shape.pairs.values)]
|
|
5465
|
+
)
|
|
5466
|
+
end
|
|
5467
|
+
|
|
5468
|
+
# The per-pair twin of issue #587 (b)'s first-iteration pin. Every pair is typed from the SAME entry scope,
|
|
5469
|
+
# so a body that rebinds a captured outer local answered the first pair's value at every pair: `total = 0;
|
|
5470
|
+
# { x: 1, y: 2 }.transform_values { total += 1 }` folded to `{ x: 1, y: 1 }` (runtime `{ x: 1, y: 2 }`),
|
|
5471
|
+
# and `r[:y] == 1` then fired always-truthy on correct code. The pairs take the per-element fold's
|
|
5472
|
+
# captured-local entry binding ({#per_element_captured_bindings}) — the fixpoint's block parameter bound
|
|
5473
|
+
# to the union of the values, or of the `Constant` keys — rather than a copy of it: issue #1198 is where
|
|
5474
|
+
# the block-entry models consolidate. A captured local the body does not change keeps its exact per-pair
|
|
5475
|
+
# fold, and a `&:symbol` block captures nothing.
|
|
5476
|
+
#
|
|
5477
|
+
# An empty shape has no pair to type, so it does not pay for the fixpoint.
|
|
5478
|
+
def hash_block_captured_bindings(block_arg, param_types)
|
|
5479
|
+
return nil unless block_arg.is_a?(Prism::BlockNode)
|
|
5480
|
+
return nil if param_types.empty?
|
|
5481
|
+
|
|
5482
|
+
per_element_captured_bindings(block_arg, param_types)
|
|
5483
|
+
end
|
|
5484
|
+
|
|
5485
|
+
# The per-pair fold has no arity cap, so running nested inside a body another pass is evaluating whole
|
|
5486
|
+
# ({#tail_only_walk?}'s second case) is the one way it reaches a tail-only pair the dependency predicate
|
|
5487
|
+
# can flag, and that pair holds the same pre-state {#tail_only_body_floor} describes:
|
|
5488
|
+
# `{ x: [], y: [] }.transform_values do |a| a << w; a end` inside a threaded `m.synchronize do w = v; …
|
|
5489
|
+
# end` answered `{ x: [], y: [] }` for a hash whose values each hold `[1]`, and `r[:x].first + 1` was
|
|
5490
|
+
# then reported on correct code. The floor is the Tuple fold's, applied per pair and over the same
|
|
5491
|
+
# `captured` names, with the fold answering it rather than declining: a decline reached the dispatcher,
|
|
5492
|
+
# whose block-return pass the same suppression typed tail-only, so `k = "#{k}#{w}"; k` read its keys
|
|
5493
|
+
# as the entry `:a | :b` until that pass re-answered the stale name itself ({#prefix_answered_scope}).
|
|
5494
|
+
# An empty shape types no pair, so it keeps its exact `{}`.
|
|
5495
|
+
def tail_only_pairs_floored?(shape, block_arg, captured)
|
|
5496
|
+
!shape.pairs.empty? && block_arg.is_a?(Prism::BlockNode) && block_body_threading_suppressed? &&
|
|
5497
|
+
unanswered_tail_dependency?(block_arg, captured)
|
|
5498
|
+
end
|
|
5499
|
+
|
|
5500
|
+
# `transform_values` keeps the keys and answers every value `Dynamic[top]`.
|
|
5501
|
+
def hash_shape_values_floor(shape)
|
|
5502
|
+
Type::Combinator.hash_shape_of(shape.pairs.transform_values { Type::Combinator.untyped })
|
|
5503
|
+
end
|
|
5504
|
+
|
|
5505
|
+
# `transform_keys` keeps the values, but a `Dynamic[top]` key cannot index a `HashShape`, and two keys may
|
|
5506
|
+
# collide, so the floor is the plain `Hash` over them.
|
|
5507
|
+
def hash_keys_floor(shape)
|
|
5508
|
+
Type::Combinator.nominal_of(
|
|
5509
|
+
"Hash", type_args: [Type::Combinator.untyped, Type::Combinator.union(*shape.pairs.values)]
|
|
5510
|
+
)
|
|
3862
5511
|
end
|
|
3863
5512
|
|
|
3864
5513
|
# Applies a single-argument block (either a full BlockNode or a `&:symbol` BlockArgumentNode) to
|
|
3865
|
-
# `param_type` and returns the resulting type, or `nil` on failure.
|
|
3866
|
-
|
|
5514
|
+
# `param_type` and returns the resulting type, or `nil` on failure. `captured:` is the pair-independent
|
|
5515
|
+
# entry binding from {#hash_block_captured_bindings}, and `position:` the pair's index, which picks the
|
|
5516
|
+
# index `||=` sites that binding marks there.
|
|
5517
|
+
def apply_hash_block(block_arg, param_type, captured: nil, position: nil)
|
|
3867
5518
|
case block_arg
|
|
3868
5519
|
when Prism::BlockNode
|
|
3869
|
-
type_block_body_with_param(block_arg, [param_type])
|
|
5520
|
+
type_block_body_with_param(block_arg, [param_type], captured: captured, position: position)
|
|
3870
5521
|
when Prism::BlockArgumentNode
|
|
3871
5522
|
expression = block_arg.expression
|
|
3872
5523
|
return nil unless expression.is_a?(Prism::SymbolNode)
|
|
@@ -3883,13 +5534,18 @@ module Rigor
|
|
|
3883
5534
|
end
|
|
3884
5535
|
end
|
|
3885
5536
|
|
|
3886
|
-
# `captured:` — issue #587 (b): the per-name entry binding of every captured outer local
|
|
5537
|
+
# `captured:` — issue #587 (b): the per-name entry binding of every captured outer local and instance
|
|
5538
|
+
# variable the body rebinds, and of every captured local it mutates in place
|
|
3887
5539
|
# ({#per_element_captured_bindings}), laid under the parameter bindings so a parameter still shadows.
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
5540
|
+
# `position:` is the element's index in the fold; the binding marks the index `||=` sites an earlier
|
|
5541
|
+
# position may have filled there ({RepeatedOrWrites::Marks}).
|
|
5542
|
+
def type_block_body_with_param(block_node, expected_param_types, captured: nil, position: nil)
|
|
5543
|
+
# Issue #1358 — a position past the first runs after an earlier one may have rebound the match globals.
|
|
5544
|
+
entry = MatchRebinding.block_entry(scope, block_node)
|
|
5545
|
+
block_scope = captured ? captured.lay(entry, position: position) : entry
|
|
5546
|
+
block_scope = BlockParameterBinder.new(expected_param_types: expected_param_types)
|
|
5547
|
+
.bind_onto(block_node, block_scope)
|
|
5548
|
+
type_block_body(block_node, block_scope, captured: captured)
|
|
3893
5549
|
rescue StandardError
|
|
3894
5550
|
nil
|
|
3895
5551
|
end
|