rigortype 0.3.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/data/core_overlay/enumerable.rbs +51 -0
- data/data/core_overlay/enumerator.rbs +84 -0
- data/data/core_overlay/hash_rbs3.rbs +41 -0
- data/data/core_overlay/process.rbs +40 -0
- data/data/core_overlay/string_io.rbs +33 -0
- data/data/effects/core.yml +3 -3
- data/data/gem_overlay/activesupport/core_ext.rbs +127 -4
- data/docs/handbook/03-narrowing.md +95 -10
- data/docs/handbook/04-tuples-and-shapes.md +8 -6
- data/docs/handbook/07-rbs-and-extended.md +16 -11
- data/docs/handbook/10-sorbet.md +9 -10
- data/docs/handbook/11-sig-gen.md +454 -13
- data/docs/manual/02-cli-reference.md +63 -2
- data/docs/manual/03-configuration.md +7 -0
- data/docs/manual/04-diagnostics.md +4 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/10-mcp-server.md +3 -2
- data/docs/manual/16-rbs-extended-annotations.md +40 -7
- data/docs/manual/19-effect-labels.md +10 -0
- data/docs/manual/plugins/README.md +7 -0
- data/docs/manual/plugins/rigor-actioncable.md +8 -1
- data/docs/manual/plugins/rigor-actionmailer.md +7 -0
- data/docs/manual/plugins/rigor-actionpack.md +243 -0
- data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
- data/docs/manual/plugins/rigor-activejob.md +7 -0
- data/docs/manual/plugins/rigor-activerecord.md +110 -4
- data/docs/manual/plugins/rigor-activestorage.md +8 -1
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
- data/docs/manual/plugins/rigor-grape.md +106 -0
- data/docs/manual/plugins/rigor-graphql.md +23 -2
- data/docs/manual/plugins/rigor-pundit.md +8 -1
- data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
- data/docs/manual/plugins/rigor-rails-routes.md +7 -0
- data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
- data/docs/manual/plugins/rigor-sidekiq.md +8 -1
- data/docs/manual/plugins/rigor-sorbet.md +21 -5
- data/exe/rigor +19 -4
- data/lib/rigor/analysis/baseline.rb +1 -1
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
- data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
- data/lib/rigor/analysis/check_rules.rb +520 -49
- data/lib/rigor/analysis/dependency_recorder.rb +23 -0
- data/lib/rigor/analysis/fact_store.rb +9 -0
- data/lib/rigor/analysis/incremental.rb +26 -0
- data/lib/rigor/analysis/incremental_session.rb +36 -4
- data/lib/rigor/analysis/project_scan.rb +11 -1
- data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
- data/lib/rigor/analysis/reachability/scan_cache.rb +1 -0
- data/lib/rigor/analysis/rule_catalog.rb +127 -0
- data/lib/rigor/analysis/run_cache_key.rb +20 -11
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +167 -11
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
- data/lib/rigor/analysis/runner/pool_coordinator.rb +107 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +26 -7
- data/lib/rigor/analysis/runner.rb +278 -20
- data/lib/rigor/analysis/template_unit_collector.rb +303 -0
- data/lib/rigor/analysis/template_unit_paths.rb +91 -0
- data/lib/rigor/analysis/template_unit_positions.rb +293 -0
- data/lib/rigor/analysis/template_units.rb +399 -0
- data/lib/rigor/analysis/worker_session.rb +53 -10
- data/lib/rigor/bleeding_edge.rb +0 -2
- data/lib/rigor/builtins/hkt_builtins.rb +1 -0
- data/lib/rigor/builtins/imported_refinements.rb +4 -0
- data/lib/rigor/builtins/regex_refinement.rb +17 -9
- data/lib/rigor/builtins/static_return_refinements.rb +2 -0
- data/lib/rigor/cache/descriptor.rb +6 -1
- data/lib/rigor/cache/engine_source.rb +29 -1
- data/lib/rigor/cache/incremental_snapshot.rb +33 -1
- data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
- data/lib/rigor/cache/rbs_descriptor.rb +37 -7
- data/lib/rigor/cache/store.rb +0 -1
- data/lib/rigor/ci_detector.rb +1 -0
- data/lib/rigor/cli/doc_links.rb +1 -1
- data/lib/rigor/cli/docs_command.rb +4 -4
- data/lib/rigor/cli/plugin_command.rb +3 -3
- data/lib/rigor/cli/prism_colorizer.rb +0 -1
- data/lib/rigor/cli/sig_gen_command.rb +210 -29
- data/lib/rigor/cli/skill_command.rb +1 -1
- data/lib/rigor/cli/skill_describe.rb +0 -1
- data/lib/rigor/cli/type_of_command.rb +19 -5
- data/lib/rigor/cli/type_of_renderer.rb +20 -9
- data/lib/rigor/cli/type_of_template_probe.rb +189 -0
- data/lib/rigor/cli.rb +21 -3
- data/lib/rigor/configuration/severity_profile.rb +19 -3
- data/lib/rigor/configuration.rb +66 -3
- data/lib/rigor/effects/ancestry_recorder.rb +191 -0
- data/lib/rigor/effects/attribution.rb +11 -2
- data/lib/rigor/effects/callee_rule.rb +368 -0
- data/lib/rigor/effects/catalog.rb +7 -4
- data/lib/rigor/effects/collector.rb +11 -5
- data/lib/rigor/effects/config_envelopes.rb +9 -2
- data/lib/rigor/effects/definition_context.rb +179 -0
- data/lib/rigor/effects/effect_table.rb +11 -3
- data/lib/rigor/effects/envelope_check.rb +1 -1
- data/lib/rigor/effects/envelope_index.rb +15 -0
- data/lib/rigor/effects/file_collection.rb +59 -5
- data/lib/rigor/effects/framework_units.rb +1 -1
- data/lib/rigor/effects/identity.rb +16 -0
- data/lib/rigor/effects/local_ownership.rb +38 -12
- data/lib/rigor/effects/method_key.rb +21 -0
- data/lib/rigor/effects/mutation_classifier.rb +23 -12
- data/lib/rigor/effects/plugin_facts.rb +43 -31
- data/lib/rigor/effects/propagator.rb +295 -16
- data/lib/rigor/effects/registry.rb +1 -1
- data/lib/rigor/effects/scanner.rb +121 -72
- data/lib/rigor/effects/signature_sources.rb +1 -1
- data/lib/rigor/effects/snapshot.rb +2 -1
- data/lib/rigor/effects/summary.rb +27 -4
- data/lib/rigor/effects/unit_scan.rb +385 -30
- data/lib/rigor/effects/visibility.rb +101 -0
- data/lib/rigor/environment/lockfile_resolver.rb +17 -0
- data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
- data/lib/rigor/environment/member_consistency.rb +298 -0
- data/lib/rigor/environment/rbs_loader.rb +399 -133
- data/lib/rigor/environment.rb +103 -38
- data/lib/rigor/hashing/xxh3.rb +264 -0
- data/lib/rigor/inference/acceptance.rb +139 -9
- data/lib/rigor/inference/block_auto_splat.rb +216 -0
- data/lib/rigor/inference/block_call_timing.rb +338 -0
- data/lib/rigor/inference/block_parameter_binder.rb +73 -27
- data/lib/rigor/inference/block_repetition.rb +71 -0
- data/lib/rigor/inference/body_fixpoint.rb +2 -1
- data/lib/rigor/inference/budget_trace.rb +2 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
- data/lib/rigor/inference/captured_locals.rb +387 -15
- data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
- data/lib/rigor/inference/content_join.rb +200 -27
- data/lib/rigor/inference/def_return_typer.rb +11 -7
- data/lib/rigor/inference/define_method_block_self.rb +64 -0
- data/lib/rigor/inference/element_read_widening.rb +22 -10
- data/lib/rigor/inference/error_info.rb +196 -0
- data/lib/rigor/inference/expression_typer.rb +1532 -496
- data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
- data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
- data/lib/rigor/inference/global_write_census.rb +239 -0
- data/lib/rigor/inference/guard_rebinding.rb +447 -0
- data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
- data/lib/rigor/inference/index_write_widening.rb +16 -3
- data/lib/rigor/inference/indexed_narrowing.rb +61 -9
- data/lib/rigor/inference/jump_targets.rb +82 -0
- data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
- data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
- data/lib/rigor/inference/last_line.rb +340 -0
- data/lib/rigor/inference/last_status.rb +144 -0
- data/lib/rigor/inference/macro_block_self_type.rb +167 -17
- data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
- data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
- data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
- data/lib/rigor/inference/match_rebinding.rb +392 -0
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
- data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
- data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
- data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +881 -38
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher.rb +214 -13
- data/lib/rigor/inference/method_parameter_binder.rb +8 -3
- data/lib/rigor/inference/multi_target_binder.rb +340 -52
- data/lib/rigor/inference/mutation_rejoin.rb +4 -1
- data/lib/rigor/inference/mutation_widening.rb +70 -48
- data/lib/rigor/inference/narrowing.rb +540 -120
- data/lib/rigor/inference/operand_effects.rb +167 -0
- data/lib/rigor/inference/operand_walk.rb +88 -0
- data/lib/rigor/inference/optimistic_origin.rb +152 -9
- data/lib/rigor/inference/parameter_inference_collector.rb +3 -2
- data/lib/rigor/inference/project_method_ownership.rb +136 -0
- data/lib/rigor/inference/project_patched_methods.rb +7 -2
- data/lib/rigor/inference/project_patched_scanner.rb +7 -3
- data/lib/rigor/inference/receiver_alias.rb +90 -1
- data/lib/rigor/inference/receiver_blind_block.rb +219 -0
- data/lib/rigor/inference/refinement_mutation.rb +15 -11
- data/lib/rigor/inference/repeated_or_writes.rb +463 -0
- data/lib/rigor/inference/return_barrier.rb +54 -0
- data/lib/rigor/inference/rewrite_mutation.rb +120 -0
- data/lib/rigor/inference/scope_indexer.rb +4615 -551
- data/lib/rigor/inference/statement_evaluator.rb +3176 -490
- data/lib/rigor/inference/stored_block_call.rb +54 -0
- data/lib/rigor/inference/string_mutation.rb +44 -7
- data/lib/rigor/inference/unknown_store_widening.rb +200 -0
- data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
- data/lib/rigor/language_server/debouncer.rb +0 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
- data/lib/rigor/language_server/hover_renderer.rb +3 -3
- data/lib/rigor/language_server/project_context.rb +5 -3
- data/lib/rigor/mcp/server.rb +2 -1
- data/lib/rigor/plugin/base.rb +168 -5
- data/lib/rigor/plugin/box_probe.rb +91 -0
- data/lib/rigor/plugin/bundled_catalog.rb +1 -1
- data/lib/rigor/plugin/effect_attribution.rb +58 -4
- data/lib/rigor/plugin/loader.rb +2 -1
- data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
- data/lib/rigor/plugin/manifest.rb +71 -10
- data/lib/rigor/plugin/registry.rb +35 -1
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
- data/lib/rigor/plugin/template_unit.rb +196 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/protection/discovery_seed.rb +3 -1
- data/lib/rigor/protection/kill_signature.rb +0 -1
- data/lib/rigor/protection/mutation_cache.rb +1 -2
- data/lib/rigor/rbs_extended.rb +27 -0
- data/lib/rigor/reflection/constant_ancestors.rb +97 -0
- data/lib/rigor/reflection/constant_path.rb +19 -6
- data/lib/rigor/reflection.rb +72 -105
- data/lib/rigor/scope/discovery_index.rb +135 -2
- data/lib/rigor/scope.rb +859 -49
- data/lib/rigor/sig_gen/alias_index.rb +289 -0
- data/lib/rigor/sig_gen/classification.rb +22 -5
- data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
- data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
- data/lib/rigor/sig_gen/generator.rb +558 -31
- data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
- data/lib/rigor/sig_gen/method_candidate.rb +47 -3
- data/lib/rigor/sig_gen/observation_collector.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +124 -9
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
- data/lib/rigor/sig_gen/write_result.rb +19 -3
- data/lib/rigor/sig_gen/writer.rb +166 -35
- data/lib/rigor/sig_gen.rb +3 -0
- data/lib/rigor/signature_path_audit.rb +1 -1
- data/lib/rigor/source/node_walker.rb +0 -3
- data/lib/rigor/source/parameter_envelope.rb +72 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/combinator.rb +88 -12
- data/lib/rigor/type/difference.rb +1 -0
- data/lib/rigor/type/hash_shape.rb +1 -1
- data/lib/rigor/type/refined.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
- data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
- data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
- data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
- data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
- data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
- data/plugins/rigor-grape/sig/grape.rbs +263 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
- data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
- data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
- data/sig/rigor/analysis/baseline.rbs +69 -7
- data/sig/rigor/analysis/fact_store.rbs +1 -1
- data/sig/rigor/analysis/project_scan.rbs +74 -0
- data/sig/rigor/effects/config_envelopes.rbs +100 -0
- data/sig/rigor/effects/effect_table.rbs +60 -0
- data/sig/rigor/effects/envelope.rbs +97 -0
- data/sig/rigor/effects/envelope_index.rbs +33 -0
- data/sig/rigor/effects/file_collection.rbs +81 -0
- data/sig/rigor/effects/label.rbs +26 -0
- data/sig/rigor/effects/label_set.rbs +46 -0
- data/sig/rigor/effects/method_key.rbs +24 -0
- data/sig/rigor/effects/origin.rbs +50 -0
- data/sig/rigor/effects/plugin_facts.rbs +141 -0
- data/sig/rigor/effects/registry.rbs +68 -0
- data/sig/rigor/effects/summary.rbs +50 -0
- data/sig/rigor/effects/taint_cause.rbs +12 -0
- data/sig/rigor/environment.rbs +12 -10
- data/sig/rigor/inference/optimistic_origin.rbs +10 -0
- data/sig/rigor/inference.rbs +6 -4
- data/sig/rigor/plugin/additional_initializer.rbs +36 -0
- data/sig/rigor/plugin/base.rbs +30 -7
- data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
- data/sig/rigor/plugin/effect_attribution.rbs +66 -0
- data/sig/rigor/plugin/effect_edge.rbs +33 -0
- data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
- data/sig/rigor/plugin/io_boundary.rbs +1 -1
- data/sig/rigor/plugin/loader.rbs +3 -3
- data/sig/rigor/plugin/manifest.rbs +50 -11
- data/sig/rigor/plugin/protocol_contract.rbs +68 -0
- data/sig/rigor/plugin/registry.rbs +62 -1
- data/sig/rigor/plugin.rbs +1 -1
- data/sig/rigor/rbs_extended.rbs +1 -1
- data/sig/rigor/reflection.rbs +7 -6
- data/sig/rigor/scope.rbs +135 -20
- data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
- data/sig/rigor/source.rbs +4 -4
- data/sig/rigor/testing.rbs +10 -4
- data/sig/rigor/type.rbs +6 -0
- data/sig/rigor.rbs +38 -20
- data/skills/rigor-ask/SKILL.md +8 -5
- data/skills/rigor-baseline-reduce/SKILL.md +4 -6
- data/skills/rigor-ci-setup/SKILL.md +17 -21
- data/skills/rigor-doctor/SKILL.md +24 -22
- data/skills/rigor-doctor/references/01-checks.md +97 -33
- data/skills/rigor-editor-setup/SKILL.md +6 -4
- data/skills/rigor-mcp-setup/SKILL.md +5 -4
- data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
- data/skills/rigor-next-steps/SKILL.md +4 -2
- data/skills/rigor-plugin-author/SKILL.md +19 -23
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
- data/skills/rigor-plugin-review/SKILL.md +6 -4
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
- data/skills/rigor-plugin-tune/SKILL.md +4 -2
- data/skills/rigor-project-init/SKILL.md +9 -7
- data/skills/rigor-project-init/references/01-detect.md +13 -9
- data/skills/rigor-project-init/references/02-configure.md +33 -8
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
- data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
- data/skills/rigor-protection-uplift/SKILL.md +4 -6
- data/skills/rigor-rbs-setup/SKILL.md +4 -2
- data/skills/rigor-type-oracle/SKILL.md +4 -6
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
- data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
- data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
- data/skills/rigor-upgrade/SKILL.md +13 -8
- metadata +108 -1
|
@@ -9,26 +9,40 @@ require_relative "../source/constant_path"
|
|
|
9
9
|
require_relative "../source/node_children"
|
|
10
10
|
require_relative "../source/node_walker"
|
|
11
11
|
require_relative "../analysis/self_call_resolution_recorder"
|
|
12
|
+
require_relative "block_call_timing"
|
|
13
|
+
require_relative "block_repetition"
|
|
12
14
|
require_relative "block_parameter_binder"
|
|
13
15
|
require_relative "method_parameter_binder"
|
|
14
16
|
require_relative "body_fixpoint"
|
|
15
17
|
require_relative "budget_trace"
|
|
16
18
|
require_relative "captured_locals"
|
|
19
|
+
require_relative "closure_escape_analyzer"
|
|
20
|
+
require_relative "receiver_blind_block"
|
|
17
21
|
require_relative "def_node_resolver"
|
|
18
22
|
require_relative "dynamic_origin"
|
|
23
|
+
require_relative "error_info"
|
|
24
|
+
require_relative "external_ancestor_resolution"
|
|
19
25
|
require_relative "origin_lookup"
|
|
20
26
|
require_relative "../effects/collector"
|
|
21
27
|
require_relative "fallback"
|
|
22
28
|
require_relative "flow_tracer"
|
|
29
|
+
require_relative "index_write_widening"
|
|
23
30
|
require_relative "indexed_narrowing"
|
|
31
|
+
require_relative "jump_targets"
|
|
32
|
+
require_relative "define_method_block_self"
|
|
24
33
|
require_relative "macro_block_self_type"
|
|
34
|
+
require_relative "match_rebinding"
|
|
25
35
|
require_relative "method_dispatcher"
|
|
26
36
|
require_relative "mutation_widening"
|
|
27
37
|
require_relative "narrowing"
|
|
38
|
+
require_relative "optimistic_origin"
|
|
28
39
|
require_relative "receiver_alias"
|
|
40
|
+
require_relative "repeated_or_writes"
|
|
29
41
|
require_relative "singleton_object_constant"
|
|
30
|
-
require_relative "
|
|
42
|
+
require_relative "stored_block_call"
|
|
31
43
|
require_relative "struct_fold_safety"
|
|
44
|
+
require_relative "unknown_store_widening"
|
|
45
|
+
require_relative "unthreaded_rebinds"
|
|
32
46
|
require_relative "version_guard"
|
|
33
47
|
|
|
34
48
|
module Rigor
|
|
@@ -99,12 +113,12 @@ module Rigor
|
|
|
99
113
|
Prism::ConstantPathNode => :type_of_constant_path,
|
|
100
114
|
Prism::ConstantWriteNode => :type_of_assignment_write,
|
|
101
115
|
Prism::ConstantPathWriteNode => :type_of_assignment_write,
|
|
102
|
-
Prism::ConstantOperatorWriteNode => :
|
|
103
|
-
Prism::ConstantOrWriteNode => :
|
|
104
|
-
Prism::ConstantAndWriteNode => :
|
|
105
|
-
Prism::ConstantPathOperatorWriteNode => :
|
|
106
|
-
Prism::ConstantPathOrWriteNode => :
|
|
107
|
-
Prism::ConstantPathAndWriteNode => :
|
|
116
|
+
Prism::ConstantOperatorWriteNode => :type_of_compound_constant_write,
|
|
117
|
+
Prism::ConstantOrWriteNode => :type_of_compound_constant_write,
|
|
118
|
+
Prism::ConstantAndWriteNode => :type_of_compound_constant_write,
|
|
119
|
+
Prism::ConstantPathOperatorWriteNode => :type_of_compound_constant_write,
|
|
120
|
+
Prism::ConstantPathOrWriteNode => :type_of_compound_constant_write,
|
|
121
|
+
Prism::ConstantPathAndWriteNode => :type_of_compound_constant_write,
|
|
108
122
|
# Self and instance/class/global variables
|
|
109
123
|
Prism::SelfNode => :type_of_self_node,
|
|
110
124
|
Prism::InstanceVariableReadNode => :type_of_instance_variable_read,
|
|
@@ -126,9 +140,9 @@ module Rigor
|
|
|
126
140
|
Prism::LocalVariableOperatorWriteNode => :type_of_compound_variable_write,
|
|
127
141
|
Prism::LocalVariableOrWriteNode => :type_of_compound_variable_write,
|
|
128
142
|
Prism::LocalVariableAndWriteNode => :type_of_compound_variable_write,
|
|
129
|
-
Prism::IndexOperatorWriteNode => :
|
|
130
|
-
Prism::IndexOrWriteNode => :
|
|
131
|
-
Prism::IndexAndWriteNode => :
|
|
143
|
+
Prism::IndexOperatorWriteNode => :type_of_index_compound_write,
|
|
144
|
+
Prism::IndexOrWriteNode => :type_of_index_compound_write,
|
|
145
|
+
Prism::IndexAndWriteNode => :type_of_index_compound_write,
|
|
132
146
|
Prism::MultiWriteNode => :type_of_assignment_write,
|
|
133
147
|
# LHS-only target nodes (destructuring assignment, pattern matching, `for x in xs`, block parameter
|
|
134
148
|
# `|a, (b, c)|`). They have no value to extract — the type-of pass acknowledges the node class so the
|
|
@@ -183,8 +197,8 @@ module Rigor
|
|
|
183
197
|
Prism::SplatNode => :type_of_non_value,
|
|
184
198
|
# Control flow (Slice 3 phase 1): branch types are unioned, jumps
|
|
185
199
|
# type as Bot, loops type as Constant[nil].
|
|
186
|
-
Prism::IfNode => :
|
|
187
|
-
Prism::UnlessNode => :
|
|
200
|
+
Prism::IfNode => :type_of_conditional,
|
|
201
|
+
Prism::UnlessNode => :type_of_conditional,
|
|
188
202
|
Prism::ElseNode => :type_of_else,
|
|
189
203
|
Prism::AndNode => :type_of_and_or,
|
|
190
204
|
Prism::OrNode => :type_of_and_or,
|
|
@@ -229,10 +243,15 @@ module Rigor
|
|
|
229
243
|
NO_STATIC_HASH_KEY = Object.new.freeze
|
|
230
244
|
private_constant :NO_STATIC_HASH_KEY
|
|
231
245
|
|
|
232
|
-
|
|
246
|
+
# `operand_types` is the identity-comparing `Prism::Node => Rigor::Type` table of the later operands a
|
|
247
|
+
# threading `StatementEvaluator` root typed from the scope the earlier operands left (issue #1256), or nil.
|
|
248
|
+
# It is consulted only by this typer's own descent: a node it holds is the same node typed from its own
|
|
249
|
+
# entry scope, so answering it here is what typing it there would answer.
|
|
250
|
+
def initialize(scope:, tracer: nil, operand_types: nil, typing_node: nil)
|
|
233
251
|
@scope = scope
|
|
234
252
|
@tracer = tracer
|
|
235
|
-
@
|
|
253
|
+
@operand_types = operand_types
|
|
254
|
+
@typing_node = typing_node
|
|
236
255
|
end
|
|
237
256
|
|
|
238
257
|
def type_of(node)
|
|
@@ -256,6 +275,9 @@ module Rigor
|
|
|
256
275
|
declared = scope.declared_types[node]
|
|
257
276
|
return declared if declared
|
|
258
277
|
|
|
278
|
+
threaded = @operand_types&.[](node)
|
|
279
|
+
return threaded if threaded
|
|
280
|
+
|
|
259
281
|
return type_of_virtual(node) if node.is_a?(AST::Node)
|
|
260
282
|
|
|
261
283
|
handler = PRISM_DISPATCH[node.class]
|
|
@@ -275,6 +297,28 @@ module Rigor
|
|
|
275
297
|
infer_user_method_return(def_node, receiver, arg_types)
|
|
276
298
|
end
|
|
277
299
|
|
|
300
|
+
# The `receiver[args]` read a compound index write performs before it stores (`c[k] += v` reads `c[k]`),
|
|
301
|
+
# or nil when no tier answers. No `Prism::CallNode` for that read exists in the tree, so the write node
|
|
302
|
+
# itself stands in as the call context: it carries the same `receiver` / `arguments` / `block` a
|
|
303
|
+
# plain `c[k]` call does, which is what the context-reading tiers consult. With it, the read reaches
|
|
304
|
+
# the tiers a plain `c[k]` takes, in the same order — the own-`def` override check, the dispatcher's
|
|
305
|
+
# node- and scope-gated tiers, then the project `def` inference a dispatch miss falls to — so a project
|
|
306
|
+
# `[]` with no signature answers from its body instead of the whole write degrading to `Dynamic[top]`.
|
|
307
|
+
#
|
|
308
|
+
# A plugin `dynamic_return` rule written for `[]` still does not answer this read. Its `methods:` gate
|
|
309
|
+
# matches `call_node.name`, which an index write does not have, and the shipped rules decline any node
|
|
310
|
+
# that is not a `Prism::CallNode`, by an explicit check or through the rescue around a rule that reads
|
|
311
|
+
# `name` anyway. Minting a `Prism::CallNode` for the read would hand plugins a node that is not in the
|
|
312
|
+
# tree, through a constructor whose field list changed inside the `prism` range the gemspec accepts.
|
|
313
|
+
def implicit_index_read_type(node, receiver, arg_types)
|
|
314
|
+
return splat_index_read_type(node, receiver, arg_types) if node.arguments&.arguments&.any?(Prism::SplatNode)
|
|
315
|
+
|
|
316
|
+
try_overriding_def_dispatch(node, receiver, arg_types, method_name: :[]) ||
|
|
317
|
+
index_read_dispatch(node, receiver, arg_types) ||
|
|
318
|
+
try_user_method_inference(receiver, node, arg_types, method_name: :[]) ||
|
|
319
|
+
try_project_singleton_inference(receiver, node, arg_types, method_name: :[])
|
|
320
|
+
end
|
|
321
|
+
|
|
278
322
|
# ADR-89 WD2 — the current run's return memo bucket as `{ def_node => [MemoEntry, …] }` (only entries
|
|
279
323
|
# that carry a call descriptor, i.e. every stored entry). Read by the incremental session right after a
|
|
280
324
|
# recording run to harvest each analyzed callee's observed call keys → return descriptors. Returns an
|
|
@@ -313,6 +357,44 @@ module Rigor
|
|
|
313
357
|
end
|
|
314
358
|
end
|
|
315
359
|
|
|
360
|
+
# Issue #1125 — the argument types of the call currently being re-typed into the body being walked,
|
|
361
|
+
# or nil outside such a frame. A `f(...)` call inside `def m(...)` re-supplies exactly the arguments
|
|
362
|
+
# `m` itself was called with, so {#call_arg_types} expands the forwarding node to THIS list instead of
|
|
363
|
+
# the `Dynamic[top]` a bare `Prism::ForwardingArgumentsNode` types as.
|
|
364
|
+
#
|
|
365
|
+
# A thread-local for the same reason the yield value is (see {YIELD_VALUE_KEY}): `...` names the
|
|
366
|
+
# frame's caller, not a binding, and the body is walked through scopes the inference rebuilds freely.
|
|
367
|
+
# It is installed unconditionally — with nil — at every user-method inference frame, so a body reached
|
|
368
|
+
# without `...` cannot read its caller's argument list. Because each frame installs its OWN list, a
|
|
369
|
+
# chain `a(...) -> b(...) -> c(...)` threads through: `b`'s frame holds the types `a` expanded and
|
|
370
|
+
# `c`'s holds `b`'s. The list is the callee's call-site `arg_types`, which is what keeps the ADR-84
|
|
371
|
+
# return memo's `(def_node, receiver, arg_types)` key complete for a forwarding def.
|
|
372
|
+
FORWARDED_ARGS_KEY = :__rigor_forwarded_call_arg_types__
|
|
373
|
+
private_constant :FORWARDED_ARGS_KEY
|
|
374
|
+
|
|
375
|
+
def self.current_forwarded_arg_types
|
|
376
|
+
Thread.current[FORWARDED_ARGS_KEY]
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Runs `block` with `types` installed as the frame's forwarded argument list, restoring the previous
|
|
380
|
+
# frame on exit.
|
|
381
|
+
def self.with_forwarded_arg_types(types, &block)
|
|
382
|
+
previous = Thread.current[FORWARDED_ARGS_KEY]
|
|
383
|
+
Thread.current[FORWARDED_ARGS_KEY] = types
|
|
384
|
+
begin
|
|
385
|
+
block.call
|
|
386
|
+
ensure
|
|
387
|
+
Thread.current[FORWARDED_ARGS_KEY] = previous
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# The two call-site channels a user-method body may read from its caller — the block a `yield`
|
|
392
|
+
# reaches ({#with_yield_value_type}) and the argument list `...` re-supplies
|
|
393
|
+
# ({#with_forwarded_arg_types}) — installed together, both even when nil, for exactly one frame.
|
|
394
|
+
def self.with_call_site_frame(yield_type, forwarded_arg_types, &)
|
|
395
|
+
with_yield_value_type(yield_type) { with_forwarded_arg_types(forwarded_arg_types, &) }
|
|
396
|
+
end
|
|
397
|
+
|
|
316
398
|
private
|
|
317
399
|
|
|
318
400
|
attr_reader :scope, :tracer
|
|
@@ -361,33 +443,86 @@ module Rigor
|
|
|
361
443
|
# evaluator takes — and an operator the receiver does not answer widens to `Dynamic[top]` rather than
|
|
362
444
|
# inventing the rvalue.
|
|
363
445
|
#
|
|
364
|
-
# Constant
|
|
365
|
-
#
|
|
366
|
-
# `StatementEvaluator#eval_index_write`.
|
|
446
|
+
# Constant targets have {#type_of_compound_constant_write}, over the same algebra. Index targets have their
|
|
447
|
+
# own handler, {#type_of_index_compound_write}.
|
|
367
448
|
def type_of_compound_variable_write(node)
|
|
368
|
-
|
|
449
|
+
compound_write_value(node, compound_write_current_binding(node), type_of(node.value))
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# `H ||= 0` / `Conf::LIMIT += 1` as an EXPRESSION: the value it stores, by
|
|
453
|
+
# {#type_of_compound_variable_write}'s algebra. Typed as the rvalue alone, `H = { x: 1 }; v = (H ||= 0);
|
|
454
|
+
# v[:x]` reported `Integer#[]` on a program whose `v` is `H`, and `F.transform_values { |e| F ||= 0 }`
|
|
455
|
+
# folded every value to `0`.
|
|
456
|
+
#
|
|
457
|
+
# A constant has no scope binding, so its current binding is what a plain read of the same spelling
|
|
458
|
+
# resolves to at the write site — the lexical ladder, the in-source table of plain writes, then RBS —
|
|
459
|
+
# less the caller-derived rungs a top-level body keeps for reads Ruby raises on. The memoization idiom
|
|
460
|
+
# (`def registry = REGISTRY ||= {}`, legal where a plain `REGISTRY = {}` is a dynamic constant
|
|
461
|
+
# assignment) writes a constant nothing else binds, so an unresolved target keeps the rvalue reading
|
|
462
|
+
# exactly as an unbound variable does — unless a write other than a memo binds it
|
|
463
|
+
# ({#written_constant_binding}). A path whose base renders no static name (`klass::X`, `self::X`) names
|
|
464
|
+
# no binding the resolver can look up, and reads as unbound too, on the same condition.
|
|
465
|
+
def type_of_compound_constant_write(node)
|
|
369
466
|
rhs = type_of(node.value)
|
|
467
|
+
compound_write_value(node, compound_write_constant_binding(node), rhs)
|
|
468
|
+
end
|
|
370
469
|
|
|
470
|
+
def compound_write_value(node, current, rhs)
|
|
371
471
|
case node
|
|
372
472
|
when Prism::LocalVariableOrWriteNode, Prism::InstanceVariableOrWriteNode,
|
|
373
|
-
Prism::ClassVariableOrWriteNode, Prism::GlobalVariableOrWriteNode
|
|
473
|
+
Prism::ClassVariableOrWriteNode, Prism::GlobalVariableOrWriteNode,
|
|
474
|
+
Prism::ConstantOrWriteNode, Prism::ConstantPathOrWriteNode
|
|
374
475
|
# An UNBOUND target is the memoization idiom (`def self.default = @default ||= new`): nothing
|
|
375
476
|
# has written the variable on any path the analyzer saw, so the stored value is the rvalue.
|
|
376
477
|
# Reading it as `Dynamic[top] | rhs` would skip every memoized singleton in `sig-gen`
|
|
377
478
|
# (ADR-5 optimism; three `.default` readers went `sig.skipped.untyped-return` without this).
|
|
378
|
-
|
|
479
|
+
#
|
|
480
|
+
# The index rule's exception applies: an rvalue with no truthy part makes the write a guard, not a
|
|
481
|
+
# memo. `@settings ||= raise "boot first"` returns only when something the analyzer did not see set
|
|
482
|
+
# `@settings`, and `@verbose ||= false` answers `true` once one did, so the value is that unseen
|
|
483
|
+
# binding — `Dynamic[top]`, the statement evaluator's reading of an unbound target — never the
|
|
484
|
+
# rvalue's `bot` or `false` alone. A local is no exception: the scope a loop body is typed from does
|
|
485
|
+
# not carry what the previous iteration stored.
|
|
486
|
+
if current.nil?
|
|
487
|
+
return rhs unless Narrowing.narrow_truthy(rhs).is_a?(Type::Bot)
|
|
488
|
+
|
|
489
|
+
current = dynamic_top
|
|
490
|
+
end
|
|
379
491
|
|
|
380
492
|
Type::Combinator.union(Narrowing.narrow_truthy(current), rhs)
|
|
381
493
|
when Prism::LocalVariableAndWriteNode, Prism::InstanceVariableAndWriteNode,
|
|
382
|
-
Prism::ClassVariableAndWriteNode, Prism::GlobalVariableAndWriteNode
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
494
|
+
Prism::ClassVariableAndWriteNode, Prism::GlobalVariableAndWriteNode,
|
|
495
|
+
Prism::ConstantAndWriteNode, Prism::ConstantPathAndWriteNode
|
|
496
|
+
# `&&=` is no memo: an unset target returns its own `nil` without evaluating the rvalue (a constant or
|
|
497
|
+
# class variable raises instead). Beside the rvalue, an UNBOUND target's value is its own falsey value —
|
|
498
|
+
# `nil` when unset, or a falsey value a write the analyzer did not see stored — which
|
|
499
|
+
# `narrow_falsey(Dynamic[top])` carries as `Dynamic[top]`, the statement evaluator's reading. The index
|
|
500
|
+
# rule says the same of `h[k] &&= v`. Read as the rvalue, `if (@x &&= 1)` folded always-truthy on a
|
|
501
|
+
# program that takes the else arm.
|
|
502
|
+
Type::Combinator.union(Narrowing.narrow_falsey(current || dynamic_top), rhs)
|
|
386
503
|
else
|
|
387
504
|
compound_operator_result(current || dynamic_top, rhs, node.binary_operator)
|
|
388
505
|
end
|
|
389
506
|
end
|
|
390
507
|
|
|
508
|
+
# `h[k] += v` / `h[k] ||= v` / `h[k] &&= v` as an EXPRESSION. Like a variable compound write, its value is
|
|
509
|
+
# what it stores through `[]=` — the dispatched `h[k] + v`, `truthy(h[k]) | v`, `falsey(h[k]) | v` — which
|
|
510
|
+
# reads the slot's current type, recorded indexed narrowing included. Typed as the rvalue alone it answered
|
|
511
|
+
# `1` for `counts[w] += 1`, so `r = words.map { |w| counts[w] += 1 }` pinned every position to `1` and
|
|
512
|
+
# `r.last == 1` drew a false `flow.always-truthy-condition`.
|
|
513
|
+
#
|
|
514
|
+
# The statement evaluator already owned that algebra for the straight-line write and the `[]=` widening
|
|
515
|
+
# join, so this reads its answer rather than keeping a second copy. It asks for the value alone, not a
|
|
516
|
+
# whole `evaluate`: the widening and the narrowing record are scope effects a value position discards,
|
|
517
|
+
# and the memoizing `@cache[k] ||= build(k)` tail is common enough not to pay for them.
|
|
518
|
+
#
|
|
519
|
+
# One exception carries over, narrowed, from {#type_of_compound_variable_write}: a memoizing `||=` whose
|
|
520
|
+
# slot the analyzer has no evidence about reads as the rvalue. The evaluator's value method owns it,
|
|
521
|
+
# because it is decided on the `[]` read the evaluator performs.
|
|
522
|
+
def type_of_index_compound_write(node)
|
|
523
|
+
StatementEvaluator.new(scope: scope, tracer: tracer).index_compound_write_value(node)
|
|
524
|
+
end
|
|
525
|
+
|
|
391
526
|
def compound_write_current_binding(node)
|
|
392
527
|
case node
|
|
393
528
|
when Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode,
|
|
@@ -400,6 +535,64 @@ module Rigor
|
|
|
400
535
|
end
|
|
401
536
|
end
|
|
402
537
|
|
|
538
|
+
def compound_write_constant_binding(node)
|
|
539
|
+
case node
|
|
540
|
+
when Prism::ConstantOperatorWriteNode, Prism::ConstantOrWriteNode, Prism::ConstantAndWriteNode
|
|
541
|
+
full_name = node.name.to_s
|
|
542
|
+
rooted = false
|
|
543
|
+
else
|
|
544
|
+
full_name = Source::ConstantPath.qualified_name_or_nil(node.target)
|
|
545
|
+
return unnamed_path_binding(node.target) if full_name.nil?
|
|
546
|
+
|
|
547
|
+
rooted = Source::ConstantPath.rooted?(node.target)
|
|
548
|
+
end
|
|
549
|
+
resolve_constant_name(full_name, rooted: rooted, caller_derived: false) ||
|
|
550
|
+
written_constant_binding(full_name, rooted)
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
# A constant the plain read cannot resolve may still be bound: another file writes a value that never
|
|
554
|
+
# published (`H = { x: 1 }`), the compound write withdrew one by being the second writer the census
|
|
555
|
+
# counts, or this file writes it in a form its own table does not carry (`A, B = …`, `A = B = …`). The
|
|
556
|
+
# value is one the analyzer does not have, so the binding is `Dynamic[top]` rather than the memo's
|
|
557
|
+
# unbound reading. The one write that does not bind is a memo `||=` no other file shares, so the
|
|
558
|
+
# memoization idiom keeps that reading however often its file repeats it; memos of the name in two files
|
|
559
|
+
# bind, since either may load first and set what the other reads.
|
|
560
|
+
#
|
|
561
|
+
# Which name the write reads is the ladder's to decide, not the spelling's: the same resolution runs
|
|
562
|
+
# again with only the census's binding names in the in-source table, so `Other::REGISTRY` elsewhere
|
|
563
|
+
# leaves a top-level `REGISTRY ||= {}` its memo reading. Two census spellings the ladder cannot reach
|
|
564
|
+
# still bind: a write through a base nothing names (`k::X = 1`), which may have written any of them,
|
|
565
|
+
# and a path the census keeps as written ([#690](https://github.com/rigortype/rigor/issues/690)), so
|
|
566
|
+
# `Foo::BAR = …` inside `module M` binds `M::Foo::BAR ||= 0`.
|
|
567
|
+
def written_constant_binding(full_name, rooted)
|
|
568
|
+
names = scope.bound_constant_names(full_name)
|
|
569
|
+
return nil if names.empty?
|
|
570
|
+
return dynamic_top if names.any? { |name| unladdered_census_name?(name, full_name) }
|
|
571
|
+
|
|
572
|
+
written = names.to_h { |name| [name, dynamic_top] }
|
|
573
|
+
probe = scope.with_discovery(scope.discovery.with(in_source_constants: written))
|
|
574
|
+
Reflection.resolve_constant_type(full_name, scope: probe, rooted: rooted, caller_derived: false)
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def unladdered_census_name?(name, full_name)
|
|
578
|
+
return true if name.start_with?(ScopeIndexer::DYNAMIC_TARGET_PREFIX)
|
|
579
|
+
|
|
580
|
+
name.include?("::") && full_name.end_with?("::#{name}")
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
# `self::X ||= v` / `klass::X ||= v` name no constant the resolver can look up, so any binding write
|
|
584
|
+
# that shares the last segment may be the one the base reaches. The answer now depends on other files'
|
|
585
|
+
# writes, and this path never reaches `Reflection.resolve_constant_type`, which records the
|
|
586
|
+
# `constant:<segment>` edge for every other form — so it records the edge itself, or an incremental
|
|
587
|
+
# run keeps serving the reading from before another file's write of the segment appeared.
|
|
588
|
+
def unnamed_path_binding(target)
|
|
589
|
+
segment = target.name&.to_s
|
|
590
|
+
return nil if segment.nil?
|
|
591
|
+
|
|
592
|
+
Analysis::DependencyRecorder.read_name(:constant, segment) if Analysis::DependencyRecorder.active?
|
|
593
|
+
dynamic_top unless scope.bound_constant_names(segment).empty?
|
|
594
|
+
end
|
|
595
|
+
|
|
403
596
|
def compound_operator_result(current, rhs, operator)
|
|
404
597
|
MethodDispatcher.dispatch(
|
|
405
598
|
receiver_type: current,
|
|
@@ -532,7 +725,17 @@ module Rigor
|
|
|
532
725
|
# `Inference::FallbackTracer` from inside `Rigor::CLI::Foo` resolves to
|
|
533
726
|
# `Rigor::Inference::FallbackTracer`.
|
|
534
727
|
def type_of_constant_read(node)
|
|
535
|
-
|
|
728
|
+
guard_narrowed_constant(node) ||
|
|
729
|
+
resolve_constant_name(node.name.to_s) || unresolved_constant_fallback(node, node.name.to_s)
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
# Issue #1429 — the type a guard narrowed this constant reference to on the edge being typed
|
|
733
|
+
# (`STDOUT.is_a?(StringIO) ? STDOUT.string : nil`), or nil. Keyed by spelling ({Narrowing.constant_key}).
|
|
734
|
+
def guard_narrowed_constant(node)
|
|
735
|
+
return nil if scope.constant_narrowings.empty?
|
|
736
|
+
|
|
737
|
+
key = Narrowing.constant_key(node)
|
|
738
|
+
key && scope.constant_narrowing(key)
|
|
536
739
|
end
|
|
537
740
|
|
|
538
741
|
# A leading `::` (`::Rails`, `::Rails::Application`) is Ruby's escape hatch out of the lexical ladder:
|
|
@@ -540,6 +743,9 @@ module Rigor
|
|
|
540
743
|
# deliberately un-rooted (the discovery tables are keyed that way), so the marker rides alongside it
|
|
541
744
|
# into the resolver (#614).
|
|
542
745
|
def type_of_constant_path(node)
|
|
746
|
+
narrowed = guard_narrowed_constant(node)
|
|
747
|
+
return narrowed if narrowed
|
|
748
|
+
|
|
543
749
|
full_name = Source::ConstantPath.qualified_name_or_nil(node)
|
|
544
750
|
return fallback_for(node, family: :prism) if full_name.nil?
|
|
545
751
|
|
|
@@ -608,16 +814,17 @@ module Rigor
|
|
|
608
814
|
# in-source value, RBS constant, across the peeled `::` prefix candidates) is reused by
|
|
609
815
|
# `Inference::Narrowing`'s `Constant[Regexp]` match-operand recognition. Returns the matched
|
|
610
816
|
# `Rigor::Type` or nil; the caller decides whether to fall back.
|
|
611
|
-
def resolve_constant_name(name, rooted: false)
|
|
612
|
-
Reflection.resolve_constant_type(name, scope: scope, rooted: rooted)
|
|
817
|
+
def resolve_constant_name(name, rooted: false, caller_derived: true)
|
|
818
|
+
Reflection.resolve_constant_type(name, scope: scope, rooted: rooted, caller_derived: caller_derived)
|
|
613
819
|
end
|
|
614
820
|
|
|
615
821
|
# Slice 5 phase 1 upgrades hash literals to `HashShape{...}` when every entry is a static `AssocNode`
|
|
616
822
|
# whose key is a value-pinned scalar literal — Symbol, plain String, Integer, Float, `true`, `false`,
|
|
617
823
|
# or `nil` (covering `{ a: 1, "b" => 2 }` and `{ 1 => 2, 1.0 => 4 }` alike) — falling back to the
|
|
618
824
|
# generic `Hash[K, V]` form otherwise. Splatted entries (`{ **other }`) and dynamic keys widen to the
|
|
619
|
-
# underlying `Hash[K, V]` form by unioning the types each entry exposes
|
|
620
|
-
#
|
|
825
|
+
# underlying `Hash[K, V]` form by unioning the types each entry exposes — a splat exposes the `[K, V]`
|
|
826
|
+
# of the hash it copies plus a `Dynamic[top]` arm (see {#hash_splat_pair}), so every entry contributes a
|
|
827
|
+
# pair. A splat never keeps a shape, even over an exact closed one.
|
|
621
828
|
def type_of_hash(node)
|
|
622
829
|
elements = node.respond_to?(:elements) ? node.elements : []
|
|
623
830
|
# v0.0.7 — `{}` resolves to the empty `HashShape{}` carrier rather than `Nominal[Hash]`, mirroring the
|
|
@@ -629,8 +836,6 @@ module Rigor
|
|
|
629
836
|
return shape if shape
|
|
630
837
|
|
|
631
838
|
keys, values = generic_hash_pairs_for(elements)
|
|
632
|
-
return Type::Combinator.nominal_of(Hash) if keys.empty? || values.empty?
|
|
633
|
-
|
|
634
839
|
Type::Combinator.nominal_of(
|
|
635
840
|
Hash,
|
|
636
841
|
type_args: [Type::Combinator.union(*keys), Type::Combinator.union(*values)]
|
|
@@ -684,14 +889,64 @@ module Rigor
|
|
|
684
889
|
keys = []
|
|
685
890
|
values = []
|
|
686
891
|
elements.each do |entry|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
892
|
+
if entry.is_a?(Prism::AssocNode)
|
|
893
|
+
keys << type_of(entry.key)
|
|
894
|
+
values << type_of(entry.value)
|
|
895
|
+
else
|
|
896
|
+
key, value = hash_splat_pair(entry)
|
|
897
|
+
keys << key
|
|
898
|
+
values << value
|
|
899
|
+
end
|
|
691
900
|
end
|
|
692
901
|
[keys, values]
|
|
693
902
|
end
|
|
694
903
|
|
|
904
|
+
# The `[K, V]` a `**splat` entry adds to the literal: what the analysis can read of the hash it copies, each
|
|
905
|
+
# side joined with `Dynamic[top]`. Ruby inserts every pair of that hash, so leaving the entry out typed
|
|
906
|
+
# `o = { a: :z }; { **o, b: :y }` as `Hash[:b, :y]` and folded `h[:a] == :z` on correct code.
|
|
907
|
+
#
|
|
908
|
+
# The `Dynamic[top]` arm is there even when the copy is read exactly. The literal builds a new hash that no
|
|
909
|
+
# declaration describes, while `MutationRejoin` regrows a `Hash[K, V]` after `[]=` / `merge!` only when it
|
|
910
|
+
# already carries a gradual arm, because it reads a precise one as a declared claim. Without the arm
|
|
911
|
+
# `h = { **o }; h[:b] = 2; h[:b] == 2` would fold always-falsey and `h[:e] = "s"; h[:e].upcase` would fire
|
|
912
|
+
# `call.undefined-method`, where the `Hash` a splat-only literal used to type as had stayed quiet. The same
|
|
913
|
+
# arm stands in for what the analysis cannot read: an anonymous `**`, an untyped value, a nominal other than
|
|
914
|
+
# `Hash`, an open shape's unlisted entries, and a hash filled through an alias the engine does not track,
|
|
915
|
+
# which still reads `{}`.
|
|
916
|
+
def hash_splat_pair(entry)
|
|
917
|
+
untyped = Type::Combinator.untyped
|
|
918
|
+
copied = entry.value && splatted_hash_pair(type_of(entry.value))
|
|
919
|
+
return [untyped, untyped] if copied.nil?
|
|
920
|
+
|
|
921
|
+
copied.map { |side| Type::Combinator.union(side, untyped) }
|
|
922
|
+
end
|
|
923
|
+
|
|
924
|
+
# The `[K, V]` the analysis can read of a splatted value, or nil when it reads nothing: a shape's keys and
|
|
925
|
+
# values, a `Hash[K, V]`'s type arguments (through a difference over one, `non-empty-hash[K, V]`), and the
|
|
926
|
+
# join over a union's members. `nil` reads as nothing because `**nil` splats nothing (Ruby 3.4+; earlier
|
|
927
|
+
# Rubies raise). A `Hash` subclass is not read: its own type arguments, if any, are not `Hash`'s `K` and `V`.
|
|
928
|
+
def splatted_hash_pair(type)
|
|
929
|
+
case type
|
|
930
|
+
when Type::HashShape then splatted_shape_pair(type)
|
|
931
|
+
when Type::Nominal then type.class_name == "Hash" && type.type_args.size == 2 ? type.type_args : nil
|
|
932
|
+
when Type::Difference then splatted_hash_pair(type.base)
|
|
933
|
+
when Type::Union then joined_hash_pair(type.members.filter_map { |member| splatted_hash_pair(member) })
|
|
934
|
+
end
|
|
935
|
+
end
|
|
936
|
+
|
|
937
|
+
def splatted_shape_pair(shape)
|
|
938
|
+
return nil if shape.pairs.empty?
|
|
939
|
+
|
|
940
|
+
[Type::Combinator.union(*shape.pairs.keys.map { |k| Type::Combinator.constant_of(k) }),
|
|
941
|
+
Type::Combinator.union(*shape.pairs.values)]
|
|
942
|
+
end
|
|
943
|
+
|
|
944
|
+
def joined_hash_pair(pairs)
|
|
945
|
+
return nil if pairs.empty?
|
|
946
|
+
|
|
947
|
+
[Type::Combinator.union(*pairs.map(&:first)), Type::Combinator.union(*pairs.map(&:last))]
|
|
948
|
+
end
|
|
949
|
+
|
|
695
950
|
# An interpolated string `"#{a}b#{c}"` is `literal-string` when every part contributes literal-bearing
|
|
696
951
|
# material: plain text segments are literal by construction, embedded expressions count when their type
|
|
697
952
|
# is itself literal-string-compatible (a `Constant<String>`, the `literal-string` carrier, an
|
|
@@ -746,132 +1001,29 @@ module Rigor
|
|
|
746
1001
|
Type::Combinator.constant_of(nil)
|
|
747
1002
|
end
|
|
748
1003
|
|
|
749
|
-
# `if
|
|
750
|
-
#
|
|
751
|
-
#
|
|
752
|
-
#
|
|
753
|
-
#
|
|
754
|
-
#
|
|
755
|
-
#
|
|
756
|
-
#
|
|
757
|
-
#
|
|
758
|
-
def
|
|
759
|
-
|
|
760
|
-
else_type = if_else_type(node.subsequent)
|
|
761
|
-
elide_or_union(node.predicate, then_type, else_type)
|
|
762
|
-
end
|
|
763
|
-
|
|
764
|
-
# `unless c; t; else; e; end`. Prism uses `else_clause` here (no `elsif` chain). Branch-elision logic
|
|
765
|
-
# mirrors `type_of_if`, inverted: a truthy predicate selects the else branch.
|
|
766
|
-
def type_of_unless(node)
|
|
767
|
-
then_type = statements_or_nil(node.statements)
|
|
768
|
-
else_type = if_else_type(node.else_clause)
|
|
769
|
-
elide_or_union(node.predicate, else_type, then_type)
|
|
770
|
-
end
|
|
771
|
-
|
|
772
|
-
# Issue #286 — the effective optimistic-nil-free cause of an expression. {OptimisticOrigin.resolve} owns
|
|
773
|
-
# the judgment, shared verbatim with `StatementEvaluator#optimistic_origin_for` and the
|
|
774
|
-
# `flow.always-truthy-condition` collector.
|
|
775
|
-
def optimistic_origin_for(node)
|
|
776
|
-
OptimisticOrigin.resolve(node, scope)
|
|
777
|
-
end
|
|
778
|
-
|
|
779
|
-
def if_else_type(subsequent)
|
|
780
|
-
return Type::Combinator.constant_of(nil) if subsequent.nil?
|
|
781
|
-
|
|
782
|
-
type_of(subsequent)
|
|
783
|
-
end
|
|
784
|
-
|
|
785
|
-
# Routes the predicate's typed value through branch elision. `live_when_truthy` and `live_when_falsey`
|
|
786
|
-
# are the branch types selected by the predicate's polarity; the names match `IfNode` semantics
|
|
787
|
-
# directly and invert at the `type_of_unless` call site.
|
|
788
|
-
def elide_or_union(predicate, live_when_truthy, live_when_falsey)
|
|
789
|
-
case constant_predicate_polarity(predicate)
|
|
790
|
-
when :truthy then live_when_truthy
|
|
791
|
-
when :falsey then live_when_falsey
|
|
792
|
-
else Type::Combinator.union(live_when_truthy, live_when_falsey)
|
|
793
|
-
end
|
|
794
|
-
end
|
|
795
|
-
|
|
796
|
-
# Returns `:truthy`, `:falsey`, or `nil` for an arbitrary predicate expression under three-valued logic.
|
|
797
|
-
# {Narrowing.predicate_certainty} owns the judgment (the same one `StatementEvaluator#live_branch_for_if`
|
|
798
|
-
# reads on the scope side): `Nominal[Integer]` (always truthy in Ruby), `Constant[nil]`, and
|
|
799
|
-
# `Constant[false]` fold one branch; `Union[true, false]`, `Dynamic[T]`, and `Top` keep both branches live.
|
|
800
|
-
def constant_predicate_polarity(predicate)
|
|
801
|
-
return nil if predicate.nil?
|
|
802
|
-
|
|
803
|
-
# ADR-47 WD5 — a decidable version guard (#627) answers first, exactly as it does on the scope side
|
|
804
|
-
# in `StatementEvaluator#branch_certainty`. Both readers ask the same pure function of the AST, so
|
|
805
|
-
# the expression form (`RUBY_VERSION >= "3.1" ? a : b`) and the statement form cannot disagree about
|
|
806
|
-
# which arm survives. The verdict rests on literals, so the ADR-101 optimistic-carrier decline below
|
|
807
|
-
# — which guards an RBS-derived judgment — does not apply to it.
|
|
808
|
-
guard = VersionGuard.verdict(predicate)
|
|
809
|
-
return guard if guard
|
|
810
|
-
# ADR-101 — decline on an optimistically nil-free carrier; see
|
|
811
|
-
# `StatementEvaluator#optimistic_carrier?` for why the gate is here and not in `Narrowing`.
|
|
812
|
-
return nil unless optimistic_origin_for(predicate).nil?
|
|
813
|
-
|
|
814
|
-
Narrowing.predicate_certainty(type_of(predicate))
|
|
1004
|
+
# `if` / `unless` in value position — the ternary `c ? a : b`, a modifier `(a unless c)`, or a
|
|
1005
|
+
# conditional written as an argument, a receiver, or a literal element. Issue #1003: this handler used
|
|
1006
|
+
# to type both arms in the receiver scope and union them, so the predicate's narrowing reached a
|
|
1007
|
+
# statement-position conditional (through `StatementEvaluator#eval_if`) and never a value-position
|
|
1008
|
+
# one: `f.finite? ? f : 0.0` read `0.0 | Float` while the `if` statement bound `0.0 | finite-float`.
|
|
1009
|
+
# It delegates to the statement evaluator instead of re-deriving the edges, so the two positions share
|
|
1010
|
+
# one narrowing application, one branch elision (ADR-47 WD5 version guards, the ADR-101
|
|
1011
|
+
# optimistic-carrier decline) and one terminating-arm rule, and cannot drift apart again. Only the
|
|
1012
|
+
# value is read; the post-scope belongs to whichever walk owns the statement.
|
|
1013
|
+
def type_of_conditional(node)
|
|
1014
|
+
scope.evaluate(node, tracer: tracer).first
|
|
815
1015
|
end
|
|
816
1016
|
|
|
817
1017
|
def type_of_else(node)
|
|
818
1018
|
statements_or_nil(node.statements)
|
|
819
1019
|
end
|
|
820
1020
|
|
|
821
|
-
# `a && b`
|
|
822
|
-
#
|
|
823
|
-
#
|
|
824
|
-
#
|
|
825
|
-
# through, so the result is one operand's type instead of a union. Otherwise the union-of-both-operands
|
|
826
|
-
# fallback is preserved.
|
|
1021
|
+
# `a && b` / `a || b` in value position. Issue #1016: this handler used to type both operands in the
|
|
1022
|
+
# receiver scope, so `x.finite? && x` read `Float | false` while the statement form bound
|
|
1023
|
+
# `false | finite-float`. Like `type_of_conditional`, it delegates to the statement evaluator, which owns
|
|
1024
|
+
# the RHS narrowing, the constant short-circuit and its issue #313 optimistic-carrier decline.
|
|
827
1025
|
def type_of_and_or(node)
|
|
828
|
-
|
|
829
|
-
polarity = left_operand_polarity(node.left, left_type)
|
|
830
|
-
return short_circuit_for(node, left_type, polarity) if polarity
|
|
831
|
-
|
|
832
|
-
# The left operand only flows through on the edge that short-circuits: `a || b` yields `a` solely
|
|
833
|
-
# when `a` is truthy, so its falsey constituents (`nil` / `false`) can never be the value of the
|
|
834
|
-
# OrNode (they hand off to `b`); `a && b` yields `a` solely when `a` is falsey. Narrow the surviving
|
|
835
|
-
# left edge before the union so `s || full` (with `s : String?`) types `String | <full>` rather than
|
|
836
|
-
# re-admitting the stripped `nil`. Mirrors `StatementEvaluator#eval_and_or`'s `skipped_type`.
|
|
837
|
-
surviving_left =
|
|
838
|
-
if node.is_a?(Prism::AndNode)
|
|
839
|
-
Narrowing.narrow_falsey(left_type)
|
|
840
|
-
else
|
|
841
|
-
Narrowing.narrow_truthy(left_type)
|
|
842
|
-
end
|
|
843
|
-
Type::Combinator.union(surviving_left, type_of(node.right))
|
|
844
|
-
end
|
|
845
|
-
|
|
846
|
-
def short_circuit_for(node, left_type, polarity)
|
|
847
|
-
and_node = node.is_a?(Prism::AndNode)
|
|
848
|
-
if polarity == :truthy
|
|
849
|
-
and_node ? type_of(node.right) : left_type
|
|
850
|
-
else
|
|
851
|
-
and_node ? left_type : type_of(node.right)
|
|
852
|
-
end
|
|
853
|
-
end
|
|
854
|
-
|
|
855
|
-
# Issue #313 — the node-aware wrapper the `&&` / `||` short-circuit reads. The spec's exclusion binds
|
|
856
|
-
# this gate as much as it binds `flow.always-truthy-condition`, and a `Constant`-only gate is not by
|
|
857
|
-
# itself enough to honour it: a literal hash whose values share one type reads as a lone `Constant`
|
|
858
|
-
# (`UNIFORM[key]` → `Constant[1]`), so the gate would judge the left operand of `UNIFORM[key] || key`
|
|
859
|
-
# provably truthy and discard the author's fallback — the counter-example the spec names verbatim.
|
|
860
|
-
# Declining returns the union of both operands, which is what `StatementEvaluator#eval_and_or` produces
|
|
861
|
-
# anyway, so the two `&&` / `||` typers stay in agreement.
|
|
862
|
-
def left_operand_polarity(left_node, left_type)
|
|
863
|
-
return nil unless optimistic_origin_for(left_node).nil?
|
|
864
|
-
|
|
865
|
-
constant_value_polarity(left_type)
|
|
866
|
-
end
|
|
867
|
-
|
|
868
|
-
# Returns `:truthy` / `:falsey` for a `Type::Constant`, nil otherwise. Mirrors
|
|
869
|
-
# `constant_predicate_polarity` but operates on a typed value (already-type-of'd) rather than a Prism
|
|
870
|
-
# node, so the same predicate analysis can be reused in both contexts.
|
|
871
|
-
def constant_value_polarity(type)
|
|
872
|
-
return nil unless type.is_a?(Type::Constant)
|
|
873
|
-
|
|
874
|
-
type.value ? :truthy : :falsey
|
|
1026
|
+
scope.evaluate(node, tracer: tracer).first
|
|
875
1027
|
end
|
|
876
1028
|
|
|
877
1029
|
# Three-valued evaluation of `case predicate when pattern` dispatch. For each `when` clause we ask:
|
|
@@ -889,29 +1041,41 @@ module Rigor
|
|
|
889
1041
|
# The `case ... in` pattern-matching form (`CaseMatchNode`) and the predicate-less form (`case; when
|
|
890
1042
|
# c1; ...`) bypass the `===` analysis: pattern matching has richer semantics, and a predicate-less
|
|
891
1043
|
# `case` reduces to a `if c1; ...; elsif c2` chain that statement-level narrowing already handles.
|
|
1044
|
+
# Issue #1429 — each arm is typed under the subject's clause narrowing (`Narrowing.case_when_scopes`), the
|
|
1045
|
+
# scope the statement evaluator runs the arm in: `when Symbol then n` on `n: Integer | Symbol` answers
|
|
1046
|
+
# `Symbol`, and the `else` arm reads the subject every earlier clause has ruled out.
|
|
892
1047
|
def type_of_case(node)
|
|
893
1048
|
return type_of_case_simple_union(node) if node.is_a?(Prism::CaseMatchNode) || node.predicate.nil?
|
|
894
1049
|
|
|
895
1050
|
subject_type = type_of(node.predicate)
|
|
896
1051
|
candidates = []
|
|
897
1052
|
reached_yes = false
|
|
1053
|
+
clause_scope = scope
|
|
898
1054
|
|
|
899
1055
|
node.conditions.each do |when_node|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1056
|
+
conditions = when_node.respond_to?(:conditions) ? when_node.conditions : []
|
|
1057
|
+
body_scope, next_scope = Narrowing.case_when_scopes(node.predicate, conditions, clause_scope)
|
|
1058
|
+
certainty = case_when_branch_certainty(subject_type, when_node)
|
|
1059
|
+
# :no — drop the branch
|
|
1060
|
+
candidates << case_arm_type(when_node, body_scope) unless certainty == :no
|
|
1061
|
+
if certainty == :yes
|
|
903
1062
|
reached_yes = true
|
|
904
1063
|
break
|
|
905
|
-
when :maybe
|
|
906
|
-
candidates << type_of(when_node)
|
|
907
|
-
# :no — drop the branch
|
|
908
1064
|
end
|
|
1065
|
+
clause_scope = next_scope
|
|
909
1066
|
end
|
|
910
1067
|
|
|
911
|
-
candidates <<
|
|
1068
|
+
candidates << case_arm_type(node.else_clause, clause_scope) unless reached_yes
|
|
912
1069
|
Type::Combinator.union(*candidates)
|
|
913
1070
|
end
|
|
914
1071
|
|
|
1072
|
+
# The value of one `case` arm (`nil` for an absent `else`), typed under `arm_scope`.
|
|
1073
|
+
def case_arm_type(arm, arm_scope)
|
|
1074
|
+
return Type::Combinator.constant_of(nil) if arm.nil?
|
|
1075
|
+
|
|
1076
|
+
arm_scope.equal?(scope) ? type_of(arm) : arm_scope.type_of(arm, tracer: tracer)
|
|
1077
|
+
end
|
|
1078
|
+
|
|
915
1079
|
def type_of_case_simple_union(node)
|
|
916
1080
|
branch_types = node.conditions.map { |branch| type_of(branch) }
|
|
917
1081
|
Type::Combinator.union(*branch_types, type_of_case_else(node))
|
|
@@ -1007,24 +1171,46 @@ module Rigor
|
|
|
1007
1171
|
Type::Combinator.union(primary_type, *rescue_types)
|
|
1008
1172
|
end
|
|
1009
1173
|
|
|
1174
|
+
# Issue #1360 — the evaluator binds `$!` in a rescue clause it enters; a clause typed here, such as one in a
|
|
1175
|
+
# `begin` passed as an argument, reads `$!`, `$@` and `$?` unbound rather than an enclosing clause's.
|
|
1010
1176
|
def rescue_chain_types(rescue_node)
|
|
1177
|
+
arm_typer = typer_under(rescue_arm_scope)
|
|
1011
1178
|
types = []
|
|
1012
1179
|
current = rescue_node
|
|
1013
1180
|
while current
|
|
1014
|
-
types << statements_or_nil
|
|
1181
|
+
types << arm_typer.send(:statements_or_nil, current.statements)
|
|
1015
1182
|
current = current.subsequent
|
|
1016
1183
|
end
|
|
1017
1184
|
types
|
|
1018
1185
|
end
|
|
1019
1186
|
|
|
1020
1187
|
def type_of_rescue(node)
|
|
1021
|
-
statements_or_nil
|
|
1188
|
+
typer_under(rescue_arm_scope).send(:statements_or_nil, node.statements)
|
|
1022
1189
|
end
|
|
1023
1190
|
|
|
1191
|
+
def rescue_arm_scope = scope.forget_error_info.forget_last_status
|
|
1192
|
+
|
|
1024
1193
|
# `expr rescue fallback` is RescueModifierNode in Prism. The result is `expr`'s type when no exception
|
|
1025
|
-
# is raised and `fallback`'s type otherwise; both paths are reachable, so the result is their union.
|
|
1194
|
+
# is raised and `fallback`'s type otherwise; both paths are reachable, so the result is their union. Issue #1360
|
|
1195
|
+
# — `fallback` runs with `$!` and `$@` bound to the `StandardError` it rescued ({ErrorInfo.modifier_entry}).
|
|
1026
1196
|
def type_of_rescue_modifier(node)
|
|
1027
|
-
|
|
1197
|
+
fallback = node.rescue_expression
|
|
1198
|
+
fallback_type =
|
|
1199
|
+
if ErrorInfo.read_in?(fallback)
|
|
1200
|
+
typer_under(ErrorInfo.modifier_entry(scope, fallback)).type_of(fallback)
|
|
1201
|
+
else
|
|
1202
|
+
type_of(fallback)
|
|
1203
|
+
end
|
|
1204
|
+
Type::Combinator.union(type_of(node.expression), fallback_type)
|
|
1205
|
+
end
|
|
1206
|
+
|
|
1207
|
+
# A typer that shares this one's tracer and operand types but reads `other_scope`.
|
|
1208
|
+
def typer_under(other_scope)
|
|
1209
|
+
return self if other_scope.equal?(scope)
|
|
1210
|
+
|
|
1211
|
+
ExpressionTyper.new(
|
|
1212
|
+
scope: other_scope, tracer: tracer, operand_types: @operand_types, typing_node: @typing_node
|
|
1213
|
+
)
|
|
1028
1214
|
end
|
|
1029
1215
|
|
|
1030
1216
|
def type_of_ensure(node)
|
|
@@ -1295,10 +1481,16 @@ module Rigor
|
|
|
1295
1481
|
# and fired `undefined method 'upcase' for nil` on correct code. The candidate is still looked up
|
|
1296
1482
|
# first — that lookup is a hash probe and owns the ADR-46 cross-file dependency edge — and
|
|
1297
1483
|
# {#self_type_answers?} then vetoes the bind for a name the enclosing class answers itself.
|
|
1484
|
+
#
|
|
1485
|
+
# Issue #963 item 2 — {#plugin_member_answers?} is the second veto, for the members no source the
|
|
1486
|
+
# engine walks can enumerate: a plugin's `dynamic_return` answer and the ADR-16 synthetic-method
|
|
1487
|
+
# index. They are members of `self` exactly as an `attr_reader` is, so a top-level `def` must not
|
|
1488
|
+
# bind ahead of them either.
|
|
1298
1489
|
def try_local_def_dispatch(node, receiver, arg_types, block_type = nil)
|
|
1299
1490
|
local_def = node.receiver.nil? ? scope.bindable_top_level_def_for(node.name) : nil
|
|
1300
1491
|
return nil unless local_def
|
|
1301
1492
|
return nil if self_type_answers?(node.name)
|
|
1493
|
+
return nil if plugin_member_answers?(node, receiver)
|
|
1302
1494
|
|
|
1303
1495
|
local_inference = infer_top_level_user_method(local_def, receiver, arg_types, block_type)
|
|
1304
1496
|
return local_inference if local_inference
|
|
@@ -1334,21 +1526,48 @@ module Rigor
|
|
|
1334
1526
|
# describes the source under analysis, while a bundled signature describes a class the project does
|
|
1335
1527
|
# not own, where a project `def` is a monkey-patch and [ADR-17] owns the question.
|
|
1336
1528
|
# `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
|
|
1529
|
+
def try_overriding_def_dispatch(node, receiver, arg_types, method_name: node.name)
|
|
1530
|
+
return nil unless overriding_own_def?(receiver, method_name)
|
|
1531
|
+
|
|
1532
|
+
try_user_method_inference(receiver, node, arg_types, method_name: method_name) || dynamic_top
|
|
1533
|
+
end
|
|
1534
|
+
|
|
1535
|
+
# The three conditions above: an own source `def` of `method_name` on the receiver's class, which has no
|
|
1536
|
+
# declaration of its own but inherits one a project-declared ancestor wrote.
|
|
1537
|
+
def overriding_own_def?(receiver, method_name)
|
|
1538
|
+
return false unless user_inference_receiver?(receiver)
|
|
1339
1539
|
|
|
1340
1540
|
class_name = receiver.class_name
|
|
1341
|
-
return
|
|
1541
|
+
return false if class_name.nil?
|
|
1342
1542
|
# `Scope#user_def_for`, not `discovered_method?`: the cross-file table deliberately withholds a
|
|
1343
1543
|
# plain instance `def` under the ADR-17 monkey-patch contract, and this gate must see one.
|
|
1344
|
-
return
|
|
1544
|
+
return false if scope.user_def_for(class_name, method_name).nil?
|
|
1345
1545
|
|
|
1346
|
-
definition = safe_rbs_method_definition(class_name,
|
|
1347
|
-
return
|
|
1348
|
-
return
|
|
1349
|
-
return nil unless project_declared_owner?(definition)
|
|
1546
|
+
definition = safe_rbs_method_definition(class_name, method_name, :instance)
|
|
1547
|
+
return false if definition.nil?
|
|
1548
|
+
return false if rbs_declared_on_class?(definition, class_name)
|
|
1350
1549
|
|
|
1351
|
-
|
|
1550
|
+
project_declared_owner?(definition)
|
|
1551
|
+
end
|
|
1552
|
+
|
|
1553
|
+
# {#implicit_index_read_type}'s dispatcher tier, with the write node as the call context.
|
|
1554
|
+
def index_read_dispatch(node, receiver, arg_types)
|
|
1555
|
+
MethodDispatcher.dispatch(
|
|
1556
|
+
receiver_type: receiver, method_name: :[], arg_types: arg_types,
|
|
1557
|
+
environment: scope.environment, call_node: node, scope: scope
|
|
1558
|
+
)
|
|
1559
|
+
end
|
|
1560
|
+
|
|
1561
|
+
# A splat index (`c[*keys] += v`) leaves the read's arity to the splat's expansion, and the one untyped
|
|
1562
|
+
# argument standing in for it would bind positionally to a body written for several: `def [](*keys) =
|
|
1563
|
+
# keys.size` folded the read of `r[*[0, 1]] += 1` to `1` and drew an always-falsey `n == 3` on a program
|
|
1564
|
+
# that takes that branch. So the body tiers decline under a splat, as `try_literal_send` does, and only
|
|
1565
|
+
# the signature-driven dispatch answers. An own `def` overriding an inherited declaration still outranks
|
|
1566
|
+
# that declaration; with its body unreadable, it answers `Dynamic[top]`.
|
|
1567
|
+
def splat_index_read_type(node, receiver, arg_types)
|
|
1568
|
+
return dynamic_top if overriding_own_def?(receiver, :[])
|
|
1569
|
+
|
|
1570
|
+
index_read_dispatch(node, receiver, arg_types)
|
|
1352
1571
|
end
|
|
1353
1572
|
|
|
1354
1573
|
# Whether the class an inherited declaration was written about is one the project declares itself.
|
|
@@ -1379,6 +1598,24 @@ module Rigor
|
|
|
1379
1598
|
end
|
|
1380
1599
|
end
|
|
1381
1600
|
|
|
1601
|
+
# Issue #963 item 2 — the plugin arm of the veto: whether a plugin answers `node.name` on the call's
|
|
1602
|
+
# own `self`. Asked of `MethodDispatcher.plugin_member_answers?`, which puts the question to the same
|
|
1603
|
+
# two tiers dispatch would consult (the gated `dynamic_return` walk and the ADR-16 synthetic-method
|
|
1604
|
+
# index), so the veto carries no plugin knowledge of its own.
|
|
1605
|
+
#
|
|
1606
|
+
# The confidence gate is #618's, unchanged and re-stated here rather than inherited: only a `self`
|
|
1607
|
+
# whose class is KNOWN participates. At genuine top level, and inside a block whose `self` is
|
|
1608
|
+
# unmodelled, `scope.self_type` is nil, `receiver` is the synthetic `Object` / `Dynamic[Top]` stand-in
|
|
1609
|
+
# that {#call_receiver_type_for} substitutes, and asking a plugin about THAT receiver would be asking
|
|
1610
|
+
# about a `self` the engine has not modelled — #316's / #319's territory, which this stays out of.
|
|
1611
|
+
def plugin_member_answers?(node, receiver)
|
|
1612
|
+
return false if scope.self_type.nil?
|
|
1613
|
+
|
|
1614
|
+
MethodDispatcher.plugin_member_answers?(
|
|
1615
|
+
call_node: node, scope: scope, receiver_type: receiver, method_name: node.name
|
|
1616
|
+
)
|
|
1617
|
+
end
|
|
1618
|
+
|
|
1382
1619
|
# The instance side of {#self_type_answers?}: the discovered methods (`def`, `attr_*`,
|
|
1383
1620
|
# `define_method`, `alias`) of the class or any project ancestor, its `Struct.new` / `Data.define`
|
|
1384
1621
|
# member accessors, a `def` reached through its project ancestors (superclass chain and included
|
|
@@ -1407,25 +1644,13 @@ module Rigor
|
|
|
1407
1644
|
# environment, so the declaration that decides the question is written about an ancestor the project
|
|
1408
1645
|
# does not declare (`StandardError`, `Array`, `Comparable`) — each is asked on its own terms, and
|
|
1409
1646
|
# each of them is itself before `::Object` in the reader's MRO by construction.
|
|
1647
|
+
#
|
|
1648
|
+
# Issue #527 slice 0 — the walk itself lives in {ExternalAncestorResolution}, which reports WHICH
|
|
1649
|
+
# declaration answered because the dispatch side needs to dispatch there. This tier only needs to
|
|
1650
|
+
# know that one did, so it reads the presence of an answer. Dependency recording stays ON: this
|
|
1651
|
+
# arm genuinely read the ancestor's declaration to decide the binding.
|
|
1410
1652
|
def rbs_ancestor_answers?(class_name, method_name)
|
|
1411
|
-
|
|
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
|
|
1653
|
+
!ExternalAncestorResolution.resolve(class_name, method_name, :instance, scope: scope).nil?
|
|
1429
1654
|
end
|
|
1430
1655
|
|
|
1431
1656
|
# The singleton side: a class-body `self` is `Singleton[Foo]`, where an implicit-self call reaches
|
|
@@ -1452,56 +1677,16 @@ module Rigor
|
|
|
1452
1677
|
!members.nil? && members.include?(method_name.to_sym)
|
|
1453
1678
|
end
|
|
1454
1679
|
|
|
1680
|
+
# Issue #527 slice 0 — the RBS lookup, its `rescue`, and the `::Object` MRO cut-off moved to
|
|
1681
|
+
# {ExternalAncestorResolution}, which the dispatch side reads too. These stay as this class's
|
|
1682
|
+
# spelling of them, because three other tiers here (`try_overriding_def_dispatch`,
|
|
1683
|
+
# {#singleton_self_answers?}) ask the own-class question without the ancestor walk.
|
|
1455
1684
|
def safe_rbs_method_definition(class_name, method_name, kind)
|
|
1456
|
-
|
|
1457
|
-
Rigor::Reflection.singleton_method_definition(class_name, method_name, scope: scope)
|
|
1458
|
-
else
|
|
1459
|
-
Rigor::Reflection.instance_method_definition(class_name, method_name, scope: scope)
|
|
1460
|
-
end
|
|
1461
|
-
rescue StandardError
|
|
1462
|
-
nil
|
|
1685
|
+
ExternalAncestorResolution.method_definition(class_name, method_name, kind, scope: scope)
|
|
1463
1686
|
end
|
|
1464
1687
|
|
|
1465
|
-
# True when the RBS declaration found for the name sits on `class_name` itself rather than on an
|
|
1466
|
-
# ancestor; mirrors `CheckRules#defined_on?` and `SigGen::Generator#declared_on_class_itself?`.
|
|
1467
1688
|
def rbs_declared_on_class?(definition, class_name)
|
|
1468
|
-
|
|
1469
|
-
return false unless definition.respond_to?(:defined_in)
|
|
1470
|
-
|
|
1471
|
-
defined_in = definition.defined_in
|
|
1472
|
-
return false if defined_in.nil?
|
|
1473
|
-
|
|
1474
|
-
defined_in.to_s.delete_prefix("::") == class_name.to_s.delete_prefix("::")
|
|
1475
|
-
end
|
|
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
|
-
[]
|
|
1689
|
+
ExternalAncestorResolution.declared_on_class?(definition, class_name)
|
|
1505
1690
|
end
|
|
1506
1691
|
|
|
1507
1692
|
# Issue #520 — Ruby defines the value of an attribute / index assignment (`x.attr = v`, `h[k] = v`)
|
|
@@ -1615,12 +1800,87 @@ module Rigor
|
|
|
1615
1800
|
# `ops.all? { |o| break false unless o; true }` still folds the no-break path to `Constant[true]`, and
|
|
1616
1801
|
# the union with the `false` arm makes the call `bool` — no `flow.always-truthy-condition` on a program
|
|
1617
1802
|
# that really can answer false.
|
|
1803
|
+
#
|
|
1804
|
+
# Issue #1095: the callee's result is kept because an arbitrary callee may return without ever running
|
|
1805
|
+
# the block. A catalogued exactly-once yielder ({BlockCallTiming}) cannot, so when its block's normal
|
|
1806
|
+
# completion is unreachable the callee's result is too, and the call is its `break` arms alone — `bot`
|
|
1807
|
+
# when every path raises or returns instead.
|
|
1808
|
+
#
|
|
1809
|
+
# Issue #1107: `Kernel#loop` declares `-> bot` but returns normally, with `StopIteration#result`, when its
|
|
1810
|
+
# block raises `StopIteration`. Unless the body provably cannot ({BlockCallTiming.loop_may_complete?}),
|
|
1811
|
+
# that normal return is `untyped` — the result's declared type — so `loop { e.next }` is not `bot` and
|
|
1812
|
+
# `loop { x = e.next; break x if x }` keeps its arm beside it.
|
|
1618
1813
|
def call_dispatch_type_for(node, receiver_override: nil)
|
|
1619
|
-
result = call_result_type_for(node, receiver_override: receiver_override)
|
|
1814
|
+
result = loop_completion_type(node, call_result_type_for(node, receiver_override: receiver_override))
|
|
1620
1815
|
arms = call_break_arm_types(node, receiver_override: receiver_override)
|
|
1621
|
-
|
|
1816
|
+
if exactly_once_block_never_completes?(node, receiver_override)
|
|
1817
|
+
return arms.empty? ? Type::Combinator.bot : Type::Combinator.union(*arms)
|
|
1818
|
+
end
|
|
1819
|
+
|
|
1820
|
+
combined = arms.empty? ? result : Type::Combinator.union(result, *arms)
|
|
1821
|
+
widen_optimistic_predicate_constant(node, combined)
|
|
1822
|
+
end
|
|
1823
|
+
|
|
1824
|
+
# Issue #1172 — the nil-collapsing predicates (`nil?`, `!`, `x == nil`, …) answer a `Constant` that
|
|
1825
|
+
# encodes the receiver's nil-freeness. When that nil-freeness is only *optimistic* — an
|
|
1826
|
+
# `%a{implicitly-returns-nil}` read `RbsDispatch` deliberately reads past — the constant is a bet,
|
|
1827
|
+
# not a fact, and unlike the in-scope `OptimisticOrigin` mark a folded `Constant[false]` survives
|
|
1828
|
+
# into the enclosing method's return summary. A caller's `helper(...) ? a : b` then sees a
|
|
1829
|
+
# proof-shaped constant and reports `flow.always-truthy-condition` on a live guard (the mark cannot
|
|
1830
|
+
# follow a value across the method boundary, so {OptimisticOrigin.resolve} at the call site finds
|
|
1831
|
+
# nothing). Widen the predicate's constant answer to `bool` when the call derives a mark: the three
|
|
1832
|
+
# certainty consumers already decline on the mark inside this scope, and the widened return keeps
|
|
1833
|
+
# the same judgment from leaking out as a folded return type. The carrier's own type is unchanged —
|
|
1834
|
+
# `h[k]` still reads nil-free, per the spec's MUST NOT.
|
|
1835
|
+
def widen_optimistic_predicate_constant(node, type)
|
|
1836
|
+
return type unless type.is_a?(Type::Constant) && [true, false].include?(type.value)
|
|
1837
|
+
# The carrier read itself keeps its declared answer even when that is a literal `true` /
|
|
1838
|
+
# `false` — the mark says its nil-freeness is a bet, not that the value's class widened.
|
|
1839
|
+
return type if scope.optimistic_origins[node]
|
|
1840
|
+
return type if OptimisticOrigin.resolve(node, scope).nil?
|
|
1841
|
+
|
|
1842
|
+
# Widen only toward what a miss really answers: a safe-navigation call answers `nil`, and
|
|
1843
|
+
# `!recv&.empty?` answers `true` either way, so widening either would invent the other boolean.
|
|
1844
|
+
miss = OptimisticOrigin.miss_answer(node, scope)
|
|
1845
|
+
return type unless miss.equal?(OptimisticOrigin::UNKNOWN_MISS) || miss == !type.value
|
|
1846
|
+
|
|
1847
|
+
Type::Combinator.union(Type::Combinator.constant_of(true), Type::Combinator.constant_of(false))
|
|
1848
|
+
end
|
|
1849
|
+
|
|
1850
|
+
def loop_completion_type(node, result)
|
|
1851
|
+
return result unless result.is_a?(Type::Bot) && BlockCallTiming.loop_may_complete?(node)
|
|
1852
|
+
|
|
1853
|
+
scope.record_dynamic_origin(node, DynamicOrigin::EXPLICIT_UNTYPED)
|
|
1854
|
+
Type::Combinator.untyped
|
|
1855
|
+
end
|
|
1856
|
+
|
|
1857
|
+
# Whether `node` calls a catalogued exactly-once yielder ({BlockCallTiming}) with a literal block that can
|
|
1858
|
+
# never complete normally. Two proofs must BOTH hold. The syntactic one
|
|
1859
|
+
# ({BlockCallTiming.never_completes_normally?}) says every path ends in a jump or a non-returning Kernel
|
|
1860
|
+
# call. The block-return pass must also answer exactly `bot`: a reachable `next` joins its value instead
|
|
1861
|
+
# (#841), so `tap { next "s" }` completes and keeps the receiver, and a nil-bearing or `Dynamic` value or a
|
|
1862
|
+
# failed pass (`nil`) keeps the #853 union. The pass alone is not enough, because it also answers `bot`
|
|
1863
|
+
# for a body whose last call merely DECLARES `-> bot` — a project method's signature, or `loop` before
|
|
1864
|
+
# #1107 widened it ({#loop_completion_type}); `loop { e.next }` returns normally once `e` is drained, and
|
|
1865
|
+
# trusting it made correct code report an always-falsey condition.
|
|
1866
|
+
#
|
|
1867
|
+
# The pre-gates run cheapest-first because the block is re-typed here: the name, a `Prism::BlockNode` (a
|
|
1868
|
+
# `&blk` / `&:sym` block-pass carries no body to prove anything about), no arguments (none of the three
|
|
1869
|
+
# takes one), the syntactic walk, then the resolved-owner check.
|
|
1870
|
+
def exactly_once_block_never_completes?(node, receiver_override)
|
|
1871
|
+
return false unless BlockCallTiming.candidate_name?(node.name)
|
|
1872
|
+
|
|
1873
|
+
block_node = node.block
|
|
1874
|
+
return false unless block_node.is_a?(Prism::BlockNode) && block_node.body
|
|
1875
|
+
return false if node.arguments
|
|
1876
|
+
return false unless BlockCallTiming.never_completes_normally?(block_node.body, scope)
|
|
1622
1877
|
|
|
1623
|
-
|
|
1878
|
+
receiver = receiver_override || call_receiver_type_for(node)
|
|
1879
|
+
return false unless BlockCallTiming.exactly_once_call?(
|
|
1880
|
+
receiver_type: receiver, method_name: node.name, scope: scope
|
|
1881
|
+
)
|
|
1882
|
+
|
|
1883
|
+
block_return_type_for(node, receiver, []).is_a?(Type::Bot)
|
|
1624
1884
|
end
|
|
1625
1885
|
|
|
1626
1886
|
def call_result_type_for(node, receiver_override: nil)
|
|
@@ -1956,6 +2216,9 @@ module Rigor
|
|
|
1956
2216
|
# The three receiver-shaped block folds, in their historical order (extracted whole from
|
|
1957
2217
|
# `call_dispatch_type_for` for method-length budget).
|
|
1958
2218
|
def try_receiver_block_folds(node, receiver, arg_types)
|
|
2219
|
+
rebound = rebound_operand_typer(node)
|
|
2220
|
+
return rebound.send(:try_receiver_block_folds, node, receiver, arg_types) if rebound
|
|
2221
|
+
|
|
1959
2222
|
per_element = try_per_element_block_fold(node, receiver)
|
|
1960
2223
|
return per_element if per_element
|
|
1961
2224
|
|
|
@@ -1965,6 +2228,25 @@ module Rigor
|
|
|
1965
2228
|
try_hash_shape_block_fold(node, receiver)
|
|
1966
2229
|
end
|
|
1967
2230
|
|
|
2231
|
+
# Issue #1365 — Ruby runs a call's receiver chain and arguments before the method yields, so when they may
|
|
2232
|
+
# rebind the match globals ({MatchRebinding.operands_may_rebind?}: `[u.index(/(q)/)].map { $1 }`) every
|
|
2233
|
+
# pass that types the call's block — the block-return pass, its captured-local fixpoint and the receiver
|
|
2234
|
+
# folds — types it under a typer whose scope has forgotten them, as {MatchRebinding.block_entry} does for
|
|
2235
|
+
# the call's block and `StatementEvaluator#forget_operand_specials` for the statement. `$_` is forgotten on
|
|
2236
|
+
# the same terms when they may set it ({LastLine.operands_may_set?}, issue #1359). nil otherwise.
|
|
2237
|
+
def rebound_operand_typer(call_node)
|
|
2238
|
+
return nil if call_node.block.nil?
|
|
2239
|
+
|
|
2240
|
+
rebound = scope
|
|
2241
|
+
if rebound.match_globals_bound? && MatchRebinding.operands_may_rebind?(call_node, scope)
|
|
2242
|
+
rebound = rebound.forget_match_globals
|
|
2243
|
+
end
|
|
2244
|
+
rebound = rebound.forget_last_line if rebound.last_line_bound? && LastLine.operands_may_set?(call_node, scope)
|
|
2245
|
+
return nil if rebound.equal?(scope)
|
|
2246
|
+
|
|
2247
|
+
ExpressionTyper.new(scope: rebound, tracer: tracer, operand_types: @operand_types, typing_node: @typing_node)
|
|
2248
|
+
end
|
|
2249
|
+
|
|
1968
2250
|
# Issue #533 — `x.send(:selector, args)` with a LITERAL symbol is statically `x.selector(args)`:
|
|
1969
2251
|
# the private-boundary idiom (protobuf's `send(:get_file_descriptor)` at 84 sites) resolves through
|
|
1970
2252
|
# the same dispatch + project-inference tiers as the direct call would. `send` legitimately crosses
|
|
@@ -2144,26 +2426,49 @@ module Rigor
|
|
|
2144
2426
|
CLASS_GRAPH_CACHE_KEY = :__rigor_class_graph_cache__
|
|
2145
2427
|
private_constant :CLASS_GRAPH_CACHE_KEY
|
|
2146
2428
|
|
|
2147
|
-
#
|
|
2148
|
-
#
|
|
2149
|
-
#
|
|
2150
|
-
#
|
|
2151
|
-
#
|
|
2152
|
-
#
|
|
2153
|
-
#
|
|
2154
|
-
#
|
|
2155
|
-
#
|
|
2156
|
-
#
|
|
2157
|
-
#
|
|
2429
|
+
# Memo for the static class-graph resolvers below. They are pure functions of the scope's *frozen*
|
|
2430
|
+
# discovery tables — `user_def_for` / `superclass_of` / `includes_of` and the ancestor-name resolver
|
|
2431
|
+
# read nothing else, and never touch the current scope's locals or narrowings — so a result computed
|
|
2432
|
+
# for one `(class, method)` is valid for every `Scope` carrying the same {DiscoveryIndex}.
|
|
2433
|
+
#
|
|
2434
|
+
# The key is that index's identity, NOT the three tables the walk is usually described by. Five
|
|
2435
|
+
# tables are in play: `discovered_def_nodes` / `discovered_superclasses` / `discovered_includes`,
|
|
2436
|
+
# plus `discovered_header_nestings` and `discovered_methods`, which {Scope#ancestor_name_candidates}
|
|
2437
|
+
# and {Scope#known_user_class?} read when resolving an ancestor name to a project class. Keying on
|
|
2438
|
+
# the trio alone would let a `discovery.with(discovered_methods: …)` that happens to share the trio
|
|
2439
|
+
# serve a name resolution computed against the OLD table. The index that owns all five is one
|
|
2440
|
+
# object, so keying on it is both cheaper to check and correct by construction — and stays correct
|
|
2441
|
+
# when a sixth table joins the walk.
|
|
2442
|
+
#
|
|
2443
|
+
# ONE slot, replaced rather than accumulated — the same shape, and now the same key, as
|
|
2444
|
+
# {MethodDispatcher::RbsDispatch}'s `core_stdlib_memo`. `ExpressionTyper` is rebuilt per
|
|
2445
|
+
# `Scope#type_of`, so the slot lives on `Thread.current` rather than on `self`. A `Scope` merges its
|
|
2446
|
+
# file's discovery with the project pre-pass, so every analysed file arrives with a fresh index: an
|
|
2447
|
+
# identity-keyed *store* grew one bucket per file and, because the keys ARE the tables, pinned every
|
|
2448
|
+
# file's whole discovery index for the length of the run. What that bought, measured over
|
|
2449
|
+
# `lib`+`plugins`: 552 buckets holding 1,834 entries between them, and 58 of 80,106 calls answered
|
|
2450
|
+
# across files — 149 of the 701 slot switches returned to a bucket seen before, all of them in the
|
|
2451
|
+
# seed phases that revisit the project index. One slot gives those 58 answers up and allocates 706
|
|
2452
|
+
# buckets instead of 552, and still allocates FEWER objects over `lib` than the store did (−368),
|
|
2453
|
+
# because one `equal?` is cheaper than three identity-hash lookups.
|
|
2454
|
+
#
|
|
2455
|
+
# Steady-state cost is one `equal?` check and zero allocation; the array and the buckets are
|
|
2456
|
+
# allocated once per index. (Pool mode forks per worker, so the `Thread.current` slot is
|
|
2457
|
+
# process-local and never crosses a project boundary — and note that the parent's reported
|
|
2458
|
+
# `Memory peak` therefore does not see a worker's share of this at all.)
|
|
2158
2459
|
def class_graph_buckets
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2460
|
+
discovery = scope.discovery
|
|
2461
|
+
slot = Thread.current[CLASS_GRAPH_CACHE_KEY]
|
|
2462
|
+
unless slot && slot[0].equal?(discovery)
|
|
2463
|
+
# `self_pure` is issue #525's grant scan (identity-keyed by def node); it belongs here because it
|
|
2464
|
+
# is a pure function of the same frozen index — the sibling resolver it walks reads nothing else.
|
|
2465
|
+
# `singleton_def` is added lazily by {#singleton_def_through_ancestors}'s caller.
|
|
2466
|
+
slot = [discovery,
|
|
2467
|
+
{ name: {}, user_def: {}, self_pure: {}.compare_by_identity,
|
|
2468
|
+
yields: {}.compare_by_identity }]
|
|
2469
|
+
Thread.current[CLASS_GRAPH_CACHE_KEY] = slot
|
|
2470
|
+
end
|
|
2471
|
+
slot[1]
|
|
2167
2472
|
end
|
|
2168
2473
|
|
|
2169
2474
|
def resolve_user_def_through_ancestors(class_name, method_name)
|
|
@@ -2222,16 +2527,40 @@ module Rigor
|
|
|
2222
2527
|
OVERRIDE_GATE_CACHE_KEY = :__rigor_overridable_method_gate__
|
|
2223
2528
|
private_constant :OVERRIDE_GATE_CACHE_KEY
|
|
2224
2529
|
|
|
2225
|
-
#
|
|
2226
|
-
# the frozen discovery
|
|
2227
|
-
#
|
|
2228
|
-
#
|
|
2229
|
-
#
|
|
2530
|
+
# Memo for {#overridden_in_project?}, nested `kind → owner → method_name`. The predicate is a pure
|
|
2531
|
+
# function of the scope's frozen discovery tables, so that index's *identity* is what says whether a
|
|
2532
|
+
# bucket still applies. Nesting under `kind` and `owner` avoids allocating a composite cache key on
|
|
2533
|
+
# the hot path (the gate runs on every adopted self-call return), so a steady-state hit is one
|
|
2534
|
+
# `equal?` check and three keyed hash reads with zero allocation.
|
|
2535
|
+
#
|
|
2536
|
+
# The key is the whole {Scope::DiscoveryIndex}, NOT the `discovered_def_nodes` /
|
|
2537
|
+
# `discovered_superclasses` / `discovered_includes` trio the walk is usually described by — the same
|
|
2538
|
+
# key, for the same reason, as {#class_graph_buckets}. {#related_to_owner?} reaches
|
|
2539
|
+
# {Scope#ancestor_name_candidates} and {Scope#known_user_class?}, which read
|
|
2540
|
+
# `discovered_header_nestings` and `discovered_methods` as well, so a trio key would let an index
|
|
2541
|
+
# that swapped only one of those serve an answer computed against the old table. Over `lib`+`plugins`
|
|
2542
|
+
# the two keys switch the same 113 times, so correctness here is free.
|
|
2543
|
+
#
|
|
2544
|
+
# ONE slot, replaced rather than accumulated — the same shape as
|
|
2545
|
+
# {MethodDispatcher::RbsDispatch}'s `core_stdlib_memo`. A `Scope` merges its file's discovery with the
|
|
2546
|
+
# project pre-pass, so every analysed file gets a fresh index: an identity-keyed *store* grew one
|
|
2547
|
+
# bucket per file and, because the key IS the index, pinned every file's whole discovery index for the
|
|
2548
|
+
# length of the run — 109 live buckets over `lib`+`plugins`, one per distinct trio at every key level,
|
|
2549
|
+
# holding 418 answers between them (211 instance, 207 singleton).
|
|
2550
|
+
#
|
|
2551
|
+
# What the store bought for that was one bucket's worth of cross-file reuse, not 109: only the
|
|
2552
|
+
# project-seed scope recurs, because {Analysis::Runner} types each file's pre-passes under it before
|
|
2553
|
+
# {ScopeIndexer} merges the file's own discovery in. A bucket is two small hashes, so giving that up
|
|
2554
|
+
# is free — unlike {#method_definers_index} below, whose bucket costs a full table scan and which is
|
|
2555
|
+
# keyed and bounded differently for exactly that reason.
|
|
2230
2556
|
def override_gate_buckets
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2557
|
+
discovery = scope.discovery
|
|
2558
|
+
slot = Thread.current[OVERRIDE_GATE_CACHE_KEY]
|
|
2559
|
+
unless slot && slot[0].equal?(discovery)
|
|
2560
|
+
slot = [discovery, { instance: {}, singleton: {} }]
|
|
2561
|
+
Thread.current[OVERRIDE_GATE_CACHE_KEY] = slot
|
|
2562
|
+
end
|
|
2563
|
+
slot[1]
|
|
2235
2564
|
end
|
|
2236
2565
|
|
|
2237
2566
|
# True when some discovered project class/module — distinct from `owner` — redefines `(method_name,
|
|
@@ -2271,14 +2600,65 @@ module Rigor
|
|
|
2271
2600
|
METHOD_DEFINERS_INDEX_KEY = :__rigor_method_definers_index__
|
|
2272
2601
|
private_constant :METHOD_DEFINERS_INDEX_KEY
|
|
2273
2602
|
|
|
2274
|
-
#
|
|
2275
|
-
#
|
|
2276
|
-
#
|
|
2277
|
-
#
|
|
2603
|
+
# The second way. Two flat thread slots rather than one slot holding an array of ways: a
|
|
2604
|
+
# `Thread.current[KEY] ||= […]` seeds the store with an array LITERAL, which folds to a tuple whose
|
|
2605
|
+
# elements are pinned, and the engine then reads the `way[0].equal?(…)` guards below as always-falsey
|
|
2606
|
+
# (`make check` fires `flow.always-truthy-condition` on this very file). Two keys read as two plain
|
|
2607
|
+
# `Thread.current` reads and keep the guards analysable.
|
|
2608
|
+
METHOD_DEFINERS_INDEX_ALT_KEY = :__rigor_method_definers_index_alt__
|
|
2609
|
+
private_constant :METHOD_DEFINERS_INDEX_ALT_KEY
|
|
2610
|
+
|
|
2611
|
+
# `method_name (Symbol) → [owner names]` inverted index over the instance / singleton def tables,
|
|
2612
|
+
# valid for exactly as long as the table it inverts: a new analysis generation, or a `Scope` that swaps
|
|
2613
|
+
# its index through {Scope#with_discovery}, needs a fresh one. The toplevel sentinel is excluded — a
|
|
2614
|
+
# toplevel `def` has no class ancestry and so can never be an override.
|
|
2615
|
+
#
|
|
2616
|
+
# Bounded to TWO slots, replaced rather than accumulated. This memo is the costliest member of the
|
|
2617
|
+
# per-file-store family, because it does not hold a handful of resolved answers but a whole inverted
|
|
2618
|
+
# index built over a file's merged def table: an identity-keyed *store* held 135 of them over
|
|
2619
|
+
# `lib`+`plugins` — 744,431 index rows, 50.0 MB in the index hashes and their owner-name arrays alone,
|
|
2620
|
+
# on top of the def tables the keys pinned.
|
|
2621
|
+
#
|
|
2622
|
+
# Two, and not the one slot the cheaper memos in this family use, because the tables ALTERNATE.
|
|
2623
|
+
# {Analysis::Runner} types each file's pre-passes under the project-seed scope and its main pass under
|
|
2624
|
+
# the file's merged tables. Where a file reaches this gate from BOTH phases the request sequence is
|
|
2625
|
+
# `seed, file_1, seed, file_2, …`, and a single slot evicts the seed's index once per file and
|
|
2626
|
+
# rebuilds it on the next — a full scan of the seed's whole def table, every file. 200 synthetic
|
|
2627
|
+
# files shaped that way measured +22.6 % allocations against the store, which a bounded slot must not
|
|
2628
|
+
# cost. Two ways — a most-recently-used slot and one alternate, swapped on a hit in the alternate —
|
|
2629
|
+
# cover that period-2 shape exactly: the same 200 files rebuild 201 times, matching the store.
|
|
2630
|
+
#
|
|
2631
|
+
# Wider rotations are not covered, and do not need to be. On `lib`+`plugins` only 6 of the 109 gating
|
|
2632
|
+
# files consult the seed's table, so its uses are separated by many other files' tables and fall out
|
|
2633
|
+
# of both ways: five rebuilds of a 700-entry table remain, about 4 ms. Retention is bounded at two
|
|
2634
|
+
# generations either way, which is what the store failed to do.
|
|
2635
|
+
#
|
|
2636
|
+
# A way is `[def_nodes, singleton_def_nodes, instance_index, singleton_index]`, so it keys on BOTH def
|
|
2637
|
+
# tables and a gate call that alternates instance and singleton kinds within one file does not rebuild
|
|
2638
|
+
# either. Each index is still built lazily: a run that never asks a singleton question never pays for
|
|
2639
|
+
# the singleton index.
|
|
2640
|
+
#
|
|
2641
|
+
# DELIBERATELY not keyed on the whole {Scope::DiscoveryIndex}, unlike {#class_graph_buckets} and
|
|
2642
|
+
# {#override_gate_buckets} above. {#build_method_definers_index} reads the ONE table it is handed and
|
|
2643
|
+
# nothing else, so the def tables are already the complete key, and the index object is a strictly
|
|
2644
|
+
# narrower one: the project-seed scope carries a fresh index per file while its def tables stay the
|
|
2645
|
+
# same object. Over the 200 synthetic files above, an index-keyed memo misses all 400 times where a
|
|
2646
|
+
# def-table-keyed one misses 201 — it would rebuild the seed's 934-entry table once per file and hand
|
|
2647
|
+
# back the +22.6 % this bound exists to avoid. Check that number before widening this key to match
|
|
2648
|
+
# its siblings.
|
|
2278
2649
|
def method_definers_index(kind)
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2650
|
+
def_nodes = scope.discovered_def_nodes
|
|
2651
|
+
singleton_def_nodes = scope.discovered_singleton_def_nodes
|
|
2652
|
+
way = Thread.current[METHOD_DEFINERS_INDEX_KEY]
|
|
2653
|
+
unless way && way[0].equal?(def_nodes) && way[1].equal?(singleton_def_nodes)
|
|
2654
|
+
alt = Thread.current[METHOD_DEFINERS_INDEX_ALT_KEY]
|
|
2655
|
+
alt = nil unless alt && alt[0].equal?(def_nodes) && alt[1].equal?(singleton_def_nodes)
|
|
2656
|
+
Thread.current[METHOD_DEFINERS_INDEX_ALT_KEY] = way
|
|
2657
|
+
way = alt || [def_nodes, singleton_def_nodes, nil, nil]
|
|
2658
|
+
Thread.current[METHOD_DEFINERS_INDEX_KEY] = way
|
|
2659
|
+
end
|
|
2660
|
+
singleton = kind == :singleton
|
|
2661
|
+
way[singleton ? 3 : 2] ||= build_method_definers_index(singleton ? singleton_def_nodes : def_nodes)
|
|
2282
2662
|
end
|
|
2283
2663
|
|
|
2284
2664
|
def build_method_definers_index(table)
|
|
@@ -2541,7 +2921,7 @@ module Rigor
|
|
|
2541
2921
|
# below reads it back from here rather than taking a second parameter, so key and frame cannot
|
|
2542
2922
|
# disagree). It is installed even when nil, which is what stops a blockless callee reached from
|
|
2543
2923
|
# inside a yielding body from inheriting the outer caller's block.
|
|
2544
|
-
ExpressionTyper.
|
|
2924
|
+
ExpressionTyper.with_call_site_frame(yield_type, forwarded_frame_types(def_node, arg_types)) do
|
|
2545
2925
|
unless memo_candidate?(stack, plain_signature)
|
|
2546
2926
|
trace_memo_refusal(stack, plain_signature)
|
|
2547
2927
|
next compute_user_method_return(def_node, body_scope, stack, summaries,
|
|
@@ -2770,6 +3150,28 @@ module Rigor
|
|
|
2770
3150
|
evaluate_guarded_user_method_body(def_node, body_scope, stack, signature, context)
|
|
2771
3151
|
end
|
|
2772
3152
|
|
|
3153
|
+
# Issue #1125 — the argument list a `def m(...)` body's `...` re-supplies, or nil for a def without the
|
|
3154
|
+
# forwarding parameter (whose body cannot contain a `...` call at all) and for a call that does not even
|
|
3155
|
+
# satisfy `m`'s own required positionals (that call raises at runtime; the body is not worth re-typing).
|
|
3156
|
+
#
|
|
3157
|
+
# Ruby allows `...` only beside leading positional parameters (`def m(a, ...)`, `def m(a = 1, ...)`) — a
|
|
3158
|
+
# keyword / rest / block parameter next to it is a syntax error — so the tail is the call's own argument
|
|
3159
|
+
# list MINUS the leading positionals those named parameters consume, keeping a trailing keyword shape
|
|
3160
|
+
# (which `takes_keywords?` reads as the keyword tail rather than as a positional). The callee's own
|
|
3161
|
+
# call-site `arg_types` are what remains.
|
|
3162
|
+
def forwarded_frame_types(def_node, arg_types)
|
|
3163
|
+
params = def_node.parameters
|
|
3164
|
+
return nil unless params.is_a?(Prism::ParametersNode)
|
|
3165
|
+
return nil unless params.keyword_rest.is_a?(Prism::ForwardingParameterNode)
|
|
3166
|
+
|
|
3167
|
+
positional = arg_types.dup
|
|
3168
|
+
kw_shape = positional.pop if takes_keywords?(params) && positional.last.is_a?(Type::HashShape)
|
|
3169
|
+
return nil if positional.size < params.requireds.size
|
|
3170
|
+
|
|
3171
|
+
tail = positional[(params.requireds.size + params.optionals.size)..] || []
|
|
3172
|
+
kw_shape ? tail + [kw_shape] : tail
|
|
3173
|
+
end
|
|
3174
|
+
|
|
2773
3175
|
# True when this frame's result is a candidate for the return memo: the one structural precondition,
|
|
2774
3176
|
# stable across the body walk, that is necessary (but not sufficient) for a FINAL result — this plain
|
|
2775
3177
|
# signature must not itself be on the recursion guard stack (else we are inside its own cycle,
|
|
@@ -3198,7 +3600,8 @@ module Rigor
|
|
|
3198
3600
|
locals = bind_params_from_call_types(params, arg_types)
|
|
3199
3601
|
return nil if locals.nil?
|
|
3200
3602
|
|
|
3201
|
-
# Construct the body scope in a SINGLE allocation
|
|
3603
|
+
# Construct the body scope in a SINGLE Scope allocation (plus the issue #1358 frame it carries, whose
|
|
3604
|
+
# scans run only on demand) — the previous `Scope.empty.with_*.with_*…` chain
|
|
3202
3605
|
# allocated a fresh frozen Scope per field, run per user-method-call inference (ADR-44). The
|
|
3203
3606
|
# discovery index is inherited whole by reference (ADR-53 Track A); the hand-copied per-field list
|
|
3204
3607
|
# this replaces had silently dropped `data_member_layouts` and `discovered_method_visibilities`.
|
|
@@ -3216,7 +3619,9 @@ module Rigor
|
|
|
3216
3619
|
lexical_nesting: recorded_def_nesting(def_node),
|
|
3217
3620
|
discovery: scope.discovery,
|
|
3218
3621
|
struct_fold_safe_locals: body_fold_safe_locals(def_node, receiver, self_fold_safe),
|
|
3219
|
-
dynamic_origins: scope.dynamic_origins
|
|
3622
|
+
dynamic_origins: scope.dynamic_origins,
|
|
3623
|
+
# Issue #1358 — the callee runs in a frame of its own ({Scope#with_match_frame}).
|
|
3624
|
+
match_frame: MatchRebinding::Frame.new(def_node.body, def_node.parameters)
|
|
3220
3625
|
)
|
|
3221
3626
|
end
|
|
3222
3627
|
|
|
@@ -3296,17 +3701,35 @@ module Rigor
|
|
|
3296
3701
|
return nil if locals.nil?
|
|
3297
3702
|
|
|
3298
3703
|
bind_keyword_params(params, kw_shape, locals)
|
|
3299
|
-
|
|
3704
|
+
rest_name = keyword_rest_name(params)
|
|
3705
|
+
locals[rest_name.to_sym] = keyword_rest_type(params, kw_shape) if rest_name
|
|
3300
3706
|
locals[params.block.name.to_sym] = dynamic_top if params.block&.name
|
|
3301
3707
|
locals
|
|
3302
3708
|
end
|
|
3303
3709
|
|
|
3304
|
-
# Trailing required positionals after a rest (`def f(a, *m, z)`) shift the correspondence
|
|
3305
|
-
#
|
|
3710
|
+
# Trailing required positionals after a rest (`def f(a, *m, z)`) shift the correspondence and stay
|
|
3711
|
+
# declined — correspondence, not width, is the issue. Issue #1125: `def m(...)`'s forwarding slot is no
|
|
3712
|
+
# longer a decline. Its NAMED parameters (a leading `def m(a, ...)` required) bind exactly as before;
|
|
3713
|
+
# the forwarded tail is not a binding, so nothing is bound for it — the body's own `f(...)` reads the
|
|
3714
|
+
# frame's argument list instead ({#forwarded_argument_types}).
|
|
3306
3715
|
def bindable_param_shape?(params)
|
|
3307
|
-
params.is_a?(Prism::ParametersNode) &&
|
|
3308
|
-
|
|
3309
|
-
|
|
3716
|
+
params.is_a?(Prism::ParametersNode) && params.posts.empty?
|
|
3717
|
+
end
|
|
3718
|
+
|
|
3719
|
+
# Whether the def's trailing slot is `...` rather than a named `*rest` / `**rest`. Such a def accepts
|
|
3720
|
+
# an unbounded positional tail it does not name, so it behaves as a rest for the positional
|
|
3721
|
+
# correspondence and binds no local.
|
|
3722
|
+
def forwarding_params?(params)
|
|
3723
|
+
params.keyword_rest.is_a?(Prism::ForwardingParameterNode)
|
|
3724
|
+
end
|
|
3725
|
+
|
|
3726
|
+
# A `**rest` parameter's name, or nil when the slot is `def m(...)`'s forwarding parameter —
|
|
3727
|
+
# `Prism::ForwardingParameterNode` carries no name at all.
|
|
3728
|
+
def keyword_rest_name(params)
|
|
3729
|
+
rest = params.keyword_rest
|
|
3730
|
+
return nil unless rest.respond_to?(:name)
|
|
3731
|
+
|
|
3732
|
+
rest.name
|
|
3310
3733
|
end
|
|
3311
3734
|
|
|
3312
3735
|
def takes_keywords?(params)
|
|
@@ -3317,7 +3740,9 @@ module Rigor
|
|
|
3317
3740
|
requireds = params.requireds
|
|
3318
3741
|
optionals = params.optionals
|
|
3319
3742
|
return nil if positional.size < requireds.size
|
|
3320
|
-
|
|
3743
|
+
|
|
3744
|
+
extra = positional.size - requireds.size - optionals.size
|
|
3745
|
+
return nil if extra.positive? && params.rest.nil? && !forwarding_params?(params)
|
|
3321
3746
|
|
|
3322
3747
|
locals = {}
|
|
3323
3748
|
requireds.each_with_index { |param, index| bind_positional_param(locals, param, positional[index]) }
|
|
@@ -3361,6 +3786,23 @@ module Rigor
|
|
|
3361
3786
|
param.respond_to?(:value) && param.value ? literal_default_type(param.value) : dynamic_top
|
|
3362
3787
|
end
|
|
3363
3788
|
|
|
3789
|
+
# Issue #1125 — `**rest` collects the keyword-shape entries no named keyword parameter consumed, as a
|
|
3790
|
+
# closed `HashShape` of their value types. The pre-#1125 `Dynamic[top]` is kept whenever there is
|
|
3791
|
+
# nothing left to collect: no keyword shape at all, an open shape (unknown extras — every remaining-key
|
|
3792
|
+
# answer would be a guess), or a shape whose every pair a named parameter consumed. That last case is
|
|
3793
|
+
# what leaves the literal form's existing binding untouched (`target(a: 1, b: 2)` against
|
|
3794
|
+
# `def target(a:, b:, **rest)` still binds `rest` to `Dynamic[top]`), while a caller that DOES leave
|
|
3795
|
+
# keys over — the only shape where the parameter holds something to report — gets them typed.
|
|
3796
|
+
def keyword_rest_type(params, kw_shape)
|
|
3797
|
+
return dynamic_top if kw_shape.nil? || kw_shape.open?
|
|
3798
|
+
|
|
3799
|
+
consumed = params.keywords.map { |param| param.name.to_s.delete_suffix(":").to_sym }
|
|
3800
|
+
leftovers = kw_shape.pairs.except(*consumed)
|
|
3801
|
+
return dynamic_top if leftovers.empty?
|
|
3802
|
+
|
|
3803
|
+
Type::Combinator.hash_shape_of(leftovers)
|
|
3804
|
+
end
|
|
3805
|
+
|
|
3364
3806
|
# A default expression contributes its type only when it is lexically scope-free — a scalar literal
|
|
3365
3807
|
# or an EMPTY collection literal (`options = {}` is the dominant Rails idiom). Anything that could
|
|
3366
3808
|
# read the def's own lexical scope binds `Dynamic` instead of being mis-typed in the caller's scope.
|
|
@@ -3405,11 +3847,60 @@ module Rigor
|
|
|
3405
3847
|
scope.environment.nominal_for_name("Object") || dynamic_top
|
|
3406
3848
|
end
|
|
3407
3849
|
|
|
3850
|
+
# Issue #1125 — two call-site argument shapes that used to reach every consumer (the parameter
|
|
3851
|
+
# binder, RBS dispatch, the block-parameter reader) as an opaque `Dynamic[top]` / bare `Nominal[Hash]`
|
|
3852
|
+
# now carry what the callee needs:
|
|
3853
|
+
#
|
|
3854
|
+
# - `f(...)` inside `def m(...)` re-supplies the arguments `m` was called with, read from the frame
|
|
3855
|
+
# {#infer_user_method_return} installed ({FORWARDED_ARGS_KEY}) — so it expands to a whole LIST, not
|
|
3856
|
+
# one type, which is why the map became a flat_map.
|
|
3857
|
+
# - a keyword hash built ENTIRELY from a double splat (`f(**h)`) is the shape of `h`, so `f(**h)`
|
|
3858
|
+
# binds the same parameters the literal form `f(a: 1, b: 2)` does.
|
|
3859
|
+
#
|
|
3860
|
+
# Both are precision-only. A `...` outside a forwarding frame (unreachable in valid Ruby) and a double
|
|
3861
|
+
# splat of anything but a Symbol-keyed closed `HashShape` — a shapeless `Hash[Symbol, V]`, an opaque
|
|
3862
|
+
# value, a mixed hash — fall back to exactly the pre-#1125 answer.
|
|
3408
3863
|
def call_arg_types(node)
|
|
3409
3864
|
arguments_node = node.arguments
|
|
3410
3865
|
return [] if arguments_node.nil?
|
|
3411
3866
|
|
|
3412
|
-
arguments_node.arguments.
|
|
3867
|
+
arguments_node.arguments.flat_map do |argument|
|
|
3868
|
+
forwarded_argument_types(argument) || [call_arg_type(argument)]
|
|
3869
|
+
end
|
|
3870
|
+
end
|
|
3871
|
+
|
|
3872
|
+
# The frame's argument list when `argument` is `...`, else nil so the caller types it normally.
|
|
3873
|
+
def forwarded_argument_types(argument)
|
|
3874
|
+
return nil unless argument.is_a?(Prism::ForwardingArgumentsNode)
|
|
3875
|
+
|
|
3876
|
+
ExpressionTyper.current_forwarded_arg_types
|
|
3877
|
+
end
|
|
3878
|
+
|
|
3879
|
+
# The type a single call argument contributes; a keyword-hash argument first offers the `HashShape` it
|
|
3880
|
+
# stands for, and otherwise keeps its own type.
|
|
3881
|
+
def call_arg_type(argument)
|
|
3882
|
+
return type_of(argument) unless argument.is_a?(Prism::KeywordHashNode)
|
|
3883
|
+
|
|
3884
|
+
double_splat_hash_shape(argument) || type_of(argument)
|
|
3885
|
+
end
|
|
3886
|
+
|
|
3887
|
+
# The `HashShape` a `f(**h)` keyword-hash argument stands for, or nil to keep the argument's own type.
|
|
3888
|
+
# Declines a MIXED hash (`f(a: 1, **h)` — merging the literal pairs with the splatted shape is a second
|
|
3889
|
+
# shape algebra this slice does not need), an OPEN shape (unknown extras make every missing-keyword
|
|
3890
|
+
# answer a guess rather than a read), a shape with a non-Symbol key (Ruby itself rejects those as
|
|
3891
|
+
# keywords), and a splat of anything that is not a shape at all.
|
|
3892
|
+
def double_splat_hash_shape(node)
|
|
3893
|
+
elements = node.elements
|
|
3894
|
+
return nil unless elements.size == 1
|
|
3895
|
+
|
|
3896
|
+
splat = elements.first
|
|
3897
|
+
return nil unless splat.is_a?(Prism::AssocSplatNode) && splat.value
|
|
3898
|
+
|
|
3899
|
+
type = type_of(splat.value)
|
|
3900
|
+
return nil unless type.is_a?(Type::HashShape)
|
|
3901
|
+
return nil unless type.closed? && type.pairs.each_key.all?(Symbol)
|
|
3902
|
+
|
|
3903
|
+
type
|
|
3413
3904
|
end
|
|
3414
3905
|
|
|
3415
3906
|
# When the call carries a `Prism::BlockNode`, build the block's entry scope (outer locals plus
|
|
@@ -3431,23 +3922,44 @@ module Rigor
|
|
|
3431
3922
|
return nil if block_arg.nil?
|
|
3432
3923
|
return nil if receiver_type.nil?
|
|
3433
3924
|
|
|
3925
|
+
rebound = rebound_operand_typer(call_node)
|
|
3926
|
+
return rebound.send(:block_return_type_for, call_node, receiver_type, arg_types) if rebound
|
|
3927
|
+
|
|
3434
3928
|
expected = MethodDispatcher.expected_block_param_types(
|
|
3435
3929
|
receiver_type: receiver_type,
|
|
3436
3930
|
method_name: call_node.name,
|
|
3437
3931
|
arg_types: arg_types,
|
|
3438
|
-
environment: scope.environment
|
|
3932
|
+
environment: scope.environment,
|
|
3933
|
+
scope: scope
|
|
3439
3934
|
)
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3935
|
+
block_return_for(
|
|
3936
|
+
block_arg, expected,
|
|
3937
|
+
call_node: call_node,
|
|
3938
|
+
narrowed_self_type: block_body_self_narrowing(call_node, receiver_type),
|
|
3939
|
+
repeats: !BLOCK_VALUE_DISCARDING.include?(call_node.name) && block_may_repeat?(call_node, receiver_type)
|
|
3445
3940
|
)
|
|
3446
|
-
block_return_for(block_arg, expected, narrowed_self_type: narrowed_self)
|
|
3447
3941
|
rescue StandardError
|
|
3448
3942
|
nil
|
|
3449
3943
|
end
|
|
3450
3944
|
|
|
3945
|
+
# The catalogued iterators that discard their block's value — it selects the block-bearing overload and
|
|
3946
|
+
# nothing more — so the value pass lays no captured binding under their block: the fixpoint would buy a
|
|
3947
|
+
# type nothing reads, and `sum = 0; xs.each { |x| sum += x }` would pay it on top of the ADR-56
|
|
3948
|
+
# write-back's own. The #853 break-arm scan still lays it, since a `break` value is the call's.
|
|
3949
|
+
BLOCK_VALUE_DISCARDING = Set[
|
|
3950
|
+
:each, :each_with_index, :each_with_object, :each_pair, :each_key, :each_value, :each_index,
|
|
3951
|
+
:reverse_each, :each_entry, :each_slice, :each_cons, :each_char, :each_byte, :each_line,
|
|
3952
|
+
:each_codepoint, :times, :upto, :downto, :step
|
|
3953
|
+
].freeze
|
|
3954
|
+
private_constant :BLOCK_VALUE_DISCARDING
|
|
3955
|
+
|
|
3956
|
+
# Whether the call may run its block more than once, so a later run reads a captured binding an earlier
|
|
3957
|
+
# run rebound — the premise of laying the #587 (b) binding ({#captured_block_bindings}) under the block.
|
|
3958
|
+
# {BlockRepetition.may_repeat?} holds the rule, which the statement pass shares (issue #1412).
|
|
3959
|
+
def block_may_repeat?(call_node, receiver_type)
|
|
3960
|
+
BlockRepetition.may_repeat?(method_name: call_node.name, receiver_type: receiver_type, scope: scope)
|
|
3961
|
+
end
|
|
3962
|
+
|
|
3451
3963
|
EMPTY_BREAK_ARMS = [].freeze
|
|
3452
3964
|
private_constant :EMPTY_BREAK_ARMS
|
|
3453
3965
|
|
|
@@ -3459,13 +3971,15 @@ module Rigor
|
|
|
3459
3971
|
# nested block, lambda, `def`, or loop targets THAT construct instead; {JUMP_BOUNDARY_NODES} stops the
|
|
3460
3972
|
# scan there, and the identity filter drops the ones the sink still collects because the nested
|
|
3461
3973
|
# construct is walked under the same installation. `break` with no argument carries nil, so the call
|
|
3462
|
-
# becomes optional — which is what Ruby does.
|
|
3974
|
+
# becomes optional — which is what Ruby does. A call that only stores its block ({StoredBlockCall}: `lambda`,
|
|
3975
|
+
# `define_method`, `Thread.new`, …) never runs it, so no arm of that block is the call's value.
|
|
3463
3976
|
#
|
|
3464
3977
|
# A failure yields no arms rather than propagating, matching {#block_return_type_for}: a call typed
|
|
3465
3978
|
# without its break arms is the pre-#853 answer, while a raise here would take out the whole call.
|
|
3466
3979
|
def call_break_arm_types(node, receiver_override: nil)
|
|
3467
3980
|
block_node = node.block
|
|
3468
3981
|
return EMPTY_BREAK_ARMS unless block_node.is_a?(Prism::BlockNode)
|
|
3982
|
+
return EMPTY_BREAK_ARMS if StoredBlockCall.stores_block?(node)
|
|
3469
3983
|
|
|
3470
3984
|
body = block_node.body
|
|
3471
3985
|
return EMPTY_BREAK_ARMS if body.nil? || !block_level_jump?(body, Prism::BreakNode)
|
|
@@ -3484,11 +3998,12 @@ module Rigor
|
|
|
3484
3998
|
receiver = receiver_override || call_receiver_type_for(call_node)
|
|
3485
3999
|
return EMPTY_BREAK_ARMS if receiver.nil?
|
|
3486
4000
|
|
|
3487
|
-
|
|
3488
|
-
scope: scope, call_node: call_node, receiver_type: receiver
|
|
3489
|
-
)
|
|
4001
|
+
param_types = break_arm_param_types(call_node, receiver)
|
|
3490
4002
|
block_scope = block_entry_scope(
|
|
3491
|
-
block_node,
|
|
4003
|
+
block_node, param_types,
|
|
4004
|
+
call_node: call_node,
|
|
4005
|
+
narrowed_self_type: block_body_self_narrowing(call_node, receiver),
|
|
4006
|
+
captured: repeating_captured_bindings(block_node, param_types, block_may_repeat?(call_node, receiver))
|
|
3492
4007
|
)
|
|
3493
4008
|
_result, collected = StatementEvaluator.with_break_value_sink do
|
|
3494
4009
|
without_block_body_threading { block_scope.evaluate(body) }
|
|
@@ -3496,39 +4011,78 @@ module Rigor
|
|
|
3496
4011
|
collected.filter_map { |jump, type| type if targets.key?(jump) }
|
|
3497
4012
|
end
|
|
3498
4013
|
|
|
4014
|
+
# The block body's narrowed `self_type`, or `nil` to leave the scope contract unchanged.
|
|
4015
|
+
#
|
|
4016
|
+
# ADR-16 Tier A: a registered plugin's `block_as_methods` entry matching `(receiver_type,
|
|
4017
|
+
# call_node.name)` narrows to the receiver class's instance type.
|
|
4018
|
+
#
|
|
4019
|
+
# Issue #963: `define_method(:name) { ... }` installs its block as an instance method and runs it with
|
|
4020
|
+
# `self` bound to the receiving instance, so the block body's `self` is the INSTANCE side of a class body's
|
|
4021
|
+
# `Singleton[X]`. Both block-entry paths narrow it, and both decline on the same `class << ...` bodies,
|
|
4022
|
+
# because the whole distinction rides on `Scope#singleton_class_body?` rather than on the statement
|
|
4023
|
+
# evaluator's frame stack. This pass therefore cannot compute a carrier the evaluator disagrees with — it
|
|
4024
|
+
# matters wherever the block's value is observable, e.g. a project-declared generic `define_method`
|
|
4025
|
+
# signature that returns the block's own type.
|
|
4026
|
+
def block_body_self_narrowing(call_node, receiver_type)
|
|
4027
|
+
MacroBlockSelfType.narrow_self_type_for(
|
|
4028
|
+
scope: scope, call_node: call_node, receiver_type: receiver_type
|
|
4029
|
+
) || DefineMethodBlockSelf.narrow_self_type_for(scope: scope, call_node: call_node)
|
|
4030
|
+
end
|
|
4031
|
+
|
|
3499
4032
|
def break_arm_param_types(call_node, receiver)
|
|
3500
4033
|
MethodDispatcher.expected_block_param_types(
|
|
3501
4034
|
receiver_type: receiver,
|
|
3502
4035
|
method_name: call_node.name,
|
|
3503
4036
|
arg_types: call_arg_types(call_node),
|
|
3504
|
-
environment: scope.environment
|
|
4037
|
+
environment: scope.environment,
|
|
4038
|
+
scope: scope
|
|
3505
4039
|
)
|
|
3506
4040
|
end
|
|
3507
4041
|
|
|
3508
|
-
def block_return_for(block_arg, expected, narrowed_self_type: nil)
|
|
4042
|
+
def block_return_for(block_arg, expected, call_node: nil, narrowed_self_type: nil, repeats: false)
|
|
3509
4043
|
case block_arg
|
|
3510
4044
|
when Prism::BlockNode
|
|
3511
|
-
|
|
4045
|
+
captured = repeating_captured_bindings(block_arg, expected, repeats)
|
|
4046
|
+
entry = block_entry_scope(
|
|
4047
|
+
block_arg, expected, call_node: call_node, narrowed_self_type: narrowed_self_type, captured: captured
|
|
4048
|
+
)
|
|
4049
|
+
type_block_body(block_arg, entry, captured: captured)
|
|
3512
4050
|
when Prism::BlockArgumentNode
|
|
3513
4051
|
symbol_block_return_type(block_arg, expected)
|
|
3514
4052
|
end
|
|
3515
4053
|
end
|
|
3516
4054
|
|
|
3517
4055
|
# The scope a block body is typed under: the surrounding scope plus the parameter bindings the receiving
|
|
3518
|
-
# method's signature implies
|
|
4056
|
+
# method's signature implies, with the #587 (b) `captured` binding ({#repeating_captured_bindings}) laid
|
|
4057
|
+
# between the two, so the body reads what a captured local, instance variable, class variable or global
|
|
4058
|
+
# holds on ANY run rather than the first.
|
|
3519
4059
|
#
|
|
3520
4060
|
# 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
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
4061
|
+
# yielding method's business, so the return-typing pass must see the same unmodelled-self mark. Issue #1358
|
|
4062
|
+
# — and the same match-global view ({MatchRebinding.block_entry}), which reads the owning `call_node`.
|
|
4063
|
+
def block_entry_scope(block_node, expected, call_node: nil, narrowed_self_type: nil, captured: nil)
|
|
4064
|
+
entry = MatchRebinding.block_entry(scope.entering_opaque_block, block_node, call_node)
|
|
4065
|
+
entry = captured.lay(entry) if captured
|
|
4066
|
+
block_scope = BlockParameterBinder.new(expected_param_types: expected).bind_onto(block_node, entry)
|
|
3527
4067
|
return block_scope unless narrowed_self_type
|
|
3528
4068
|
|
|
3529
4069
|
block_scope.with_self_type(narrowed_self_type)
|
|
3530
4070
|
end
|
|
3531
4071
|
|
|
4072
|
+
# The #587 (b) captured binding when the call may run the block more than once ({#block_may_repeat?}), and
|
|
4073
|
+
# nil — no binding — for a block it runs at most once, whose captures no earlier run can have moved.
|
|
4074
|
+
def repeating_captured_bindings(block_node, expected, repeats)
|
|
4075
|
+
repeats ? generic_captured_bindings(block_node, expected) : nil
|
|
4076
|
+
end
|
|
4077
|
+
|
|
4078
|
+
# The #587 (b) captured binding for the generic pass — or nil, which keeps the entry scope, when computing
|
|
4079
|
+
# it fails: a raise here would otherwise take out the whole block-return pass.
|
|
4080
|
+
def generic_captured_bindings(block_node, expected)
|
|
4081
|
+
captured_block_bindings(block_node, expected)
|
|
4082
|
+
rescue StandardError
|
|
4083
|
+
nil
|
|
4084
|
+
end
|
|
4085
|
+
|
|
3532
4086
|
# `&:symbol` desugars to a one-arg Proc that dispatches `symbol` against its argument. When the param
|
|
3533
4087
|
# type is known and the resulting inner dispatch is precise, this returns the precise carrier;
|
|
3534
4088
|
# otherwise it returns `Dynamic[Top]` (still non-nil) so the outer dispatcher selects the
|
|
@@ -3568,14 +4122,114 @@ module Rigor
|
|
|
3568
4122
|
# join, `ops.all? { |o| next false unless o; true }` read as `Constant[true]` and
|
|
3569
4123
|
# {MethodDispatcher::BlockFolding} folded the call to always-truthy on a program that really can answer
|
|
3570
4124
|
# false — a warning on correct code.
|
|
3571
|
-
|
|
4125
|
+
#
|
|
4126
|
+
# `captured` is the #587 (b) binding `block_scope` was laid over, if any; the names it answers are left to it
|
|
4127
|
+
# ({#tail_only_block_body_type}).
|
|
4128
|
+
def type_block_body(block_node, block_scope, captured: nil)
|
|
3572
4129
|
body = block_node.body
|
|
3573
4130
|
return Type::Combinator.constant_of(nil) if body.nil?
|
|
3574
4131
|
|
|
3575
4132
|
arms = block_level_next_arms(body)
|
|
3576
4133
|
return block_body_type_joining_nexts(body, block_scope, arms) if arms
|
|
3577
4134
|
|
|
3578
|
-
threaded_block_body_type(body, block_scope) ||
|
|
4135
|
+
threaded_block_body_type(body, block_scope) || tail_only_block_body_type(body, block_scope, captured)
|
|
4136
|
+
end
|
|
4137
|
+
|
|
4138
|
+
# The tail typed in the entry scope — except under {#block_body_threading_suppressed?}, where a tail that
|
|
4139
|
+
# reads what its own prefix changed would get the ENTRY binding back, a stale answer rather than a wider
|
|
4140
|
+
# one. There the tail is typed over {#prefix_answered_scope}, which re-answers exactly those names without
|
|
4141
|
+
# evaluating the prefix. The threading's other declines keep the plain tail-only answer: a body with no
|
|
4142
|
+
# prefix, or whose tail ignores it, is not stale, while a `rescue` body and a prefix that can `break`
|
|
4143
|
+
# ({JUMP_NODES}) are typed tail-only with or without the suppression, and neither path re-answers them.
|
|
4144
|
+
#
|
|
4145
|
+
# A failure in the re-answer falls back to the plain tail-only answer, for the reason
|
|
4146
|
+
# {#threaded_block_body_type} gives: a raise reaching `block_return_type_for` would report "no block".
|
|
4147
|
+
def tail_only_block_body_type(body, block_scope, captured)
|
|
4148
|
+
return block_scope.type_of(body) unless block_body_threading_suppressed? && body.is_a?(Prism::StatementsNode)
|
|
4149
|
+
|
|
4150
|
+
answered =
|
|
4151
|
+
begin
|
|
4152
|
+
prefix_answered_scope(body.body, block_scope, captured)
|
|
4153
|
+
rescue StandardError
|
|
4154
|
+
block_scope
|
|
4155
|
+
end
|
|
4156
|
+
answered.type_of(body)
|
|
4157
|
+
end
|
|
4158
|
+
|
|
4159
|
+
# `block_scope` with every name the tail reads and the prefix changed ({#tail_dependent_body_names}) bound to
|
|
4160
|
+
# what the prefix can have left in it, answered without evaluating the prefix — which is the cost the
|
|
4161
|
+
# suppression refuses — on the terms the per-element fold's captured binding answers a capture under the same
|
|
4162
|
+
# suppression:
|
|
4163
|
+
#
|
|
4164
|
+
# - a name the prefix REBINDS reads `Dynamic[top]` ({#captured_floor}'s answer). `i += w; i` inside a block
|
|
4165
|
+
# the call runs once read `i` at its entry `0`, and `k == 0` then fired always-truthy on a `k` Ruby holds
|
|
4166
|
+
# as `1`.
|
|
4167
|
+
# - a name the prefix only MUTATES IN PLACE reads its unknown-store widening ({UnknownStoreWidening.widen}),
|
|
4168
|
+
# the binding {#stored_capture_bindings} lays: `|_k, a| a << w; a` over `{ x: [], y: [] }` read `a` as its
|
|
4169
|
+
# entry `[]` and the call as `Array[[]]`; it now reads `Array[Array[Dynamic[top]]]`.
|
|
4170
|
+
#
|
|
4171
|
+
# That is a floor per NAME, not per block, so the structure around a floored name survives (`e = e.to_s; [e, w]`
|
|
4172
|
+
# keeps its Tuple), and it is limited to the names tail-only answers stale. A name the entry scope does not bind —
|
|
4173
|
+
# a body-local, or an instance variable, class variable or global nothing bound yet — already reads
|
|
4174
|
+
# `Dynamic[top]`. An instance variable on its ADR-58 class-wide seed takes no floor for a rebind, since the seed
|
|
4175
|
+
# is the union of every write in the class, this prefix's included, though its mutation sites still widen it
|
|
4176
|
+
# ({#class_seeded_ivar?}). A name whose widening declines keeps its entry binding, because the threaded body
|
|
4177
|
+
# would have kept it as well: `s = String.new; … { s << "x"; s }` is `String` either way, and a precise nominal
|
|
4178
|
+
# `Array[String]` is a claim the widening may not grow on either path. And a name the #587 (b) `captured` binding
|
|
4179
|
+
# answers is left to it, as {#unanswered_tail_dependency?} leaves it: the per-element fold computes that binding
|
|
4180
|
+
# before it suppresses the threading above its cap, so it can hold the fixpoint's converged `Integer` for
|
|
4181
|
+
# `total += e; total`, which a floor here would throw away.
|
|
4182
|
+
def prefix_answered_scope(statements, block_scope, captured)
|
|
4183
|
+
return block_scope if statements.size < 2
|
|
4184
|
+
|
|
4185
|
+
names = tail_dependent_body_names(statements) - (captured&.names || EMPTY_NAME_SET)
|
|
4186
|
+
return block_scope if names.empty?
|
|
4187
|
+
|
|
4188
|
+
rebound, sites = prefix_changes(statements)
|
|
4189
|
+
names.reduce(block_scope) do |acc, name|
|
|
4190
|
+
answer = prefix_left_binding(block_scope, name, rebound, sites)
|
|
4191
|
+
answer ? CapturedLocals.bind(acc, name, answer) : acc
|
|
4192
|
+
end
|
|
4193
|
+
end
|
|
4194
|
+
|
|
4195
|
+
def prefix_left_binding(block_scope, name, rebound, sites)
|
|
4196
|
+
entry = CapturedLocals.bound_type(block_scope, name)
|
|
4197
|
+
return nil if entry.nil?
|
|
4198
|
+
|
|
4199
|
+
return Type::Combinator.untyped if rebound.include?(name) && !class_seeded_ivar?(block_scope, name)
|
|
4200
|
+
|
|
4201
|
+
widened = UnknownStoreWidening.widen(entry, sites.fetch(name, NO_MUTATION_SITES))
|
|
4202
|
+
widened == entry ? nil : widened
|
|
4203
|
+
end
|
|
4204
|
+
|
|
4205
|
+
# An instance variable still on its ADR-58 class-wide seed: the union of every WRITE in the class, so a rebind
|
|
4206
|
+
# in the prefix is already in it and needs no floor. An in-place mutation is no write — `@out << w.to_s` leaves
|
|
4207
|
+
# the seed `"k"` while the object holds `"k1"` — so the mutation sites still widen the seed, as they widen any
|
|
4208
|
+
# other entry binding.
|
|
4209
|
+
def class_seeded_ivar?(block_scope, name)
|
|
4210
|
+
CapturedLocals.ivar_name?(name) && block_scope.declaration_sourced?(:ivar, name)
|
|
4211
|
+
end
|
|
4212
|
+
|
|
4213
|
+
NO_MUTATION_SITES = [].freeze
|
|
4214
|
+
private_constant :NO_MUTATION_SITES
|
|
4215
|
+
|
|
4216
|
+
# The prefix's changes split the way {#prefix_left_binding} answers them: the names a write node rebinds,
|
|
4217
|
+
# and each in-place mutation site filed under every variable its receiver can evaluate to. The walk and both
|
|
4218
|
+
# predicates are {#prefix_statement_jump_free?}'s, so a name {#tail_dependent_body_names} reports is always
|
|
4219
|
+
# filed here under one of the two. Only a suppressed tail-only body with a dependent tail pays it.
|
|
4220
|
+
def prefix_changes(statements)
|
|
4221
|
+
rebound = Set.new
|
|
4222
|
+
sites = {}
|
|
4223
|
+
statements[0...-1].each do |statement|
|
|
4224
|
+
Source::NodeWalker.each_with_ancestors(statement) do |node, ancestors|
|
|
4225
|
+
rebound << node.name if VARIABLE_WRITE_NODES.include?(node.class)
|
|
4226
|
+
next unless in_place_mutation?(node)
|
|
4227
|
+
|
|
4228
|
+
nested = ancestors.any? { |ancestor| CLOSURE_NODES.include?(ancestor.class) }
|
|
4229
|
+
each_mutated_name(node, nested) { |name| (sites[name] ||= []) << node }
|
|
4230
|
+
end
|
|
4231
|
+
end
|
|
4232
|
+
[rebound, sites]
|
|
3579
4233
|
end
|
|
3580
4234
|
|
|
3581
4235
|
# Evaluates the body once under a `next` sink and joins the arms that leave THIS block with the
|
|
@@ -3616,39 +4270,14 @@ module Rigor
|
|
|
3616
4270
|
block_level_jump_nodes(body, Prism::NextNode)
|
|
3617
4271
|
end
|
|
3618
4272
|
|
|
3619
|
-
# True when a `klass` jump is reachable from `node` without crossing a construct that retargets it
|
|
3620
|
-
#
|
|
3621
|
-
|
|
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)
|
|
4273
|
+
# True when a `klass` jump is reachable from `node` without crossing a construct that retargets it
|
|
4274
|
+
# ({JumpTargets.any?}, allocation-free and early-exiting: this is the scan every block body pays).
|
|
4275
|
+
def block_level_jump?(node, klass) = JumpTargets.any?(node, klass)
|
|
3648
4276
|
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
4277
|
+
# The identity-keyed set of `klass` jumps that target THIS block ({JumpTargets.of}). The sinks in
|
|
4278
|
+
# `StatementEvaluator` also collect jumps from nested blocks / loops / defs evaluated under the same
|
|
4279
|
+
# installation, so the consumer filters against this set by node identity.
|
|
4280
|
+
def block_level_jump_nodes(body, klass) = JumpTargets.of(body, klass)
|
|
3652
4281
|
|
|
3653
4282
|
# Re-typing the whole body would be wrong to do unconditionally: this path runs for EVERY block-bearing
|
|
3654
4283
|
# call, and the statements ahead of the tail are pure cost whenever the tail does not depend on them.
|
|
@@ -3663,8 +4292,11 @@ module Rigor
|
|
|
3663
4292
|
# - the fold is not re-entrant. `StatementEvaluator#eval_call` already evaluates each nested block body
|
|
3664
4293
|
# once, plus up to three more times under the ADR-56 `BodyFixpoint` when the block rebinds a captured
|
|
3665
4294
|
# local, so a fold nested inside a fold would multiply that work per block-nesting level. Inside a
|
|
3666
|
-
# threaded body a nested block-bearing call reverts to the tail-only path
|
|
3667
|
-
#
|
|
4295
|
+
# threaded body a nested block-bearing call reverts to the tail-only path. That is a wider answer only
|
|
4296
|
+
# while the nested tail ignores its own prefix: a tail reading a parameter or captured local the prefix
|
|
4297
|
+
# mutated gets the ENTRY binding back, so the per-element and per-pair folds floor that shape
|
|
4298
|
+
# ({#tail_only_body_floor}), and the generic block-return pass re-answers each stale name on its own
|
|
4299
|
+
# ({#prefix_answered_scope}).
|
|
3668
4300
|
#
|
|
3669
4301
|
# ADR-56 interaction: the fold cannot double-apply or fight the captured-local write-back. That
|
|
3670
4302
|
# write-back is `StatementEvaluator#write_back_block_captures`, computed from the CALLER's scope into
|
|
@@ -3728,10 +4360,11 @@ module Rigor
|
|
|
3728
4360
|
private_constant :VARIABLE_WRITE_NODES
|
|
3729
4361
|
|
|
3730
4362
|
# Every node that OBSERVES a variable binding: the plain reads plus the compound writes, which read
|
|
3731
|
-
# their target before rebinding it (`v += 1` in the tail depends on an earlier `v = 0`).
|
|
4363
|
+
# their target before rebinding it (`v += 1` in the tail depends on an earlier `v = 0`). An `it` read
|
|
4364
|
+
# observes the local `:it` ({ReceiverAlias.read_name}); it has no `name` of its own.
|
|
3732
4365
|
VARIABLE_READ_NODES = (
|
|
3733
4366
|
VARIABLE_WRITE_NODES | [
|
|
3734
|
-
Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode,
|
|
4367
|
+
Prism::LocalVariableReadNode, Prism::ItLocalVariableReadNode, Prism::InstanceVariableReadNode,
|
|
3735
4368
|
Prism::ClassVariableReadNode, Prism::GlobalVariableReadNode
|
|
3736
4369
|
]
|
|
3737
4370
|
).freeze
|
|
@@ -3758,11 +4391,9 @@ module Rigor
|
|
|
3758
4391
|
# nothing about our block's value — it neither triggers the decline nor joins as an arm. A nested
|
|
3759
4392
|
# `BlockNode` / `LambdaNode` is the jump's own block (`do xs.each { next 1 }; v = 42; v end` threads
|
|
3760
4393
|
# 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.
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
Prism::WhileNode, Prism::UntilNode, Prism::ForNode
|
|
3765
|
-
].freeze
|
|
4394
|
+
# end` continues the loop); a `DefNode` body is a different method entirely. The set is
|
|
4395
|
+
# {JumpTargets::BOUNDARY_NODES}, shared with `StatementEvaluator`'s loop and block joins.
|
|
4396
|
+
JUMP_BOUNDARY_NODES = JumpTargets::BOUNDARY_NODES
|
|
3766
4397
|
private_constant :JUMP_BOUNDARY_NODES
|
|
3767
4398
|
|
|
3768
4399
|
# True when the tail statement observes a variable name one of the earlier statements binds OR mutates
|
|
@@ -3777,9 +4408,13 @@ module Rigor
|
|
|
3777
4408
|
# hands downstream rules a provably-empty array. Threading is the fix, not a cost: `StatementEvaluator`
|
|
3778
4409
|
# runs `MutationWidening.widen_after_call` on the `push`, so the threaded tail reads the widened
|
|
3779
4410
|
# `Array[…]`. A call therefore contributes every variable its receiver can evaluate to
|
|
3780
|
-
# ({ReceiverAlias.
|
|
3781
|
-
# is one the widening responds to ({MutationWidening::SHAPE_MUTATORS}); keying on the widening's own
|
|
3782
|
-
# tables is what keeps "the scan says thread" and "threading changes something" the
|
|
4411
|
+
# ({ReceiverAlias.mutated_reads} — the ternary-selected receiver of issue #277 included) whenever its
|
|
4412
|
+
# name is one the widening responds to ({MutationWidening::SHAPE_MUTATORS}); keying on the widening's own
|
|
4413
|
+
# tables and receiver answer is what keeps "the scan says thread" and "threading changes something" the
|
|
4414
|
+
# same predicate. The index-write nodes ({INDEX_WRITE_NODES}) store through `[]=` without being a call,
|
|
4415
|
+
# so a name-keyed scan missed them and `h[:a] += 1; h[:a]` kept the literal's `0`; they contribute their
|
|
4416
|
+
# receiver the same way. A scan that read only local and instance-variable receivers missed `$g << w; $g`
|
|
4417
|
+
# and `it << w; it` the same way, while the tail kept the entry `"k"` / `[]`.
|
|
3783
4418
|
#
|
|
3784
4419
|
# Cost is two walks of the body, the second only when the first found a write and no jump — the same
|
|
3785
4420
|
# order of cost `StatementEvaluator`'s own per-call captured-write scan already pays, and far below
|
|
@@ -3792,22 +4427,25 @@ module Rigor
|
|
|
3792
4427
|
return false if written.nil?
|
|
3793
4428
|
|
|
3794
4429
|
Source::NodeWalker.each(statements.last) do |node|
|
|
3795
|
-
return true if VARIABLE_READ_NODES.include?(node.class) && written.include?(node
|
|
4430
|
+
return true if VARIABLE_READ_NODES.include?(node.class) && written.include?(ReceiverAlias.read_name(node))
|
|
3796
4431
|
end
|
|
3797
4432
|
false
|
|
3798
4433
|
end
|
|
3799
4434
|
|
|
3800
4435
|
# WHICH names the predicate above answers YES on — every name the tail observes that the prefix binds
|
|
3801
|
-
# or mutates in place. Only the
|
|
4436
|
+
# or mutates in place. Only the tail-only floor ({#unanswered_tail_dependency?}) needs them, which is
|
|
3802
4437
|
# 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
|
|
4438
|
+
# body and short-circuits on the first hit, the floor runs only for a walk typed tail-only. Sharing
|
|
3804
4439
|
# {#prefix_written_names} is what keeps the two from drifting about what the prefix binds.
|
|
3805
4440
|
def tail_dependent_body_names(statements)
|
|
3806
4441
|
written = prefix_written_names(statements)
|
|
3807
4442
|
return EMPTY_NAME_SET if written.nil?
|
|
3808
4443
|
|
|
3809
4444
|
Source::NodeWalker.each(statements.last).filter_map do |node|
|
|
3810
|
-
|
|
4445
|
+
next unless VARIABLE_READ_NODES.include?(node.class)
|
|
4446
|
+
|
|
4447
|
+
name = ReceiverAlias.read_name(node)
|
|
4448
|
+
name if written.include?(name)
|
|
3811
4449
|
end.to_set
|
|
3812
4450
|
end
|
|
3813
4451
|
|
|
@@ -3816,7 +4454,7 @@ module Rigor
|
|
|
3816
4454
|
def prefix_written_names(statements)
|
|
3817
4455
|
written = Set.new
|
|
3818
4456
|
statements[0...-1].each do |statement|
|
|
3819
|
-
return nil unless prefix_statement_jump_free?(statement, written, false)
|
|
4457
|
+
return nil unless prefix_statement_jump_free?(statement, written, false, false)
|
|
3820
4458
|
end
|
|
3821
4459
|
return nil if written.empty?
|
|
3822
4460
|
|
|
@@ -3827,40 +4465,73 @@ module Rigor
|
|
|
3827
4465
|
private_constant :EMPTY_NAME_SET
|
|
3828
4466
|
|
|
3829
4467
|
# True when `node` cannot jump out of the block with a value, collecting into `written` the names it
|
|
3830
|
-
# binds (a variable-write node) or mutates in place (a {MutationWidening::SHAPE_MUTATORS} call
|
|
3831
|
-
# every variable its receiver can evaluate to) on the way down.
|
|
3832
|
-
# has passed a boundary
|
|
4468
|
+
# binds (a variable-write node) or mutates in place (a {MutationWidening::SHAPE_MUTATORS} call or an
|
|
4469
|
+
# {INDEX_WRITE_NODES} store, through every variable its receiver can evaluate to) on the way down.
|
|
4470
|
+
# `retargeted` is true once the descent has passed a boundary, and `nested` once it has passed a block or
|
|
4471
|
+
# lambda ({CLOSURE_NODES}), whose `it` is not the body's.
|
|
3833
4472
|
#
|
|
3834
4473
|
# A `Prism::DefinedNode`'s operand is never evaluated, so it is not descended into — the same rule
|
|
3835
4474
|
# {Source::NodeWalker} applies, for the same reason: neither a write nor a jump under `defined?` runs.
|
|
3836
|
-
def prefix_statement_jump_free?(node, written, retargeted)
|
|
4475
|
+
def prefix_statement_jump_free?(node, written, retargeted, nested)
|
|
3837
4476
|
return false if !retargeted && JUMP_NODES.include?(node.class)
|
|
3838
4477
|
|
|
3839
4478
|
written << node.name if VARIABLE_WRITE_NODES.include?(node.class)
|
|
3840
|
-
|
|
4479
|
+
each_mutated_name(node, nested) { |name| written << name } if in_place_mutation?(node)
|
|
3841
4480
|
return true if node.is_a?(Prism::DefinedNode)
|
|
3842
4481
|
|
|
3843
4482
|
child_retargeted = retargeted || JUMP_BOUNDARY_NODES.include?(node.class)
|
|
4483
|
+
child_nested = nested || CLOSURE_NODES.include?(node.class)
|
|
3844
4484
|
node.rigor_each_child do |child|
|
|
3845
|
-
return false unless prefix_statement_jump_free?(child, written, child_retargeted)
|
|
4485
|
+
return false unless prefix_statement_jump_free?(child, written, child_retargeted, child_nested)
|
|
3846
4486
|
end
|
|
3847
4487
|
true
|
|
3848
4488
|
end
|
|
3849
4489
|
|
|
3850
|
-
|
|
3851
|
-
|
|
4490
|
+
# The forms that store through `[]=` without being a `[]=` call ({IndexWriteWidening::CONTENT_WRITE_NODE_CLASSES}).
|
|
4491
|
+
# `StatementEvaluator` widens the three compound writes in straight-line code, and its captured-local
|
|
4492
|
+
# write-back widens all four when a nested block stores through one. A straight-line index TARGET widens
|
|
4493
|
+
# too, at the multi-assign, `for` index or `rescue =>` reference that owns it (`eval_multi_write`,
|
|
4494
|
+
# `bind_for_index`, `bind_rescue_reference`).
|
|
4495
|
+
INDEX_WRITE_NODES = Set.new(IndexWriteWidening::CONTENT_WRITE_NODE_CLASSES).freeze
|
|
4496
|
+
private_constant :INDEX_WRITE_NODES
|
|
4497
|
+
|
|
4498
|
+
def in_place_mutation?(node)
|
|
4499
|
+
return MutationWidening::SHAPE_MUTATORS.include?(node.name) if node.is_a?(Prism::CallNode)
|
|
4500
|
+
|
|
4501
|
+
INDEX_WRITE_NODES.include?(node.class)
|
|
4502
|
+
end
|
|
4503
|
+
|
|
4504
|
+
# Yields the name of every variable the in-place mutation `node` changes: its receiver's
|
|
4505
|
+
# {ReceiverAlias.mutated_reads}, the answer the straight-line widening the threaded body runs reads too, so a
|
|
4506
|
+
# global or class variable counts (`$g << w; $g`) as well as a local, an instance variable and the `it`
|
|
4507
|
+
# parameter. An `it` read `nested` under a block or lambda inside the prefix is that closure's own parameter,
|
|
4508
|
+
# never the body's `it`, so it names nothing here: `[[]].each { it << w }; it` leaves the body's `it` as it was.
|
|
4509
|
+
def each_mutated_name(node, nested)
|
|
4510
|
+
ReceiverAlias.mutated_reads(node.receiver).each do |read|
|
|
4511
|
+
next if nested && read.is_a?(Prism::ItLocalVariableReadNode)
|
|
3852
4512
|
|
|
3853
|
-
|
|
4513
|
+
yield ReceiverAlias.read_name(read)
|
|
4514
|
+
end
|
|
3854
4515
|
end
|
|
3855
4516
|
|
|
4517
|
+
# The closures whose `it` is their own: a nested block or lambda always binds `it` to its own parameter.
|
|
4518
|
+
CLOSURE_NODES = Set[Prism::BlockNode, Prism::LambdaNode].freeze
|
|
4519
|
+
private_constant :CLOSURE_NODES
|
|
4520
|
+
|
|
3856
4521
|
# v0.0.6 phase 2 — per-element block fold for Tuple receivers under `:map` / `:collect`. Walks every
|
|
3857
4522
|
# Tuple position, binds the block parameter to that element's type, and re-types the block body. The
|
|
3858
4523
|
# per-position results are assembled into `Tuple[U_1..U_n]`, strictly tighter than the RBS-projected
|
|
3859
4524
|
# `Array[union]`.
|
|
3860
4525
|
#
|
|
3861
4526
|
# Declines (returns nil) when the receiver is not a `Tuple` with at least one element, when the call
|
|
3862
|
-
# has no `Prism::BlockNode`, when the method is outside the supported set, when
|
|
3863
|
-
# mid-loop, or when the block has no body. The decline path leaves the
|
|
4527
|
+
# has no `Prism::BlockNode`, when the method is outside the supported set, when the call carries an
|
|
4528
|
+
# argument, when block typing raises mid-loop, or when the block has no body. The decline path leaves the
|
|
4529
|
+
# dispatch chain untouched.
|
|
4530
|
+
#
|
|
4531
|
+
# The walk reads only the block, so an argument declines: `index(value)` / `find_index(value)` search by
|
|
4532
|
+
# `==` without running the block, and `find(ifnone)` / `detect(ifnone)` answer `ifnone.call` when no
|
|
4533
|
+
# position matches. `r = [1, 2].find(-> { 0 }) { |e| e > 5 }` is `0` at runtime; the walk answered `nil`,
|
|
4534
|
+
# and `r + 1` then reported a nil receiver on correct code. The other supported methods take no argument.
|
|
3864
4535
|
PER_ELEMENT_TUPLE_METHODS = Set[
|
|
3865
4536
|
:map, :collect, :filter_map, :flat_map,
|
|
3866
4537
|
:select, :filter, :reject,
|
|
@@ -3874,6 +4545,9 @@ module Rigor
|
|
|
3874
4545
|
].freeze
|
|
3875
4546
|
private_constant :HASH_SHAPE_TRANSFORM_METHODS
|
|
3876
4547
|
|
|
4548
|
+
IN_PLACE_HASH_SHAPE_TRANSFORMS = Set[:transform_keys!, :transform_values!].freeze
|
|
4549
|
+
private_constant :IN_PLACE_HASH_SHAPE_TRANSFORMS
|
|
4550
|
+
|
|
3877
4551
|
# Cardinality cap for per-element block fold over finite-bound `Constant<Range>` receivers. Walking
|
|
3878
4552
|
# `(1..1_000_000).map { … }` element-wise would balloon block-typing cost and explode the resulting
|
|
3879
4553
|
# Tuple, so only short ranges expand into per-position folds. Larger ranges decline so the RBS tier
|
|
@@ -3883,7 +4557,7 @@ module Rigor
|
|
|
3883
4557
|
|
|
3884
4558
|
def try_per_element_block_fold(call_node, receiver_type)
|
|
3885
4559
|
return nil unless PER_ELEMENT_TUPLE_METHODS.include?(call_node.name)
|
|
3886
|
-
return nil
|
|
4560
|
+
return nil unless call_node.arguments.nil?
|
|
3887
4561
|
|
|
3888
4562
|
element_types = per_element_elements_of(receiver_type)
|
|
3889
4563
|
return nil if element_types.nil? || element_types.empty?
|
|
@@ -3892,12 +4566,13 @@ module Rigor
|
|
|
3892
4566
|
return nil if per_position.nil? || per_position.any?(&:nil?)
|
|
3893
4567
|
|
|
3894
4568
|
assemble_per_element_result(call_node.name, per_position, element_types) ||
|
|
3895
|
-
|
|
4569
|
+
undecided_fold_floor(call_node.name, element_types, receiver_type)
|
|
3896
4570
|
end
|
|
3897
4571
|
|
|
3898
|
-
# The honest answer for `find` / `detect` / `find_index` / `index`
|
|
3899
|
-
#
|
|
3900
|
-
#
|
|
4572
|
+
# The honest answer for the find family (`find` / `detect` / `find_index` / `index`) and the filter family
|
|
4573
|
+
# (`select` / `filter` / `reject`) when this fold walked every position and the assembler still could not
|
|
4574
|
+
# decide — which happens for exactly one reason: some position's predicate is not a `Constant`, so "the
|
|
4575
|
+
# first matching one" or "the ones kept" is not a static fact.
|
|
3901
4576
|
#
|
|
3902
4577
|
# Falling through to the dispatcher was WRONG for this family, and issue #617 residue (1) is the bill:
|
|
3903
4578
|
# `seen = 0; [1, 2].find do |e| seen += 1; seen == 2 end` answered `nil` where the runtime answers `2`.
|
|
@@ -3912,19 +4587,39 @@ module Rigor
|
|
|
3912
4587
|
# back an element, it does not compute one. `find_index` / `index` answer a position in the receiver, so
|
|
3913
4588
|
# `Integer?` is their floor.
|
|
3914
4589
|
#
|
|
3915
|
-
#
|
|
3916
|
-
#
|
|
3917
|
-
#
|
|
3918
|
-
#
|
|
3919
|
-
|
|
4590
|
+
# `select` / `filter` / `reject` take a floor for the same reason. Their fall-through is not merely
|
|
4591
|
+
# wider: `BlockFolding`'s filter folds DO answer on a `Constant` block, and the entry-scope pin hands
|
|
4592
|
+
# them one, so `seen = 0; [1, 2].select do |e| seen += 1; seen == 2 end` answered a provably-empty `[]`
|
|
4593
|
+
# where Ruby answers `[2]`. Nested inside a threaded body, where a position can be typed tail-only
|
|
4594
|
+
# ({#tail_only_body_floor}), `[[], []].select do |a| a << w; a.any? end` did the same. Their floor is an
|
|
4595
|
+
# Array of the receiver's own elements: which of them survive is undecided, and what they are is not.
|
|
4596
|
+
# A range receiver's elements are widened first ({#filter_family_floor}).
|
|
4597
|
+
#
|
|
4598
|
+
# Nothing else in {PER_ELEMENT_TUPLE_METHODS} takes a floor. `map`'s assembler cannot decline, and
|
|
4599
|
+
# `BlockFolding` never folds `filter_map` / `flat_map`, so their fall-through is the RBS `Array[U]`
|
|
4600
|
+
# projection.
|
|
4601
|
+
def undecided_fold_floor(method_name, element_types, receiver_type)
|
|
3920
4602
|
case method_name
|
|
3921
4603
|
when :find, :detect
|
|
3922
4604
|
Type::Combinator.union(*element_types, Type::Combinator.constant_of(nil))
|
|
4605
|
+
when :select, :filter, :reject
|
|
4606
|
+
filter_family_floor(element_types, receiver_type)
|
|
3923
4607
|
when :find_index, :index
|
|
3924
4608
|
Type::Combinator.union(Type::Combinator.nominal_of("Integer"), Type::Combinator.constant_of(nil))
|
|
3925
4609
|
end
|
|
3926
4610
|
end
|
|
3927
4611
|
|
|
4612
|
+
# A `Constant<Range>` receiver's elements are values this walk enumerated, not ones the program wrote, and
|
|
4613
|
+
# the RBS answer the floor replaces carried no pin. Kept, the pins would make `(1..4).filter { … }` an
|
|
4614
|
+
# `Array[1 | 2 | 3 | 4]` that a later `q << 9` cannot widen (#580 leaves a precise Array without a
|
|
4615
|
+
# gradual arm alone), so `q.last == 9` would fold always-falsey on correct code. A Tuple receiver's pins
|
|
4616
|
+
# are the literal's own and stay, as the RBS projection kept them.
|
|
4617
|
+
def filter_family_floor(element_types, receiver_type)
|
|
4618
|
+
element = Type::Combinator.union(*element_types)
|
|
4619
|
+
element = Type::Combinator.widen_value_pinned(element) if receiver_type.is_a?(Type::Constant)
|
|
4620
|
+
Type::Combinator.nominal_of("Array", type_args: [element])
|
|
4621
|
+
end
|
|
4622
|
+
|
|
3928
4623
|
# Evaluates the call's block once per receiver element. Two block shapes are supported:
|
|
3929
4624
|
#
|
|
3930
4625
|
# - `Prism::BlockNode` — a full `do … end` / `{ … }` block; the body is re-typed per position with the
|
|
@@ -3957,49 +4652,84 @@ module Rigor
|
|
|
3957
4652
|
def per_element_body_results(block, element_types)
|
|
3958
4653
|
captured = per_element_captured_bindings(block, element_types)
|
|
3959
4654
|
results = lambda do
|
|
3960
|
-
element_types.map
|
|
4655
|
+
element_types.each_with_index.map do |element_type, position|
|
|
4656
|
+
type_block_body_with_param(block, [element_type], captured: captured, position: position)
|
|
4657
|
+
end
|
|
3961
4658
|
end
|
|
3962
|
-
return results.call
|
|
3963
|
-
return
|
|
4659
|
+
return results.call unless tail_only_walk?(element_types)
|
|
4660
|
+
return tail_only_body_floor(element_types) if unanswered_tail_dependency?(block, captured)
|
|
3964
4661
|
|
|
3965
4662
|
without_block_body_threading(&results)
|
|
3966
4663
|
end
|
|
3967
4664
|
|
|
3968
|
-
#
|
|
3969
|
-
#
|
|
3970
|
-
#
|
|
4665
|
+
# A position is typed tail-only in two cases: above {PER_ELEMENT_THREADING_LIMIT}, where this walk
|
|
4666
|
+
# suppresses the threading itself, and anywhere the walk runs nested inside a body some other pass is
|
|
4667
|
+
# already evaluating whole. {#threaded_block_body_type}, the `next` join
|
|
4668
|
+
# ({#block_body_type_joining_nexts}), the break-arm collection ({#collect_break_arm_types}), the
|
|
4669
|
+
# captured-local fixpoint ({#captured_exit_bindings}) and an enclosing walk of this fold above the cap
|
|
4670
|
+
# all suppress the threading while they run, so a fold never re-enters. The second case has no arity in
|
|
4671
|
+
# it: `[[], []].map do |a| a << w; a end` inside a threaded `m.synchronize do w = v; … end` is typed
|
|
4672
|
+
# tail-only at two positions.
|
|
4673
|
+
def tail_only_walk?(element_types)
|
|
4674
|
+
element_types.size > PER_ELEMENT_THREADING_LIMIT || block_body_threading_suppressed?
|
|
4675
|
+
end
|
|
4676
|
+
|
|
4677
|
+
# Wherever a position is typed tail-only ({#tail_only_walk?}), a tail that reads what the body's own
|
|
4678
|
+
# prefix wrote or mutated does not get a wider answer. It gets the ENTRY binding, which the prefix has
|
|
4679
|
+
# already falsified.
|
|
3971
4680
|
#
|
|
3972
4681
|
# #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
|
-
#
|
|
3975
|
-
#
|
|
3976
|
-
#
|
|
3977
|
-
#
|
|
3978
|
-
#
|
|
3979
|
-
#
|
|
3980
|
-
#
|
|
3981
|
-
#
|
|
3982
|
-
#
|
|
3983
|
-
#
|
|
3984
|
-
|
|
4682
|
+
# 9].map do v = e; v end` has no entry binding for `v`, so tail-only lands on `Dynamic[top]` by itself,
|
|
4683
|
+
# which is why a body-local alone does not call for the floor. A mutated PARAMETER has one, and issue
|
|
4684
|
+
# #617 residue (2) is what it buys: `([[]] * 9).map do |a| a << 1; a end` answered nine stale `[]`, a
|
|
4685
|
+
# provably-empty array at every position of a result whose slots all hold the one array the nine `<<`
|
|
4686
|
+
# filled. Nested under the suppression the same body did it at two positions, and a `transform_values`
|
|
4687
|
+
# over `{ x: [], y: [] }` did it at every pair ({#tail_only_pairs_floored?}). Flooring every position
|
|
4688
|
+
# restores the promise for the positions; the assembler still decides the call from them, and a method
|
|
4689
|
+
# whose fall-through could read a pin again takes its own floor ({#undecided_fold_floor}). What the cap
|
|
4690
|
+
# and the suppression refuse to pay is the per-position body evaluation, and refusing to pay it means
|
|
4691
|
+
# declining to know, not answering the pre-state.
|
|
4692
|
+
#
|
|
4693
|
+
# The names come from the scan the threading gate uses ({#tail_depends_on_body_binding?}), so "would
|
|
4694
|
+
# threading have changed this tail" and "is tail-only untrustworthy here" stay one question. A body
|
|
4695
|
+
# {#unanswered_tail_dependency?} answers false for keeps its exact fold: every single-statement block,
|
|
4696
|
+
# and every multi-statement block whose tail ignores its prefix, reads only its own body-locals, or
|
|
4697
|
+
# leaves through a `next` (the last is evaluated whole, never tail-only).
|
|
4698
|
+
def tail_only_body_floor(element_types)
|
|
3985
4699
|
Array.new(element_types.size) { Type::Combinator.untyped }
|
|
3986
4700
|
end
|
|
3987
4701
|
|
|
3988
4702
|
# True when the tail reads something the prefix changed that NOTHING has re-answered for this walk.
|
|
3989
4703
|
#
|
|
3990
|
-
# `captured` is the
|
|
3991
|
-
# and its cost is independent of the
|
|
3992
|
-
# 9].map do total += e; total end` reads
|
|
3993
|
-
#
|
|
3994
|
-
#
|
|
3995
|
-
#
|
|
4704
|
+
# `captured` is the #587 (b) any-iteration binding of every outer local and instance variable the block
|
|
4705
|
+
# rebinds or mutates in place ({#per_element_captured_bindings}), and its cost is independent of the
|
|
4706
|
+
# arity, so it keeps working above the cap: `total = 0; [1, …, 9].map do total += e; total end` reads
|
|
4707
|
+
# `total` as the fixpoint's `Integer` at every position and needs no floor, and `out = []; … do out << e;
|
|
4708
|
+
# out.size end` reads `out` as the widened `Array[Dynamic[top]]`, whose content carries a gradual arm and
|
|
4709
|
+
# whose arity is open. Under the nesting suppression a rebound name takes the escaping-block floor name by
|
|
4710
|
+
# name, so `[total, e]` keeps its `Tuple` rather than collapsing whole, and the in-place widening, which
|
|
4711
|
+
# evaluates no body, applies as it does anywhere. What a tail-only walk withholds is the per-position body
|
|
4712
|
+
# evaluation, so the names left unanswered are the ones neither binding covers — a mutated block
|
|
4713
|
+
# PARAMETER (issue #617 residue (2)'s `|a| a << 1; a`), and a captured local whose in-place widening
|
|
4714
|
+
# declined, which is left out of `captured` for exactly this reason.
|
|
4715
|
+
#
|
|
4716
|
+
# Two tails are answered without it. A name with no ENTRY binding — a body-local such as `key = k.to_s;
|
|
4717
|
+
# [key, n + w]` — reads as `Dynamic[top]` under tail-only by itself, which is sound, so flooring the
|
|
4718
|
+
# whole position would only erase the structure around it. Instance and global variables always count,
|
|
4719
|
+
# since the scope may still hold a pre-state for them. And a body with a block-level `next` is never
|
|
4720
|
+
# typed tail-only at all: {#block_body_type_joining_nexts} evaluates the whole body whatever the
|
|
4721
|
+
# suppression says.
|
|
3996
4722
|
def unanswered_tail_dependency?(block, captured)
|
|
3997
4723
|
body = block.body
|
|
3998
4724
|
return false unless body.is_a?(Prism::StatementsNode)
|
|
3999
4725
|
return false if body.body.size < 2
|
|
4726
|
+
return false if block_level_jump?(body, Prism::NextNode)
|
|
4727
|
+
|
|
4728
|
+
names = tail_dependent_body_names(body.body) - (captured&.names || EMPTY_NAME_SET)
|
|
4729
|
+
return false if names.empty?
|
|
4000
4730
|
|
|
4001
|
-
|
|
4002
|
-
|
|
4731
|
+
entry = BlockParameterBinder.new.bind_onto(block, scope)
|
|
4732
|
+
names.any? { |name| name.start_with?("@", "$") || !entry.local(name).nil? }
|
|
4003
4733
|
end
|
|
4004
4734
|
|
|
4005
4735
|
# Issue #587 (b) — first-iteration pinning. Every position of this fold is typed from the SAME entry
|
|
@@ -4014,12 +4744,12 @@ module Rigor
|
|
|
4014
4744
|
# position's entry scope, so a position answers what the local can be in ANY iteration
|
|
4015
4745
|
# (`[Integer, Integer]`), never what it was in the first.
|
|
4016
4746
|
#
|
|
4017
|
-
# Only the
|
|
4018
|
-
# keeps its exact fold (`[5, 5]`, `[42, 42]`), and a predicate that ignores the rebound
|
|
4019
|
-
# decides (`select do seen += 1; e > 1 end` still folds to `[2]`); a blanket decline would
|
|
4020
|
-
# three for nothing. The fixpoint binds the block parameter to the union of the elements,
|
|
4021
|
-
# is independent of the arity — which is why the per-element threading cap is NOT a reason to
|
|
4022
|
-
# ninth element keeps `Integer` where it would otherwise keep the stale `0`.
|
|
4747
|
+
# Only the names the body changes move. A position whose tail reads an untouched captured local or a
|
|
4748
|
+
# block-local keeps its exact fold (`[5, 5]`, `[42, 42]`), and a predicate that ignores the rebound
|
|
4749
|
+
# counter still decides (`select do seen += 1; e > 1 end` still folds to `[2]`); a blanket decline would
|
|
4750
|
+
# have lost all three for nothing. The fixpoint binds the block parameter to the union of the elements,
|
|
4751
|
+
# so its cost is independent of the arity — which is why the per-element threading cap is NOT a reason to
|
|
4752
|
+
# floor: a ninth element keeps `Integer` where it would otherwise keep the stale `0`.
|
|
4023
4753
|
#
|
|
4024
4754
|
# Under threading suppression — this fold nested inside another threaded body — the fixpoint's body
|
|
4025
4755
|
# evaluations are exactly the re-entrant cost the suppression exists to refuse, so the names take the
|
|
@@ -4027,74 +4757,256 @@ module Rigor
|
|
|
4027
4757
|
# fixpoint takes the same floor rather than the seed — a seed that reaches a position is the pin this
|
|
4028
4758
|
# exists to remove.
|
|
4029
4759
|
#
|
|
4030
|
-
#
|
|
4760
|
+
# An instance variable pins the same way — the block shares the caller's `self`, so `@t = 0; [1,
|
|
4761
|
+
# 2].map { @t += 1 }` folded to `[1, 1]` too — and so do a class variable and a global. Each takes the
|
|
4762
|
+
# same treatment under every rule above: the ones the body rebinds ({CapturedLocals.writes} with
|
|
4763
|
+
# `non_locals: true`, which also counts `self.w = …` as a rebind of `@w`) join the name set. They keep
|
|
4764
|
+
# their sigil, so the one map cannot confuse `@t` with a local `t`, and the arity-cap floor
|
|
4765
|
+
# ({#unanswered_tail_dependency?}), which compares names sigil-and-all, counts a rebound ivar as answered.
|
|
4766
|
+
#
|
|
4767
|
+
# A name the body rebinds where the fixpoint's pass cannot see it ({UnthreadedRebinds}) takes the floor
|
|
4768
|
+
# outright; see {#converged_captured_bindings}. The optimistic nil-freeness mark a pass's exit binding
|
|
4769
|
+
# carries is kept with the converged type ({CapturedLocals::Bindings}).
|
|
4770
|
+
#
|
|
4771
|
+
# The same pin has a CONTENT half the rebind set cannot see: a captured receiver the body mutates in place
|
|
4772
|
+
# is never rebound, so `h = { a: 0 }; [:a, :a].map { |k| h[k] = h[k] + 1 }` read `h` at its entry
|
|
4773
|
+
# contents at every position and folded to `[1, 1]` (runtime `[1, 2]`). Each such local
|
|
4774
|
+
# ({CapturedLocals.content_mutations}) is bound as if every mutation site in the body had already stored
|
|
4775
|
+
# an unknown value ({UnknownStoreWidening.widen}): `Hash[Dynamic[top] | Symbol, Dynamic[top] |
|
|
4776
|
+
# Integer]`, an open arity with a gradual arm on every content parameter a storing site touches. A local
|
|
4777
|
+
# whose widening declines (a precise nominal) gets no binding at all, so it keeps today's answer. The
|
|
4778
|
+
# binding evaluates no body, so it applies under threading suppression too; the rebind fixpoint runs
|
|
4779
|
+
# over it, and a local the body both rebinds and mutates takes the same widening over its converged
|
|
4780
|
+
# type — the rebind can bring a fresh literal back, which the next iteration then mutates. A local mutated
|
|
4781
|
+
# through an element read (`a[0] << e`) or passed to a self-call whose callee content-mutates that
|
|
4782
|
+
# parameter (`add_to(a, e)`) takes it too, widened as straight-line code widens it after such a site. An
|
|
4783
|
+
# unmutated captured local keeps its exact binding (`h = { a: 0 }; [:a, :a].map { |k| h[k] }` still folds
|
|
4784
|
+
# to `[0, 0]`). An instance variable the body mutates in place takes the same binding
|
|
4785
|
+
# ({CapturedLocals.content_mutations} with `non_locals: true`, on the rebind set's terms): once the #587
|
|
4786
|
+
# (a) gate threads an index write, `@cache[:first] ||= e` pins the same way. So do a class variable and a
|
|
4787
|
+
# global.
|
|
4788
|
+
#
|
|
4789
|
+
# The generic block-return pass lays the same binding ({#block_entry_scope}), with the fixpoint's block
|
|
4790
|
+
# parameters bound to the signature's instead of to the elements' union.
|
|
4791
|
+
#
|
|
4792
|
+
# The binding also marks the index `||=` sites whose slot an earlier position may have filled
|
|
4793
|
+
# ({RepeatedOrWrites}), so the memoizing `||=` reading (`StatementEvaluator#index_compound_write_value`)
|
|
4794
|
+
# does not take such a slot's lone `Dynamic` for "no evidence about the slot" and answer one position's
|
|
4795
|
+
# rvalue: `cache = {}; [1, 2].find { |e| (cache[:first] ||= e) == 2 }` answered `2 == 2` at the second
|
|
4796
|
+
# position and folded `find` to `2` where Ruby, keeping the first iteration's `1`, answers `nil`.
|
|
4797
|
+
#
|
|
4798
|
+
# Returns `nil` (no binding to apply) for the overwhelmingly common body that rebinds and mutates nothing
|
|
4799
|
+
# captured and holds no such site.
|
|
4031
4800
|
def per_element_captured_bindings(block, element_types)
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4801
|
+
captured_block_bindings(block, [Type::Combinator.union(*element_types)], element_types: element_types)
|
|
4802
|
+
end
|
|
4803
|
+
|
|
4804
|
+
# The #587 (b) binding for `block`, with the fixpoint's block parameters bound to `param_types`.
|
|
4805
|
+
# `element_types` is the per-element fold's positions, which {RepeatedOrWrites} reads; nil for the generic
|
|
4806
|
+
# block-return pass.
|
|
4807
|
+
def captured_block_bindings(block, param_types, element_types: nil)
|
|
4808
|
+
stores = CapturedLocals.content_mutations(block, scope, non_locals: true)
|
|
4809
|
+
names = CapturedLocals.writes(block, scope, non_locals: true)
|
|
4810
|
+
repeated = RepeatedOrWrites.sites(block, stores, scope, element_types: element_types)
|
|
4811
|
+
return nil if stores.empty? && names.empty? && repeated.empty?
|
|
4812
|
+
|
|
4813
|
+
stored = stored_capture_bindings(stores)
|
|
4814
|
+
types = stored.dup
|
|
4815
|
+
marks = {}
|
|
4816
|
+
unless names.empty?
|
|
4817
|
+
rebound_capture_bindings(block, names, param_types, stored, marks).each do |name, converged|
|
|
4818
|
+
types[name] = stores.key?(name) ? UnknownStoreWidening.widen(converged, stores[name]) : converged
|
|
4819
|
+
end
|
|
4820
|
+
end
|
|
4821
|
+
return nil if types.empty? && repeated.empty?
|
|
4035
4822
|
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4823
|
+
CapturedLocals::Bindings.new(types: types, marks: marks, repeated: repeated)
|
|
4824
|
+
end
|
|
4825
|
+
|
|
4826
|
+
# Only a binding the widening MOVED is recorded. One it declined (a precise nominal, a refinement under
|
|
4827
|
+
# `sort!`) is still the entry binding, which says nothing about later iterations; recording it would make the
|
|
4828
|
+
# arity-cap floor ({#unanswered_tail_dependency?}) treat the name as answered and type the tail from that
|
|
4829
|
+
# stale binding. Left out, the name keeps the entry binding below the cap and
|
|
4830
|
+
# the floor above it, which is the fold's answer without this pass.
|
|
4831
|
+
def stored_capture_bindings(stores)
|
|
4832
|
+
stores.each_with_object({}) do |(name, sites), bindings|
|
|
4833
|
+
seed = CapturedLocals.bound_type(scope, name)
|
|
4834
|
+
next if seed.nil?
|
|
4835
|
+
|
|
4836
|
+
widened = UnknownStoreWidening.widen(seed, sites)
|
|
4837
|
+
bindings[name] = widened unless widened == seed
|
|
4040
4838
|
end
|
|
4041
4839
|
end
|
|
4042
4840
|
|
|
4841
|
+
# `stored` is laid under every fixpoint pass, so the rebind converges over the widened contents rather
|
|
4842
|
+
# than the entry ones. `marks` collects the optimistic nil-freeness mark a pass's exit binding carries.
|
|
4843
|
+
def rebound_capture_bindings(block, names, param_types, stored, marks)
|
|
4844
|
+
return captured_floor(names) if block_body_threading_suppressed?
|
|
4845
|
+
|
|
4846
|
+
converged_captured_bindings(block, names, param_types, stored, marks)
|
|
4847
|
+
rescue StandardError
|
|
4848
|
+
captured_floor(names)
|
|
4849
|
+
end
|
|
4850
|
+
|
|
4043
4851
|
def captured_floor(names)
|
|
4044
4852
|
names.to_h { |name| [name, Type::Combinator.untyped] }
|
|
4045
4853
|
end
|
|
4046
4854
|
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4855
|
+
# A name the body rebinds somewhere the pass's exit scope never sees ({UnthreadedRebinds}) takes the floor
|
|
4856
|
+
# without running the fixpoint, and is bound to it under every pass, so a name that reads it converges over
|
|
4857
|
+
# the floor rather than over the pin: `t = s` after an unthreaded `s` rebind would otherwise converge on
|
|
4858
|
+
# the first iteration's `s`. The unmoved-pin test below still runs over every other name.
|
|
4859
|
+
def converged_captured_bindings(block, names, param_types, stored, marks)
|
|
4860
|
+
floored = UnthreadedRebinds.names(block, names)
|
|
4861
|
+
moving = names.reject { |name| floored.include?(name) }
|
|
4862
|
+
# Issue #1358 — each pass is an iteration, so it reads the match globals the block entry does.
|
|
4863
|
+
base = stored.reduce(MatchRebinding.block_entry(scope, block)) do |acc, (name, type)|
|
|
4864
|
+
CapturedLocals.bind(acc, name, type)
|
|
4865
|
+
end
|
|
4866
|
+
base = floored.reduce(base) { |acc, name| CapturedLocals.bind(acc, name, Type::Combinator.untyped) }
|
|
4867
|
+
seeds = moving.to_h { |name| [name, CapturedLocals.bound_type(base, name)] }
|
|
4868
|
+
moved = {}
|
|
4050
4869
|
converged = BodyFixpoint.converge(
|
|
4051
|
-
names:
|
|
4870
|
+
names: moving,
|
|
4052
4871
|
seed_bindings: seeds,
|
|
4053
4872
|
widen: Type::Combinator.method(:widen_value_pinned),
|
|
4054
|
-
evaluate_body:
|
|
4873
|
+
evaluate_body: lambda do |bindings|
|
|
4874
|
+
captured_exit_bindings(block, param_types, base, bindings, moved, marks)
|
|
4875
|
+
end
|
|
4055
4876
|
)
|
|
4056
|
-
unmoved_pins_floored(converged, seeds)
|
|
4877
|
+
unmoved_pins_floored(converged, seeds, moved).merge(captured_floor(floored))
|
|
4057
4878
|
end
|
|
4058
4879
|
|
|
4059
4880
|
# A name the write scan says this block REBINDS, whose fixpoint came back on exactly its value-pinned
|
|
4060
4881
|
# seed, is floored rather than believed.
|
|
4061
4882
|
#
|
|
4062
|
-
# The fixpoint reads each pass's exit binding out of `StatementEvaluator`, and that evaluator
|
|
4063
|
-
#
|
|
4064
|
-
# the
|
|
4065
|
-
# the
|
|
4066
|
-
# (
|
|
4067
|
-
#
|
|
4068
|
-
#
|
|
4883
|
+
# The fixpoint reads each pass's exit binding out of `StatementEvaluator`, and that evaluator does not
|
|
4884
|
+
# thread a write from every position: a rebind nested where it types a pure value leaves the exit scope
|
|
4885
|
+
# holding the entry binding, so the fixpoint converges on the seed and every position of the fold answers
|
|
4886
|
+
# the first iteration again. That is issue #617 residue (1)'s one-liner, `seen = 0; [1, 2].find { |e|
|
|
4887
|
+
# (seen += 1) == 2 }` answering `nil` where Ruby answers `2`: the pinned `Constant[0]` made both
|
|
4888
|
+
# predicates `Constant[false]`, and `find` short-circuits on a provably-falsey block. The evaluator has
|
|
4889
|
+
# threaded a call's receiver since #1223, but a `when` condition or a `yield` argument is still such a
|
|
4890
|
+
# position.
|
|
4069
4891
|
#
|
|
4070
4892
|
# An unmoved pin cannot be distinguished from a write that genuinely restores its own entry value
|
|
4071
4893
|
# (`x = 5; xs.each { x = 5 }`), so the floor gives that shape up too. It is the far cheaper side: a
|
|
4072
4894
|
# value-pinned seed the block rebinds is the exact pre-state this fold exists to stop trusting, and
|
|
4073
4895
|
# `Dynamic[top]` is the same escaping-block floor {#captured_floor} already uses. Seeds that carry no
|
|
4074
|
-
#
|
|
4896
|
+
# pinning are left alone — there is no first-iteration value or shape in them to remove, and widening a
|
|
4075
4897
|
# `Nominal` here would only lose a class for nothing.
|
|
4076
|
-
|
|
4898
|
+
#
|
|
4899
|
+
# A second route reaches this test with nothing hidden from the evaluator. The fixpoint stops after
|
|
4900
|
+
# {BodyFixpoint::CAP} passes, so a rebind a counter guards past the third iteration (`row = [v, v] if
|
|
4901
|
+
# count > 3`) runs in none of them, and the widen on the capped pass erases a `Constant`'s value but leaves
|
|
4902
|
+
# a `Tuple` / `HashShape` as it is. `row = []` therefore came back on its seed, and `sizes.last == 2`
|
|
4903
|
+
# folded always-falsey on a runtime `2`, so a shape carrier counts as pinned ({#value_pinned?}). For a
|
|
4904
|
+
# shape the unmoved test only asks whether the seed already covers the converged type, so any rebind the
|
|
4905
|
+
# seed covers is floored with it, not only one restoring the entry value: `qr = n.divmod(3)` rebound to
|
|
4906
|
+
# another `divmod` reads `Dynamic[top]` too.
|
|
4907
|
+
#
|
|
4908
|
+
# The pin test reads a binding as a whole and nothing else. A `0 | Integer` seed is floored too, although a
|
|
4909
|
+
# threaded `x += 1` only joins back into it: an unthreaded write storing another class (`log(x = nil)`)
|
|
4910
|
+
# converges on the same seed, and only the floor keeps `x.nil?` from folding to `false`. Nor does a pass
|
|
4911
|
+
# whose exit binding moved prove the rebind was threaded — a narrowing (`next false unless x`) or a
|
|
4912
|
+
# threaded prefix (`x ||= 0`) moves it while `(x += 1) == 2` stays unthreaded.
|
|
4913
|
+
#
|
|
4914
|
+
# "Unmoved" compares against the fixpoint's own seed, but "pinned" is asked of the CALL-SITE binding. For a
|
|
4915
|
+
# name the body also mutates in place the seed is already the in-place widening of that binding
|
|
4916
|
+
# ({#stored_capture_bindings}), and the widening erases exactly the pin this test looks for: `s = +"ab"`
|
|
4917
|
+
# seeds `String`, not `"ab"`. Asking the seed would let a nested rebind the evaluator cannot see (`(s &&=
|
|
4918
|
+
# s.to_sym)` inside an expression) converge on `String` and be believed, where the local really holds a
|
|
4919
|
+
# Symbol from the second iteration on. For every other name the two bindings are the same. The price is
|
|
4920
|
+
# the trade the paragraph above already makes: a name the body mutates and VISIBLY rebinds to the widened
|
|
4921
|
+
# class (`t << "c"; t = t.strip`) converges on that seed as well, and is floored with the hidden case.
|
|
4922
|
+
#
|
|
4923
|
+
# A statement rebind does not buy that case back, because converging on the widened seed is not evidence
|
|
4924
|
+
# that the rebind was all there was. The capped pass above runs no rebind a counter guards past the third
|
|
4925
|
+
# iteration (`s = s.dup; n += 1; s = [e] if n > 3`), and the scans see only write nodes in the body, so a
|
|
4926
|
+
# lambda defined outside it (`close = -> { cur = nil }; … close.call`) or `binding.local_variable_set`
|
|
4927
|
+
# rebinds the local where no pass looks. Both converge on the seed next to a statement rebind, and
|
|
4928
|
+
# believing `String` there reports `undefined method` or an always-falsey `cur.nil?` on correct code.
|
|
4929
|
+
#
|
|
4930
|
+
# A pass's exit binding also joins every block-level `next` ({StatementEvaluator#evaluate_invocation}), and
|
|
4931
|
+
# a rebind on a `next` arm moves the converged binding off its seed while an unthreaded write on the
|
|
4932
|
+
# fall-through stays invisible: `if c; seen = 100; next false; end; (seen += 1) == 2` converged on `0 |
|
|
4933
|
+
# 100` and folded `find` to `nil`. So `moved` ({#captured_exit_bindings}) records which path moved each
|
|
4934
|
+
# name. One the FALL-THROUGH moved is believed; one only a `next` arm moved is unmoved for this test, and
|
|
4935
|
+
# floored with the hidden case; one neither moved is judged against its seed as before, so a block without
|
|
4936
|
+
# a block-level `next` is judged exactly as it always was.
|
|
4937
|
+
#
|
|
4938
|
+
# {UnthreadedRebinds} floors every name it can show is rebound out of the pass's sight before this test
|
|
4939
|
+
# runs, whatever the fixpoint converged to — the threaded `||=` that moves the fall-through beside an
|
|
4940
|
+
# unthreaded `(seen += 1) == 2` included — so this test only ever sees a name whose rebinds the scan found
|
|
4941
|
+
# threaded. It stays as the backstop for what the scan cannot see, and so the `0 | Integer` seed above
|
|
4942
|
+
# keeps its floor: skipping it for a scan-clean name would trade that backstop for precision.
|
|
4943
|
+
def unmoved_pins_floored(converged, seeds, moved)
|
|
4077
4944
|
converged.to_h do |name, type|
|
|
4078
|
-
|
|
4079
|
-
|
|
4945
|
+
unmoved =
|
|
4946
|
+
case moved[name]
|
|
4947
|
+
when :fall_through then false
|
|
4948
|
+
when :jump then true
|
|
4949
|
+
else type == seeds[name]
|
|
4950
|
+
end
|
|
4951
|
+
next [name, type] unless unmoved && value_pinned?(CapturedLocals.bound_type(scope, name))
|
|
4080
4952
|
|
|
4081
4953
|
[name, Type::Combinator.untyped]
|
|
4082
4954
|
end
|
|
4083
4955
|
end
|
|
4084
4956
|
|
|
4957
|
+
# A binding carries a pin when widening its values changes it, or when it is a `Tuple` / `HashShape` —
|
|
4958
|
+
# alone or as one member of a union. A shape carrier is pinned whatever its elements are: its arity and
|
|
4959
|
+
# key set are one assignment's, and the fixpoint's final widen erases a value but never a shape.
|
|
4085
4960
|
def value_pinned?(type)
|
|
4086
|
-
|
|
4961
|
+
case type
|
|
4962
|
+
when nil then false
|
|
4963
|
+
when Type::Tuple, Type::HashShape then true
|
|
4964
|
+
when Type::Union then type.members.any? { |member| value_pinned?(member) }
|
|
4965
|
+
else Type::Combinator.widen_value_pinned(type) != type
|
|
4966
|
+
end
|
|
4087
4967
|
end
|
|
4088
4968
|
|
|
4089
4969
|
# One fixpoint pass: the body evaluated from `bindings` with the block parameters bound over them (the
|
|
4090
|
-
# same layering as {#type_block_body_with_param}), returning the per-name exit binding
|
|
4091
|
-
#
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4970
|
+
# same layering as {#type_block_body_with_param}), returning the per-name exit binding — the invocation's
|
|
4971
|
+
# exit, which joins every block-level `next` ({StatementEvaluator#evaluate_invocation}), so a rebind on a
|
|
4972
|
+
# jumping path reaches the fixpoint. `moved` records, per name, whether this pass's FALL-THROUGH moved it
|
|
4973
|
+
# off its entry (`:fall_through`) or only the joined `next` arms did (`:jump`), for {#unmoved_pins_floored}.
|
|
4974
|
+
# An exit mark is recorded into `marks` and carried into later passes, since the next iteration enters on
|
|
4975
|
+
# that binding. Threading is suppressed for the pass, as it is for every full body evaluation the
|
|
4976
|
+
# block-return pass runs.
|
|
4977
|
+
def captured_exit_bindings(block, param_types, base, bindings, moved, marks)
|
|
4978
|
+
entry = bindings.reduce(base) do |acc, (name, type)|
|
|
4979
|
+
CapturedLocals.bind(acc, name, type, optimistic: marks[name])
|
|
4980
|
+
end
|
|
4981
|
+
entry = BlockParameterBinder.new(expected_param_types: param_types).bind_onto(block, entry)
|
|
4982
|
+
_type, fall_through, exit_scope = without_block_body_threading do
|
|
4983
|
+
StatementEvaluator.new(scope: entry).evaluate_invocation(block)
|
|
4984
|
+
end
|
|
4985
|
+
bindings.to_h do |name, entry_type|
|
|
4986
|
+
exit_type = CapturedLocals.bound_type(exit_scope, name)
|
|
4987
|
+
record_capture_move(moved, name, entry_type, fall_through, exit_type)
|
|
4988
|
+
mark = CapturedLocals.optimistic_mark(exit_scope, name)
|
|
4989
|
+
marks[name] ||= mark if mark
|
|
4990
|
+
[name, exit_type]
|
|
4991
|
+
end
|
|
4992
|
+
end
|
|
4993
|
+
|
|
4994
|
+
# Which path of one pass moved `name` off its `entry_type`: the fall-through outranks a `next` arm, and a
|
|
4995
|
+
# name the fall-through moved in any pass stays moved.
|
|
4996
|
+
def record_capture_move(moved, name, entry_type, fall_through, exit_type)
|
|
4997
|
+
if moved_off?(entry_type, CapturedLocals.bound_type(fall_through, name))
|
|
4998
|
+
moved[name] = :fall_through
|
|
4999
|
+
elsif moved_off?(entry_type, exit_type)
|
|
5000
|
+
moved[name] ||= :jump
|
|
5001
|
+
end
|
|
5002
|
+
end
|
|
5003
|
+
|
|
5004
|
+
# True when `after` is not already contained in `before` — the same stability test `BodyFixpoint` applies.
|
|
5005
|
+
def moved_off?(before, after)
|
|
5006
|
+
return false if after.nil?
|
|
5007
|
+
return true if before.nil?
|
|
5008
|
+
|
|
5009
|
+
Type::Combinator.union(before, after) != before
|
|
4098
5010
|
end
|
|
4099
5011
|
|
|
4100
5012
|
def per_element_symbol_results(block_arg, element_types)
|
|
@@ -4318,15 +5230,6 @@ module Rigor
|
|
|
4318
5230
|
converged[:__inject_acc__] || seed_acc
|
|
4319
5231
|
end
|
|
4320
5232
|
|
|
4321
|
-
# `index(value)` and `find_index(value)` carry a positional argument and search by `==` rather than
|
|
4322
|
-
# running the block. Decline so the RBS tier owns those forms.
|
|
4323
|
-
def find_family_with_args?(call_node)
|
|
4324
|
-
return false unless %i[find_index index].include?(call_node.name)
|
|
4325
|
-
|
|
4326
|
-
args = call_node.arguments
|
|
4327
|
-
!args.nil? && !args.arguments.empty?
|
|
4328
|
-
end
|
|
4329
|
-
|
|
4330
5233
|
def assemble_per_element_result(method_name, per_position, element_types)
|
|
4331
5234
|
case method_name
|
|
4332
5235
|
when :map, :collect then Type::Combinator.tuple_of(*per_position)
|
|
@@ -4394,28 +5297,40 @@ module Rigor
|
|
|
4394
5297
|
# `find` / `detect`: returns the first receiver element whose block result is Ruby-truthy, or `nil`
|
|
4395
5298
|
# when no position folds to truthy.
|
|
4396
5299
|
#
|
|
4397
|
-
#
|
|
4398
|
-
#
|
|
4399
|
-
#
|
|
4400
|
-
# the answer is `Constant[nil]`.
|
|
5300
|
+
# When the first decisive truthy position is found, the answer is the corresponding receiver element,
|
|
5301
|
+
# joined with the element of every earlier position whose result is not a `Type::Constant`: such a
|
|
5302
|
+
# position may match first, but the search ends at the truthy one whatever it answers, so the call
|
|
5303
|
+
# never returns `nil`. When every position folds to falsey, the answer is `Constant[nil]`. With no
|
|
5304
|
+
# decisive truthy position and some undecided one, it declines ({#undecided_fold_floor}).
|
|
4401
5305
|
def assemble_find_result(per_position, element_types)
|
|
4402
|
-
|
|
5306
|
+
candidates = first_match_candidates(per_position)
|
|
5307
|
+
return candidates if candidates.nil? || candidates.equal?(NO_MATCH)
|
|
4403
5308
|
|
|
4404
|
-
|
|
4405
|
-
return Type::Combinator.constant_of(nil) if first_truthy_index.nil?
|
|
4406
|
-
|
|
4407
|
-
element_types[first_truthy_index]
|
|
5309
|
+
Type::Combinator.union(*candidates.map { |index| element_types[index] })
|
|
4408
5310
|
end
|
|
4409
5311
|
|
|
4410
5312
|
# `find_index` / `index`: returns the index of the first truthy position, or `Constant[nil]` when
|
|
4411
|
-
# nothing matches.
|
|
5313
|
+
# nothing matches, on {#assemble_find_result}'s terms.
|
|
4412
5314
|
def assemble_find_index_result(per_position)
|
|
4413
|
-
|
|
5315
|
+
candidates = first_match_candidates(per_position)
|
|
5316
|
+
return candidates if candidates.nil? || candidates.equal?(NO_MATCH)
|
|
5317
|
+
|
|
5318
|
+
Type::Combinator.union(*candidates.map { |index| Type::Combinator.constant_of(index) })
|
|
5319
|
+
end
|
|
5320
|
+
|
|
5321
|
+
NO_MATCH = Type::Combinator.constant_of(nil)
|
|
5322
|
+
private_constant :NO_MATCH
|
|
4414
5323
|
|
|
5324
|
+
# The positions `find` may stop at — the first decisive truthy one and every undecided one before it —
|
|
5325
|
+
# `NO_MATCH` when every position folds falsey, or nil when an undecided position has no truthy one after it.
|
|
5326
|
+
def first_match_candidates(per_position)
|
|
4415
5327
|
first_truthy_index = per_position.index { |type| truthy_constant?(type) }
|
|
4416
|
-
|
|
5328
|
+
if first_truthy_index.nil?
|
|
5329
|
+
return per_position.all?(Type::Constant) ? NO_MATCH : nil
|
|
5330
|
+
end
|
|
4417
5331
|
|
|
4418
|
-
|
|
5332
|
+
undecided = (0...first_truthy_index).reject { |index| per_position[index].is_a?(Type::Constant) }
|
|
5333
|
+
undecided + [first_truthy_index]
|
|
4419
5334
|
end
|
|
4420
5335
|
|
|
4421
5336
|
def truthy_constant?(type)
|
|
@@ -4436,28 +5351,47 @@ module Rigor
|
|
|
4436
5351
|
# `Constant[Symbol | String]` — otherwise the tier declines (the new key cannot be used as a static
|
|
4437
5352
|
# HashShape index). Collisions (two old keys mapping to the same new key) also decline.
|
|
4438
5353
|
#
|
|
5354
|
+
# Two runtime behaviours fall outside this model, and both decline:
|
|
5355
|
+
#
|
|
5356
|
+
# - A call with arguments. `transform_keys(mapping)` renames the keys `mapping` names and yields only
|
|
5357
|
+
# the rest to the block, and the fold never reads the mapping. `transform_values` takes none.
|
|
5358
|
+
# - An in-place form whose block could see the receiver ({ReceiverBlindBlock}). The bang forms rewrite
|
|
5359
|
+
# the receiver pair by pair, so the block can see pairs it has already rewritten, while the fold types
|
|
5360
|
+
# every pair against the pre-call shape. The non-bang forms build a new hash and keep the exact fold.
|
|
5361
|
+
#
|
|
4439
5362
|
# Returns `nil` on any decline so the dispatcher falls through to `RbsDispatch` and gets the widened
|
|
4440
5363
|
# `Hash[K, V]` answer.
|
|
4441
5364
|
def try_hash_shape_block_fold(call_node, receiver_type)
|
|
4442
|
-
return nil unless
|
|
4443
|
-
return nil unless receiver_type.is_a?(Type::HashShape)
|
|
4444
|
-
return nil unless receiver_type.closed?
|
|
4445
|
-
return nil unless receiver_type.optional_keys.empty?
|
|
5365
|
+
return nil unless hash_shape_fold_applicable?(call_node, receiver_type)
|
|
4446
5366
|
|
|
4447
5367
|
block_arg = call_node.block
|
|
4448
|
-
return nil if block_arg
|
|
5368
|
+
return nil if in_place_block_sees_receiver?(call_node.name, block_arg)
|
|
4449
5369
|
|
|
4450
5370
|
if %i[transform_values transform_values!].include?(call_node.name)
|
|
4451
5371
|
fold_hash_shape_transform_values(receiver_type, block_arg)
|
|
4452
5372
|
else
|
|
4453
|
-
fold_hash_shape_transform_keys(receiver_type, block_arg)
|
|
5373
|
+
fold_hash_shape_transform_keys(receiver_type, block_arg, in_place: call_node.name == :transform_keys!)
|
|
4454
5374
|
end
|
|
4455
5375
|
end
|
|
4456
5376
|
|
|
5377
|
+
def hash_shape_fold_applicable?(call_node, receiver_type)
|
|
5378
|
+
HASH_SHAPE_TRANSFORM_METHODS.include?(call_node.name) &&
|
|
5379
|
+
receiver_type.is_a?(Type::HashShape) && receiver_type.closed? && receiver_type.optional_keys.empty? &&
|
|
5380
|
+
call_node.arguments.nil? && !call_node.block.nil?
|
|
5381
|
+
end
|
|
5382
|
+
|
|
5383
|
+
def in_place_block_sees_receiver?(method_name, block_arg)
|
|
5384
|
+
IN_PLACE_HASH_SHAPE_TRANSFORMS.include?(method_name) &&
|
|
5385
|
+
block_arg.is_a?(Prism::BlockNode) && !ReceiverBlindBlock.blind?(block_arg, scope)
|
|
5386
|
+
end
|
|
5387
|
+
|
|
4457
5388
|
def fold_hash_shape_transform_values(shape, block_arg)
|
|
5389
|
+
captured = hash_block_captured_bindings(block_arg, shape.pairs.values)
|
|
5390
|
+
return hash_shape_values_floor(shape) if tail_only_pairs_floored?(shape, block_arg, captured)
|
|
5391
|
+
|
|
4458
5392
|
new_pairs = {}
|
|
4459
|
-
shape.pairs.
|
|
4460
|
-
new_value = apply_hash_block(block_arg, value)
|
|
5393
|
+
shape.pairs.each_with_index do |(key, value), position|
|
|
5394
|
+
new_value = apply_hash_block(block_arg, value, captured: captured, position: position)
|
|
4461
5395
|
return nil if new_value.nil?
|
|
4462
5396
|
|
|
4463
5397
|
new_pairs[key] = new_value
|
|
@@ -4465,28 +5399,125 @@ module Rigor
|
|
|
4465
5399
|
Type::Combinator.hash_shape_of(new_pairs)
|
|
4466
5400
|
end
|
|
4467
5401
|
|
|
4468
|
-
def fold_hash_shape_transform_keys(shape, block_arg)
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
new_key_type = apply_hash_block(block_arg, key_type)
|
|
4473
|
-
return nil unless new_key_type.is_a?(Type::Constant)
|
|
5402
|
+
def fold_hash_shape_transform_keys(shape, block_arg, in_place: false)
|
|
5403
|
+
key_types = shape.pairs.keys.map { |key| Type::Combinator.constant_of(key) }
|
|
5404
|
+
captured = hash_block_captured_bindings(block_arg, key_types)
|
|
5405
|
+
return hash_keys_floor(shape) if tail_only_pairs_floored?(shape, block_arg, captured)
|
|
4474
5406
|
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
5407
|
+
new_pairs = {}
|
|
5408
|
+
key_types.zip(shape.pairs.values).each_with_index do |(key_type, value), position|
|
|
5409
|
+
typed_key = apply_hash_block(block_arg, key_type, captured: captured, position: position)
|
|
5410
|
+
new_key = new_shape_key(typed_key, new_pairs)
|
|
5411
|
+
return undecided_keys_floor(shape, block_arg, key_types, captured) if new_key.nil?
|
|
4478
5412
|
|
|
4479
5413
|
new_pairs[new_key] = value
|
|
4480
5414
|
end
|
|
4481
|
-
Type::Combinator.hash_shape_of(new_pairs)
|
|
5415
|
+
Type::Combinator.hash_shape_of(in_place ? in_place_key_order(shape.pairs, new_pairs) : new_pairs)
|
|
5416
|
+
end
|
|
5417
|
+
|
|
5418
|
+
# `transform_keys!` rewrites the receiver in place, pair by pair, so its pairs end in a different order from
|
|
5419
|
+
# the new hash `transform_keys` builds. For each old pair, CRuby's `rb_hash_transform_keys_bang` deletes the
|
|
5420
|
+
# old key unless an earlier pair already produced it as a new key, then stores the new key: in place when the
|
|
5421
|
+
# new key is an old key not yet reached, appended otherwise. `{ a: 1, b: 2, c: 3 }.transform_keys! { |k| k ==
|
|
5422
|
+
# :b ? :c : (k == :c ? :b : k) }` is `{ c: 2, a: 1, b: 3 }`, not `{ a: 1, c: 2, b: 3 }`, and `r.keys.first ==
|
|
5423
|
+
# :a` folded always-truthy on the wrong order. Replaying those steps over a Ruby `Hash`, which orders its keys
|
|
5424
|
+
# the same way, gives the same order. `new_pairs` is collision-free and in the old pairs' order, so every
|
|
5425
|
+
# old key is either deleted or overwritten, and no old value survives.
|
|
5426
|
+
def in_place_key_order(old_pairs, new_pairs)
|
|
5427
|
+
produced = {}
|
|
5428
|
+
old_pairs.keys.zip(new_pairs).each_with_object(old_pairs.dup) do |(old_key, (new_key, value)), result|
|
|
5429
|
+
result.delete(old_key) unless produced.key?(old_key)
|
|
5430
|
+
result[new_key] = value
|
|
5431
|
+
produced[new_key] = true
|
|
5432
|
+
end
|
|
5433
|
+
end
|
|
5434
|
+
|
|
5435
|
+
# The key a pair's block result can index the new `HashShape` with — a `Constant` Symbol or String that no
|
|
5436
|
+
# earlier pair already took — or nil.
|
|
5437
|
+
def new_shape_key(new_key_type, new_pairs)
|
|
5438
|
+
return nil unless new_key_type.is_a?(Type::Constant)
|
|
5439
|
+
|
|
5440
|
+
new_key = new_key_type.value
|
|
5441
|
+
return nil unless new_key.is_a?(Symbol) || new_key.is_a?(String)
|
|
5442
|
+
|
|
5443
|
+
new_pairs.key?(new_key) ? nil : new_key
|
|
5444
|
+
end
|
|
5445
|
+
|
|
5446
|
+
# A key fold that cannot build a `HashShape` declines to the dispatcher — except under the suppression, where
|
|
5447
|
+
# the dispatcher's block-return pass is typed tail-only too and read a captured value the body mutates in
|
|
5448
|
+
# place at its entry contents: `buf = +"k"; … { a: 1 }.transform_keys { |k| buf << w.to_s; buf }` answered
|
|
5449
|
+
# `Hash["k", 1]` for `{ "k1" => 1 }`, though the pairs themselves were typed over the #587 (b) binding that
|
|
5450
|
+
# answers `buf`. There the fold answers from the keys it typed, `Hash[union(<new keys>), union(<values>)]`,
|
|
5451
|
+
# and takes the plain floor ({#hash_keys_floor}) when a pair could not be typed at all. The pairs are
|
|
5452
|
+
# re-typed for it, which only a suppressed, undecided key fold pays. That pass now re-answers such a name
|
|
5453
|
+
# itself ({#prefix_answered_scope}), so a decline would no longer read `"k"`; the fold still answers from
|
|
5454
|
+
# the pairs it has typed.
|
|
5455
|
+
def undecided_keys_floor(shape, block_arg, key_types, captured)
|
|
5456
|
+
return nil unless block_body_threading_suppressed?
|
|
5457
|
+
|
|
5458
|
+
new_key_types = key_types.each_with_index.map do |key_type, position|
|
|
5459
|
+
apply_hash_block(block_arg, key_type, captured: captured, position: position)
|
|
5460
|
+
end
|
|
5461
|
+
return hash_keys_floor(shape) if new_key_types.any?(&:nil?)
|
|
5462
|
+
|
|
5463
|
+
Type::Combinator.nominal_of(
|
|
5464
|
+
"Hash", type_args: [Type::Combinator.union(*new_key_types), Type::Combinator.union(*shape.pairs.values)]
|
|
5465
|
+
)
|
|
5466
|
+
end
|
|
5467
|
+
|
|
5468
|
+
# The per-pair twin of issue #587 (b)'s first-iteration pin. Every pair is typed from the SAME entry scope,
|
|
5469
|
+
# so a body that rebinds a captured outer local answered the first pair's value at every pair: `total = 0;
|
|
5470
|
+
# { x: 1, y: 2 }.transform_values { total += 1 }` folded to `{ x: 1, y: 1 }` (runtime `{ x: 1, y: 2 }`),
|
|
5471
|
+
# and `r[:y] == 1` then fired always-truthy on correct code. The pairs take the per-element fold's
|
|
5472
|
+
# captured-local entry binding ({#per_element_captured_bindings}) — the fixpoint's block parameter bound
|
|
5473
|
+
# to the union of the values, or of the `Constant` keys — rather than a copy of it: issue #1198 is where
|
|
5474
|
+
# the block-entry models consolidate. A captured local the body does not change keeps its exact per-pair
|
|
5475
|
+
# fold, and a `&:symbol` block captures nothing.
|
|
5476
|
+
#
|
|
5477
|
+
# An empty shape has no pair to type, so it does not pay for the fixpoint.
|
|
5478
|
+
def hash_block_captured_bindings(block_arg, param_types)
|
|
5479
|
+
return nil unless block_arg.is_a?(Prism::BlockNode)
|
|
5480
|
+
return nil if param_types.empty?
|
|
5481
|
+
|
|
5482
|
+
per_element_captured_bindings(block_arg, param_types)
|
|
5483
|
+
end
|
|
5484
|
+
|
|
5485
|
+
# The per-pair fold has no arity cap, so running nested inside a body another pass is evaluating whole
|
|
5486
|
+
# ({#tail_only_walk?}'s second case) is the one way it reaches a tail-only pair the dependency predicate
|
|
5487
|
+
# can flag, and that pair holds the same pre-state {#tail_only_body_floor} describes:
|
|
5488
|
+
# `{ x: [], y: [] }.transform_values do |a| a << w; a end` inside a threaded `m.synchronize do w = v; …
|
|
5489
|
+
# end` answered `{ x: [], y: [] }` for a hash whose values each hold `[1]`, and `r[:x].first + 1` was
|
|
5490
|
+
# then reported on correct code. The floor is the Tuple fold's, applied per pair and over the same
|
|
5491
|
+
# `captured` names, with the fold answering it rather than declining: a decline reached the dispatcher,
|
|
5492
|
+
# whose block-return pass the same suppression typed tail-only, so `k = "#{k}#{w}"; k` read its keys
|
|
5493
|
+
# as the entry `:a | :b` until that pass re-answered the stale name itself ({#prefix_answered_scope}).
|
|
5494
|
+
# An empty shape types no pair, so it keeps its exact `{}`.
|
|
5495
|
+
def tail_only_pairs_floored?(shape, block_arg, captured)
|
|
5496
|
+
!shape.pairs.empty? && block_arg.is_a?(Prism::BlockNode) && block_body_threading_suppressed? &&
|
|
5497
|
+
unanswered_tail_dependency?(block_arg, captured)
|
|
5498
|
+
end
|
|
5499
|
+
|
|
5500
|
+
# `transform_values` keeps the keys and answers every value `Dynamic[top]`.
|
|
5501
|
+
def hash_shape_values_floor(shape)
|
|
5502
|
+
Type::Combinator.hash_shape_of(shape.pairs.transform_values { Type::Combinator.untyped })
|
|
5503
|
+
end
|
|
5504
|
+
|
|
5505
|
+
# `transform_keys` keeps the values, but a `Dynamic[top]` key cannot index a `HashShape`, and two keys may
|
|
5506
|
+
# collide, so the floor is the plain `Hash` over them.
|
|
5507
|
+
def hash_keys_floor(shape)
|
|
5508
|
+
Type::Combinator.nominal_of(
|
|
5509
|
+
"Hash", type_args: [Type::Combinator.untyped, Type::Combinator.union(*shape.pairs.values)]
|
|
5510
|
+
)
|
|
4482
5511
|
end
|
|
4483
5512
|
|
|
4484
5513
|
# Applies a single-argument block (either a full BlockNode or a `&:symbol` BlockArgumentNode) to
|
|
4485
|
-
# `param_type` and returns the resulting type, or `nil` on failure.
|
|
4486
|
-
|
|
5514
|
+
# `param_type` and returns the resulting type, or `nil` on failure. `captured:` is the pair-independent
|
|
5515
|
+
# entry binding from {#hash_block_captured_bindings}, and `position:` the pair's index, which picks the
|
|
5516
|
+
# index `||=` sites that binding marks there.
|
|
5517
|
+
def apply_hash_block(block_arg, param_type, captured: nil, position: nil)
|
|
4487
5518
|
case block_arg
|
|
4488
5519
|
when Prism::BlockNode
|
|
4489
|
-
type_block_body_with_param(block_arg, [param_type])
|
|
5520
|
+
type_block_body_with_param(block_arg, [param_type], captured: captured, position: position)
|
|
4490
5521
|
when Prism::BlockArgumentNode
|
|
4491
5522
|
expression = block_arg.expression
|
|
4492
5523
|
return nil unless expression.is_a?(Prism::SymbolNode)
|
|
@@ -4503,13 +5534,18 @@ module Rigor
|
|
|
4503
5534
|
end
|
|
4504
5535
|
end
|
|
4505
5536
|
|
|
4506
|
-
# `captured:` — issue #587 (b): the per-name entry binding of every captured outer local
|
|
5537
|
+
# `captured:` — issue #587 (b): the per-name entry binding of every captured outer local and instance
|
|
5538
|
+
# variable the body rebinds, and of every captured local it mutates in place
|
|
4507
5539
|
# ({#per_element_captured_bindings}), laid under the parameter bindings so a parameter still shadows.
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
5540
|
+
# `position:` is the element's index in the fold; the binding marks the index `||=` sites an earlier
|
|
5541
|
+
# position may have filled there ({RepeatedOrWrites::Marks}).
|
|
5542
|
+
def type_block_body_with_param(block_node, expected_param_types, captured: nil, position: nil)
|
|
5543
|
+
# Issue #1358 — a position past the first runs after an earlier one may have rebound the match globals.
|
|
5544
|
+
entry = MatchRebinding.block_entry(scope, block_node)
|
|
5545
|
+
block_scope = captured ? captured.lay(entry, position: position) : entry
|
|
5546
|
+
block_scope = BlockParameterBinder.new(expected_param_types: expected_param_types)
|
|
5547
|
+
.bind_onto(block_node, block_scope)
|
|
5548
|
+
type_block_body(block_node, block_scope, captured: captured)
|
|
4513
5549
|
rescue StandardError
|
|
4514
5550
|
nil
|
|
4515
5551
|
end
|