rigortype 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/data/core_overlay/csv.rbs +28 -0
- data/data/core_overlay/psych.rbs +22 -0
- data/docs/handbook/01-getting-started.md +9 -1
- data/docs/handbook/02-everyday-types.md +4 -1
- data/docs/handbook/08-understanding-errors.md +3 -3
- data/docs/manual/01-installation.md +1 -0
- data/docs/manual/02-cli-reference.md +16 -7
- data/docs/manual/04-diagnostics.md +7 -4
- data/docs/manual/06-baseline.md +5 -2
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/14-rails-quickstart.md +4 -2
- data/docs/manual/15-type-protection-coverage.md +27 -4
- data/docs/manual/plugins/rigor-actionpack.md +1 -1
- data/docs/manual/plugins/rigor-activerecord.md +12 -5
- data/docs/manual/plugins/rigor-rails-routes.md +11 -0
- data/lib/rigor/analysis/baseline.rb +56 -96
- data/lib/rigor/analysis/buffer_binding.rb +13 -20
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +29 -48
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +35 -54
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +23 -37
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +16 -23
- data/lib/rigor/analysis/check_rules/rule_walk.rb +48 -71
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +19 -28
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +48 -67
- data/lib/rigor/analysis/check_rules.rb +174 -274
- data/lib/rigor/analysis/dependency_recorder.rb +26 -38
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +13 -25
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +17 -30
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +15 -26
- data/lib/rigor/analysis/dependency_source_inference/index.rb +45 -76
- data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +23 -40
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +50 -83
- data/lib/rigor/analysis/dependency_source_inference.rb +6 -10
- data/lib/rigor/analysis/diagnostic.rb +48 -75
- data/lib/rigor/analysis/erb_template_detector.rb +13 -18
- data/lib/rigor/analysis/fact_store.rb +12 -22
- data/lib/rigor/analysis/incremental.rb +47 -63
- data/lib/rigor/analysis/incremental_session.rb +74 -100
- data/lib/rigor/analysis/project_scan.rb +12 -23
- data/lib/rigor/analysis/result.rb +3 -5
- data/lib/rigor/analysis/rule_catalog.rb +67 -95
- data/lib/rigor/analysis/run_stats.rb +38 -61
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +101 -160
- data/lib/rigor/analysis/runner/pool_coordinator.rb +121 -188
- data/lib/rigor/analysis/runner/project_pre_passes.rb +62 -96
- data/lib/rigor/analysis/runner/run_snapshots.rb +12 -18
- data/lib/rigor/analysis/runner.rb +220 -333
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +30 -43
- data/lib/rigor/analysis/worker_session.rb +64 -102
- data/lib/rigor/ast/type_node.rb +6 -8
- data/lib/rigor/ast.rb +6 -10
- data/lib/rigor/bleeding_edge.rb +27 -35
- data/lib/rigor/builtins/hkt_builtins.rb +7 -14
- data/lib/rigor/builtins/imported_refinements.rb +106 -149
- data/lib/rigor/builtins/predefined_constant_refinements.rb +51 -65
- data/lib/rigor/builtins/regex_refinement.rb +32 -43
- data/lib/rigor/builtins/static_return_refinements.rb +57 -81
- data/lib/rigor/cache/descriptor.rb +58 -94
- data/lib/rigor/cache/incremental_snapshot.rb +32 -48
- data/lib/rigor/cache/rbs_cache_producer.rb +10 -15
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +10 -16
- data/lib/rigor/cache/rbs_class_type_param_names.rb +10 -16
- data/lib/rigor/cache/rbs_constant_table.rb +8 -13
- data/lib/rigor/cache/rbs_descriptor.rb +8 -13
- data/lib/rigor/cache/rbs_environment.rb +11 -19
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +11 -19
- data/lib/rigor/cache/rbs_known_class_names.rb +8 -12
- data/lib/rigor/cache/store.rb +248 -186
- data/lib/rigor/cli/annotate_command.rb +51 -88
- data/lib/rigor/cli/baseline_command.rb +10 -17
- data/lib/rigor/cli/check_command.rb +121 -199
- data/lib/rigor/cli/check_runner_factory.rb +8 -14
- data/lib/rigor/cli/ci_detector.rb +9 -14
- data/lib/rigor/cli/command.rb +8 -14
- data/lib/rigor/cli/coverage_command.rb +62 -48
- data/lib/rigor/cli/coverage_mutation.rb +14 -19
- data/lib/rigor/cli/coverage_report.rb +0 -2
- data/lib/rigor/cli/coverage_scan.rb +7 -11
- data/lib/rigor/cli/diagnostic_formats.rb +46 -62
- data/lib/rigor/cli/diff_command.rb +10 -20
- data/lib/rigor/cli/docs_command.rb +26 -40
- data/lib/rigor/cli/doctor_command.rb +8 -9
- data/lib/rigor/cli/explain_command.rb +4 -7
- data/lib/rigor/cli/fused_protection_renderer.rb +4 -5
- data/lib/rigor/cli/fused_protection_report.rb +6 -8
- data/lib/rigor/cli/lsp_command.rb +11 -19
- data/lib/rigor/cli/mcp_command.rb +4 -6
- data/lib/rigor/cli/mutation_protection_renderer.rb +3 -4
- data/lib/rigor/cli/mutation_protection_report.rb +6 -9
- data/lib/rigor/cli/options.rb +11 -19
- data/lib/rigor/cli/plugin_command.rb +18 -30
- data/lib/rigor/cli/plugins_command.rb +29 -51
- data/lib/rigor/cli/plugins_renderer.rb +12 -20
- data/lib/rigor/cli/prism_colorizer.rb +13 -19
- data/lib/rigor/cli/protection_fork_scan.rb +55 -0
- data/lib/rigor/cli/protection_renderer.rb +5 -7
- data/lib/rigor/cli/protection_report.rb +34 -18
- data/lib/rigor/cli/renderable.rb +4 -6
- data/lib/rigor/cli/show_bleedingedge_command.rb +5 -8
- data/lib/rigor/cli/sig_gen_command.rb +14 -26
- data/lib/rigor/cli/skill_command.rb +30 -47
- data/lib/rigor/cli/skill_describe.rb +40 -64
- data/lib/rigor/cli/trace_command.rb +9 -16
- data/lib/rigor/cli/trace_renderer.rb +35 -51
- data/lib/rigor/cli/triage_command.rb +6 -10
- data/lib/rigor/cli/triage_renderer.rb +1 -2
- data/lib/rigor/cli/type_of_command.rb +14 -23
- data/lib/rigor/cli/type_of_renderer.rb +3 -5
- data/lib/rigor/cli/type_scan_command.rb +6 -10
- data/lib/rigor/cli/type_scan_renderer.rb +3 -5
- data/lib/rigor/cli/type_scan_report.rb +2 -3
- data/lib/rigor/cli/upgrade_command.rb +2 -3
- data/lib/rigor/cli.rb +16 -28
- data/lib/rigor/config_audit.rb +35 -44
- data/lib/rigor/configuration/dependencies.rb +41 -70
- data/lib/rigor/configuration/severity_profile.rb +28 -42
- data/lib/rigor/configuration.rb +150 -241
- data/lib/rigor/environment/bundle_sig_discovery.rb +74 -118
- data/lib/rigor/environment/class_registry.rb +19 -26
- data/lib/rigor/environment/constant_type_cache_holder.rb +14 -21
- data/lib/rigor/environment/hkt_registry_holder.rb +6 -12
- data/lib/rigor/environment/lockfile_resolver.rb +31 -47
- data/lib/rigor/environment/missing_gem_constant_index.rb +128 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +39 -63
- data/lib/rigor/environment/rbs_coverage_report.rb +24 -42
- data/lib/rigor/environment/rbs_loader.rb +420 -518
- data/lib/rigor/environment/reflection.rb +28 -53
- data/lib/rigor/environment/reporters.rb +13 -24
- data/lib/rigor/environment.rb +225 -304
- data/lib/rigor/flow_contribution/conflict.rb +16 -27
- data/lib/rigor/flow_contribution/element.rb +7 -12
- data/lib/rigor/flow_contribution/fact.rb +33 -57
- data/lib/rigor/flow_contribution/merge_result.rb +6 -9
- data/lib/rigor/flow_contribution/merger.rb +32 -47
- data/lib/rigor/flow_contribution.rb +37 -55
- data/lib/rigor/inference/acceptance.rb +133 -219
- data/lib/rigor/inference/block_parameter_binder.rb +52 -82
- data/lib/rigor/inference/body_fixpoint.rb +22 -30
- data/lib/rigor/inference/budget_trace.rb +43 -63
- data/lib/rigor/inference/builtins/array_catalog.rb +4 -6
- data/lib/rigor/inference/builtins/comparable_catalog.rb +4 -6
- data/lib/rigor/inference/builtins/complex_catalog.rb +13 -19
- data/lib/rigor/inference/builtins/date_catalog.rb +38 -67
- data/lib/rigor/inference/builtins/encoding_catalog.rb +22 -36
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +4 -6
- data/lib/rigor/inference/builtins/exception_catalog.rb +38 -57
- data/lib/rigor/inference/builtins/hash_catalog.rb +9 -14
- data/lib/rigor/inference/builtins/method_catalog.rb +29 -48
- data/lib/rigor/inference/builtins/numeric_catalog.rb +13 -18
- data/lib/rigor/inference/builtins/pathname_catalog.rb +8 -13
- data/lib/rigor/inference/builtins/proc_catalog.rb +38 -62
- data/lib/rigor/inference/builtins/random_catalog.rb +21 -32
- data/lib/rigor/inference/builtins/range_catalog.rb +12 -21
- data/lib/rigor/inference/builtins/rational_catalog.rb +9 -15
- data/lib/rigor/inference/builtins/re_catalog.rb +29 -48
- data/lib/rigor/inference/builtins/set_catalog.rb +18 -27
- data/lib/rigor/inference/builtins/string_catalog.rb +12 -19
- data/lib/rigor/inference/builtins/struct_catalog.rb +18 -30
- data/lib/rigor/inference/builtins/time_catalog.rb +28 -44
- data/lib/rigor/inference/closure_escape_analyzer.rb +44 -67
- data/lib/rigor/inference/coverage_scanner.rb +13 -18
- data/lib/rigor/inference/def_return_typer.rb +8 -14
- data/lib/rigor/inference/dynamic_origin.rb +23 -20
- data/lib/rigor/inference/expression_typer.rb +794 -1154
- data/lib/rigor/inference/fallback.rb +8 -12
- data/lib/rigor/inference/fallback_tracer.rb +4 -10
- data/lib/rigor/inference/flow_tracer.rb +25 -36
- data/lib/rigor/inference/fork_map.rb +87 -0
- data/lib/rigor/inference/hkt_body.rb +45 -68
- data/lib/rigor/inference/hkt_body_parser.rb +33 -52
- data/lib/rigor/inference/hkt_reducer.rb +38 -59
- data/lib/rigor/inference/hkt_registry.rb +50 -76
- data/lib/rigor/inference/indexed_narrowing.rb +55 -81
- data/lib/rigor/inference/macro_block_self_type.rb +21 -34
- data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +11 -18
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +49 -83
- data/lib/rigor/inference/method_dispatcher/call_context.rb +16 -23
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +13 -22
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +307 -454
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +24 -34
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +22 -37
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +52 -81
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +51 -78
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +60 -97
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +23 -34
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +18 -26
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +37 -71
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +114 -182
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +122 -193
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +15 -24
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +64 -93
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +31 -45
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -9
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +356 -511
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +22 -32
- data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +15 -21
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +66 -82
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +10 -15
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -13
- data/lib/rigor/inference/method_dispatcher.rb +378 -524
- data/lib/rigor/inference/method_parameter_binder.rb +81 -114
- data/lib/rigor/inference/multi_target_binder.rb +51 -68
- data/lib/rigor/inference/mutation_widening.rb +115 -158
- data/lib/rigor/inference/narrowing.rb +627 -727
- data/lib/rigor/inference/origin_lookup.rb +38 -0
- data/lib/rigor/inference/parameter_inference_collector.rb +154 -109
- data/lib/rigor/inference/precision_scanner.rb +26 -35
- data/lib/rigor/inference/project_patched_methods.rb +20 -32
- data/lib/rigor/inference/project_patched_scanner.rb +23 -37
- data/lib/rigor/inference/protection_scanner.rb +16 -18
- data/lib/rigor/inference/rbs_type_translator.rb +49 -66
- data/lib/rigor/inference/scope_indexer.rb +473 -821
- data/lib/rigor/inference/statement_evaluator.rb +754 -1114
- data/lib/rigor/inference/struct_fold_safety.rb +34 -48
- data/lib/rigor/inference/synthetic_method.rb +9 -16
- data/lib/rigor/inference/synthetic_method_index.rb +20 -35
- data/lib/rigor/inference/synthetic_method_scanner.rb +63 -103
- data/lib/rigor/language_server/buffer_resolution.rb +6 -9
- data/lib/rigor/language_server/buffer_table.rb +11 -18
- data/lib/rigor/language_server/completion_provider.rb +69 -116
- data/lib/rigor/language_server/debouncer.rb +16 -25
- data/lib/rigor/language_server/diagnostic_publisher.rb +26 -40
- data/lib/rigor/language_server/document_symbol_provider.rb +14 -19
- data/lib/rigor/language_server/folding_range_provider.rb +11 -19
- data/lib/rigor/language_server/hover_provider.rb +8 -11
- data/lib/rigor/language_server/hover_renderer.rb +56 -93
- data/lib/rigor/language_server/loop.rb +14 -19
- data/lib/rigor/language_server/project_context.rb +46 -74
- data/lib/rigor/language_server/selection_range_provider.rb +9 -12
- data/lib/rigor/language_server/server.rb +55 -83
- data/lib/rigor/language_server/signature_help_provider.rb +29 -46
- data/lib/rigor/language_server/synchronized_writer.rb +4 -7
- data/lib/rigor/language_server/uri.rb +8 -13
- data/lib/rigor/language_server.rb +4 -6
- data/lib/rigor/mcp/loop.rb +2 -3
- data/lib/rigor/mcp/server.rb +4 -7
- data/lib/rigor/mcp.rb +3 -6
- data/lib/rigor/plugin/access_denied_error.rb +5 -8
- data/lib/rigor/plugin/additional_initializer.rb +21 -31
- data/lib/rigor/plugin/base.rb +335 -518
- data/lib/rigor/plugin/blueprint.rb +14 -23
- data/lib/rigor/plugin/box.rb +18 -29
- data/lib/rigor/plugin/fact_store.rb +16 -26
- data/lib/rigor/plugin/inflector.rb +37 -53
- data/lib/rigor/plugin/io_boundary.rb +33 -56
- data/lib/rigor/plugin/isolation.rb +42 -55
- data/lib/rigor/plugin/load_error.rb +10 -15
- data/lib/rigor/plugin/loader.rb +30 -49
- data/lib/rigor/plugin/macro/block_as_method.rb +20 -32
- data/lib/rigor/plugin/macro/heredoc_template.rb +35 -58
- data/lib/rigor/plugin/macro/nested_class_template.rb +22 -36
- data/lib/rigor/plugin/macro/trait_registry.rb +34 -51
- data/lib/rigor/plugin/macro.rb +10 -15
- data/lib/rigor/plugin/manifest.rb +85 -144
- data/lib/rigor/plugin/node_context.rb +14 -22
- data/lib/rigor/plugin/node_rule_walk.rb +49 -74
- data/lib/rigor/plugin/protocol_contract.rb +25 -39
- data/lib/rigor/plugin/registry.rb +132 -205
- data/lib/rigor/plugin/services.rb +21 -33
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -19
- data/lib/rigor/plugin/trust_policy.rb +24 -38
- data/lib/rigor/plugin/type_node_resolver.rb +15 -24
- data/lib/rigor/protection/diagnostic_oracle.rb +11 -13
- data/lib/rigor/protection/mutation_scanner.rb +27 -35
- data/lib/rigor/protection/mutator.rb +50 -70
- data/lib/rigor/protection/test_suite_oracle.rb +20 -27
- data/lib/rigor/rbs_extended/conformance_checker.rb +48 -68
- data/lib/rigor/rbs_extended/hkt_directives.rb +28 -54
- data/lib/rigor/rbs_extended/reporter.rb +24 -40
- data/lib/rigor/rbs_extended.rb +107 -197
- data/lib/rigor/reflection.rb +68 -86
- data/lib/rigor/scope/discovery_index.rb +14 -19
- data/lib/rigor/scope.rb +255 -310
- data/lib/rigor/sig_gen/classification.rb +6 -10
- data/lib/rigor/sig_gen/generator.rb +197 -323
- data/lib/rigor/sig_gen/layout_index.rb +12 -20
- data/lib/rigor/sig_gen/method_candidate.rb +12 -17
- data/lib/rigor/sig_gen/observation_collector.rb +38 -70
- data/lib/rigor/sig_gen/observed_call.rb +13 -23
- data/lib/rigor/sig_gen/path_mapper.rb +17 -29
- data/lib/rigor/sig_gen/renderer.rb +7 -13
- data/lib/rigor/sig_gen/type_elaborator.rb +15 -28
- data/lib/rigor/sig_gen/write_result.rb +8 -16
- data/lib/rigor/sig_gen/writer.rb +95 -174
- data/lib/rigor/sig_gen.rb +3 -6
- data/lib/rigor/signature_path_audit.rb +24 -30
- data/lib/rigor/source/constant_path.rb +10 -14
- data/lib/rigor/source/literals.rb +31 -45
- data/lib/rigor/source/node_locator.rb +9 -11
- data/lib/rigor/source/node_walker.rb +9 -13
- data/lib/rigor/source.rb +3 -4
- data/lib/rigor/testing.rb +16 -20
- data/lib/rigor/triage/catalogue.rb +38 -62
- data/lib/rigor/triage.rb +31 -52
- data/lib/rigor/trinary.rb +9 -13
- data/lib/rigor/type/acceptance_router.rb +4 -6
- data/lib/rigor/type/accepts_result.rb +10 -14
- data/lib/rigor/type/app.rb +19 -27
- data/lib/rigor/type/bot.rb +4 -6
- data/lib/rigor/type/bound_method.rb +10 -15
- data/lib/rigor/type/combinator.rb +165 -257
- data/lib/rigor/type/constant.rb +23 -34
- data/lib/rigor/type/data_class.rb +10 -15
- data/lib/rigor/type/data_instance.rb +14 -20
- data/lib/rigor/type/difference.rb +21 -32
- data/lib/rigor/type/dynamic.rb +3 -5
- data/lib/rigor/type/hash_shape.rb +32 -18
- data/lib/rigor/type/integer_range.rb +11 -16
- data/lib/rigor/type/intersection.rb +27 -42
- data/lib/rigor/type/nominal.rb +10 -15
- data/lib/rigor/type/plain_lattice.rb +9 -13
- data/lib/rigor/type/refined.rb +67 -114
- data/lib/rigor/type/singleton.rb +4 -6
- data/lib/rigor/type/struct_class.rb +11 -16
- data/lib/rigor/type/struct_instance.rb +15 -21
- data/lib/rigor/type/tuple.rb +14 -19
- data/lib/rigor/type/union.rb +30 -42
- data/lib/rigor/type_node/generic.rb +14 -26
- data/lib/rigor/type_node/identifier.rb +12 -19
- data/lib/rigor/type_node.rb +3 -12
- data/lib/rigor/value_semantics.rb +16 -21
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +18 -30
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +16 -29
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +12 -23
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +22 -39
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +26 -46
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +51 -87
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +10 -17
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +26 -39
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +141 -184
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +30 -51
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +57 -92
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +92 -108
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +13 -22
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +15 -25
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +8 -13
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +16 -24
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +40 -48
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +140 -129
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +56 -59
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +27 -41
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +16 -24
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +172 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +168 -241
- data/plugins/rigor-activerecord/lib/rigor-activerecord.rb +3 -5
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +8 -14
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +9 -15
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +4 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +23 -41
- data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +3 -5
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +6 -10
- data/plugins/rigor-activesupport-core-ext/lib/rigor-activesupport-core-ext.rb +8 -11
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +32 -0
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +30 -45
- data/plugins/rigor-devise/lib/rigor-devise.rb +3 -5
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +43 -69
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +26 -35
- data/plugins/rigor-dry-schema/lib/rigor-dry-schema.rb +3 -5
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +30 -47
- data/plugins/rigor-dry-struct/lib/rigor-dry-struct.rb +3 -5
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +67 -113
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +29 -46
- data/plugins/rigor-dry-types/lib/rigor-dry-types.rb +3 -5
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -20
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +14 -23
- data/plugins/rigor-dry-validation/lib/rigor-dry-validation.rb +2 -3
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +22 -37
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +28 -46
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +13 -17
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -47
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +52 -80
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +21 -31
- data/plugins/rigor-graphql/lib/rigor-graphql.rb +3 -5
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +11 -17
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +20 -34
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +45 -75
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +22 -36
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +22 -41
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +17 -32
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +6 -12
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +4 -8
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +17 -32
- data/plugins/rigor-rails/lib/rigor-rails.rb +11 -18
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +58 -92
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +20 -34
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +20 -31
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +49 -82
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +55 -92
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +51 -90
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +17 -30
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +189 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +38 -70
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +61 -75
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +319 -505
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +91 -96
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +32 -54
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +8 -13
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +9 -16
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +24 -39
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +13 -22
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +34 -56
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +22 -42
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +43 -75
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +27 -39
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +14 -19
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +20 -34
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +28 -47
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +21 -37
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +13 -22
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +18 -31
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +6 -10
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +14 -29
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +23 -41
- data/plugins/rigor-sinatra/lib/rigor-sinatra.rb +3 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +23 -38
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +57 -96
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +22 -35
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +41 -69
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +6 -10
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +24 -41
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +29 -50
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +55 -88
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +159 -262
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +21 -29
- data/plugins/rigor-statesman/lib/rigor-statesman.rb +3 -5
- data/plugins/rigor-typescript-utility-types/lib/rigor/plugin/typescript_utility_types.rb +23 -37
- data/plugins/rigor-typescript-utility-types/lib/rigor-typescript-utility-types.rb +4 -6
- data/sig/rigor/environment.rbs +1 -0
- data/sig/rigor/scope.rbs +6 -0
- metadata +9 -1
|
@@ -11,39 +11,31 @@ require_relative "../builtins/regex_refinement"
|
|
|
11
11
|
|
|
12
12
|
module Rigor
|
|
13
13
|
module Inference
|
|
14
|
-
# Control-flow predicate narrowing and type-lattice narrowing
|
|
15
|
-
# primitives.
|
|
14
|
+
# Control-flow predicate narrowing and type-lattice narrowing primitives.
|
|
16
15
|
#
|
|
17
16
|
# `Rigor::Inference::Narrowing` answers two related questions:
|
|
18
17
|
#
|
|
19
|
-
# 1. Type-level narrowing: given a `Rigor::Type` value, what is its
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
# equality/inequality against static literals, `case`/`when`,
|
|
29
|
-
# regex match globals, string predicates (`start_with?` etc.),
|
|
30
|
-
# key-presence, array emptiness, numeric comparison, and
|
|
18
|
+
# 1. Type-level narrowing: given a `Rigor::Type` value, what is its truthy fragment, its
|
|
19
|
+
# falsey fragment, its nil fragment, and its non-nil fragment? These primitives
|
|
20
|
+
# understand the value-lattice algebra (`Constant`, `Nominal`, `Singleton`, `Tuple`,
|
|
21
|
+
# `HashShape`, `Union`) and stay conservative on `Top` and `Dynamic[T]`.
|
|
22
|
+
# 2. Predicate-level narrowing: given a Prism predicate node and an entry scope, what are the
|
|
23
|
+
# truthy-edge scope and the falsey-edge scope? The catalogue covers truthiness, `nil?`,
|
|
24
|
+
# `!`, `&&`/`||`, class-membership (`is_a?`, `kind_of?`, `instance_of?`), trusted
|
|
25
|
+
# equality/inequality against static literals, `case`/`when`, regex match globals, string
|
|
26
|
+
# predicates (`start_with?` etc.), key-presence, array emptiness, numeric comparison, and
|
|
31
27
|
# `respond_to?`.
|
|
32
28
|
#
|
|
33
|
-
# Consumed by `Rigor::Inference::StatementEvaluator` to refine
|
|
34
|
-
# `
|
|
35
|
-
# `case`/`when` branches.
|
|
29
|
+
# Consumed by `Rigor::Inference::StatementEvaluator` to refine `then`/`else` scopes of
|
|
30
|
+
# `IfNode`/`UnlessNode` and `case`/`when` branches.
|
|
36
31
|
#
|
|
37
|
-
# The module is pure: every public function returns fresh values and
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
# `[truthy_scope, falsey_scope]` pair (the entry scope twice when no
|
|
42
|
-
# rule matches).
|
|
32
|
+
# The module is pure: every public function returns fresh values and MUST NOT mutate its
|
|
33
|
+
# inputs. Unrecognised predicate shapes degrade silently to "no narrowing" by returning
|
|
34
|
+
# `nil` from the internal analyser; the public `predicate_scopes` always returns an
|
|
35
|
+
# `[truthy_scope, falsey_scope]` pair (the entry scope twice when no rule matches).
|
|
43
36
|
#
|
|
44
37
|
# See docs/internal-spec/inference-engine.md (Narrowing) and
|
|
45
|
-
# docs/type-specification/control-flow-analysis.md for the
|
|
46
|
-
# binding contract.
|
|
38
|
+
# docs/type-specification/control-flow-analysis.md for the binding contract.
|
|
47
39
|
# rubocop:disable Metrics/ModuleLength
|
|
48
40
|
module Narrowing
|
|
49
41
|
TRUSTED_EQUALITY_LITERAL_CLASSES = [String, Symbol, Integer, TrueClass, FalseClass, NilClass].freeze
|
|
@@ -53,15 +45,13 @@ module Rigor
|
|
|
53
45
|
|
|
54
46
|
module_function
|
|
55
47
|
|
|
56
|
-
# Truthy fragment of `type`: the subset whose inhabitants are truthy
|
|
57
|
-
#
|
|
48
|
+
# Truthy fragment of `type`: the subset whose inhabitants are truthy in Ruby's sense
|
|
49
|
+
# (anything other than `nil` and `false`).
|
|
58
50
|
#
|
|
59
|
-
# `Top`, `Dynamic[T]`, `Bot`, `Singleton[C]`, `Tuple[*]`, and
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
# preserve its provenance under the value-lattice algebra; the
|
|
64
|
-
# remaining carriers are already truthy by inhabitance.
|
|
51
|
+
# `Top`, `Dynamic[T]`, `Bot`, `Singleton[C]`, `Tuple[*]`, and `HashShape{*}` flow through
|
|
52
|
+
# unchanged: Top/Dynamic stay conservative because the analyzer cannot express the
|
|
53
|
+
# difference type without a richer carrier and Dynamic must preserve its provenance under
|
|
54
|
+
# the value-lattice algebra; the remaining carriers are already truthy by inhabitance.
|
|
65
55
|
def narrow_truthy(type)
|
|
66
56
|
case type
|
|
67
57
|
when Type::Constant
|
|
@@ -75,9 +65,8 @@ module Rigor
|
|
|
75
65
|
end
|
|
76
66
|
end
|
|
77
67
|
|
|
78
|
-
# Falsey fragment of `type`: the subset whose inhabitants are
|
|
79
|
-
#
|
|
80
|
-
# collapse to `Bot`.
|
|
68
|
+
# Falsey fragment of `type`: the subset whose inhabitants are `nil` or `false`. Carriers
|
|
69
|
+
# that cannot inhabit a falsey value collapse to `Bot`.
|
|
81
70
|
def narrow_falsey(type)
|
|
82
71
|
case type
|
|
83
72
|
when Type::Constant then falsey_value?(type.value) ? type : Type::Combinator.bot
|
|
@@ -87,12 +76,10 @@ module Rigor
|
|
|
87
76
|
end
|
|
88
77
|
end
|
|
89
78
|
|
|
90
|
-
# Nil fragment of `type`: the subset whose inhabitants are `nil`.
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
# (`Singleton`, `Tuple`, `HashShape`) collapse to `Bot`. `Bot`
|
|
95
|
-
# is its own nil fragment.
|
|
79
|
+
# Nil fragment of `type`: the subset whose inhabitants are `nil`. Used by `nil?` predicate
|
|
80
|
+
# narrowing. `Top`/`Dynamic` narrow to the canonical `Constant[nil]` so downstream
|
|
81
|
+
# dispatch resolves through `NilClass`; carriers that never inhabit `nil` (`Singleton`,
|
|
82
|
+
# `Tuple`, `HashShape`) collapse to `Bot`. `Bot` is its own nil fragment.
|
|
96
83
|
def narrow_nil(type)
|
|
97
84
|
case type
|
|
98
85
|
when Type::Constant then type.value.nil? ? type : Type::Combinator.bot
|
|
@@ -102,9 +89,8 @@ module Rigor
|
|
|
102
89
|
end
|
|
103
90
|
end
|
|
104
91
|
|
|
105
|
-
# Non-nil fragment of `type`: the subset whose inhabitants are
|
|
106
|
-
#
|
|
107
|
-
# `x.nil?`.
|
|
92
|
+
# Non-nil fragment of `type`: the subset whose inhabitants are not `nil`. Mirror of
|
|
93
|
+
# {.narrow_nil} for the falsey edge of `x.nil?`.
|
|
108
94
|
def narrow_non_nil(type)
|
|
109
95
|
case type
|
|
110
96
|
when Type::Constant
|
|
@@ -114,23 +100,19 @@ module Rigor
|
|
|
114
100
|
when Type::Union
|
|
115
101
|
Type::Combinator.union(*type.members.map { |m| narrow_non_nil(m) })
|
|
116
102
|
else
|
|
117
|
-
# Top, Dynamic, Singleton, Tuple, HashShape, Bot: there is
|
|
118
|
-
#
|
|
119
|
-
# non-nil fragment.
|
|
103
|
+
# Top, Dynamic, Singleton, Tuple, HashShape, Bot: there is no nil contribution to
|
|
104
|
+
# remove, so the type is its own non-nil fragment.
|
|
120
105
|
type
|
|
121
106
|
end
|
|
122
107
|
end
|
|
123
108
|
|
|
124
|
-
# Three-valued truthiness certainty of a predicate's type,
|
|
125
|
-
#
|
|
126
|
-
# when no inhabitant is
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
# `
|
|
130
|
-
#
|
|
131
|
-
# (`ExpressionTyper#elide_or_union` on the value side,
|
|
132
|
-
# `StatementEvaluator#live_branch_for_if` on the scope side), so
|
|
133
|
-
# the type a dead branch is elided from and the scope that stops
|
|
109
|
+
# Three-valued truthiness certainty of a predicate's type, derived from the truthy /
|
|
110
|
+
# falsey fragments above: `:truthy` when no inhabitant is falsey (the falsey fragment is
|
|
111
|
+
# `Bot`), `:falsey` when no inhabitant is truthy, nil when both fragments are inhabited —
|
|
112
|
+
# or when the type itself is nil / `Bot` (dead code is not a certainty claim). This is the
|
|
113
|
+
# single owner of the judgment both branch-elision consumers read
|
|
114
|
+
# (`ExpressionTyper#elide_or_union` on the value side, `StatementEvaluator#live_branch_for_if`
|
|
115
|
+
# on the scope side), so the type a dead branch is elided from and the scope that stops
|
|
134
116
|
# flowing through it can never disagree.
|
|
135
117
|
def predicate_certainty(type)
|
|
136
118
|
return nil if type.nil? || type.is_a?(Type::Bot)
|
|
@@ -144,14 +126,12 @@ module Rigor
|
|
|
144
126
|
nil
|
|
145
127
|
end
|
|
146
128
|
|
|
147
|
-
# Three-valued certainty of `C === subject` for a class / module
|
|
148
|
-
#
|
|
149
|
-
#
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
#
|
|
153
|
-
# here so the branch a `case` expression's type drops and the
|
|
154
|
-
# clause whose body scope goes dead derive from one judgment.
|
|
129
|
+
# Three-valued certainty of `C === subject` for a class / module `when` pattern, derived
|
|
130
|
+
# from {.narrow_class} / {.narrow_not_class}: `:no` when no inhabitant of the subject
|
|
131
|
+
# matches, `:yes` when every inhabitant matches, `:maybe` otherwise. The value-side
|
|
132
|
+
# counterpart of the scope narrowing {.case_when_scopes} performs for the same condition
|
|
133
|
+
# shape, kept here so the branch a `case` expression's type drops and the clause whose
|
|
134
|
+
# body scope goes dead derive from one judgment.
|
|
155
135
|
def class_pattern_certainty(subject_type, class_name, environment:)
|
|
156
136
|
truthy_bot = narrow_class(subject_type, class_name, environment: environment).is_a?(Type::Bot)
|
|
157
137
|
falsey_bot = narrow_not_class(subject_type, class_name, environment: environment).is_a?(Type::Bot)
|
|
@@ -162,18 +142,16 @@ module Rigor
|
|
|
162
142
|
:maybe
|
|
163
143
|
end
|
|
164
144
|
|
|
165
|
-
# Classes whose `===` is plain value equality, so a literal
|
|
166
|
-
#
|
|
167
|
-
#
|
|
168
|
-
# and stays `:maybe` in {.value_pattern_certainty}.
|
|
145
|
+
# Classes whose `===` is plain value equality, so a literal `when` pattern against a
|
|
146
|
+
# pinned `Constant` subject is exact in both directions. Anything else keeps custom-`===`
|
|
147
|
+
# semantics and stays `:maybe` in {.value_pattern_certainty}.
|
|
169
148
|
VALUE_EQUALITY_CLASSES = [Integer, Float, Rational, Complex, String, Symbol,
|
|
170
149
|
TrueClass, FalseClass, NilClass].freeze
|
|
171
150
|
|
|
172
|
-
# Three-valued certainty of `<literal> === subject` for a
|
|
173
|
-
#
|
|
174
|
-
#
|
|
175
|
-
#
|
|
176
|
-
# isn't pinned, or `===` may be user-defined).
|
|
151
|
+
# Three-valued certainty of `<literal> === subject` for a value-equality literal pattern:
|
|
152
|
+
# exact (`:yes` / `:no`) only when the subject is itself a pinned `Constant` of a
|
|
153
|
+
# value-equality class; `:maybe` otherwise (the runtime value isn't pinned, or `===` may
|
|
154
|
+
# be user-defined).
|
|
177
155
|
def value_pattern_certainty(subject_type, pattern_value)
|
|
178
156
|
return :maybe unless subject_type.is_a?(Type::Constant)
|
|
179
157
|
return :maybe unless VALUE_EQUALITY_CLASSES.any? { |klass| subject_type.value.is_a?(klass) }
|
|
@@ -183,10 +161,9 @@ module Rigor
|
|
|
183
161
|
|
|
184
162
|
# Equality fragment of `type` against a trusted literal.
|
|
185
163
|
#
|
|
186
|
-
# String/Symbol/Integer equality narrows only when the current
|
|
187
|
-
#
|
|
188
|
-
#
|
|
189
|
-
# mixed union such as `Integer | nil` without manufacturing a new
|
|
164
|
+
# String/Symbol/Integer equality narrows only when the current domain is already a
|
|
165
|
+
# finite union of trusted literals. Nil and booleans are singleton values, so they can be
|
|
166
|
+
# extracted from a mixed union such as `Integer | nil` without manufacturing a new
|
|
190
167
|
# positive domain from the comparison alone.
|
|
191
168
|
def narrow_equal(type, literal)
|
|
192
169
|
return type unless trusted_equality_literal?(literal)
|
|
@@ -200,9 +177,9 @@ module Rigor
|
|
|
200
177
|
end
|
|
201
178
|
end
|
|
202
179
|
|
|
203
|
-
# Complement of {.narrow_equal}. Negative facts are domain-relative:
|
|
204
|
-
#
|
|
205
|
-
#
|
|
180
|
+
# Complement of {.narrow_equal}. Negative facts are domain-relative: they remove a literal
|
|
181
|
+
# from an already-known domain but do not create an unbounded difference type when the
|
|
182
|
+
# domain is broad or dynamic.
|
|
206
183
|
def narrow_not_equal(type, literal)
|
|
207
184
|
return type unless trusted_equality_literal?(literal)
|
|
208
185
|
|
|
@@ -215,46 +192,41 @@ module Rigor
|
|
|
215
192
|
end
|
|
216
193
|
end
|
|
217
194
|
|
|
218
|
-
# Integer-comparison fragment of `type` against an Integer
|
|
219
|
-
#
|
|
220
|
-
#
|
|
221
|
-
#
|
|
222
|
-
#
|
|
223
|
-
#
|
|
224
|
-
#
|
|
225
|
-
#
|
|
226
|
-
#
|
|
227
|
-
#
|
|
228
|
-
# - `Nominal[Integer]` becomes the half-line itself (e.g.
|
|
229
|
-
# `x > 0` on `Nominal[Integer]` is `positive_int`).
|
|
195
|
+
# Integer-comparison fragment of `type` against an Integer literal `bound`. Narrows the
|
|
196
|
+
# receiver of `x < n`, `x <= n`, `x > n`, `x >= n` (and the reversed forms) to the subset
|
|
197
|
+
# of the existing domain that satisfies the comparison. Hooks in:
|
|
198
|
+
# - `Constant<Integer>` is preserved when it satisfies the comparison, otherwise collapsed
|
|
199
|
+
# to `Bot`.
|
|
200
|
+
# - `IntegerRange[a..b]` becomes the intersection with the half-line implied by the
|
|
201
|
+
# comparison; an empty intersection collapses to `Bot`, a single-point intersection
|
|
202
|
+
# collapses to `Constant<Integer>`.
|
|
203
|
+
# - `Nominal[Integer]` becomes the half-line itself (e.g. `x > 0` on `Nominal[Integer]` is
|
|
204
|
+
# `positive_int`).
|
|
230
205
|
# - `Union` narrows each member independently.
|
|
231
|
-
# - Other carriers (Float, String, Top, Dynamic) flow through
|
|
232
|
-
#
|
|
233
|
-
#
|
|
206
|
+
# - Other carriers (Float, String, Top, Dynamic) flow through unchanged: the analyzer does
|
|
207
|
+
# not have a Float-range carrier today, and no other carrier participates in numeric
|
|
208
|
+
# ordering.
|
|
234
209
|
def narrow_integer_comparison(type, comparator, bound)
|
|
235
210
|
return type unless bound.is_a?(Integer) && %i[< <= > >=].include?(comparator)
|
|
236
211
|
|
|
237
212
|
narrow_integer_comparison_dispatch(type, comparator, bound)
|
|
238
213
|
end
|
|
239
214
|
|
|
240
|
-
# Equality fragment of `type` against an Integer `value`.
|
|
241
|
-
# `
|
|
242
|
-
#
|
|
243
|
-
# narrows to `Constant[value]
|
|
244
|
-
# collapses to `Bot`. `Nominal[Integer]` narrows to
|
|
245
|
-
# `Constant[value]`. `Union` narrows each member.
|
|
215
|
+
# Equality fragment of `type` against an Integer `value`. `Constant<Integer>` is preserved
|
|
216
|
+
# when it equals `value`, otherwise collapses to `Bot`. `IntegerRange` covers? `value`
|
|
217
|
+
# narrows to `Constant[value]`; an out-of-range comparison collapses to `Bot`.
|
|
218
|
+
# `Nominal[Integer]` narrows to `Constant[value]`. `Union` narrows each member.
|
|
246
219
|
def narrow_integer_equal(type, value)
|
|
247
220
|
return type unless value.is_a?(Integer)
|
|
248
221
|
|
|
249
222
|
narrow_integer_equal_dispatch(type, value)
|
|
250
223
|
end
|
|
251
224
|
|
|
252
|
-
# Complement of {.narrow_integer_equal}. Removes a single
|
|
253
|
-
#
|
|
254
|
-
#
|
|
255
|
-
#
|
|
256
|
-
#
|
|
257
|
-
# require a two-piece carrier the lattice does not yet model.
|
|
225
|
+
# Complement of {.narrow_integer_equal}. Removes a single integer value from the domain
|
|
226
|
+
# when one endpoint of an `IntegerRange` is exactly that value (so the result stays a
|
|
227
|
+
# contiguous range). Domains where the value sits strictly between the endpoints stay
|
|
228
|
+
# unchanged: punching a hole would require a two-piece carrier the lattice does not yet
|
|
229
|
+
# model.
|
|
258
230
|
def narrow_integer_not_equal(type, value)
|
|
259
231
|
return type unless value.is_a?(Integer)
|
|
260
232
|
|
|
@@ -270,70 +242,54 @@ module Rigor
|
|
|
270
242
|
end
|
|
271
243
|
end
|
|
272
244
|
|
|
273
|
-
# Class-membership fragment of `type`: the subset whose
|
|
274
|
-
#
|
|
275
|
-
#
|
|
276
|
-
#
|
|
277
|
-
#
|
|
278
|
-
# / `if x.kind_of?(C)` / `if x.instance_of?(C)` truthy edge.
|
|
245
|
+
# Class-membership fragment of `type`: the subset whose inhabitants are instances of
|
|
246
|
+
# `class_name` (or its subclasses when `exact: false`). `class_name` is the qualified name
|
|
247
|
+
# of the class as it appears in source (`"Integer"`, `"Foo::Bar"`). Slice 6 phase 2
|
|
248
|
+
# sub-phase 1 narrows the `if x.is_a?(C)` / `if x.kind_of?(C)` / `if x.instance_of?(C)`
|
|
249
|
+
# truthy edge.
|
|
279
250
|
#
|
|
280
|
-
# Nominal narrowing is hierarchy-aware through the analyzer
|
|
281
|
-
#
|
|
282
|
-
# `
|
|
283
|
-
#
|
|
284
|
-
#
|
|
285
|
-
#
|
|
286
|
-
# fall back to the conservative answer (the type unchanged) so
|
|
287
|
-
# the analyzer never asserts narrowing it cannot prove.
|
|
251
|
+
# Nominal narrowing is hierarchy-aware through the analyzer environment: when the bound
|
|
252
|
+
# type is a supertype of `class_name` the result narrows DOWN to `Nominal[class_name]`
|
|
253
|
+
# (e.g., `Numeric & Integer = Integer`); when the bound type is already a subtype it is
|
|
254
|
+
# preserved; disjoint hierarchies collapse to `Bot`. Classes the environment cannot
|
|
255
|
+
# resolve fall back to the conservative answer (the type unchanged) so the analyzer never
|
|
256
|
+
# asserts narrowing it cannot prove.
|
|
288
257
|
def narrow_class(type, class_name, exact: false, environment: Environment.default)
|
|
289
258
|
context = ClassNarrowingContext.new(exact: exact, polarity: :positive, environment: environment)
|
|
290
259
|
narrow_class_dispatch(type, class_name, context)
|
|
291
260
|
end
|
|
292
261
|
|
|
293
|
-
# Mirror of {.narrow_class} for the falsey edge of
|
|
294
|
-
#
|
|
295
|
-
#
|
|
296
|
-
#
|
|
297
|
-
# unchanged) because the analyzer cannot prove the negative
|
|
298
|
-
# without a richer carrier.
|
|
262
|
+
# Mirror of {.narrow_class} for the falsey edge of `is_a?`/`kind_of?`/`instance_of?`.
|
|
263
|
+
# Inhabitants that DO satisfy the predicate are removed; inhabitants that do not are
|
|
264
|
+
# preserved. Conservative on Top/Dynamic/Bot (preserved unchanged) because the analyzer
|
|
265
|
+
# cannot prove the negative without a richer carrier.
|
|
299
266
|
def narrow_not_class(type, class_name, exact: false, environment: Environment.default)
|
|
300
267
|
context = ClassNarrowingContext.new(exact: exact, polarity: :negative, environment: environment)
|
|
301
268
|
narrow_class_dispatch(type, class_name, context)
|
|
302
269
|
end
|
|
303
270
|
|
|
304
|
-
# Negation pair for `assert_value is ~refinement` /
|
|
305
|
-
#
|
|
306
|
-
#
|
|
307
|
-
# local's domain `current_type`.
|
|
271
|
+
# Negation pair for `assert_value is ~refinement` / `predicate-if-* … is ~refinement`
|
|
272
|
+
# directives. Computes the complement of `refinement` within the current local's domain
|
|
273
|
+
# `current_type`.
|
|
308
274
|
#
|
|
309
275
|
# Carrier-by-carrier rules:
|
|
310
276
|
#
|
|
311
|
-
# - `Difference[base, Constant[v]]`. Complement of
|
|
312
|
-
#
|
|
313
|
-
#
|
|
314
|
-
# `
|
|
315
|
-
#
|
|
316
|
-
#
|
|
317
|
-
# `
|
|
318
|
-
#
|
|
319
|
-
#
|
|
320
|
-
#
|
|
321
|
-
#
|
|
322
|
-
#
|
|
323
|
-
#
|
|
324
|
-
#
|
|
325
|
-
#
|
|
326
|
-
#
|
|
327
|
-
# Morgan: `D \ (M1 ∩ M2) = (D \ M1) ∪ (D \ M2)`. Each
|
|
328
|
-
# member's complement is computed independently within
|
|
329
|
-
# `current_type` and the results are unioned. Members
|
|
330
|
-
# the algebra cannot complement (Refined, non-Constant
|
|
331
|
-
# Difference, …) contribute `current_type` itself, so
|
|
332
|
-
# the union widens the answer to `current_type` —
|
|
333
|
-
# sound but imprecise.
|
|
334
|
-
# - `Refined[base, predicate]`. Predicate complements are
|
|
335
|
-
# not reducible to a finite carrier without a richer
|
|
336
|
-
# shape (e.g. `~lowercase-string` is "uppercase OR
|
|
277
|
+
# - `Difference[base, Constant[v]]`. Complement of `base \ {v}` within `current_type`.
|
|
278
|
+
# Walk the current type's union members, keep each part disjoint from `base`, and add
|
|
279
|
+
# the removed-value Constant once when any current member covers it. `assert s is
|
|
280
|
+
# ~non-empty-string` over `s: String | nil` narrows to `Constant[""] | NilClass`.
|
|
281
|
+
# - `IntegerRange[a, b]` (v0.0.5+ slice). Complement is the two open halves `int<min,
|
|
282
|
+
# a-1>` and `int<b+1, max>`, each intersected with the integer-domain parts of
|
|
283
|
+
# `current_type`. Non-integer parts (nil, String, …) of a Union receiver survive
|
|
284
|
+
# unchanged. `assert n is ~int<5, 10>` over `n: Integer | nil` narrows to `int<min, 4> |
|
|
285
|
+
# int<11, max> | NilClass`.
|
|
286
|
+
# - `Type::Intersection[M1, M2, …]` (v0.0.5+ slice). De Morgan: `D \ (M1 ∩ M2) = (D \ M1)
|
|
287
|
+
# ∪ (D \ M2)`. Each member's complement is computed independently within
|
|
288
|
+
# `current_type` and the results are unioned. Members the algebra cannot complement
|
|
289
|
+
# (Refined, non-Constant Difference, …) contribute `current_type` itself, so the union
|
|
290
|
+
# widens the answer to `current_type` — sound but imprecise.
|
|
291
|
+
# - `Refined[base, predicate]`. Predicate complements are not reducible to a finite
|
|
292
|
+
# carrier without a richer shape (e.g. `~lowercase-string` is "uppercase OR
|
|
337
293
|
# mixed-case"); `current_type` is returned unchanged.
|
|
338
294
|
def narrow_not_refinement(current_type, refinement_type)
|
|
339
295
|
case refinement_type
|
|
@@ -352,15 +308,12 @@ module Rigor
|
|
|
352
308
|
end
|
|
353
309
|
end
|
|
354
310
|
|
|
355
|
-
# ADR-7 § "Slice 4-A" — public Fact-shaped narrowing
|
|
356
|
-
#
|
|
357
|
-
#
|
|
358
|
-
#
|
|
359
|
-
#
|
|
360
|
-
#
|
|
361
|
-
# `narrow_class` and `narrow_not_refinement`; consumers
|
|
362
|
-
# outside `Narrowing` (today: `StatementEvaluator`'s
|
|
363
|
-
# post-return assertion path) reach for it via
|
|
311
|
+
# ADR-7 § "Slice 4-A" — public Fact-shaped narrowing entry. Distinguishes a
|
|
312
|
+
# `Nominal[<class>]`-typed Fact (uses `narrow_class` / `narrow_not_class` for
|
|
313
|
+
# hierarchy-aware narrowing) from a refinement-shaped Fact (refined types, IntegerRange,
|
|
314
|
+
# Difference, …). The implementation lives next to its sibling helpers `narrow_class` and
|
|
315
|
+
# `narrow_not_refinement`; consumers outside `Narrowing` (today:
|
|
316
|
+
# `StatementEvaluator`'s post-return assertion path) reach for it via
|
|
364
317
|
# `Rigor::Inference::Narrowing.narrow_for_fact`.
|
|
365
318
|
def narrow_for_fact(current, fact, environment)
|
|
366
319
|
if fact.type.is_a?(Type::Nominal) && fact.type.type_args.empty?
|
|
@@ -375,9 +328,8 @@ module Rigor
|
|
|
375
328
|
fact.type
|
|
376
329
|
end
|
|
377
330
|
|
|
378
|
-
# Public predicate analyser. Returns `[truthy_scope, falsey_scope]`,
|
|
379
|
-
#
|
|
380
|
-
# entries are the receiver scope unchanged.
|
|
331
|
+
# Public predicate analyser. Returns `[truthy_scope, falsey_scope]`, always; when no
|
|
332
|
+
# narrowing rule matches the predicate node both entries are the receiver scope unchanged.
|
|
381
333
|
#
|
|
382
334
|
# @param node [Prism::Node, nil]
|
|
383
335
|
# @param scope [Rigor::Scope]
|
|
@@ -391,29 +343,21 @@ module Rigor
|
|
|
391
343
|
|
|
392
344
|
# Slice 7 phase 5 — `case`/`when` narrowing.
|
|
393
345
|
#
|
|
394
|
-
# Given the subject of a `case` (the expression after the
|
|
395
|
-
# `
|
|
396
|
-
# nodes (`when_clause.conditions`), returns a pair of
|
|
397
|
-
# scopes:
|
|
346
|
+
# Given the subject of a `case` (the expression after the `case` keyword) and an array of
|
|
347
|
+
# `when`-clause condition nodes (`when_clause.conditions`), returns a pair of scopes:
|
|
398
348
|
#
|
|
399
|
-
# - `body_scope`: the scope under which the body of the
|
|
400
|
-
#
|
|
401
|
-
#
|
|
402
|
-
#
|
|
403
|
-
#
|
|
404
|
-
#
|
|
405
|
-
# (the next `when` or the `else`) MUST be evaluated.
|
|
406
|
-
# The subject is narrowed by the conjunction of every
|
|
407
|
-
# condition's falsey edge.
|
|
349
|
+
# - `body_scope`: the scope under which the body of the `when` clause MUST be evaluated.
|
|
350
|
+
# The subject local is narrowed by the union of every condition's truthy edge so the
|
|
351
|
+
# body sees the most specific type compatible with "any of the conditions matched".
|
|
352
|
+
# - `falsey_scope`: the scope under which the next branch (the next `when` or the `else`)
|
|
353
|
+
# MUST be evaluated. The subject is narrowed by the conjunction of every condition's
|
|
354
|
+
# falsey edge.
|
|
408
355
|
#
|
|
409
|
-
# The narrowing is best-effort: if the subject is not a
|
|
410
|
-
#
|
|
411
|
-
#
|
|
412
|
-
#
|
|
413
|
-
#
|
|
414
|
-
# constants narrow as `is_a?`; integer/float-endpoint
|
|
415
|
-
# ranges narrow to `Numeric`; string-endpoint ranges and
|
|
416
|
-
# regexp literals narrow to `String`.
|
|
356
|
+
# The narrowing is best-effort: if the subject is not a `Prism::LocalVariableReadNode` or
|
|
357
|
+
# none of the condition shapes are recognised, both returned scopes equal the input
|
|
358
|
+
# scope. The catalogue mirrors {.case_equality_target_class}: static class/module
|
|
359
|
+
# constants narrow as `is_a?`; integer/float-endpoint ranges narrow to `Numeric`;
|
|
360
|
+
# string-endpoint ranges and regexp literals narrow to `String`.
|
|
417
361
|
#
|
|
418
362
|
# @param subject [Prism::Node, nil] the `case` subject.
|
|
419
363
|
# @param conditions [Array<Prism::Node>] the `when`
|
|
@@ -421,12 +365,10 @@ module Rigor
|
|
|
421
365
|
# @param scope [Rigor::Scope]
|
|
422
366
|
# @return [Array(Rigor::Scope, Rigor::Scope)]
|
|
423
367
|
def case_when_scopes(subject, conditions, scope)
|
|
424
|
-
# C1 — `case x when /re/` runs `/re/ === x`, which sets the
|
|
425
|
-
#
|
|
426
|
-
#
|
|
427
|
-
#
|
|
428
|
-
# later clause may match a different regex. Applied even when
|
|
429
|
-
# the subject is not a narrowable local read.
|
|
368
|
+
# C1 — `case x when /re/` runs `/re/ === x`, which sets the regex match-data globals
|
|
369
|
+
# exactly as a successful `=~` does. Narrow `$~`/`$&`/`$1..$N` on the clause body (the
|
|
370
|
+
# match edge); the falsey scope keeps the entry globals because a later clause may match
|
|
371
|
+
# a different regex. Applied even when the subject is not a narrowable local read.
|
|
430
372
|
body_scope = apply_when_regex_globals(conditions, scope)
|
|
431
373
|
|
|
432
374
|
return [body_scope, scope] unless subject.is_a?(Prism::LocalVariableReadNode)
|
|
@@ -440,14 +382,12 @@ module Rigor
|
|
|
440
382
|
[truthy, falsey]
|
|
441
383
|
end
|
|
442
384
|
|
|
443
|
-
# When the clause has exactly one `RegularExpressionNode`
|
|
444
|
-
#
|
|
445
|
-
# edge
|
|
446
|
-
#
|
|
447
|
-
#
|
|
448
|
-
#
|
|
449
|
-
# alternative stay `String | nil`. With no regex condition the
|
|
450
|
-
# entry scope passes through unchanged.
|
|
385
|
+
# When the clause has exactly one `RegularExpressionNode` literal condition, narrow the
|
|
386
|
+
# match-data globals on the body edge (same rule as `analyse_regex_match_predicate`'s
|
|
387
|
+
# truthy edge). With multiple regex conditions (`when /a/, /b/`) the body is reachable
|
|
388
|
+
# through any of them, so only `$~`/`$&` are safely non-nil; numbered groups whose
|
|
389
|
+
# presence differs per alternative stay `String | nil`. With no regex condition the entry
|
|
390
|
+
# scope passes through unchanged.
|
|
451
391
|
def apply_when_regex_globals(conditions, scope)
|
|
452
392
|
regexes = conditions.grep(Prism::RegularExpressionNode)
|
|
453
393
|
return scope if regexes.empty?
|
|
@@ -462,10 +402,9 @@ module Rigor
|
|
|
462
402
|
truthy
|
|
463
403
|
end
|
|
464
404
|
|
|
465
|
-
# Internal analyser. Returns `[truthy_scope, falsey_scope]` when
|
|
466
|
-
#
|
|
467
|
-
#
|
|
468
|
-
# scope.
|
|
405
|
+
# Internal analyser. Returns `[truthy_scope, falsey_scope]` when the predicate shape is
|
|
406
|
+
# recognised, or `nil` to signal "no narrowing" so the public surface can fall back to the
|
|
407
|
+
# entry scope.
|
|
469
408
|
def analyse(node, scope)
|
|
470
409
|
case node
|
|
471
410
|
when Prism::ParenthesesNode
|
|
@@ -497,15 +436,12 @@ module Rigor
|
|
|
497
436
|
class << self
|
|
498
437
|
private
|
|
499
438
|
|
|
500
|
-
# Complement of `Difference[base, Constant[v]]` within
|
|
501
|
-
#
|
|
502
|
-
#
|
|
503
|
-
#
|
|
504
|
-
# the
|
|
505
|
-
#
|
|
506
|
-
# contained in the refinement (i.e. inside `base` and
|
|
507
|
-
# NOT equal to the removed value) are dropped — they are
|
|
508
|
-
# exactly the values the negation excludes.
|
|
439
|
+
# Complement of `Difference[base, Constant[v]]` within `current_type`. Walks the
|
|
440
|
+
# current type's union members, keeps each member disjoint from `base` (those values
|
|
441
|
+
# were never in the refinement to begin with), and adds the removed-value `Constant[v]`
|
|
442
|
+
# exactly once when any current member covers it. Members that are fully contained in
|
|
443
|
+
# the refinement (i.e. inside `base` and NOT equal to the removed value) are dropped —
|
|
444
|
+
# they are exactly the values the negation excludes.
|
|
509
445
|
def complement_difference(current_type, difference)
|
|
510
446
|
base = difference.base
|
|
511
447
|
removed = difference.removed
|
|
@@ -536,13 +472,11 @@ module Rigor
|
|
|
536
472
|
result.yes? || result.maybe?
|
|
537
473
|
end
|
|
538
474
|
|
|
539
|
-
# Complement of an `IntegerRange[a, b]` within
|
|
540
|
-
#
|
|
541
|
-
#
|
|
542
|
-
#
|
|
543
|
-
#
|
|
544
|
-
# `current_type`. Non-integer parts of a Union receiver
|
|
545
|
-
# (nil, String, …) survive unchanged.
|
|
475
|
+
# Complement of an `IntegerRange[a, b]` within `current_type`. Splits the range
|
|
476
|
+
# complement into the two open halves `int<min, a-1>` and `int<b+1, max>` (skipping a
|
|
477
|
+
# half when its bound is infinity), then intersects each half with the integer-domain
|
|
478
|
+
# parts of `current_type`. Non-integer parts of a Union receiver (nil, String, …)
|
|
479
|
+
# survive unchanged.
|
|
546
480
|
def complement_integer_range(current_type, range)
|
|
547
481
|
halves = integer_range_complement_halves(range)
|
|
548
482
|
parts = current_type.is_a?(Type::Union) ? current_type.members : [current_type]
|
|
@@ -564,11 +498,10 @@ module Rigor
|
|
|
564
498
|
Type::Combinator.union(*survivors)
|
|
565
499
|
end
|
|
566
500
|
|
|
567
|
-
# Returns the two open halves of an IntegerRange's
|
|
568
|
-
#
|
|
569
|
-
#
|
|
570
|
-
#
|
|
571
|
-
# an empty array — the complement is empty.
|
|
501
|
+
# Returns the two open halves of an IntegerRange's complement: the left half `int<-∞,
|
|
502
|
+
# a-1>` (when `a` is finite) and the right half `int<b+1, ∞>` (when `b` is finite).
|
|
503
|
+
# Universal ranges (both bounds infinite) yield an empty array — the complement is
|
|
504
|
+
# empty.
|
|
572
505
|
def integer_range_complement_halves(range)
|
|
573
506
|
halves = []
|
|
574
507
|
left_max = range.min
|
|
@@ -592,12 +525,10 @@ module Rigor
|
|
|
592
525
|
end
|
|
593
526
|
end
|
|
594
527
|
|
|
595
|
-
# Intersect an integer-domain part with a complement
|
|
596
|
-
#
|
|
597
|
-
#
|
|
598
|
-
#
|
|
599
|
-
# meet is the constant when the half covers it,
|
|
600
|
-
# otherwise nil.
|
|
528
|
+
# Intersect an integer-domain part with a complement half-range. For a
|
|
529
|
+
# Nominal[Integer] receiver the meet is the half itself; for an existing IntegerRange
|
|
530
|
+
# the meet narrows both bounds; for a Constant[Integer] the meet is the constant when
|
|
531
|
+
# the half covers it, otherwise nil.
|
|
601
532
|
def intersect_integer_part(part, half)
|
|
602
533
|
case part
|
|
603
534
|
when Type::Nominal
|
|
@@ -638,11 +569,9 @@ module Rigor
|
|
|
638
569
|
low > high
|
|
639
570
|
end
|
|
640
571
|
|
|
641
|
-
# De Morgan: `D \ (M1 ∩ M2 ∩ …) = (D \ M1) ∪ (D \ M2) ∪
|
|
642
|
-
#
|
|
643
|
-
#
|
|
644
|
-
# Members the algebra cannot complement contribute
|
|
645
|
-
# `current_type` itself, so the union widens to
|
|
572
|
+
# De Morgan: `D \ (M1 ∩ M2 ∩ …) = (D \ M1) ∪ (D \ M2) ∪ …`. Each member's complement is
|
|
573
|
+
# computed independently within `current_type` and the results are unioned. Members the
|
|
574
|
+
# algebra cannot complement contribute `current_type` itself, so the union widens to
|
|
646
575
|
# `current_type` overall — sound but imprecise.
|
|
647
576
|
def complement_intersection(current_type, intersection)
|
|
648
577
|
per_member = intersection.members.map do |member|
|
|
@@ -651,29 +580,22 @@ module Rigor
|
|
|
651
580
|
Type::Combinator.union(*per_member)
|
|
652
581
|
end
|
|
653
582
|
|
|
654
|
-
# v0.0.7 — complement of a `Refined[base, predicate]`
|
|
655
|
-
#
|
|
656
|
-
#
|
|
657
|
-
# the
|
|
658
|
-
#
|
|
659
|
-
# exactly the negated subset); parts that overlap the
|
|
660
|
-
# base contribute `Difference[part, refined]` so
|
|
661
|
-
# downstream narrowing knows the refinement subset is
|
|
662
|
-
# excluded.
|
|
583
|
+
# v0.0.7 — complement of a `Refined[base, predicate]` refinement within `current_type`.
|
|
584
|
+
# Walks the current type's union members; parts that are disjoint from the refinement's
|
|
585
|
+
# base survive unchanged; parts equal to the refinement itself drop entirely (they were
|
|
586
|
+
# exactly the negated subset); parts that overlap the base contribute `Difference[part,
|
|
587
|
+
# refined]` so downstream narrowing knows the refinement subset is excluded.
|
|
663
588
|
#
|
|
664
|
-
# v0.0.9 — when the predicate has a registered
|
|
665
|
-
#
|
|
666
|
-
#
|
|
667
|
-
#
|
|
668
|
-
#
|
|
669
|
-
# `~T` symmetry the spec promises: `~lowercase-string`
|
|
670
|
-
# narrows `String` to `non-lowercase-string` rather than
|
|
589
|
+
# v0.0.9 — when the predicate has a registered complement (see
|
|
590
|
+
# {Type::Refined::COMPLEMENT_PAIRS}) and the part is exactly the refinement's base, the
|
|
591
|
+
# narrowing returns `Refined[base, complement_predicate]` instead of
|
|
592
|
+
# `Difference[base, refined]`. This is the `~T` symmetry the spec promises:
|
|
593
|
+
# `~lowercase-string` narrows `String` to `non-lowercase-string` rather than
|
|
671
594
|
# `Difference[String, lowercase-string]`.
|
|
672
595
|
#
|
|
673
|
-
# Predicates without a registered complement still fall
|
|
674
|
-
#
|
|
675
|
-
#
|
|
676
|
-
# untouched call sites.
|
|
596
|
+
# Predicates without a registered complement still fall back to the imprecise but sound
|
|
597
|
+
# `Difference[part, refined]` carrier so behaviour is unchanged for untouched call
|
|
598
|
+
# sites.
|
|
677
599
|
def complement_refined(current_type, refined)
|
|
678
600
|
complement = registered_complement_for(refined)
|
|
679
601
|
parts = current_type.is_a?(Type::Union) ? current_type.members : [current_type]
|
|
@@ -706,10 +628,9 @@ module Rigor
|
|
|
706
628
|
%w[NilClass FalseClass].include?(nominal.class_name)
|
|
707
629
|
end
|
|
708
630
|
|
|
709
|
-
# Carriers that the {.narrow_falsey} fast path does not handle
|
|
710
|
-
#
|
|
711
|
-
#
|
|
712
|
-
# else (Top, Dynamic, Bot, and any future carrier) stays
|
|
631
|
+
# Carriers that the {.narrow_falsey} fast path does not handle by structural
|
|
632
|
+
# inspection. Singleton/Tuple/HashShape inhabit truthy values, so their falsey fragment
|
|
633
|
+
# is empty; everything else (Top, Dynamic, Bot, and any future carrier) stays
|
|
713
634
|
# conservative and is returned unchanged.
|
|
714
635
|
def narrow_falsey_other(type)
|
|
715
636
|
case type
|
|
@@ -718,12 +639,10 @@ module Rigor
|
|
|
718
639
|
end
|
|
719
640
|
end
|
|
720
641
|
|
|
721
|
-
# Carriers that the {.narrow_nil} fast path does not handle by
|
|
722
|
-
#
|
|
723
|
-
#
|
|
724
|
-
#
|
|
725
|
-
# HashShape, and any future carrier whose inhabitants exclude
|
|
726
|
-
# nil) collapse to `Bot`.
|
|
642
|
+
# Carriers that the {.narrow_nil} fast path does not handle by structural inspection.
|
|
643
|
+
# Top/Dynamic narrow to `Constant[nil]` so dispatch resolves through `NilClass`; Bot is
|
|
644
|
+
# its own nil fragment; the remaining carriers (Singleton, Tuple, HashShape, and any
|
|
645
|
+
# future carrier whose inhabitants exclude nil) collapse to `Bot`.
|
|
727
646
|
def narrow_nil_other(type)
|
|
728
647
|
case type
|
|
729
648
|
when Type::Dynamic, Type::Top then Type::Combinator.constant_of(nil)
|
|
@@ -811,13 +730,11 @@ module Rigor
|
|
|
811
730
|
analyse(node.body, scope)
|
|
812
731
|
end
|
|
813
732
|
|
|
814
|
-
# The truthiness of a `StatementsNode` is determined by its
|
|
815
|
-
#
|
|
816
|
-
#
|
|
817
|
-
#
|
|
818
|
-
#
|
|
819
|
-
# has already produced `post_pred` for the call site, and
|
|
820
|
-
# narrowing is layered on that scope).
|
|
733
|
+
# The truthiness of a `StatementsNode` is determined by its last statement
|
|
734
|
+
# (intermediate statements run for effect and then the predicate's value is the
|
|
735
|
+
# tail's). Earlier statements MAY have scope effects, but Slice 6 phase 1 does NOT
|
|
736
|
+
# thread those through the analyser (the StatementEvaluator has already produced
|
|
737
|
+
# `post_pred` for the call site, and narrowing is layered on that scope).
|
|
821
738
|
def analyse_statements(node, scope)
|
|
822
739
|
return nil if node.body.empty?
|
|
823
740
|
|
|
@@ -834,19 +751,15 @@ module Rigor
|
|
|
834
751
|
]
|
|
835
752
|
end
|
|
836
753
|
|
|
837
|
-
# Assignment-in-condition: `if name = expr` and the more
|
|
838
|
-
#
|
|
839
|
-
#
|
|
840
|
-
#
|
|
841
|
-
# the
|
|
842
|
-
#
|
|
843
|
-
#
|
|
844
|
-
#
|
|
845
|
-
#
|
|
846
|
-
# because the only meaningful difference between
|
|
847
|
-
# "predicate is `var`" and "predicate is `var = expr`" is
|
|
848
|
-
# which scope holds the just-bound value; the narrowing
|
|
849
|
-
# contract on the surrounding `if` is the same.
|
|
754
|
+
# Assignment-in-condition: `if name = expr` and the more frequent `if cond && (name =
|
|
755
|
+
# expr)` / `if cond && name = expr` Redmine-style guard. By the time narrowing runs,
|
|
756
|
+
# `StatementEvaluator#eval_local_write` has already bound the assigned local in `scope`
|
|
757
|
+
# to the rvalue type. The write's own truthiness IS the assigned value's truthiness, so
|
|
758
|
+
# the truthy edge narrows the local by `narrow_truthy(current)` and the falsey edge by
|
|
759
|
+
# `narrow_falsey(current)`. Mirrors `analyse_local_read` because the only meaningful
|
|
760
|
+
# difference between "predicate is `var`" and "predicate is `var = expr`" is which scope
|
|
761
|
+
# holds the just-bound value; the narrowing contract on the surrounding `if` is the
|
|
762
|
+
# same.
|
|
850
763
|
def analyse_local_write(node, scope)
|
|
851
764
|
current = scope.local(node.name)
|
|
852
765
|
return nil if current.nil?
|
|
@@ -857,16 +770,13 @@ module Rigor
|
|
|
857
770
|
]
|
|
858
771
|
end
|
|
859
772
|
|
|
860
|
-
# Truthy guard on an instance variable. Covers both the bare
|
|
861
|
-
#
|
|
862
|
-
# the
|
|
863
|
-
#
|
|
864
|
-
#
|
|
865
|
-
#
|
|
866
|
-
#
|
|
867
|
-
# narrowing is scoped to the guarded branch only, so it is
|
|
868
|
-
# purely additive — it never widens or re-narrows the ivar
|
|
869
|
-
# binding seen elsewhere.
|
|
773
|
+
# Truthy guard on an instance variable. Covers both the bare read — `if @ivar`, the
|
|
774
|
+
# left arm of `@ivar && @ivar.foo`, the receiver of `unless @ivar.nil?` once `!`
|
|
775
|
+
# reverses it — and the assignment-in-condition write `if @ivar = expr`. Mirrors
|
|
776
|
+
# `analyse_local_read` / `analyse_local_write`: the truthy edge narrows the ivar by
|
|
777
|
+
# `narrow_truthy` (dropping `nil` / `false`), the falsey edge by `narrow_falsey`. The
|
|
778
|
+
# narrowing is scoped to the guarded branch only, so it is purely additive — it never
|
|
779
|
+
# widens or re-narrows the ivar binding seen elsewhere.
|
|
870
780
|
def analyse_ivar(node, scope)
|
|
871
781
|
current = scope.ivar(node.name)
|
|
872
782
|
return nil if current.nil?
|
|
@@ -897,24 +807,18 @@ module Rigor
|
|
|
897
807
|
]
|
|
898
808
|
end
|
|
899
809
|
|
|
900
|
-
# `if /(?<x>...)/ =~ str` — Prism wraps the `=~` call in a
|
|
901
|
-
#
|
|
902
|
-
#
|
|
903
|
-
#
|
|
904
|
-
#
|
|
905
|
-
#
|
|
906
|
-
# `nil`. Subtract the dead half on each edge so callers
|
|
907
|
-
# like `year.upcase` inside the truthy branch no longer
|
|
908
|
-
# fire `possible-nil-receiver`.
|
|
810
|
+
# `if /(?<x>...)/ =~ str` — Prism wraps the `=~` call in a `MatchWriteNode` listing the
|
|
811
|
+
# named-capture targets. The parent `eval_match_write` has already bound each target to
|
|
812
|
+
# `String | nil`; in the truthy branch (the regex matched) every named capture is
|
|
813
|
+
# guaranteed `String`, and in the falsey branch (no match) every capture is `nil`.
|
|
814
|
+
# Subtract the dead half on each edge so callers like `year.upcase` inside the truthy
|
|
815
|
+
# branch no longer fire `possible-nil-receiver`.
|
|
909
816
|
#
|
|
910
|
-
# v0.1.1 Track 1 slice 1 — when the regex source is a
|
|
911
|
-
#
|
|
912
|
-
#
|
|
913
|
-
#
|
|
914
|
-
#
|
|
915
|
-
# imported refinement (e.g. `decimal-int-string` for
|
|
916
|
-
# `\d+`). Bodies outside the table fall back to the
|
|
917
|
-
# v0.1.0 baseline (plain `String`).
|
|
817
|
+
# v0.1.1 Track 1 slice 1 — when the regex source is a statically known
|
|
818
|
+
# `RegularExpressionNode` and a named capture's body matches one of the curated shapes
|
|
819
|
+
# in {Rigor::Builtins::RegexRefinement::RULES}, the truthy branch narrows further than
|
|
820
|
+
# `String` to the matching imported refinement (e.g. `decimal-int-string` for `\d+`).
|
|
821
|
+
# Bodies outside the table fall back to the v0.1.0 baseline (plain `String`).
|
|
918
822
|
def analyse_match_write(node, scope)
|
|
919
823
|
string_t = Type::Combinator.nominal_of("String")
|
|
920
824
|
nil_t = Type::Combinator.constant_of(nil)
|
|
@@ -930,12 +834,10 @@ module Rigor
|
|
|
930
834
|
[truthy, falsey]
|
|
931
835
|
end
|
|
932
836
|
|
|
933
|
-
# Extracts `{ capture_name => Refinement }` for every named
|
|
934
|
-
#
|
|
935
|
-
#
|
|
936
|
-
#
|
|
937
|
-
# else outside the curated forms drop out and the caller
|
|
938
|
-
# falls back to plain `String`.
|
|
837
|
+
# Extracts `{ capture_name => Refinement }` for every named capture group in the
|
|
838
|
+
# `MatchWriteNode`'s wrapped `=~` call whose body the recogniser table accepts. Bodies
|
|
839
|
+
# that contain nested groups, anchors, alternation, or anything else outside the curated
|
|
840
|
+
# forms drop out and the caller falls back to plain `String`.
|
|
939
841
|
NAMED_CAPTURE_BODY_RE = /\(\?<([A-Za-z_][A-Za-z0-9_]*)>([^()|]*)\)/
|
|
940
842
|
private_constant :NAMED_CAPTURE_BODY_RE
|
|
941
843
|
|
|
@@ -954,13 +856,12 @@ module Rigor
|
|
|
954
856
|
# Recognised CallNode predicates:
|
|
955
857
|
# - `recv.nil?` (Slice 6 phase 1, no args, no block)
|
|
956
858
|
# - unary `!recv` (`name == :!`, no args, no block)
|
|
957
|
-
# - `recv.is_a?(C)` / `recv.kind_of?(C)` / `recv.instance_of?(C)`
|
|
958
|
-
#
|
|
959
|
-
#
|
|
960
|
-
#
|
|
961
|
-
#
|
|
962
|
-
#
|
|
963
|
-
# through to the no-narrowing fallback.
|
|
859
|
+
# - `recv.is_a?(C)` / `recv.kind_of?(C)` / `recv.instance_of?(C)` with a single
|
|
860
|
+
# static-constant argument and no block (Slice 6 phase 2 sub-phase 1).
|
|
861
|
+
# - `local == literal` / `literal == local` and the `!=` mirror for trusted static
|
|
862
|
+
# literals (Slice 6 phase 2 sub-phase 2).
|
|
863
|
+
# Anything else returns nil so the surrounding analyser falls through to the
|
|
864
|
+
# no-narrowing fallback.
|
|
964
865
|
def analyse_call(node, scope)
|
|
965
866
|
return nil if node.block
|
|
966
867
|
|
|
@@ -972,42 +873,41 @@ module Rigor
|
|
|
972
873
|
string_predicate_result = analyse_string_predicate(node, scope)
|
|
973
874
|
return apply_safe_nav_non_nil(node, scope, string_predicate_result) if string_predicate_result
|
|
974
875
|
|
|
975
|
-
# A
|
|
976
|
-
#
|
|
977
|
-
#
|
|
978
|
-
#
|
|
979
|
-
#
|
|
980
|
-
#
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
876
|
+
# A predicate whose RBS says it always returns `false` for one union arm cannot have
|
|
877
|
+
# been called on that arm when it answered truthily. `NilClass#present?: () -> false`
|
|
878
|
+
# (ActiveSupport) is the motivating case: `if login.present?` must narrow `String | nil`
|
|
879
|
+
# to `String`, and nothing else in the catalogue does it — `present?` is a gem method the
|
|
880
|
+
# engine must not hardcode, and `rigor:v1:predicate-if-true` facts never reach a union
|
|
881
|
+
# receiver.
|
|
882
|
+
polarity_result = analyse_union_predicate_polarity(node, scope)
|
|
883
|
+
return polarity_result if polarity_result
|
|
884
|
+
|
|
885
|
+
# A safe-navigation call (`v&.foo`) whose result is truthy proves the receiver was
|
|
886
|
+
# non-nil — `&.` returns `nil` when the receiver is nil, so a truthy outcome can
|
|
887
|
+
# only come from a non-nil receiver. Narrow the receiver on the truthy edge even
|
|
888
|
+
# when the call itself carries no other flow fact, so `v&.start_with?('[') &&
|
|
889
|
+
# v.end_with?(']')` sees `v` non-nil in the `&&` right operand. The falsey edge is
|
|
890
|
+
# the conservative no-op (falsey could mean nil receiver OR a falsey method result).
|
|
984
891
|
safe_nav_result = analyse_safe_nav_receiver(node, scope)
|
|
985
892
|
return safe_nav_result if safe_nav_result
|
|
986
893
|
end
|
|
987
894
|
|
|
988
|
-
# Slice 7 phase 15 — RBS::Extended predicate
|
|
989
|
-
#
|
|
990
|
-
#
|
|
991
|
-
#
|
|
992
|
-
#
|
|
993
|
-
# 1 slice 3 — implicit-self calls (`recv == nil`,
|
|
994
|
-
# e.g. `admin?` inside an instance method body) flow
|
|
995
|
-
# through here too so `self`-targeted facts can edit
|
|
895
|
+
# Slice 7 phase 15 — RBS::Extended predicate effects. When the method's RBS signature
|
|
896
|
+
# carries `rigor:v1:predicate-if-true` / `predicate-if-false` annotations, apply them
|
|
897
|
+
# to narrow the corresponding local-variable arguments on each edge. v0.1.1 Track 1
|
|
898
|
+
# slice 3 — implicit-self calls (`recv == nil`, e.g. `admin?` inside an instance
|
|
899
|
+
# method body) flow through here too so `self`-targeted facts can edit
|
|
996
900
|
# `scope.self_type`.
|
|
997
901
|
analyse_rbs_extended_contribution(node, scope)
|
|
998
902
|
end
|
|
999
903
|
|
|
1000
|
-
# v0.1.1 Track 1 slice 4 — `String#start_with?` /
|
|
1001
|
-
#
|
|
1002
|
-
#
|
|
1003
|
-
#
|
|
1004
|
-
#
|
|
1005
|
-
#
|
|
1006
|
-
#
|
|
1007
|
-
# plugin's `prepare(services)` hook in v0.1.x).
|
|
1008
|
-
# Truthy edge: positive polarity. Falsey edge: negative
|
|
1009
|
-
# polarity. Mirrors the equality-predicate fact pattern
|
|
1010
|
-
# already used by `analyse_equality_predicate`.
|
|
904
|
+
# v0.1.1 Track 1 slice 4 — `String#start_with?` / `#end_with?` / `#include?` against a
|
|
905
|
+
# `Constant<String>` needle attaches a relational `FactStore::Fact` to the local on
|
|
906
|
+
# each edge. The receiver's type does NOT change — Rigor has no "starts-with-X" carrier
|
|
907
|
+
# today — so the fact carries the predicate semantics for any downstream consumer that
|
|
908
|
+
# wants to read it (e.g. a plugin's `prepare(services)` hook in v0.1.x). Truthy edge:
|
|
909
|
+
# positive polarity. Falsey edge: negative polarity. Mirrors the equality-predicate fact
|
|
910
|
+
# pattern already used by `analyse_equality_predicate`.
|
|
1011
911
|
STRING_PREDICATE_NAMES = %i[start_with? end_with? include?].freeze
|
|
1012
912
|
private_constant :STRING_PREDICATE_NAMES
|
|
1013
913
|
|
|
@@ -1080,21 +980,17 @@ module Rigor
|
|
|
1080
980
|
end
|
|
1081
981
|
end
|
|
1082
982
|
|
|
1083
|
-
# T3 (template-corpora survey) — `recv.respond_to?(sym)` truthy
|
|
1084
|
-
#
|
|
1085
|
-
#
|
|
1086
|
-
#
|
|
1087
|
-
# the
|
|
1088
|
-
# `:inspect`, `:nil?`, …) — `nil` DOES respond to those, so the
|
|
1089
|
-
# truthy edge admits a nil receiver and we narrow nothing.
|
|
983
|
+
# T3 (template-corpora survey) — `recv.respond_to?(sym)` truthy edge narrows `recv`
|
|
984
|
+
# non-nil. `nil.respond_to?(m)` is `false` for every method `m` that `NilClass` does not
|
|
985
|
+
# define, so a truthy `respond_to?` proves the receiver was not `nil` UNLESS the queried
|
|
986
|
+
# symbol is one of `NilClass`'s own methods (`:to_s`, `:inspect`, `:nil?`, …) — `nil`
|
|
987
|
+
# DOES respond to those, so the truthy edge admits a nil receiver and we narrow nothing.
|
|
1090
988
|
#
|
|
1091
|
-
# Conservative floor: narrow only on a literal `Symbol`/`String`
|
|
1092
|
-
#
|
|
1093
|
-
#
|
|
1094
|
-
#
|
|
1095
|
-
#
|
|
1096
|
-
# Narrowing-only: it removes the `nil` constituent and never
|
|
1097
|
-
# promotes a non-nil type.
|
|
989
|
+
# Conservative floor: narrow only on a literal `Symbol`/`String` argument resolved
|
|
990
|
+
# against the RBS environment; a non-literal symbol, a missing argument, or a symbol
|
|
991
|
+
# that IS in `NilClass`'s method set declines. The falsey edge is always the no-op
|
|
992
|
+
# ("does not respond" proves little about the receiver's type). Narrowing-only: it
|
|
993
|
+
# removes the `nil` constituent and never promotes a non-nil type.
|
|
1098
994
|
def analyse_respond_to_predicate(node, scope)
|
|
1099
995
|
return nil if node.block
|
|
1100
996
|
return nil if node.arguments.nil? || node.arguments.arguments.size != 1
|
|
@@ -1115,12 +1011,11 @@ module Rigor
|
|
|
1115
1011
|
[scope.public_send(writer, node.receiver.name, non_nil), scope]
|
|
1116
1012
|
end
|
|
1117
1013
|
|
|
1118
|
-
# True when `nil` responds to `sym` — i.e. `NilClass` (own,
|
|
1119
|
-
#
|
|
1120
|
-
#
|
|
1121
|
-
#
|
|
1122
|
-
#
|
|
1123
|
-
# non-nil narrowing.
|
|
1014
|
+
# True when `nil` responds to `sym` — i.e. `NilClass` (own, inherited
|
|
1015
|
+
# Kernel/BasicObject) defines an instance method named `sym`. Resolved against the RBS
|
|
1016
|
+
# environment; when the lookup is unavailable the answer is conservatively `true`
|
|
1017
|
+
# (decline to narrow) so an unknown environment never manufactures a false non-nil
|
|
1018
|
+
# narrowing.
|
|
1124
1019
|
def nilclass_method?(sym, scope)
|
|
1125
1020
|
name = sym.respond_to?(:to_sym) ? sym.to_sym : sym
|
|
1126
1021
|
return true unless name.is_a?(Symbol)
|
|
@@ -1132,23 +1027,20 @@ module Rigor
|
|
|
1132
1027
|
true
|
|
1133
1028
|
end
|
|
1134
1029
|
|
|
1135
|
-
# ADR-47 §4-4 (Elixir `tuple_size`/non-empty analogue) — a bare
|
|
1136
|
-
# `arr.
|
|
1137
|
-
#
|
|
1138
|
-
# edge that implies "at least one element":
|
|
1030
|
+
# ADR-47 §4-4 (Elixir `tuple_size`/non-empty analogue) — a bare `arr.empty?` /
|
|
1031
|
+
# `arr.any?` / `arr.none?` (no block, no args) narrows an Array-typed receiver to
|
|
1032
|
+
# `non-empty-array[T]` on the edge that implies "at least one element":
|
|
1139
1033
|
#
|
|
1140
1034
|
# - `empty?` → false edge (the array is NOT empty)
|
|
1141
1035
|
# - `any?` → true edge (a truthy element exists ⇒ non-empty)
|
|
1142
1036
|
# - `none?` → false edge (a truthy element exists ⇒ non-empty)
|
|
1143
1037
|
#
|
|
1144
|
-
# The opposite edge is the conservative no-op (`any?`/`none?`
|
|
1145
|
-
#
|
|
1146
|
-
#
|
|
1147
|
-
#
|
|
1148
|
-
#
|
|
1149
|
-
#
|
|
1150
|
-
# (`String#empty?`, `Range#any?`, …) bails so the existing string
|
|
1151
|
-
# / predicate paths still run.
|
|
1038
|
+
# The opposite edge is the conservative no-op (`any?`/`none?` falseness does not imply
|
|
1039
|
+
# emptiness — the array may hold only falsey elements; `empty?` truth could narrow to an
|
|
1040
|
+
# empty array but that carrier move is deferred). Only `Nominal[Array, [T]]` receivers
|
|
1041
|
+
# narrow — `Tuple` is already known-length, `Dynamic` is left alone (gradual
|
|
1042
|
+
# guarantee), and a non-Array receiver (`String#empty?`, `Range#any?`, …) bails so the
|
|
1043
|
+
# existing string / predicate paths still run.
|
|
1152
1044
|
def analyse_array_emptiness_predicate(node, scope, name)
|
|
1153
1045
|
return nil if node.block
|
|
1154
1046
|
return nil unless node.arguments.nil? || node.arguments.arguments.empty?
|
|
@@ -1174,9 +1066,9 @@ module Rigor
|
|
|
1174
1066
|
end
|
|
1175
1067
|
end
|
|
1176
1068
|
|
|
1177
|
-
# Refines `Array[T]` (and every Array member of a Union) to
|
|
1178
|
-
#
|
|
1179
|
-
#
|
|
1069
|
+
# Refines `Array[T]` (and every Array member of a Union) to `non-empty-array[T]`;
|
|
1070
|
+
# returns the input unchanged when nothing applies, so the caller can detect "no
|
|
1071
|
+
# narrowing".
|
|
1180
1072
|
def narrow_to_non_empty_array(type)
|
|
1181
1073
|
case type
|
|
1182
1074
|
when Type::Nominal
|
|
@@ -1190,18 +1082,16 @@ module Rigor
|
|
|
1190
1082
|
end
|
|
1191
1083
|
end
|
|
1192
1084
|
|
|
1193
|
-
# ADR-47 §4-3 (Elixir `is_map_key/2` analogue) — `h.key?(:foo)`
|
|
1194
|
-
#
|
|
1195
|
-
#
|
|
1196
|
-
#
|
|
1197
|
-
#
|
|
1198
|
-
#
|
|
1199
|
-
#
|
|
1200
|
-
#
|
|
1201
|
-
#
|
|
1202
|
-
#
|
|
1203
|
-
# receivers narrow; everything else (Dynamic, `Nominal[Hash]`,
|
|
1204
|
-
# method-chain receivers, dynamic keys) bails to no narrowing.
|
|
1085
|
+
# ADR-47 §4-3 (Elixir `is_map_key/2` analogue) — `h.key?(:foo)` narrows the receiver on
|
|
1086
|
+
# the truthy edge: a `HashShape` whose `:foo` is an OPTIONAL key has it promoted to
|
|
1087
|
+
# REQUIRED, so a subsequent `h[:foo]` reads the declared value type instead of `value |
|
|
1088
|
+
# nil` (the optionality nil is gone). Sound because key presence removes only the
|
|
1089
|
+
# optionality-injected nil, never the value's own intrinsic nil (`h = {foo: nil}` keeps
|
|
1090
|
+
# `h[:foo]` nil-typed). The falsey edge is left unchanged — "key absent" is the
|
|
1091
|
+
# conservative no-op. Only literal `Symbol`/`String` arguments and
|
|
1092
|
+
# `LocalVariableReadNode`/`InstanceVariableReadNode` receivers narrow; everything else
|
|
1093
|
+
# (Dynamic, `Nominal[Hash]`, method-chain receivers, dynamic keys) bails to no
|
|
1094
|
+
# narrowing.
|
|
1205
1095
|
def analyse_key_presence_predicate(node, scope)
|
|
1206
1096
|
return nil if node.arguments.nil?
|
|
1207
1097
|
return nil unless node.arguments.arguments.size == 1
|
|
@@ -1222,10 +1112,9 @@ module Rigor
|
|
|
1222
1112
|
return nil if current.nil?
|
|
1223
1113
|
|
|
1224
1114
|
truthy = narrow_hash_key_present(current, key)
|
|
1225
|
-
# §4-3 false edge — `h.key?(:foo)` being false proves `:foo` is
|
|
1226
|
-
#
|
|
1227
|
-
#
|
|
1228
|
-
# edge dead and an unknown key is already nil, both no-ops.
|
|
1115
|
+
# §4-3 false edge — `h.key?(:foo)` being false proves `:foo` is absent, so on that
|
|
1116
|
+
# edge `h[:foo]` reads `nil`. Remove the (optional) key from the shape; a required
|
|
1117
|
+
# key makes the false edge dead and an unknown key is already nil, both no-ops.
|
|
1229
1118
|
falsey = narrow_hash_key_absent(current, key)
|
|
1230
1119
|
return nil if truthy.equal?(current) && falsey.equal?(current) # predicate is opaque
|
|
1231
1120
|
|
|
@@ -1241,9 +1130,9 @@ module Rigor
|
|
|
1241
1130
|
end
|
|
1242
1131
|
end
|
|
1243
1132
|
|
|
1244
|
-
# Promotes `key` from optional to required across a HashShape (or
|
|
1245
|
-
#
|
|
1246
|
-
#
|
|
1133
|
+
# Promotes `key` from optional to required across a HashShape (or every HashShape
|
|
1134
|
+
# member of a Union). Returns the input unchanged when nothing applies, so the caller
|
|
1135
|
+
# can detect "no narrowing".
|
|
1247
1136
|
def narrow_hash_key_present(type, key)
|
|
1248
1137
|
case type
|
|
1249
1138
|
when Type::HashShape
|
|
@@ -1268,12 +1157,11 @@ module Rigor
|
|
|
1268
1157
|
)
|
|
1269
1158
|
end
|
|
1270
1159
|
|
|
1271
|
-
# §4-3 false edge — drops `key` from a HashShape (or every HashShape
|
|
1272
|
-
#
|
|
1273
|
-
#
|
|
1274
|
-
#
|
|
1275
|
-
#
|
|
1276
|
-
# shape opaque) and a key absent from `pairs` already reads `nil`.
|
|
1160
|
+
# §4-3 false edge — drops `key` from a HashShape (or every HashShape member of a Union)
|
|
1161
|
+
# so the proven-absent key reads `nil`. Returns the input unchanged when nothing
|
|
1162
|
+
# applies (caller detects "no narrowing"). Only an *optional* present key is removed: a
|
|
1163
|
+
# required key makes `key?` always true (the false edge is dead, leave the shape opaque)
|
|
1164
|
+
# and a key absent from `pairs` already reads `nil`.
|
|
1277
1165
|
def narrow_hash_key_absent(type, key)
|
|
1278
1166
|
case type
|
|
1279
1167
|
when Type::HashShape
|
|
@@ -1298,27 +1186,21 @@ module Rigor
|
|
|
1298
1186
|
)
|
|
1299
1187
|
end
|
|
1300
1188
|
|
|
1301
|
-
# Survey item (b): `/regex/ =~ str` and `str =~ /regex/`
|
|
1302
|
-
#
|
|
1189
|
+
# Survey item (b): `/regex/ =~ str` and `str =~ /regex/` bind the regex match-data
|
|
1190
|
+
# globals on each edge.
|
|
1303
1191
|
#
|
|
1304
|
-
# - Truthy edge (`=~` returned an Integer position — the
|
|
1305
|
-
#
|
|
1306
|
-
#
|
|
1307
|
-
#
|
|
1308
|
-
#
|
|
1309
|
-
#
|
|
1310
|
-
# `
|
|
1311
|
-
#
|
|
1312
|
-
#
|
|
1313
|
-
# (required captures) and lets `unless /(\d+)/ =~ s;
|
|
1314
|
-
# raise; end; $1.to_i` resolve cleanly.
|
|
1315
|
-
# - Falsey edge (`=~` returned nil — no match): `$~` and
|
|
1316
|
-
# every numbered / back-reference global bound to
|
|
1317
|
-
# `Constant<nil>`.
|
|
1192
|
+
# - Truthy edge (`=~` returned an Integer position — the match succeeded): `$~` to
|
|
1193
|
+
# `Nominal[MatchData]`; `$&` and `$1..$N` (where N is the number of capture groups in
|
|
1194
|
+
# the regex source) to `Nominal[String]`. This is the same optimistic-narrowing shape
|
|
1195
|
+
# the existing `analyse_match_write` uses for named captures inside `if /(?<x>...)/
|
|
1196
|
+
# =~ str` — optional groups in the regex source (`(\d+)?`) would bind `$N` to `nil`
|
|
1197
|
+
# at runtime, but the floor here matches the common idiom (required captures) and
|
|
1198
|
+
# lets `unless /(\d+)/ =~ s; raise; end; $1.to_i` resolve cleanly.
|
|
1199
|
+
# - Falsey edge (`=~` returned nil — no match): `$~` and every numbered / back-reference
|
|
1200
|
+
# global bound to `Constant<nil>`.
|
|
1318
1201
|
#
|
|
1319
|
-
# Returns nil (no narrowing) when the receiver / argument
|
|
1320
|
-
#
|
|
1321
|
-
# we can count.
|
|
1202
|
+
# Returns nil (no narrowing) when the receiver / argument pair does not include a
|
|
1203
|
+
# `RegularExpressionNode` literal we can count.
|
|
1322
1204
|
def analyse_regex_match_predicate(node, scope)
|
|
1323
1205
|
return nil if node.arguments.nil?
|
|
1324
1206
|
return nil unless node.arguments.arguments.size == 1
|
|
@@ -1343,14 +1225,12 @@ module Rigor
|
|
|
1343
1225
|
REGEX_MATCH_GLOBALS = %i[$~ $& $` $' $+].freeze
|
|
1344
1226
|
private_constant :REGEX_MATCH_GLOBALS
|
|
1345
1227
|
|
|
1346
|
-
# `unconditional` is the Set of 1-based numbered-capture
|
|
1347
|
-
#
|
|
1348
|
-
#
|
|
1349
|
-
#
|
|
1350
|
-
#
|
|
1351
|
-
#
|
|
1352
|
-
# truthy match leaves an optional group nil at runtime), so
|
|
1353
|
-
# we do not narrow it on the truthy edge.
|
|
1228
|
+
# `unconditional` is the Set of 1-based numbered-capture indices whose group is
|
|
1229
|
+
# guaranteed to participate in any successful match (no optional quantifier on the
|
|
1230
|
+
# group or an ancestor, no alternation in the pattern). Those `$N` are bound to
|
|
1231
|
+
# `String`; every other numbered group present in the pattern stays `String | nil` on
|
|
1232
|
+
# both edges (a truthy match leaves an optional group nil at runtime), so we do not
|
|
1233
|
+
# narrow it on the truthy edge.
|
|
1354
1234
|
def regex_match_predicate_scopes(scope, unconditional)
|
|
1355
1235
|
string_t = Type::Combinator.nominal_of("String")
|
|
1356
1236
|
match_data_t = Type::Combinator.nominal_of("MatchData")
|
|
@@ -1374,31 +1254,24 @@ module Rigor
|
|
|
1374
1254
|
[truthy, falsey]
|
|
1375
1255
|
end
|
|
1376
1256
|
|
|
1377
|
-
# Returns the Set of 1-based numbered-capture indices that
|
|
1378
|
-
#
|
|
1379
|
-
#
|
|
1380
|
-
#
|
|
1381
|
-
#
|
|
1382
|
-
#
|
|
1383
|
-
#
|
|
1384
|
-
#
|
|
1385
|
-
#
|
|
1386
|
-
#
|
|
1387
|
-
# `(?=…)`/`(?!…)`, and lookbehind `(?<=…)`/`(?<!…)` do not
|
|
1388
|
-
# capture; named groups `(?<name>…)` do. Conservatism is
|
|
1389
|
-
# one-directional — when in doubt a group is treated as
|
|
1390
|
-
# conditional (dropped from the Set), never the reverse.
|
|
1257
|
+
# Returns the Set of 1-based numbered-capture indices that are UNCONDITIONAL in
|
|
1258
|
+
# `source`: present on every successful match because no optional quantifier (`?`,
|
|
1259
|
+
# `*`, `{0,…}`) applies to the group or any ancestor group, and the pattern contains no
|
|
1260
|
+
# alternation (`|`). Optional and alternation-reachable groups are excluded — at
|
|
1261
|
+
# runtime `$N` is `nil` for them even when the overall match succeeds, so narrowing
|
|
1262
|
+
# them to non-nil `String` would be unsound. The walker is intentionally light (char
|
|
1263
|
+
# scan, not a regex-AST parse): backslash escapes are skipped; `(?:…)`, lookahead
|
|
1264
|
+
# `(?=…)`/`(?!…)`, and lookbehind `(?<=…)`/`(?<!…)` do not capture; named groups
|
|
1265
|
+
# `(?<name>…)` do. Conservatism is one-directional — when in doubt a group is treated
|
|
1266
|
+
# as conditional (dropped from the Set), never the reverse.
|
|
1391
1267
|
def unconditional_capture_groups(source)
|
|
1392
|
-
# `unconditional` collects every capturing index; a group is
|
|
1393
|
-
#
|
|
1394
|
-
#
|
|
1395
|
-
#
|
|
1396
|
-
#
|
|
1397
|
-
#
|
|
1398
|
-
#
|
|
1399
|
-
# branches are mutually exclusive, so its descendant captures
|
|
1400
|
-
# may be absent on a successful match; the group itself still
|
|
1401
|
-
# participates. Closing a frame rolls its subtree up to the
|
|
1268
|
+
# `unconditional` collects every capturing index; a group is later removed (with its
|
|
1269
|
+
# whole subtree) when it is optionally quantified, nested under an optional ancestor,
|
|
1270
|
+
# or sits in an alternation branch. `stack` holds one frame per open group (plus a
|
|
1271
|
+
# virtual root frame for the top level) as `[group_index_or_nil, descendant_indices,
|
|
1272
|
+
# alternated?]`. A `|` marks the CURRENT group's frame alternated — its branches are
|
|
1273
|
+
# mutually exclusive, so its descendant captures may be absent on a successful match;
|
|
1274
|
+
# the group itself still participates. Closing a frame rolls its subtree up to the
|
|
1402
1275
|
# parent so an optional / alternated ancestor disqualifies it.
|
|
1403
1276
|
state = { unconditional: Set.new, stack: [[nil, [], false]], group_index: 0 }
|
|
1404
1277
|
pos = 0
|
|
@@ -1422,8 +1295,8 @@ module Rigor
|
|
|
1422
1295
|
end
|
|
1423
1296
|
|
|
1424
1297
|
# Updates the walk `state` at a group-relevant char during
|
|
1425
|
-
# {#unconditional_capture_groups}: `(` pushes a frame, `)` pops
|
|
1426
|
-
#
|
|
1298
|
+
# {#unconditional_capture_groups}: `(` pushes a frame, `)` pops and resolves it, `|`
|
|
1299
|
+
# flags the current frame as alternated.
|
|
1427
1300
|
def scan_group_char(source, pos, chr, state)
|
|
1428
1301
|
case chr
|
|
1429
1302
|
when "("
|
|
@@ -1440,11 +1313,10 @@ module Rigor
|
|
|
1440
1313
|
end
|
|
1441
1314
|
end
|
|
1442
1315
|
|
|
1443
|
-
# Resolves a closed (or virtual-root) group frame: its subtree is
|
|
1444
|
-
#
|
|
1445
|
-
#
|
|
1446
|
-
#
|
|
1447
|
-
# but a self subtree always keeps its own index unless optional).
|
|
1316
|
+
# Resolves a closed (or virtual-root) group frame: its subtree is its own index plus
|
|
1317
|
+
# every descendant index. The subtree is disqualified when the group is optionally
|
|
1318
|
+
# quantified or its branches are alternated (only the descendants in that case — but a
|
|
1319
|
+
# self subtree always keeps its own index unless optional).
|
|
1448
1320
|
def finalize_frame(state, frame, optional:)
|
|
1449
1321
|
idx, descendants, alternated = frame
|
|
1450
1322
|
subtree = descendants.dup
|
|
@@ -1456,11 +1328,10 @@ module Rigor
|
|
|
1456
1328
|
state[:stack].last && state[:stack].last[1].concat(subtree)
|
|
1457
1329
|
end
|
|
1458
1330
|
|
|
1459
|
-
# True when the group whose closing paren is at `source[pos]`
|
|
1460
|
-
#
|
|
1461
|
-
#
|
|
1462
|
-
#
|
|
1463
|
-
# zero-permitting on the base quantifier.
|
|
1331
|
+
# True when the group whose closing paren is at `source[pos]` is followed by a
|
|
1332
|
+
# quantifier that permits zero repetitions (`?`, `*`, `{0…}`). `+` and `{1,…}` do NOT
|
|
1333
|
+
# make a group optional. A lazy/possessive suffix (`*?`, `*+`) is still zero-permitting
|
|
1334
|
+
# on the base quantifier.
|
|
1464
1335
|
def next_quantifier_optional?(source, pos)
|
|
1465
1336
|
case source[pos]
|
|
1466
1337
|
when "?", "*" then true
|
|
@@ -1499,9 +1370,8 @@ module Rigor
|
|
|
1499
1370
|
end
|
|
1500
1371
|
end
|
|
1501
1372
|
|
|
1502
|
-
# `:positive?` / `:negative?` / `:zero?` / `:nonzero?` are
|
|
1503
|
-
#
|
|
1504
|
-
# comparisons against the literal 0 so the existing range
|
|
1373
|
+
# `:positive?` / `:negative?` / `:zero?` / `:nonzero?` are zero-arg predicates on
|
|
1374
|
+
# `Numeric`. We model them as comparisons against the literal 0 so the existing range
|
|
1505
1375
|
# narrowing handles them uniformly.
|
|
1506
1376
|
ZERO_CLASS_PREDICATE_RULES = Ractor.make_shareable({
|
|
1507
1377
|
positive?: { truthy: [:>, 0], falsey: [:<=, 0] },
|
|
@@ -1534,11 +1404,9 @@ module Rigor
|
|
|
1534
1404
|
end
|
|
1535
1405
|
end
|
|
1536
1406
|
|
|
1537
|
-
# `x.between?(a, b)` truthy edge narrows to
|
|
1538
|
-
# `narrow_integer_comparison(
|
|
1539
|
-
# `
|
|
1540
|
-
# unchanged because the complement is a two-piece domain
|
|
1541
|
-
# (`x < a || x > b`) that the lattice cannot express
|
|
1407
|
+
# `x.between?(a, b)` truthy edge narrows to `narrow_integer_comparison(>=, a)` ∩
|
|
1408
|
+
# `narrow_integer_comparison(<=, b)`. The falsey edge is left unchanged because the
|
|
1409
|
+
# complement is a two-piece domain (`x < a || x > b`) that the lattice cannot express
|
|
1542
1410
|
# precisely. `a` and `b` MUST both be integer literals.
|
|
1543
1411
|
def analyse_between_predicate(node, scope)
|
|
1544
1412
|
return nil unless node.receiver.is_a?(Prism::LocalVariableReadNode)
|
|
@@ -1559,9 +1427,8 @@ module Rigor
|
|
|
1559
1427
|
[scope.with_local(local_name, truthy), scope]
|
|
1560
1428
|
end
|
|
1561
1429
|
|
|
1562
|
-
# Helper for {.narrow_integer_not_equal}. Only adjusts when the
|
|
1563
|
-
#
|
|
1564
|
-
# contiguous; otherwise the input range is preserved.
|
|
1430
|
+
# Helper for {.narrow_integer_not_equal}. Only adjusts when the value sits exactly on
|
|
1431
|
+
# one endpoint, so the result stays contiguous; otherwise the input range is preserved.
|
|
1565
1432
|
def narrow_integer_range_not_equal(range, value)
|
|
1566
1433
|
return range if range.lower > value || range.upper < value
|
|
1567
1434
|
return Type::Combinator.bot if single_point_range_equal?(range, value)
|
|
@@ -1607,9 +1474,8 @@ module Rigor
|
|
|
1607
1474
|
# Comparison predicate analyser. Recognised shapes:
|
|
1608
1475
|
# x < Int x <= Int x > Int x >= Int
|
|
1609
1476
|
# Int < x Int <= x Int > x Int >= x
|
|
1610
|
-
# The reversed (literal-on-left) form is normalised by
|
|
1611
|
-
#
|
|
1612
|
-
# appears on the left of the rule.
|
|
1477
|
+
# The reversed (literal-on-left) form is normalised by transposing the operator so the
|
|
1478
|
+
# receiver-local always appears on the left of the rule.
|
|
1613
1479
|
INVERT_COMPARISON_OP = { :< => :>=, :<= => :>, :> => :<=, :>= => :< }.freeze
|
|
1614
1480
|
REVERSE_COMPARISON_OP = { :< => :>, :<= => :>=, :> => :<, :>= => :<= }.freeze
|
|
1615
1481
|
private_constant :INVERT_COMPARISON_OP, :REVERSE_COMPARISON_OP
|
|
@@ -1778,12 +1644,10 @@ module Rigor
|
|
|
1778
1644
|
narrowed == original ? :relational : :local_binding
|
|
1779
1645
|
end
|
|
1780
1646
|
|
|
1781
|
-
# `recv.is_a?(C)` / `recv.kind_of?(C)` / `recv.instance_of?(C)`
|
|
1782
|
-
#
|
|
1783
|
-
#
|
|
1784
|
-
#
|
|
1785
|
-
# can resolve to a qualified class name. Anything else falls
|
|
1786
|
-
# through to "no narrowing".
|
|
1647
|
+
# `recv.is_a?(C)` / `recv.kind_of?(C)` / `recv.instance_of?(C)` narrowing. The receiver
|
|
1648
|
+
# MUST be a `LocalVariableReadNode` (so we have a name to rebind), and the argument
|
|
1649
|
+
# MUST be a single static constant reference (`Foo` or `Foo::Bar`) we can resolve to a
|
|
1650
|
+
# qualified class name. Anything else falls through to "no narrowing".
|
|
1787
1651
|
def analyse_class_predicate(node, scope, exact:)
|
|
1788
1652
|
return nil if node.arguments.nil?
|
|
1789
1653
|
return nil unless node.arguments.arguments.size == 1
|
|
@@ -1791,16 +1655,12 @@ module Rigor
|
|
|
1791
1655
|
bare_name = static_class_name(node.arguments.arguments.first)
|
|
1792
1656
|
return nil if bare_name.nil?
|
|
1793
1657
|
|
|
1794
|
-
# Resolve `bare_name` through the lexical-scope chain
|
|
1795
|
-
#
|
|
1796
|
-
#
|
|
1797
|
-
#
|
|
1798
|
-
#
|
|
1799
|
-
# `
|
|
1800
|
-
# should resolve to `Rigor::Type::Singleton`, not the
|
|
1801
|
-
# top-level stdlib `Singleton` mixin (which would
|
|
1802
|
-
# surface as a spurious `undefined-method` on
|
|
1803
|
-
# subsequent `other.class_name` calls).
|
|
1658
|
+
# Resolve `bare_name` through the lexical-scope chain so a name shadowed by the
|
|
1659
|
+
# current class / enclosing module wins over the top-level constant. Mirrors Ruby's
|
|
1660
|
+
# `Module.nesting`-driven constant lookup. The canonical motivating case: inside
|
|
1661
|
+
# `Rigor::Type::Singleton#==`, `is_a?(Singleton)` should resolve to
|
|
1662
|
+
# `Rigor::Type::Singleton`, not the top-level stdlib `Singleton` mixin (which would
|
|
1663
|
+
# surface as a spurious `undefined-method` on subsequent `other.class_name` calls).
|
|
1804
1664
|
class_name = resolve_class_name_lexically(bare_name, scope)
|
|
1805
1665
|
|
|
1806
1666
|
case node.receiver
|
|
@@ -1818,27 +1678,20 @@ module Rigor
|
|
|
1818
1678
|
class_predicate_scopes(scope, node.receiver.name, current, class_name, exact: exact)
|
|
1819
1679
|
end
|
|
1820
1680
|
|
|
1821
|
-
# Stable single-hop method-chain narrowing (ROADMAP §
|
|
1822
|
-
#
|
|
1823
|
-
#
|
|
1824
|
-
#
|
|
1825
|
-
#
|
|
1826
|
-
# `Scope#method_chain_narrowings` keyed on the chain
|
|
1827
|
-
# address. The dominated body's identical chain reads
|
|
1828
|
-
# then observe the narrowed type through
|
|
1681
|
+
# Stable single-hop method-chain narrowing (ROADMAP § Future cycles — "Method-call
|
|
1682
|
+
# receiver narrowing across stable receivers"). When the predicate's receiver is
|
|
1683
|
+
# `<local/ivar>.<method>` with no args and no block, record the truthy / falsey
|
|
1684
|
+
# narrowing in `Scope#method_chain_narrowings` keyed on the chain address. The
|
|
1685
|
+
# dominated body's identical chain reads then observe the narrowed type through
|
|
1829
1686
|
# `ExpressionTyper#call_type_for`'s lookup.
|
|
1830
1687
|
#
|
|
1831
|
-
# Heuristic-by-design (ROADMAP § "Soundness gap"): a
|
|
1832
|
-
#
|
|
1833
|
-
#
|
|
1834
|
-
#
|
|
1835
|
-
# `
|
|
1836
|
-
#
|
|
1837
|
-
#
|
|
1838
|
-
# invalidation step). The Law of Demeter justifies the
|
|
1839
|
-
# single-hop restriction: a single-hop chain is the
|
|
1840
|
-
# idiomatic Ruby shape where re-evaluation soundness is
|
|
1841
|
-
# the strongest.
|
|
1688
|
+
# Heuristic-by-design (ROADMAP § "Soundness gap"): a second call to `x.last` could in
|
|
1689
|
+
# principle return a different value than the first. The chain is dropped on (1)
|
|
1690
|
+
# receiver variable rebind (handled inside `Scope#with_local` / `#with_ivar`), and (2)
|
|
1691
|
+
# any intervening call against the same root receiver (handled by
|
|
1692
|
+
# `StatementEvaluator#eval_call`'s invalidation step). The Law of Demeter justifies the
|
|
1693
|
+
# single-hop restriction: a single-hop chain is the idiomatic Ruby shape where
|
|
1694
|
+
# re-evaluation soundness is the strongest.
|
|
1842
1695
|
def analyse_class_predicate_on_chain(node, scope, class_name, exact)
|
|
1843
1696
|
address = stable_chain_address(node.receiver)
|
|
1844
1697
|
return nil if address.nil?
|
|
@@ -1855,12 +1708,10 @@ module Rigor
|
|
|
1855
1708
|
]
|
|
1856
1709
|
end
|
|
1857
1710
|
|
|
1858
|
-
# Returns `[receiver_kind, receiver_name, method_name]`
|
|
1859
|
-
#
|
|
1860
|
-
#
|
|
1861
|
-
#
|
|
1862
|
-
# (multi-hop, args, block, method-defined-on-arbitrary-
|
|
1863
|
-
# receiver) lose stability for one of the reasons
|
|
1711
|
+
# Returns `[receiver_kind, receiver_name, method_name]` iff `chain_call` is a stable
|
|
1712
|
+
# single-hop chain whose root is a local / ivar read and whose own call shape has no
|
|
1713
|
+
# positional arguments and no block. Other shapes (multi-hop, args, block,
|
|
1714
|
+
# method-defined-on-arbitrary-receiver) lose stability for one of the reasons
|
|
1864
1715
|
# enumerated in the slice's design notes.
|
|
1865
1716
|
def stable_chain_address(chain_call)
|
|
1866
1717
|
return nil unless chain_call.is_a?(Prism::CallNode)
|
|
@@ -1877,13 +1728,10 @@ module Rigor
|
|
|
1877
1728
|
end
|
|
1878
1729
|
end
|
|
1879
1730
|
|
|
1880
|
-
# Walks the lexical-nesting chain derived from
|
|
1881
|
-
#
|
|
1882
|
-
#
|
|
1883
|
-
#
|
|
1884
|
-
# to `bare_name` itself when nothing in the chain
|
|
1885
|
-
# resolves; the downstream `narrow_class` then yields
|
|
1886
|
-
# the conservative answer for unknown receivers.
|
|
1731
|
+
# Walks the lexical-nesting chain derived from `scope.self_type` and returns the first
|
|
1732
|
+
# `<prefix>::<bare_name>` (or bare `<bare_name>` at the top level) that the environment
|
|
1733
|
+
# recognises. Falls back to `bare_name` itself when nothing in the chain resolves; the
|
|
1734
|
+
# downstream `narrow_class` then yields the conservative answer for unknown receivers.
|
|
1887
1735
|
def resolve_class_name_lexically(bare_name, scope)
|
|
1888
1736
|
return bare_name if bare_name.include?("::") # Already qualified.
|
|
1889
1737
|
|
|
@@ -1895,9 +1743,8 @@ module Rigor
|
|
|
1895
1743
|
bare_name
|
|
1896
1744
|
end
|
|
1897
1745
|
|
|
1898
|
-
# Combines the environment's RBS-known set with the
|
|
1899
|
-
#
|
|
1900
|
-
# lexical-nesting candidate matches a class the project
|
|
1746
|
+
# Combines the environment's RBS-known set with the scope's in-source
|
|
1747
|
+
# `discovered_classes` table so a lexical-nesting candidate matches a class the project
|
|
1901
1748
|
# declares but has no RBS for.
|
|
1902
1749
|
def class_known_to_scope?(scope, candidate)
|
|
1903
1750
|
return true if scope.environment.class_known?(candidate)
|
|
@@ -1905,14 +1752,11 @@ module Rigor
|
|
|
1905
1752
|
scope.discovered_classes.key?(candidate)
|
|
1906
1753
|
end
|
|
1907
1754
|
|
|
1908
|
-
# Approximates `Module.nesting` from the inferable
|
|
1909
|
-
#
|
|
1910
|
-
#
|
|
1911
|
-
#
|
|
1912
|
-
#
|
|
1913
|
-
# the chain is `T`'s namespace path — `Foo::Bar::Baz`
|
|
1914
|
-
# → `["Foo::Bar::Baz", "Foo::Bar", "Foo"]`. Returns an
|
|
1915
|
-
# empty array when `self_type` is unknown.
|
|
1755
|
+
# Approximates `Module.nesting` from the inferable `self_type`. Today's implementation
|
|
1756
|
+
# handles the common case: when the surrounding method is a regular instance method
|
|
1757
|
+
# (`self_type = Nominal[T]`) or a class-body / singleton (`self_type = Singleton[T]`),
|
|
1758
|
+
# the chain is `T`'s namespace path — `Foo::Bar::Baz` → `["Foo::Bar::Baz", "Foo::Bar",
|
|
1759
|
+
# "Foo"]`. Returns an empty array when `self_type` is unknown.
|
|
1916
1760
|
def lexical_nesting_for(scope)
|
|
1917
1761
|
self_type = scope.self_type
|
|
1918
1762
|
base = case self_type
|
|
@@ -1937,33 +1781,24 @@ module Rigor
|
|
|
1937
1781
|
]
|
|
1938
1782
|
end
|
|
1939
1783
|
|
|
1940
|
-
# Slice 7 phase 4 — `===`-narrowing. The case-equality
|
|
1941
|
-
#
|
|
1942
|
-
#
|
|
1943
|
-
# shapes produce sound narrowing rules:
|
|
1784
|
+
# Slice 7 phase 4 — `===`-narrowing. The case-equality predicate `<receiver> === local`
|
|
1785
|
+
# is the operator that backs Ruby's `case`/`when` dispatch. Three receiver shapes
|
|
1786
|
+
# produce sound narrowing rules:
|
|
1944
1787
|
#
|
|
1945
|
-
# - **Class / Module receiver**: `Foo === x` is
|
|
1946
|
-
#
|
|
1947
|
-
#
|
|
1948
|
-
#
|
|
1949
|
-
#
|
|
1950
|
-
#
|
|
1951
|
-
# default `Range#===` includes `x` iff the endpoints
|
|
1952
|
-
# compare it. We conservatively narrow `x` to
|
|
1953
|
-
# `Numeric` for integer-endpoint ranges and to
|
|
1954
|
-
# `String` for string-endpoint ranges; other endpoint
|
|
1788
|
+
# - **Class / Module receiver**: `Foo === x` is isomorphic to `x.is_a?(Foo)` (the
|
|
1789
|
+
# default behaviour of `Module#===`). Reuse `class_predicate_scopes` so the truthy
|
|
1790
|
+
# edge narrows down to `Foo` and the falsey edge subtracts `Foo` from a union.
|
|
1791
|
+
# - **Range literal receiver** (`(1..10) === x`): the default `Range#===` includes `x`
|
|
1792
|
+
# iff the endpoints compare it. We conservatively narrow `x` to `Numeric` for
|
|
1793
|
+
# integer-endpoint ranges and to `String` for string-endpoint ranges; other endpoint
|
|
1955
1794
|
# types fall through.
|
|
1956
|
-
# - **Regexp literal receiver** (`/foo/ === x`): the
|
|
1957
|
-
#
|
|
1958
|
-
#
|
|
1959
|
-
#
|
|
1960
|
-
# `Regexp#===` returns false for non-Strings AND for
|
|
1961
|
-
# Strings that simply do not match.
|
|
1795
|
+
# - **Regexp literal receiver** (`/foo/ === x`): the match operator coerces `x` to a
|
|
1796
|
+
# String, so the truthy edge narrows `x` to `String`. The falsey edge keeps the entry
|
|
1797
|
+
# type unchanged because `Regexp#===` returns false for non-Strings AND for Strings
|
|
1798
|
+
# that simply do not match.
|
|
1962
1799
|
#
|
|
1963
|
-
# Anything else — non-local LHS argument, dynamic
|
|
1964
|
-
#
|
|
1965
|
-
# the no-narrowing branch (nil), preserving the
|
|
1966
|
-
# entry scope on both edges.
|
|
1800
|
+
# Anything else — non-local LHS argument, dynamic receiver, custom `===` method — falls
|
|
1801
|
+
# through to the no-narrowing branch (nil), preserving the entry scope on both edges.
|
|
1967
1802
|
def analyse_case_equality_predicate(node, scope)
|
|
1968
1803
|
return nil if node.arguments.nil?
|
|
1969
1804
|
return nil unless node.arguments.arguments.size == 1
|
|
@@ -1980,14 +1815,12 @@ module Rigor
|
|
|
1980
1815
|
end
|
|
1981
1816
|
end
|
|
1982
1817
|
|
|
1983
|
-
# `Class === <local/ivar>.<method>` — the case-equality counterpart
|
|
1984
|
-
#
|
|
1985
|
-
# `
|
|
1986
|
-
#
|
|
1987
|
-
#
|
|
1988
|
-
#
|
|
1989
|
-
# Regexp literal receivers (`(1..10) === x.foo`) are not a common
|
|
1990
|
-
# method-chain shape and stay deferred.
|
|
1818
|
+
# `Class === <local/ivar>.<method>` — the case-equality counterpart of
|
|
1819
|
+
# {.analyse_class_predicate_on_chain}. The `open3` idiom `if Hash === cmd.last` narrows
|
|
1820
|
+
# `cmd.last` to the class inside the branch, recorded as a single-hop method-chain
|
|
1821
|
+
# narrowing keyed on the chain address (same stability rules as `is_a?` on a chain).
|
|
1822
|
+
# Only static class/module receivers narrow here — the Range / Regexp literal receivers
|
|
1823
|
+
# (`(1..10) === x.foo`) are not a common method-chain shape and stay deferred.
|
|
1991
1824
|
def analyse_case_equality_on_chain(receiver, chain_arg, scope)
|
|
1992
1825
|
class_name = static_class_name(receiver)
|
|
1993
1826
|
return nil if class_name.nil?
|
|
@@ -2021,14 +1854,11 @@ module Rigor
|
|
|
2021
1854
|
]
|
|
2022
1855
|
end
|
|
2023
1856
|
|
|
2024
|
-
# Maps a case-equality literal receiver to the class
|
|
2025
|
-
#
|
|
2026
|
-
# `
|
|
2027
|
-
#
|
|
2028
|
-
#
|
|
2029
|
-
# endpoints → `Numeric`; string endpoints → `String`.
|
|
2030
|
-
# Regexp literals → `String`. Other shapes return nil
|
|
2031
|
-
# so the caller falls through.
|
|
1857
|
+
# Maps a case-equality literal receiver to the class whose membership is implied by the
|
|
1858
|
+
# truthy edge. `Prism::ParenthesesNode` wrappers are transparently unwrapped (`(1..10)
|
|
1859
|
+
# === x` is parsed with the range inside parentheses). Range literals: integer
|
|
1860
|
+
# endpoints → `Numeric`; string endpoints → `String`. Regexp literals → `String`. Other
|
|
1861
|
+
# shapes return nil so the caller falls through.
|
|
2032
1862
|
def case_equality_target_class(receiver)
|
|
2033
1863
|
receiver = unwrap_parens(receiver)
|
|
2034
1864
|
case receiver
|
|
@@ -2045,34 +1875,24 @@ module Rigor
|
|
|
2045
1875
|
node
|
|
2046
1876
|
end
|
|
2047
1877
|
|
|
2048
|
-
# Slice 4b-1 (ADR-7 § "Slice 4-A/4-B") — single-point
|
|
2049
|
-
#
|
|
2050
|
-
#
|
|
2051
|
-
#
|
|
2052
|
-
#
|
|
2053
|
-
# `
|
|
2054
|
-
# `
|
|
2055
|
-
#
|
|
2056
|
-
# 4a routing closed the v0.0.9 imperfection); the
|
|
2057
|
-
# `falsey_facts` slot mirrors that. The merger composes
|
|
2058
|
-
# any future plugin contribution at the same site
|
|
2059
|
-
# alongside the RBS::Extended bundle without changing
|
|
2060
|
-
# this analyser.
|
|
1878
|
+
# Slice 4b-1 (ADR-7 § "Slice 4-A/4-B") — single-point RBS::Extended contribution
|
|
1879
|
+
# analyser. Replaces the earlier sibling pair (`analyse_rbs_extended_predicate` +
|
|
1880
|
+
# `analyse_rbs_extended_assert_if`) with one path that routes through
|
|
1881
|
+
# `RbsExtended.read_flow_contribution` and `Rigor::FlowContribution::Merger.merge`. The
|
|
1882
|
+
# bundle's `truthy_facts` slot already includes both the `predicate-if-true` and
|
|
1883
|
+
# `assert-if-true` Facts (slice 4a routing closed the v0.0.9 imperfection); the
|
|
1884
|
+
# `falsey_facts` slot mirrors that. The merger composes any future plugin contribution
|
|
1885
|
+
# at the same site alongside the RBS::Extended bundle without changing this analyser.
|
|
2061
1886
|
#
|
|
2062
|
-
# Conservative envelope (carried over from the previous
|
|
2063
|
-
#
|
|
2064
|
-
# - Receiver type must be `Type::Nominal`,
|
|
2065
|
-
# `Type::Singleton`, or `Type::Constant`.
|
|
1887
|
+
# Conservative envelope (carried over from the previous implementation):
|
|
1888
|
+
# - Receiver type must be `Type::Nominal`, `Type::Singleton`, or `Type::Constant`.
|
|
2066
1889
|
# - The method must be present in the loader.
|
|
2067
|
-
# - For each fact, the corresponding positional
|
|
2068
|
-
#
|
|
2069
|
-
#
|
|
2070
|
-
#
|
|
2071
|
-
#
|
|
2072
|
-
#
|
|
2073
|
-
# `self` itself, so `self`-targeted facts are
|
|
2074
|
-
# accepted by the merger but currently produce no
|
|
2075
|
-
# scope edits.
|
|
1890
|
+
# - For each fact, the corresponding positional argument (matched by parameter name in
|
|
1891
|
+
# the selected overload) MUST be a `Prism::LocalVariableReadNode` for narrowing to
|
|
1892
|
+
# apply.
|
|
1893
|
+
# - When the target is `self`, narrowing applies to the receiver — but the engine does
|
|
1894
|
+
# not yet narrow `self` itself, so `self`-targeted facts are accepted by the merger
|
|
1895
|
+
# but currently produce no scope edits.
|
|
2076
1896
|
def analyse_rbs_extended_contribution(node, scope)
|
|
2077
1897
|
method_def = resolve_rbs_extended_method(node, scope)
|
|
2078
1898
|
return nil if method_def.nil?
|
|
@@ -2113,10 +1933,9 @@ module Rigor
|
|
|
2113
1933
|
target_scope.with_local(local_name, narrowed)
|
|
2114
1934
|
end
|
|
2115
1935
|
|
|
2116
|
-
# v0.1.1 Track 1 slice 3 — `target_kind == :self` facts
|
|
2117
|
-
#
|
|
2118
|
-
#
|
|
2119
|
-
# the call's receiver. Four receiver shapes participate:
|
|
1936
|
+
# v0.1.1 Track 1 slice 3 — `target_kind == :self` facts (`predicate-if-true self is T`,
|
|
1937
|
+
# `predicate-if-false self is T`) narrow whichever scope binding is bound to the call's
|
|
1938
|
+
# receiver. Four receiver shapes participate:
|
|
2120
1939
|
#
|
|
2121
1940
|
# `recv.method?` where recv is...
|
|
2122
1941
|
# - `LocalVariableReadNode` -> narrow that local
|
|
@@ -2125,8 +1944,8 @@ module Rigor
|
|
|
2125
1944
|
# - nil (implicit self call inside a method body)
|
|
2126
1945
|
# -> narrow `self_type`
|
|
2127
1946
|
#
|
|
2128
|
-
# Other receiver shapes (method chains, expressions) have
|
|
2129
|
-
#
|
|
1947
|
+
# Other receiver shapes (method chains, expressions) have no scope binding to narrow
|
|
1948
|
+
# against and fall through.
|
|
2130
1949
|
def apply_self_fact(fact, receiver_node, entry_scope, target_scope)
|
|
2131
1950
|
case receiver_node
|
|
2132
1951
|
when nil, Prism::SelfNode
|
|
@@ -2152,11 +1971,9 @@ module Rigor
|
|
|
2152
1971
|
end
|
|
2153
1972
|
end
|
|
2154
1973
|
|
|
2155
|
-
# Resolves a Fact's target node. Mirrors the earlier
|
|
2156
|
-
# `
|
|
2157
|
-
#
|
|
2158
|
-
# receiver, otherwise we look up the matching
|
|
2159
|
-
# positional argument by parameter name.
|
|
1974
|
+
# Resolves a Fact's target node. Mirrors the earlier `effect_target_node` helper but
|
|
1975
|
+
# reads from the canonical Fact carrier; `:self` routes to the call receiver, otherwise
|
|
1976
|
+
# we look up the matching positional argument by parameter name.
|
|
2160
1977
|
def fact_target_node(fact, call_node, method_def)
|
|
2161
1978
|
if fact.target_kind == :self
|
|
2162
1979
|
call_node.receiver
|
|
@@ -2182,9 +1999,8 @@ module Rigor
|
|
|
2182
1999
|
nil
|
|
2183
2000
|
end
|
|
2184
2001
|
|
|
2185
|
-
# Implicit self call (`admin?` with no receiver inside an
|
|
2186
|
-
#
|
|
2187
|
-
# `scope.self_type` per v0.1.1 Track 1 slice 3.
|
|
2002
|
+
# Implicit self call (`admin?` with no receiver inside an instance method body) — read
|
|
2003
|
+
# the method definition from `scope.self_type` per v0.1.1 Track 1 slice 3.
|
|
2188
2004
|
def receiver_type_for_resolve(node, scope)
|
|
2189
2005
|
node.receiver.nil? ? scope.self_type : scope.type_of(node.receiver)
|
|
2190
2006
|
end
|
|
@@ -2209,11 +2025,9 @@ module Rigor
|
|
|
2209
2025
|
nil
|
|
2210
2026
|
end
|
|
2211
2027
|
|
|
2212
|
-
# Maps the effect's target parameter name to the call
|
|
2213
|
-
#
|
|
2214
|
-
#
|
|
2215
|
-
# arg node at that position, or nil when the overload
|
|
2216
|
-
# shape does not allow a precise match.
|
|
2028
|
+
# Maps the effect's target parameter name to the call site argument by inspecting the
|
|
2029
|
+
# selected overload's required-positional parameter list. Returns the Prism arg node at
|
|
2030
|
+
# that position, or nil when the overload shape does not allow a precise match.
|
|
2217
2031
|
def lookup_positional_arg(call_node, method_def, target_name)
|
|
2218
2032
|
arguments = call_node.arguments&.arguments || []
|
|
2219
2033
|
method_def.method_types.each do |mt|
|
|
@@ -2224,18 +2038,13 @@ module Rigor
|
|
|
2224
2038
|
nil
|
|
2225
2039
|
end
|
|
2226
2040
|
|
|
2227
|
-
# Slice 7 phase 5 — case/when accumulator. Walks each
|
|
2228
|
-
#
|
|
2229
|
-
#
|
|
2230
|
-
#
|
|
2231
|
-
#
|
|
2232
|
-
#
|
|
2233
|
-
#
|
|
2234
|
-
# as "no narrowing": the body falls back to the union of
|
|
2235
|
-
# what we did learn (or the entry type when nothing
|
|
2236
|
-
# learned), and the falsey edge is the entry type
|
|
2237
|
-
# (because we cannot prove the unknown condition didn't
|
|
2238
|
-
# match).
|
|
2041
|
+
# Slice 7 phase 5 — case/when accumulator. Walks each `when` condition, computes the
|
|
2042
|
+
# narrowed type for the subject as if `condition === subject`, and accumulates them.
|
|
2043
|
+
# The body's narrowed type is the union across all conditions; the falsey type is the
|
|
2044
|
+
# running result after subtracting every condition's class. Conditions whose shape we
|
|
2045
|
+
# cannot statically classify are treated as "no narrowing": the body falls back to the
|
|
2046
|
+
# union of what we did learn (or the entry type when nothing learned), and the falsey
|
|
2047
|
+
# edge is the entry type (because we cannot prove the unknown condition didn't match).
|
|
2239
2048
|
def accumulate_case_when_scopes(scope, local_name, current, conditions)
|
|
2240
2049
|
truthy_members = []
|
|
2241
2050
|
falsey_type = current
|
|
@@ -2259,9 +2068,9 @@ module Rigor
|
|
|
2259
2068
|
]
|
|
2260
2069
|
end
|
|
2261
2070
|
|
|
2262
|
-
# Per-condition rule. Returns `nil` when the condition shape
|
|
2263
|
-
#
|
|
2264
|
-
#
|
|
2071
|
+
# Per-condition rule. Returns `nil` when the condition shape is not recognised (caller
|
|
2072
|
+
# marks `fully_narrowable = false`), or `{truthy:, falsey:, fully_narrowable:}` when it
|
|
2073
|
+
# is.
|
|
2265
2074
|
def apply_case_when_condition(scope, current, condition, falsey_acc)
|
|
2266
2075
|
int_range = case_equality_integer_range(condition)
|
|
2267
2076
|
return integer_range_when_result(current, int_range, falsey_acc) if int_range && integer_rooted_type?(current)
|
|
@@ -2283,11 +2092,10 @@ module Rigor
|
|
|
2283
2092
|
end
|
|
2284
2093
|
|
|
2285
2094
|
def integer_literal_when_result(current, value, falsey_acc)
|
|
2286
|
-
# `case n when k` is `k === n` which for Integer is value
|
|
2287
|
-
#
|
|
2288
|
-
# `
|
|
2289
|
-
#
|
|
2290
|
-
# at one endpoint of the current range).
|
|
2095
|
+
# `case n when k` is `k === n` which for Integer is value equality. The truthy edge
|
|
2096
|
+
# collapses the local to `Constant[k]`; the falsey edge tightens via
|
|
2097
|
+
# `narrow_integer_not_equal` (only effective when k sits at one endpoint of the
|
|
2098
|
+
# current range).
|
|
2291
2099
|
{
|
|
2292
2100
|
truthy: narrow_integer_equal(current, value),
|
|
2293
2101
|
falsey: narrow_integer_not_equal(falsey_acc, value),
|
|
@@ -2301,10 +2109,9 @@ module Rigor
|
|
|
2301
2109
|
narrow_integer_comparison(current, :>=, low),
|
|
2302
2110
|
:<=, high
|
|
2303
2111
|
)
|
|
2304
|
-
# The falsey edge of `n in [a, b]` is two-piece; we cannot
|
|
2305
|
-
#
|
|
2306
|
-
#
|
|
2307
|
-
# forces the else-branch to see `current` (the unmodified
|
|
2112
|
+
# The falsey edge of `n in [a, b]` is two-piece; we cannot express the complement
|
|
2113
|
+
# precisely with a single carrier, so keep the accumulator unchanged.
|
|
2114
|
+
# `fully_narrowable: false` forces the else-branch to see `current` (the unmodified
|
|
2308
2115
|
# entry type), which mirrors `between?` falsey behaviour.
|
|
2309
2116
|
{ truthy: truthy, falsey: falsey_acc, fully_narrowable: false }
|
|
2310
2117
|
end
|
|
@@ -2317,12 +2124,10 @@ module Rigor
|
|
|
2317
2124
|
}
|
|
2318
2125
|
end
|
|
2319
2126
|
|
|
2320
|
-
# Returns `[low, high]` for a `Prism::RangeNode` whose
|
|
2321
|
-
#
|
|
2322
|
-
#
|
|
2323
|
-
#
|
|
2324
|
-
# narrowing tier handles them. Returns `nil` for any other
|
|
2325
|
-
# shape (Float endpoints, String endpoints, dynamic
|
|
2127
|
+
# Returns `[low, high]` for a `Prism::RangeNode` whose endpoints are both
|
|
2128
|
+
# `Prism::IntegerNode` literals, with `..`/`...` exclusivity respected. Open-ended
|
|
2129
|
+
# ranges use the symbolic infinities so the existing comparison narrowing tier handles
|
|
2130
|
+
# them. Returns `nil` for any other shape (Float endpoints, String endpoints, dynamic
|
|
2326
2131
|
# expressions).
|
|
2327
2132
|
def case_equality_integer_range(condition)
|
|
2328
2133
|
condition = unwrap_parens(condition)
|
|
@@ -2369,9 +2174,8 @@ module Rigor
|
|
|
2369
2174
|
node.is_a?(Prism::StringNode)
|
|
2370
2175
|
end
|
|
2371
2176
|
|
|
2372
|
-
# Walks a constant-reference subtree (`Prism::ConstantReadNode`,
|
|
2373
|
-
#
|
|
2374
|
-
# Returns nil for any non-constant argument shape so the
|
|
2177
|
+
# Walks a constant-reference subtree (`Prism::ConstantReadNode`, `Prism::ConstantPathNode`)
|
|
2178
|
+
# and renders its qualified name. Returns nil for any non-constant argument shape so the
|
|
2375
2179
|
# caller can fall through.
|
|
2376
2180
|
def static_class_name(node)
|
|
2377
2181
|
case node
|
|
@@ -2390,11 +2194,10 @@ module Rigor
|
|
|
2390
2194
|
|
|
2391
2195
|
# ----- narrow_class / narrow_not_class helpers -----
|
|
2392
2196
|
|
|
2393
|
-
# Polarity-aware dispatch table for {.narrow_class} /
|
|
2394
|
-
#
|
|
2395
|
-
#
|
|
2396
|
-
#
|
|
2397
|
-
# apply by looking at `polarity:`.
|
|
2197
|
+
# Polarity-aware dispatch table for {.narrow_class} / {.narrow_not_class}. Avoids
|
|
2198
|
+
# duplicating the per-carrier case statement and keeps each public surface a thin
|
|
2199
|
+
# delegate; the per-carrier helpers know which polarity to apply by looking at
|
|
2200
|
+
# `polarity:`.
|
|
2398
2201
|
def narrow_class_dispatch(type, class_name, context)
|
|
2399
2202
|
case type
|
|
2400
2203
|
when Type::Constant then narrow_constant_class(type, class_name, context)
|
|
@@ -2459,15 +2262,12 @@ module Rigor
|
|
|
2459
2262
|
subclass_of?(rigor_class, class_name, context) ? Type::Combinator.bot : constant
|
|
2460
2263
|
end
|
|
2461
2264
|
|
|
2462
|
-
# Narrow a Nominal under `is_a?(class_name)`: when the
|
|
2463
|
-
#
|
|
2464
|
-
#
|
|
2465
|
-
# `
|
|
2466
|
-
#
|
|
2467
|
-
#
|
|
2468
|
-
# under `is_a?`, mismatch under `instance_of?`) collapse to
|
|
2469
|
-
# `Bot`. Conservative when the analyzer environment cannot
|
|
2470
|
-
# resolve either class.
|
|
2265
|
+
# Narrow a Nominal under `is_a?(class_name)`: when the nominal's class is already a
|
|
2266
|
+
# subclass of `class_name` (or matches under `exact: true`) preserve it; when
|
|
2267
|
+
# `class_name` is a subclass of the nominal's class (`Nominal[Numeric]` under
|
|
2268
|
+
# `is_a?(Integer)`) narrow DOWN to `Nominal[class_name]`; otherwise (disjoint
|
|
2269
|
+
# hierarchies under `is_a?`, mismatch under `instance_of?`) collapse to `Bot`.
|
|
2270
|
+
# Conservative when the analyzer environment cannot resolve either class.
|
|
2471
2271
|
def narrow_nominal_to_class(nominal, class_name, context)
|
|
2472
2272
|
return nominal if nominal.class_name == class_name
|
|
2473
2273
|
return Type::Combinator.bot if context.exact
|
|
@@ -2498,11 +2298,10 @@ module Rigor
|
|
|
2498
2298
|
subclass_of?(projected_class, class_name, context) ? Type::Combinator.bot : shape
|
|
2499
2299
|
end
|
|
2500
2300
|
|
|
2501
|
-
# `Singleton[Foo]` is the *class object* `Foo`, an instance of
|
|
2502
|
-
#
|
|
2503
|
-
# `Foo
|
|
2504
|
-
#
|
|
2505
|
-
# by treating singletons uniformly as `Class` instances.
|
|
2301
|
+
# `Singleton[Foo]` is the *class object* `Foo`, an instance of `Class` (which is a
|
|
2302
|
+
# subclass of `Module`). Asking `Foo.is_a?(Class)` returns true; `Foo.is_a?(Foo)`
|
|
2303
|
+
# returns false unless `Foo` is `Class` itself. We approximate this by treating
|
|
2304
|
+
# singletons uniformly as `Class` instances.
|
|
2506
2305
|
def narrow_singleton_to_class(singleton, class_name, context)
|
|
2507
2306
|
subclass_of?("Class", class_name, context) ? singleton : Type::Combinator.bot
|
|
2508
2307
|
end
|
|
@@ -2511,8 +2310,8 @@ module Rigor
|
|
|
2511
2310
|
subclass_of?("Class", class_name, context) ? Type::Combinator.bot : singleton
|
|
2512
2311
|
end
|
|
2513
2312
|
|
|
2514
|
-
# Top/Dynamic narrow to `Nominal[class_name]` so dispatch
|
|
2515
|
-
#
|
|
2313
|
+
# Top/Dynamic narrow to `Nominal[class_name]` so dispatch can resolve through the asked
|
|
2314
|
+
# class; Bot stays Bot.
|
|
2516
2315
|
def narrow_class_other(type, class_name)
|
|
2517
2316
|
case type
|
|
2518
2317
|
when Type::Dynamic, Type::Top then Type::Combinator.nominal_of(class_name)
|
|
@@ -2520,11 +2319,10 @@ module Rigor
|
|
|
2520
2319
|
end
|
|
2521
2320
|
end
|
|
2522
2321
|
|
|
2523
|
-
# Returns `true` when an instance of `rigor_class_name`
|
|
2524
|
-
#
|
|
2525
|
-
# `
|
|
2526
|
-
#
|
|
2527
|
-
# resolve through the analyzer environment.
|
|
2322
|
+
# Returns `true` when an instance of `rigor_class_name` satisfies
|
|
2323
|
+
# `is_a?(target_class_name)` (or `instance_of?(target_class_name)` when `exact: true`).
|
|
2324
|
+
# Falls back to the safe `false` when either name does not resolve through the analyzer
|
|
2325
|
+
# environment.
|
|
2528
2326
|
def subclass_of?(rigor_class_name, target_class_name, context)
|
|
2529
2327
|
return rigor_class_name == target_class_name if context.exact
|
|
2530
2328
|
|
|
@@ -2533,11 +2331,9 @@ module Rigor
|
|
|
2533
2331
|
)
|
|
2534
2332
|
end
|
|
2535
2333
|
|
|
2536
|
-
# Compares two class names through the analyzer environment.
|
|
2537
|
-
#
|
|
2538
|
-
# `:
|
|
2539
|
-
# `rhs <= lhs`, `:disjoint` when neither, and `:unknown` when
|
|
2540
|
-
# either name does not resolve.
|
|
2334
|
+
# Compares two class names through the analyzer environment. Returns `:equal` when they
|
|
2335
|
+
# resolve to the same class, `:subclass` when `lhs <= rhs`, `:superclass` when `rhs <=
|
|
2336
|
+
# lhs`, `:disjoint` when neither, and `:unknown` when either name does not resolve.
|
|
2541
2337
|
def class_ordering(lhs, rhs, context)
|
|
2542
2338
|
return :equal if lhs == rhs
|
|
2543
2339
|
|
|
@@ -2565,11 +2361,119 @@ module Rigor
|
|
|
2565
2361
|
end
|
|
2566
2362
|
end
|
|
2567
2363
|
|
|
2568
|
-
#
|
|
2569
|
-
#
|
|
2570
|
-
#
|
|
2571
|
-
|
|
2572
|
-
|
|
2364
|
+
# The three value-pinned classes a union arm can name whose instances are the single
|
|
2365
|
+
# value itself. Their finality is what makes the polarity rule below sound: no subclass
|
|
2366
|
+
# can override the predicate, because there is no subclass.
|
|
2367
|
+
POLARITY_ARM_CLASSES = { nil => "NilClass", true => "TrueClass", false => "FalseClass" }.freeze
|
|
2368
|
+
private_constant :POLARITY_ARM_CLASSES
|
|
2369
|
+
|
|
2370
|
+
# Drops union arms a zero-argument predicate's own signature rules out on each edge.
|
|
2371
|
+
#
|
|
2372
|
+
# `if x.present?` with `x: String | nil` must see `String` in the body. Rigor could not:
|
|
2373
|
+
# `present?` is an ActiveSupport method (so the engine must not join the hardcoded
|
|
2374
|
+
# `nil?` / `empty?` catalogue with it), and `resolve_rbs_extended_method` hands a union
|
|
2375
|
+
# receiver no `rigor:v1:predicate-if-true` facts, so annotating the RBS would not help
|
|
2376
|
+
# either. But the answer is already written down: the bundled ActiveSupport signature
|
|
2377
|
+
# declares `NilClass#present?: () -> false`, and a method that always returns `false` for
|
|
2378
|
+
# `nil` cannot have answered truthily on a `nil` receiver.
|
|
2379
|
+
#
|
|
2380
|
+
# Soundness rests on the arm being a value-pinned `nil` / `true` / `false`. For an
|
|
2381
|
+
# ordinary `Nominal[Foo]` arm the static type admits Foo's subclasses, any of which may
|
|
2382
|
+
# override the predicate and return the other polarity; the three classes here have no
|
|
2383
|
+
# subclasses, so the declared return is the runtime return.
|
|
2384
|
+
#
|
|
2385
|
+
# Safe navigation is excluded: `x&.blank?` yields `nil` (falsey) for a nil receiver
|
|
2386
|
+
# rather than `NilClass#blank?`'s declared `true`, so the falsey edge would wrongly drop
|
|
2387
|
+
# the nil arm. `analyse_safe_nav_receiver` below handles that shape's truthy edge.
|
|
2388
|
+
#
|
|
2389
|
+
# Returns `[truthy_scope, falsey_scope]`, or nil when no arm is ruled out, when an edge
|
|
2390
|
+
# would be emptied, or when the receiver has no scope binding to narrow.
|
|
2391
|
+
def analyse_union_predicate_polarity(node, scope)
|
|
2392
|
+
return nil if node.safe_navigation? || !argument_free?(node)
|
|
2393
|
+
return nil unless node.name.end_with?("?")
|
|
2394
|
+
|
|
2395
|
+
receiver_type = receiver_binding_type(node.receiver, scope)
|
|
2396
|
+
return nil unless receiver_type.is_a?(Type::Union)
|
|
2397
|
+
|
|
2398
|
+
truthy, falsey = partition_arms_by_polarity(receiver_type.members, node.name, scope)
|
|
2399
|
+
return nil if truthy.size == receiver_type.members.size && falsey.size == receiver_type.members.size
|
|
2400
|
+
return nil if truthy.empty? || falsey.empty?
|
|
2401
|
+
|
|
2402
|
+
[
|
|
2403
|
+
narrow_receiver_binding(node.receiver, scope, Type::Combinator.union(*truthy)),
|
|
2404
|
+
narrow_receiver_binding(node.receiver, scope, Type::Combinator.union(*falsey))
|
|
2405
|
+
]
|
|
2406
|
+
end
|
|
2407
|
+
|
|
2408
|
+
# Splits `members` into the arms that survive the truthy edge and those that survive the
|
|
2409
|
+
# falsey edge. An arm whose polarity is unknown survives both.
|
|
2410
|
+
def partition_arms_by_polarity(members, method_name, scope)
|
|
2411
|
+
truthy = []
|
|
2412
|
+
falsey = []
|
|
2413
|
+
members.each do |arm|
|
|
2414
|
+
case arm_predicate_polarity(arm, method_name, scope)
|
|
2415
|
+
when :always_false then falsey << arm
|
|
2416
|
+
when :always_true then truthy << arm
|
|
2417
|
+
else
|
|
2418
|
+
truthy << arm
|
|
2419
|
+
falsey << arm
|
|
2420
|
+
end
|
|
2421
|
+
end
|
|
2422
|
+
[truthy, falsey]
|
|
2423
|
+
end
|
|
2424
|
+
|
|
2425
|
+
# `:always_false` / `:always_true` when every RBS overload of `method_name` on the arm's
|
|
2426
|
+
# class returns that literal; nil for any other arm, signature, or lookup failure.
|
|
2427
|
+
def arm_predicate_polarity(arm, method_name, scope)
|
|
2428
|
+
returns = arm_predicate_return_types(arm, method_name, scope)
|
|
2429
|
+
return nil if returns.nil? || returns.empty?
|
|
2430
|
+
return :always_false if returns.all? { |type| literal_boolean?(type, false) }
|
|
2431
|
+
return :always_true if returns.all? { |type| literal_boolean?(type, true) }
|
|
2432
|
+
|
|
2433
|
+
nil
|
|
2434
|
+
end
|
|
2435
|
+
|
|
2436
|
+
# Declared return type of every RBS overload of `method_name` on the arm's class, or nil
|
|
2437
|
+
# when the arm is not one of the three value-pinned classes or the lookup fails.
|
|
2438
|
+
def arm_predicate_return_types(arm, method_name, scope)
|
|
2439
|
+
return nil unless arm.is_a?(Type::Constant)
|
|
2440
|
+
|
|
2441
|
+
class_name = POLARITY_ARM_CLASSES[arm.value]
|
|
2442
|
+
return nil if class_name.nil?
|
|
2443
|
+
|
|
2444
|
+
definition = Rigor::Reflection.instance_method_definition(class_name, method_name, scope: scope)
|
|
2445
|
+
definition&.method_types&.map { |method_type| method_type.type.return_type }
|
|
2446
|
+
rescue StandardError
|
|
2447
|
+
nil
|
|
2448
|
+
end
|
|
2449
|
+
|
|
2450
|
+
def literal_boolean?(rbs_type, value)
|
|
2451
|
+
rbs_type.is_a?(RBS::Types::Literal) && rbs_type.literal == value
|
|
2452
|
+
end
|
|
2453
|
+
|
|
2454
|
+
# The four receiver shapes with a scope binding the edges can rebind, mirroring
|
|
2455
|
+
# `apply_self_fact`. A method chain or arbitrary expression has none, so it reads nil and
|
|
2456
|
+
# the caller declines.
|
|
2457
|
+
def receiver_binding_type(receiver, scope)
|
|
2458
|
+
case receiver
|
|
2459
|
+
when Prism::LocalVariableReadNode then scope.local(receiver.name)
|
|
2460
|
+
when Prism::InstanceVariableReadNode then scope.ivar(receiver.name)
|
|
2461
|
+
when Prism::SelfNode then scope.self_type
|
|
2462
|
+
end
|
|
2463
|
+
end
|
|
2464
|
+
|
|
2465
|
+
def narrow_receiver_binding(receiver, scope, narrowed)
|
|
2466
|
+
case receiver
|
|
2467
|
+
when Prism::LocalVariableReadNode then scope.with_local(receiver.name, narrowed)
|
|
2468
|
+
when Prism::InstanceVariableReadNode then scope.with_ivar(receiver.name, narrowed)
|
|
2469
|
+
when Prism::SelfNode then scope.with_self_type(narrowed)
|
|
2470
|
+
end
|
|
2471
|
+
end
|
|
2472
|
+
|
|
2473
|
+
# Narrows a safe-navigation call's receiver (`v&.foo`) to its non-nil fragment on the
|
|
2474
|
+
# truthy edge, returning `[truthy, falsey]` or nil when nothing applies (not safe-nav,
|
|
2475
|
+
# opaque receiver, or already non-nil). Used standalone for a bare `v&.foo` truthy edge
|
|
2476
|
+
# and as a post-pass over the existing predicate edges.
|
|
2573
2477
|
def analyse_safe_nav_receiver(node, scope)
|
|
2574
2478
|
return nil unless node.safe_navigation?
|
|
2575
2479
|
|
|
@@ -2590,12 +2494,11 @@ module Rigor
|
|
|
2590
2494
|
[scope.public_send(writer, receiver.name, non_nil), scope]
|
|
2591
2495
|
end
|
|
2592
2496
|
|
|
2593
|
-
# Layers the safe-nav non-nil truthy narrowing over the edges an
|
|
2594
|
-
#
|
|
2595
|
-
#
|
|
2596
|
-
#
|
|
2597
|
-
#
|
|
2598
|
-
# to the narrowed binding. No-op for non-safe-nav calls.
|
|
2497
|
+
# Layers the safe-nav non-nil truthy narrowing over the edges an existing predicate path
|
|
2498
|
+
# already produced, so a safe-nav string predicate (`v&.start_with?(x)`) keeps its
|
|
2499
|
+
# relational fact AND proves `v` non-nil on the truthy edge. Re-runs the predicate's
|
|
2500
|
+
# narrowing under the non-nil truthy scope so the fact is attached to the narrowed
|
|
2501
|
+
# binding. No-op for non-safe-nav calls.
|
|
2599
2502
|
def apply_safe_nav_non_nil(node, scope, edges)
|
|
2600
2503
|
return edges unless node.safe_navigation? && edges
|
|
2601
2504
|
|
|
@@ -2610,23 +2513,20 @@ module Rigor
|
|
|
2610
2513
|
[truthy.public_send(writer, receiver.name, non_nil), falsey]
|
|
2611
2514
|
end
|
|
2612
2515
|
|
|
2613
|
-
# `a && b` short-circuits: the truthy edge is the truthy edge
|
|
2614
|
-
#
|
|
2615
|
-
#
|
|
2616
|
-
#
|
|
2617
|
-
#
|
|
2618
|
-
# caller still sees consistent keys across the two output
|
|
2619
|
-
# scopes.
|
|
2516
|
+
# `a && b` short-circuits: the truthy edge is the truthy edge of `b` evaluated under
|
|
2517
|
+
# `a`'s truthy scope; the falsey edge is the union of `a`'s falsey scope (b skipped) and
|
|
2518
|
+
# `b`'s falsey scope (b ran but returned falsey). When a sub-edge cannot be narrowed we
|
|
2519
|
+
# fall back to the entry scope so the caller still sees consistent keys across the two
|
|
2520
|
+
# output scopes.
|
|
2620
2521
|
def analyse_and(node, scope)
|
|
2621
2522
|
truthy_a, falsey_a = analyse(node.left, scope) || [scope, scope]
|
|
2622
2523
|
truthy_b, falsey_b = analyse(node.right, truthy_a) || [truthy_a, truthy_a]
|
|
2623
2524
|
[truthy_b, falsey_a.join(falsey_b)]
|
|
2624
2525
|
end
|
|
2625
2526
|
|
|
2626
|
-
# `a || b` short-circuits: the truthy edge is the union of
|
|
2627
|
-
# `
|
|
2628
|
-
#
|
|
2629
|
-
# evaluated under `a`'s falsey scope.
|
|
2527
|
+
# `a || b` short-circuits: the truthy edge is the union of `a`'s truthy scope (b
|
|
2528
|
+
# skipped) and `b`'s truthy scope (b ran and was truthy); the falsey edge is `b`'s
|
|
2529
|
+
# falsey scope evaluated under `a`'s falsey scope.
|
|
2630
2530
|
def analyse_or(node, scope)
|
|
2631
2531
|
truthy_a, falsey_a = analyse(node.left, scope) || [scope, scope]
|
|
2632
2532
|
truthy_b, falsey_b = analyse(node.right, falsey_a) || [falsey_a, falsey_a]
|