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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../type"
|
|
4
4
|
require_relative "range_constant"
|
|
5
|
+
require_relative "content_join"
|
|
5
6
|
|
|
6
7
|
module Rigor
|
|
7
8
|
module Inference
|
|
@@ -74,10 +75,64 @@ module Rigor
|
|
|
74
75
|
}.freeze
|
|
75
76
|
private_constant :TYPE_HANDLERS
|
|
76
77
|
|
|
78
|
+
UNREAD_HASH_REFUSED_KEY = :__rigor_acceptance_unread_hash_refused__
|
|
79
|
+
private_constant :UNREAD_HASH_REFUSED_KEY
|
|
80
|
+
|
|
81
|
+
# Whether a `maybe` for `param` against `arg` rests on a record that could not read a hash's entries: a
|
|
82
|
+
# `Hash` with a gradual arm ({#gradual_hash?}) or an open `HashShape`. The record cannot prove the key set
|
|
83
|
+
# of either, and its `maybe` climbs through whatever holds the record. `OverloadSelector`'s strict pass
|
|
84
|
+
# reads that `maybe` as no evidence for the overload, so this re-asks with each such record answering
|
|
85
|
+
# `no` and reports whether the pair then fails. A `maybe` from anywhere else (a subclass only the Ruby
|
|
86
|
+
# source declares, a splat under a parameter that is no record) survives the re-ask and still counts.
|
|
87
|
+
def maybe_rests_on_unread_hash?(param, arg)
|
|
88
|
+
return false unless contains_type?(param) { |type| type.is_a?(Type::HashShape) }
|
|
89
|
+
return false unless contains_type?(arg) { |type| unread_hash?(type) }
|
|
90
|
+
|
|
91
|
+
refusing_unread_hashes { accepts(param, arg, mode: :gradual).no? }
|
|
92
|
+
end
|
|
93
|
+
|
|
77
94
|
# rubocop:disable-next Metrics/ClassLength
|
|
78
95
|
class << self
|
|
79
96
|
private
|
|
80
97
|
|
|
98
|
+
# The flag lives on the thread, not on a parameter, because the record may sit any number of `accepts`
|
|
99
|
+
# calls below the one asked (a tuple element, a `Hash` value, a union member), and those calls go
|
|
100
|
+
# through each carrier's `Type#accepts`. It is set only for the duration of the block.
|
|
101
|
+
def refusing_unread_hashes
|
|
102
|
+
previous = Thread.current[UNREAD_HASH_REFUSED_KEY]
|
|
103
|
+
Thread.current[UNREAD_HASH_REFUSED_KEY] = true
|
|
104
|
+
yield
|
|
105
|
+
ensure
|
|
106
|
+
Thread.current[UNREAD_HASH_REFUSED_KEY] = previous
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# The `maybe` a record answers for a hash whose entries it cannot read, or `no` inside
|
|
110
|
+
# {#refusing_unread_hashes}.
|
|
111
|
+
def unread_hash_maybe(mode, reason)
|
|
112
|
+
return hash_shape_no(mode, "#{reason} (refused as evidence)") if Thread.current[UNREAD_HASH_REFUSED_KEY]
|
|
113
|
+
|
|
114
|
+
Type::AcceptsResult.maybe(mode: mode, reasons: reason)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def unread_hash?(type)
|
|
118
|
+
gradual_hash?(type) || (type.is_a?(Type::HashShape) && type.open?)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def contains_type?(type, &)
|
|
122
|
+
return true if yield(type)
|
|
123
|
+
|
|
124
|
+
components =
|
|
125
|
+
case type
|
|
126
|
+
when Type::Union then type.members
|
|
127
|
+
when Type::Nominal then type.type_args
|
|
128
|
+
when Type::Tuple then type.elements
|
|
129
|
+
when Type::HashShape then type.pairs.values
|
|
130
|
+
when Type::Difference then [type.base]
|
|
131
|
+
else return false
|
|
132
|
+
end
|
|
133
|
+
components.any? { |component| contains_type?(component, &) }
|
|
134
|
+
end
|
|
135
|
+
|
|
81
136
|
def accepts_one(self_type, other_type, mode)
|
|
82
137
|
handler = TYPE_HANDLERS[self_type.class]
|
|
83
138
|
return send(handler, self_type, other_type, mode) if handler
|
|
@@ -755,10 +810,15 @@ module Rigor
|
|
|
755
810
|
# `Difference[base, removed]` accepts another type X when the base accepts X *and* X's value set is
|
|
756
811
|
# provably disjoint from `removed`. The disjointness test is the subtle part — it is NOT the same as
|
|
757
812
|
# `removed.accepts(X)`, because `Nominal[String]` includes `""` even though `Constant[""]` does not
|
|
758
|
-
# "accept" `Nominal[String]`. The conservative rule here: we can prove disjointness only when X is
|
|
759
|
-
#
|
|
760
|
-
# removed value (already exhibits the disjointness)
|
|
761
|
-
#
|
|
813
|
+
# "accept" `Nominal[String]`. The conservative rule here: we can prove disjointness only when X is a
|
|
814
|
+
# carrier whose value set is visibly apart from `removed` — a `Constant` (compare values directly),
|
|
815
|
+
# another `Difference` with the same removed value (already exhibits the disjointness), an
|
|
816
|
+
# `IntegerRange` that skips an Integer removed value, a `Tuple` whose fixed arity differs from a
|
|
817
|
+
# removed `Tuple`, or a `HashShape` with a required key a closed removed `HashShape` cannot hold. The
|
|
818
|
+
# last three cover the empty witnesses of `non-zero-int`, `non-empty-array[T]` and
|
|
819
|
+
# `non-empty-hash[K, V]`. Any other carrier — a Nominal above all — is no proof and the difference
|
|
820
|
+
# rejects it under gradual mode, even where its values happen to exclude `removed`. A Union or
|
|
821
|
+
# Intersection argument never reaches this rule whole: `accepts` splits it into members first.
|
|
762
822
|
def accepts_difference(self_type, other_type, mode)
|
|
763
823
|
base_result = accepts(self_type.base, other_type, mode: mode)
|
|
764
824
|
return base_result if base_result.no?
|
|
@@ -787,9 +847,27 @@ module Rigor
|
|
|
787
847
|
# Disjointness is monotonic over Intersection: if any member is provably disjoint from
|
|
788
848
|
# `removed`, the meet is too.
|
|
789
849
|
other_type.members.any? { |m| provably_disjoint_from_removed?(m, removed) }
|
|
850
|
+
when Type::IntegerRange
|
|
851
|
+
removed.is_a?(Type::Constant) && removed.value.is_a?(Integer) && !other_type.covers?(removed.value)
|
|
852
|
+
when Type::Tuple
|
|
853
|
+
# A Tuple's arity is fixed, so it shares no inhabitant with a Tuple of another arity — the
|
|
854
|
+
# zero-arity witness `non-empty-array[T]` removes included.
|
|
855
|
+
removed.is_a?(Type::Tuple) && removed.elements.size != other_type.elements.size
|
|
856
|
+
when Type::HashShape
|
|
857
|
+
hash_shape_excludes_removed?(other_type, removed)
|
|
790
858
|
end
|
|
791
859
|
end
|
|
792
860
|
|
|
861
|
+
# Every inhabitant of `shape` holds each of its required keys, and a closed `removed` shape holds no
|
|
862
|
+
# key outside its pairs, so one required key `removed` does not list makes the two disjoint. Against
|
|
863
|
+
# the closed `{}` witness `non-empty-hash[K, V]` removes, that is any required key; an optional key
|
|
864
|
+
# proves nothing, since the shape is inhabited without it.
|
|
865
|
+
def hash_shape_excludes_removed?(shape, removed)
|
|
866
|
+
return false unless removed.is_a?(Type::HashShape) && removed.closed?
|
|
867
|
+
|
|
868
|
+
shape.required_keys.any? { |key| !removed.pairs.key?(key) }
|
|
869
|
+
end
|
|
870
|
+
|
|
793
871
|
# `Refined[base, predicate]` accepts another type X when the base accepts the *base* of X *and* X is
|
|
794
872
|
# provably contained in the predicate's value set. The base check is delegated to
|
|
795
873
|
# `accepts(self.base, X.base)` so handlers like `accepts_nominal` see Nominal-vs-Nominal and return
|
|
@@ -924,11 +1002,15 @@ module Rigor
|
|
|
924
1002
|
|
|
925
1003
|
# HashShape{k1: T1, ...} accepts another HashShape when every required key of self is required on
|
|
926
1004
|
# the other side and Ti accepts Ui (depth covariant). Optional keys may be absent on the other side;
|
|
927
|
-
# when present, their values are checked. A closed self rejects known
|
|
928
|
-
#
|
|
1005
|
+
# when present, their values are checked. A closed self rejects known extra keys, and answers at most
|
|
1006
|
+
# `maybe` for an open source: `h = { a: 1 }; h.default = 0` reopens a shape whose key set is still
|
|
1007
|
+
# exactly the literal's. Other types are rejected, except a `Hash` with a gradual arm (see
|
|
1008
|
+
# {#gradual_hash?}); the converse direction (a Nominal accepting a HashShape) is handled by
|
|
929
1009
|
# `accepts_nominal` via projection.
|
|
930
1010
|
def accepts_hash_shape(self_type, other_type, mode)
|
|
931
1011
|
unless other_type.is_a?(Type::HashShape)
|
|
1012
|
+
return gradual_hash_verdict(self_type, other_type, mode) if gradual_hash?(other_type)
|
|
1013
|
+
|
|
932
1014
|
return Type::AcceptsResult.no(
|
|
933
1015
|
mode: mode,
|
|
934
1016
|
reasons: "HashShape does not accept #{other_type.class}"
|
|
@@ -939,8 +1021,6 @@ module Rigor
|
|
|
939
1021
|
return hash_shape_no(mode, "HashShape missing required keys: #{missing.inspect}") unless missing.empty?
|
|
940
1022
|
|
|
941
1023
|
if self_type.closed?
|
|
942
|
-
return hash_shape_no(mode, "HashShape closed target rejects open source") if other_type.open?
|
|
943
|
-
|
|
944
1024
|
extra = other_type.pairs.keys - self_type.pairs.keys
|
|
945
1025
|
unless extra.empty?
|
|
946
1026
|
return hash_shape_no(mode, "HashShape closed target rejects extra keys: #{extra.inspect}")
|
|
@@ -948,9 +1028,59 @@ module Rigor
|
|
|
948
1028
|
end
|
|
949
1029
|
|
|
950
1030
|
per_entry = hash_shape_entry_results(self_type, other_type, mode)
|
|
951
|
-
combine_arg_results(per_entry, mode)
|
|
1031
|
+
result = combine_arg_results(per_entry, mode)
|
|
1032
|
+
return result unless result.yes? && self_type.closed? && other_type.open?
|
|
1033
|
+
|
|
1034
|
+
unread_hash_maybe(mode, "HashShape closed target cannot check open source")
|
|
1035
|
+
end
|
|
1036
|
+
|
|
1037
|
+
# A `Hash` nominal — directly or as the base of a `Difference` (`non-empty-hash[K, V]`) — that is raw or
|
|
1038
|
+
# carries a `Dynamic` arm in a type argument. A hash literal with a `**splat` entry carries the arm by
|
|
1039
|
+
# design, `Hash.new` filled key by key reads as the raw `Hash`, and a declared `Hash[Symbol, untyped]`
|
|
1040
|
+
# is the author saying the hash may hold anything (the reading `MutationRejoin#regrowable_carrier?`
|
|
1041
|
+
# gives a declared gradual arm). The runtime value may then hold exactly the record's keys, so a shape
|
|
1042
|
+
# cannot reject it on the arm's account, the way `Hash[K, V]` accepts a raw `Hash` as maybe. A `Hash`
|
|
1043
|
+
# subclass is not read: its entries need not be the record's (`HashWithIndifferentAccess` stores String
|
|
1044
|
+
# keys).
|
|
1045
|
+
def gradual_hash?(type)
|
|
1046
|
+
nominal = type.is_a?(Type::Difference) ? type.base : type
|
|
1047
|
+
nominal.is_a?(Type::Nominal) && nominal.class_name == "Hash" &&
|
|
1048
|
+
(nominal.type_args.empty? || nominal.type_args.any? { |arg| gradual_arm?(arg) })
|
|
1049
|
+
end
|
|
1050
|
+
|
|
1051
|
+
# `maybe`, unless an argument without a gradual arm already rules the record out: a precise `K` that
|
|
1052
|
+
# rejects a required key (`Hash[String, untyped]` never holds `:a`), or a precise `V` none of whose
|
|
1053
|
+
# members a required key's type accepts.
|
|
1054
|
+
def gradual_hash_verdict(self_type, other_type, mode)
|
|
1055
|
+
nominal = other_type.is_a?(Type::Difference) ? other_type.base : other_type
|
|
1056
|
+
key_type, value_type = nominal.type_args
|
|
1057
|
+
missing = unholdable_keys(self_type, key_type, mode)
|
|
1058
|
+
return hash_shape_no(mode, "Hash key type cannot hold required keys: #{missing.inspect}") if missing.any?
|
|
1059
|
+
|
|
1060
|
+
refused = refused_value_keys(self_type, value_type, mode)
|
|
1061
|
+
return hash_shape_no(mode, "Hash value type refused for required keys: #{refused.inspect}") if refused.any?
|
|
1062
|
+
|
|
1063
|
+
unread_hash_maybe(mode, "HashShape cannot check #{other_type.describe(:short)}")
|
|
952
1064
|
end
|
|
953
1065
|
|
|
1066
|
+
def unholdable_keys(self_type, key_type, mode)
|
|
1067
|
+
return [] if key_type.nil? || gradual_arm?(key_type)
|
|
1068
|
+
|
|
1069
|
+
self_type.required_keys.select { |key| key_type.accepts(Type::Combinator.constant_of(key), mode: mode).no? }
|
|
1070
|
+
end
|
|
1071
|
+
|
|
1072
|
+
def refused_value_keys(self_type, value_type, mode)
|
|
1073
|
+
return [] if value_type.nil? || gradual_arm?(value_type)
|
|
1074
|
+
|
|
1075
|
+
members = ContentJoin.union_members(value_type)
|
|
1076
|
+
self_type.required_keys.select do |key|
|
|
1077
|
+
formal = self_type.pairs.fetch(key)
|
|
1078
|
+
members.all? { |member| accepts(formal, member, mode: mode).no? }
|
|
1079
|
+
end
|
|
1080
|
+
end
|
|
1081
|
+
|
|
1082
|
+
def gradual_arm?(type) = ContentJoin.union_members(type).any?(Type::Dynamic)
|
|
1083
|
+
|
|
954
1084
|
def hash_shape_entry_results(self_type, other_type, mode)
|
|
955
1085
|
self_type.pairs.filter_map do |key, formal|
|
|
956
1086
|
next unless other_type.pairs.key?(key)
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../type"
|
|
4
|
+
require_relative "multi_target_binder"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Inference
|
|
8
|
+
# The auto-splat half of {BlockParameterBinder}: given a block's positional parameter shape and the one
|
|
9
|
+
# value the receiver yields, it answers the positional table the block's names are bound from.
|
|
10
|
+
#
|
|
11
|
+
# Ruby blocks (NOT lambdas) spread a single yielded array-ish value across their positionals, and CRuby
|
|
12
|
+
# decides that on the PARAMETER LIST alone ({ParameterShape#splats?}) — never on the value. So the
|
|
13
|
+
# carrier only decides how precisely each position can be filled, and a carrier this module cannot
|
|
14
|
+
# decompose lowers the positions to `Dynamic[Top]` rather than cancelling the spread (issue #1116).
|
|
15
|
+
#
|
|
16
|
+
# Each member of the carrier (a union distributes; anything else is its own single member) contributes
|
|
17
|
+
# one {Table}, and {join} folds them into the answer:
|
|
18
|
+
#
|
|
19
|
+
# - a `Type::Tuple` spreads element-wise, leading positionals (required, then optional) from the head
|
|
20
|
+
# and, with a rest present, trailing positionals from the tail with the rest absorbing the middle —
|
|
21
|
+
# the split `MultiTargetBinder` applies to `a, *r, b = tuple`, so `|*r, v|` over `[K, V]` binds `v` to
|
|
22
|
+
# `V`. A slot past the tuple is left unfilled and lands on `Dynamic[Top]`;
|
|
23
|
+
# - an `Array[T]` ({MultiTargetBinder.array_element_type}) binds `T` to every required and trailing
|
|
24
|
+
# positional, `Dynamic[Top]` to every optional one (a short array hands it its default, not `nil`),
|
|
25
|
+
# and `Array[T]` to a named rest. A short array pads the fixed positions with `nil` at runtime, so
|
|
26
|
+
# those positions are reported optimistic (issue #1093);
|
|
27
|
+
# - an opaque array carrier — a raw `Array`, an `Array[untyped]` / `Array[top]`, a `Refined` /
|
|
28
|
+
# `Difference` over either, or a `Dynamic` over any `Array` however precise — binds `Dynamic[Top]` to
|
|
29
|
+
# every position: the floor `MultiTargetBinder` binds for the same undecomposable right-hand side, and
|
|
30
|
+
# the reason `[1, 2].tap { |a, b| a.succ }` no longer reads `a` as the whole `Array` (issue #1116);
|
|
31
|
+
# - a bare `nil` binds `nil` to every position. `nil` has no `to_ary`, so CRuby passes it as the one
|
|
32
|
+
# argument and `|a, b|` sees `nil, nil` — which is also what the wrap `[nil]` pads to, so the two
|
|
33
|
+
# readings agree here, and {join} takes that `nil` back out of any position another member fills;
|
|
34
|
+
# - every other member takes the same `Dynamic[Top]` floor as the opaque one, and does NOT license the
|
|
35
|
+
# spread on its own. It is the member the union knows least about, and joining it as a value would put
|
|
36
|
+
# a nil-bearing element beside a nil-free one: `String#scan`'s `String | Array[String?]` yield joins a
|
|
37
|
+
# capture's `String?` against the whole match's `String` and fires `call.possible-nil-receiver` on the
|
|
38
|
+
# correlated-invariant `refinements[name.to_sym]`, which ADR-5 ranks above the precision.
|
|
39
|
+
#
|
|
40
|
+
# The spread needs at least one Tuple, `Array[T]` or opaque array member; a carrier with none of them
|
|
41
|
+
# ({#for} answers nil) leaves the binder's declared types alone, which binds the value to the first
|
|
42
|
+
# parameter and `Dynamic[Top]` after. So a lone value with no `to_ary` is NOT wrapped as `[value]` the
|
|
43
|
+
# way `a, b = value` wraps it: the one-value premise of auto-splat is the RBS block signature's arity,
|
|
44
|
+
# which nothing here can check against the actual yield, and a wrong wrap binds `nil` to a parameter the
|
|
45
|
+
# block receives a value in.
|
|
46
|
+
#
|
|
47
|
+
# See docs/internal-spec/inference-engine.md for the binding contract.
|
|
48
|
+
module BlockAutoSplat
|
|
49
|
+
# The positional part of a parameter list, as counts: all the auto-splat rule reads. An explicit
|
|
50
|
+
# `BlockParametersNode` list and a numbered-parameter block (`maximum` required positionals) both
|
|
51
|
+
# reduce to it, so the two spellings cannot drift.
|
|
52
|
+
ParameterShape = Data.define(:required, :optional, :post, :rest) do
|
|
53
|
+
def self.of(params_node)
|
|
54
|
+
new(required: params_node.requireds.size, optional: params_node.optionals.size,
|
|
55
|
+
post: params_node.posts.size, rest: !params_node.rest.nil?)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The `_1` / `_2` form, read as the explicit list of `arity` required positionals.
|
|
59
|
+
def self.of_arity(arity)
|
|
60
|
+
new(required: arity, optional: 0, post: 0, rest: false)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def positional_count = required + optional + post
|
|
64
|
+
|
|
65
|
+
# CRuby's own condition (`vm_callee_setup_block_arg` / `setup_parameters_complex`): a block splats a
|
|
66
|
+
# lone array argument when it has a mandatory positional (`lead + post > 0`) or more than one
|
|
67
|
+
# optional, except a bare `|a|` (the iseq's `ambiguous_param0`). So `|k, *r|`, `|*r, v|`,
|
|
68
|
+
# `|a = 1, b = 2|` and the trailing-comma `|k,|` (Prism's `ImplicitRestNode`) splat, while `|*r|`,
|
|
69
|
+
# `|a = 1, *r|` and `|a, &b|` / `|a, k: 1|` do not.
|
|
70
|
+
def splats?
|
|
71
|
+
mandatory = required + post
|
|
72
|
+
return false unless mandatory.positive? || optional > 1
|
|
73
|
+
|
|
74
|
+
!(mandatory == 1 && optional.zero? && !rest)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# A positional table: the type per position, the positions bound optimistically, and the named rest's
|
|
79
|
+
# type (nil for the `Array[Dynamic[Top]]` default). One arm produces one; {join} folds them into one.
|
|
80
|
+
# Private as a name — {#for} hands the folded value out, and nothing outside constructs one.
|
|
81
|
+
Table = Data.define(:types, :optimistic_positions, :rest_type)
|
|
82
|
+
private_constant :Table
|
|
83
|
+
|
|
84
|
+
# The arm kinds that make a member an array as far as the spread is concerned, so that a union
|
|
85
|
+
# carrying one of them spreads even where its other members do not decompose.
|
|
86
|
+
ARRAY_CARRIER_ARMS = %i[tuple array opaque].freeze
|
|
87
|
+
private_constant :ARRAY_CARRIER_ARMS
|
|
88
|
+
|
|
89
|
+
module_function
|
|
90
|
+
|
|
91
|
+
# @param shape — the block's {ParameterShape}.
|
|
92
|
+
# @param carrier — the one value the receiver yields.
|
|
93
|
+
# @return the folded {Table}, or nil when no member is an array carrier and the
|
|
94
|
+
# spread must leave the declared types alone.
|
|
95
|
+
def for(shape, carrier)
|
|
96
|
+
arms = arms_of(carrier)
|
|
97
|
+
return nil if arms.nil?
|
|
98
|
+
|
|
99
|
+
join(arms.map { |member, kind| table_for(shape, member, kind) })
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def arms_of(carrier)
|
|
103
|
+
members = carrier.is_a?(Type::Union) ? carrier.members : [carrier]
|
|
104
|
+
arms = members.map { |member| [member, arm_of(member)] }
|
|
105
|
+
arms.any? { |_, kind| ARRAY_CARRIER_ARMS.include?(kind) } ? arms : nil
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def arm_of(member)
|
|
109
|
+
return :tuple if member.is_a?(Type::Tuple)
|
|
110
|
+
return :array if MultiTargetBinder.array_element_type(member)
|
|
111
|
+
return :opaque if opaque_array_carrier?(member)
|
|
112
|
+
return :nil_value if nil_literal?(member)
|
|
113
|
+
|
|
114
|
+
:unknown
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# A carrier that is an array the module may not decompose into per-position types: the raw `Array`, an
|
|
118
|
+
# `Array[untyped]` / `Array[top]`, a `Refined` / `Difference` over either, and a `Dynamic` over ANY
|
|
119
|
+
# carrier that would license the spread — `Dynamic[Array[Integer]]`, `Dynamic[[Integer, String]]`,
|
|
120
|
+
# `Dynamic[Array[Integer] | nil]` alike. {MultiTargetBinder.array_element_type} declines every
|
|
121
|
+
# `Dynamic` wrapper on purpose: the value is gradual, and projecting its static facet would hand each
|
|
122
|
+
# position a bare `T` that licenses the negative rules (issue #1093, ADR-5). That is a reason to floor
|
|
123
|
+
# the positions, not a reason to leave the whole value on the first parameter, so the facet decides
|
|
124
|
+
# only whether CRuby would splat.
|
|
125
|
+
def opaque_array_carrier?(type)
|
|
126
|
+
case type
|
|
127
|
+
when Type::Nominal then type.class_name == "Array"
|
|
128
|
+
when Type::Refined, Type::Difference then opaque_array_carrier?(type.base)
|
|
129
|
+
when Type::Dynamic then !arms_of(type.static_facet).nil?
|
|
130
|
+
else false
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def table_for(shape, member, kind)
|
|
135
|
+
case kind
|
|
136
|
+
when :tuple then tuple_table(shape, member.elements)
|
|
137
|
+
when :array then array_table(shape, MultiTargetBinder.array_element_type(member))
|
|
138
|
+
when :nil_value then uniform_table(shape, member)
|
|
139
|
+
else uniform_table(shape, Type::Combinator.untyped)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# The split is mirrored rather than delegated to `MultiTargetBinder.decompose_tuple` because that one
|
|
144
|
+
# pads a missing slot with `Constant[nil]` and softens `X | nil`; a block slot past the tuple has
|
|
145
|
+
# always bound `Dynamic[Top]`. Without a rest the trailing positionals continue from the head (Ruby
|
|
146
|
+
# fills `|a, b = 1, c|` from the head when the tuple is long enough).
|
|
147
|
+
def tuple_table(shape, elements)
|
|
148
|
+
head = shape.required + shape.optional
|
|
149
|
+
posts = shape.post
|
|
150
|
+
tail_start = shape.rest ? [elements.size - posts, head].max : head
|
|
151
|
+
types = Array.new(head) { |i| elements[i] } + Array.new(posts) { |j| elements[tail_start + j] }
|
|
152
|
+
Table.new(types: types, optimistic_positions: [], rest_type: nil)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def array_table(shape, element)
|
|
156
|
+
required = shape.required
|
|
157
|
+
optional = shape.optional
|
|
158
|
+
posts = shape.post
|
|
159
|
+
Table.new(
|
|
160
|
+
types: Array.new(required) { element } + Array.new(optional) { Type::Combinator.untyped } +
|
|
161
|
+
Array.new(posts) { element },
|
|
162
|
+
optimistic_positions: (0...required).to_a + ((required + optional)...(required + optional + posts)).to_a,
|
|
163
|
+
rest_type: Type::Combinator.nominal_of("Array", type_args: [element])
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# One type for every position: the `Dynamic[Top]` floor of a member the module cannot decompose, or
|
|
168
|
+
# the `nil` a `nil` member hands each of them. The `nil` rest_type leaves a named rest on the binder's
|
|
169
|
+
# `Array[Dynamic[Top]]` default, which is right for both — CRuby gives `nil.tap { |a, *r| }` an empty
|
|
170
|
+
# `r`, and an opaque carrier says nothing about the leftovers.
|
|
171
|
+
def uniform_table(shape, type)
|
|
172
|
+
Table.new(types: Array.new(shape.positional_count) { type }, optimistic_positions: [], rest_type: nil)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# A single member's table is used as is. Across members, a position any member leaves unfilled (a slot
|
|
176
|
+
# past a short Tuple) or fills with `Dynamic[Top]` stays `Dynamic[Top]`, and a member's bare `nil`
|
|
177
|
+
# drops out of a position another member fills with a value, that position then being optimistic —
|
|
178
|
+
# `MultiTargetBinder`'s cross-member softening. A named rest joins only when every member supplies
|
|
179
|
+
# one; otherwise the binder's `Array[Dynamic[Top]]` default stands.
|
|
180
|
+
def join(tables)
|
|
181
|
+
types, softened = join_types(tables.map(&:types))
|
|
182
|
+
rests = tables.map(&:rest_type)
|
|
183
|
+
Table.new(
|
|
184
|
+
types: types,
|
|
185
|
+
optimistic_positions: (tables.flat_map(&:optimistic_positions) + softened).uniq,
|
|
186
|
+
rest_type: rests.include?(nil) ? nil : Type::Combinator.union(*rests)
|
|
187
|
+
)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# @return the joined table and the positions it softened.
|
|
191
|
+
def join_types(tables)
|
|
192
|
+
return [tables.first, []] if tables.size == 1
|
|
193
|
+
|
|
194
|
+
softened = []
|
|
195
|
+
joined = Array.new(tables.map(&:size).max) do |i|
|
|
196
|
+
column = tables.map { |table| table[i] }
|
|
197
|
+
next Type::Combinator.untyped if column.any? { |t| t.nil? || t == Type::Combinator.untyped }
|
|
198
|
+
|
|
199
|
+
firm = column.reject { |t| nil_literal?(t) }
|
|
200
|
+
next Type::Combinator.union(*column) if firm.empty? || firm.size == column.size
|
|
201
|
+
|
|
202
|
+
softened << i
|
|
203
|
+
Type::Combinator.union(*firm)
|
|
204
|
+
end
|
|
205
|
+
[joined, softened]
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def nil_literal?(type) = type.is_a?(Type::Constant) && type.value.nil?
|
|
209
|
+
|
|
210
|
+
# {#for} is the whole surface; the arms and the fold are this module's own business, and keeping them
|
|
211
|
+
# off the singleton stops a caller reaching past the fold for one member's table.
|
|
212
|
+
private_class_method :arms_of, :arm_of, :opaque_array_carrier?, :table_for, :tuple_table, :array_table,
|
|
213
|
+
:uniform_table, :join, :join_types, :nil_literal?
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|