rigortype 0.2.7 → 0.2.8
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/docs/manual/04-diagnostics.md +7 -4
- data/docs/manual/06-baseline.md +5 -2
- data/docs/manual/15-type-protection-coverage.md +6 -4
- 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 +96 -151
- 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 +19 -26
- 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 +43 -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_renderer.rb +5 -7
- data/lib/rigor/cli/protection_report.rb +27 -17
- 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/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 +176 -291
- 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 +774 -1152
- 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/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 +509 -727
- data/lib/rigor/inference/origin_lookup.rb +38 -0
- data/lib/rigor/inference/parameter_inference_collector.rb +100 -100
- 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 -817
- data/lib/rigor/inference/statement_evaluator.rb +706 -1111
- 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 +118 -176
- 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 +71 -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 +30 -47
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +79 -129
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +36 -57
- 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.rb +148 -235
- 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-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/helper_discoverer.rb +38 -70
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +42 -74
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +280 -497
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +54 -89
- 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/scope.rbs +6 -0
- metadata +2 -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,32 @@ 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 safe-navigation call (`v&.foo`) whose result is truthy
|
|
976
|
-
#
|
|
977
|
-
#
|
|
978
|
-
#
|
|
979
|
-
#
|
|
980
|
-
#
|
|
981
|
-
# in the `&&` right operand. The falsey edge is the
|
|
982
|
-
# conservative no-op (falsey could mean nil receiver OR a
|
|
983
|
-
# falsey method result).
|
|
876
|
+
# A safe-navigation call (`v&.foo`) whose result is truthy proves the receiver was
|
|
877
|
+
# non-nil — `&.` returns `nil` when the receiver is nil, so a truthy outcome can
|
|
878
|
+
# only come from a non-nil receiver. Narrow the receiver on the truthy edge even
|
|
879
|
+
# when the call itself carries no other flow fact, so `v&.start_with?('[') &&
|
|
880
|
+
# v.end_with?(']')` sees `v` non-nil in the `&&` right operand. The falsey edge is
|
|
881
|
+
# the conservative no-op (falsey could mean nil receiver OR a falsey method result).
|
|
984
882
|
safe_nav_result = analyse_safe_nav_receiver(node, scope)
|
|
985
883
|
return safe_nav_result if safe_nav_result
|
|
986
884
|
end
|
|
987
885
|
|
|
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
|
|
886
|
+
# Slice 7 phase 15 — RBS::Extended predicate effects. When the method's RBS signature
|
|
887
|
+
# carries `rigor:v1:predicate-if-true` / `predicate-if-false` annotations, apply them
|
|
888
|
+
# to narrow the corresponding local-variable arguments on each edge. v0.1.1 Track 1
|
|
889
|
+
# slice 3 — implicit-self calls (`recv == nil`, e.g. `admin?` inside an instance
|
|
890
|
+
# method body) flow through here too so `self`-targeted facts can edit
|
|
996
891
|
# `scope.self_type`.
|
|
997
892
|
analyse_rbs_extended_contribution(node, scope)
|
|
998
893
|
end
|
|
999
894
|
|
|
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`.
|
|
895
|
+
# v0.1.1 Track 1 slice 4 — `String#start_with?` / `#end_with?` / `#include?` against a
|
|
896
|
+
# `Constant<String>` needle attaches a relational `FactStore::Fact` to the local on
|
|
897
|
+
# each edge. The receiver's type does NOT change — Rigor has no "starts-with-X" carrier
|
|
898
|
+
# today — so the fact carries the predicate semantics for any downstream consumer that
|
|
899
|
+
# wants to read it (e.g. a plugin's `prepare(services)` hook in v0.1.x). Truthy edge:
|
|
900
|
+
# positive polarity. Falsey edge: negative polarity. Mirrors the equality-predicate fact
|
|
901
|
+
# pattern already used by `analyse_equality_predicate`.
|
|
1011
902
|
STRING_PREDICATE_NAMES = %i[start_with? end_with? include?].freeze
|
|
1012
903
|
private_constant :STRING_PREDICATE_NAMES
|
|
1013
904
|
|
|
@@ -1080,21 +971,17 @@ module Rigor
|
|
|
1080
971
|
end
|
|
1081
972
|
end
|
|
1082
973
|
|
|
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.
|
|
974
|
+
# T3 (template-corpora survey) — `recv.respond_to?(sym)` truthy edge narrows `recv`
|
|
975
|
+
# non-nil. `nil.respond_to?(m)` is `false` for every method `m` that `NilClass` does not
|
|
976
|
+
# define, so a truthy `respond_to?` proves the receiver was not `nil` UNLESS the queried
|
|
977
|
+
# symbol is one of `NilClass`'s own methods (`:to_s`, `:inspect`, `:nil?`, …) — `nil`
|
|
978
|
+
# DOES respond to those, so the truthy edge admits a nil receiver and we narrow nothing.
|
|
1090
979
|
#
|
|
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.
|
|
980
|
+
# Conservative floor: narrow only on a literal `Symbol`/`String` argument resolved
|
|
981
|
+
# against the RBS environment; a non-literal symbol, a missing argument, or a symbol
|
|
982
|
+
# that IS in `NilClass`'s method set declines. The falsey edge is always the no-op
|
|
983
|
+
# ("does not respond" proves little about the receiver's type). Narrowing-only: it
|
|
984
|
+
# removes the `nil` constituent and never promotes a non-nil type.
|
|
1098
985
|
def analyse_respond_to_predicate(node, scope)
|
|
1099
986
|
return nil if node.block
|
|
1100
987
|
return nil if node.arguments.nil? || node.arguments.arguments.size != 1
|
|
@@ -1115,12 +1002,11 @@ module Rigor
|
|
|
1115
1002
|
[scope.public_send(writer, node.receiver.name, non_nil), scope]
|
|
1116
1003
|
end
|
|
1117
1004
|
|
|
1118
|
-
# True when `nil` responds to `sym` — i.e. `NilClass` (own,
|
|
1119
|
-
#
|
|
1120
|
-
#
|
|
1121
|
-
#
|
|
1122
|
-
#
|
|
1123
|
-
# non-nil narrowing.
|
|
1005
|
+
# True when `nil` responds to `sym` — i.e. `NilClass` (own, inherited
|
|
1006
|
+
# Kernel/BasicObject) defines an instance method named `sym`. Resolved against the RBS
|
|
1007
|
+
# environment; when the lookup is unavailable the answer is conservatively `true`
|
|
1008
|
+
# (decline to narrow) so an unknown environment never manufactures a false non-nil
|
|
1009
|
+
# narrowing.
|
|
1124
1010
|
def nilclass_method?(sym, scope)
|
|
1125
1011
|
name = sym.respond_to?(:to_sym) ? sym.to_sym : sym
|
|
1126
1012
|
return true unless name.is_a?(Symbol)
|
|
@@ -1132,23 +1018,20 @@ module Rigor
|
|
|
1132
1018
|
true
|
|
1133
1019
|
end
|
|
1134
1020
|
|
|
1135
|
-
# ADR-47 §4-4 (Elixir `tuple_size`/non-empty analogue) — a bare
|
|
1136
|
-
# `arr.
|
|
1137
|
-
#
|
|
1138
|
-
# edge that implies "at least one element":
|
|
1021
|
+
# ADR-47 §4-4 (Elixir `tuple_size`/non-empty analogue) — a bare `arr.empty?` /
|
|
1022
|
+
# `arr.any?` / `arr.none?` (no block, no args) narrows an Array-typed receiver to
|
|
1023
|
+
# `non-empty-array[T]` on the edge that implies "at least one element":
|
|
1139
1024
|
#
|
|
1140
1025
|
# - `empty?` → false edge (the array is NOT empty)
|
|
1141
1026
|
# - `any?` → true edge (a truthy element exists ⇒ non-empty)
|
|
1142
1027
|
# - `none?` → false edge (a truthy element exists ⇒ non-empty)
|
|
1143
1028
|
#
|
|
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.
|
|
1029
|
+
# The opposite edge is the conservative no-op (`any?`/`none?` falseness does not imply
|
|
1030
|
+
# emptiness — the array may hold only falsey elements; `empty?` truth could narrow to an
|
|
1031
|
+
# empty array but that carrier move is deferred). Only `Nominal[Array, [T]]` receivers
|
|
1032
|
+
# narrow — `Tuple` is already known-length, `Dynamic` is left alone (gradual
|
|
1033
|
+
# guarantee), and a non-Array receiver (`String#empty?`, `Range#any?`, …) bails so the
|
|
1034
|
+
# existing string / predicate paths still run.
|
|
1152
1035
|
def analyse_array_emptiness_predicate(node, scope, name)
|
|
1153
1036
|
return nil if node.block
|
|
1154
1037
|
return nil unless node.arguments.nil? || node.arguments.arguments.empty?
|
|
@@ -1174,9 +1057,9 @@ module Rigor
|
|
|
1174
1057
|
end
|
|
1175
1058
|
end
|
|
1176
1059
|
|
|
1177
|
-
# Refines `Array[T]` (and every Array member of a Union) to
|
|
1178
|
-
#
|
|
1179
|
-
#
|
|
1060
|
+
# Refines `Array[T]` (and every Array member of a Union) to `non-empty-array[T]`;
|
|
1061
|
+
# returns the input unchanged when nothing applies, so the caller can detect "no
|
|
1062
|
+
# narrowing".
|
|
1180
1063
|
def narrow_to_non_empty_array(type)
|
|
1181
1064
|
case type
|
|
1182
1065
|
when Type::Nominal
|
|
@@ -1190,18 +1073,16 @@ module Rigor
|
|
|
1190
1073
|
end
|
|
1191
1074
|
end
|
|
1192
1075
|
|
|
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.
|
|
1076
|
+
# ADR-47 §4-3 (Elixir `is_map_key/2` analogue) — `h.key?(:foo)` narrows the receiver on
|
|
1077
|
+
# the truthy edge: a `HashShape` whose `:foo` is an OPTIONAL key has it promoted to
|
|
1078
|
+
# REQUIRED, so a subsequent `h[:foo]` reads the declared value type instead of `value |
|
|
1079
|
+
# nil` (the optionality nil is gone). Sound because key presence removes only the
|
|
1080
|
+
# optionality-injected nil, never the value's own intrinsic nil (`h = {foo: nil}` keeps
|
|
1081
|
+
# `h[:foo]` nil-typed). The falsey edge is left unchanged — "key absent" is the
|
|
1082
|
+
# conservative no-op. Only literal `Symbol`/`String` arguments and
|
|
1083
|
+
# `LocalVariableReadNode`/`InstanceVariableReadNode` receivers narrow; everything else
|
|
1084
|
+
# (Dynamic, `Nominal[Hash]`, method-chain receivers, dynamic keys) bails to no
|
|
1085
|
+
# narrowing.
|
|
1205
1086
|
def analyse_key_presence_predicate(node, scope)
|
|
1206
1087
|
return nil if node.arguments.nil?
|
|
1207
1088
|
return nil unless node.arguments.arguments.size == 1
|
|
@@ -1222,10 +1103,9 @@ module Rigor
|
|
|
1222
1103
|
return nil if current.nil?
|
|
1223
1104
|
|
|
1224
1105
|
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.
|
|
1106
|
+
# §4-3 false edge — `h.key?(:foo)` being false proves `:foo` is absent, so on that
|
|
1107
|
+
# edge `h[:foo]` reads `nil`. Remove the (optional) key from the shape; a required
|
|
1108
|
+
# key makes the false edge dead and an unknown key is already nil, both no-ops.
|
|
1229
1109
|
falsey = narrow_hash_key_absent(current, key)
|
|
1230
1110
|
return nil if truthy.equal?(current) && falsey.equal?(current) # predicate is opaque
|
|
1231
1111
|
|
|
@@ -1241,9 +1121,9 @@ module Rigor
|
|
|
1241
1121
|
end
|
|
1242
1122
|
end
|
|
1243
1123
|
|
|
1244
|
-
# Promotes `key` from optional to required across a HashShape (or
|
|
1245
|
-
#
|
|
1246
|
-
#
|
|
1124
|
+
# Promotes `key` from optional to required across a HashShape (or every HashShape
|
|
1125
|
+
# member of a Union). Returns the input unchanged when nothing applies, so the caller
|
|
1126
|
+
# can detect "no narrowing".
|
|
1247
1127
|
def narrow_hash_key_present(type, key)
|
|
1248
1128
|
case type
|
|
1249
1129
|
when Type::HashShape
|
|
@@ -1268,12 +1148,11 @@ module Rigor
|
|
|
1268
1148
|
)
|
|
1269
1149
|
end
|
|
1270
1150
|
|
|
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`.
|
|
1151
|
+
# §4-3 false edge — drops `key` from a HashShape (or every HashShape member of a Union)
|
|
1152
|
+
# so the proven-absent key reads `nil`. Returns the input unchanged when nothing
|
|
1153
|
+
# applies (caller detects "no narrowing"). Only an *optional* present key is removed: a
|
|
1154
|
+
# required key makes `key?` always true (the false edge is dead, leave the shape opaque)
|
|
1155
|
+
# and a key absent from `pairs` already reads `nil`.
|
|
1277
1156
|
def narrow_hash_key_absent(type, key)
|
|
1278
1157
|
case type
|
|
1279
1158
|
when Type::HashShape
|
|
@@ -1298,27 +1177,21 @@ module Rigor
|
|
|
1298
1177
|
)
|
|
1299
1178
|
end
|
|
1300
1179
|
|
|
1301
|
-
# Survey item (b): `/regex/ =~ str` and `str =~ /regex/`
|
|
1302
|
-
#
|
|
1180
|
+
# Survey item (b): `/regex/ =~ str` and `str =~ /regex/` bind the regex match-data
|
|
1181
|
+
# globals on each edge.
|
|
1303
1182
|
#
|
|
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>`.
|
|
1183
|
+
# - Truthy edge (`=~` returned an Integer position — the match succeeded): `$~` to
|
|
1184
|
+
# `Nominal[MatchData]`; `$&` and `$1..$N` (where N is the number of capture groups in
|
|
1185
|
+
# the regex source) to `Nominal[String]`. This is the same optimistic-narrowing shape
|
|
1186
|
+
# the existing `analyse_match_write` uses for named captures inside `if /(?<x>...)/
|
|
1187
|
+
# =~ str` — optional groups in the regex source (`(\d+)?`) would bind `$N` to `nil`
|
|
1188
|
+
# at runtime, but the floor here matches the common idiom (required captures) and
|
|
1189
|
+
# lets `unless /(\d+)/ =~ s; raise; end; $1.to_i` resolve cleanly.
|
|
1190
|
+
# - Falsey edge (`=~` returned nil — no match): `$~` and every numbered / back-reference
|
|
1191
|
+
# global bound to `Constant<nil>`.
|
|
1318
1192
|
#
|
|
1319
|
-
# Returns nil (no narrowing) when the receiver / argument
|
|
1320
|
-
#
|
|
1321
|
-
# we can count.
|
|
1193
|
+
# Returns nil (no narrowing) when the receiver / argument pair does not include a
|
|
1194
|
+
# `RegularExpressionNode` literal we can count.
|
|
1322
1195
|
def analyse_regex_match_predicate(node, scope)
|
|
1323
1196
|
return nil if node.arguments.nil?
|
|
1324
1197
|
return nil unless node.arguments.arguments.size == 1
|
|
@@ -1343,14 +1216,12 @@ module Rigor
|
|
|
1343
1216
|
REGEX_MATCH_GLOBALS = %i[$~ $& $` $' $+].freeze
|
|
1344
1217
|
private_constant :REGEX_MATCH_GLOBALS
|
|
1345
1218
|
|
|
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.
|
|
1219
|
+
# `unconditional` is the Set of 1-based numbered-capture indices whose group is
|
|
1220
|
+
# guaranteed to participate in any successful match (no optional quantifier on the
|
|
1221
|
+
# group or an ancestor, no alternation in the pattern). Those `$N` are bound to
|
|
1222
|
+
# `String`; every other numbered group present in the pattern stays `String | nil` on
|
|
1223
|
+
# both edges (a truthy match leaves an optional group nil at runtime), so we do not
|
|
1224
|
+
# narrow it on the truthy edge.
|
|
1354
1225
|
def regex_match_predicate_scopes(scope, unconditional)
|
|
1355
1226
|
string_t = Type::Combinator.nominal_of("String")
|
|
1356
1227
|
match_data_t = Type::Combinator.nominal_of("MatchData")
|
|
@@ -1374,31 +1245,24 @@ module Rigor
|
|
|
1374
1245
|
[truthy, falsey]
|
|
1375
1246
|
end
|
|
1376
1247
|
|
|
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.
|
|
1248
|
+
# Returns the Set of 1-based numbered-capture indices that are UNCONDITIONAL in
|
|
1249
|
+
# `source`: present on every successful match because no optional quantifier (`?`,
|
|
1250
|
+
# `*`, `{0,…}`) applies to the group or any ancestor group, and the pattern contains no
|
|
1251
|
+
# alternation (`|`). Optional and alternation-reachable groups are excluded — at
|
|
1252
|
+
# runtime `$N` is `nil` for them even when the overall match succeeds, so narrowing
|
|
1253
|
+
# them to non-nil `String` would be unsound. The walker is intentionally light (char
|
|
1254
|
+
# scan, not a regex-AST parse): backslash escapes are skipped; `(?:…)`, lookahead
|
|
1255
|
+
# `(?=…)`/`(?!…)`, and lookbehind `(?<=…)`/`(?<!…)` do not capture; named groups
|
|
1256
|
+
# `(?<name>…)` do. Conservatism is one-directional — when in doubt a group is treated
|
|
1257
|
+
# as conditional (dropped from the Set), never the reverse.
|
|
1391
1258
|
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
|
|
1259
|
+
# `unconditional` collects every capturing index; a group is later removed (with its
|
|
1260
|
+
# whole subtree) when it is optionally quantified, nested under an optional ancestor,
|
|
1261
|
+
# or sits in an alternation branch. `stack` holds one frame per open group (plus a
|
|
1262
|
+
# virtual root frame for the top level) as `[group_index_or_nil, descendant_indices,
|
|
1263
|
+
# alternated?]`. A `|` marks the CURRENT group's frame alternated — its branches are
|
|
1264
|
+
# mutually exclusive, so its descendant captures may be absent on a successful match;
|
|
1265
|
+
# the group itself still participates. Closing a frame rolls its subtree up to the
|
|
1402
1266
|
# parent so an optional / alternated ancestor disqualifies it.
|
|
1403
1267
|
state = { unconditional: Set.new, stack: [[nil, [], false]], group_index: 0 }
|
|
1404
1268
|
pos = 0
|
|
@@ -1422,8 +1286,8 @@ module Rigor
|
|
|
1422
1286
|
end
|
|
1423
1287
|
|
|
1424
1288
|
# Updates the walk `state` at a group-relevant char during
|
|
1425
|
-
# {#unconditional_capture_groups}: `(` pushes a frame, `)` pops
|
|
1426
|
-
#
|
|
1289
|
+
# {#unconditional_capture_groups}: `(` pushes a frame, `)` pops and resolves it, `|`
|
|
1290
|
+
# flags the current frame as alternated.
|
|
1427
1291
|
def scan_group_char(source, pos, chr, state)
|
|
1428
1292
|
case chr
|
|
1429
1293
|
when "("
|
|
@@ -1440,11 +1304,10 @@ module Rigor
|
|
|
1440
1304
|
end
|
|
1441
1305
|
end
|
|
1442
1306
|
|
|
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).
|
|
1307
|
+
# Resolves a closed (or virtual-root) group frame: its subtree is its own index plus
|
|
1308
|
+
# every descendant index. The subtree is disqualified when the group is optionally
|
|
1309
|
+
# quantified or its branches are alternated (only the descendants in that case — but a
|
|
1310
|
+
# self subtree always keeps its own index unless optional).
|
|
1448
1311
|
def finalize_frame(state, frame, optional:)
|
|
1449
1312
|
idx, descendants, alternated = frame
|
|
1450
1313
|
subtree = descendants.dup
|
|
@@ -1456,11 +1319,10 @@ module Rigor
|
|
|
1456
1319
|
state[:stack].last && state[:stack].last[1].concat(subtree)
|
|
1457
1320
|
end
|
|
1458
1321
|
|
|
1459
|
-
# True when the group whose closing paren is at `source[pos]`
|
|
1460
|
-
#
|
|
1461
|
-
#
|
|
1462
|
-
#
|
|
1463
|
-
# zero-permitting on the base quantifier.
|
|
1322
|
+
# True when the group whose closing paren is at `source[pos]` is followed by a
|
|
1323
|
+
# quantifier that permits zero repetitions (`?`, `*`, `{0…}`). `+` and `{1,…}` do NOT
|
|
1324
|
+
# make a group optional. A lazy/possessive suffix (`*?`, `*+`) is still zero-permitting
|
|
1325
|
+
# on the base quantifier.
|
|
1464
1326
|
def next_quantifier_optional?(source, pos)
|
|
1465
1327
|
case source[pos]
|
|
1466
1328
|
when "?", "*" then true
|
|
@@ -1499,9 +1361,8 @@ module Rigor
|
|
|
1499
1361
|
end
|
|
1500
1362
|
end
|
|
1501
1363
|
|
|
1502
|
-
# `:positive?` / `:negative?` / `:zero?` / `:nonzero?` are
|
|
1503
|
-
#
|
|
1504
|
-
# comparisons against the literal 0 so the existing range
|
|
1364
|
+
# `:positive?` / `:negative?` / `:zero?` / `:nonzero?` are zero-arg predicates on
|
|
1365
|
+
# `Numeric`. We model them as comparisons against the literal 0 so the existing range
|
|
1505
1366
|
# narrowing handles them uniformly.
|
|
1506
1367
|
ZERO_CLASS_PREDICATE_RULES = Ractor.make_shareable({
|
|
1507
1368
|
positive?: { truthy: [:>, 0], falsey: [:<=, 0] },
|
|
@@ -1534,11 +1395,9 @@ module Rigor
|
|
|
1534
1395
|
end
|
|
1535
1396
|
end
|
|
1536
1397
|
|
|
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
|
|
1398
|
+
# `x.between?(a, b)` truthy edge narrows to `narrow_integer_comparison(>=, a)` ∩
|
|
1399
|
+
# `narrow_integer_comparison(<=, b)`. The falsey edge is left unchanged because the
|
|
1400
|
+
# complement is a two-piece domain (`x < a || x > b`) that the lattice cannot express
|
|
1542
1401
|
# precisely. `a` and `b` MUST both be integer literals.
|
|
1543
1402
|
def analyse_between_predicate(node, scope)
|
|
1544
1403
|
return nil unless node.receiver.is_a?(Prism::LocalVariableReadNode)
|
|
@@ -1559,9 +1418,8 @@ module Rigor
|
|
|
1559
1418
|
[scope.with_local(local_name, truthy), scope]
|
|
1560
1419
|
end
|
|
1561
1420
|
|
|
1562
|
-
# Helper for {.narrow_integer_not_equal}. Only adjusts when the
|
|
1563
|
-
#
|
|
1564
|
-
# contiguous; otherwise the input range is preserved.
|
|
1421
|
+
# Helper for {.narrow_integer_not_equal}. Only adjusts when the value sits exactly on
|
|
1422
|
+
# one endpoint, so the result stays contiguous; otherwise the input range is preserved.
|
|
1565
1423
|
def narrow_integer_range_not_equal(range, value)
|
|
1566
1424
|
return range if range.lower > value || range.upper < value
|
|
1567
1425
|
return Type::Combinator.bot if single_point_range_equal?(range, value)
|
|
@@ -1607,9 +1465,8 @@ module Rigor
|
|
|
1607
1465
|
# Comparison predicate analyser. Recognised shapes:
|
|
1608
1466
|
# x < Int x <= Int x > Int x >= Int
|
|
1609
1467
|
# 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.
|
|
1468
|
+
# The reversed (literal-on-left) form is normalised by transposing the operator so the
|
|
1469
|
+
# receiver-local always appears on the left of the rule.
|
|
1613
1470
|
INVERT_COMPARISON_OP = { :< => :>=, :<= => :>, :> => :<=, :>= => :< }.freeze
|
|
1614
1471
|
REVERSE_COMPARISON_OP = { :< => :>, :<= => :>=, :> => :<, :>= => :<= }.freeze
|
|
1615
1472
|
private_constant :INVERT_COMPARISON_OP, :REVERSE_COMPARISON_OP
|
|
@@ -1778,12 +1635,10 @@ module Rigor
|
|
|
1778
1635
|
narrowed == original ? :relational : :local_binding
|
|
1779
1636
|
end
|
|
1780
1637
|
|
|
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".
|
|
1638
|
+
# `recv.is_a?(C)` / `recv.kind_of?(C)` / `recv.instance_of?(C)` narrowing. The receiver
|
|
1639
|
+
# MUST be a `LocalVariableReadNode` (so we have a name to rebind), and the argument
|
|
1640
|
+
# MUST be a single static constant reference (`Foo` or `Foo::Bar`) we can resolve to a
|
|
1641
|
+
# qualified class name. Anything else falls through to "no narrowing".
|
|
1787
1642
|
def analyse_class_predicate(node, scope, exact:)
|
|
1788
1643
|
return nil if node.arguments.nil?
|
|
1789
1644
|
return nil unless node.arguments.arguments.size == 1
|
|
@@ -1791,16 +1646,12 @@ module Rigor
|
|
|
1791
1646
|
bare_name = static_class_name(node.arguments.arguments.first)
|
|
1792
1647
|
return nil if bare_name.nil?
|
|
1793
1648
|
|
|
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).
|
|
1649
|
+
# Resolve `bare_name` through the lexical-scope chain so a name shadowed by the
|
|
1650
|
+
# current class / enclosing module wins over the top-level constant. Mirrors Ruby's
|
|
1651
|
+
# `Module.nesting`-driven constant lookup. The canonical motivating case: inside
|
|
1652
|
+
# `Rigor::Type::Singleton#==`, `is_a?(Singleton)` should resolve to
|
|
1653
|
+
# `Rigor::Type::Singleton`, not the top-level stdlib `Singleton` mixin (which would
|
|
1654
|
+
# surface as a spurious `undefined-method` on subsequent `other.class_name` calls).
|
|
1804
1655
|
class_name = resolve_class_name_lexically(bare_name, scope)
|
|
1805
1656
|
|
|
1806
1657
|
case node.receiver
|
|
@@ -1818,27 +1669,20 @@ module Rigor
|
|
|
1818
1669
|
class_predicate_scopes(scope, node.receiver.name, current, class_name, exact: exact)
|
|
1819
1670
|
end
|
|
1820
1671
|
|
|
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
|
|
1672
|
+
# Stable single-hop method-chain narrowing (ROADMAP § Future cycles — "Method-call
|
|
1673
|
+
# receiver narrowing across stable receivers"). When the predicate's receiver is
|
|
1674
|
+
# `<local/ivar>.<method>` with no args and no block, record the truthy / falsey
|
|
1675
|
+
# narrowing in `Scope#method_chain_narrowings` keyed on the chain address. The
|
|
1676
|
+
# dominated body's identical chain reads then observe the narrowed type through
|
|
1829
1677
|
# `ExpressionTyper#call_type_for`'s lookup.
|
|
1830
1678
|
#
|
|
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.
|
|
1679
|
+
# Heuristic-by-design (ROADMAP § "Soundness gap"): a second call to `x.last` could in
|
|
1680
|
+
# principle return a different value than the first. The chain is dropped on (1)
|
|
1681
|
+
# receiver variable rebind (handled inside `Scope#with_local` / `#with_ivar`), and (2)
|
|
1682
|
+
# any intervening call against the same root receiver (handled by
|
|
1683
|
+
# `StatementEvaluator#eval_call`'s invalidation step). The Law of Demeter justifies the
|
|
1684
|
+
# single-hop restriction: a single-hop chain is the idiomatic Ruby shape where
|
|
1685
|
+
# re-evaluation soundness is the strongest.
|
|
1842
1686
|
def analyse_class_predicate_on_chain(node, scope, class_name, exact)
|
|
1843
1687
|
address = stable_chain_address(node.receiver)
|
|
1844
1688
|
return nil if address.nil?
|
|
@@ -1855,12 +1699,10 @@ module Rigor
|
|
|
1855
1699
|
]
|
|
1856
1700
|
end
|
|
1857
1701
|
|
|
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
|
|
1702
|
+
# Returns `[receiver_kind, receiver_name, method_name]` iff `chain_call` is a stable
|
|
1703
|
+
# single-hop chain whose root is a local / ivar read and whose own call shape has no
|
|
1704
|
+
# positional arguments and no block. Other shapes (multi-hop, args, block,
|
|
1705
|
+
# method-defined-on-arbitrary-receiver) lose stability for one of the reasons
|
|
1864
1706
|
# enumerated in the slice's design notes.
|
|
1865
1707
|
def stable_chain_address(chain_call)
|
|
1866
1708
|
return nil unless chain_call.is_a?(Prism::CallNode)
|
|
@@ -1877,13 +1719,10 @@ module Rigor
|
|
|
1877
1719
|
end
|
|
1878
1720
|
end
|
|
1879
1721
|
|
|
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.
|
|
1722
|
+
# Walks the lexical-nesting chain derived from `scope.self_type` and returns the first
|
|
1723
|
+
# `<prefix>::<bare_name>` (or bare `<bare_name>` at the top level) that the environment
|
|
1724
|
+
# recognises. Falls back to `bare_name` itself when nothing in the chain resolves; the
|
|
1725
|
+
# downstream `narrow_class` then yields the conservative answer for unknown receivers.
|
|
1887
1726
|
def resolve_class_name_lexically(bare_name, scope)
|
|
1888
1727
|
return bare_name if bare_name.include?("::") # Already qualified.
|
|
1889
1728
|
|
|
@@ -1895,9 +1734,8 @@ module Rigor
|
|
|
1895
1734
|
bare_name
|
|
1896
1735
|
end
|
|
1897
1736
|
|
|
1898
|
-
# Combines the environment's RBS-known set with the
|
|
1899
|
-
#
|
|
1900
|
-
# lexical-nesting candidate matches a class the project
|
|
1737
|
+
# Combines the environment's RBS-known set with the scope's in-source
|
|
1738
|
+
# `discovered_classes` table so a lexical-nesting candidate matches a class the project
|
|
1901
1739
|
# declares but has no RBS for.
|
|
1902
1740
|
def class_known_to_scope?(scope, candidate)
|
|
1903
1741
|
return true if scope.environment.class_known?(candidate)
|
|
@@ -1905,14 +1743,11 @@ module Rigor
|
|
|
1905
1743
|
scope.discovered_classes.key?(candidate)
|
|
1906
1744
|
end
|
|
1907
1745
|
|
|
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.
|
|
1746
|
+
# Approximates `Module.nesting` from the inferable `self_type`. Today's implementation
|
|
1747
|
+
# handles the common case: when the surrounding method is a regular instance method
|
|
1748
|
+
# (`self_type = Nominal[T]`) or a class-body / singleton (`self_type = Singleton[T]`),
|
|
1749
|
+
# the chain is `T`'s namespace path — `Foo::Bar::Baz` → `["Foo::Bar::Baz", "Foo::Bar",
|
|
1750
|
+
# "Foo"]`. Returns an empty array when `self_type` is unknown.
|
|
1916
1751
|
def lexical_nesting_for(scope)
|
|
1917
1752
|
self_type = scope.self_type
|
|
1918
1753
|
base = case self_type
|
|
@@ -1937,33 +1772,24 @@ module Rigor
|
|
|
1937
1772
|
]
|
|
1938
1773
|
end
|
|
1939
1774
|
|
|
1940
|
-
# Slice 7 phase 4 — `===`-narrowing. The case-equality
|
|
1941
|
-
#
|
|
1942
|
-
#
|
|
1943
|
-
# shapes produce sound narrowing rules:
|
|
1775
|
+
# Slice 7 phase 4 — `===`-narrowing. The case-equality predicate `<receiver> === local`
|
|
1776
|
+
# is the operator that backs Ruby's `case`/`when` dispatch. Three receiver shapes
|
|
1777
|
+
# produce sound narrowing rules:
|
|
1944
1778
|
#
|
|
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
|
|
1779
|
+
# - **Class / Module receiver**: `Foo === x` is isomorphic to `x.is_a?(Foo)` (the
|
|
1780
|
+
# default behaviour of `Module#===`). Reuse `class_predicate_scopes` so the truthy
|
|
1781
|
+
# edge narrows down to `Foo` and the falsey edge subtracts `Foo` from a union.
|
|
1782
|
+
# - **Range literal receiver** (`(1..10) === x`): the default `Range#===` includes `x`
|
|
1783
|
+
# iff the endpoints compare it. We conservatively narrow `x` to `Numeric` for
|
|
1784
|
+
# integer-endpoint ranges and to `String` for string-endpoint ranges; other endpoint
|
|
1955
1785
|
# 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.
|
|
1786
|
+
# - **Regexp literal receiver** (`/foo/ === x`): the match operator coerces `x` to a
|
|
1787
|
+
# String, so the truthy edge narrows `x` to `String`. The falsey edge keeps the entry
|
|
1788
|
+
# type unchanged because `Regexp#===` returns false for non-Strings AND for Strings
|
|
1789
|
+
# that simply do not match.
|
|
1962
1790
|
#
|
|
1963
|
-
# Anything else — non-local LHS argument, dynamic
|
|
1964
|
-
#
|
|
1965
|
-
# the no-narrowing branch (nil), preserving the
|
|
1966
|
-
# entry scope on both edges.
|
|
1791
|
+
# Anything else — non-local LHS argument, dynamic receiver, custom `===` method — falls
|
|
1792
|
+
# through to the no-narrowing branch (nil), preserving the entry scope on both edges.
|
|
1967
1793
|
def analyse_case_equality_predicate(node, scope)
|
|
1968
1794
|
return nil if node.arguments.nil?
|
|
1969
1795
|
return nil unless node.arguments.arguments.size == 1
|
|
@@ -1980,14 +1806,12 @@ module Rigor
|
|
|
1980
1806
|
end
|
|
1981
1807
|
end
|
|
1982
1808
|
|
|
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.
|
|
1809
|
+
# `Class === <local/ivar>.<method>` — the case-equality counterpart of
|
|
1810
|
+
# {.analyse_class_predicate_on_chain}. The `open3` idiom `if Hash === cmd.last` narrows
|
|
1811
|
+
# `cmd.last` to the class inside the branch, recorded as a single-hop method-chain
|
|
1812
|
+
# narrowing keyed on the chain address (same stability rules as `is_a?` on a chain).
|
|
1813
|
+
# Only static class/module receivers narrow here — the Range / Regexp literal receivers
|
|
1814
|
+
# (`(1..10) === x.foo`) are not a common method-chain shape and stay deferred.
|
|
1991
1815
|
def analyse_case_equality_on_chain(receiver, chain_arg, scope)
|
|
1992
1816
|
class_name = static_class_name(receiver)
|
|
1993
1817
|
return nil if class_name.nil?
|
|
@@ -2021,14 +1845,11 @@ module Rigor
|
|
|
2021
1845
|
]
|
|
2022
1846
|
end
|
|
2023
1847
|
|
|
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.
|
|
1848
|
+
# Maps a case-equality literal receiver to the class whose membership is implied by the
|
|
1849
|
+
# truthy edge. `Prism::ParenthesesNode` wrappers are transparently unwrapped (`(1..10)
|
|
1850
|
+
# === x` is parsed with the range inside parentheses). Range literals: integer
|
|
1851
|
+
# endpoints → `Numeric`; string endpoints → `String`. Regexp literals → `String`. Other
|
|
1852
|
+
# shapes return nil so the caller falls through.
|
|
2032
1853
|
def case_equality_target_class(receiver)
|
|
2033
1854
|
receiver = unwrap_parens(receiver)
|
|
2034
1855
|
case receiver
|
|
@@ -2045,34 +1866,24 @@ module Rigor
|
|
|
2045
1866
|
node
|
|
2046
1867
|
end
|
|
2047
1868
|
|
|
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.
|
|
1869
|
+
# Slice 4b-1 (ADR-7 § "Slice 4-A/4-B") — single-point RBS::Extended contribution
|
|
1870
|
+
# analyser. Replaces the earlier sibling pair (`analyse_rbs_extended_predicate` +
|
|
1871
|
+
# `analyse_rbs_extended_assert_if`) with one path that routes through
|
|
1872
|
+
# `RbsExtended.read_flow_contribution` and `Rigor::FlowContribution::Merger.merge`. The
|
|
1873
|
+
# bundle's `truthy_facts` slot already includes both the `predicate-if-true` and
|
|
1874
|
+
# `assert-if-true` Facts (slice 4a routing closed the v0.0.9 imperfection); the
|
|
1875
|
+
# `falsey_facts` slot mirrors that. The merger composes any future plugin contribution
|
|
1876
|
+
# at the same site alongside the RBS::Extended bundle without changing this analyser.
|
|
2061
1877
|
#
|
|
2062
|
-
# Conservative envelope (carried over from the previous
|
|
2063
|
-
#
|
|
2064
|
-
# - Receiver type must be `Type::Nominal`,
|
|
2065
|
-
# `Type::Singleton`, or `Type::Constant`.
|
|
1878
|
+
# Conservative envelope (carried over from the previous implementation):
|
|
1879
|
+
# - Receiver type must be `Type::Nominal`, `Type::Singleton`, or `Type::Constant`.
|
|
2066
1880
|
# - 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.
|
|
1881
|
+
# - For each fact, the corresponding positional argument (matched by parameter name in
|
|
1882
|
+
# the selected overload) MUST be a `Prism::LocalVariableReadNode` for narrowing to
|
|
1883
|
+
# apply.
|
|
1884
|
+
# - When the target is `self`, narrowing applies to the receiver — but the engine does
|
|
1885
|
+
# not yet narrow `self` itself, so `self`-targeted facts are accepted by the merger
|
|
1886
|
+
# but currently produce no scope edits.
|
|
2076
1887
|
def analyse_rbs_extended_contribution(node, scope)
|
|
2077
1888
|
method_def = resolve_rbs_extended_method(node, scope)
|
|
2078
1889
|
return nil if method_def.nil?
|
|
@@ -2113,10 +1924,9 @@ module Rigor
|
|
|
2113
1924
|
target_scope.with_local(local_name, narrowed)
|
|
2114
1925
|
end
|
|
2115
1926
|
|
|
2116
|
-
# v0.1.1 Track 1 slice 3 — `target_kind == :self` facts
|
|
2117
|
-
#
|
|
2118
|
-
#
|
|
2119
|
-
# the call's receiver. Four receiver shapes participate:
|
|
1927
|
+
# v0.1.1 Track 1 slice 3 — `target_kind == :self` facts (`predicate-if-true self is T`,
|
|
1928
|
+
# `predicate-if-false self is T`) narrow whichever scope binding is bound to the call's
|
|
1929
|
+
# receiver. Four receiver shapes participate:
|
|
2120
1930
|
#
|
|
2121
1931
|
# `recv.method?` where recv is...
|
|
2122
1932
|
# - `LocalVariableReadNode` -> narrow that local
|
|
@@ -2125,8 +1935,8 @@ module Rigor
|
|
|
2125
1935
|
# - nil (implicit self call inside a method body)
|
|
2126
1936
|
# -> narrow `self_type`
|
|
2127
1937
|
#
|
|
2128
|
-
# Other receiver shapes (method chains, expressions) have
|
|
2129
|
-
#
|
|
1938
|
+
# Other receiver shapes (method chains, expressions) have no scope binding to narrow
|
|
1939
|
+
# against and fall through.
|
|
2130
1940
|
def apply_self_fact(fact, receiver_node, entry_scope, target_scope)
|
|
2131
1941
|
case receiver_node
|
|
2132
1942
|
when nil, Prism::SelfNode
|
|
@@ -2152,11 +1962,9 @@ module Rigor
|
|
|
2152
1962
|
end
|
|
2153
1963
|
end
|
|
2154
1964
|
|
|
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.
|
|
1965
|
+
# Resolves a Fact's target node. Mirrors the earlier `effect_target_node` helper but
|
|
1966
|
+
# reads from the canonical Fact carrier; `:self` routes to the call receiver, otherwise
|
|
1967
|
+
# we look up the matching positional argument by parameter name.
|
|
2160
1968
|
def fact_target_node(fact, call_node, method_def)
|
|
2161
1969
|
if fact.target_kind == :self
|
|
2162
1970
|
call_node.receiver
|
|
@@ -2182,9 +1990,8 @@ module Rigor
|
|
|
2182
1990
|
nil
|
|
2183
1991
|
end
|
|
2184
1992
|
|
|
2185
|
-
# Implicit self call (`admin?` with no receiver inside an
|
|
2186
|
-
#
|
|
2187
|
-
# `scope.self_type` per v0.1.1 Track 1 slice 3.
|
|
1993
|
+
# Implicit self call (`admin?` with no receiver inside an instance method body) — read
|
|
1994
|
+
# the method definition from `scope.self_type` per v0.1.1 Track 1 slice 3.
|
|
2188
1995
|
def receiver_type_for_resolve(node, scope)
|
|
2189
1996
|
node.receiver.nil? ? scope.self_type : scope.type_of(node.receiver)
|
|
2190
1997
|
end
|
|
@@ -2209,11 +2016,9 @@ module Rigor
|
|
|
2209
2016
|
nil
|
|
2210
2017
|
end
|
|
2211
2018
|
|
|
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.
|
|
2019
|
+
# Maps the effect's target parameter name to the call site argument by inspecting the
|
|
2020
|
+
# selected overload's required-positional parameter list. Returns the Prism arg node at
|
|
2021
|
+
# that position, or nil when the overload shape does not allow a precise match.
|
|
2217
2022
|
def lookup_positional_arg(call_node, method_def, target_name)
|
|
2218
2023
|
arguments = call_node.arguments&.arguments || []
|
|
2219
2024
|
method_def.method_types.each do |mt|
|
|
@@ -2224,18 +2029,13 @@ module Rigor
|
|
|
2224
2029
|
nil
|
|
2225
2030
|
end
|
|
2226
2031
|
|
|
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).
|
|
2032
|
+
# Slice 7 phase 5 — case/when accumulator. Walks each `when` condition, computes the
|
|
2033
|
+
# narrowed type for the subject as if `condition === subject`, and accumulates them.
|
|
2034
|
+
# The body's narrowed type is the union across all conditions; the falsey type is the
|
|
2035
|
+
# running result after subtracting every condition's class. Conditions whose shape we
|
|
2036
|
+
# cannot statically classify are treated as "no narrowing": the body falls back to the
|
|
2037
|
+
# union of what we did learn (or the entry type when nothing learned), and the falsey
|
|
2038
|
+
# edge is the entry type (because we cannot prove the unknown condition didn't match).
|
|
2239
2039
|
def accumulate_case_when_scopes(scope, local_name, current, conditions)
|
|
2240
2040
|
truthy_members = []
|
|
2241
2041
|
falsey_type = current
|
|
@@ -2259,9 +2059,9 @@ module Rigor
|
|
|
2259
2059
|
]
|
|
2260
2060
|
end
|
|
2261
2061
|
|
|
2262
|
-
# Per-condition rule. Returns `nil` when the condition shape
|
|
2263
|
-
#
|
|
2264
|
-
#
|
|
2062
|
+
# Per-condition rule. Returns `nil` when the condition shape is not recognised (caller
|
|
2063
|
+
# marks `fully_narrowable = false`), or `{truthy:, falsey:, fully_narrowable:}` when it
|
|
2064
|
+
# is.
|
|
2265
2065
|
def apply_case_when_condition(scope, current, condition, falsey_acc)
|
|
2266
2066
|
int_range = case_equality_integer_range(condition)
|
|
2267
2067
|
return integer_range_when_result(current, int_range, falsey_acc) if int_range && integer_rooted_type?(current)
|
|
@@ -2283,11 +2083,10 @@ module Rigor
|
|
|
2283
2083
|
end
|
|
2284
2084
|
|
|
2285
2085
|
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).
|
|
2086
|
+
# `case n when k` is `k === n` which for Integer is value equality. The truthy edge
|
|
2087
|
+
# collapses the local to `Constant[k]`; the falsey edge tightens via
|
|
2088
|
+
# `narrow_integer_not_equal` (only effective when k sits at one endpoint of the
|
|
2089
|
+
# current range).
|
|
2291
2090
|
{
|
|
2292
2091
|
truthy: narrow_integer_equal(current, value),
|
|
2293
2092
|
falsey: narrow_integer_not_equal(falsey_acc, value),
|
|
@@ -2301,10 +2100,9 @@ module Rigor
|
|
|
2301
2100
|
narrow_integer_comparison(current, :>=, low),
|
|
2302
2101
|
:<=, high
|
|
2303
2102
|
)
|
|
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
|
|
2103
|
+
# The falsey edge of `n in [a, b]` is two-piece; we cannot express the complement
|
|
2104
|
+
# precisely with a single carrier, so keep the accumulator unchanged.
|
|
2105
|
+
# `fully_narrowable: false` forces the else-branch to see `current` (the unmodified
|
|
2308
2106
|
# entry type), which mirrors `between?` falsey behaviour.
|
|
2309
2107
|
{ truthy: truthy, falsey: falsey_acc, fully_narrowable: false }
|
|
2310
2108
|
end
|
|
@@ -2317,12 +2115,10 @@ module Rigor
|
|
|
2317
2115
|
}
|
|
2318
2116
|
end
|
|
2319
2117
|
|
|
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
|
|
2118
|
+
# Returns `[low, high]` for a `Prism::RangeNode` whose endpoints are both
|
|
2119
|
+
# `Prism::IntegerNode` literals, with `..`/`...` exclusivity respected. Open-ended
|
|
2120
|
+
# ranges use the symbolic infinities so the existing comparison narrowing tier handles
|
|
2121
|
+
# them. Returns `nil` for any other shape (Float endpoints, String endpoints, dynamic
|
|
2326
2122
|
# expressions).
|
|
2327
2123
|
def case_equality_integer_range(condition)
|
|
2328
2124
|
condition = unwrap_parens(condition)
|
|
@@ -2369,9 +2165,8 @@ module Rigor
|
|
|
2369
2165
|
node.is_a?(Prism::StringNode)
|
|
2370
2166
|
end
|
|
2371
2167
|
|
|
2372
|
-
# Walks a constant-reference subtree (`Prism::ConstantReadNode`,
|
|
2373
|
-
#
|
|
2374
|
-
# Returns nil for any non-constant argument shape so the
|
|
2168
|
+
# Walks a constant-reference subtree (`Prism::ConstantReadNode`, `Prism::ConstantPathNode`)
|
|
2169
|
+
# and renders its qualified name. Returns nil for any non-constant argument shape so the
|
|
2375
2170
|
# caller can fall through.
|
|
2376
2171
|
def static_class_name(node)
|
|
2377
2172
|
case node
|
|
@@ -2390,11 +2185,10 @@ module Rigor
|
|
|
2390
2185
|
|
|
2391
2186
|
# ----- narrow_class / narrow_not_class helpers -----
|
|
2392
2187
|
|
|
2393
|
-
# Polarity-aware dispatch table for {.narrow_class} /
|
|
2394
|
-
#
|
|
2395
|
-
#
|
|
2396
|
-
#
|
|
2397
|
-
# apply by looking at `polarity:`.
|
|
2188
|
+
# Polarity-aware dispatch table for {.narrow_class} / {.narrow_not_class}. Avoids
|
|
2189
|
+
# duplicating the per-carrier case statement and keeps each public surface a thin
|
|
2190
|
+
# delegate; the per-carrier helpers know which polarity to apply by looking at
|
|
2191
|
+
# `polarity:`.
|
|
2398
2192
|
def narrow_class_dispatch(type, class_name, context)
|
|
2399
2193
|
case type
|
|
2400
2194
|
when Type::Constant then narrow_constant_class(type, class_name, context)
|
|
@@ -2459,15 +2253,12 @@ module Rigor
|
|
|
2459
2253
|
subclass_of?(rigor_class, class_name, context) ? Type::Combinator.bot : constant
|
|
2460
2254
|
end
|
|
2461
2255
|
|
|
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.
|
|
2256
|
+
# Narrow a Nominal under `is_a?(class_name)`: when the nominal's class is already a
|
|
2257
|
+
# subclass of `class_name` (or matches under `exact: true`) preserve it; when
|
|
2258
|
+
# `class_name` is a subclass of the nominal's class (`Nominal[Numeric]` under
|
|
2259
|
+
# `is_a?(Integer)`) narrow DOWN to `Nominal[class_name]`; otherwise (disjoint
|
|
2260
|
+
# hierarchies under `is_a?`, mismatch under `instance_of?`) collapse to `Bot`.
|
|
2261
|
+
# Conservative when the analyzer environment cannot resolve either class.
|
|
2471
2262
|
def narrow_nominal_to_class(nominal, class_name, context)
|
|
2472
2263
|
return nominal if nominal.class_name == class_name
|
|
2473
2264
|
return Type::Combinator.bot if context.exact
|
|
@@ -2498,11 +2289,10 @@ module Rigor
|
|
|
2498
2289
|
subclass_of?(projected_class, class_name, context) ? Type::Combinator.bot : shape
|
|
2499
2290
|
end
|
|
2500
2291
|
|
|
2501
|
-
# `Singleton[Foo]` is the *class object* `Foo`, an instance of
|
|
2502
|
-
#
|
|
2503
|
-
# `Foo
|
|
2504
|
-
#
|
|
2505
|
-
# by treating singletons uniformly as `Class` instances.
|
|
2292
|
+
# `Singleton[Foo]` is the *class object* `Foo`, an instance of `Class` (which is a
|
|
2293
|
+
# subclass of `Module`). Asking `Foo.is_a?(Class)` returns true; `Foo.is_a?(Foo)`
|
|
2294
|
+
# returns false unless `Foo` is `Class` itself. We approximate this by treating
|
|
2295
|
+
# singletons uniformly as `Class` instances.
|
|
2506
2296
|
def narrow_singleton_to_class(singleton, class_name, context)
|
|
2507
2297
|
subclass_of?("Class", class_name, context) ? singleton : Type::Combinator.bot
|
|
2508
2298
|
end
|
|
@@ -2511,8 +2301,8 @@ module Rigor
|
|
|
2511
2301
|
subclass_of?("Class", class_name, context) ? Type::Combinator.bot : singleton
|
|
2512
2302
|
end
|
|
2513
2303
|
|
|
2514
|
-
# Top/Dynamic narrow to `Nominal[class_name]` so dispatch
|
|
2515
|
-
#
|
|
2304
|
+
# Top/Dynamic narrow to `Nominal[class_name]` so dispatch can resolve through the asked
|
|
2305
|
+
# class; Bot stays Bot.
|
|
2516
2306
|
def narrow_class_other(type, class_name)
|
|
2517
2307
|
case type
|
|
2518
2308
|
when Type::Dynamic, Type::Top then Type::Combinator.nominal_of(class_name)
|
|
@@ -2520,11 +2310,10 @@ module Rigor
|
|
|
2520
2310
|
end
|
|
2521
2311
|
end
|
|
2522
2312
|
|
|
2523
|
-
# Returns `true` when an instance of `rigor_class_name`
|
|
2524
|
-
#
|
|
2525
|
-
# `
|
|
2526
|
-
#
|
|
2527
|
-
# resolve through the analyzer environment.
|
|
2313
|
+
# Returns `true` when an instance of `rigor_class_name` satisfies
|
|
2314
|
+
# `is_a?(target_class_name)` (or `instance_of?(target_class_name)` when `exact: true`).
|
|
2315
|
+
# Falls back to the safe `false` when either name does not resolve through the analyzer
|
|
2316
|
+
# environment.
|
|
2528
2317
|
def subclass_of?(rigor_class_name, target_class_name, context)
|
|
2529
2318
|
return rigor_class_name == target_class_name if context.exact
|
|
2530
2319
|
|
|
@@ -2533,11 +2322,9 @@ module Rigor
|
|
|
2533
2322
|
)
|
|
2534
2323
|
end
|
|
2535
2324
|
|
|
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.
|
|
2325
|
+
# Compares two class names through the analyzer environment. Returns `:equal` when they
|
|
2326
|
+
# resolve to the same class, `:subclass` when `lhs <= rhs`, `:superclass` when `rhs <=
|
|
2327
|
+
# lhs`, `:disjoint` when neither, and `:unknown` when either name does not resolve.
|
|
2541
2328
|
def class_ordering(lhs, rhs, context)
|
|
2542
2329
|
return :equal if lhs == rhs
|
|
2543
2330
|
|
|
@@ -2565,11 +2352,10 @@ module Rigor
|
|
|
2565
2352
|
end
|
|
2566
2353
|
end
|
|
2567
2354
|
|
|
2568
|
-
# Narrows a safe-navigation call's receiver (`v&.foo`) to its
|
|
2569
|
-
#
|
|
2570
|
-
# or nil
|
|
2571
|
-
#
|
|
2572
|
-
# edge and as a post-pass over the existing predicate edges.
|
|
2355
|
+
# Narrows a safe-navigation call's receiver (`v&.foo`) to its non-nil fragment on the
|
|
2356
|
+
# truthy edge, returning `[truthy, falsey]` or nil when nothing applies (not safe-nav,
|
|
2357
|
+
# opaque receiver, or already non-nil). Used standalone for a bare `v&.foo` truthy edge
|
|
2358
|
+
# and as a post-pass over the existing predicate edges.
|
|
2573
2359
|
def analyse_safe_nav_receiver(node, scope)
|
|
2574
2360
|
return nil unless node.safe_navigation?
|
|
2575
2361
|
|
|
@@ -2590,12 +2376,11 @@ module Rigor
|
|
|
2590
2376
|
[scope.public_send(writer, receiver.name, non_nil), scope]
|
|
2591
2377
|
end
|
|
2592
2378
|
|
|
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.
|
|
2379
|
+
# Layers the safe-nav non-nil truthy narrowing over the edges an existing predicate path
|
|
2380
|
+
# already produced, so a safe-nav string predicate (`v&.start_with?(x)`) keeps its
|
|
2381
|
+
# relational fact AND proves `v` non-nil on the truthy edge. Re-runs the predicate's
|
|
2382
|
+
# narrowing under the non-nil truthy scope so the fact is attached to the narrowed
|
|
2383
|
+
# binding. No-op for non-safe-nav calls.
|
|
2599
2384
|
def apply_safe_nav_non_nil(node, scope, edges)
|
|
2600
2385
|
return edges unless node.safe_navigation? && edges
|
|
2601
2386
|
|
|
@@ -2610,23 +2395,20 @@ module Rigor
|
|
|
2610
2395
|
[truthy.public_send(writer, receiver.name, non_nil), falsey]
|
|
2611
2396
|
end
|
|
2612
2397
|
|
|
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.
|
|
2398
|
+
# `a && b` short-circuits: the truthy edge is the truthy edge of `b` evaluated under
|
|
2399
|
+
# `a`'s truthy scope; the falsey edge is the union of `a`'s falsey scope (b skipped) and
|
|
2400
|
+
# `b`'s falsey scope (b ran but returned falsey). When a sub-edge cannot be narrowed we
|
|
2401
|
+
# fall back to the entry scope so the caller still sees consistent keys across the two
|
|
2402
|
+
# output scopes.
|
|
2620
2403
|
def analyse_and(node, scope)
|
|
2621
2404
|
truthy_a, falsey_a = analyse(node.left, scope) || [scope, scope]
|
|
2622
2405
|
truthy_b, falsey_b = analyse(node.right, truthy_a) || [truthy_a, truthy_a]
|
|
2623
2406
|
[truthy_b, falsey_a.join(falsey_b)]
|
|
2624
2407
|
end
|
|
2625
2408
|
|
|
2626
|
-
# `a || b` short-circuits: the truthy edge is the union of
|
|
2627
|
-
# `
|
|
2628
|
-
#
|
|
2629
|
-
# evaluated under `a`'s falsey scope.
|
|
2409
|
+
# `a || b` short-circuits: the truthy edge is the union of `a`'s truthy scope (b
|
|
2410
|
+
# skipped) and `b`'s truthy scope (b ran and was truthy); the falsey edge is `b`'s
|
|
2411
|
+
# falsey scope evaluated under `a`'s falsey scope.
|
|
2630
2412
|
def analyse_or(node, scope)
|
|
2631
2413
|
truthy_a, falsey_a = analyse(node.left, scope) || [scope, scope]
|
|
2632
2414
|
truthy_b, falsey_b = analyse(node.right, falsey_a) || [falsey_a, falsey_a]
|