rigortype 0.3.8 → 0.3.9
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 +1 -1
- data/data/capability_roles/capability_roles.rbs +36 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +109 -13
- data/docs/handbook/02-everyday-types.md +17 -14
- data/docs/handbook/03-narrowing.md +37 -4
- 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 +14 -1
- data/docs/handbook/11-sig-gen.md +72 -1
- 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 +32 -4
- data/docs/manual/03-configuration.md +11 -0
- data/docs/manual/04-diagnostics.md +11 -0
- data/docs/manual/07-plugins.md +15 -5
- data/docs/manual/08-skills.md +23 -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 +6 -1
- data/docs/manual/18-removing-dead-code.md +10 -8
- data/docs/manual/plugins/rigor-actionpack.md +18 -0
- data/docs/manual/plugins/rigor-activerecord.md +28 -0
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +32 -2
- data/docs/manual/plugins/rigor-rbs-inline.md +42 -1
- data/lib/rigor/analysis/baseline.rb +2 -2
- 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/ivar_write_collector.rb +10 -2
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +15 -6
- data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
- data/lib/rigor/analysis/check_rules/rule_walk.rb +39 -4
- 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/unreachable_clause_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +0 -1
- data/lib/rigor/analysis/check_rules.rb +200 -32
- data/lib/rigor/analysis/crash_signature.rb +8 -24
- 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 +6 -1
- data/lib/rigor/analysis/incremental.rb +16 -0
- data/lib/rigor/analysis/incremental_session.rb +81 -22
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +0 -1
- data/lib/rigor/analysis/reachability/graph.rb +3 -5
- data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -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 +2 -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 +4 -1
- data/lib/rigor/analysis/run_cache_key.rb +1 -1
- 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 +177 -26
- data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +2 -3
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -10
- data/lib/rigor/analysis/runner/pool_coordinator.rb +256 -88
- data/lib/rigor/analysis/runner/project_pre_passes.rb +12 -6
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +133 -35
- data/lib/rigor/analysis/worker_session.rb +9 -10
- data/lib/rigor/bleeding_edge.rb +12 -24
- data/lib/rigor/builtins/hkt_builtins.rb +2 -2
- data/lib/rigor/builtins/imported_refinements.rb +155 -21
- data/lib/rigor/builtins/predefined_constant_refinements.rb +5 -7
- data/lib/rigor/builtins/regex_refinement.rb +4 -4
- data/lib/rigor/builtins/static_return_refinements.rb +5 -6
- data/lib/rigor/cache/annotation_location.rb +2 -4
- data/lib/rigor/cache/descriptor.rb +58 -20
- data/lib/rigor/cache/engine_source.rb +4 -4
- data/lib/rigor/cache/file_digest.rb +9 -1
- data/lib/rigor/cache/incremental_snapshot.rb +40 -6
- 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 +79 -11
- 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 +77 -32
- 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 +3 -3
- data/lib/rigor/cli/docs_command.rb +2 -2
- 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 +1 -1
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/prism_colorizer.rb +2 -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 +2 -2
- data/lib/rigor/cli/skill_command.rb +1 -1
- data/lib/rigor/cli/skill_deep_probe.rb +4 -4
- data/lib/rigor/cli/skill_describe.rb +36 -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 +10 -6
- 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 +68 -6
- data/lib/rigor/config_audit.rb +30 -5
- data/lib/rigor/configuration/severity_profile.rb +6 -6
- data/lib/rigor/configuration.rb +43 -4
- data/lib/rigor/effects/attribution.rb +1 -3
- data/lib/rigor/effects/config_envelopes.rb +6 -8
- data/lib/rigor/effects/definition_lines.rb +25 -6
- data/lib/rigor/effects/effect_table.rb +0 -1
- data/lib/rigor/effects/entry_points.rb +0 -2
- data/lib/rigor/effects/envelope_check.rb +8 -8
- data/lib/rigor/effects/envelope_index.rb +5 -8
- data/lib/rigor/effects/framework_units.rb +4 -6
- data/lib/rigor/effects/identity.rb +4 -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/method_key.rb +1 -1
- data/lib/rigor/effects/plugin_facts.rb +4 -6
- data/lib/rigor/effects/propagator.rb +2 -3
- data/lib/rigor/effects/scanner.rb +1 -1
- data/lib/rigor/effects/signature_sources.rb +3 -5
- data/lib/rigor/effects/snapshot.rb +10 -10
- data/lib/rigor/effects/snapshot_diff.rb +1 -1
- data/lib/rigor/effects/unit_scan.rb +10 -10
- data/lib/rigor/effects/unknown_label_check.rb +3 -8
- data/lib/rigor/effects/unknown_label_report.rb +3 -4
- 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 +51 -4
- 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 +388 -59
- data/lib/rigor/environment.rb +105 -20
- data/lib/rigor/flow_contribution/merger.rb +0 -2
- data/lib/rigor/flow_contribution.rb +11 -13
- data/lib/rigor/inference/acceptance.rb +153 -8
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/body_fixpoint.rb +5 -5
- data/lib/rigor/inference/budget_trace.rb +1 -5
- data/lib/rigor/inference/captured_locals.rb +2 -3
- data/lib/rigor/inference/closure_escape_analyzer.rb +2 -4
- data/lib/rigor/inference/coverage_scanner.rb +4 -6
- data/lib/rigor/inference/dynamic_origin.rb +1 -1
- data/lib/rigor/inference/element_read_widening.rb +168 -0
- data/lib/rigor/inference/expression_typer.rb +763 -143
- data/lib/rigor/inference/fork_map.rb +5 -7
- 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 +1 -5
- data/lib/rigor/inference/macro_block_self_type.rb +1 -4
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +2 -9
- 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/file_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +2 -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/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 +61 -47
- data/lib/rigor/inference/method_dispatcher/random_folding.rb +82 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +165 -31
- 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/set_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -1
- 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 +4 -5
- 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 +12 -13
- data/lib/rigor/inference/method_parameter_binder.rb +4 -6
- data/lib/rigor/inference/multi_target_binder.rb +1 -3
- data/lib/rigor/inference/mutation_rejoin.rb +168 -0
- data/lib/rigor/inference/mutation_widening.rb +100 -51
- data/lib/rigor/inference/narrowing.rb +299 -46
- data/lib/rigor/inference/optimistic_origin.rb +1 -7
- data/lib/rigor/inference/origin_lookup.rb +2 -3
- data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
- data/lib/rigor/inference/pre_eval_constants.rb +6 -5
- data/lib/rigor/inference/precision_scanner.rb +3 -4
- data/lib/rigor/inference/project_patched_methods.rb +2 -2
- data/lib/rigor/inference/project_patched_scanner.rb +3 -3
- 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 +3 -3
- data/lib/rigor/inference/refinement_mutation.rb +72 -0
- data/lib/rigor/inference/scope_indexer.rb +688 -151
- data/lib/rigor/inference/statement_evaluator.rb +221 -50
- data/lib/rigor/inference/string_mutation.rb +60 -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/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 +1 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
- 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 +2 -2
- data/lib/rigor/language_server/incremental_sync.rb +6 -6
- data/lib/rigor/language_server/project_context.rb +2 -5
- 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/plugin/base.rb +90 -21
- data/lib/rigor/plugin/bundled_catalog.rb +167 -0
- data/lib/rigor/plugin/effect_attribution.rb +7 -7
- 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 +31 -12
- 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 +23 -1
- data/lib/rigor/plugin/registry.rb +42 -12
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +12 -1
- data/lib/rigor/plugin/trust_policy.rb +2 -4
- data/lib/rigor/plugin/type_node_resolver.rb +3 -3
- 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 +11 -11
- data/lib/rigor/protection/kill_signature.rb +2 -4
- data/lib/rigor/protection/measurement_integrity.rb +1 -3
- data/lib/rigor/protection/mutation_cache.rb +6 -9
- 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 +61 -21
- data/lib/rigor/reflection/constant_path.rb +126 -0
- data/lib/rigor/reflection.rb +26 -10
- data/lib/rigor/runtime/jit.rb +6 -8
- data/lib/rigor/scope/discovery_index.rb +30 -0
- data/lib/rigor/scope.rb +203 -4
- data/lib/rigor/sig_gen/generator.rb +115 -22
- 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/observation_collector.rb +17 -15
- 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 +4 -5
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +111 -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/writer.rb +23 -6
- 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 +2 -5
- 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 +68 -5
- data/lib/rigor/type/data_class.rb +2 -2
- data/lib/rigor/type/data_instance.rb +4 -4
- data/lib/rigor/type/float_range.rb +128 -0
- data/lib/rigor/type/hash_shape.rb +5 -5
- data/lib/rigor/type/integer_range.rb +13 -8
- data/lib/rigor/type/nominal.rb +8 -2
- data/lib/rigor/type/refined.rb +3 -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/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.rb +1 -0
- 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 +1 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +33 -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.rb +28 -2
- data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +0 -4
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -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 +1 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +3 -4
- 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 +42 -7
- data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -0
- 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 +119 -14
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +1 -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 +1 -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.rb +36 -4
- data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +8 -6
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +6 -3
- 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 +0 -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 +1 -0
- 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 +1 -0
- 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 +1 -10
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +3 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +2 -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 +1 -0
- 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.rb +128 -3
- 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 +1 -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 +1 -0
- 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 +4 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +5 -5
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -0
- data/sig/rigor/analysis/reachability/scan_cache.rbs +12 -0
- data/sig/rigor/cache.rbs +2 -3
- data/sig/rigor/environment.rbs +5 -0
- data/sig/rigor/inference.rbs +3 -10
- data/sig/rigor/plugin/io_boundary.rbs +4 -0
- data/sig/rigor/plugin/manifest.rbs +2 -1
- data/sig/rigor/reflection.rbs +3 -1
- data/sig/rigor/scope.rbs +24 -3
- data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -0
- data/sig/rigor/type.rbs +27 -1
- data/sig/rigor.rbs +1 -0
- data/skills/rigor-ask/SKILL.md +8 -6
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +13 -2
- data/skills/rigor-project-init/SKILL.md +6 -3
- data/skills/rigor-project-init/references/02-configure.md +10 -6
- data/skills/rigor-project-init/references/06-agent-contract.md +67 -0
- data/skills/rigor-type-oracle/SKILL.md +228 -0
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +261 -0
- data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
- data/skills/rigor-type-oracle/references/03-gap-protocol.md +128 -0
- data/skills/rigor-unused-adjudicate/SKILL.md +8 -5
- metadata +26 -3
- data/lib/rigor/cli/probe_environment.rb +0 -85
- data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
|
@@ -109,23 +109,23 @@ module Rigor
|
|
|
109
109
|
Prism::SelfNode => :type_of_self_node,
|
|
110
110
|
Prism::InstanceVariableReadNode => :type_of_instance_variable_read,
|
|
111
111
|
Prism::InstanceVariableWriteNode => :type_of_assignment_write,
|
|
112
|
-
Prism::InstanceVariableOperatorWriteNode => :
|
|
113
|
-
Prism::InstanceVariableOrWriteNode => :
|
|
114
|
-
Prism::InstanceVariableAndWriteNode => :
|
|
112
|
+
Prism::InstanceVariableOperatorWriteNode => :type_of_compound_variable_write,
|
|
113
|
+
Prism::InstanceVariableOrWriteNode => :type_of_compound_variable_write,
|
|
114
|
+
Prism::InstanceVariableAndWriteNode => :type_of_compound_variable_write,
|
|
115
115
|
Prism::ClassVariableReadNode => :type_of_class_variable_read,
|
|
116
116
|
Prism::ClassVariableWriteNode => :type_of_assignment_write,
|
|
117
|
-
Prism::ClassVariableOperatorWriteNode => :
|
|
118
|
-
Prism::ClassVariableOrWriteNode => :
|
|
119
|
-
Prism::ClassVariableAndWriteNode => :
|
|
117
|
+
Prism::ClassVariableOperatorWriteNode => :type_of_compound_variable_write,
|
|
118
|
+
Prism::ClassVariableOrWriteNode => :type_of_compound_variable_write,
|
|
119
|
+
Prism::ClassVariableAndWriteNode => :type_of_compound_variable_write,
|
|
120
120
|
Prism::GlobalVariableReadNode => :type_of_global_variable_read,
|
|
121
121
|
Prism::GlobalVariableWriteNode => :type_of_assignment_write,
|
|
122
|
-
Prism::GlobalVariableOperatorWriteNode => :
|
|
123
|
-
Prism::GlobalVariableOrWriteNode => :
|
|
124
|
-
Prism::GlobalVariableAndWriteNode => :
|
|
122
|
+
Prism::GlobalVariableOperatorWriteNode => :type_of_compound_variable_write,
|
|
123
|
+
Prism::GlobalVariableOrWriteNode => :type_of_compound_variable_write,
|
|
124
|
+
Prism::GlobalVariableAndWriteNode => :type_of_compound_variable_write,
|
|
125
125
|
# Compound writes that share the `.value` rvalue accessor
|
|
126
|
-
Prism::LocalVariableOperatorWriteNode => :
|
|
127
|
-
Prism::LocalVariableOrWriteNode => :
|
|
128
|
-
Prism::LocalVariableAndWriteNode => :
|
|
126
|
+
Prism::LocalVariableOperatorWriteNode => :type_of_compound_variable_write,
|
|
127
|
+
Prism::LocalVariableOrWriteNode => :type_of_compound_variable_write,
|
|
128
|
+
Prism::LocalVariableAndWriteNode => :type_of_compound_variable_write,
|
|
129
129
|
Prism::IndexOperatorWriteNode => :type_of_assignment_write,
|
|
130
130
|
Prism::IndexOrWriteNode => :type_of_assignment_write,
|
|
131
131
|
Prism::IndexAndWriteNode => :type_of_assignment_write,
|
|
@@ -201,7 +201,7 @@ module Rigor
|
|
|
201
201
|
Prism::NextNode => :type_of_jump,
|
|
202
202
|
Prism::RetryNode => :type_of_jump,
|
|
203
203
|
Prism::RedoNode => :type_of_jump,
|
|
204
|
-
Prism::YieldNode => :
|
|
204
|
+
Prism::YieldNode => :type_of_yield,
|
|
205
205
|
Prism::SuperNode => :type_of_dynamic_top,
|
|
206
206
|
Prism::ForwardingArgumentsNode => :type_of_non_value,
|
|
207
207
|
Prism::WhileNode => :type_of_loop,
|
|
@@ -287,6 +287,32 @@ module Rigor
|
|
|
287
287
|
slot[1].transform_values(&:values)
|
|
288
288
|
end
|
|
289
289
|
|
|
290
|
+
# Issue #720 — the value the block passed by the call site currently being re-typed evaluates to, or
|
|
291
|
+
# `nil` outside such a frame. Read by {#type_of_yield}.
|
|
292
|
+
#
|
|
293
|
+
# A thread-local rather than a `Scope` field for the same reason the `return` / `next` / `break` sinks
|
|
294
|
+
# are: `yield` names the frame's caller, not a binding, and the body is walked through scopes the
|
|
295
|
+
# inference rebuilds freely. It is installed unconditionally — with `nil` — at every user-method
|
|
296
|
+
# inference frame, so a callee reached WITHOUT a block from inside a yielding body cannot read its
|
|
297
|
+
# caller's block.
|
|
298
|
+
YIELD_VALUE_KEY = :__rigor_yield_value_type__
|
|
299
|
+
private_constant :YIELD_VALUE_KEY
|
|
300
|
+
|
|
301
|
+
def self.current_yield_value_type
|
|
302
|
+
Thread.current[YIELD_VALUE_KEY]
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Runs `block` with `type` installed as the frame's yield value, restoring the previous frame on exit.
|
|
306
|
+
def self.with_yield_value_type(type)
|
|
307
|
+
previous = Thread.current[YIELD_VALUE_KEY]
|
|
308
|
+
Thread.current[YIELD_VALUE_KEY] = type
|
|
309
|
+
begin
|
|
310
|
+
yield
|
|
311
|
+
ensure
|
|
312
|
+
Thread.current[YIELD_VALUE_KEY] = previous
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
290
316
|
private
|
|
291
317
|
|
|
292
318
|
attr_reader :scope, :tracer
|
|
@@ -318,6 +344,71 @@ module Rigor
|
|
|
318
344
|
type_of(node.value)
|
|
319
345
|
end
|
|
320
346
|
|
|
347
|
+
# `v += 1` / `@v ||= x` / `$v &&= x` as an EXPRESSION. The value of a compound write is the value it
|
|
348
|
+
# stores, which is a function of the variable's CURRENT binding — not of the rvalue alone. Typing it as
|
|
349
|
+
# `type_of(node.value)` answered `1` for `total += 1` whatever `total` held.
|
|
350
|
+
#
|
|
351
|
+
# `StatementEvaluator#compound_eval` already computes the right answer on the straight-line path, so the
|
|
352
|
+
# gap only showed where a body is typed WITHOUT that evaluator: the block-return pass types a
|
|
353
|
+
# single-statement body by expression alone, and issue #617 residue (3) is the live consequence —
|
|
354
|
+
# `total = 0; r = [1, 2].map { total += 1 }` folded to `[1, 1]`, and `r.last == 1` fired a false
|
|
355
|
+
# `flow.always-truthy-condition` on a program where it is `2`. Under the per-element fold the captured
|
|
356
|
+
# local is already bound to its converged `Integer` (issue #587 (b)); reading that binding here is what
|
|
357
|
+
# turns the stale pin into the honest `Integer`.
|
|
358
|
+
#
|
|
359
|
+
# The algebra is `StatementEvaluator#compound_result_type`'s, deliberately: the two must not disagree
|
|
360
|
+
# about what a compound write evaluates to. An unbound target is `Dynamic[top]` — the same fallback the
|
|
361
|
+
# evaluator takes — and an operator the receiver does not answer widens to `Dynamic[top]` rather than
|
|
362
|
+
# inventing the rvalue.
|
|
363
|
+
#
|
|
364
|
+
# Constant and index targets keep {#type_of_assignment_write}: a constant is not rebound in a loop body,
|
|
365
|
+
# and `IndexOperatorWriteNode` is typed through `Scope#type_of`'s own indexed path by
|
|
366
|
+
# `StatementEvaluator#eval_index_write`.
|
|
367
|
+
def type_of_compound_variable_write(node)
|
|
368
|
+
current = compound_write_current_binding(node)
|
|
369
|
+
rhs = type_of(node.value)
|
|
370
|
+
|
|
371
|
+
case node
|
|
372
|
+
when Prism::LocalVariableOrWriteNode, Prism::InstanceVariableOrWriteNode,
|
|
373
|
+
Prism::ClassVariableOrWriteNode, Prism::GlobalVariableOrWriteNode
|
|
374
|
+
# An UNBOUND target is the memoization idiom (`def self.default = @default ||= new`): nothing
|
|
375
|
+
# has written the variable on any path the analyzer saw, so the stored value is the rvalue.
|
|
376
|
+
# Reading it as `Dynamic[top] | rhs` would skip every memoized singleton in `sig-gen`
|
|
377
|
+
# (ADR-5 optimism; three `.default` readers went `sig.skipped.untyped-return` without this).
|
|
378
|
+
return rhs if current.nil?
|
|
379
|
+
|
|
380
|
+
Type::Combinator.union(Narrowing.narrow_truthy(current), rhs)
|
|
381
|
+
when Prism::LocalVariableAndWriteNode, Prism::InstanceVariableAndWriteNode,
|
|
382
|
+
Prism::ClassVariableAndWriteNode, Prism::GlobalVariableAndWriteNode
|
|
383
|
+
return rhs if current.nil?
|
|
384
|
+
|
|
385
|
+
Type::Combinator.union(Narrowing.narrow_falsey(current), rhs)
|
|
386
|
+
else
|
|
387
|
+
compound_operator_result(current || dynamic_top, rhs, node.binary_operator)
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def compound_write_current_binding(node)
|
|
392
|
+
case node
|
|
393
|
+
when Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode,
|
|
394
|
+
Prism::LocalVariableAndWriteNode then scope.local(node.name)
|
|
395
|
+
when Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableOrWriteNode,
|
|
396
|
+
Prism::InstanceVariableAndWriteNode then scope.ivar(node.name)
|
|
397
|
+
when Prism::ClassVariableOperatorWriteNode, Prism::ClassVariableOrWriteNode,
|
|
398
|
+
Prism::ClassVariableAndWriteNode then scope.cvar(node.name)
|
|
399
|
+
else scope.global(node.name)
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def compound_operator_result(current, rhs, operator)
|
|
404
|
+
MethodDispatcher.dispatch(
|
|
405
|
+
receiver_type: current,
|
|
406
|
+
method_name: operator.to_sym,
|
|
407
|
+
arg_types: [rhs],
|
|
408
|
+
environment: scope.environment
|
|
409
|
+
) || dynamic_top
|
|
410
|
+
end
|
|
411
|
+
|
|
321
412
|
# Slice 7 phase 1 — instance/class/global variable reads. Each lookup returns the type currently bound
|
|
322
413
|
# in the surrounding scope's per-kind binding map (populated by `StatementEvaluator` write handlers
|
|
323
414
|
# within the same method body), falling through to `Dynamic[Top]` when no binding is recorded.
|
|
@@ -372,6 +463,19 @@ module Rigor
|
|
|
372
463
|
dynamic_top
|
|
373
464
|
end
|
|
374
465
|
|
|
466
|
+
# Issue #720 — `yield` evaluates the block the CALLER passed, so its value is that block's value. The
|
|
467
|
+
# caller is only known when this body is being re-typed on behalf of one call site
|
|
468
|
+
# ({#infer_user_method_return} installs the frame); on the main walk over a `def` no caller exists and
|
|
469
|
+
# the answer stays `Dynamic[top]`.
|
|
470
|
+
#
|
|
471
|
+
# Nothing about the yield's arguments is checked here. The block's return type was computed at the call
|
|
472
|
+
# site under the parameter bindings the call implies, so it already answers "what does this block
|
|
473
|
+
# evaluate to for this caller"; a `yield` that passes different arguments than the block's parameters
|
|
474
|
+
# accept is a separate (arity) question this frame does not decide.
|
|
475
|
+
def type_of_yield(_node)
|
|
476
|
+
ExpressionTyper.current_yield_value_type || dynamic_top
|
|
477
|
+
end
|
|
478
|
+
|
|
375
479
|
# `defined?(expr)` returns `String | nil` per Ruby semantics — a description of the expression's
|
|
376
480
|
# category (`"local-variable"`, `"method"`, ...) when defined, or `nil` when not. The argument is not
|
|
377
481
|
# evaluated (it is statically inspected by the runtime), so the typer does not recurse into it.
|
|
@@ -843,9 +947,21 @@ module Rigor
|
|
|
843
947
|
#
|
|
844
948
|
# Other pattern shapes (Range, Regexp, custom `===`) stay `:maybe` — the existing union fallback
|
|
845
949
|
# handles them.
|
|
950
|
+
#
|
|
951
|
+
# The class reference is resolved through {Narrowing.lexical_class_name}, the walk the narrowing
|
|
952
|
+
# side of the same `case` already uses (#655). Matching on the as-written spelling instead made a
|
|
953
|
+
# SHADOWED name confidently wrong rather than merely unresolved: `when Random` inside
|
|
954
|
+
# `Bar::Nested` names `Bar::Nested::Random`, the environment answered the core `::Random` the
|
|
955
|
+
# spelling matches at top level, and a subject that is a core `Random` then made the arm certain
|
|
956
|
+
# — dropping the `else` Ruby actually takes and typing the expression as a branch that never
|
|
957
|
+
# runs. The documented argument that an unresolvable pattern can only lose certainty holds for an
|
|
958
|
+
# unresolvable name and not for a shadowed one.
|
|
846
959
|
def case_when_pattern_certainty(subject_type, pattern_node)
|
|
847
|
-
class_name =
|
|
848
|
-
|
|
960
|
+
class_name = Narrowing.lexical_class_name(pattern_node, scope)
|
|
961
|
+
if class_name
|
|
962
|
+
return Narrowing.class_pattern_certainty(subject_type, class_name, environment: scope.environment,
|
|
963
|
+
scope: scope)
|
|
964
|
+
end
|
|
849
965
|
|
|
850
966
|
literal = literal_pattern_value(pattern_node)
|
|
851
967
|
return Narrowing.value_pattern_certainty(subject_type, literal[:value]) if literal
|
|
@@ -963,6 +1079,8 @@ module Rigor
|
|
|
963
1079
|
Type::Combinator.nominal_of(value.class.name)
|
|
964
1080
|
when Type::IntegerRange
|
|
965
1081
|
Type::Combinator.nominal_of("Integer")
|
|
1082
|
+
when Type::FloatRange
|
|
1083
|
+
Type::Combinator.nominal_of("Float")
|
|
966
1084
|
else
|
|
967
1085
|
type
|
|
968
1086
|
end
|
|
@@ -1177,12 +1295,12 @@ module Rigor
|
|
|
1177
1295
|
# and fired `undefined method 'upcase' for nil` on correct code. The candidate is still looked up
|
|
1178
1296
|
# first — that lookup is a hash probe and owns the ADR-46 cross-file dependency edge — and
|
|
1179
1297
|
# {#self_type_answers?} then vetoes the bind for a name the enclosing class answers itself.
|
|
1180
|
-
def try_local_def_dispatch(node, receiver, arg_types)
|
|
1298
|
+
def try_local_def_dispatch(node, receiver, arg_types, block_type = nil)
|
|
1181
1299
|
local_def = node.receiver.nil? ? scope.bindable_top_level_def_for(node.name) : nil
|
|
1182
1300
|
return nil unless local_def
|
|
1183
1301
|
return nil if self_type_answers?(node.name)
|
|
1184
1302
|
|
|
1185
|
-
local_inference = infer_top_level_user_method(local_def, receiver, arg_types)
|
|
1303
|
+
local_inference = infer_top_level_user_method(local_def, receiver, arg_types, block_type)
|
|
1186
1304
|
return local_inference if local_inference
|
|
1187
1305
|
|
|
1188
1306
|
# The local def matches by name but the inference was disqualified — the parameter shape is too
|
|
@@ -1194,6 +1312,55 @@ module Rigor
|
|
|
1194
1312
|
dynamic_top
|
|
1195
1313
|
end
|
|
1196
1314
|
|
|
1315
|
+
# ADR-110 WD1 / #856 — the receiver's own `def` outranks a declaration that only an ANCESTOR carries.
|
|
1316
|
+
#
|
|
1317
|
+
# RBS resolves through the fully built method table, so `RbsDispatch` answers a subclass call with a
|
|
1318
|
+
# signature written about the base. redmine's `FieldFormat::Base#target_class` honestly declares
|
|
1319
|
+
# `-> nil`; `RecordList` overrides it with a real lookup, and every call on a `RecordList` receiver
|
|
1320
|
+
# typed as `nil` and fired `undefined method ... for nil` on the subclass's own working code (#744).
|
|
1321
|
+
# A signature is authoritative for a receiver only where someone wrote it about that receiver;
|
|
1322
|
+
# reaching it through an ancestor is a lookup convenience, not an act of authorship.
|
|
1323
|
+
#
|
|
1324
|
+
# This is {#try_local_def_dispatch}'s shape one level up — there the enclosing class shadows a
|
|
1325
|
+
# top-level `def`, here the receiver's class shadows an ancestor's declaration — and it answers the
|
|
1326
|
+
# same way: re-type the override's body, and when that is not possible answer `Dynamic[Top]` rather
|
|
1327
|
+
# than let the inherited declaration speak for a method it does not describe.
|
|
1328
|
+
#
|
|
1329
|
+
# Three conditions, and the third is the blast radius. Clause A of the ADR (an own source `def`, no
|
|
1330
|
+
# own declaration) does NOT by itself stay clear of [ADR-43]: `class Foo; def each; end` inheriting
|
|
1331
|
+
# `Enumerable#each` satisfies it, and disqualifying bundled declarations that way is the blanket fix
|
|
1332
|
+
# ADR-43 rejected — the same reason {#instance_self_answers?}'s RBS arm is own-class only. So the
|
|
1333
|
+
# ancestor whose declaration is being disqualified must itself be PROJECT-declared: a project sidecar
|
|
1334
|
+
# describes the source under analysis, while a bundled signature describes a class the project does
|
|
1335
|
+
# not own, where a project `def` is a monkey-patch and [ADR-17] owns the question.
|
|
1336
|
+
# `project_declared_class?` fail-softs to false, so an unattributable environment changes nothing.
|
|
1337
|
+
def try_overriding_def_dispatch(node, receiver, arg_types)
|
|
1338
|
+
return nil unless user_inference_receiver?(receiver)
|
|
1339
|
+
|
|
1340
|
+
class_name = receiver.class_name
|
|
1341
|
+
return nil if class_name.nil?
|
|
1342
|
+
# `Scope#user_def_for`, not `discovered_method?`: the cross-file table deliberately withholds a
|
|
1343
|
+
# plain instance `def` under the ADR-17 monkey-patch contract, and this gate must see one.
|
|
1344
|
+
return nil if scope.user_def_for(class_name, node.name).nil?
|
|
1345
|
+
|
|
1346
|
+
definition = safe_rbs_method_definition(class_name, node.name, :instance)
|
|
1347
|
+
return nil if definition.nil?
|
|
1348
|
+
return nil if rbs_declared_on_class?(definition, class_name)
|
|
1349
|
+
return nil unless project_declared_owner?(definition)
|
|
1350
|
+
|
|
1351
|
+
try_user_method_inference(receiver, node, arg_types) || dynamic_top
|
|
1352
|
+
end
|
|
1353
|
+
|
|
1354
|
+
# Whether the class an inherited declaration was written about is one the project declares itself.
|
|
1355
|
+
def project_declared_owner?(definition)
|
|
1356
|
+
defined_in = definition.defined_in
|
|
1357
|
+
return false if defined_in.nil?
|
|
1358
|
+
|
|
1359
|
+
Rigor::Reflection.project_declared_class?(defined_in.to_s, scope: scope)
|
|
1360
|
+
rescue StandardError
|
|
1361
|
+
false
|
|
1362
|
+
end
|
|
1363
|
+
|
|
1197
1364
|
# Issue #618 — whether the call's enclosing `self` already answers `method_name`. Only a `self` whose
|
|
1198
1365
|
# class is KNOWN participates: at genuine top level, and inside a block whose `self` is unmodelled,
|
|
1199
1366
|
# `scope.self_type` is nil, the predicate is false, and the historical top-level binding stands — that
|
|
@@ -1212,30 +1379,65 @@ module Rigor
|
|
|
1212
1379
|
end
|
|
1213
1380
|
end
|
|
1214
1381
|
|
|
1215
|
-
# The instance side of {#self_type_answers?}: the
|
|
1216
|
-
# `define_method`, `alias`), its `Struct.new` / `Data.define`
|
|
1217
|
-
# its project ancestors (superclass chain and included
|
|
1218
|
-
#
|
|
1382
|
+
# The instance side of {#self_type_answers?}: the discovered methods (`def`, `attr_*`,
|
|
1383
|
+
# `define_method`, `alias`) of the class or any project ancestor, its `Struct.new` / `Data.define`
|
|
1384
|
+
# member accessors, a `def` reached through its project ancestors (superclass chain and included
|
|
1385
|
+
# modules), and an RBS method declared on an owner that PRECEDES `::Object` in the MRO.
|
|
1219
1386
|
#
|
|
1220
|
-
#
|
|
1221
|
-
#
|
|
1222
|
-
#
|
|
1223
|
-
#
|
|
1387
|
+
# Issue #633 — the discovery arm walks ancestors because inheritance does not distinguish how a
|
|
1388
|
+
# method got there: a superclass `attr_accessor :stext` and an included module's `attr_reader` bind
|
|
1389
|
+
# ahead of a top-level `def stext` exactly as an inherited `def` does, and only the latter had a walk.
|
|
1390
|
+
#
|
|
1391
|
+
# The RBS arm's cut-off is `::Object`, not the own class. A top-level `def` IS a private `Object`
|
|
1392
|
+
# instance method, the last link of every MRO, so any declared owner that comes before `::Object`
|
|
1393
|
+
# wins at runtime — `Exception#message` for a `StandardError` subclass, `Array#first`,
|
|
1394
|
+
# `Comparable#clamp`. What the cut-off keeps out is the tier v0.0.3 A exists for and #316 / #319
|
|
1395
|
+
# refined: a name owned by `Object` or `Kernel` themselves (`inspect`, `format`) is at or after the
|
|
1396
|
+
# top-level `def`'s own rung, so the historical binding still stands there.
|
|
1224
1397
|
def instance_self_answers?(class_name, method_name)
|
|
1225
1398
|
return false if class_name.nil?
|
|
1226
|
-
return true if scope.
|
|
1399
|
+
return true if scope.discovered_method_through_ancestors?(class_name, method_name, :instance)
|
|
1227
1400
|
return true if meta_member?(class_name, method_name)
|
|
1228
1401
|
return true if resolve_user_def_through_ancestors(class_name, method_name)
|
|
1229
1402
|
|
|
1230
|
-
|
|
1403
|
+
rbs_ancestor_answers?(class_name, method_name)
|
|
1404
|
+
end
|
|
1405
|
+
|
|
1406
|
+
# The RBS arm of {#instance_self_answers?}. A project class is usually absent from the RBS
|
|
1407
|
+
# environment, so the declaration that decides the question is written about an ancestor the project
|
|
1408
|
+
# does not declare (`StandardError`, `Array`, `Comparable`) — each is asked on its own terms, and
|
|
1409
|
+
# each of them is itself before `::Object` in the reader's MRO by construction.
|
|
1410
|
+
def rbs_ancestor_answers?(class_name, method_name)
|
|
1411
|
+
definition = safe_rbs_method_definition(class_name, method_name, :instance)
|
|
1412
|
+
return true if rbs_declared_before_object?(definition, class_name)
|
|
1413
|
+
|
|
1414
|
+
scope.external_ancestor_name_candidates(class_name, name_memo: class_graph_buckets[:name])
|
|
1415
|
+
.any? { |candidates| external_ancestor_answers?(candidates, method_name) }
|
|
1416
|
+
end
|
|
1417
|
+
|
|
1418
|
+
# The first candidate spelling the RBS environment knows is the ancestor Ruby resolves; a name it
|
|
1419
|
+
# knows nothing about contributes no evidence either way.
|
|
1420
|
+
def external_ancestor_answers?(candidates, method_name)
|
|
1421
|
+
candidates.each do |candidate|
|
|
1422
|
+
next if instance_ancestor_names(candidate).empty?
|
|
1423
|
+
|
|
1424
|
+
return rbs_declared_before_object?(
|
|
1425
|
+
safe_rbs_method_definition(candidate, method_name, :instance), candidate
|
|
1426
|
+
)
|
|
1427
|
+
end
|
|
1428
|
+
false
|
|
1231
1429
|
end
|
|
1232
1430
|
|
|
1233
1431
|
# The singleton side: a class-body `self` is `Singleton[Foo]`, where an implicit-self call reaches
|
|
1234
1432
|
# `Foo`'s own class methods before `Object`'s private top-level `def`.
|
|
1433
|
+
#
|
|
1434
|
+
# Issue #633 — both discovery arms follow the superclass chain, because a subclass inherits its
|
|
1435
|
+
# parent's class methods; `Scope` narrows the singleton walk to superclasses for the same reason
|
|
1436
|
+
# (an `include`d module's `def self.x` is not callable on the includer).
|
|
1235
1437
|
def singleton_self_answers?(class_name, method_name)
|
|
1236
1438
|
return false if class_name.nil?
|
|
1237
|
-
return true if scope.
|
|
1238
|
-
return true unless scope.
|
|
1439
|
+
return true if scope.discovered_method_through_ancestors?(class_name, method_name, :singleton)
|
|
1440
|
+
return true unless scope.singleton_def_through_ancestors(class_name, method_name).first.nil?
|
|
1239
1441
|
|
|
1240
1442
|
rbs_declared_on_class?(safe_rbs_method_definition(class_name, method_name, :singleton), class_name)
|
|
1241
1443
|
end
|
|
@@ -1272,6 +1474,36 @@ module Rigor
|
|
|
1272
1474
|
defined_in.to_s.delete_prefix("::") == class_name.to_s.delete_prefix("::")
|
|
1273
1475
|
end
|
|
1274
1476
|
|
|
1477
|
+
# Issue #633 — true when the declaration's owner sits strictly before `::Object` in `class_name`'s
|
|
1478
|
+
# instance MRO, i.e. Ruby dispatches to it ahead of a top-level `def` (which is `Object`'s own
|
|
1479
|
+
# private instance method). The own class trivially qualifies. An owner absent from the ancestor
|
|
1480
|
+
# list, an unbuildable class, and an ancestry that does not reach `Object` (a `BasicObject`
|
|
1481
|
+
# descendant) all answer false, leaving the historical top-level binding untouched.
|
|
1482
|
+
def rbs_declared_before_object?(definition, class_name)
|
|
1483
|
+
return true if rbs_declared_on_class?(definition, class_name)
|
|
1484
|
+
return false if definition.nil? || !definition.respond_to?(:defined_in)
|
|
1485
|
+
|
|
1486
|
+
owner = definition.defined_in
|
|
1487
|
+
return false if owner.nil?
|
|
1488
|
+
|
|
1489
|
+
ancestors = instance_ancestor_names(class_name)
|
|
1490
|
+
object_index = ancestors.index("Object")
|
|
1491
|
+
owner_index = ancestors.index(owner.to_s.delete_prefix("::"))
|
|
1492
|
+
!object_index.nil? && !owner_index.nil? && owner_index < object_index
|
|
1493
|
+
end
|
|
1494
|
+
|
|
1495
|
+
# The class's instance-side ancestors in MRO order, `::`-stripped, or `[]` for a class the RBS
|
|
1496
|
+
# environment does not know or cannot build. Read through the loader's accessor rather than
|
|
1497
|
+
# `instance_definition(...).ancestors` because that is the one wired to the ancestor-name cache and
|
|
1498
|
+
# marked as RIGOR'S OWN demand — ordering two ancestors is not the analysis asking whether either
|
|
1499
|
+
# one's methods resolve, and the `rbs.coverage` bookkeeping must not record it as such.
|
|
1500
|
+
def instance_ancestor_names(class_name)
|
|
1501
|
+
loader = scope.environment&.rbs_loader
|
|
1502
|
+
loader ? loader.ancestor_names_for(class_name.to_s) : []
|
|
1503
|
+
rescue StandardError
|
|
1504
|
+
[]
|
|
1505
|
+
end
|
|
1506
|
+
|
|
1275
1507
|
# Issue #520 — Ruby defines the value of an attribute / index assignment (`x.attr = v`, `h[k] = v`)
|
|
1276
1508
|
# as the RHS object itself, whatever the writer method returns. The dispatch pipeline still runs
|
|
1277
1509
|
# first for everything it observes on the side (effect collection, provenance, recorders, and the
|
|
@@ -1376,7 +1608,22 @@ module Rigor
|
|
|
1376
1608
|
# `receiver_override` substitutes the receiver type for the whole pipeline (folds, dispatch, the
|
|
1377
1609
|
# inference tiers) without re-reading the receiver node — the safe-navigation path (#518) dispatches
|
|
1378
1610
|
# on the nil-stripped fragment, and the optional-receiver retry (#519) re-runs the pipeline on it.
|
|
1611
|
+
# Issue #853: `break value` terminates the yielding CALL and is that call's value, so the answer every
|
|
1612
|
+
# tier below produces — an RBS return, a precision fold, an inter-procedural inference — describes only
|
|
1613
|
+
# the path where the block never breaks. The call's value is that result unioned with every `break` arm
|
|
1614
|
+
# the block can reach. Modelling it here rather than inside each fold is what keeps the folds honest:
|
|
1615
|
+
# `ops.all? { |o| break false unless o; true }` still folds the no-break path to `Constant[true]`, and
|
|
1616
|
+
# the union with the `false` arm makes the call `bool` — no `flow.always-truthy-condition` on a program
|
|
1617
|
+
# that really can answer false.
|
|
1379
1618
|
def call_dispatch_type_for(node, receiver_override: nil)
|
|
1619
|
+
result = call_result_type_for(node, receiver_override: receiver_override)
|
|
1620
|
+
arms = call_break_arm_types(node, receiver_override: receiver_override)
|
|
1621
|
+
return result if arms.empty?
|
|
1622
|
+
|
|
1623
|
+
Type::Combinator.union(result, *arms)
|
|
1624
|
+
end
|
|
1625
|
+
|
|
1626
|
+
def call_result_type_for(node, receiver_override: nil)
|
|
1380
1627
|
narrowed = indexed_narrowing_for(node)
|
|
1381
1628
|
return narrowed if narrowed
|
|
1382
1629
|
|
|
@@ -1392,7 +1639,7 @@ module Rigor
|
|
|
1392
1639
|
literal_send = try_literal_send(node, receiver)
|
|
1393
1640
|
return literal_send if literal_send
|
|
1394
1641
|
|
|
1395
|
-
local_def_result = try_local_def_dispatch(node, receiver, arg_types)
|
|
1642
|
+
local_def_result = try_local_def_dispatch(node, receiver, arg_types, block_type)
|
|
1396
1643
|
return local_def_result if local_def_result
|
|
1397
1644
|
|
|
1398
1645
|
# v0.0.6 phase 2 — per-element block fold for Tuple receivers. When `[a, b, c].map { |x| f(x) }` and
|
|
@@ -1403,6 +1650,9 @@ module Rigor
|
|
|
1403
1650
|
block_fold = try_receiver_block_folds(node, receiver, arg_types)
|
|
1404
1651
|
return block_fold if block_fold
|
|
1405
1652
|
|
|
1653
|
+
overriding_def = try_overriding_def_dispatch(node, receiver, arg_types)
|
|
1654
|
+
return overriding_def if overriding_def
|
|
1655
|
+
|
|
1406
1656
|
result = MethodDispatcher.dispatch(
|
|
1407
1657
|
receiver_type: receiver,
|
|
1408
1658
|
method_name: node.name,
|
|
@@ -1414,16 +1664,16 @@ module Rigor
|
|
|
1414
1664
|
)
|
|
1415
1665
|
return result if result
|
|
1416
1666
|
|
|
1417
|
-
dispatch_miss_result(node, receiver, arg_types)
|
|
1667
|
+
dispatch_miss_result(node, receiver, arg_types, block_type)
|
|
1418
1668
|
end
|
|
1419
1669
|
|
|
1420
1670
|
# The post-dispatch tiers for a call `MethodDispatcher` could not answer, in their historical
|
|
1421
1671
|
# order; extracted from {#call_dispatch_type_for} whole.
|
|
1422
|
-
def dispatch_miss_result(node, receiver, arg_types)
|
|
1672
|
+
def dispatch_miss_result(node, receiver, arg_types, block_type = nil)
|
|
1423
1673
|
# v0.0.2 #5 — inter-procedural inference for user-defined methods. When dispatch misses but the
|
|
1424
1674
|
# receiver is a user class with a `def` body, re-type the body with the call's argument types bound
|
|
1425
1675
|
# and return the body's last-expression type.
|
|
1426
|
-
user_inference = try_user_method_inference(receiver, node, arg_types)
|
|
1676
|
+
user_inference = try_user_method_inference(receiver, node, arg_types, block_type: block_type)
|
|
1427
1677
|
return user_inference if user_inference
|
|
1428
1678
|
|
|
1429
1679
|
# Module-singleton call resolution (ADR-57 follow-up) — when the receiver is `Singleton[Foo]` (a
|
|
@@ -1431,7 +1681,7 @@ module Rigor
|
|
|
1431
1681
|
# `module_function` body, re-type that body with the call args bound. Sits after the RBS dispatch
|
|
1432
1682
|
# tier, so foreign / RBS-known singletons (`Math.sqrt`) keep their catalog answer; only
|
|
1433
1683
|
# project-defined singleton methods reach here.
|
|
1434
|
-
singleton_inference = try_project_singleton_inference(receiver, node, arg_types)
|
|
1684
|
+
singleton_inference = try_project_singleton_inference(receiver, node, arg_types, block_type: block_type)
|
|
1435
1685
|
return singleton_inference if singleton_inference
|
|
1436
1686
|
|
|
1437
1687
|
# Dynamic-origin propagation: when the receiver is Dynamic[T] and no positive rule resolves the call,
|
|
@@ -1634,8 +1884,8 @@ module Rigor
|
|
|
1634
1884
|
# `scope.self_type` (or implicit `Object`) as the receiver carrier so the body's own self is
|
|
1635
1885
|
# consistent with the call site's. Returns nil when the parameter shape disqualifies the def, when the
|
|
1636
1886
|
# 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)
|
|
1887
|
+
def infer_top_level_user_method(def_node, receiver, arg_types, block_type = nil)
|
|
1888
|
+
infer_user_method_return(def_node, receiver, arg_types, yield_type: block_type)
|
|
1639
1889
|
rescue StandardError
|
|
1640
1890
|
nil
|
|
1641
1891
|
end
|
|
@@ -1745,14 +1995,15 @@ module Rigor
|
|
|
1745
1995
|
try_project_singleton_inference(receiver, node, inner_args, method_name: inner_name)
|
|
1746
1996
|
end
|
|
1747
1997
|
|
|
1748
|
-
def try_user_method_inference(receiver, call_node, arg_types, method_name: call_node.name)
|
|
1998
|
+
def try_user_method_inference(receiver, call_node, arg_types, method_name: call_node.name, block_type: nil)
|
|
1749
1999
|
return nil unless user_inference_receiver?(receiver)
|
|
1750
2000
|
|
|
1751
2001
|
def_node, owner = resolve_user_def_with_owner(receiver.class_name, method_name)
|
|
1752
2002
|
return nil if def_node.nil?
|
|
1753
2003
|
|
|
1754
2004
|
result = infer_user_method_return(def_node, receiver, arg_types,
|
|
1755
|
-
self_fold_safe: fold_safe_call_receiver?(call_node, receiver)
|
|
2005
|
+
self_fold_safe: fold_safe_call_receiver?(call_node, receiver),
|
|
2006
|
+
yield_type: block_type)
|
|
1756
2007
|
return result if result.nil?
|
|
1757
2008
|
|
|
1758
2009
|
degrade_if_overridable(result, owner, method_name, :instance)
|
|
@@ -1823,13 +2074,14 @@ module Rigor
|
|
|
1823
2074
|
# The OWNER — not the receiver class — is what the overridable gate keys on, exactly as the instance
|
|
1824
2075
|
# side does: adopting `Base`'s literal return is unsound when a subclass redefines the method, and
|
|
1825
2076
|
# 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
|
|
2077
|
+
def try_singleton_method_inference(receiver, call_node, arg_types, method_name: call_node.name,
|
|
2078
|
+
block_type: nil)
|
|
1827
2079
|
return nil unless receiver.is_a?(Type::Singleton)
|
|
1828
2080
|
|
|
1829
2081
|
def_node, owner = resolve_singleton_def_with_owner(receiver.class_name, method_name)
|
|
1830
2082
|
return nil if def_node.nil?
|
|
1831
2083
|
|
|
1832
|
-
result = infer_user_method_return(def_node, receiver, arg_types)
|
|
2084
|
+
result = infer_user_method_return(def_node, receiver, arg_types, yield_type: block_type)
|
|
1833
2085
|
return result if result.nil?
|
|
1834
2086
|
|
|
1835
2087
|
degrade_if_overridable(result, owner, method_name, :singleton)
|
|
@@ -1857,9 +2109,11 @@ module Rigor
|
|
|
1857
2109
|
# body the project itself wrote. Which of the two tiers applies is decided by the receiver carrier —
|
|
1858
2110
|
# `Singleton[Foo]` when the constant names a class or module, `Nominal[…]` when it holds an ordinary
|
|
1859
2111
|
# 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
|
-
|
|
2112
|
+
def try_project_singleton_inference(receiver, call_node, arg_types, method_name: call_node.name,
|
|
2113
|
+
block_type: nil)
|
|
2114
|
+
kwargs = { method_name: method_name, block_type: block_type }
|
|
2115
|
+
try_singleton_method_inference(receiver, call_node, arg_types, **kwargs) ||
|
|
2116
|
+
try_singleton_object_constant_inference(receiver, call_node, arg_types, **kwargs)
|
|
1863
2117
|
end
|
|
1864
2118
|
|
|
1865
2119
|
# #320 — resolves a call whose receiver is a constant holding an ordinary object with a `class << Const`
|
|
@@ -1867,13 +2121,14 @@ module Rigor
|
|
|
1867
2121
|
# that object, so the receiver carrier is passed through unchanged. Own-constant only, and only for a
|
|
1868
2122
|
# name the project actually recorded — a miss degrades to today's `Dynamic[top]`, never a false
|
|
1869
2123
|
# 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
|
|
2124
|
+
def try_singleton_object_constant_inference(receiver, call_node, arg_types, method_name: call_node.name,
|
|
2125
|
+
block_type: nil)
|
|
1871
2126
|
return nil unless receiver.is_a?(Type::Nominal)
|
|
1872
2127
|
|
|
1873
2128
|
def_node = SingletonObjectConstant.def_node_for(call_node, receiver, method_name, scope)
|
|
1874
2129
|
return nil if def_node.nil?
|
|
1875
2130
|
|
|
1876
|
-
infer_user_method_return(def_node, receiver, arg_types)
|
|
2131
|
+
infer_user_method_return(def_node, receiver, arg_types, yield_type: block_type)
|
|
1877
2132
|
rescue StandardError
|
|
1878
2133
|
nil
|
|
1879
2134
|
end
|
|
@@ -1907,7 +2162,8 @@ module Rigor
|
|
|
1907
2162
|
# `self_pure` is issue #525's grant scan (identity-keyed by def node); it belongs here because it
|
|
1908
2163
|
# is a pure function of the same frozen index trio — the sibling resolver it walks reads nothing
|
|
1909
2164
|
# else.
|
|
1910
|
-
by_super[scope.discovered_includes] ||=
|
|
2165
|
+
by_super[scope.discovered_includes] ||=
|
|
2166
|
+
{ name: {}, user_def: {}, self_pure: {}.compare_by_identity, yields: {}.compare_by_identity }
|
|
1911
2167
|
end
|
|
1912
2168
|
|
|
1913
2169
|
def resolve_user_def_through_ancestors(class_name, method_name)
|
|
@@ -2078,14 +2334,6 @@ module Rigor
|
|
|
2078
2334
|
INFERENCE_SUMMARY_KEY = :__rigor_user_method_return_summary__
|
|
2079
2335
|
private_constant :INFERENCE_SUMMARY_KEY
|
|
2080
2336
|
|
|
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
2337
|
# Per-thread append-only log of the seed depths of every in-flight summary `consult_summary` read
|
|
2090
2338
|
# (ADR-55 slice 2 mutual-recursion soundness fix, 2026-06-12). Each fixpoint owner records the guard
|
|
2091
2339
|
# stack size at seed time on its entry (`depth:`); a consult appends the consulted entry's depth here.
|
|
@@ -2248,9 +2496,15 @@ module Rigor
|
|
|
2248
2496
|
# the body both qualify. It does NOT need its own memo-key slot here — the bit is observable on the
|
|
2249
2497
|
# built `body_scope`, which is where every downstream consumer (the memo key, the recursion context)
|
|
2250
2498
|
# reads it from, so the two can never disagree.
|
|
2251
|
-
|
|
2499
|
+
#
|
|
2500
|
+
# `yield_type` (issue #720) is the OTHER call-site-varying input, and unlike `self_fold_safe` it is
|
|
2501
|
+
# not observable on the body scope, so it does need its own memo-key slot. It is dropped for a def
|
|
2502
|
+
# that cannot reach a `yield` — nearly all of them — which keeps the key shape constant for the
|
|
2503
|
+
# methods the memo actually carries and confines the extra dimension to yielding callees.
|
|
2504
|
+
def infer_user_method_return(def_node, receiver, arg_types, self_fold_safe: false, yield_type: nil)
|
|
2252
2505
|
return nil if def_node.body.nil?
|
|
2253
2506
|
|
|
2507
|
+
yield_type = nil unless yield_type && body_yields?(def_node)
|
|
2254
2508
|
body_scope = build_user_method_body_scope(def_node, receiver, arg_types,
|
|
2255
2509
|
self_fold_safe: self_fold_safe)
|
|
2256
2510
|
return nil if body_scope.nil?
|
|
@@ -2283,42 +2537,87 @@ module Rigor
|
|
|
2283
2537
|
# outermost method and thereafter the old `summaries.empty?` gate disabled the memo for every nested
|
|
2284
2538
|
# call, re-walking the shared sub-readers combinatorially (~932k body evaluations for ~20 tiny
|
|
2285
2539
|
# methods). The computation itself lives in `compute_user_method_return`.
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2540
|
+
# Issue #720 — the frame the body's `yield`s read, installed for the whole inference (the memo key
|
|
2541
|
+
# below reads it back from here rather than taking a second parameter, so key and frame cannot
|
|
2542
|
+
# disagree). It is installed even when nil, which is what stops a blockless callee reached from
|
|
2543
|
+
# inside a yielding body from inheriting the outer caller's block.
|
|
2544
|
+
ExpressionTyper.with_yield_value_type(yield_type) do
|
|
2545
|
+
unless memo_candidate?(stack, plain_signature)
|
|
2546
|
+
trace_memo_refusal(stack, plain_signature)
|
|
2547
|
+
next compute_user_method_return(def_node, body_scope, stack, summaries,
|
|
2289
2548
|
receiver, arg_types, plain_signature)
|
|
2549
|
+
end
|
|
2550
|
+
|
|
2551
|
+
# INVARIANT (ADR-46 recording soundness, ADR-84 WD2) — the deep cross-file dependency edges (the
|
|
2552
|
+
# reads a callee body's dispatches perform through the instrumented `Scope` accessors) are recorded,
|
|
2553
|
+
# per consumer, only as a side effect of evaluating that body, and a memo hit serves the return
|
|
2554
|
+
# WITHOUT re-evaluating it. Since the bucket is run-scoped, hits CROSS consumer-file boundaries, so
|
|
2555
|
+
# every cross-file hit is PAIRED WITH CACHE-AND-REPLAY of the callee's read-set: under recording,
|
|
2556
|
+
# the first evaluation of a key captures the recorder events of its body walk
|
|
2557
|
+
# (`DependencyRecorder.capture` — observe-and-forward, so the first consumer's own record is
|
|
2558
|
+
# untouched) onto the entry, and a hit replays that set into the current consumer's accumulator
|
|
2559
|
+
# (`DependencyRecorder.replay`, which re-applies the per-consumer self-read filter). A memo hit is
|
|
2560
|
+
# thereby edge-equivalent to a fresh body evaluation for EVERY consumer. The naive alternative —
|
|
2561
|
+
# bypassing the memo while the recorder is active — measured >200x wall on analyzer-shaped files
|
|
2562
|
+
# (ActiveStorage video_analyzer.rb's subtree, 0.43s -> >90s; PR #79) and stays rejected. Pinned by
|
|
2563
|
+
# spec/rigor/inference/return_memo_recording_spec.rb (cross-file replay completeness) and
|
|
2564
|
+
# dependency_recorder_spec.rb's transitive deep-edge example.
|
|
2565
|
+
consult_and_store_return_memo(def_node, body_scope, stack, summaries,
|
|
2566
|
+
receiver, arg_types, plain_signature)
|
|
2290
2567
|
end
|
|
2568
|
+
end
|
|
2569
|
+
|
|
2570
|
+
# Issue #720 — the constructs that own a `yield` written inside them, so a scan for "can this def's
|
|
2571
|
+
# body reach a `yield` that names THIS def's block?" must stop at them. A nested `def` / `class` /
|
|
2572
|
+
# `module` / `class << self` starts a new method-block binding; a block or a lambda does NOT — `yield`
|
|
2573
|
+
# inside `[1, 2].each { yield }` still calls the enclosing method's block, which is precisely the
|
|
2574
|
+
# `each`-wrapper idiom this fix has to keep seeing.
|
|
2575
|
+
YIELD_BOUNDARY_NODES = Set[
|
|
2576
|
+
Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode
|
|
2577
|
+
].freeze
|
|
2578
|
+
private_constant :YIELD_BOUNDARY_NODES
|
|
2291
2579
|
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2580
|
+
# Whether `def_node`'s body can reach a `yield`, memoised by def-node identity: it gates the
|
|
2581
|
+
# per-call-site memo dimension above and would otherwise be re-walked once per call site of every
|
|
2582
|
+
# user method. It rides in {#class_graph_buckets} for LIFETIME, not for correctness — a pure
|
|
2583
|
+
# function of the AST depends on none of the tables that bucket keys on, but a store that is
|
|
2584
|
+
# replaced with the analysis generation cannot outlive the nodes it holds.
|
|
2585
|
+
def body_yields?(def_node)
|
|
2586
|
+
cache = class_graph_buckets[:yields]
|
|
2587
|
+
return cache[def_node] if cache.key?(def_node)
|
|
2588
|
+
|
|
2589
|
+
cache[def_node] = yield_reachable?(def_node.body)
|
|
2590
|
+
end
|
|
2591
|
+
|
|
2592
|
+
def yield_reachable?(node)
|
|
2593
|
+
return false if node.nil?
|
|
2594
|
+
return true if node.is_a?(Prism::YieldNode)
|
|
2595
|
+
|
|
2596
|
+
node.rigor_each_child do |child|
|
|
2597
|
+
next if YIELD_BOUNDARY_NODES.include?(child.class)
|
|
2598
|
+
|
|
2599
|
+
return true if yield_reachable?(child)
|
|
2600
|
+
end
|
|
2601
|
+
false
|
|
2308
2602
|
end
|
|
2309
2603
|
|
|
2310
2604
|
# The candidate-frame memo path: consult the current run generation's bucket, and on a miss compute
|
|
2311
2605
|
# 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
|
-
#
|
|
2606
|
+
# The store gate (ADR-84 WD3): a result is stored when no transient-machinery event during the
|
|
2607
|
+
# bracket referenced a stack frame BELOW the bracket's entry depth (see TRANSIENT_EVENT_DEPTHS_KEY).
|
|
2608
|
+
# A separate ADR-55 fixpoint-consult bracket counter used to sit in front of this test; it is gone
|
|
2609
|
+
# (issue #875) because the event log already subsumes it — the sole `consult_summary` call site is
|
|
2610
|
+
# the in-cycle guard hit, which logs its event at the consulted owner's position first, so a consult
|
|
2611
|
+
# that could taint a bracket is a below-entry event by construction. Below-entry events — an
|
|
2612
|
+
# ancestor's in-flight Kleene iterate read by a guard hit, a shared-fuel exhaustion, a
|
|
2613
|
+
# possibly-ancestor-caused WD1 clamp — mean the ancestor context influenced `result`, which a
|
|
2614
|
+
# standalone recompute would not reproduce; at-or-above-entry events are the compute's own
|
|
2615
|
+
# deterministic machinery (its own converged fixpoint, sub-cycles that opened and closed inside the
|
|
2616
|
+
# bracket) and do not block the store. A top-of-stack compute (entry depth 0) is standalone by
|
|
2617
|
+
# construction. ADR-84 WD6 adds one exemption on top: a tainted result that
|
|
2618
|
+
# is already `Dynamic[top]` is stored anyway — see `top_result?`. A hit under ADR-46 recording
|
|
2619
|
+
# replays the entry's captured read-set into the current consumer (see the INVARIANT comment at the
|
|
2620
|
+
# call site).
|
|
2322
2621
|
def consult_and_store_return_memo(def_node, body_scope, stack, summaries,
|
|
2323
2622
|
receiver, arg_types, plain_signature)
|
|
2324
2623
|
per_def = (return_memo_bucket[def_node] ||= {})
|
|
@@ -2327,9 +2626,14 @@ module Rigor
|
|
|
2327
2626
|
# was foldable and `Dynamic[top]` when it was not. Without it in the key the first call site to
|
|
2328
2627
|
# reach a def would poison every later one with the other polarity. It is read off the body scope
|
|
2329
2628
|
# rather than passed in, so the key cannot drift from the scope that produced the result.
|
|
2629
|
+
# Issue #720 — the FOURTH such dimension, and it is read off the installed frame rather than
|
|
2630
|
+
# passed in for the same reason: a yielding callee's return depends on the block THIS call site
|
|
2631
|
+
# hands it, so two call sites passing differently-typed blocks must not share an entry. It is nil
|
|
2632
|
+
# for every def that cannot reach a `yield`, which is where the memo's mass is.
|
|
2330
2633
|
memo_key = [receiver.describe(:short),
|
|
2331
2634
|
arg_types.map { |type| type.describe(:short) },
|
|
2332
|
-
body_scope.struct_fold_safe?(:self)
|
|
2635
|
+
body_scope.struct_fold_safe?(:self),
|
|
2636
|
+
ExpressionTyper.current_yield_value_type&.describe(:short)]
|
|
2333
2637
|
if (entry = per_def[memo_key])
|
|
2334
2638
|
BudgetTrace.hit(BudgetTrace::MEMO_HITS)
|
|
2335
2639
|
Analysis::DependencyRecorder.replay(entry.read_set) if Analysis::DependencyRecorder.active?
|
|
@@ -2340,13 +2644,10 @@ module Rigor
|
|
|
2340
2644
|
|
|
2341
2645
|
entry_depth = stack.size
|
|
2342
2646
|
event_mark = transient_event_mark
|
|
2343
|
-
consults_before = summary_consult_count
|
|
2344
2647
|
result, read_set = compute_with_read_capture(def_node, body_scope, stack, summaries,
|
|
2345
2648
|
receiver, arg_types, plain_signature)
|
|
2346
2649
|
|
|
2347
|
-
if
|
|
2348
|
-
BudgetTrace.hit(BudgetTrace::MEMO_REFUSE_CONSULT_TAINTED)
|
|
2349
|
-
elsif context_tainted?(event_mark, entry_depth)
|
|
2650
|
+
if context_tainted?(event_mark, entry_depth) && !top_result?(result)
|
|
2350
2651
|
BudgetTrace.hit(BudgetTrace::MEMO_REFUSE_TRANSIENT)
|
|
2351
2652
|
else
|
|
2352
2653
|
per_def[memo_key] = MemoEntry.new(result: result, read_set: read_set,
|
|
@@ -2359,19 +2660,48 @@ module Rigor
|
|
|
2359
2660
|
Thread.current[TRANSIENT_EVENT_DEPTHS_KEY]&.size || 0
|
|
2360
2661
|
end
|
|
2361
2662
|
|
|
2362
|
-
def summary_consult_count
|
|
2363
|
-
Thread.current[SUMMARY_CONSULT_COUNTER_KEY] || 0
|
|
2364
|
-
end
|
|
2365
|
-
|
|
2366
2663
|
# ADR-84 WD3 — true when a transient-machinery event logged during the bracket (entries past
|
|
2367
2664
|
# `event_mark`) referenced a frame below `entry_depth`. The log is cleared when the guard stack drains
|
|
2368
2665
|
# (only possible mid-bracket for a top-of-stack compute, whose events are deterministic anyway), so a
|
|
2369
2666
|
# missing / shorter log reads as untainted.
|
|
2667
|
+
#
|
|
2668
|
+
# Scanned by index rather than over `log[event_mark..]`: the log is append-only for the whole
|
|
2669
|
+
# outermost entry, so the suffix slice allocated an array per candidate compute — a term quadratic in
|
|
2670
|
+
# the event count, and the `ary_ensure_room_for_push` / GC-sweep profile issue #870 sampled.
|
|
2370
2671
|
def context_tainted?(event_mark, entry_depth)
|
|
2371
2672
|
log = Thread.current[TRANSIENT_EVENT_DEPTHS_KEY]
|
|
2372
2673
|
return false if log.nil? || log.size <= event_mark
|
|
2373
2674
|
|
|
2374
|
-
|
|
2675
|
+
index = event_mark
|
|
2676
|
+
size = log.size
|
|
2677
|
+
while index < size
|
|
2678
|
+
return true if log[index] < entry_depth
|
|
2679
|
+
|
|
2680
|
+
index += 1
|
|
2681
|
+
end
|
|
2682
|
+
false
|
|
2683
|
+
end
|
|
2684
|
+
|
|
2685
|
+
# ADR-84 WD6 (issue #872) — the top-result exemption to the WD3 store gate.
|
|
2686
|
+
#
|
|
2687
|
+
# WD3 refuses a store whenever the bracket saw a below-entry transient event, because such a result
|
|
2688
|
+
# embeds an ancestor's in-flight state and a standalone recompute need not reproduce it. Inside a
|
|
2689
|
+
# strongly connected component of mutually recursive methods that holds for nearly every frame — each
|
|
2690
|
+
# nested compute re-enters a signature already on the guard stack and the guard site logs at the
|
|
2691
|
+
# ancestor's shallower position — so nothing in the component is ever memoised and every call edge
|
|
2692
|
+
# re-walks its whole callee subtree. Issue #870's fixture measured 18,412 of 18,436 memo misses
|
|
2693
|
+
# computing a result and discarding it, and rufo 0.18.2's `formatter.rb` did not finish in 25 minutes.
|
|
2694
|
+
#
|
|
2695
|
+
# The exemption: when the computed result is `Dynamic[top]` the entry carries no context-dependent
|
|
2696
|
+
# information to serve. `untyped` is the lattice top and the ADR-5 degradation floor — the only way a
|
|
2697
|
+
# standalone recompute can differ is by being MORE precise, so serving the stored top can never
|
|
2698
|
+
# surface a type the ancestor context invented, and can never raise a diagnostic that a fresh
|
|
2699
|
+
# evaluation would not (nothing is reported off `untyped`). The cost is a possible missed diagnostic
|
|
2700
|
+
# where the untainted recompute would have been precise; the corpus evidence that this does not
|
|
2701
|
+
# happen in practice is the byte-identical `--format json` runs over `lib`, `plugins/*/lib`,
|
|
2702
|
+
# `examples/*/lib` and the survey projects recorded on issue #872.
|
|
2703
|
+
def top_result?(result)
|
|
2704
|
+
result.equal?(Type::Combinator.untyped)
|
|
2375
2705
|
end
|
|
2376
2706
|
|
|
2377
2707
|
# ADR-84 WD2 — the memo-miss compute, wrapped in a `DependencyRecorder.capture` window when ADR-46
|
|
@@ -2444,10 +2774,10 @@ module Rigor
|
|
|
2444
2774
|
# stable across the body walk, that is necessary (but not sufficient) for a FINAL result — this plain
|
|
2445
2775
|
# signature must not itself be on the recursion guard stack (else we are inside its own cycle,
|
|
2446
2776
|
# returning a Kleene iterate by construction). Sufficiency is decided post-hoc in
|
|
2447
|
-
# `consult_and_store_return_memo` by the
|
|
2448
|
-
#
|
|
2449
|
-
#
|
|
2450
|
-
#
|
|
2777
|
+
# `consult_and_store_return_memo` by the ADR-84 WD3 transient-machinery event bracket — so unlike
|
|
2778
|
+
# the prior form this deliberately does NOT refuse while a constant-arg unroll is in flight: a
|
|
2779
|
+
# nested frame whose compute finishes without a single transient event ran exactly as it would
|
|
2780
|
+
# standalone (fuel consumption without exhaustion is invisible), and
|
|
2451
2781
|
# the blanket exclusion refused 82% of mail's body evaluations for such final results (ADR-84).
|
|
2452
2782
|
def memo_candidate?(stack, plain_signature)
|
|
2453
2783
|
stack.none? { |frame| plain_part(frame) == plain_signature }
|
|
@@ -2578,7 +2908,6 @@ module Rigor
|
|
|
2578
2908
|
summaries = context.summaries
|
|
2579
2909
|
depth = seed_fixpoint_summary(summaries, plain_signature)
|
|
2580
2910
|
consult_depths = (Thread.current[SUMMARY_CONSULT_DEPTHS_KEY] ||= [])
|
|
2581
|
-
computed = nil
|
|
2582
2911
|
|
|
2583
2912
|
RECURSION_FIXPOINT_CAP.times do |iteration|
|
|
2584
2913
|
summaries[plain_signature][:consulted] = false
|
|
@@ -2612,9 +2941,15 @@ module Rigor
|
|
|
2612
2941
|
return resolved unless resolved.nil?
|
|
2613
2942
|
end
|
|
2614
2943
|
|
|
2615
|
-
|
|
2616
|
-
return
|
|
2944
|
+
converged = fixpoint_step(summaries, plain_signature, computed, iteration)
|
|
2945
|
+
return converged if converged
|
|
2617
2946
|
end
|
|
2947
|
+
|
|
2948
|
+
# Out of iterations and still unstable. The collapse is the loop's own tail rather than a branch
|
|
2949
|
+
# inside `fixpoint_step`: `Integer#times` yields its receiver when no iteration returns, so a
|
|
2950
|
+
# tail-less loop reads as a path that returns an Integer summary (the `def.return-type-mismatch`
|
|
2951
|
+
# on `return_type_for` this shape retired), and the loop, not the step, owns its cap.
|
|
2952
|
+
collapse_fixpoint_cap(summaries, plain_signature)
|
|
2618
2953
|
end
|
|
2619
2954
|
|
|
2620
2955
|
# Seeds the thread-local summary entry for a fixpoint owner: the `bot` Kleene seed plus the
|
|
@@ -2650,8 +2985,10 @@ module Rigor
|
|
|
2650
2985
|
|
|
2651
2986
|
# One Kleene-iteration step of the fixpoint loop. Joins `computed` into the running assumption
|
|
2652
2987
|
# (widening value-pinned constituents on the final permitted iteration to force convergence) and
|
|
2653
|
-
#
|
|
2654
|
-
#
|
|
2988
|
+
# returns the converged type, or `nil` — no final type yet — having advanced the stored assumption
|
|
2989
|
+
# so the caller's next body evaluation reads the wider iterate. The step never decides that the
|
|
2990
|
+
# loop is over: whether another evaluation follows is the caller's cap, and a `nil` on the final
|
|
2991
|
+
# iteration lands in `collapse_fixpoint_cap`. ADR-55 WD2.
|
|
2655
2992
|
def fixpoint_step(summaries, plain_signature, computed, iteration)
|
|
2656
2993
|
assumption = summaries[plain_signature][:assumption]
|
|
2657
2994
|
last_iteration = iteration == RECURSION_FIXPOINT_CAP - 1
|
|
@@ -2662,17 +2999,19 @@ module Rigor
|
|
|
2662
2999
|
# (joining it back changes nothing).
|
|
2663
3000
|
return candidate if joined == assumption
|
|
2664
3001
|
|
|
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
3002
|
summaries[plain_signature][:assumption] = joined
|
|
2675
|
-
|
|
3003
|
+
nil
|
|
3004
|
+
end
|
|
3005
|
+
|
|
3006
|
+
# The capped collapse: `RECURSION_FIXPOINT_CAP` evaluations advanced the assumption without
|
|
3007
|
+
# converging, so the summary degrades to today's widening behaviour, `untyped`, parked in the
|
|
3008
|
+
# assumption for any consumer that still reads it. ADR-84 WD3: the cap is a per-owner constant, so
|
|
3009
|
+
# the event references the owner's own frame.
|
|
3010
|
+
def collapse_fixpoint_cap(summaries, plain_signature)
|
|
3011
|
+
note_transient_fallback(BudgetTrace::RECURSION_FIXPOINT_CAP, own_guard_frame_position)
|
|
3012
|
+
scope.record_dynamic_origin(@typing_node, DynamicOrigin::ANALYZER_BUDGET_CUTOFF) if @typing_node
|
|
3013
|
+
summaries[plain_signature][:assumption] = Type::Combinator.untyped
|
|
3014
|
+
Type::Combinator.untyped
|
|
2676
3015
|
end
|
|
2677
3016
|
|
|
2678
3017
|
# Rebuilds the user-method body scope with every bound positional parameter widened to its nominal
|
|
@@ -3109,22 +3448,87 @@ module Rigor
|
|
|
3109
3448
|
nil
|
|
3110
3449
|
end
|
|
3111
3450
|
|
|
3451
|
+
EMPTY_BREAK_ARMS = [].freeze
|
|
3452
|
+
private_constant :EMPTY_BREAK_ARMS
|
|
3453
|
+
|
|
3454
|
+
# The types the `break`s that terminate THIS call carry out of it, for {#call_dispatch_type_for} to union
|
|
3455
|
+
# with the result the dispatch tiers computed (issue #853).
|
|
3456
|
+
#
|
|
3457
|
+
# The syntactic scan runs first and answers "none" for every call whose block cannot reach a block-level
|
|
3458
|
+
# `break` — nearly all of them — so only that small minority pays the body evaluation. A `break` under a
|
|
3459
|
+
# nested block, lambda, `def`, or loop targets THAT construct instead; {JUMP_BOUNDARY_NODES} stops the
|
|
3460
|
+
# scan there, and the identity filter drops the ones the sink still collects because the nested
|
|
3461
|
+
# construct is walked under the same installation. `break` with no argument carries nil, so the call
|
|
3462
|
+
# becomes optional — which is what Ruby does.
|
|
3463
|
+
#
|
|
3464
|
+
# A failure yields no arms rather than propagating, matching {#block_return_type_for}: a call typed
|
|
3465
|
+
# without its break arms is the pre-#853 answer, while a raise here would take out the whole call.
|
|
3466
|
+
def call_break_arm_types(node, receiver_override: nil)
|
|
3467
|
+
block_node = node.block
|
|
3468
|
+
return EMPTY_BREAK_ARMS unless block_node.is_a?(Prism::BlockNode)
|
|
3469
|
+
|
|
3470
|
+
body = block_node.body
|
|
3471
|
+
return EMPTY_BREAK_ARMS if body.nil? || !block_level_jump?(body, Prism::BreakNode)
|
|
3472
|
+
|
|
3473
|
+
collect_break_arm_types(node, block_node, body, receiver_override)
|
|
3474
|
+
rescue StandardError
|
|
3475
|
+
EMPTY_BREAK_ARMS
|
|
3476
|
+
end
|
|
3477
|
+
|
|
3478
|
+
# Evaluates the block body once under a `break`-value sink, in the same entry scope the block-return pass
|
|
3479
|
+
# uses, so each arm is typed in the scope that actually reaches it — a `break v` after `v = "s"`
|
|
3480
|
+
# contributes `"s"`, not the entry binding — and an arm on a branch the analysis proved dead is never
|
|
3481
|
+
# reached at all.
|
|
3482
|
+
def collect_break_arm_types(call_node, block_node, body, receiver_override)
|
|
3483
|
+
targets = block_level_jump_nodes(body, Prism::BreakNode)
|
|
3484
|
+
receiver = receiver_override || call_receiver_type_for(call_node)
|
|
3485
|
+
return EMPTY_BREAK_ARMS if receiver.nil?
|
|
3486
|
+
|
|
3487
|
+
narrowed_self = MacroBlockSelfType.narrow_self_type_for(
|
|
3488
|
+
scope: scope, call_node: call_node, receiver_type: receiver
|
|
3489
|
+
)
|
|
3490
|
+
block_scope = block_entry_scope(
|
|
3491
|
+
block_node, break_arm_param_types(call_node, receiver), narrowed_self_type: narrowed_self
|
|
3492
|
+
)
|
|
3493
|
+
_result, collected = StatementEvaluator.with_break_value_sink do
|
|
3494
|
+
without_block_body_threading { block_scope.evaluate(body) }
|
|
3495
|
+
end
|
|
3496
|
+
collected.filter_map { |jump, type| type if targets.key?(jump) }
|
|
3497
|
+
end
|
|
3498
|
+
|
|
3499
|
+
def break_arm_param_types(call_node, receiver)
|
|
3500
|
+
MethodDispatcher.expected_block_param_types(
|
|
3501
|
+
receiver_type: receiver,
|
|
3502
|
+
method_name: call_node.name,
|
|
3503
|
+
arg_types: call_arg_types(call_node),
|
|
3504
|
+
environment: scope.environment
|
|
3505
|
+
)
|
|
3506
|
+
end
|
|
3507
|
+
|
|
3112
3508
|
def block_return_for(block_arg, expected, narrowed_self_type: nil)
|
|
3113
3509
|
case block_arg
|
|
3114
3510
|
when Prism::BlockNode
|
|
3115
|
-
|
|
3116
|
-
# Issue #316 — mirrors `StatementEvaluator#build_block_entry_scope`: the block body's `self` is the
|
|
3117
|
-
# yielding method's business, so the return-typing pass must see the same unmodelled-self mark.
|
|
3118
|
-
block_scope = bindings.reduce(scope.entering_opaque_block) do |acc, (name, type)|
|
|
3119
|
-
acc.with_local(name, type)
|
|
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)
|
|
3511
|
+
type_block_body(block_arg, block_entry_scope(block_arg, expected, narrowed_self_type: narrowed_self_type))
|
|
3123
3512
|
when Prism::BlockArgumentNode
|
|
3124
3513
|
symbol_block_return_type(block_arg, expected)
|
|
3125
3514
|
end
|
|
3126
3515
|
end
|
|
3127
3516
|
|
|
3517
|
+
# The scope a block body is typed under: the surrounding scope plus the parameter bindings the receiving
|
|
3518
|
+
# method's signature implies.
|
|
3519
|
+
#
|
|
3520
|
+
# Issue #316 — mirrors `StatementEvaluator#build_block_entry_scope`: the block body's `self` is the
|
|
3521
|
+
# yielding method's business, so the return-typing pass must see the same unmodelled-self mark.
|
|
3522
|
+
def block_entry_scope(block_node, expected, narrowed_self_type: nil)
|
|
3523
|
+
bindings = BlockParameterBinder.new(expected_param_types: expected).bind(block_node)
|
|
3524
|
+
block_scope = bindings.reduce(scope.entering_opaque_block) do |acc, (name, type)|
|
|
3525
|
+
acc.with_local(name, type)
|
|
3526
|
+
end
|
|
3527
|
+
return block_scope unless narrowed_self_type
|
|
3528
|
+
|
|
3529
|
+
block_scope.with_self_type(narrowed_self_type)
|
|
3530
|
+
end
|
|
3531
|
+
|
|
3128
3532
|
# `&:symbol` desugars to a one-arg Proc that dispatches `symbol` against its argument. When the param
|
|
3129
3533
|
# type is known and the resulting inner dispatch is precise, this returns the precise carrier;
|
|
3130
3534
|
# otherwise it returns `Dynamic[Top]` (still non-nil) so the outer dispatcher selects the
|
|
@@ -3157,13 +3561,95 @@ module Rigor
|
|
|
3157
3561
|
# — which threads scope statement by statement through `StatementEvaluator` — held `42` for the very
|
|
3158
3562
|
# same node. {#threaded_block_body_type} closes that gap by reusing the main pass's evaluator; a decline
|
|
3159
3563
|
# keeps the tail-only answer verbatim.
|
|
3564
|
+
#
|
|
3565
|
+
# Issue #841: the fall-through tail is only ONE of the block's exits. Every `next value` that leaves this
|
|
3566
|
+
# block makes `value` the block's result for that invocation, so the arms join into the value type the
|
|
3567
|
+
# same way {#evaluate_body_with_returns} joins a method's early `return`s with its tail. Without the
|
|
3568
|
+
# join, `ops.all? { |o| next false unless o; true }` read as `Constant[true]` and
|
|
3569
|
+
# {MethodDispatcher::BlockFolding} folded the call to always-truthy on a program that really can answer
|
|
3570
|
+
# false — a warning on correct code.
|
|
3160
3571
|
def type_block_body(block_node, block_scope)
|
|
3161
3572
|
body = block_node.body
|
|
3162
3573
|
return Type::Combinator.constant_of(nil) if body.nil?
|
|
3163
3574
|
|
|
3575
|
+
arms = block_level_next_arms(body)
|
|
3576
|
+
return block_body_type_joining_nexts(body, block_scope, arms) if arms
|
|
3577
|
+
|
|
3164
3578
|
threaded_block_body_type(body, block_scope) || block_scope.type_of(body)
|
|
3165
3579
|
end
|
|
3166
3580
|
|
|
3581
|
+
# Evaluates the body once under a `next` sink and joins the arms that leave THIS block with the
|
|
3582
|
+
# fall-through tail the same evaluation produced. Both halves come from one `StatementEvaluator` run, so
|
|
3583
|
+
# each arm is typed in the scope that actually reaches it — a `next y` after `y = "s"` contributes
|
|
3584
|
+
# `"s"`, not the entry scope's stale binding.
|
|
3585
|
+
#
|
|
3586
|
+
# This run ignores the {#block_body_threading_suppressed?} flag that gates the precision fold: that flag
|
|
3587
|
+
# bounds the cost of an OPTIONAL widening, while skipping the join re-opens a false-positive class. It
|
|
3588
|
+
# still SETS the flag for the duration, so nothing folds below it and the cost stays one evaluation per
|
|
3589
|
+
# block-level `next` — a shape that is a small minority of blocks.
|
|
3590
|
+
#
|
|
3591
|
+
# A failure falls back to the tail-only answer rather than propagating, for the same reason
|
|
3592
|
+
# {#threaded_block_body_type} does: the enclosing `block_return_type_for` rescue would otherwise report
|
|
3593
|
+
# "no block" to the dispatcher, a far larger regression than a wide block return.
|
|
3594
|
+
def block_body_type_joining_nexts(body, block_scope, arms)
|
|
3595
|
+
(fall_through, _exit_scope), collected = StatementEvaluator.with_next_sink do
|
|
3596
|
+
without_block_body_threading { block_scope.evaluate(body) }
|
|
3597
|
+
end
|
|
3598
|
+
joined = collected.filter_map { |node, type| type if arms.key?(node) }
|
|
3599
|
+
joined.empty? ? fall_through : Type::Combinator.union(fall_through, *joined)
|
|
3600
|
+
rescue StandardError
|
|
3601
|
+
block_scope.type_of(body)
|
|
3602
|
+
end
|
|
3603
|
+
|
|
3604
|
+
# The identity-keyed set of `next` nodes whose value is THIS block's value — reachable from the body
|
|
3605
|
+
# without crossing a {JUMP_BOUNDARY_NODES} construct that retargets them — or nil for "do not join".
|
|
3606
|
+
#
|
|
3607
|
+
# nil means "do not join": a body with no block-level `next` keeps today's path exactly, and pays only
|
|
3608
|
+
# the allocation-free scan that answers so. A co-resident block-level `break` used to decline here as
|
|
3609
|
+
# well, because its value was dropped and joining the `next` arms would have dressed a still-incomplete
|
|
3610
|
+
# answer as a complete one; since issue #853 the `break` arms are unioned into the yielding call
|
|
3611
|
+
# ({#call_break_arm_types}), so the two exits are modelled at their own levels and neither blocks the
|
|
3612
|
+
# other.
|
|
3613
|
+
def block_level_next_arms(body)
|
|
3614
|
+
return nil unless block_level_jump?(body, Prism::NextNode)
|
|
3615
|
+
|
|
3616
|
+
block_level_jump_nodes(body, Prism::NextNode)
|
|
3617
|
+
end
|
|
3618
|
+
|
|
3619
|
+
# True when a `klass` jump is reachable from `node` without crossing a construct that retargets it.
|
|
3620
|
+
# Allocation-free and early-exiting: this is the scan every block body pays, and the overwhelming
|
|
3621
|
+
# majority of them answer false on it.
|
|
3622
|
+
def block_level_jump?(node, klass)
|
|
3623
|
+
return false if node.nil?
|
|
3624
|
+
return true if node.is_a?(klass)
|
|
3625
|
+
|
|
3626
|
+
node.rigor_each_child do |child|
|
|
3627
|
+
next if JUMP_BOUNDARY_NODES.include?(child.class)
|
|
3628
|
+
|
|
3629
|
+
return true if block_level_jump?(child, klass)
|
|
3630
|
+
end
|
|
3631
|
+
false
|
|
3632
|
+
end
|
|
3633
|
+
|
|
3634
|
+
# The identity-keyed set of `klass` jumps that target THIS block — every one {#block_level_jump?} would
|
|
3635
|
+
# answer true for, rather than the first. The sinks in `StatementEvaluator` also collect jumps from
|
|
3636
|
+
# nested blocks / loops / defs evaluated under the same installation, so the consumer filters against
|
|
3637
|
+
# this set by node identity.
|
|
3638
|
+
def block_level_jump_nodes(body, klass)
|
|
3639
|
+
found = {}.compare_by_identity
|
|
3640
|
+
collect_block_level_jumps(body, klass, found)
|
|
3641
|
+
found
|
|
3642
|
+
end
|
|
3643
|
+
|
|
3644
|
+
def collect_block_level_jumps(node, klass, found)
|
|
3645
|
+
found[node] = true if node.is_a?(klass)
|
|
3646
|
+
node.rigor_each_child do |child|
|
|
3647
|
+
next if JUMP_BOUNDARY_NODES.include?(child.class)
|
|
3648
|
+
|
|
3649
|
+
collect_block_level_jumps(child, klass, found)
|
|
3650
|
+
end
|
|
3651
|
+
end
|
|
3652
|
+
|
|
3167
3653
|
# Re-typing the whole body would be wrong to do unconditionally: this path runs for EVERY block-bearing
|
|
3168
3654
|
# call, and the statements ahead of the tail are pure cost whenever the tail does not depend on them.
|
|
3169
3655
|
# Three declines keep that cost where the defect actually is, each falling back to the tail-only path:
|
|
@@ -3252,18 +3738,27 @@ module Rigor
|
|
|
3252
3738
|
private_constant :VARIABLE_READ_NODES
|
|
3253
3739
|
|
|
3254
3740
|
# 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
|
-
#
|
|
3741
|
+
# is the fall-through value only.
|
|
3742
|
+
#
|
|
3743
|
+
# The `next` entry no longer fires: {#type_block_body} routes such a body past this scan entirely, since
|
|
3744
|
+
# issue #841 joins those arms into the block's value. It is kept because the question this predicate
|
|
3745
|
+
# answers ("can this prefix escape with a value the fall-through misses?") is the caller's premise
|
|
3746
|
+
# rather than the caller's filter restated.
|
|
3747
|
+
#
|
|
3748
|
+
# The `break` entry still fires, and since issue #853 it is conservative rather than load-bearing: the
|
|
3749
|
+
# arms are unioned into the CALL ({#call_break_arm_types}), so the missing value is no longer missing.
|
|
3750
|
+
# What the decline still costs is the threaded tail — `m.synchronize do break 5 if flag; v = 42; v end`
|
|
3751
|
+
# answers `5 | Dynamic[top]` where threading would reach `5 | 42`. Lifting it widens no answer and
|
|
3752
|
+
# invents no fact, but it moves the type of every block that carries a `break`, so it is left to a
|
|
3753
|
+
# change that can measure that.
|
|
3259
3754
|
JUMP_NODES = Set[Prism::NextNode, Prism::BreakNode].freeze
|
|
3260
3755
|
private_constant :JUMP_NODES
|
|
3261
3756
|
|
|
3262
3757
|
# 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.
|
|
3758
|
+
# nothing about our block's value — it neither triggers the decline nor joins as an arm. A nested
|
|
3759
|
+
# `BlockNode` / `LambdaNode` is the jump's own block (`do xs.each { next 1 }; v = 42; v end` threads
|
|
3760
|
+
# soundly — the inner `next` ends the inner iteration); a loop consumes both forms (`while … next 5 …
|
|
3761
|
+
# end` continues the loop); a `DefNode` body is a different method entirely.
|
|
3267
3762
|
JUMP_BOUNDARY_NODES = Set[
|
|
3268
3763
|
Prism::BlockNode, Prism::LambdaNode, Prism::DefNode,
|
|
3269
3764
|
Prism::WhileNode, Prism::UntilNode, Prism::ForNode
|
|
@@ -3293,11 +3788,8 @@ module Rigor
|
|
|
3293
3788
|
# so `[1].each { v = 5 }` really does bind the outer `v`, and `[1].each { outer << 1 }` really does
|
|
3294
3789
|
# mutate the outer `outer`), while a jump counts only above the nearest {JUMP_BOUNDARY_NODES} boundary.
|
|
3295
3790
|
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?
|
|
3791
|
+
written = prefix_written_names(statements)
|
|
3792
|
+
return false if written.nil?
|
|
3301
3793
|
|
|
3302
3794
|
Source::NodeWalker.each(statements.last) do |node|
|
|
3303
3795
|
return true if VARIABLE_READ_NODES.include?(node.class) && written.include?(node.name)
|
|
@@ -3305,6 +3797,35 @@ module Rigor
|
|
|
3305
3797
|
false
|
|
3306
3798
|
end
|
|
3307
3799
|
|
|
3800
|
+
# WHICH names the predicate above answers YES on — every name the tail observes that the prefix binds
|
|
3801
|
+
# or mutates in place. Only the arity-cap floor ({#unanswered_tail_dependency?}) needs them, which is
|
|
3802
|
+
# why the predicate is not written over this method: the predicate runs for every multi-statement block
|
|
3803
|
+
# body and short-circuits on the first hit, the floor runs for a handful of calls. Sharing
|
|
3804
|
+
# {#prefix_written_names} is what keeps the two from drifting about what the prefix binds.
|
|
3805
|
+
def tail_dependent_body_names(statements)
|
|
3806
|
+
written = prefix_written_names(statements)
|
|
3807
|
+
return EMPTY_NAME_SET if written.nil?
|
|
3808
|
+
|
|
3809
|
+
Source::NodeWalker.each(statements.last).filter_map do |node|
|
|
3810
|
+
node.name if VARIABLE_READ_NODES.include?(node.class) && written.include?(node.name)
|
|
3811
|
+
end.to_set
|
|
3812
|
+
end
|
|
3813
|
+
|
|
3814
|
+
# The names the prefix binds or mutates in place, or `nil` when the scan declines — a prefix that can
|
|
3815
|
+
# jump out of the block with a value the fall-through misses, or one that binds nothing at all.
|
|
3816
|
+
def prefix_written_names(statements)
|
|
3817
|
+
written = Set.new
|
|
3818
|
+
statements[0...-1].each do |statement|
|
|
3819
|
+
return nil unless prefix_statement_jump_free?(statement, written, false)
|
|
3820
|
+
end
|
|
3821
|
+
return nil if written.empty?
|
|
3822
|
+
|
|
3823
|
+
written
|
|
3824
|
+
end
|
|
3825
|
+
|
|
3826
|
+
EMPTY_NAME_SET = Set.new.freeze
|
|
3827
|
+
private_constant :EMPTY_NAME_SET
|
|
3828
|
+
|
|
3308
3829
|
# True when `node` cannot jump out of the block with a value, collecting into `written` the names it
|
|
3309
3830
|
# binds (a variable-write node) or mutates in place (a {MutationWidening::SHAPE_MUTATORS} call, through
|
|
3310
3831
|
# every variable its receiver can evaluate to) on the way down. `retargeted` is true once the descent
|
|
@@ -3370,7 +3891,38 @@ module Rigor
|
|
|
3370
3891
|
per_position = per_element_block_results(call_node.block, element_types)
|
|
3371
3892
|
return nil if per_position.nil? || per_position.any?(&:nil?)
|
|
3372
3893
|
|
|
3373
|
-
assemble_per_element_result(call_node.name, per_position, element_types)
|
|
3894
|
+
assemble_per_element_result(call_node.name, per_position, element_types) ||
|
|
3895
|
+
find_family_floor(call_node.name, element_types)
|
|
3896
|
+
end
|
|
3897
|
+
|
|
3898
|
+
# The honest answer for `find` / `detect` / `find_index` / `index` when this fold walked every position
|
|
3899
|
+
# and the assembler still could not decide — which happens for exactly one reason: some position's
|
|
3900
|
+
# predicate is not a `Constant`, so "the first matching one" is not a static fact.
|
|
3901
|
+
#
|
|
3902
|
+
# Falling through to the dispatcher was WRONG for this family, and issue #617 residue (1) is the bill:
|
|
3903
|
+
# `seen = 0; [1, 2].find do |e| seen += 1; seen == 2 end` answered `nil` where the runtime answers `2`.
|
|
3904
|
+
# The dispatcher's `BlockFolding` reads ONE block-return type, typed from the call's ENTRY scope, so a
|
|
3905
|
+
# predicate over a rebound capture pins the first iteration (`Constant[false]`) and
|
|
3906
|
+
# `FALSEY_BLOCK_NIL_METHODS` short-circuits the whole call to `nil`. This walk already knows better: it
|
|
3907
|
+
# typed the predicate per position and saw that it does not fold. Answering here is what keeps that
|
|
3908
|
+
# knowledge from being thrown away in favour of a worse-informed tier.
|
|
3909
|
+
#
|
|
3910
|
+
# The floor is what `find` can return and no tighter: one of the receiver's own elements, or `nil` when
|
|
3911
|
+
# no element matches. Value pinning survives because it is still true of every candidate — `find` hands
|
|
3912
|
+
# back an element, it does not compute one. `find_index` / `index` answer a position in the receiver, so
|
|
3913
|
+
# `Integer?` is their floor.
|
|
3914
|
+
#
|
|
3915
|
+
# Nothing else in {PER_ELEMENT_TUPLE_METHODS} takes a floor: `map`'s assembler cannot decline,
|
|
3916
|
+
# and `select` / `reject` / `filter_map` / `flat_map` fall through to an RBS projection that is merely
|
|
3917
|
+
# wider, never wrong — `BlockFolding`'s filter folds decline on a non-Constant block instead of
|
|
3918
|
+
# answering.
|
|
3919
|
+
def find_family_floor(method_name, element_types)
|
|
3920
|
+
case method_name
|
|
3921
|
+
when :find, :detect
|
|
3922
|
+
Type::Combinator.union(*element_types, Type::Combinator.constant_of(nil))
|
|
3923
|
+
when :find_index, :index
|
|
3924
|
+
Type::Combinator.union(Type::Combinator.nominal_of("Integer"), Type::Combinator.constant_of(nil))
|
|
3925
|
+
end
|
|
3374
3926
|
end
|
|
3375
3927
|
|
|
3376
3928
|
# Evaluates the call's block once per receiver element. Two block shapes are supported:
|
|
@@ -3408,10 +3960,48 @@ module Rigor
|
|
|
3408
3960
|
element_types.map { |element_type| type_block_body_with_param(block, [element_type], captured: captured) }
|
|
3409
3961
|
end
|
|
3410
3962
|
return results.call if element_types.size <= PER_ELEMENT_THREADING_LIMIT
|
|
3963
|
+
return uncapped_body_floor(element_types) if unanswered_tail_dependency?(block, captured)
|
|
3411
3964
|
|
|
3412
3965
|
without_block_body_threading(&results)
|
|
3413
3966
|
end
|
|
3414
3967
|
|
|
3968
|
+
# Above {PER_ELEMENT_THREADING_LIMIT} the threading is suppressed, and a position is typed tail-only.
|
|
3969
|
+
# For a tail that reads what the body's own prefix wrote or mutated, tail-only is not a wider answer —
|
|
3970
|
+
# it is the ENTRY binding, which the prefix has already falsified.
|
|
3971
|
+
#
|
|
3972
|
+
# #584's cliff comment promised `Dynamic[top]` above the cap, and that held for a body-LOCAL: `[1, …,
|
|
3973
|
+
# 9].map do v = e; v end` has no entry binding for `v`, so tail-only lands on `Dynamic[top]` by itself.
|
|
3974
|
+
# A mutated PARAMETER has one, and issue #617 residue (2) is what it buys: `([[]] * 9).map do |a| a <<
|
|
3975
|
+
# 1; a end` answered nine stale `[]`, a provably-empty array at every position of a result whose slots
|
|
3976
|
+
# each hold `[1]`. Flooring the whole walk restores the promise for both shapes — the cost the cap
|
|
3977
|
+
# refuses to pay is the per-position body evaluation, and declining to pay it means declining to know,
|
|
3978
|
+
# not answering the pre-state.
|
|
3979
|
+
#
|
|
3980
|
+
# {#tail_depends_on_body_binding?} is the same predicate the threading gate uses, so "would threading
|
|
3981
|
+
# have changed this tail" and "is tail-only untrustworthy here" stay one question. A body it answers
|
|
3982
|
+
# false for keeps its exact tail-only fold above the cap, which is every single-statement block and
|
|
3983
|
+
# every multi-statement block whose tail ignores its prefix.
|
|
3984
|
+
def uncapped_body_floor(element_types)
|
|
3985
|
+
Array.new(element_types.size) { Type::Combinator.untyped }
|
|
3986
|
+
end
|
|
3987
|
+
|
|
3988
|
+
# True when the tail reads something the prefix changed that NOTHING has re-answered for this walk.
|
|
3989
|
+
#
|
|
3990
|
+
# `captured` is the converged binding of every outer local the block rebinds ({#per_element_captured_bindings}),
|
|
3991
|
+
# and its cost is independent of the arity, so it keeps working above the cap: `total = 0; [1, …,
|
|
3992
|
+
# 9].map do total += e; total end` reads `total` as the fixpoint's `Integer` at every position and needs
|
|
3993
|
+
# no floor. What the cap actually withholds is the per-position body evaluation, so the names it leaves
|
|
3994
|
+
# unanswered are the ones the fixpoint does not cover — a mutated block PARAMETER (issue #617 residue
|
|
3995
|
+
# (2)'s `|a| a << 1; a`) or a mutated outer local the block never rebinds.
|
|
3996
|
+
def unanswered_tail_dependency?(block, captured)
|
|
3997
|
+
body = block.body
|
|
3998
|
+
return false unless body.is_a?(Prism::StatementsNode)
|
|
3999
|
+
return false if body.body.size < 2
|
|
4000
|
+
|
|
4001
|
+
answered = captured&.keys || []
|
|
4002
|
+
tail_dependent_body_names(body.body).any? { |name| !answered.include?(name) }
|
|
4003
|
+
end
|
|
4004
|
+
|
|
3415
4005
|
# Issue #587 (b) — first-iteration pinning. Every position of this fold is typed from the SAME entry
|
|
3416
4006
|
# scope, so a body that rebinds a captured outer local answers the FIRST iteration's value at every
|
|
3417
4007
|
# position: `total = 0; [1, 2].map do total += 1; total end` folded to `[1, 1]` (runtime `[1, 2]`), and
|
|
@@ -3456,12 +4046,44 @@ module Rigor
|
|
|
3456
4046
|
|
|
3457
4047
|
def converged_captured_bindings(block, names, element_types)
|
|
3458
4048
|
param_types = [Type::Combinator.union(*element_types)]
|
|
3459
|
-
|
|
4049
|
+
seeds = names.to_h { |name| [name, scope.local(name)] }
|
|
4050
|
+
converged = BodyFixpoint.converge(
|
|
3460
4051
|
names: names,
|
|
3461
|
-
seed_bindings:
|
|
4052
|
+
seed_bindings: seeds,
|
|
3462
4053
|
widen: Type::Combinator.method(:widen_value_pinned),
|
|
3463
4054
|
evaluate_body: ->(bindings) { captured_exit_bindings(block, param_types, bindings, names) }
|
|
3464
4055
|
)
|
|
4056
|
+
unmoved_pins_floored(converged, seeds)
|
|
4057
|
+
end
|
|
4058
|
+
|
|
4059
|
+
# A name the write scan says this block REBINDS, whose fixpoint came back on exactly its value-pinned
|
|
4060
|
+
# seed, is floored rather than believed.
|
|
4061
|
+
#
|
|
4062
|
+
# The fixpoint reads each pass's exit binding out of `StatementEvaluator`, and that evaluator only
|
|
4063
|
+
# threads a write it sees as a STATEMENT: a rebind nested inside an expression — `(seen += 1) == 2` as
|
|
4064
|
+
# the block's whole body — leaves the exit scope holding the entry binding, so the fixpoint converges on
|
|
4065
|
+
# the seed and every position of the fold answers the first iteration again. That is issue #617 residue
|
|
4066
|
+
# (1)'s one-liner, `seen = 0; [1, 2].find { |e| (seen += 1) == 2 }` answering `nil` where Ruby answers
|
|
4067
|
+
# `2`: the pinned `Constant[0]` made both predicates `Constant[false]`, and `find` short-circuits on a
|
|
4068
|
+
# provably-falsey block.
|
|
4069
|
+
#
|
|
4070
|
+
# An unmoved pin cannot be distinguished from a write that genuinely restores its own entry value
|
|
4071
|
+
# (`x = 5; xs.each { x = 5 }`), so the floor gives that shape up too. It is the far cheaper side: a
|
|
4072
|
+
# value-pinned seed the block rebinds is the exact pre-state this fold exists to stop trusting, and
|
|
4073
|
+
# `Dynamic[top]` is the same escaping-block floor {#captured_floor} already uses. Seeds that carry no
|
|
4074
|
+
# value pinning are left alone — there is no first-iteration constant in them to remove, and widening a
|
|
4075
|
+
# `Nominal` here would only lose a class for nothing.
|
|
4076
|
+
def unmoved_pins_floored(converged, seeds)
|
|
4077
|
+
converged.to_h do |name, type|
|
|
4078
|
+
seed = seeds[name]
|
|
4079
|
+
next [name, type] unless type == seed && value_pinned?(seed)
|
|
4080
|
+
|
|
4081
|
+
[name, Type::Combinator.untyped]
|
|
4082
|
+
end
|
|
4083
|
+
end
|
|
4084
|
+
|
|
4085
|
+
def value_pinned?(type)
|
|
4086
|
+
!type.nil? && Type::Combinator.widen_value_pinned(type) != type
|
|
3465
4087
|
end
|
|
3466
4088
|
|
|
3467
4089
|
# One fixpoint pass: the body evaluated from `bindings` with the block parameters bound over them (the
|
|
@@ -3501,7 +4123,7 @@ module Rigor
|
|
|
3501
4123
|
# `Constant<a..b>` → [Constant[a], …, Constant[b]]
|
|
3502
4124
|
# everything else → nil
|
|
3503
4125
|
#
|
|
3504
|
-
# Note: `Type::IntegerRange` is the bounded-Integer carrier (`
|
|
4126
|
+
# Note: `Type::IntegerRange` is the bounded-Integer carrier (`Integer[a..b]` represents "an Integer between
|
|
3505
4127
|
# a and b"), not a Range value. Calls like `.map` / `.find` on an `IntegerRange` receiver would resolve
|
|
3506
4128
|
# to `Integer#map` / `Integer#find` — neither exists — so IntegerRange does NOT participate in this
|
|
3507
4129
|
# fold.
|
|
@@ -3541,7 +4163,7 @@ module Rigor
|
|
|
3541
4163
|
#
|
|
3542
4164
|
# Part 1 (soundness): the accumulator of a block-form fold must reach a fixpoint over an unknown
|
|
3543
4165
|
# 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 `
|
|
4166
|
+
# block pass (acc=seed, elem=element-join), so `(1..5).inject(1) { |a, i| a * i }` types `Integer[1..5]`
|
|
3545
4167
|
# while the runtime is 120 (out of range — unsound). We iterate the accumulator type to a capped
|
|
3546
4168
|
# fixpoint (ADR-55/56 `BodyFixpoint`) so the multiply converges to `Integer`, never a value-bounded
|
|
3547
4169
|
# interval the runtime escapes.
|
|
@@ -3557,8 +4179,6 @@ module Rigor
|
|
|
3557
4179
|
# tier. Captured-local write-back (ADR-56) runs at the statement level independent of this return-type
|
|
3558
4180
|
# computation, so a block that both accumulates and mutates captured state keeps its write-back
|
|
3559
4181
|
# regardless of which arm answers here.
|
|
3560
|
-
#
|
|
3561
|
-
# @return [Rigor::Type, nil]
|
|
3562
4182
|
def try_block_inject_fold(call_node, receiver, arg_types)
|
|
3563
4183
|
return nil unless INJECT_METHODS.include?(call_node.name)
|
|
3564
4184
|
|
|
@@ -3577,7 +4197,7 @@ module Rigor
|
|
|
3577
4197
|
# Splits the positional args into the optional seed. A Symbol final arg (`inject(seed, :*)`) is the
|
|
3578
4198
|
# no-block Symbol form and never reaches here (the block guard already failed for it).
|
|
3579
4199
|
#
|
|
3580
|
-
# @return
|
|
4200
|
+
# @return `[seed, has_seed]`
|
|
3581
4201
|
def inject_seed(arg_types)
|
|
3582
4202
|
case arg_types.size
|
|
3583
4203
|
when 0 then [nil, false]
|
|
@@ -3637,7 +4257,7 @@ module Rigor
|
|
|
3637
4257
|
# member is folded; without a seed the first member seeds the memo and the rest are folded. The
|
|
3638
4258
|
# accumulator is carried as a `Constant` type (so the block body sees a value-pinned param).
|
|
3639
4259
|
#
|
|
3640
|
-
# @return
|
|
4260
|
+
# @return `[acc, rest]`
|
|
3641
4261
|
def inject_constant_start(members, seed, has_seed)
|
|
3642
4262
|
if has_seed
|
|
3643
4263
|
return [nil, []] unless seed.is_a?(Type::Constant) && inject_foldable?(seed.value)
|