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
|
@@ -3,28 +3,64 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "../type"
|
|
6
|
+
require_relative "optimistic_origin"
|
|
7
|
+
require_relative "method_dispatcher/rbs_dispatch"
|
|
6
8
|
|
|
7
9
|
module Rigor
|
|
8
10
|
module Inference
|
|
9
11
|
# Slice 5 phase 2 sub-phase 2 destructuring binder.
|
|
10
12
|
#
|
|
11
|
-
# `Rigor::Inference::MultiTargetBinder` decomposes a tuple-shaped right-hand side type
|
|
13
|
+
# `Rigor::Inference::MultiTargetBinder` decomposes a tuple- or array-shaped right-hand side type
|
|
12
14
|
# against a Prism multi-target tree and produces a `name -> Rigor::Type` binding map. The
|
|
13
|
-
# binder is shared between
|
|
15
|
+
# binder is shared between four surfaces:
|
|
14
16
|
#
|
|
15
17
|
# 1. `Rigor::Inference::StatementEvaluator#eval_multi_write` for the statement-level `a, b =
|
|
16
18
|
# rhs` form (`Prism::MultiWriteNode`).
|
|
17
19
|
# 2. `Rigor::Inference::BlockParameterBinder` for nested destructuring inside block parameter
|
|
18
20
|
# lists (`Prism::MultiTargetNode` under `BlockParametersNode#requireds`).
|
|
21
|
+
# 3. `Rigor::Inference::StatementEvaluator#bind_for_index` for `for a, b in pairs`.
|
|
22
|
+
# 4. `Rigor::Inference::ScopeIndexer`'s class-ivar pre-pass, for the ivar targets of `@a, @b = rhs`
|
|
23
|
+
# (issue #1110), which records {Result#ivars} and drops the marks, as it does for `@x = xs.first`,
|
|
24
|
+
# with `soften_slots: false` (see {.bind_marked}).
|
|
25
|
+
# 5. `Rigor::Inference::StatementEvaluator`'s `case/in` pattern binding (issue #1122), through
|
|
26
|
+
# {.decompose_slots}: a pattern binds its names against the same carriers, minus the two
|
|
27
|
+
# statement-only properties that method documents.
|
|
19
28
|
#
|
|
20
29
|
# Both Prism nodes share the same `lefts` / `rest` (a `Prism::SplatNode`) / `rights` triple,
|
|
21
30
|
# so the binder treats them uniformly. The binder is pure: it MUST NOT mutate its inputs and
|
|
22
31
|
# MUST return a fresh `Hash` on every call.
|
|
23
32
|
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
33
|
+
# Per-carrier rule for the right-hand side:
|
|
34
|
+
#
|
|
35
|
+
# - `Type::Tuple` (known arity) decomposes element-wise; see {slot_type} for the softening.
|
|
36
|
+
# - `Nominal[Array, [T]]`, and a `Refined` / `Difference` over one (`non-empty-array[T]`
|
|
37
|
+
# after an `empty?` guard), binds every fixed front/back slot to `T` and a named rest to
|
|
38
|
+
# `Array[T]` (issue #1093). A short array pads the fixed slots with `nil` at runtime, so
|
|
39
|
+
# `T` is the same optimistic read `Array#[]` / `Array#first` get past core RBS's
|
|
40
|
+
# `%a{implicitly-returns-nil}`: the binder reports those names in {Result#optimistic}, and
|
|
41
|
+
# every caller that applies the bindings to a scope records them through
|
|
42
|
+
# `Scope#with_optimistic_local` ({Result#apply_to}) so the ADR-101 branch elision
|
|
43
|
+
# declines on them. A nested target under an optimistic slot inherits the mark (`nil`
|
|
44
|
+
# destructures to `nil` for every inner name); the rest never carries it.
|
|
45
|
+
# - A `Type::Union` distributes (issue #1094): each member decomposes on its own against the
|
|
46
|
+
# same target tree, and each name binds the join of its per-member types. A member that binds
|
|
47
|
+
# a name to `Dynamic[Top]` — every name, for a member no rule here decomposes — makes that
|
|
48
|
+
# name `Dynamic[Top]` for the whole union rather than dropping out of the join. A member that
|
|
49
|
+
# binds a name to bare `nil` while another binds a value drops out of that name's join, and
|
|
50
|
+
# the name is marked optimistic: the per-slot softening of {slot_type}, applied across
|
|
51
|
+
# members (see {join_member_bindings}). So `a, b = ints[1..]` (`Array[Integer] | nil`) binds
|
|
52
|
+
# `Integer` to each slot, marked, exactly as a short `ints` would. A name is also optimistic
|
|
53
|
+
# when any member marked it, the rule `Scope#join` applies to the same mark at a merge.
|
|
54
|
+
# - A value that provably has no implicit `to_ary` conversion
|
|
55
|
+
# ({MethodDispatcher::RbsDispatch.array_conversion_free?}: `Integer`, `String`, `Hash`,
|
|
56
|
+
# `nil`, ... and RBS-known classes whose ancestry neither defines `to_ary` nor overrides
|
|
57
|
+
# `method_missing` / `respond_to_missing?`) is what Ruby wraps as `[rhs]`, so it decomposes
|
|
58
|
+
# as that one-element `Tuple` (issue #1094): `a, b = 1` binds `1` and `nil`. The wrap is
|
|
59
|
+
# exact rather than a bet, so it adds no mark.
|
|
60
|
+
# - Everything else — raw `Array`, `Array[Dynamic[top]]`, `Dynamic[Array[T]]` (whose static
|
|
61
|
+
# facet must not surface as a bare `T`), a value that may convert (`SimpleDelegator`, a
|
|
62
|
+
# Ruby-source class, a module, `Object`), `Top`, `Bot` — collapses to `Dynamic[Top]` per
|
|
63
|
+
# slot.
|
|
28
64
|
#
|
|
29
65
|
# Targets the binder recognises:
|
|
30
66
|
#
|
|
@@ -37,65 +73,299 @@ module Rigor
|
|
|
37
73
|
# same observable semantics (binding a fresh local in the block-entry scope).
|
|
38
74
|
# - `Prism::MultiTargetNode` — recurses with the slot's type as the new right-hand side.
|
|
39
75
|
# - `Prism::SplatNode` (used for `rest`) — its `expression` MUST be a
|
|
40
|
-
# `Prism::LocalVariableTargetNode
|
|
41
|
-
#
|
|
76
|
+
# `Prism::LocalVariableTargetNode`, a `Prism::RequiredParameterNode`, a
|
|
77
|
+
# `Prism::InstanceVariableTargetNode` or a `Prism::IndexTargetNode` to be observable; an
|
|
78
|
+
# anonymous `*` splat or any other target is skipped.
|
|
79
|
+
# - `Prism::InstanceVariableTargetNode` (issue #1110), as a fixed slot, a rest
|
|
80
|
+
# (`*@rest`), or inside a nested target. It decomposes by the same carrier rules as a
|
|
81
|
+
# local and is reported apart, in {Result#ivars} / {Result#optimistic_ivars}, so a caller
|
|
82
|
+
# that threads only locals never sees it. The binder keys it internally by its `:@name`,
|
|
83
|
+
# which no local name can collide with.
|
|
84
|
+
# - `Prism::IndexTargetNode` (`h[:a], x = rhs`), in the same three positions. It binds no
|
|
85
|
+
# name: it stores its slot through `[]=` on its receiver. The binder reports the value it
|
|
86
|
+
# stores in {Result#index_targets}, keyed by the target node itself, so
|
|
87
|
+
# `StatementEvaluator#eval_multi_write` and `#bind_for_index` (`for h[:a], w in pairs`) can
|
|
88
|
+
# widen the receiver's literal shape with that value as content evidence, exactly as a plain
|
|
89
|
+
# `h[:a] = 1` does. It carries no optimistic mark: there is no binding for one to qualify.
|
|
90
|
+
# Its slot is softened as a local's is; `eval_multi_write` documents why that stays honest
|
|
91
|
+
# without the mark.
|
|
42
92
|
#
|
|
43
|
-
# Other target kinds (`
|
|
44
|
-
# `
|
|
45
|
-
# MUST be silently skipped: they have no observable contribution to the
|
|
46
|
-
#
|
|
93
|
+
# Other target kinds (`ClassVariableTargetNode`, `GlobalVariableTargetNode`,
|
|
94
|
+
# `ConstantTargetNode`, `CallTargetNode`, `ConstantPathTargetNode`, `ImplicitRestNode`, ...)
|
|
95
|
+
# MUST be silently skipped: they have no observable contribution to the scope the
|
|
96
|
+
# StatementEvaluator threads.
|
|
47
97
|
#
|
|
48
98
|
# See docs/internal-spec/inference-engine.md for the binding contract and
|
|
49
99
|
# docs/adr/4-type-inference-engine.md for the slice rationale.
|
|
50
100
|
module MultiTargetBinder
|
|
101
|
+
NO_BINDINGS = {}.freeze
|
|
102
|
+
NO_NAMES = [].freeze
|
|
103
|
+
private_constant :NO_BINDINGS, :NO_NAMES
|
|
104
|
+
|
|
105
|
+
# `types` is the local `name -> Rigor::Type` map {.bind} returns; `optimistic` the frozen list
|
|
106
|
+
# of local names whose nil-freeness is the short-array bet described in the module comment.
|
|
107
|
+
# `ivars` / `optimistic_ivars` are the same pair for instance-variable targets.
|
|
108
|
+
# `index_targets` maps each `Prism::IndexTargetNode` to the value it stores; {#apply_to}
|
|
109
|
+
# binds nothing for it.
|
|
110
|
+
Result = Data.define(:types, :optimistic, :ivars, :optimistic_ivars, :index_targets) do
|
|
111
|
+
def initialize(types:, optimistic:, ivars: NO_BINDINGS, optimistic_ivars: NO_NAMES,
|
|
112
|
+
index_targets: NO_BINDINGS)
|
|
113
|
+
super
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Binds every name into `scope` and records the optimistic mark after the binding, since
|
|
117
|
+
# `Scope#with_local` / `Scope#with_ivar` drop any mark the name carried before. `miss` is what
|
|
118
|
+
# each marked slot answers on a miss (issue #1302): `nil` for every mark the binder makes itself —
|
|
119
|
+
# a short array pads with `nil`, and a `nil` destructures to `nil` — so only a caller whose
|
|
120
|
+
# right-hand side a miss can make something else passes another answer
|
|
121
|
+
# ({OptimisticOrigin.destructuring_miss}).
|
|
122
|
+
def apply_to(scope, miss: nil)
|
|
123
|
+
bound = types.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
|
|
124
|
+
bound = ivars.reduce(bound) { |acc, (name, type)| acc.with_ivar(name, type) }
|
|
125
|
+
bound = optimistic.reduce(bound) do |acc, name|
|
|
126
|
+
acc.with_optimistic_local(name, OptimisticOrigin::IMPLICITLY_RETURNS_NIL, miss: miss)
|
|
127
|
+
end
|
|
128
|
+
optimistic_ivars.reduce(bound) do |acc, name|
|
|
129
|
+
acc.with_optimistic_ivar(name, OptimisticOrigin::IMPLICITLY_RETURNS_NIL, miss: miss)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
51
134
|
module_function
|
|
52
135
|
|
|
53
136
|
# @param rhs_type — type of the right-hand side
|
|
54
|
-
|
|
137
|
+
# @param scope — the scope the destructure is evaluated in, which answers the `to_ary`
|
|
138
|
+
# question for a class outside the core list; without one only the core list wraps.
|
|
139
|
+
def bind(target_node, rhs_type, scope: nil)
|
|
140
|
+
bind_marked(target_node, rhs_type, scope: scope).types
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# @param optimistic — whether `rhs_type` itself is an optimistic slot of an enclosing
|
|
144
|
+
# decomposition (a block's `|(g, h)|` fed from an auto-splatted `Array[T]`) or an
|
|
145
|
+
# optimistically nil-free value (`k, v = xs.first`), in which case every name bound under it
|
|
146
|
+
# inherits the mark. It may instead be the per-element Array
|
|
147
|
+
# {OptimisticOrigin.destructuring_marks} builds for a literal right-hand side
|
|
148
|
+
# (`x, y = xs.first, 1`), which marks the names under each fixed slot by that slot's element.
|
|
149
|
+
# @param soften_slots — false keeps a present `X | nil` tuple slot and a union member's bare
|
|
150
|
+
# `nil` in the binding instead of applying the ADR-57 softening of {slot_type} /
|
|
151
|
+
# {join_member_bindings}. That softening is only honest together with the optimistic mark
|
|
152
|
+
# it records, so a consumer that drops the marks (the class-ivar seed, issue #1110) turns it
|
|
153
|
+
# off; the `Array[T]` bet is unaffected, matching what `@x = xs.first` records.
|
|
154
|
+
def bind_marked(target_node, rhs_type, optimistic: false, scope: nil, soften_slots: true)
|
|
55
155
|
bindings = {}
|
|
56
|
-
|
|
57
|
-
bindings
|
|
156
|
+
marked = []
|
|
157
|
+
visit(target_node, rhs_type, optimistic, bindings, marked, [scope, soften_slots])
|
|
158
|
+
split_result(bindings, marked)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# The per-slot types of a positional pattern (`in [i, s]`, `in [*pre, m, *post]`), sharing
|
|
162
|
+
# every carrier rule {.bind_marked} applies to a multi-write target except two statement-only
|
|
163
|
+
# properties (issue #1122):
|
|
164
|
+
#
|
|
165
|
+
# - the multi-assign `[rhs]` wrap is NOT applied. `a, b = 1` binds `a` to `1` because Ruby wraps
|
|
166
|
+
# a right-hand side with no implicit `to_ary`; an array pattern instead matches through the
|
|
167
|
+
# subject's `deconstruct` / `to_ary`, and a subject with neither raises
|
|
168
|
+
# `NoMatchingPatternError` — no body is reached, so binding `1` there would type dead code.
|
|
169
|
+
# - no optimistic mark is reported. The ADR-101 short-array bet exists because a SHORT
|
|
170
|
+
# right-hand side pads the fixed slots with `nil`; a pattern that matched has every fixed slot
|
|
171
|
+
# it named, so there is nothing to bet on and `Result#optimistic` has no counterpart here.
|
|
172
|
+
#
|
|
173
|
+
# A `Type::Union` is the caller's to distribute: a pattern matches SOME member, and only the
|
|
174
|
+
# caller can tell which members can match it at all. Returns `[fronts, rest_type, backs]`, with
|
|
175
|
+
# every carrier no rule decomposes answering `Dynamic[Top]` per slot — the floor.
|
|
176
|
+
def decompose_slots(rhs_type, front_count:, back_count:, rest_present:, scope: nil, soften_slots: true)
|
|
177
|
+
fronts, rest_type, backs, = decompose(
|
|
178
|
+
rhs_type, front_count, back_count, rest_present: rest_present,
|
|
179
|
+
context: [scope, soften_slots], wrap_single: false
|
|
180
|
+
)
|
|
181
|
+
[fronts, rest_type, backs]
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# The `T` of an `Array[T]` carrier the binder may decompose, or nil. Declines raw `Array`
|
|
185
|
+
# and an untyped / top element, which bind `Dynamic[Top]` per slot as before, and every
|
|
186
|
+
# `Dynamic` wrapper: `Dynamic[Array[T]]` is gradual, and projecting its static facet would
|
|
187
|
+
# hand the slot a bare `T` that licenses the negative rules (ADR-5). Shared with
|
|
188
|
+
# {BlockParameterBinder}'s auto-splat so both surfaces accept the same carriers.
|
|
189
|
+
def array_element_type(type)
|
|
190
|
+
case type
|
|
191
|
+
when Type::Nominal
|
|
192
|
+
return nil unless type.class_name == "Array" && type.type_args.size == 1
|
|
193
|
+
|
|
194
|
+
element = type.type_args.first
|
|
195
|
+
element.is_a?(Type::Dynamic) || element.is_a?(Type::Top) ? nil : element
|
|
196
|
+
when Type::Refined, Type::Difference
|
|
197
|
+
array_element_type(type.base)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# The class whose instances `type` describes, for the `to_ary` question, or nil when `type`
|
|
202
|
+
# names no single class: `Dynamic`, `Top`, `Singleton`, `Intersection` and the rest decline.
|
|
203
|
+
# A union never reaches here; the binder distributes it first.
|
|
204
|
+
def conversion_class_name(type)
|
|
205
|
+
case type
|
|
206
|
+
when Type::Constant then type.value.class.name
|
|
207
|
+
when Type::Nominal then type.class_name
|
|
208
|
+
when Type::HashShape then "Hash"
|
|
209
|
+
when Type::IntegerRange then "Integer"
|
|
210
|
+
when Type::Refined, Type::Difference then conversion_class_name(type.base)
|
|
211
|
+
end
|
|
58
212
|
end
|
|
59
213
|
|
|
60
214
|
class << self
|
|
61
215
|
private
|
|
62
216
|
|
|
63
|
-
|
|
217
|
+
# Partitions the walk's single map: an ivar name carries its `@` sigil, which a local name
|
|
218
|
+
# never does, and an index target is keyed by its node rather than by a name.
|
|
219
|
+
def split_result(bindings, marked)
|
|
220
|
+
return Result.new(types: bindings, optimistic: marked.freeze) if bindings.each_key.all? { |k| local_key?(k) }
|
|
221
|
+
|
|
222
|
+
grouped = bindings.group_by { |key, _| key_kind(key) }.transform_values(&:to_h)
|
|
223
|
+
ivar_marked, local_marked = marked.partition { |name| key_kind(name) == :ivar }
|
|
224
|
+
Result.new(types: grouped.fetch(:local, {}), optimistic: local_marked.freeze,
|
|
225
|
+
ivars: grouped.fetch(:ivar, NO_BINDINGS), optimistic_ivars: ivar_marked.freeze,
|
|
226
|
+
index_targets: grouped.fetch(:index, NO_BINDINGS))
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def local_key?(key) = key.is_a?(Symbol) && !key.start_with?("@")
|
|
230
|
+
|
|
231
|
+
def key_kind(key)
|
|
232
|
+
return :index unless key.is_a?(Symbol)
|
|
233
|
+
|
|
234
|
+
key.start_with?("@") ? :ivar : :local
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# `context` is the `[scope, soften_slots]` pair every step of the walk shares.
|
|
238
|
+
def visit(node, rhs_type, optimistic, bindings, marked, context)
|
|
239
|
+
if rhs_type.is_a?(Type::Union)
|
|
240
|
+
return visit_union(node, rhs_type.members, optimistic, bindings, marked, context)
|
|
241
|
+
end
|
|
242
|
+
|
|
64
243
|
lefts = node.lefts || []
|
|
65
244
|
rest = node.rest
|
|
66
245
|
rights = node.rights || []
|
|
246
|
+
rest_present = !rest.nil?
|
|
67
247
|
|
|
68
|
-
fronts, rest_type, backs
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
248
|
+
fronts, rest_type, backs, slots_optimistic =
|
|
249
|
+
decompose(rhs_type, lefts.size, rights.size, rest_present: rest_present, context: context)
|
|
250
|
+
rest_type = arity_free_rest(rest_type) if optimistic == true
|
|
251
|
+
front_marks, back_marks =
|
|
252
|
+
slot_marks(optimistic, slots_optimistic, rhs_type, [lefts.size, rights.size], rest_present: rest_present)
|
|
253
|
+
lefts.each_with_index { |t, i| bind_target(t, fronts[i], front_marks[i], bindings, marked, context) }
|
|
254
|
+
bind_rest_target(rest, rest_type, bindings, marked) if rest
|
|
255
|
+
rights.each_with_index { |t, i| bind_target(t, backs[i], back_marks[i], bindings, marked, context) }
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# The `[front_marks, back_marks]` pair a visit hands its fixed slots. A boolean `optimistic`
|
|
259
|
+
# applies to every slot, as does the decomposition's own short-array bet (`fallback`, the
|
|
260
|
+
# issue #1093 `Array[T]` mark). A per-element Array applies only to the `Type::Tuple` a literal
|
|
261
|
+
# right-hand side types as, of the same arity, and each slot picks its element at the offset
|
|
262
|
+
# {decompose_tuple} reads ({slot_offsets}); any other carrier falls back to `fallback`, and a
|
|
263
|
+
# slot past the literal's end binds an exact `nil` that no mark qualifies.
|
|
264
|
+
def slot_marks(optimistic, fallback, rhs_type, (front_count, back_count), rest_present:)
|
|
265
|
+
literal = optimistic.is_a?(Array) && rhs_type.is_a?(Type::Tuple) && rhs_type.elements.size == optimistic.size
|
|
266
|
+
uniform = optimistic.is_a?(Array) ? fallback : optimistic || fallback
|
|
267
|
+
return [[uniform] * front_count, [uniform] * back_count] unless literal
|
|
268
|
+
|
|
269
|
+
slot_offsets(optimistic.size, front_count, back_count, rest_present: rest_present)
|
|
270
|
+
.map { |offsets| offsets.map { |i| optimistic.fetch(i, false) } }
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Every member walks the same target tree, so each binds the same names; the first member's
|
|
274
|
+
# key order is the declaration order.
|
|
275
|
+
def visit_union(node, members, optimistic, bindings, marked, context)
|
|
276
|
+
walks = members.map do |member|
|
|
277
|
+
member_bindings = {}
|
|
278
|
+
member_marked = []
|
|
279
|
+
visit(node, member, optimistic, member_bindings, member_marked, context)
|
|
280
|
+
[member_bindings, member_marked]
|
|
281
|
+
end
|
|
282
|
+
walks.first.first.each_key do |name|
|
|
283
|
+
types = walks.map { |member_bindings, _| member_bindings[name] }
|
|
284
|
+
joined, softened = join_member_bindings(types, context.last)
|
|
285
|
+
mark = softened || walks.any? { |_, member_marked| member_marked.include?(name) }
|
|
286
|
+
bind_name(name, joined, mark, bindings, marked)
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Returns `[type, softened]`. `Dynamic[Top]` from any member is the whole answer: the join
|
|
291
|
+
# must not let a decomposable member's precise type stand for a member nothing is known
|
|
292
|
+
# about. A member that binds the name to bare `nil` — a `nil` slot, a slot past a short
|
|
293
|
+
# member, or a `nil` member wrapped as `[nil]` — is left out of the join when another member
|
|
294
|
+
# binds a value, and `softened` reports it so the caller marks the name optimistic. This is
|
|
295
|
+
# {slot_type}'s ADR-57 softening across members: which member arrived is correlated with the
|
|
296
|
+
# other slots (`k, v = hash.find { ... }; v.x if k`, `status, value = ok ? [:ok, v] : [:err]`),
|
|
297
|
+
# and a per-slot `T | nil` fires `call.possible-nil-receiver` on the guarded read.
|
|
298
|
+
def join_member_bindings(types, soften_slots)
|
|
299
|
+
untyped = Type::Combinator.untyped
|
|
300
|
+
return [untyped, false] if types.any? { |type| type == untyped }
|
|
301
|
+
|
|
302
|
+
firm = types.reject { |type| nil_literal?(type) }
|
|
303
|
+
return [Type::Combinator.union(*types), false] if !soften_slots || firm.empty? || firm.size == types.size
|
|
304
|
+
|
|
305
|
+
[Type::Combinator.union(*firm), true]
|
|
72
306
|
end
|
|
73
307
|
|
|
74
308
|
# Decomposes the right-hand side type into the per-slot types. Returns a `[fronts,
|
|
75
|
-
# rest_type, backs]`
|
|
76
|
-
# `front_count`/`back_count`,
|
|
77
|
-
# `rest_present:` is true) or `nil
|
|
78
|
-
|
|
309
|
+
# rest_type, backs, optimistic]` quadruple, with `fronts` and `backs` each an ordered
|
|
310
|
+
# array of length `front_count`/`back_count`, `rest_type` either a `Rigor::Type` (when
|
|
311
|
+
# `rest_present:` is true) or `nil`, and `optimistic` true when the fixed slots are the
|
|
312
|
+
# short-array bet rather than known elements.
|
|
313
|
+
def decompose(rhs_type, front_count, back_count, rest_present:, context:, wrap_single: true)
|
|
314
|
+
scope, soften = context
|
|
79
315
|
if rhs_type.is_a?(Type::Tuple)
|
|
80
|
-
decompose_tuple(rhs_type, front_count, back_count, rest_present: rest_present)
|
|
316
|
+
[*decompose_tuple(rhs_type, front_count, back_count, rest_present: rest_present, soften: soften), false]
|
|
317
|
+
elsif (element = array_element_type(rhs_type))
|
|
318
|
+
[*decompose_array(element, front_count, back_count, rest_present: rest_present), true]
|
|
319
|
+
elsif wrap_single && wraps_as_single_element?(rhs_type, scope)
|
|
320
|
+
wrapped = Type::Combinator.tuple_of(rhs_type)
|
|
321
|
+
[*decompose_tuple(wrapped, front_count, back_count, rest_present: rest_present, soften: soften), false]
|
|
81
322
|
else
|
|
82
|
-
decompose_default(front_count, back_count, rest_present: rest_present)
|
|
323
|
+
[*decompose_default(front_count, back_count, rest_present: rest_present), false]
|
|
83
324
|
end
|
|
84
325
|
end
|
|
85
326
|
|
|
86
|
-
|
|
327
|
+
# Ruby's `[rhs]` wrap for a value with no implicit `to_ary`; see the module comment.
|
|
328
|
+
def wraps_as_single_element?(rhs_type, scope)
|
|
329
|
+
class_name = conversion_class_name(rhs_type)
|
|
330
|
+
!class_name.nil? && MethodDispatcher::RbsDispatch.array_conversion_free?(class_name, scope)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Under an inherited mark the whole right-hand side may be the `nil` a short outer array padded in, and
|
|
334
|
+
# `(p, *q) = nil` binds `q = []`. A Tuple rest would claim the middle elements are present
|
|
335
|
+
# (`q.first.nil?` folding to `false`), so it widens to `Array[union of the middle]`, which admits the
|
|
336
|
+
# empty array; an empty middle has no element type to offer and stays `Array[Dynamic[top]]`.
|
|
337
|
+
def arity_free_rest(rest_type)
|
|
338
|
+
return rest_type unless rest_type.is_a?(Type::Tuple)
|
|
339
|
+
|
|
340
|
+
element = rest_type.elements.empty? ? Type::Combinator.untyped : Type::Combinator.union(*rest_type.elements)
|
|
341
|
+
Type::Combinator.nominal_of("Array", type_args: [element])
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def decompose_array(element, front_count, back_count, rest_present:)
|
|
345
|
+
[
|
|
346
|
+
Array.new(front_count) { element },
|
|
347
|
+
rest_present ? Type::Combinator.nominal_of("Array", type_args: [element]) : nil,
|
|
348
|
+
Array.new(back_count) { element }
|
|
349
|
+
]
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def decompose_tuple(tuple, front_count, back_count, rest_present:, soften:)
|
|
87
353
|
elements = tuple.elements
|
|
88
|
-
fronts =
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
354
|
+
fronts, backs = slot_offsets(elements.size, front_count, back_count, rest_present: rest_present)
|
|
355
|
+
.map { |offsets| offsets.map { |i| slot_type(elements, i, soften) } }
|
|
356
|
+
# The end is clamped at the fronts, as {slot_offsets} clamps the backs: an unclamped negative end
|
|
357
|
+
# (more back slots than elements) would count from the end instead. A range starting past the end
|
|
358
|
+
# slices to `nil`.
|
|
359
|
+
middle = elements[front_count...[elements.size - back_count, front_count].max] || []
|
|
360
|
+
[fronts, rest_present ? Type::Combinator.tuple_of(*middle) : nil, backs]
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# The element offsets a `[fronts, backs]` pair of fixed slots reads out of `size` elements: the
|
|
364
|
+
# back slots follow the middle a rest absorbs, and follow the fronts directly without one. An
|
|
365
|
+
# offset past `size` is a slot the source is too short to fill.
|
|
366
|
+
def slot_offsets(size, front_count, back_count, rest_present:)
|
|
367
|
+
back_start = rest_present ? [size - back_count, front_count].max : front_count
|
|
368
|
+
[Array.new(front_count) { |i| i }, Array.new(back_count) { |i| back_start + i }]
|
|
99
369
|
end
|
|
100
370
|
|
|
101
371
|
# The per-slot type for index `i` of a tuple decomposition, FP-safely softened: a
|
|
@@ -116,11 +386,11 @@ module Rigor
|
|
|
116
386
|
# outranks the worst-case per-slot reading, so we drop the `nil` from a destructured slot
|
|
117
387
|
# and keep the non-`nil` constituent (a bare `nil` slot stays `nil` — there is nothing to
|
|
118
388
|
# soften). A pure non-optional element keeps its precise type unchanged.
|
|
119
|
-
def slot_type(elements, index)
|
|
389
|
+
def slot_type(elements, index, soften)
|
|
120
390
|
element = elements[index]
|
|
121
391
|
return Type::Combinator.constant_of(nil) if element.nil?
|
|
122
392
|
|
|
123
|
-
soften_optional_slot(element)
|
|
393
|
+
soften ? soften_optional_slot(element) : element
|
|
124
394
|
end
|
|
125
395
|
|
|
126
396
|
def soften_optional_slot(element)
|
|
@@ -145,24 +415,42 @@ module Rigor
|
|
|
145
415
|
]
|
|
146
416
|
end
|
|
147
417
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
418
|
+
# A per-element Array mark reaches a name only through a nested target: a name bound to a
|
|
419
|
+
# whole literal array holds an Array, which is never `nil`. Nor does any mark reach a name bound
|
|
420
|
+
# to an exact `nil` — a slot past a Tuple's end or a wrap's empty slot — which is `nil` whether
|
|
421
|
+
# or not the source missed, so the mark could only withhold an honest verdict.
|
|
422
|
+
def bind_target(target, type, optimistic, bindings, marked, context)
|
|
423
|
+
return visit(target, type, optimistic, bindings, marked, context) if target.is_a?(Prism::MultiTargetNode)
|
|
424
|
+
|
|
425
|
+
key = binding_key(target)
|
|
426
|
+
bind_name(key, type, optimistic == true && !nil_literal?(type), bindings, marked) if key
|
|
155
427
|
end
|
|
156
428
|
|
|
157
|
-
|
|
429
|
+
# The rest is an `Array` even when the right-hand side is short, so it is never marked.
|
|
430
|
+
def bind_rest_target(splat_node, type, bindings, marked)
|
|
158
431
|
return unless splat_node.is_a?(Prism::SplatNode)
|
|
159
432
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
433
|
+
key = binding_key(splat_node.expression)
|
|
434
|
+
bind_name(key, type, false, bindings, marked) if key
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# A named target's name, an index target's node (it binds no name, see {bind_name}), or nil for
|
|
438
|
+
# a target that contributes nothing to the scope.
|
|
439
|
+
def binding_key(target)
|
|
440
|
+
case target
|
|
441
|
+
when Prism::LocalVariableTargetNode, Prism::RequiredParameterNode, Prism::InstanceVariableTargetNode
|
|
442
|
+
target.name
|
|
443
|
+
when Prism::IndexTargetNode then target
|
|
164
444
|
end
|
|
165
445
|
end
|
|
446
|
+
|
|
447
|
+
# A later binding of the same name (`a, a = ints`) wins, mark included. `name` is the
|
|
448
|
+
# target node for an index target, which binds no name and so is never marked.
|
|
449
|
+
def bind_name(name, type, optimistic, bindings, marked)
|
|
450
|
+
bindings[name] = type
|
|
451
|
+
marked.delete(name)
|
|
452
|
+
marked << name if optimistic && name.is_a?(Symbol)
|
|
453
|
+
end
|
|
166
454
|
end
|
|
167
455
|
end
|
|
168
456
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../type"
|
|
4
4
|
require_relative "content_join"
|
|
5
|
+
require_relative "rewrite_mutation"
|
|
5
6
|
|
|
6
7
|
module Rigor
|
|
7
8
|
module Inference
|
|
@@ -48,7 +49,9 @@ module Rigor
|
|
|
48
49
|
MutationWidening.join_added_pairs(pre_state, method_name, arg_types,
|
|
49
50
|
[pinned_evidence(seed_keys), pinned_evidence(seed_values)])
|
|
50
51
|
end
|
|
51
|
-
|
|
52
|
+
# The arm lands after the precise parameters are restored: a rewriter falsifies a proven side too
|
|
53
|
+
# (`Hash.new(0)` under `transform_values!(&:to_s)` holds Strings), which an adder's one store cannot.
|
|
54
|
+
rejoined = RewriteMutation.arm(keep_precise_parameters(pre_state, rejoined), method_name)
|
|
52
55
|
rejoined == nominal ? nil : rejoined
|
|
53
56
|
end
|
|
54
57
|
|