rigortype 0.4.5 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/data/gem_overlay/activesupport/core_ext.rbs +16 -13
- data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
- data/docs/handbook/01-getting-started.md +5 -2
- data/docs/handbook/11-sig-gen.md +61 -5
- data/docs/handbook/appendix-phpstan.md +4 -4
- data/docs/manual/02-cli-reference.md +192 -27
- data/docs/manual/03-configuration.md +38 -4
- data/docs/manual/04-diagnostics.md +1 -1
- data/docs/manual/06-baseline.md +6 -4
- data/docs/manual/09-editor-integration.md +13 -2
- data/docs/manual/10-mcp-server.md +47 -3
- data/docs/manual/12-caching.md +9 -1
- data/docs/manual/plugins/rigor-actionmailer.md +30 -0
- data/docs/manual/plugins/rigor-actionpack.md +49 -11
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +20 -1
- data/docs/manual/plugins/rigor-devise.md +15 -0
- data/docs/manual/plugins/rigor-rspec.md +2 -1
- data/lib/rigor/analysis/baseline.rb +24 -27
- data/lib/rigor/analysis/buffer_binding.rb +14 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +4 -4
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +105 -12
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +143 -15
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -1
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +1 -1
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
- data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
- data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
- data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +53 -2
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules.rb +217 -52
- data/lib/rigor/analysis/crash_signature.rb +2 -2
- data/lib/rigor/analysis/dependency_recorder.rb +15 -4
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
- data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
- data/lib/rigor/analysis/edit_replay.rb +154 -0
- data/lib/rigor/analysis/fact_store.rb +3 -3
- data/lib/rigor/analysis/incremental.rb +37 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -2
- data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
- data/lib/rigor/analysis/incremental_session.rb +202 -43
- data/lib/rigor/analysis/path_expansion.rb +41 -0
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +7 -3
- data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
- data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +8 -4
- data/lib/rigor/analysis/runner/declaration_position.rb +1 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +204 -96
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +1 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +157 -320
- data/lib/rigor/analysis/runner/project_pre_passes.rb +48 -6
- data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
- data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
- data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
- data/lib/rigor/analysis/runner.rb +233 -120
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +1 -1
- data/lib/rigor/analysis/template_unit_positions.rb +2 -3
- data/lib/rigor/analysis/template_units.rb +9 -1
- data/lib/rigor/analysis/worker_session.rb +38 -33
- data/lib/rigor/bleeding_edge.rb +3 -6
- data/lib/rigor/builtins/imported_refinements.rb +6 -8
- data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
- data/lib/rigor/builtins/regex_refinement.rb +4 -6
- data/lib/rigor/cache/descriptor.rb +6 -5
- data/lib/rigor/cache/incremental_snapshot.rb +60 -18
- data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
- data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
- data/lib/rigor/cache/rbs_constant_table.rb +1 -1
- data/lib/rigor/cache/rbs_descriptor.rb +54 -3
- data/lib/rigor/cache/rbs_environment.rb +4 -1
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
- data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
- data/lib/rigor/cache/store.rb +11 -10
- data/lib/rigor/ci_detector.rb +1 -1
- data/lib/rigor/cli/annotate_command.rb +24 -19
- data/lib/rigor/cli/baseline_command.rb +145 -55
- data/lib/rigor/cli/catalogue.rb +256 -0
- data/lib/rigor/cli/check_command.rb +188 -93
- data/lib/rigor/cli/check_runner_factory.rb +2 -2
- data/lib/rigor/cli/command.rb +15 -1
- data/lib/rigor/cli/coverage_command.rb +21 -8
- data/lib/rigor/cli/coverage_mutation.rb +5 -7
- data/lib/rigor/cli/coverage_renderer.rb +2 -1
- data/lib/rigor/cli/coverage_scan.rb +3 -5
- data/lib/rigor/cli/diff_command.rb +27 -21
- data/lib/rigor/cli/docs_command.rb +1 -2
- data/lib/rigor/cli/doctor_command.rb +12 -12
- data/lib/rigor/cli/effects_command.rb +50 -40
- data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
- data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
- data/lib/rigor/cli/effects_renderer.rb +2 -1
- data/lib/rigor/cli/effects_snapshot_command.rb +18 -17
- data/lib/rigor/cli/explain_command.rb +18 -8
- data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
- data/lib/rigor/cli/json_document.rb +41 -0
- data/lib/rigor/cli/lens_command.rb +17 -13
- data/lib/rigor/cli/lsp_command.rb +250 -23
- data/lib/rigor/cli/mcp_command.rb +14 -8
- data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
- data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
- data/lib/rigor/cli/options.rb +60 -4
- data/lib/rigor/cli/plugin_command.rb +93 -46
- data/lib/rigor/cli/plugins_command.rb +22 -18
- data/lib/rigor/cli/plugins_renderer.rb +34 -8
- data/lib/rigor/cli/protection_fork_scan.rb +2 -2
- data/lib/rigor/cli/protection_renderer.rb +2 -1
- data/lib/rigor/cli/show_bleedingedge_command.rb +23 -16
- data/lib/rigor/cli/sig_gen_command.rb +130 -34
- data/lib/rigor/cli/skill_command.rb +32 -3
- data/lib/rigor/cli/skill_deep_probe.rb +1 -1
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/trace_command.rb +14 -9
- data/lib/rigor/cli/triage_command.rb +10 -11
- data/lib/rigor/cli/triage_renderer.rb +2 -1
- data/lib/rigor/cli/type_of_command.rb +18 -12
- data/lib/rigor/cli/type_of_project_scope.rb +24 -0
- data/lib/rigor/cli/type_of_renderer.rb +4 -1
- data/lib/rigor/cli/type_scan_command.rb +14 -9
- data/lib/rigor/cli/type_scan_renderer.rb +2 -1
- data/lib/rigor/cli/unused_command.rb +27 -17
- data/lib/rigor/cli/upgrade_command.rb +5 -0
- data/lib/rigor/cli.rb +209 -54
- data/lib/rigor/config_audit.rb +17 -6
- data/lib/rigor/configuration/severity_profile.rb +5 -5
- data/lib/rigor/configuration.rb +86 -34
- data/lib/rigor/effects/definition_context.rb +0 -2
- data/lib/rigor/effects/envelope.rb +1 -1
- data/lib/rigor/effects/framework_units.rb +1 -1
- data/lib/rigor/effects/mutation_classifier.rb +4 -7
- data/lib/rigor/effects/propagator.rb +1 -1
- data/lib/rigor/effects/registry.rb +2 -2
- data/lib/rigor/effects/snapshot.rb +1 -1
- data/lib/rigor/effects/taint_cause.rb +0 -2
- data/lib/rigor/effects/unit_scan.rb +9 -13
- data/lib/rigor/environment/bundle_sig_discovery.rb +5 -9
- data/lib/rigor/environment/class_registry.rb +3 -3
- data/lib/rigor/environment/core_root.rb +77 -0
- data/lib/rigor/environment/default_libraries.rb +1 -1
- data/lib/rigor/environment/deferred_signature_plan.rb +283 -0
- data/lib/rigor/environment/gated_signature_guard.rb +1 -1
- data/lib/rigor/environment/gated_signature_plan.rb +5 -2
- data/lib/rigor/environment/lockfile_resolver.rb +28 -12
- data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
- data/lib/rigor/environment/member_consistency.rb +56 -7
- data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
- data/lib/rigor/environment/project_path_gems.rb +124 -0
- data/lib/rigor/environment/rbs_hierarchy.rb +6 -15
- data/lib/rigor/environment/rbs_loader.rb +592 -70
- data/lib/rigor/environment/reflection.rb +7 -7
- data/lib/rigor/environment/required_features.rb +136 -32
- data/lib/rigor/environment/ruby_library_roots.rb +122 -0
- data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
- data/lib/rigor/environment.rb +44 -11
- data/lib/rigor/flow_contribution/merger.rb +4 -4
- data/lib/rigor/inference/acceptance.rb +93 -32
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/block_run_count.rb +178 -0
- data/lib/rigor/inference/budget_trace.rb +8 -2
- data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
- data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +126 -0
- data/lib/rigor/inference/callee_write_census.rb +815 -0
- data/lib/rigor/inference/construction_escape.rb +217 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
- data/lib/rigor/inference/deep_erasure.rb +170 -0
- data/lib/rigor/inference/def_handle.rb +1 -1
- data/lib/rigor/inference/def_node_resolver.rb +1 -1
- data/lib/rigor/inference/def_return_typer.rb +204 -12
- data/lib/rigor/inference/definer_resolution.rb +21 -5
- data/lib/rigor/inference/definers_index.rb +106 -0
- data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
- data/lib/rigor/inference/element_read_widening.rb +49 -3
- data/lib/rigor/inference/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +173 -57
- data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
- data/lib/rigor/inference/global_write_census.rb +110 -1
- data/lib/rigor/inference/global_writer_reach.rb +260 -0
- data/lib/rigor/inference/hkt_body.rb +1 -2
- data/lib/rigor/inference/hkt_body_parser.rb +3 -5
- data/lib/rigor/inference/hkt_reducer.rb +2 -2
- data/lib/rigor/inference/hkt_registry.rb +3 -5
- data/lib/rigor/inference/indexed_narrowing.rb +4 -7
- data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
- data/lib/rigor/inference/ivar_writer_census.rb +246 -0
- data/lib/rigor/inference/key_presence_guard.rb +2 -1
- data/lib/rigor/inference/loop_value_record.rb +57 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +65 -0
- data/lib/rigor/inference/match_rebinding/yielding_match.rb +45 -0
- data/lib/rigor/inference/match_rebinding.rb +11 -1
- data/lib/rigor/inference/memo_audit.rb +55 -0
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +113 -12
- data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -3
- data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +39 -27
- data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +203 -17
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +59 -24
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +83 -20
- data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher.rb +109 -29
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +23 -1
- data/lib/rigor/inference/multi_target_binder.rb +1 -1
- data/lib/rigor/inference/mutation_rejoin.rb +240 -4
- data/lib/rigor/inference/mutation_widening.rb +162 -29
- data/lib/rigor/inference/narrowing.rb +113 -38
- data/lib/rigor/inference/object_mixins.rb +13 -1
- data/lib/rigor/inference/optimistic_origin.rb +1 -1
- data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
- data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
- data/lib/rigor/inference/pre_eval_constants.rb +1 -1
- data/lib/rigor/inference/project_method_ownership.rb +6 -3
- data/lib/rigor/inference/project_seed_tables.rb +2 -1
- data/lib/rigor/inference/project_yielder.rb +122 -0
- data/lib/rigor/inference/receiver_alias.rb +20 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +3 -1
- data/lib/rigor/inference/refined_dispatch/own_method.rb +2 -1
- data/lib/rigor/inference/remover_holder_scan.rb +233 -0
- data/lib/rigor/inference/remover_narrowing.rb +224 -0
- data/lib/rigor/inference/rewrite_mutation.rb +53 -2
- data/lib/rigor/inference/scope_indexer/certainty.rb +9 -1
- data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
- data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +205 -0
- data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
- data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
- data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +135 -0
- data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
- data/lib/rigor/inference/scope_indexer.rb +614 -146
- data/lib/rigor/inference/self_call_ivar_writes.rb +101 -17
- data/lib/rigor/inference/set_mutation.rb +189 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +1 -2
- data/lib/rigor/inference/statement_evaluator.rb +3036 -278
- data/lib/rigor/inference/struct_fold_safety.rb +4 -6
- data/lib/rigor/inference/synthetic_method_index.rb +23 -0
- data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +73 -14
- data/lib/rigor/inference/toplevel_def_census.rb +59 -0
- data/lib/rigor/inference/unknown_store_widening.rb +103 -4
- data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
- data/lib/rigor/inference/void_tail_summary.rb +2 -1
- data/lib/rigor/language_server/buffer_table.rb +9 -3
- data/lib/rigor/language_server/completion_provider.rb +26 -20
- data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +28 -9
- data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
- data/lib/rigor/language_server/folding_range_provider.rb +1 -2
- data/lib/rigor/language_server/hover_provider.rb +1 -7
- data/lib/rigor/language_server/hover_renderer.rb +6 -8
- data/lib/rigor/language_server/project_context.rb +98 -4
- data/lib/rigor/language_server/publish_batcher.rb +3 -4
- data/lib/rigor/language_server/selection_range_provider.rb +2 -4
- data/lib/rigor/language_server/server.rb +12 -6
- data/lib/rigor/language_server/signature_help_provider.rb +4 -4
- data/lib/rigor/lens/plugins.rb +1 -1
- data/lib/rigor/mcp/server.rb +82 -3
- data/lib/rigor/mcp.rb +1 -1
- data/lib/rigor/plugin/access_denied_error.rb +1 -2
- data/lib/rigor/plugin/base.rb +26 -11
- data/lib/rigor/plugin/bundled_catalog.rb +1 -12
- data/lib/rigor/plugin/io_boundary.rb +1 -1
- data/lib/rigor/plugin/loader.rb +25 -8
- data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
- data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
- data/lib/rigor/plugin/manifest.rb +38 -16
- data/lib/rigor/plugin/registry.rb +41 -3
- data/lib/rigor/plugin/services.rb +2 -3
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
- data/lib/rigor/plugin/trust_policy.rb +2 -2
- data/lib/rigor/plugin/type_node_resolver.rb +2 -4
- data/lib/rigor/plugin.rb +6 -8
- data/lib/rigor/plugin_gap_advisory.rb +5 -1
- data/lib/rigor/project_environment.rb +16 -2
- data/lib/rigor/protection/analysis_guard.rb +1 -1
- data/lib/rigor/protection/discovery_seed.rb +96 -2
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
- data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
- data/lib/rigor/rbs_extended/reporter.rb +27 -3
- data/lib/rigor/rbs_extended.rb +2 -3
- data/lib/rigor/reflection/constant_ancestors.rb +5 -3
- data/lib/rigor/rooted_glob.rb +25 -2
- data/lib/rigor/run_scoped_slots.rb +38 -0
- data/lib/rigor/runtime/jit.rb +1 -1
- data/lib/rigor/scope/discovery_index.rb +19 -6
- data/lib/rigor/scope/resolution_chain.rb +3 -1
- data/lib/rigor/scope.rb +379 -36
- data/lib/rigor/sig_gen/classification.rb +4 -0
- data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
- data/lib/rigor/sig_gen/generator.rb +668 -84
- data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
- data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
- data/lib/rigor/sig_gen/layout_index.rb +3 -3
- data/lib/rigor/sig_gen/method_candidate.rb +23 -4
- data/lib/rigor/sig_gen/observation_collector.rb +7 -6
- data/lib/rigor/sig_gen/observed_call.rb +1 -1
- data/lib/rigor/sig_gen/path_mapper.rb +6 -0
- data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +21 -12
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +15 -2
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
- data/lib/rigor/sig_gen/write_reporter.rb +63 -14
- data/lib/rigor/sig_gen/write_result.rb +23 -12
- data/lib/rigor/sig_gen/writer.rb +217 -37
- data/lib/rigor/type/acceptance_router.rb +2 -2
- data/lib/rigor/type/accepts_result.rb +1 -1
- data/lib/rigor/type/app.rb +4 -6
- data/lib/rigor/type/combinator.rb +13 -16
- data/lib/rigor/type/constant.rb +48 -7
- data/lib/rigor/type/hash_shape.rb +26 -40
- data/lib/rigor/type/struct_instance.rb +3 -2
- data/lib/rigor/type/union.rb +1 -1
- data/lib/rigor/type_node/identifier.rb +1 -2
- data/lib/rigor/type_node/resolver_chain.rb +1 -1
- data/lib/rigor/type_node/string_literal.rb +1 -2
- data/lib/rigor/type_node/symbol_literal.rb +1 -2
- data/lib/rigor/type_node/union.rb +3 -6
- data/lib/rigor/value_semantics.rb +1 -10
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +89 -70
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +25 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +79 -19
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +6 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +2 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +0 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +5 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +7 -12
- data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +19 -6
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +26 -13
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -5
- data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -2
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +5 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +1 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +17 -21
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +3 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
- data/sig/rigor/analysis/baseline.rbs +0 -2
- data/sig/rigor/analysis/fact_store.rbs +1 -0
- data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
- data/sig/rigor/cli/diff_command.rbs +1 -1
- data/sig/rigor/cli/explain_command.rbs +1 -1
- data/sig/rigor/effects/file_collection.rbs +5 -6
- data/sig/rigor/effects/registry.rbs +8 -6
- data/sig/rigor/effects/summary.rbs +6 -5
- data/sig/rigor/environment.rbs +5 -3
- data/sig/rigor/inference/optimistic_origin.rbs +11 -0
- data/sig/rigor/inference.rbs +10 -1
- data/sig/rigor/plugin/additional_initializer.rbs +0 -5
- data/sig/rigor/plugin/base.rbs +1 -0
- data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
- data/sig/rigor/plugin/effect_attribution.rbs +25 -10
- data/sig/rigor/plugin/effect_edge.rbs +12 -5
- data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
- data/sig/rigor/plugin/io_boundary.rbs +6 -5
- data/sig/rigor/plugin/manifest.rbs +16 -18
- data/sig/rigor/plugin/protocol_contract.rbs +6 -9
- data/sig/rigor/plugin/registry.rbs +1 -1
- data/sig/rigor/scope.rbs +51 -50
- data/sig/rigor/type.rbs +0 -6
- data/skills/rigor-plugin-author/SKILL.md +4 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
- data/skills/rigor-plugin-review/SKILL.md +3 -3
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +12 -1
- metadata +55 -3
|
@@ -31,7 +31,12 @@ module Rigor
|
|
|
31
31
|
# `nil` when the re-join changed nothing, so a non-adder (`pop`, `sort!`) on an already-widened
|
|
32
32
|
# carrier does not rebind the binding at all: a rebind drops the per-slot narrowings `with_local`
|
|
33
33
|
# keys to the old value, and paying that for an identical type is a straight loss.
|
|
34
|
-
def widen_nominal(nominal, method_name, values: :widen, arg_types: MutationWidening::NO_ARG_TYPES
|
|
34
|
+
def widen_nominal(nominal, method_name, values: :widen, arg_types: MutationWidening::NO_ARG_TYPES,
|
|
35
|
+
synthesised: false)
|
|
36
|
+
if synthesised && (synthesised_carrier?(nominal) || partially_gradual_hash?(nominal))
|
|
37
|
+
return widen_synthesised(nominal, method_name, values:, arg_types:)
|
|
38
|
+
end
|
|
39
|
+
return widen_precise_array(nominal, method_name, values:, arg_types:) if precise_array?(nominal)
|
|
35
40
|
return nil unless regrowable_carrier?(nominal)
|
|
36
41
|
|
|
37
42
|
pre_state = values == :widen ? value_pin_widened_args(nominal) : nominal
|
|
@@ -115,19 +120,250 @@ module Rigor
|
|
|
115
120
|
# This is a superset of "mutation-widened": a DECLARED `Array[untyped]` or
|
|
116
121
|
# `Array[Integer | untyped]` matches too. Deliberate — the soundness argument above is about
|
|
117
122
|
# the carrier being open, not about who opened it, and a declared gradual arm is the author
|
|
118
|
-
# saying the collection may hold anything.
|
|
119
|
-
#
|
|
120
|
-
#
|
|
123
|
+
# saying the collection may hold anything.
|
|
124
|
+
#
|
|
125
|
+
# Issue #2555 L3 — the provenance this boundary turns on travels beside the binding, not with the value: a
|
|
126
|
+
# local whose object the engine built carries the `:synthesised` construction fact ({Scope#construction_fact?}),
|
|
127
|
+
# and {#widen_synthesised} answers for it before this predicate is asked. A field on `Type::Nominal` was the
|
|
128
|
+
# other route; `Combinator.union`'s structural dedupe would keep whichever tagged twin it met first.
|
|
121
129
|
def regrowable_carrier?(nominal)
|
|
122
130
|
return false unless %w[Array Hash].include?(nominal.class_name)
|
|
123
131
|
|
|
124
132
|
nominal.type_args.any? { |arg| gradual_parameter?(arg) }
|
|
125
133
|
end
|
|
126
134
|
|
|
135
|
+
# Issue #2555 L3 — a PRECISE `Array` / `Hash` nominal (no parameter carries a `Dynamic` arm), the carrier
|
|
136
|
+
# {#widen_synthesised} re-grows when its binding carries the `:synthesised` fact. A carrier that already carries
|
|
137
|
+
# a gradual arm keeps the ordinary re-join, whose floor it holds already.
|
|
138
|
+
def synthesised_carrier?(nominal)
|
|
139
|
+
%w[Array Hash].include?(nominal.class_name) && !nominal.type_args.empty? &&
|
|
140
|
+
nominal.type_args.none? { |arg| gradual_parameter?(arg) }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Issue #2555 L3 — a `Hash` with one gradual parameter and one precise one: `Hash.new(0)` is
|
|
144
|
+
# `Hash[Dynamic[top], Integer]`. On a synthesised binding the precise side is this engine's own fold of the
|
|
145
|
+
# default, not a declaration, so {#keep_precise_parameters} must not restore it over a store:
|
|
146
|
+
# `h = Hash.new(0); h[:y] = "s"` reads `Hash[Dynamic[top] | Symbol, Integer | String]`, not
|
|
147
|
+
# `Hash[Dynamic[top] | Symbol, Integer]` (which made `h[:y].upcase` an `undefined method` on Integer). The
|
|
148
|
+
# counter idiom keeps its `Integer`: `h[:x] += 1` stores an `Integer`.
|
|
149
|
+
def partially_gradual_hash?(nominal)
|
|
150
|
+
nominal.class_name == "Hash" && nominal.type_args.size == 2 &&
|
|
151
|
+
nominal.type_args.any? { |arg| gradual_parameter?(arg) } &&
|
|
152
|
+
nominal.type_args.any? { |arg| !gradual_parameter?(arg) }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Issue #2555 L3 — the re-join for a precise carrier the engine built itself: a collection literal closed by a
|
|
156
|
+
# value-preserving mutator (`a = [1, 2]; a.sort!` is `Array[1 | 2]`, `g = { a: 1 }; g.delete(:b)` is
|
|
157
|
+
# `Hash[Symbol, 1]`), a constructor fold (`Array.new(n, 0)` is `Array[Integer]`), or a literal built as a
|
|
158
|
+
# nominal outright (`{ ENV["A"].to_s => 1 }`). Its element types are this engine's inference, not a claim a
|
|
159
|
+
# declaration made, so the #561 boundary ({#regrowable_carrier?}, {#precise_admitted}) does not apply:
|
|
160
|
+
#
|
|
161
|
+
# - an added member joins as itself (value pin and literal shape dropped, {MutationWidening.value_pin_widened}),
|
|
162
|
+
# with no seed-admissibility gate and no `Dynamic[top]` floor — `Array.new(n, 0) << "s"` reads
|
|
163
|
+
# `Array[Integer | String]`;
|
|
164
|
+
# - a rewriter takes {RewriteMutation.arm}'s gradual position, as a gradual carrier does —
|
|
165
|
+
# `g.transform_values!(&:to_s)` reads `Hash[Symbol, Dynamic[top]]`;
|
|
166
|
+
# - an adder whose arguments yield no evidence is one unknown store and joins `Dynamic[top]`; one called with no
|
|
167
|
+
# argument types at all (the block-capture seam, whose content join adds the stores afterwards) changes
|
|
168
|
+
# nothing.
|
|
169
|
+
#
|
|
170
|
+
# The floor the ordinary re-join adds exists because a carrier it closed could not re-grow; this one stays
|
|
171
|
+
# re-growable while the binding keeps the fact, so every later store made THROUGH THE BINDING is seen. A store
|
|
172
|
+
# through another holder is not, which is why a local that may escape anywhere in its body never carries the
|
|
173
|
+
# fact ({ConstructionEscape}). Nor is a remover taking a store out again, which is why a remover on a carrier a
|
|
174
|
+
# store has grown restores the floor ({MutationWidening.floor_grown_carrier}).
|
|
175
|
+
def widen_synthesised(nominal, method_name, values:, arg_types:)
|
|
176
|
+
pre_state = values == :widen ? value_pin_widened_args(nominal) : nominal
|
|
177
|
+
rejoined =
|
|
178
|
+
case nominal.class_name
|
|
179
|
+
when "Array"
|
|
180
|
+
return nil unless MutationWidening::ARRAY_MUTATORS.include?(method_name)
|
|
181
|
+
|
|
182
|
+
synthesised_array_join(RewriteMutation.arm(pre_state, method_name), method_name, arg_types)
|
|
183
|
+
when "Hash"
|
|
184
|
+
return nil unless MutationWidening::HASH_MUTATORS.include?(method_name)
|
|
185
|
+
|
|
186
|
+
synthesised_hash_join(RewriteMutation.arm(pre_state, method_name), method_name, arg_types)
|
|
187
|
+
end
|
|
188
|
+
rejoined == nominal ? nil : rejoined
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Issue #2555 — the removers and filters of an Array, a Hash or a Set: each can take a stored member out again,
|
|
192
|
+
# which nothing narrows the carrier back from.
|
|
193
|
+
SYNTHESISED_REMOVERS = %i[
|
|
194
|
+
pop shift slice! delete delete? delete_at delete_if reject! select! filter! keep_if compact! clear subtract
|
|
195
|
+
].to_set.freeze
|
|
196
|
+
|
|
197
|
+
# The adders whose store marks a synthesised binding `:grown`.
|
|
198
|
+
def content_adder?(method_name)
|
|
199
|
+
ContentJoin::CONTENT_ADDERS.include?(method_name) || SetMutation::CONTENT_ADDERS.include?(method_name)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Whether a store turned `current` into `widened` by changing an Array / Hash / Set parameter that carried no
|
|
203
|
+
# `Dynamic` arm before it: a member joined where no floor covered it, which a later remover could leave stale.
|
|
204
|
+
# A store the parameter already held, or one that joined beside an earlier floor (`Hash.new(0)`'s key side),
|
|
205
|
+
# grows nothing.
|
|
206
|
+
def grew_unfloored?(current, widened)
|
|
207
|
+
return false if widened.nil?
|
|
208
|
+
|
|
209
|
+
before = ContentJoin.union_members(current)
|
|
210
|
+
ContentJoin.union_members(widened).any? do |member|
|
|
211
|
+
next false unless member.is_a?(Type::Nominal) && %w[Array Hash Set].include?(member.class_name)
|
|
212
|
+
|
|
213
|
+
prior = before.find { |b| same_carrier?(b, member) }
|
|
214
|
+
member.type_args.each_with_index.any? do |arg, i|
|
|
215
|
+
prior.nil? || (!gradual_parameter?(prior.type_args[i]) && prior.type_args[i] != arg)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def same_carrier?(type, nominal)
|
|
221
|
+
type.is_a?(Type::Nominal) && type.class_name == nominal.class_name &&
|
|
222
|
+
type.type_args.size == nominal.type_args.size
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Whether `method_name` on the synthesised local `name` takes the floor: a remover on a carrier a store grew.
|
|
226
|
+
def floors_grown_carrier?(scope, name, method_name)
|
|
227
|
+
SYNTHESISED_REMOVERS.include?(method_name) && scope.construction_fact?(name, :grown)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# `type` with every Array / Hash / Set parameter joined with `Dynamic[top]`, member-wise through a union: the
|
|
231
|
+
# reading the carrier would have had without the fact. With `drop_nil` (a remover that can take a `nil` out,
|
|
232
|
+
# {MutationWidening.removes_nil?}) the parameter's `nil` member goes too: on the path without the fact a stored
|
|
233
|
+
# `nil` the seed did not accept joined as the floor alone ({#precise_admitted}), and a `nil` kept beside the floor
|
|
234
|
+
# would still draw `call.possible-nil-receiver` after `parts << nil; parts.compact!`. Any other remover keeps the
|
|
235
|
+
# `nil`, which it may leave behind (`s << nil; s.delete(1)`).
|
|
236
|
+
def floor_parameters(type, drop_nil: false)
|
|
237
|
+
case type
|
|
238
|
+
when Type::Union then Type::Combinator.union(*type.members.map { |member| floor_parameters(member, drop_nil:) })
|
|
239
|
+
when Type::Nominal
|
|
240
|
+
return type unless %w[Array Hash Set].include?(type.class_name) && !type.type_args.empty?
|
|
241
|
+
|
|
242
|
+
Type::Combinator.nominal_of(type.class_name,
|
|
243
|
+
type_args: type.type_args.map { |arg| floored_parameter(arg, drop_nil) })
|
|
244
|
+
else type
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def hash_carrier?(type)
|
|
249
|
+
ContentJoin.union_members(type).any? { |member| member.is_a?(Type::Nominal) && member.class_name == "Hash" }
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def floored_parameter(arg, drop_nil)
|
|
253
|
+
kept = ContentJoin.union_members(arg)
|
|
254
|
+
kept = kept.reject { |member| member.is_a?(Type::Constant) && member.value.nil? } if drop_nil
|
|
255
|
+
Type::Combinator.union(*kept, Type::Combinator.untyped)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def synthesised_array_join(carrier, method_name, arg_types)
|
|
259
|
+
return carrier unless ContentJoin::ARRAY_CONTENT_ADDERS.include?(method_name) && !arg_types.empty?
|
|
260
|
+
|
|
261
|
+
added = MutationWidening.value_pin_widened(ContentJoin.array_added_elements(method_name, arg_types))
|
|
262
|
+
ContentJoin.join_array_content(carrier, added.empty? ? [Type::Combinator.untyped] : added)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def synthesised_hash_join(carrier, method_name, arg_types)
|
|
266
|
+
return carrier unless ContentJoin::HASH_CONTENT_ADDERS.include?(method_name) && !arg_types.empty?
|
|
267
|
+
|
|
268
|
+
# A `nil` key marks a splat, an unknown key ({MutationWidening.join_added_pairs}).
|
|
269
|
+
key, value = MutationWidening.value_pin_widened([arg_types.first || Type::Combinator.untyped, arg_types.last])
|
|
270
|
+
value ||= Type::Combinator.untyped
|
|
271
|
+
ContentJoin.join_hash_content(carrier, [[key, value]])
|
|
272
|
+
end
|
|
273
|
+
|
|
127
274
|
def gradual_parameter?(type)
|
|
128
275
|
ContentJoin.union_members(type).any?(Type::Dynamic)
|
|
129
276
|
end
|
|
130
277
|
|
|
278
|
+
# A typed `Array[E]` whose `E` has no `Dynamic` arm — the carrier {#regrowable_carrier?} refuses.
|
|
279
|
+
def precise_array?(nominal)
|
|
280
|
+
nominal.class_name == "Array" && nominal.type_args.size == 1 && !gradual_parameter?(nominal.type_args.first)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Issue #2361 — a PRECISE `Array[E]` re-grows under a store, on the literal arms' terms: `a = ARGV.dup;
|
|
284
|
+
# a.clear; a << 1; a.each { |v| v.fdiv(2) }` read `Array[String]` and reported `undefined method 'fdiv' for
|
|
285
|
+
# String` on code that prints `[0.5]`.
|
|
286
|
+
#
|
|
287
|
+
# Only a content adder ({ContentJoin::ARRAY_CONTENT_ADDERS}) re-grows it, and the join keeps the seed: `clear`
|
|
288
|
+
# empties the array but leaves `E` standing, a superset of what it holds. A remover leaves the carrier as it
|
|
289
|
+
# found it. An element rewriter (`map!`, `collect!`, `flatten!`) replaces `E` (issue #2427): with the element
|
|
290
|
+
# the non-bang counterpart answers when the call site could type it ({RewriteMutation.typed}, answered before
|
|
291
|
+
# this method), and otherwise with {RewriteMutation.arm}'s gradual element — `a.map!.with_index { … }` holds
|
|
292
|
+
# whatever the block returns, and keeping the stale `E` reported a missing method on correct code.
|
|
293
|
+
#
|
|
294
|
+
# Each added member the element accepts joins as itself; any other takes the `Dynamic[top]` floor
|
|
295
|
+
# ({#precise_admitted}), and the floor joins every re-grow as it does on the literal arms. `a << 1` therefore
|
|
296
|
+
# reads `Array[Dynamic[top] | String]` and `s = ints.map { "x" }; s << "y"` reads `Array["x" | Dynamic[top]]`.
|
|
297
|
+
# Nothing narrows a carrier back when a remover or rewriter takes the store out again (`a << 1; a.pop`,
|
|
298
|
+
# `a << nil; a.compact!`, `a.map!(&:to_s)`), so a joined `Integer` or `nil` made `t.take(a)` against a declared
|
|
299
|
+
# `Array[String]` report `call.argument-type-mismatch`, and a parameter returned as its declared
|
|
300
|
+
# `-> Array[String]` report `def.return-type-mismatch`, on correct Ruby. A union carrying the floor neither folds
|
|
301
|
+
# nor dispatches loudly, so the read stays silent. Once floored, the carrier is gradual and later stores re-join
|
|
302
|
+
# through {#widen_nominal}'s ordinary arm.
|
|
303
|
+
#
|
|
304
|
+
# A store the element already accepts changes nothing ({#already_held?}). An adder with no argument types at
|
|
305
|
+
# all is left alone, as {MutationWidening.join_added_elements} leaves it: the block-capture seams pass none,
|
|
306
|
+
# and their own content join adds the stores.
|
|
307
|
+
def widen_precise_array(nominal, method_name, values:, arg_types:)
|
|
308
|
+
return RewriteMutation.arm(nominal, method_name) if RewriteMutation::TYPED_COUNTERPARTS.key?(method_name)
|
|
309
|
+
return nil unless ContentJoin::ARRAY_CONTENT_ADDERS.include?(method_name) && !arg_types.empty?
|
|
310
|
+
|
|
311
|
+
stored = ContentJoin.array_added_elements(method_name, arg_types)
|
|
312
|
+
return nil if already_held?(nominal.type_args.first, stored)
|
|
313
|
+
|
|
314
|
+
added = MutationWidening.value_pin_widened(stored)
|
|
315
|
+
admitted = precise_admitted(nominal.type_args.first, added)
|
|
316
|
+
pre_state = values == :widen ? value_pin_widened_args(nominal) : nominal
|
|
317
|
+
rejoined = ContentJoin.join_array_content(pre_state, MutationWidening.gradual_floor(admitted))
|
|
318
|
+
rejoined == nominal ? nil : rejoined
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# The seed-admissibility gate for a precise element: an added member joins as itself only when `element`
|
|
322
|
+
# already accepts it, and takes the `Dynamic[top]` floor otherwise. A precise element therefore never gains a
|
|
323
|
+
# member it rejects, which is what a declared boundary checks: nothing narrows the carrier back when a remover
|
|
324
|
+
# or rewriter takes the store out again, and a joined foreign arm stays rejected beside the floor — `Integer`
|
|
325
|
+
# under a declared `Array[String]`, `String` under `-> Array["x"]`, `Array[Integer]` under
|
|
326
|
+
# `Array[Array[String]]` each reported on correct Ruby. A class set ({ContentJoin.admissible_evidence}) is too
|
|
327
|
+
# coarse for that: it admits `Array[Integer]` beside `[String]` and `"y"` beside `"x"`.
|
|
328
|
+
def precise_admitted(element, added)
|
|
329
|
+
added.flat_map do |type|
|
|
330
|
+
ContentJoin.union_members(type).map do |member|
|
|
331
|
+
Acceptance.accepts(element, member).yes? ? member : Type::Combinator.untyped
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# True when the precise element `element` already covers every type `stored` puts in, so the store changes
|
|
337
|
+
# nothing: `s = ints.map { "x" }; s << "x"` keeps `Array["x"]` and its `s.last == "x"` fold, which is correct,
|
|
338
|
+
# and a `[Integer, Integer]` pushed onto an `Array[[0 | Integer, Integer]]` keeps the Tuple element. Joining
|
|
339
|
+
# the value-pin-widened, shape-erased store instead would only lose precision.
|
|
340
|
+
#
|
|
341
|
+
# A stored type carrying `Dynamic` anywhere is never covered, though `Acceptance` passes it gradually: keeping
|
|
342
|
+
# the element would claim the unknown part is what the element says (`retry_binding_accepted?`'s rule).
|
|
343
|
+
def already_held?(element, stored)
|
|
344
|
+
return false if stored.empty?
|
|
345
|
+
|
|
346
|
+
held = ContentJoin.union_members(element)
|
|
347
|
+
stored.all? do |type|
|
|
348
|
+
next false if type.nil? || carries_dynamic?(type)
|
|
349
|
+
|
|
350
|
+
ContentJoin.union_members(type).all? { |member| held.include?(member) } ||
|
|
351
|
+
Acceptance.accepts(element, type).yes?
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def carries_dynamic?(type)
|
|
356
|
+
case type
|
|
357
|
+
when Type::Dynamic then true
|
|
358
|
+
when Type::Union, Type::Intersection then type.members.any? { |member| carries_dynamic?(member) }
|
|
359
|
+
when Type::Nominal then type.type_args.any? { |arg| carries_dynamic?(arg) }
|
|
360
|
+
when Type::Tuple then type.elements.any? { |element| carries_dynamic?(element) }
|
|
361
|
+
when Type::HashShape then type.pairs.each_value.any? { |value| carries_dynamic?(value) }
|
|
362
|
+
when Type::Difference, Type::Refined then carries_dynamic?(type.base)
|
|
363
|
+
else false
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
131
367
|
# The value-rewriting counterpart of {MutationWidening.widen_tuple}'s `values: :widen`. A mutator that lands a
|
|
132
368
|
# new value in an EXISTING slot falsifies that slot's pinning on a re-opened carrier exactly as
|
|
133
369
|
# it does on a literal one, so `d = [:multi]; d << x; d[0] = "s"` must not keep reading `:multi`
|
|
@@ -7,9 +7,13 @@ require_relative "../source/node_children"
|
|
|
7
7
|
require_relative "content_join"
|
|
8
8
|
require_relative "hash_lookup_mutation"
|
|
9
9
|
require_relative "mutation_rejoin"
|
|
10
|
+
require_relative "deep_erasure"
|
|
10
11
|
require_relative "receiver_alias"
|
|
11
12
|
require_relative "refinement_mutation"
|
|
13
|
+
# `RemoverNarrowing` is loaded with the engine (`scope.rb`), not here: the engine-free value layer requires this file
|
|
14
|
+
# and must not pull in the remover narrowing and its holder scan (`spec/rigor/cli/effects_cache_probe_spec.rb`).
|
|
12
15
|
require_relative "rewrite_mutation"
|
|
16
|
+
require_relative "set_mutation"
|
|
13
17
|
require_relative "string_mutation"
|
|
14
18
|
|
|
15
19
|
module Rigor
|
|
@@ -18,8 +22,7 @@ module Rigor
|
|
|
18
22
|
# AND whose method is a known in-place mutator.
|
|
19
23
|
#
|
|
20
24
|
# Closes the **G1 / G2** flow-folding gaps documented at
|
|
21
|
-
# `docs/notes/20260521-mastodon-cluster4-flow-folding-triage.md
|
|
22
|
-
# [`docs/CURRENT_WORK.md`](../../../docs/CURRENT_WORK.md) § "Flow-folding". The user-visible
|
|
25
|
+
# `docs/notes/20260521-mastodon-cluster4-flow-folding-triage.md`. The user-visible
|
|
23
26
|
# symptom they shared was a spurious `flow.always-truthy-condition` on a `arr.size == N` /
|
|
24
27
|
# `arr.empty?` / `@arr.empty?` check that follows a loop body or sibling method that mutates
|
|
25
28
|
# `arr` / `@arr` in place.
|
|
@@ -75,6 +78,7 @@ module Rigor
|
|
|
75
78
|
# ARE also handled** (ivars live in the method-body scope, not the block-local scope).
|
|
76
79
|
#
|
|
77
80
|
# The remaining three items above are demand-gated; see ADR-56.
|
|
81
|
+
# rubocop:disable-next Metrics/ModuleLength
|
|
78
82
|
module MutationWidening
|
|
79
83
|
# Array mutators that change either the size or the element set of a literal-shape carrier
|
|
80
84
|
# (Tuple). Receiver-mutating methods only — non-mutating siblings (`map` vs `map!`,
|
|
@@ -106,7 +110,8 @@ module Rigor
|
|
|
106
110
|
# Every method name {#widen_for_mutator} responds to — the one set a body scan asks "could an
|
|
107
111
|
# in-place call on this name change its binding?" against (issue #587: the block-return
|
|
108
112
|
# threading gate). Derived from the tables rather than spelled out, so the scan and the
|
|
109
|
-
# widening it predicts cannot drift apart.
|
|
113
|
+
# widening it predicts cannot drift apart. The Set-only names ({SetMutation::MUTATORS}) stay out: the scans read
|
|
114
|
+
# a name without its receiver, and `merge` on a Hash returns a new Hash (issue #2294).
|
|
110
115
|
SHAPE_MUTATORS = (ARRAY_MUTATORS | HASH_MUTATORS | HashLookupMutation::MUTATORS | StringMutation::MUTATORS).freeze
|
|
111
116
|
|
|
112
117
|
# Methods that return the receiver (or a shallow copy) and cannot mutate it. They must not
|
|
@@ -133,10 +138,61 @@ module Rigor
|
|
|
133
138
|
# pre-join behaviour exactly.
|
|
134
139
|
NO_ARG_TYPES = [].freeze
|
|
135
140
|
|
|
136
|
-
|
|
141
|
+
# `rewritten_element` is the element a typed element rewriter stores ({RewriteMutation.typed}), or `nil`.
|
|
142
|
+
def widen_after_call(call_node:, current_scope:, arg_types: NO_ARG_TYPES, rewritten_element: nil)
|
|
137
143
|
return current_scope if pure_self_returner?(call_node.name)
|
|
138
144
|
|
|
139
|
-
|
|
145
|
+
if RemoverNarrowing.candidate?(call_node)
|
|
146
|
+
narrowed = narrow_remover_receiver(call_node, current_scope, arg_types)
|
|
147
|
+
return narrowed if narrowed
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
widen_receiver_aliases(call_node.receiver, call_node.name, current_scope, arg_types:, rewritten_element:,
|
|
151
|
+
removes_nil: removes_nil?(call_node))
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Issue #2608 — a remover that narrows its receiver by what it removes ({RemoverNarrowing}): the receiver's
|
|
155
|
+
# binding widens as any mutator's does, then loses what the call provably took out. This answers before the
|
|
156
|
+
# #2555 remover floor, so a synthesised carrier a store grew keeps its precise reading (`parts << maybe_nil;
|
|
157
|
+
# parts.compact!` reads `Array[String]`). `nil` when the call narrows nothing; the caller then takes the
|
|
158
|
+
# ordinary path, the floor included.
|
|
159
|
+
def narrow_remover_receiver(call_node, scope, arg_types)
|
|
160
|
+
read = call_node.receiver
|
|
161
|
+
getter, builder = ALIAS_ACCESSORS[read.class]
|
|
162
|
+
name = ReceiverAlias.read_name(read)
|
|
163
|
+
current = scope.public_send(getter, name)
|
|
164
|
+
return nil if current.nil? || RemoverHolderScan.unsafe_receiver?(scope, name)
|
|
165
|
+
|
|
166
|
+
narrowed = RemoverNarrowing.narrow_shape(current, call_node)
|
|
167
|
+
if narrowed.nil?
|
|
168
|
+
synthesised = synthesised_binding?(scope, getter, name)
|
|
169
|
+
base = widen_for_mutator(current, call_node.name, arg_types:, synthesised:)
|
|
170
|
+
narrowed = RemoverNarrowing.narrow(base || current, call_node, scope)
|
|
171
|
+
end
|
|
172
|
+
return nil if narrowed.nil?
|
|
173
|
+
return scope if narrowed == current
|
|
174
|
+
|
|
175
|
+
MutationRejoin.rebind(scope, builder, name, narrowed, pre_state: current, kind: getter)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Issue #2555 — the removers that may take a `nil` out, which is every one but a `delete` / `delete?` given a
|
|
179
|
+
# non-nil literal: false positives outrank, so a positional remover (`pop`, `shift`, `slice!`), `subtract`
|
|
180
|
+
# (whose argument may hold `nil`) and a `delete` of an unknown value count. `Hash#delete` removes by key, so
|
|
181
|
+
# whatever value the key held may go ({.floor_grown_carrier} reads the receiver for it).
|
|
182
|
+
NIL_REMOVERS = %i[compact! reject! select! filter! delete_if keep_if clear pop shift slice!
|
|
183
|
+
subtract].to_set.freeze
|
|
184
|
+
NON_NIL_LITERALS = [
|
|
185
|
+
Prism::IntegerNode, Prism::FloatNode, Prism::RationalNode, Prism::ImaginaryNode, Prism::StringNode,
|
|
186
|
+
Prism::SymbolNode, Prism::TrueNode, Prism::FalseNode
|
|
187
|
+
].freeze
|
|
188
|
+
private_constant :NON_NIL_LITERALS
|
|
189
|
+
|
|
190
|
+
def removes_nil?(call_node)
|
|
191
|
+
return true if NIL_REMOVERS.include?(call_node.name)
|
|
192
|
+
return false unless %i[delete delete?].include?(call_node.name)
|
|
193
|
+
|
|
194
|
+
argument = call_node.arguments&.arguments&.first
|
|
195
|
+
NON_NIL_LITERALS.none? { |klass| argument.is_a?(klass) }
|
|
140
196
|
end
|
|
141
197
|
|
|
142
198
|
# True when `receiver` names at least one variable whose CURRENT binding is a literal-shape
|
|
@@ -147,10 +203,19 @@ module Rigor
|
|
|
147
203
|
|
|
148
204
|
ReceiverAlias.mutated_reads(receiver).any? do |read|
|
|
149
205
|
getter, = ALIAS_ACCESSORS[read.class]
|
|
150
|
-
|
|
206
|
+
next false if getter.nil?
|
|
207
|
+
|
|
208
|
+
name = ReceiverAlias.read_name(read)
|
|
209
|
+
shape_carrier?(scope.public_send(getter, name), synthesised: synthesised_binding?(scope, getter, name))
|
|
151
210
|
end
|
|
152
211
|
end
|
|
153
212
|
|
|
213
|
+
# Issue #2555 L3 — whether the binding `getter` / `name` reads carries the `:synthesised` construction fact
|
|
214
|
+
# ({Scope#construction_fact?}). Only a local can: an instance, class or global variable never carries one.
|
|
215
|
+
def synthesised_binding?(scope, getter, name)
|
|
216
|
+
getter == :local && scope.construction_fact?(name)
|
|
217
|
+
end
|
|
218
|
+
|
|
154
219
|
# A union carrying a shape member is joinable too (issue #645): {#widen_union} widens that
|
|
155
220
|
# member through the same join, so the arguments the caller would skip are the evidence it
|
|
156
221
|
# needs.
|
|
@@ -159,26 +224,32 @@ module Rigor
|
|
|
159
224
|
# An empty-witness `Difference` counts too (issue #936): the `non-empty-array[T]` arm joins the
|
|
160
225
|
# mutator's added element now, so its arguments are evidence this seam consumes rather than
|
|
161
226
|
# arguments nothing will read.
|
|
162
|
-
|
|
227
|
+
# A precise `Hash` counts too when its binding carries the `:synthesised` construction fact (issue #2555): the
|
|
228
|
+
# re-join grows it from these arguments.
|
|
229
|
+
def shape_carrier?(type, synthesised: false)
|
|
163
230
|
case type
|
|
164
231
|
when Type::Tuple, Type::HashShape then true
|
|
165
|
-
when Type::Constant then StringMutation.constant?(type)
|
|
232
|
+
when Type::Constant then StringMutation.constant?(type) || SetMutation.constant?(type)
|
|
166
233
|
# A re-openable nominal counts since issue #580: skipping it made the one-way door
|
|
167
234
|
# self-fulfilling, since the arm never saw the arg types it grows from.
|
|
168
|
-
|
|
235
|
+
# A precise `Array[E]` / `Set[E]` counts since issue #2361, which re-grows it from these arguments.
|
|
236
|
+
when Type::Nominal
|
|
237
|
+
MutationRejoin.regrowable_carrier?(type) || MutationRejoin.precise_array?(type) ||
|
|
238
|
+
SetMutation.regrowable?(type) || (synthesised && MutationRejoin.synthesised_carrier?(type))
|
|
169
239
|
when Type::Difference then type.removes_empty_witness?
|
|
170
|
-
when Type::Union then type.members.any? { |m| shape_carrier?(m) }
|
|
240
|
+
when Type::Union then type.members.any? { |m| shape_carrier?(m, synthesised:) }
|
|
171
241
|
else false
|
|
172
242
|
end
|
|
173
243
|
end
|
|
174
244
|
|
|
175
245
|
# Widens every variable `receiver` can evaluate to ({ReceiverAlias.mutated_reads}), against `method_name`'s
|
|
176
246
|
# mutator table.
|
|
177
|
-
def widen_receiver_aliases(receiver, method_name, current_scope, arg_types: NO_ARG_TYPES
|
|
247
|
+
def widen_receiver_aliases(receiver, method_name, current_scope, arg_types: NO_ARG_TYPES, rewritten_element: nil,
|
|
248
|
+
removes_nil: false)
|
|
178
249
|
return current_scope if receiver.nil?
|
|
179
250
|
|
|
180
251
|
ReceiverAlias.mutated_reads(receiver).reduce(current_scope) do |acc, read|
|
|
181
|
-
widen_alias_read(method_name, read, acc, arg_types:
|
|
252
|
+
widen_alias_read(method_name, read, acc, arg_types:, rewritten_element:, removes_nil:)
|
|
182
253
|
end
|
|
183
254
|
end
|
|
184
255
|
|
|
@@ -195,17 +266,19 @@ module Rigor
|
|
|
195
266
|
# always safe — it can only LOSE precision — so blindly propagating is sound regardless of
|
|
196
267
|
# whether the block actually runs.
|
|
197
268
|
#
|
|
198
|
-
# Recurses into nested
|
|
199
|
-
# g(y)`, `arr.push(x) if cond`) are all caught.
|
|
200
|
-
# `Prism::BlockNode`s — each block is processed by its own `eval_call`.
|
|
269
|
+
# Recurses into every nested node, blocks included, so chained / nested forms (`arr << f(x); arr <<
|
|
270
|
+
# g(y)`, `arr.push(x) if cond`) are all caught.
|
|
201
271
|
def widen_after_block(call_node:, outer_scope:)
|
|
202
272
|
block = call_node.block
|
|
273
|
+
return DeepErasure.widen_symbol_proc(call_node, outer_scope) if DeepErasure.symbol_proc_mutation?(block)
|
|
203
274
|
return outer_scope unless block.is_a?(Prism::BlockNode)
|
|
204
275
|
|
|
205
276
|
body = block.body
|
|
206
277
|
return outer_scope if body.nil?
|
|
207
278
|
|
|
208
|
-
walk_for_outer_mutations(body, outer_scope)
|
|
279
|
+
scope = walk_for_outer_mutations(body, outer_scope)
|
|
280
|
+
names = DeepErasure.parameter_names(block.parameters)
|
|
281
|
+
DeepErasure.widen_element_aliases(call_node.receiver, block.body, names, scope)
|
|
209
282
|
end
|
|
210
283
|
|
|
211
284
|
def walk_for_outer_mutations(node, scope)
|
|
@@ -231,6 +304,9 @@ module Rigor
|
|
|
231
304
|
receiver = call_node.receiver
|
|
232
305
|
return scope if receiver.nil?
|
|
233
306
|
|
|
307
|
+
widened = widen_outer_element_read(call_node, scope)
|
|
308
|
+
return widened if widened
|
|
309
|
+
|
|
234
310
|
ReceiverAlias.candidates(receiver).reduce(scope) do |acc, read|
|
|
235
311
|
# A block-local read ({ReceiverAlias.block_local?}: a `depth == 0` local, or `it`) is not a
|
|
236
312
|
# capture of the outer scope, so widening its name against the OUTER scope would hit an
|
|
@@ -241,10 +317,27 @@ module Rigor
|
|
|
241
317
|
# after it re-adds the appended values' types onto the widened seed, so the seed's own value
|
|
242
318
|
# pinning is still justified (`out = [0]; arr.each { out << x }` never rewrites slot 0). The
|
|
243
319
|
# straight-line paths have no such join, which is why their default is `:widen` (issue #560).
|
|
244
|
-
widen_alias_read(call_node.name, read, acc, values: :keep)
|
|
320
|
+
widen_alias_read(call_node.name, read, acc, values: :keep, unread_arguments: !call_node.arguments.nil?)
|
|
245
321
|
end
|
|
246
322
|
end
|
|
247
323
|
|
|
324
|
+
# Issue #2287 — a mutator on an element read rooted at a captured local (`[0].each { |i| a[i] << 2 }`,
|
|
325
|
+
# `loop { a[0] << 2; break }`) mutates an element the outer binding still carries, and names no variable
|
|
326
|
+
# {ReceiverAlias.candidates} could widen. The element at that path widens in the outer scope as the
|
|
327
|
+
# straight-line {ElementReadWidening.widen_element_read} widens it; a block-local root is skipped as a
|
|
328
|
+
# block-local read is. Answers the widened scope, or `nil` when the receiver is no such read or nothing widened.
|
|
329
|
+
def widen_outer_element_read(call_node, scope)
|
|
330
|
+
path = ElementReadWidening.element_read_path(call_node.receiver)
|
|
331
|
+
return nil if path.nil?
|
|
332
|
+
|
|
333
|
+
read, steps = path
|
|
334
|
+
return nil if ReceiverAlias.block_local?(read)
|
|
335
|
+
|
|
336
|
+
current = scope.local(read.name)
|
|
337
|
+
widened = current && ElementReadWidening.widen_through_path(current, steps, call_node.name, NO_ARG_TYPES)
|
|
338
|
+
widened && scope.with_mutated_local(read.name, widened)
|
|
339
|
+
end
|
|
340
|
+
|
|
248
341
|
# The arms per kind of binding differ only in which pair of `Scope` accessors they use, so they are read out
|
|
249
342
|
# of one table rather than written once per kind: a carrier the widening responds to must not be widened on
|
|
250
343
|
# one kind of binding and left on another. `$g << x; $g == "k"` folded always-truthy on a `$g` Ruby holds
|
|
@@ -257,18 +350,38 @@ module Rigor
|
|
|
257
350
|
Prism::ClassVariableReadNode => %i[cvar with_cvar],
|
|
258
351
|
Prism::GlobalVariableReadNode => %i[global with_global]
|
|
259
352
|
})
|
|
260
|
-
private_constant :ALIAS_ACCESSORS
|
|
261
353
|
|
|
262
|
-
|
|
354
|
+
# `unread_arguments` says the call passes arguments this seam did not type (the block-capture path): a Set,
|
|
355
|
+
# which has no block content join, takes one unknown store for it ({SetMutation.widen}).
|
|
356
|
+
def widen_alias_read(method_name, read, scope, values: :widen, arg_types: NO_ARG_TYPES, unread_arguments: false, # rubocop:disable Metrics/ParameterLists
|
|
357
|
+
rewritten_element: nil, removes_nil: false)
|
|
263
358
|
getter, builder = ALIAS_ACCESSORS[read.class]
|
|
264
359
|
return scope if getter.nil?
|
|
265
360
|
|
|
266
361
|
name = ReceiverAlias.read_name(read)
|
|
267
362
|
current = scope.public_send(getter, name)
|
|
268
|
-
|
|
269
|
-
|
|
363
|
+
synthesised = synthesised_binding?(scope, getter, name)
|
|
364
|
+
if synthesised && MutationRejoin.floors_grown_carrier?(scope, name, method_name)
|
|
365
|
+
return floor_grown_carrier(scope, builder, name, current, method_name, getter, removes_nil)
|
|
366
|
+
end
|
|
270
367
|
|
|
271
|
-
|
|
368
|
+
widened = widen_for_mutator(current, method_name, values:, arg_types:, unread_arguments:, rewritten_element:,
|
|
369
|
+
synthesised:)
|
|
370
|
+
scope = MutationRejoin.rebind(scope, builder, name, widened, pre_state: current, kind: getter) if widened
|
|
371
|
+
return scope unless synthesised && MutationRejoin.content_adder?(method_name)
|
|
372
|
+
|
|
373
|
+
MutationRejoin.grew_unfloored?(current, widened) ? scope.with_construction_fact(name, :grown) : scope
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Issue #2555 — a remover or filter on a synthesised carrier a store has grown. Nothing narrows the carrier back,
|
|
377
|
+
# so the stored arm would outlive the values the call removes (`parts << nil; parts.compact!`); the carrier takes
|
|
378
|
+
# the `Dynamic[top]` floor it would have carried without the fact. The binding keeps its facts: the floored
|
|
379
|
+
# carrier is gradual, so the synthesised re-join no longer applies to it, and a later remover floors it again.
|
|
380
|
+
def floor_grown_carrier(scope, builder, name, current, method_name, getter, removes_nil)
|
|
381
|
+
drop_nil = removes_nil || (method_name == :delete && MutationRejoin.hash_carrier?(current))
|
|
382
|
+
floored = MutationRejoin.floor_parameters(current, drop_nil:)
|
|
383
|
+
floored = widen_for_mutator(floored, method_name) || floored
|
|
384
|
+
MutationRejoin.rebind(scope, builder, name, floored, pre_state: current, kind: getter)
|
|
272
385
|
end
|
|
273
386
|
|
|
274
387
|
# Mutators that can land a NEW value in an EXISTING slot, falsifying that slot's value pinning
|
|
@@ -282,15 +395,32 @@ module Rigor
|
|
|
282
395
|
# Returns the widened type for a binding whose receiver is about to be mutated by
|
|
283
396
|
# `method_name`, or `nil` when no widening applies (binding is not a carrier the mutation
|
|
284
397
|
# stands for, OR the method is not a mutator for that shape, OR the carrier is a PRECISE
|
|
285
|
-
# nominal
|
|
286
|
-
|
|
398
|
+
# nominal the call stores nothing into — see {MutationRejoin.widen_precise_array}).
|
|
399
|
+
# A `rewritten_element` answers first for the carriers {RewriteMutation.typed} types (issue #2427).
|
|
400
|
+
# `synthesised` says the binding carries the `:synthesised` construction fact (issue #2555,
|
|
401
|
+
# {Scope#construction_fact?}): a precise nominal or Set it holds re-grows without the `Dynamic[top]` floor
|
|
402
|
+
# ({MutationRejoin.widen_synthesised}, {SetMutation.widen}).
|
|
403
|
+
def widen_for_mutator(type, method_name, values: :widen, arg_types: NO_ARG_TYPES, unread_arguments: false,
|
|
404
|
+
rewritten_element: nil, synthesised: false)
|
|
405
|
+
typed = rewritten_element && type && RewriteMutation.typed(type, method_name, rewritten_element)
|
|
406
|
+
return typed == type ? nil : typed if typed
|
|
407
|
+
|
|
408
|
+
widen_carrier(type, method_name, values:, arg_types:, unread_arguments:, rewritten_element:, synthesised:)
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# {#widen_for_mutator} for a carrier no typed element rewrite answered.
|
|
412
|
+
def widen_carrier(type, method_name, values:, arg_types:, unread_arguments:, rewritten_element:,
|
|
413
|
+
synthesised: false)
|
|
287
414
|
values = :keep unless VALUE_REWRITING_MUTATORS.include?(method_name)
|
|
288
415
|
values = RewriteMutation.values_mode(type, method_name, values)
|
|
289
416
|
|
|
290
417
|
return nil if type.nil?
|
|
418
|
+
if SetMutation.set?(type)
|
|
419
|
+
return SetMutation.widen(type, method_name, arg_types, unread_arguments:, synthesised:)
|
|
420
|
+
end
|
|
291
421
|
|
|
292
422
|
case type
|
|
293
|
-
when Type::Nominal then MutationRejoin.widen_nominal(type, method_name, values:, arg_types:)
|
|
423
|
+
when Type::Nominal then MutationRejoin.widen_nominal(type, method_name, values:, arg_types:, synthesised:)
|
|
294
424
|
when Type::Tuple
|
|
295
425
|
return nil unless ARRAY_MUTATORS.include?(method_name)
|
|
296
426
|
|
|
@@ -304,7 +434,7 @@ module Rigor
|
|
|
304
434
|
when Type::Difference
|
|
305
435
|
widen_difference(type, method_name, arg_types: arg_types)
|
|
306
436
|
when Type::Union
|
|
307
|
-
widen_union(type, method_name, values
|
|
437
|
+
widen_union(type, method_name, values:, arg_types:, unread_arguments:, rewritten_element:, synthesised:)
|
|
308
438
|
end
|
|
309
439
|
end
|
|
310
440
|
|
|
@@ -322,12 +452,14 @@ module Rigor
|
|
|
322
452
|
#
|
|
323
453
|
# `nil` when no member widened — a union with no carrier member, or one whose carriers are
|
|
324
454
|
# already nominal, is untouched exactly as the scalar path leaves them.
|
|
325
|
-
def widen_union(union, method_name, values: :widen, arg_types: NO_ARG_TYPES
|
|
455
|
+
def widen_union(union, method_name, values: :widen, arg_types: NO_ARG_TYPES, unread_arguments: false,
|
|
456
|
+
rewritten_element: nil, synthesised: false)
|
|
326
457
|
widened_any = false
|
|
327
458
|
members = union.members.map do |member|
|
|
328
459
|
next member unless mutation_carrier?(member, method_name)
|
|
329
460
|
|
|
330
|
-
widened = widen_for_mutator(member, method_name, values
|
|
461
|
+
widened = widen_for_mutator(member, method_name, values:, arg_types:, unread_arguments:, rewritten_element:,
|
|
462
|
+
synthesised:)
|
|
331
463
|
next member if widened.nil?
|
|
332
464
|
|
|
333
465
|
widened_any = true
|
|
@@ -345,7 +477,8 @@ module Rigor
|
|
|
345
477
|
(ARRAY_MUTATORS.include?(method_name) && ContentJoin.array_residue(member).empty?) ||
|
|
346
478
|
((HASH_MUTATORS.include?(method_name) || HashLookupMutation::MUTATORS.include?(method_name)) &&
|
|
347
479
|
ContentJoin.hash_residue(member).empty?) ||
|
|
348
|
-
(StringMutation::MUTATORS.include?(method_name) && StringMutation.constant?(member))
|
|
480
|
+
(StringMutation::MUTATORS.include?(method_name) && StringMutation.constant?(member)) ||
|
|
481
|
+
(SetMutation::MUTATORS.include?(method_name) && SetMutation.carrier?(member))
|
|
349
482
|
end
|
|
350
483
|
|
|
351
484
|
# Joins the element evidence the mutator's own ARGUMENTS introduce into the already-widened
|