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
|
@@ -9,6 +9,7 @@ require_relative "../source/node_walker"
|
|
|
9
9
|
require_relative "../source/constant_path"
|
|
10
10
|
require_relative "block_parameter_binder"
|
|
11
11
|
require_relative "body_fixpoint"
|
|
12
|
+
require_relative "dynamic_origin"
|
|
12
13
|
require_relative "struct_fold_safety"
|
|
13
14
|
require_relative "closure_escape_analyzer"
|
|
14
15
|
require_relative "indexed_narrowing"
|
|
@@ -20,14 +21,11 @@ require_relative "narrowing"
|
|
|
20
21
|
|
|
21
22
|
module Rigor
|
|
22
23
|
module Inference
|
|
23
|
-
# Statement-level evaluator that complements `Rigor::Inference::ExpressionTyper`
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
# evaluated node produces, and the scope that callers should observe
|
|
27
|
-
# AFTER the node has run.
|
|
24
|
+
# Statement-level evaluator that complements `Rigor::Inference::ExpressionTyper` by threading an immutable
|
|
25
|
+
# {Rigor::Scope} through control-flow constructs. The output is the pair `[Rigor::Type, Rigor::Scope]`: the type
|
|
26
|
+
# that the evaluated node produces, and the scope that callers should observe AFTER the node has run.
|
|
28
27
|
#
|
|
29
|
-
# Slice 3 phase 2 ships the evaluator surface and the scope-threading
|
|
30
|
-
# rules for the canonical statement-y nodes:
|
|
28
|
+
# Slice 3 phase 2 ships the evaluator surface and the scope-threading rules for the canonical statement-y nodes:
|
|
31
29
|
#
|
|
32
30
|
# - sequential evaluation across `Prism::StatementsNode`/`ProgramNode`,
|
|
33
31
|
# - local-variable assignment (`Prism::LocalVariableWriteNode`) binding
|
|
@@ -40,24 +38,20 @@ module Rigor
|
|
|
40
38
|
# - pass-through helpers for `ParenthesesNode`, `ElseNode`,
|
|
41
39
|
# `WhenNode`/`InNode`, and `RescueNode`.
|
|
42
40
|
#
|
|
43
|
-
# Anything outside the catalogue defers to `Rigor::Scope#type_of` and
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
# raise and MUST keep the scope intact.
|
|
41
|
+
# Anything outside the catalogue defers to `Rigor::Scope#type_of` and returns the receiver scope unchanged. This
|
|
42
|
+
# matches the Slice 1 fail-soft policy: an unrecognised statement-level node MUST NOT raise and MUST keep the scope
|
|
43
|
+
# intact.
|
|
47
44
|
#
|
|
48
|
-
# The class is stateful (`@scope`, `@tracer`) but every public call
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
# forked scope so different branches stay isolated.
|
|
45
|
+
# The class is stateful (`@scope`, `@tracer`) but every public call returns fresh values; the receiver scope MUST
|
|
46
|
+
# never be mutated. Recursive evaluation always allocates a new instance with the forked scope so different branches
|
|
47
|
+
# stay isolated.
|
|
52
48
|
#
|
|
53
|
-
# See docs/internal-spec/inference-engine.md for the public contract
|
|
54
|
-
#
|
|
49
|
+
# See docs/internal-spec/inference-engine.md for the public contract and docs/adr/4-type-inference-engine.md for the
|
|
50
|
+
# slice rationale.
|
|
55
51
|
# rubocop:disable Metrics/ClassLength
|
|
56
52
|
class StatementEvaluator
|
|
57
|
-
# Hash-based dispatch keeps `evaluate` linear and lets future slices
|
|
58
|
-
#
|
|
59
|
-
# statement past RuboCop's cyclomatic budget. Anonymous Prism
|
|
60
|
-
# subclasses are not expected.
|
|
53
|
+
# Hash-based dispatch keeps `evaluate` linear and lets future slices add control-flow node kinds without growing a
|
|
54
|
+
# single case statement past RuboCop's cyclomatic budget. Anonymous Prism subclasses are not expected.
|
|
61
55
|
HANDLERS = {
|
|
62
56
|
Prism::StatementsNode => :eval_statements,
|
|
63
57
|
Prism::ProgramNode => :eval_program,
|
|
@@ -107,34 +101,27 @@ module Rigor
|
|
|
107
101
|
}.freeze
|
|
108
102
|
private_constant :HANDLERS
|
|
109
103
|
|
|
110
|
-
# Thread-local sink (an Array) collecting the value types of explicit
|
|
111
|
-
#
|
|
112
|
-
# `
|
|
113
|
-
# method
|
|
114
|
-
# `
|
|
115
|
-
# only records what the method *returns* through that edge. nil means
|
|
116
|
-
# "not collecting" — a top-level / DSL-block walk, or inside a nested
|
|
117
|
-
# `def` barrier (whose returns belong to the inner method).
|
|
104
|
+
# Thread-local sink (an Array) collecting the value types of explicit `return value` nodes reached while
|
|
105
|
+
# evaluating a method body, so `ExpressionTyper#infer_user_method_return` can join them into the method's inferred
|
|
106
|
+
# return type. The flow value of a `return` is still `Bot` (it transfers control rather than producing a value);
|
|
107
|
+
# the sink only records what the method *returns* through that edge. nil means "not collecting" — a top-level /
|
|
108
|
+
# DSL-block walk, or inside a nested `def` barrier (whose returns belong to the inner method).
|
|
118
109
|
RETURN_SINK_KEY = :rigor_return_sink
|
|
119
110
|
private_constant :RETURN_SINK_KEY
|
|
120
111
|
|
|
121
|
-
# Thread-local sink (an Array of `[BreakNode, Scope]`) collecting the
|
|
122
|
-
#
|
|
123
|
-
#
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
#
|
|
128
|
-
# inner construct, not the lexical loop — filtered out by the
|
|
129
|
-
# directly-targeting break set, see {#directly_targeting_breaks}.
|
|
130
|
-
# See docs/notes/20260615-loop-break-binding-propagation-design.md.
|
|
112
|
+
# Thread-local sink (an Array of `[BreakNode, Scope]`) collecting the scope at each `break` reached while
|
|
113
|
+
# evaluating a loop body, so `eval_loop` / `eval_for` can join a `break`-path binding (`flag = true; break`) into
|
|
114
|
+
# the loop continuation that the fall-through would otherwise drop. Stacks like the return sink: a nested loop
|
|
115
|
+
# installs its own sink, restored on exit, so an inner loop's break does not leak to the outer one. A `break`
|
|
116
|
+
# inside a block / nested loop targets that inner construct, not the lexical loop — filtered out by the
|
|
117
|
+
# directly-targeting break set, see {#directly_targeting_breaks}. See
|
|
118
|
+
# docs/notes/20260615-loop-break-binding-propagation-design.md.
|
|
131
119
|
BREAK_SINK_KEY = :rigor_break_sink
|
|
132
120
|
private_constant :BREAK_SINK_KEY
|
|
133
121
|
|
|
134
|
-
# Lexical class frame: the `name:` field is the qualified class
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
# nested defs resolve to singleton-method RBS lookups.
|
|
122
|
+
# Lexical class frame: the `name:` field is the qualified class name as it would render in Ruby (e.g.,
|
|
123
|
+
# `"Foo::Bar"`); the `singleton:` field is `true` for `class << self` frames so nested defs resolve to
|
|
124
|
+
# singleton-method RBS lookups.
|
|
138
125
|
ClassFrame = Data.define(:name, :singleton)
|
|
139
126
|
|
|
140
127
|
# @param scope [Rigor::Scope]
|
|
@@ -167,13 +154,11 @@ module Rigor
|
|
|
167
154
|
@converged_loop_recording = converged_loop_recording
|
|
168
155
|
end
|
|
169
156
|
|
|
170
|
-
# Runs `block` with a fresh return sink installed, then yields the
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
#
|
|
174
|
-
#
|
|
175
|
-
# Used by `ExpressionTyper#infer_user_method_return` to join the
|
|
176
|
-
# explicit returns into the inferred method-return type.
|
|
157
|
+
# Runs `block` with a fresh return sink installed, then yields the collected explicit-`return` value types to the
|
|
158
|
+
# caller. The sink is an array of `Rigor::Type`. Nested invocations stack: the previous sink is restored on exit
|
|
159
|
+
# so a `def` evaluated inside another method's body (which itself installed a sink) does not corrupt the outer
|
|
160
|
+
# one. Used by `ExpressionTyper#infer_user_method_return` to join the explicit returns into the inferred
|
|
161
|
+
# method-return type.
|
|
177
162
|
def self.with_return_sink
|
|
178
163
|
previous = Thread.current[RETURN_SINK_KEY]
|
|
179
164
|
sink = []
|
|
@@ -186,10 +171,8 @@ module Rigor
|
|
|
186
171
|
[result, sink]
|
|
187
172
|
end
|
|
188
173
|
|
|
189
|
-
# Evaluate `node` under the receiver scope. Returns `[type, scope']`
|
|
190
|
-
#
|
|
191
|
-
# scope observable after the node has run. The receiver scope is
|
|
192
|
-
# never mutated.
|
|
174
|
+
# Evaluate `node` under the receiver scope. Returns `[type, scope']` where `type` is the value the node produces
|
|
175
|
+
# and `scope'` is the scope observable after the node has run. The receiver scope is never mutated.
|
|
193
176
|
#
|
|
194
177
|
# @param node [Prism::Node]
|
|
195
178
|
# @return [Array(Rigor::Type, Rigor::Scope)]
|
|
@@ -199,9 +182,8 @@ module Rigor
|
|
|
199
182
|
handler = HANDLERS[node.class]
|
|
200
183
|
return send(handler, node) if handler
|
|
201
184
|
|
|
202
|
-
# Default: the node is treated as a pure expression. Type it
|
|
203
|
-
#
|
|
204
|
-
# current scope's locals) and leave the scope unchanged.
|
|
185
|
+
# Default: the node is treated as a pure expression. Type it through the existing expression typer (which
|
|
186
|
+
# observes the current scope's locals) and leave the scope unchanged.
|
|
205
187
|
[@scope.type_of(node, tracer: @tracer), @scope]
|
|
206
188
|
end
|
|
207
189
|
|
|
@@ -209,10 +191,9 @@ module Rigor
|
|
|
209
191
|
|
|
210
192
|
attr_reader :scope, :tracer
|
|
211
193
|
|
|
212
|
-
# Thread the scope through every child statement in declaration
|
|
213
|
-
#
|
|
214
|
-
#
|
|
215
|
-
# types are discarded, but their scope effects are preserved.
|
|
194
|
+
# Thread the scope through every child statement in declaration order. The body's value is the type of the last
|
|
195
|
+
# statement (or `Constant[nil]` for an empty body); intermediate statements' types are discarded, but their scope
|
|
196
|
+
# effects are preserved.
|
|
216
197
|
def eval_statements(node)
|
|
217
198
|
result_type = Type::Combinator.constant_of(nil)
|
|
218
199
|
current = scope
|
|
@@ -228,48 +209,53 @@ module Rigor
|
|
|
228
209
|
sub_eval(node.statements, scope)
|
|
229
210
|
end
|
|
230
211
|
|
|
231
|
-
# `name = rvalue` evaluates the rvalue under the entry scope (so
|
|
232
|
-
#
|
|
233
|
-
#
|
|
234
|
-
# assignment forms (`+=` etc.) are deferred to a follow-up; for
|
|
235
|
-
# now they degrade to "type the rhs, do not rebind" via the
|
|
236
|
-
# default branch in {#evaluate}.
|
|
212
|
+
# `name = rvalue` evaluates the rvalue under the entry scope (so earlier assignments in a chained `a = b = expr`
|
|
213
|
+
# propagate left-to-right) and binds `name` to the result type. Compound assignment forms (`+=` etc.) are deferred
|
|
214
|
+
# to a follow-up; for now they degrade to "type the rhs, do not rebind" via the default branch in {#evaluate}.
|
|
237
215
|
def eval_local_write(node)
|
|
238
216
|
rhs_type, post_rhs = sub_eval(node.value, scope)
|
|
239
|
-
# ADR-58 WD1 — `r = @right` where `@right`'s optionality is purely
|
|
240
|
-
# declaration-sourced
|
|
241
|
-
#
|
|
242
|
-
#
|
|
243
|
-
#
|
|
244
|
-
# Any other RHS (a call result, a method-local-nil-bearing value)
|
|
245
|
-
# leaves the local flow-live and the diagnostic fires as before.
|
|
217
|
+
# ADR-58 WD1 — `r = @right` where `@right`'s optionality is purely declaration-sourced makes `r`
|
|
218
|
+
# declaration-sourced too (the survey's exact rotation/traversal shape `r = @right; r.key`). The mark is
|
|
219
|
+
# computed on the RHS *value*'s provenance — a pure ivar read of a currently declaration-sourced ivar — so it
|
|
220
|
+
# survives the local copy. Any other RHS (a call result, a method-local-nil-bearing value) leaves the local
|
|
221
|
+
# flow-live and the diagnostic fires as before.
|
|
246
222
|
if declaration_sourced_ivar_read?(node.value, post_rhs)
|
|
247
223
|
return [rhs_type, post_rhs.with_declaration_sourced_local(node.name, rhs_type)]
|
|
248
224
|
end
|
|
249
225
|
|
|
250
|
-
|
|
226
|
+
bound = post_rhs.with_local(node.name, rhs_type)
|
|
227
|
+
bound = bound.with_local_origin(node.name, rhs_origin(node.value, post_rhs, rhs_type))
|
|
228
|
+
[rhs_type, bound]
|
|
251
229
|
end
|
|
252
230
|
|
|
253
|
-
#
|
|
254
|
-
#
|
|
231
|
+
# ADR-82 WD1 — the {Inference::DynamicOrigin} cause to propagate onto a local / ivar being bound to `rhs`.
|
|
232
|
+
# Returns the cause recorded on the assignment's rhs node when the value is `Dynamic` (so a later
|
|
233
|
+
# `x` / `@x` receiver-read resolves to why it is dynamic), else `nil` — `with_local_origin` /
|
|
234
|
+
# `with_ivar_origin` treat `nil` as a no-op, so this is safe to call unconditionally.
|
|
235
|
+
def rhs_origin(value_node, scope_after_rhs, rhs_type)
|
|
236
|
+
return nil unless rhs_type.is_a?(Type::Dynamic)
|
|
237
|
+
|
|
238
|
+
scope_after_rhs.dynamic_origins[value_node]
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# True when `value_node` is a bare instance-variable read whose binding in `scope_at_read` is currently marked
|
|
242
|
+
# declaration-sourced.
|
|
255
243
|
def declaration_sourced_ivar_read?(value_node, scope_at_read)
|
|
256
244
|
return false unless value_node.is_a?(Prism::InstanceVariableReadNode)
|
|
257
245
|
|
|
258
246
|
scope_at_read.declaration_sourced?(:ivar, value_node.name)
|
|
259
247
|
end
|
|
260
248
|
|
|
261
|
-
# Slice 7 phase 1 — instance/class/global variable
|
|
262
|
-
#
|
|
263
|
-
#
|
|
264
|
-
#
|
|
265
|
-
#
|
|
266
|
-
# are method-local: a fresh scope is built at every `def`
|
|
267
|
-
# entry through `build_method_entry_scope`, so writes do
|
|
268
|
-
# not leak across method boundaries until cross-method
|
|
269
|
-
# ivar/cvar tracking lands.
|
|
249
|
+
# Slice 7 phase 1 — instance/class/global variable writes. Each handler evaluates the rvalue under the entry scope
|
|
250
|
+
# and binds the named variable into the post-scope's per-kind binding map. The expression value is the rvalue
|
|
251
|
+
# type, matching Ruby's semantics. Bindings are method-local: a fresh scope is built at every `def` entry through
|
|
252
|
+
# `build_method_entry_scope`, so writes do not leak across method boundaries until cross-method ivar/cvar tracking
|
|
253
|
+
# lands.
|
|
270
254
|
def eval_ivar_write(node)
|
|
271
255
|
rhs_type, post_rhs = sub_eval(node.value, scope)
|
|
272
|
-
|
|
256
|
+
bound = post_rhs.with_ivar(node.name, rhs_type)
|
|
257
|
+
bound = bound.with_ivar_origin(node.name, rhs_origin(node.value, post_rhs, rhs_type))
|
|
258
|
+
[rhs_type, bound]
|
|
273
259
|
end
|
|
274
260
|
|
|
275
261
|
def eval_cvar_write(node)
|
|
@@ -282,8 +268,7 @@ module Rigor
|
|
|
282
268
|
[rhs_type, post_rhs.with_global(node.name, rhs_type)]
|
|
283
269
|
end
|
|
284
270
|
|
|
285
|
-
# Slice 7 phase 3 — compound writes (||=, &&=, +=/-=/...)
|
|
286
|
-
# for every variable kind. Each handler:
|
|
271
|
+
# Slice 7 phase 3 — compound writes (||=, &&=, +=/-=/...) for every variable kind. Each handler:
|
|
287
272
|
# 1. Reads the current type from the appropriate scope
|
|
288
273
|
# binding map (or `Dynamic[Top]` when unbound).
|
|
289
274
|
# 2. Evaluates the rvalue under the entry scope and
|
|
@@ -380,18 +365,14 @@ module Rigor
|
|
|
380
365
|
end
|
|
381
366
|
end
|
|
382
367
|
|
|
383
|
-
# `receiver[key] ||= default` — the Redmine `Query#as_params`
|
|
384
|
-
#
|
|
385
|
-
#
|
|
386
|
-
# a Tuple / Hash carrier instead of the `Constant[nil]` an
|
|
387
|
-
# empty `HashShape{}` lookup would otherwise fold to.
|
|
368
|
+
# `receiver[key] ||= default` — the Redmine `Query#as_params` idiom. After the `||=`, the next read at
|
|
369
|
+
# `receiver[key]` is known non-nil; the next `<<` / `[]=` / other mutator runs against a Tuple / Hash carrier
|
|
370
|
+
# instead of the `Constant[nil]` an empty `HashShape{}` lookup would otherwise fold to.
|
|
388
371
|
#
|
|
389
|
-
# The handler:
|
|
390
|
-
# 1. Types the equivalent `receiver[key]` read under the
|
|
372
|
+
# The handler: 1. Types the equivalent `receiver[key]` read under the
|
|
391
373
|
# entry scope (so any previously-recorded narrowing for
|
|
392
374
|
# the same address is already applied).
|
|
393
|
-
# 2. Types the rvalue under the entry scope.
|
|
394
|
-
# 3. Computes `union(narrow_truthy(current), rhs)` — the
|
|
375
|
+
# 2. Types the rvalue under the entry scope. 3. Computes `union(narrow_truthy(current), rhs)` — the
|
|
395
376
|
# standard `||=` result shape used by locals / ivars /
|
|
396
377
|
# cvars / globals.
|
|
397
378
|
# 4. Records the result type in the post-scope as a
|
|
@@ -401,8 +382,7 @@ module Rigor
|
|
|
401
382
|
# fall through to "no scope effect", matching the old
|
|
402
383
|
# `Prism::IndexOrWriteNode` default-branch behaviour.
|
|
403
384
|
#
|
|
404
|
-
# The expression value is the result type, matching Ruby's
|
|
405
|
-
# semantics: `(x = params[:f] ||= []); x` observes the
|
|
385
|
+
# The expression value is the result type, matching Ruby's semantics: `(x = params[:f] ||= []); x` observes the
|
|
406
386
|
# post-`||=` value, not the rvalue alone.
|
|
407
387
|
def eval_index_or_write(node)
|
|
408
388
|
rhs_type, post_rhs = sub_eval(node.value, scope)
|
|
@@ -435,15 +415,11 @@ module Rigor
|
|
|
435
415
|
result || Type::Combinator.untyped
|
|
436
416
|
end
|
|
437
417
|
|
|
438
|
-
# `a, b = rhs` — Slice 5 phase 2 sub-phase 2 destructuring.
|
|
439
|
-
#
|
|
440
|
-
#
|
|
441
|
-
#
|
|
442
|
-
#
|
|
443
|
-
# right-hand sides produce per-slot types element-wise; other
|
|
444
|
-
# carriers fall back to `Dynamic[Top]` per slot. The expression
|
|
445
|
-
# value is the right-hand side type (matching Ruby's semantics:
|
|
446
|
-
# `(a, b = [1, 2])` evaluates to `[1, 2]`).
|
|
418
|
+
# `a, b = rhs` — Slice 5 phase 2 sub-phase 2 destructuring. Evaluates the right-hand side under the entry scope,
|
|
419
|
+
# then decomposes its type against the multi-write target tree (Prism::MultiWriteNode#lefts/rest/rights, including
|
|
420
|
+
# nested Prism::MultiTargetNode for the `(b, c)` form). Tuple-shaped right-hand sides produce per-slot types
|
|
421
|
+
# element-wise; other carriers fall back to `Dynamic[Top]` per slot. The expression value is the right-hand side
|
|
422
|
+
# type (matching Ruby's semantics: `(a, b = [1, 2])` evaluates to `[1, 2]`).
|
|
447
423
|
def eval_multi_write(node)
|
|
448
424
|
rhs_type, post_rhs = sub_eval(node.value, scope)
|
|
449
425
|
bindings = MultiTargetBinder.bind(node, rhs_type)
|
|
@@ -451,34 +427,24 @@ module Rigor
|
|
|
451
427
|
[rhs_type, post]
|
|
452
428
|
end
|
|
453
429
|
|
|
454
|
-
# `if pred; t; (elsif/else)?` runs the predicate first (its
|
|
455
|
-
#
|
|
456
|
-
#
|
|
457
|
-
#
|
|
458
|
-
#
|
|
459
|
-
#
|
|
460
|
-
# specialise return the post-predicate scope unchanged on both
|
|
461
|
-
# edges, preserving the Slice 3 phase 2 behaviour. The branches'
|
|
462
|
-
# result types are unioned; their post-scopes are joined with
|
|
463
|
-
# nil-injection on half-bound names so a name set in one branch
|
|
464
|
-
# but not the other is observable as `T | nil` after the if.
|
|
430
|
+
# `if pred; t; (elsif/else)?` runs the predicate first (its post-scope is shared by both branches), then asks
|
|
431
|
+
# `Rigor::Inference::Narrowing` for the truthy and falsey edge scopes derived from the predicate. Slice 6 phase 1
|
|
432
|
+
# narrows local-variable bindings on truthiness, `nil?`, `!`, and `&&`/ `||` predicate composition; predicates the
|
|
433
|
+
# analyser does not specialise return the post-predicate scope unchanged on both edges, preserving the Slice 3
|
|
434
|
+
# phase 2 behaviour. The branches' result types are unioned; their post-scopes are joined with nil-injection on
|
|
435
|
+
# half-bound names so a name set in one branch but not the other is observable as `T | nil` after the if.
|
|
465
436
|
def eval_if(node)
|
|
466
437
|
pred_type, post_pred = sub_eval(node.predicate, scope)
|
|
467
438
|
|
|
468
|
-
# When the predicate is a known-truthy / known-falsey type
|
|
469
|
-
#
|
|
470
|
-
#
|
|
471
|
-
# type and a post-scope. The dead branch is skipped so the
|
|
472
|
-
# result type is precise (`Constant[:even]` instead of the
|
|
473
|
-
# joined `Constant[:even] | Constant[:odd]`).
|
|
439
|
+
# When the predicate is a known-truthy / known-falsey type (notably `Constant[true]` / `Constant[false]` after
|
|
440
|
+
# the constant-fold tier), only the live branch contributes a type and a post-scope. The dead branch is skipped
|
|
441
|
+
# so the result type is precise (`Constant[:even]` instead of the joined `Constant[:even] | Constant[:odd]`).
|
|
474
442
|
live = live_branch_for_if(node, pred_type, post_pred)
|
|
475
443
|
if live
|
|
476
444
|
live_type, _live_scope = live
|
|
477
|
-
# When the provably-live then-branch terminates and there is no
|
|
478
|
-
#
|
|
479
|
-
#
|
|
480
|
-
# with an ivar seeded as Constant[nil] (making nil? = Constant[true]
|
|
481
|
-
# and the then-branch "provably live") propagates the un-narrowed
|
|
445
|
+
# When the provably-live then-branch terminates and there is no else, apply the same falsey-scope narrowing as
|
|
446
|
+
# the standard early-return path below. Without this, `return if @ivar.nil?` with an ivar seeded as
|
|
447
|
+
# Constant[nil] (making nil? = Constant[true] and the then-branch "provably live") propagates the un-narrowed
|
|
482
448
|
# nil scope past the guard instead of Bot.
|
|
483
449
|
if branch_terminates?(node.statements, live_type) && node.subsequent.nil?
|
|
484
450
|
_, falsey_scope = Narrowing.predicate_scopes(node.predicate, post_pred)
|
|
@@ -490,27 +456,19 @@ module Rigor
|
|
|
490
456
|
truthy_scope, falsey_scope = Narrowing.predicate_scopes(node.predicate, post_pred)
|
|
491
457
|
then_type, then_scope = eval_branch_or_nil(node.statements, truthy_scope)
|
|
492
458
|
else_type, else_scope = eval_branch_or_nil(node.subsequent, falsey_scope)
|
|
493
|
-
# Slice 7 phase 14 — early-return narrowing. When the
|
|
494
|
-
#
|
|
495
|
-
#
|
|
496
|
-
# is the
|
|
497
|
-
# statements observe the predicate-was-false world). The
|
|
498
|
-
# then-body is the *skipped* path, so the bare narrowing
|
|
499
|
-
# (no body assignments) is the correct continuation.
|
|
459
|
+
# Slice 7 phase 14 — early-return narrowing. When the then-branch unconditionally exits (return / next / break /
|
|
460
|
+
# raise) and there is no else, the post-scope is the falsey edge of the predicate (subsequent statements observe
|
|
461
|
+
# the predicate-was-false world). The then-body is the *skipped* path, so the bare narrowing (no body
|
|
462
|
+
# assignments) is the correct continuation.
|
|
500
463
|
return [Type::Combinator.union(then_type, else_type), falsey_scope] \
|
|
501
464
|
if branch_terminates?(node.statements, then_type) && node.subsequent.nil?
|
|
502
|
-
# Symmetric case: the else / elsif-chain (`node.subsequent`)
|
|
503
|
-
#
|
|
504
|
-
# then-
|
|
505
|
-
#
|
|
506
|
-
# then
|
|
507
|
-
#
|
|
508
|
-
#
|
|
509
|
-
# spuriously nil-inject: e.g. the inner `elsif … else raise`
|
|
510
|
-
# of `if a then x=… elsif b then x=… else raise end` would
|
|
511
|
-
# return with `x` unbound, and the outer if's join would then
|
|
512
|
-
# read `x` as `… | nil` and fire a false `possible-nil-receiver`
|
|
513
|
-
# (liquid v5.x sweep, Event 3).
|
|
465
|
+
# Symmetric case: the else / elsif-chain (`node.subsequent`) unconditionally exits, so the only surviving path
|
|
466
|
+
# is the then-branch that RAN. The continuation must therefore carry `then_scope` — the predicate-truthy
|
|
467
|
+
# narrowing PLUS the then-body's assignments — not the bare `truthy_scope`. Returning `truthy_scope` drops every
|
|
468
|
+
# local the then-body bound, leaving it unbound for any enclosing merge to spuriously nil-inject: e.g. the inner
|
|
469
|
+
# `elsif … else raise` of `if a then x=… elsif b then x=… else raise end` would return with `x` unbound, and the
|
|
470
|
+
# outer if's join would then read `x` as `… | nil` and fire a false `possible-nil-receiver` (liquid v5.x sweep,
|
|
471
|
+
# Event 3).
|
|
514
472
|
return [Type::Combinator.union(then_type, else_type), then_scope] \
|
|
515
473
|
if branch_terminates?(node.subsequent, else_type) && node.statements
|
|
516
474
|
|
|
@@ -520,20 +478,18 @@ module Rigor
|
|
|
520
478
|
]
|
|
521
479
|
end
|
|
522
480
|
|
|
523
|
-
# `unless pred; t; else; e; end`. Same shape as `if`, but Prism
|
|
524
|
-
#
|
|
525
|
-
#
|
|
526
|
-
# because `unless` runs its body when the predicate is falsey.
|
|
481
|
+
# `unless pred; t; else; e; end`. Same shape as `if`, but Prism exposes the else-branch as `else_clause` (no elsif
|
|
482
|
+
# chain). The narrower's truthy/falsey edges are routed in swapped form because `unless` runs its body when the
|
|
483
|
+
# predicate is falsey.
|
|
527
484
|
def eval_unless(node)
|
|
528
485
|
pred_type, post_pred = sub_eval(node.predicate, scope)
|
|
529
486
|
|
|
530
487
|
live = live_branch_for_unless(node, pred_type, post_pred)
|
|
531
488
|
if live
|
|
532
489
|
live_type, _live_scope = live
|
|
533
|
-
# Mirror of the eval_if fix: when the provably-live unless-body
|
|
534
|
-
#
|
|
535
|
-
#
|
|
536
|
-
# the nil scope past the guard.
|
|
490
|
+
# Mirror of the eval_if fix: when the provably-live unless-body terminates and there is no else, apply the
|
|
491
|
+
# truthy-scope narrowing so `return unless @ivar` with a nil-seeded ivar doesn't propagate the nil scope past
|
|
492
|
+
# the guard.
|
|
537
493
|
if branch_terminates?(node.statements, live_type) && node.else_clause.nil?
|
|
538
494
|
truthy_scope, = Narrowing.predicate_scopes(node.predicate, post_pred)
|
|
539
495
|
return [live_type, truthy_scope]
|
|
@@ -544,18 +500,13 @@ module Rigor
|
|
|
544
500
|
truthy_scope, falsey_scope = Narrowing.predicate_scopes(node.predicate, post_pred)
|
|
545
501
|
then_type, then_scope = eval_branch_or_nil(node.statements, falsey_scope)
|
|
546
502
|
else_type, else_scope = eval_branch_or_nil(node.else_clause, truthy_scope)
|
|
547
|
-
# Slice 7 phase 14 — same early-return narrowing as
|
|
548
|
-
#
|
|
549
|
-
# is no else, the post-scope is the truthy edge (the body
|
|
550
|
-
# is the skipped path, so the bare narrowing is correct).
|
|
503
|
+
# Slice 7 phase 14 — same early-return narrowing as `if`: when the body unconditionally exits and there is no
|
|
504
|
+
# else, the post-scope is the truthy edge (the body is the skipped path, so the bare narrowing is correct).
|
|
551
505
|
return [Type::Combinator.union(then_type, else_type), truthy_scope] \
|
|
552
506
|
if branch_terminates?(node.statements, then_type) && node.else_clause.nil?
|
|
553
|
-
# Symmetric to the `if` else-exits fix: when the else-clause
|
|
554
|
-
#
|
|
555
|
-
#
|
|
556
|
-
# narrowing PLUS the body's assignments), not the bare
|
|
557
|
-
# `falsey_scope` — otherwise body-bound locals are dropped and
|
|
558
|
-
# an enclosing merge nil-injects them.
|
|
507
|
+
# Symmetric to the `if` else-exits fix: when the else-clause exits, the surviving path is the unless-body that
|
|
508
|
+
# RAN, so the continuation carries `then_scope` (the predicate-falsey narrowing PLUS the body's assignments),
|
|
509
|
+
# not the bare `falsey_scope` — otherwise body-bound locals are dropped and an enclosing merge nil-injects them.
|
|
559
510
|
return [Type::Combinator.union(then_type, else_type), then_scope] \
|
|
560
511
|
if branch_terminates?(node.else_clause, else_type) && node.statements
|
|
561
512
|
|
|
@@ -565,12 +516,10 @@ module Rigor
|
|
|
565
516
|
]
|
|
566
517
|
end
|
|
567
518
|
|
|
568
|
-
# Returns the `[type, post_scope]` of the live branch when the
|
|
569
|
-
#
|
|
570
|
-
#
|
|
571
|
-
#
|
|
572
|
-
# carriers like `Nominal[Integer]` (Integer is always truthy
|
|
573
|
-
# in Ruby — including 0) also collapse the dead else.
|
|
519
|
+
# Returns the `[type, post_scope]` of the live branch when the predicate is provably truthy / falsey, else nil so
|
|
520
|
+
# the caller falls through to the standard both-branch evaluation. Constant `true`/`false` is the obvious trigger;
|
|
521
|
+
# non-falsey carriers like `Nominal[Integer]` (Integer is always truthy in Ruby — including 0) also collapse the
|
|
522
|
+
# dead else.
|
|
574
523
|
def live_branch_for_if(node, pred_type, post_pred)
|
|
575
524
|
case Narrowing.predicate_certainty(pred_type)
|
|
576
525
|
when :truthy then eval_branch_or_nil(node.statements, post_pred)
|
|
@@ -591,11 +540,9 @@ module Rigor
|
|
|
591
540
|
sub_eval(node.statements, scope)
|
|
592
541
|
end
|
|
593
542
|
|
|
594
|
-
# `case pred; when ...; when ...; else; end` and the pattern-
|
|
595
|
-
#
|
|
596
|
-
#
|
|
597
|
-
# independently and merged with nil-injection so half-bound
|
|
598
|
-
# names degrade to `T | nil`.
|
|
543
|
+
# `case pred; when ...; when ...; else; end` and the pattern- matching variant. The predicate's post-scope is
|
|
544
|
+
# shared with every branch (including the else); branches are evaluated independently and merged with
|
|
545
|
+
# nil-injection so half-bound names degrade to `T | nil`.
|
|
599
546
|
def eval_case(node)
|
|
600
547
|
post_pred = node.predicate ? sub_eval(node.predicate, scope).last : scope
|
|
601
548
|
branch_results, falsey_scope = eval_case_when_branches(node.predicate, node.conditions, post_pred)
|
|
@@ -609,16 +556,13 @@ module Rigor
|
|
|
609
556
|
]
|
|
610
557
|
end
|
|
611
558
|
|
|
612
|
-
# Joins the post-scopes of every `when`/`in`/`else` branch, dropping
|
|
613
|
-
#
|
|
614
|
-
#
|
|
615
|
-
#
|
|
616
|
-
#
|
|
617
|
-
#
|
|
618
|
-
#
|
|
619
|
-
# keeps `v: "a" | "b"` instead of `... | nil`. When every branch
|
|
620
|
-
# terminates the merge is itself unreachable; fall back to the full
|
|
621
|
-
# join so the continuation scope stays well-formed.
|
|
559
|
+
# Joins the post-scopes of every `when`/`in`/`else` branch, dropping the scope of any branch that terminates
|
|
560
|
+
# (raises / returns / throws / types to `Bot`) before the merge — control never falls through such a branch, so
|
|
561
|
+
# its half-bound locals must not nil-inject the names a live sibling branch assigned. Mirrors the
|
|
562
|
+
# `branch_terminates?` rule `eval_if`/`eval_unless` already apply to the if/else merge: e.g. `case x; when 1 then
|
|
563
|
+
# v="a"; when 2 then v="b"; else raise; end` keeps `v: "a" | "b"` instead of `... | nil`. When every branch
|
|
564
|
+
# terminates the merge is itself unreachable; fall back to the full join so the continuation scope stays
|
|
565
|
+
# well-formed.
|
|
622
566
|
def join_case_branch_scopes(results, nodes)
|
|
623
567
|
live = []
|
|
624
568
|
results.each_with_index do |(type, branch_scope), i|
|
|
@@ -633,14 +577,11 @@ module Rigor
|
|
|
633
577
|
results = []
|
|
634
578
|
falsey_scope = entry_scope
|
|
635
579
|
conditions.each do |branch|
|
|
636
|
-
# ADR-47 WD2 — record the scope ENTERING this clause (the
|
|
637
|
-
#
|
|
638
|
-
# clause
|
|
639
|
-
#
|
|
640
|
-
#
|
|
641
|
-
# disjoint). `on_enter`-only (no recursion) so no condition
|
|
642
|
-
# sub-expression is newly typed; `propagate` preserves the
|
|
643
|
-
# entry because it already keys the node.
|
|
580
|
+
# ADR-47 WD2 — record the scope ENTERING this clause (the subject narrowed by every earlier clause's negation)
|
|
581
|
+
# on the clause's first condition node, so `flow.unreachable-clause` can tell a prior-exhausted subject (entry
|
|
582
|
+
# already `bot`) from a per-clause-disjoint one (entry concrete, this clause disjoint). `on_enter`-only (no
|
|
583
|
+
# recursion) so no condition sub-expression is newly typed; `propagate` preserves the entry because it already
|
|
584
|
+
# keys the node.
|
|
644
585
|
record_clause_entry_scope(branch, falsey_scope)
|
|
645
586
|
body_scope, falsey_scope = branch_body_and_falsey_scopes(subject, branch, falsey_scope)
|
|
646
587
|
results << sub_eval(branch, body_scope)
|
|
@@ -648,10 +589,9 @@ module Rigor
|
|
|
648
589
|
[results, falsey_scope]
|
|
649
590
|
end
|
|
650
591
|
|
|
651
|
-
# ADR-47 WD2/WD3 — record the scope ENTERING a `when`/`in` clause on
|
|
652
|
-
#
|
|
653
|
-
#
|
|
654
|
-
# sub-expression is newly typed; `propagate` preserves it.
|
|
592
|
+
# ADR-47 WD2/WD3 — record the scope ENTERING a `when`/`in` clause on the node `flow.unreachable-clause` reads to
|
|
593
|
+
# classify a dead clause (`when`: first condition; `in`: the pattern). `on_enter`-only so no sub-expression is
|
|
594
|
+
# newly typed; `propagate` preserves it.
|
|
655
595
|
def record_clause_entry_scope(branch, entry_scope)
|
|
656
596
|
node =
|
|
657
597
|
case branch
|
|
@@ -661,11 +601,9 @@ module Rigor
|
|
|
661
601
|
@on_enter&.call(node, entry_scope) if node
|
|
662
602
|
end
|
|
663
603
|
|
|
664
|
-
# Returns `[body_scope, updated_falsey_scope]` for a single branch.
|
|
665
|
-
# `
|
|
666
|
-
# `
|
|
667
|
-
# (`in C` / `in C => x`, pure `is_a?`); every other pattern keeps
|
|
668
|
-
# the conservative "body = entry + bindings, falsey unchanged" shape.
|
|
604
|
+
# Returns `[body_scope, updated_falsey_scope]` for a single branch. `WhenNode` branches narrow through
|
|
605
|
+
# `Narrowing.case_when_scopes`. `InNode` branches narrow soundly only for a bare class pattern (`in C` / `in C =>
|
|
606
|
+
# x`, pure `is_a?`); every other pattern keeps the conservative "body = entry + bindings, falsey unchanged" shape.
|
|
669
607
|
def branch_body_and_falsey_scopes(subject, branch, falsey_scope)
|
|
670
608
|
if branch.is_a?(Prism::InNode)
|
|
671
609
|
in_branch_body_and_falsey_scopes(subject, branch, falsey_scope)
|
|
@@ -675,13 +613,11 @@ module Rigor
|
|
|
675
613
|
end
|
|
676
614
|
end
|
|
677
615
|
|
|
678
|
-
# ADR-47 WD3a — a bare class pattern matches on `C === subject`, i.e.
|
|
679
|
-
#
|
|
680
|
-
#
|
|
681
|
-
#
|
|
682
|
-
#
|
|
683
|
-
# hash keys, ...), so removing anything from the falsey scope would
|
|
684
|
-
# be unsound — they keep the conservative shape.
|
|
616
|
+
# ADR-47 WD3a — a bare class pattern matches on `C === subject`, i.e. exactly `subject.is_a?(C)` with no
|
|
617
|
+
# deconstruction, so it narrows like `when C`: the body sees the subject narrowed to `C` and the next clause's
|
|
618
|
+
# falsey scope has `C` removed. Other patterns can fail to match even when a class test would pass (deconstruction
|
|
619
|
+
# arity, hash keys, ...), so removing anything from the falsey scope would be unsound — they keep the conservative
|
|
620
|
+
# shape.
|
|
685
621
|
def in_branch_body_and_falsey_scopes(subject, branch, falsey_scope)
|
|
686
622
|
class_node = bare_class_pattern_node(branch.pattern)
|
|
687
623
|
return [apply_in_pattern_bindings(subject, branch.pattern, falsey_scope), falsey_scope] unless class_node
|
|
@@ -690,9 +626,8 @@ module Rigor
|
|
|
690
626
|
[apply_in_pattern_bindings(subject, branch.pattern, truthy_scope), narrowed_falsey]
|
|
691
627
|
end
|
|
692
628
|
|
|
693
|
-
# The class-constant node of a `in C` / `in C => x` pattern (the only
|
|
694
|
-
#
|
|
695
|
-
# that deconstructs, binds, or matches a value.
|
|
629
|
+
# The class-constant node of a `in C` / `in C => x` pattern (the only `in` shapes whose match is pure `is_a?`), or
|
|
630
|
+
# nil for any pattern that deconstructs, binds, or matches a value.
|
|
696
631
|
def bare_class_pattern_node(pattern)
|
|
697
632
|
case pattern
|
|
698
633
|
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
@@ -715,49 +650,35 @@ module Rigor
|
|
|
715
650
|
sub_eval(node.statements, scope)
|
|
716
651
|
end
|
|
717
652
|
|
|
718
|
-
# `begin; body; rescue ...; else; ensure; end`. The body and the
|
|
719
|
-
#
|
|
720
|
-
#
|
|
721
|
-
#
|
|
722
|
-
# body raises no exception). The ensure-clause runs but does not
|
|
723
|
-
# contribute to the value; its scope effects are layered on the
|
|
724
|
-
# joined exit scope so locals bound exclusively in `ensure` stay
|
|
653
|
+
# `begin; body; rescue ...; else; ensure; end`. The body and the rescue chain are alternative exit paths whose
|
|
654
|
+
# scopes are joined with nil-injection. The else-clause replaces the body's value when present (matching Ruby
|
|
655
|
+
# semantics: else runs only if the body raises no exception). The ensure-clause runs but does not contribute to
|
|
656
|
+
# the value; its scope effects are layered on the joined exit scope so locals bound exclusively in `ensure` stay
|
|
725
657
|
# observable.
|
|
726
658
|
def eval_begin(node)
|
|
727
659
|
entry = scope
|
|
728
660
|
primary_type, primary_scope = eval_begin_primary_under(node, entry)
|
|
729
661
|
rescue_chain = collect_rescue_chain_results(node.rescue_clause, entry)
|
|
730
662
|
|
|
731
|
-
# B2.1 — retry-edge widening. When any rescue body
|
|
732
|
-
#
|
|
733
|
-
#
|
|
734
|
-
#
|
|
735
|
-
#
|
|
736
|
-
#
|
|
737
|
-
#
|
|
738
|
-
# The fix: widen rebound locals / ivars in any
|
|
739
|
-
# retry-emitting arm to their Nominal envelope (Constant
|
|
740
|
-
# → Nominal[<class>], Tuple → Array, HashShape → Hash),
|
|
741
|
-
# then re-evaluate primary body AND rescue chain once
|
|
742
|
-
# under the widened entry. Nominal envelope is the
|
|
743
|
-
# maximally widened form so the re-evaluation converges
|
|
744
|
-
# in one step.
|
|
663
|
+
# B2.1 — retry-edge widening. When any rescue body contains `Prism::RetryNode`, control re-enters the primary
|
|
664
|
+
# body with the rescue arm's rebinds visible. Today's flow loses that effect, so a counter like `tries = 0; ...;
|
|
665
|
+
# rescue; tries += 1; retry; end` observes `tries: Constant[0]` inside the body and any `tries > 100` predicate
|
|
666
|
+
# folds to always-falsey. The fix: widen rebound locals / ivars in any retry-emitting arm to their Nominal
|
|
667
|
+
# envelope (Constant → Nominal[<class>], Tuple → Array, HashShape → Hash), then re-evaluate primary body AND
|
|
668
|
+
# rescue chain once under the widened entry. Nominal envelope is the maximally widened form so the re-evaluation
|
|
669
|
+
# converges in one step.
|
|
745
670
|
widened_entry = widen_entry_for_retry(entry, rescue_chain)
|
|
746
671
|
if widened_entry
|
|
747
672
|
primary_type, primary_scope = eval_begin_primary_under(node, widened_entry)
|
|
748
673
|
rescue_chain = collect_rescue_chain_results(node.rescue_clause, widened_entry)
|
|
749
674
|
end
|
|
750
675
|
|
|
751
|
-
# Rescue arms whose body unconditionally exits (`return`,
|
|
752
|
-
# `
|
|
753
|
-
# `
|
|
754
|
-
#
|
|
755
|
-
#
|
|
756
|
-
#
|
|
757
|
-
# filter, a `rescue ... return` on a local bound only in
|
|
758
|
-
# the primary body nil-injects that local across the
|
|
759
|
-
# join, defeating the rescue arm's whole point of guaranteeing
|
|
760
|
-
# the primary local is in scope for downstream statements.
|
|
676
|
+
# Rescue arms whose body unconditionally exits (`return`, `next`, `break`, `raise`, `throw`, `exit`, `abort`,
|
|
677
|
+
# `fail`) contribute neither a type fragment NOR a scope to the post-begin flow — control left the `begin` via
|
|
678
|
+
# that arm. Mirrors the `eval_if` / `eval_unless` / `eval_and_or` early-return narrowing. Without this filter, a
|
|
679
|
+
# `rescue ... return` on a local bound only in the primary body nil-injects that local across the join,
|
|
680
|
+
# defeating the rescue arm's whole point of guaranteeing the primary local is in scope for downstream
|
|
681
|
+
# statements.
|
|
761
682
|
live_rescues = rescue_chain.reject { |_pair, arm_node| branch_unconditionally_exits?(arm_node.statements) }
|
|
762
683
|
.map(&:first)
|
|
763
684
|
|
|
@@ -777,11 +698,9 @@ module Rigor
|
|
|
777
698
|
[exit_type, exit_scope]
|
|
778
699
|
end
|
|
779
700
|
|
|
780
|
-
# `BeginNode#statements` is the primary body; when an else-clause
|
|
781
|
-
#
|
|
782
|
-
#
|
|
783
|
-
# body's scope effects still apply because the body did run
|
|
784
|
-
# before the else.
|
|
701
|
+
# `BeginNode#statements` is the primary body; when an else-clause is present, its value replaces the body's per
|
|
702
|
+
# Ruby semantics (the else runs only when no exception was raised), but the body's scope effects still apply
|
|
703
|
+
# because the body did run before the else.
|
|
785
704
|
def eval_begin_primary_under(node, entry_scope)
|
|
786
705
|
body_type, body_scope =
|
|
787
706
|
if node.statements
|
|
@@ -798,17 +717,12 @@ module Rigor
|
|
|
798
717
|
end
|
|
799
718
|
end
|
|
800
719
|
|
|
801
|
-
# B2.1 — return a widened entry scope when at least one
|
|
802
|
-
#
|
|
803
|
-
#
|
|
804
|
-
# to the original entry. Returns nil when no widening is
|
|
805
|
-
# needed (no retry, or no rebinds reachable across the
|
|
806
|
-
# retry edge).
|
|
720
|
+
# B2.1 — return a widened entry scope when at least one rescue arm in `rescue_chain` contains a `Prism::RetryNode`
|
|
721
|
+
# AND that arm rebinds at least one local or ivar relative to the original entry. Returns nil when no widening is
|
|
722
|
+
# needed (no retry, or no rebinds reachable across the retry edge).
|
|
807
723
|
#
|
|
808
|
-
# Always-safe: the widening can only LOSE precision; it
|
|
809
|
-
#
|
|
810
|
-
# the Constant / shape carrier it widens from). Convergent
|
|
811
|
-
# in one step because Nominal envelope is the maximally
|
|
724
|
+
# Always-safe: the widening can only LOSE precision; it never invents a fact (Nominal envelope is a superset of
|
|
725
|
+
# the Constant / shape carrier it widens from). Convergent in one step because Nominal envelope is the maximally
|
|
812
726
|
# widened form against the engine's current carrier set.
|
|
813
727
|
def widen_entry_for_retry(entry_scope, rescue_chain)
|
|
814
728
|
widened = nil
|
|
@@ -827,9 +741,8 @@ module Rigor
|
|
|
827
741
|
def arm_contains_retry?(node)
|
|
828
742
|
return false unless node.is_a?(Prism::Node)
|
|
829
743
|
return true if node.is_a?(Prism::RetryNode)
|
|
830
|
-
# Don't descend into nested blocks / defs / classes /
|
|
831
|
-
#
|
|
832
|
-
# block targets its own enclosing `begin`, not this one.
|
|
744
|
+
# Don't descend into nested blocks / defs / classes / modules — a `retry` inside a nested method body or block
|
|
745
|
+
# targets its own enclosing `begin`, not this one.
|
|
833
746
|
return false if node.is_a?(Prism::DefNode) ||
|
|
834
747
|
node.is_a?(Prism::ClassNode) ||
|
|
835
748
|
node.is_a?(Prism::ModuleNode) ||
|
|
@@ -840,8 +753,7 @@ module Rigor
|
|
|
840
753
|
|
|
841
754
|
def absorb_retry_rebinds(accumulator, entry_scope, arm_post_scope)
|
|
842
755
|
scope_acc = accumulator
|
|
843
|
-
# Walk every local visible in either side, compare types,
|
|
844
|
-
# widen to Nominal envelope on a difference.
|
|
756
|
+
# Walk every local visible in either side, compare types, widen to Nominal envelope on a difference.
|
|
845
757
|
local_keys = arm_post_scope.locals.keys | entry_scope.locals.keys
|
|
846
758
|
local_keys.each do |name|
|
|
847
759
|
pre = entry_scope.local(name)
|
|
@@ -864,8 +776,7 @@ module Rigor
|
|
|
864
776
|
end
|
|
865
777
|
|
|
866
778
|
def retry_widened_type(pre, post)
|
|
867
|
-
# `pre` is nil when the local was introduced inside the
|
|
868
|
-
# rescue body. The retry edge brings it back into the
|
|
779
|
+
# `pre` is nil when the local was introduced inside the rescue body. The retry edge brings it back into the
|
|
869
780
|
# primary body's entry — widen the post type itself.
|
|
870
781
|
envelope = nominal_envelope_for(post)
|
|
871
782
|
return envelope if pre.nil?
|
|
@@ -873,11 +784,9 @@ module Rigor
|
|
|
873
784
|
nominal_envelope_for(Type::Combinator.union(pre, envelope))
|
|
874
785
|
end
|
|
875
786
|
|
|
876
|
-
# Nominal envelope of a value type: widens Constant /
|
|
877
|
-
#
|
|
878
|
-
#
|
|
879
|
-
# `Union` of non-shape members, `Top`, `Dynamic`, `Bot`).
|
|
880
|
-
# Union members are walked individually.
|
|
787
|
+
# Nominal envelope of a value type: widens Constant / Tuple / HashShape carriers to the underlying class's
|
|
788
|
+
# `Nominal`, preserving everything else (`Nominal`, `Union` of non-shape members, `Top`, `Dynamic`, `Bot`). Union
|
|
789
|
+
# members are walked individually.
|
|
881
790
|
def nominal_envelope_for(type)
|
|
882
791
|
members = type.is_a?(Type::Union) ? type.members : [type]
|
|
883
792
|
widened = members.map { |m| nominal_envelope_member(m) }
|
|
@@ -916,92 +825,75 @@ module Rigor
|
|
|
916
825
|
eval_branch_or_nil(node.statements, scope)
|
|
917
826
|
end
|
|
918
827
|
|
|
919
|
-
# `while pred; body; end` / `until pred; body; end`. The body
|
|
920
|
-
#
|
|
921
|
-
#
|
|
922
|
-
# types as `Constant[nil]` (Slice 3 phase 1), reflecting the
|
|
923
|
-
# common case where no `break VALUE` is observed.
|
|
828
|
+
# `while pred; body; end` / `until pred; body; end`. The body might run zero or more times, so half-bound names
|
|
829
|
+
# degrade to `T | nil` in the post-loop scope. The loop expression itself types as `Constant[nil]` (Slice 3 phase
|
|
830
|
+
# 1), reflecting the common case where no `break VALUE` is observed.
|
|
924
831
|
def eval_loop(node)
|
|
925
832
|
_pred_type, post_pred = sub_eval(node.predicate, scope)
|
|
926
833
|
return [Type::Combinator.constant_of(nil), narrow_loop_exit_edge(node, post_pred)] if node.statements.nil?
|
|
927
834
|
|
|
928
|
-
# The historical single body pass joined with the pre-loop scope.
|
|
929
|
-
#
|
|
930
|
-
#
|
|
931
|
-
# widens `buf`'s Tuple), body-introduced locals' nil-injection, and
|
|
932
|
-
# the loop value itself. The fixpoint then OVERLAYS only the
|
|
835
|
+
# The historical single body pass joined with the pre-loop scope. This continues to carry everything the
|
|
836
|
+
# fixpoint does NOT track: receiver-mutation widening of non-rebound locals (`buf.push(i)` widens `buf`'s
|
|
837
|
+
# Tuple), body-introduced locals' nil-injection, and the loop value itself. The fixpoint then OVERLAYS only the
|
|
933
838
|
# rebound-local bindings it corrects.
|
|
934
839
|
#
|
|
935
|
-
# The pass runs under a break sink so a `break`-path binding
|
|
936
|
-
#
|
|
937
|
-
# collected for the continuation join below.
|
|
840
|
+
# The pass runs under a break sink so a `break`-path binding (`flag = true; break`) the fall-through
|
|
841
|
+
# `body_scope` drops is collected for the continuation join below.
|
|
938
842
|
break_targets, break_sink, body_scope = capture_loop_body_breaks(node.statements, post_pred)
|
|
939
843
|
base_scope = join_with_nil_injection(post_pred, body_scope)
|
|
940
844
|
|
|
941
845
|
rebound, body_first = loop_body_local_writes(node.statements, post_pred)
|
|
942
846
|
names = rebound + body_first
|
|
943
847
|
|
|
944
|
-
# Fast path: a loop whose body rebinds no local skips the rebind
|
|
945
|
-
#
|
|
946
|
-
#
|
|
947
|
-
# x`), so apply it to the single-pass join before returning.
|
|
848
|
+
# Fast path: a loop whose body rebinds no local skips the rebind fixpoint, but still needs the slice-C content
|
|
849
|
+
# writeback (a loop may content-mutate a collection without rebinding any local — `acc << x`), so apply it to
|
|
850
|
+
# the single-pass join before returning.
|
|
948
851
|
if names.empty?
|
|
949
852
|
fast = loop_content_writeback(node.statements, base_scope)
|
|
950
853
|
return [Type::Combinator.constant_of(nil), narrow_loop_exit_edge(node, fast)]
|
|
951
854
|
end
|
|
952
855
|
|
|
953
856
|
post_loop = converged_loop_scope(node, post_pred, base_scope, names, body_first)
|
|
954
|
-
# Recover `break`-path bindings the fall-through dropped (`flag = true;
|
|
955
|
-
#
|
|
857
|
+
# Recover `break`-path bindings the fall-through dropped (`flag = true; break` -> `flag` is `false | true`, not
|
|
858
|
+
# the stale `false`).
|
|
956
859
|
post_loop = join_break_scopes(post_loop, break_sink, break_targets, names)
|
|
957
860
|
post_loop = narrow_loop_exit_edge(node, post_loop)
|
|
958
861
|
[Type::Combinator.constant_of(nil), post_loop]
|
|
959
862
|
end
|
|
960
863
|
|
|
961
|
-
# The continuation scope for a loop whose body rebinds locals: the
|
|
962
|
-
#
|
|
963
|
-
# slice-C receiver-content writeback.
|
|
864
|
+
# The continuation scope for a loop whose body rebinds locals: the ADR-56 slice-B rebind fixpoint overlaid on
|
|
865
|
+
# `base_scope`, then the slice-C receiver-content writeback.
|
|
964
866
|
def converged_loop_scope(node, post_pred, base_scope, names, body_first)
|
|
965
|
-
# ADR-56 slice B — loop-body fixpoint. The body runs 0..N times and
|
|
966
|
-
#
|
|
967
|
-
#
|
|
968
|
-
#
|
|
969
|
-
#
|
|
970
|
-
#
|
|
971
|
-
# a pre-existing local seeds with its post-predicate binding; a local
|
|
972
|
-
# FIRST assigned inside the body seeds with `nil` so the 0-iteration
|
|
973
|
-
# path degrades it to `T | nil`, matching the nil-injection treatment.
|
|
867
|
+
# ADR-56 slice B — loop-body fixpoint. The body runs 0..N times and may compound (`d *= 2`), so the historical
|
|
868
|
+
# single body pass joined with the pre-loop scope kept stale folded constants (`d = 1; while …; d *= 2; end` →
|
|
869
|
+
# `1 | 2`, never reaching `4, 8`). Fold each body-written local's continuation binding through the same capped
|
|
870
|
+
# fixpoint slice A uses for non-escaping block captures. Seed: a pre-existing local seeds with its
|
|
871
|
+
# post-predicate binding; a local FIRST assigned inside the body seeds with `nil` so the 0-iteration path
|
|
872
|
+
# degrades it to `T | nil`, matching the nil-injection treatment.
|
|
974
873
|
result = loop_rebind_fixpoint(node, post_pred, names, body_first)
|
|
975
|
-
# Display-path re-record: the fixpoint's body re-evaluations fire
|
|
976
|
-
#
|
|
977
|
-
#
|
|
978
|
-
# stale pre-convergence constants. One extra pass from the converged
|
|
979
|
-
# bindings (result discarded) re-records the body's entry scopes.
|
|
874
|
+
# Display-path re-record: the fixpoint's body re-evaluations fire `on_enter` with the cap-N INTERMEDIATE
|
|
875
|
+
# assumptions, so the last-visit-wins scope index would annotate loop-body lines with stale pre-convergence
|
|
876
|
+
# constants. One extra pass from the converged bindings (result discarded) re-records the body's entry scopes.
|
|
980
877
|
record_converged_loop_body(node, post_pred, result, names, body_first)
|
|
981
878
|
post_loop = result.reduce(base_scope) { |acc, (name, type)| acc.with_local(name, type) }
|
|
982
|
-
# ADR-56 slice C — loop-body receiver-content element-type join. A loop
|
|
983
|
-
#
|
|
984
|
-
#
|
|
985
|
-
#
|
|
986
|
-
# `post_loop` so a local both rebound and content-mutated composes).
|
|
879
|
+
# ADR-56 slice C — loop-body receiver-content element-type join. A loop that content-mutates a collection (`acc
|
|
880
|
+
# << n`) keeps only the seed's element types after the single-pass widen; join the appended/stored types into
|
|
881
|
+
# the continuation collection (pre-state read from `post_loop` so a local both rebound and content-mutated
|
|
882
|
+
# composes).
|
|
987
883
|
loop_content_writeback(node.statements, post_loop)
|
|
988
884
|
end
|
|
989
885
|
|
|
990
|
-
# Item 4 — loop-exit predicate narrowing. A `while pred` / `until pred`
|
|
991
|
-
#
|
|
992
|
-
#
|
|
993
|
-
#
|
|
994
|
-
# io.gets; …; end; line.foo` reads `line` non-nil because the loop ran
|
|
995
|
-
# until `gets` returned a truthy (non-nil) line. Apply the exit edge of
|
|
886
|
+
# Item 4 — loop-exit predicate narrowing. A `while pred` / `until pred` loop exits PRECISELY on the predicate's
|
|
887
|
+
# exit edge: `while` exits when `pred` is falsey, `until` when `pred` is truthy. So after the loop the
|
|
888
|
+
# predicate-assignment target carries the exit polarity — `until line = io.gets; …; end; line.foo` reads `line`
|
|
889
|
+
# non-nil because the loop ran until `gets` returned a truthy (non-nil) line. Apply the exit edge of
|
|
996
890
|
# `Narrowing.predicate_scopes` to the continuation scope.
|
|
997
891
|
#
|
|
998
|
-
# Guarded against `break`: a `break` exits the loop WITHOUT the predicate
|
|
999
|
-
#
|
|
1000
|
-
#
|
|
1001
|
-
#
|
|
1002
|
-
#
|
|
1003
|
-
# nested-loop `break` is rare in predicate-assignment loops and the
|
|
1004
|
-
# conservative bail only costs precision, never soundness.
|
|
892
|
+
# Guarded against `break`: a `break` exits the loop WITHOUT the predicate ever going false (`while line = gets;
|
|
893
|
+
# break if done; end` can leave `line` truthy on a `while`, or exit before the `until` predicate fires), so the
|
|
894
|
+
# exit-edge proof does not hold and the loop is left un-narrowed. `break` inside a NESTED loop/block does not
|
|
895
|
+
# target this loop, but a nested-loop `break` is rare in predicate-assignment loops and the conservative bail only
|
|
896
|
+
# costs precision, never soundness.
|
|
1005
897
|
def narrow_loop_exit_edge(node, post_loop)
|
|
1006
898
|
return post_loop if loop_body_breaks?(node.statements)
|
|
1007
899
|
|
|
@@ -1009,10 +901,9 @@ module Rigor
|
|
|
1009
901
|
node.is_a?(Prism::UntilNode) ? truthy_scope : falsey_scope
|
|
1010
902
|
end
|
|
1011
903
|
|
|
1012
|
-
# True when the loop body can `break` out of THIS loop. Conservatively
|
|
1013
|
-
#
|
|
1014
|
-
#
|
|
1015
|
-
# but bailing is precision-only).
|
|
904
|
+
# True when the loop body can `break` out of THIS loop. Conservatively treats any `BreakNode` under the body as a
|
|
905
|
+
# break for this loop (a break inside a nested loop/block actually targets the inner construct, but bailing is
|
|
906
|
+
# precision-only).
|
|
1016
907
|
def loop_body_breaks?(statements)
|
|
1017
908
|
return false if statements.nil?
|
|
1018
909
|
|
|
@@ -1023,10 +914,8 @@ module Rigor
|
|
|
1023
914
|
found
|
|
1024
915
|
end
|
|
1025
916
|
|
|
1026
|
-
# A `break` inside one of these nested constructs targets the inner
|
|
1027
|
-
#
|
|
1028
|
-
# lexical loop — so the directly-targeting break scan does not descend
|
|
1029
|
-
# into them.
|
|
917
|
+
# A `break` inside one of these nested constructs targets the inner construct (an inner loop, a block's method, a
|
|
918
|
+
# nested def), NOT the lexical loop — so the directly-targeting break scan does not descend into them.
|
|
1030
919
|
BREAK_BOUNDARY_NODES = [
|
|
1031
920
|
Prism::ForNode, Prism::WhileNode, Prism::UntilNode,
|
|
1032
921
|
Prism::BlockNode, Prism::LambdaNode, Prism::DefNode,
|
|
@@ -1034,11 +923,9 @@ module Rigor
|
|
|
1034
923
|
].freeze
|
|
1035
924
|
private_constant :BREAK_BOUNDARY_NODES
|
|
1036
925
|
|
|
1037
|
-
# The `BreakNode`s that lexically target THIS loop — reachable from the
|
|
1038
|
-
#
|
|
1039
|
-
#
|
|
1040
|
-
# break scopes (the thread-local sink also collects breaks from nested
|
|
1041
|
-
# blocks that did not install their own sink).
|
|
926
|
+
# The `BreakNode`s that lexically target THIS loop — reachable from the body without crossing a nested loop /
|
|
927
|
+
# block / def boundary. An identity-keyed Hash used as a membership set to filter the collected break scopes (the
|
|
928
|
+
# thread-local sink also collects breaks from nested blocks that did not install their own sink).
|
|
1042
929
|
def directly_targeting_breaks(statements)
|
|
1043
930
|
found = {}.compare_by_identity
|
|
1044
931
|
collect_direct_breaks(statements, found)
|
|
@@ -1056,10 +943,9 @@ module Rigor
|
|
|
1056
943
|
end
|
|
1057
944
|
end
|
|
1058
945
|
|
|
1059
|
-
# Installs a fresh thread-local break sink around `yield` (a loop-body
|
|
1060
|
-
#
|
|
1061
|
-
#
|
|
1062
|
-
# leak to the enclosing loop.
|
|
946
|
+
# Installs a fresh thread-local break sink around `yield` (a loop-body evaluation), returning `[collected,
|
|
947
|
+
# yield_result]`. Stacks: the previous sink is restored on exit so a nested loop's breaks do not leak to the
|
|
948
|
+
# enclosing loop.
|
|
1063
949
|
def collect_break_scopes
|
|
1064
950
|
previous = Thread.current[BREAK_SINK_KEY]
|
|
1065
951
|
sink = []
|
|
@@ -1072,22 +958,19 @@ module Rigor
|
|
|
1072
958
|
[sink, result]
|
|
1073
959
|
end
|
|
1074
960
|
|
|
1075
|
-
# Runs a loop body's single pass under a break sink. Returns the
|
|
1076
|
-
#
|
|
1077
|
-
#
|
|
1078
|
-
# {#join_break_scopes} needs. Shared by `eval_loop` and `eval_for`.
|
|
961
|
+
# Runs a loop body's single pass under a break sink. Returns the directly-targeting break set, the collected break
|
|
962
|
+
# scopes, and the fall-through body scope — the three inputs the continuation's {#join_break_scopes} needs. Shared
|
|
963
|
+
# by `eval_loop` and `eval_for`.
|
|
1079
964
|
def capture_loop_body_breaks(statements, entry)
|
|
1080
965
|
targets = directly_targeting_breaks(statements)
|
|
1081
966
|
sink, (_type, body_scope) = collect_break_scopes { sub_eval(statements, entry) }
|
|
1082
967
|
[targets, sink, body_scope]
|
|
1083
968
|
end
|
|
1084
969
|
|
|
1085
|
-
# Joins each directly-targeting break's body-written local bindings into
|
|
1086
|
-
# the
|
|
1087
|
-
#
|
|
1088
|
-
#
|
|
1089
|
-
# unions to itself; a break-only-written local is already present via the
|
|
1090
|
-
# fixpoint / nil-injection seed, so the union reflects its break value.
|
|
970
|
+
# Joins each directly-targeting break's body-written local bindings into the loop continuation, so a `break`-path
|
|
971
|
+
# binding the fall-through dropped is recovered (`flag = true; break` -> `flag` becomes `false | true`). Only
|
|
972
|
+
# loop-body-written names are joined — an unchanged local unions to itself; a break-only-written local is already
|
|
973
|
+
# present via the fixpoint / nil-injection seed, so the union reflects its break value.
|
|
1091
974
|
def join_break_scopes(continuation, sink, targeting, names)
|
|
1092
975
|
return continuation if sink.empty? || names.empty?
|
|
1093
976
|
|
|
@@ -1104,14 +987,11 @@ module Rigor
|
|
|
1104
987
|
end
|
|
1105
988
|
end
|
|
1106
989
|
|
|
1107
|
-
# Joins loop-body content mutations into the continuation collection
|
|
1108
|
-
#
|
|
1109
|
-
#
|
|
1110
|
-
#
|
|
1111
|
-
#
|
|
1112
|
-
# would be captured, an unsound under-approximation). Pre-state is
|
|
1113
|
-
# read from `post_loop` too. A loop body shares the surrounding scope,
|
|
1114
|
-
# so the receiver is any `LocalVariableReadNode` (no depth filter).
|
|
990
|
+
# Joins loop-body content mutations into the continuation collection bindings. The mutator arguments are typed
|
|
991
|
+
# against `post_loop`, whose locals already carry the loop-body fixpoint widening (so an appended `n` that the
|
|
992
|
+
# loop decrements types `Integer`, not its entry `Constant[3]` — otherwise only the first iteration's value would
|
|
993
|
+
# be captured, an unsound under-approximation). Pre-state is read from `post_loop` too. A loop body shares the
|
|
994
|
+
# surrounding scope, so the receiver is any `LocalVariableReadNode` (no depth filter).
|
|
1115
995
|
def loop_content_writeback(statements, post_loop)
|
|
1116
996
|
return post_loop if statements.nil?
|
|
1117
997
|
|
|
@@ -1128,12 +1008,9 @@ module Rigor
|
|
|
1128
1008
|
end
|
|
1129
1009
|
end
|
|
1130
1010
|
|
|
1131
|
-
# Re-evaluates the loop body once from the converged fixpoint
|
|
1132
|
-
#
|
|
1133
|
-
# the body
|
|
1134
|
-
# display-path-only `converged_loop_recording` flag so the check
|
|
1135
|
-
# path neither pays the extra body evaluation nor risks any
|
|
1136
|
-
# diagnostic drift.
|
|
1011
|
+
# Re-evaluates the loop body once from the converged fixpoint bindings, solely for the `on_enter` side effect of
|
|
1012
|
+
# re-recording the body's per-node entry scopes. Gated behind the display-path-only `converged_loop_recording`
|
|
1013
|
+
# flag so the check path neither pays the extra body evaluation nor risks any diagnostic drift.
|
|
1137
1014
|
def record_converged_loop_body(node, post_pred, bindings, names, body_first)
|
|
1138
1015
|
return unless @converged_loop_recording && @on_enter
|
|
1139
1016
|
|
|
@@ -1141,11 +1018,10 @@ module Rigor
|
|
|
1141
1018
|
nil
|
|
1142
1019
|
end
|
|
1143
1020
|
|
|
1144
|
-
# Runs the slice-B loop-body rebind fixpoint, returning the per-name
|
|
1145
|
-
#
|
|
1146
|
-
#
|
|
1147
|
-
#
|
|
1148
|
-
# it to `T | nil`, matching the historical nil-injection treatment.
|
|
1021
|
+
# Runs the slice-B loop-body rebind fixpoint, returning the per-name continuation binding. Seed: a pre-existing
|
|
1022
|
+
# local seeds with its post-predicate binding; a local FIRST assigned inside the body seeds with `nil` so the
|
|
1023
|
+
# 0-iteration path (the body may never run) degrades it to `T | nil`, matching the historical nil-injection
|
|
1024
|
+
# treatment.
|
|
1149
1025
|
def loop_rebind_fixpoint(node, post_pred, names, body_first)
|
|
1150
1026
|
nil_const = Type::Combinator.constant_of(nil)
|
|
1151
1027
|
seed = names.to_h { |name| [name, post_pred.local(name) || nil_const] }
|
|
@@ -1157,13 +1033,10 @@ module Rigor
|
|
|
1157
1033
|
)
|
|
1158
1034
|
end
|
|
1159
1035
|
|
|
1160
|
-
# Names of locals the loop body can rebind, partitioned into those
|
|
1161
|
-
#
|
|
1162
|
-
#
|
|
1163
|
-
#
|
|
1164
|
-
# introduces no new binding scope — every write leaks to the
|
|
1165
|
-
# surrounding scope — so unlike a block there is no introduced-name
|
|
1166
|
-
# filter; every local-write form under the body node counts.
|
|
1036
|
+
# Names of locals the loop body can rebind, partitioned into those already bound in `base_scope` (their pre-loop
|
|
1037
|
+
# binding seeds the fixpoint) and those FIRST assigned inside the body (no pre-state, so they seed with `nil` for
|
|
1038
|
+
# 0-iteration soundness). A loop body introduces no new binding scope — every write leaks to the surrounding scope
|
|
1039
|
+
# — so unlike a block there is no introduced-name filter; every local-write form under the body node counts.
|
|
1167
1040
|
def loop_body_local_writes(statements, base_scope)
|
|
1168
1041
|
pre_existing = []
|
|
1169
1042
|
body_first = []
|
|
@@ -1180,30 +1053,22 @@ module Rigor
|
|
|
1180
1053
|
[pre_existing.uniq, body_first.uniq - pre_existing.uniq]
|
|
1181
1054
|
end
|
|
1182
1055
|
|
|
1183
|
-
# Evaluates the loop body once with each fixpoint-tracked local bound
|
|
1184
|
-
#
|
|
1185
|
-
# binding. Used as the {BodyFixpoint} body-evaluator for `eval_loop`.
|
|
1056
|
+
# Evaluates the loop body once with each fixpoint-tracked local bound to the supplied running assumption and
|
|
1057
|
+
# returns the per-name exit binding. Used as the {BodyFixpoint} body-evaluator for `eval_loop`.
|
|
1186
1058
|
#
|
|
1187
|
-
# The body runs from `post_pred` overlaid with the assumptions, then
|
|
1188
|
-
#
|
|
1189
|
-
#
|
|
1190
|
-
#
|
|
1191
|
-
#
|
|
1192
|
-
#
|
|
1193
|
-
#
|
|
1194
|
-
# and false-fire `possible nil receiver` on the guarded re-read. The
|
|
1195
|
-
# historical single body pass (which seeds these locals from their
|
|
1196
|
-
# never-nil pre-loop binding) did not need this; the fixpoint, which
|
|
1197
|
-
# feeds the widened assumption back in, does.
|
|
1059
|
+
# The body runs from `post_pred` overlaid with the assumptions, then narrowed by the predicate's loop-entry edge:
|
|
1060
|
+
# a `while` body only runs when the predicate is TRUTHY, an `until` body only when it is FALSEY. Re-applying that
|
|
1061
|
+
# narrowing per iteration keeps loop-carried narrowing sound — without it, an accumulator whose rebind can
|
|
1062
|
+
# introduce `nil` (`prefix = idx ? prefix[0, idx] : nil` under `while prefix && …`) would re-enter the body with
|
|
1063
|
+
# `nil` un-narrowed and false-fire `possible nil receiver` on the guarded re-read. The historical single body pass
|
|
1064
|
+
# (which seeds these locals from their never-nil pre-loop binding) did not need this; the fixpoint, which feeds
|
|
1065
|
+
# the widened assumption back in, does.
|
|
1198
1066
|
#
|
|
1199
|
-
# A body-FIRST local (no pre-loop binding) is deliberately NOT overlaid
|
|
1200
|
-
#
|
|
1201
|
-
#
|
|
1202
|
-
#
|
|
1203
|
-
#
|
|
1204
|
-
# back into the body re-evaluation would leak it past a condition-form
|
|
1205
|
-
# assignment the engine does not thread into the branch (`if exps.size
|
|
1206
|
-
# > (count = 3)`), false-firing `+`/nil-receiver on the guarded use.
|
|
1067
|
+
# A body-FIRST local (no pre-loop binding) is deliberately NOT overlaid into the body-entry scope: when the body
|
|
1068
|
+
# runs it assigns the local before any use, exactly as the historical single body pass saw it. Its `nil` seed
|
|
1069
|
+
# exists only to model the 0-iteration path and is kept as a join constituent by {BodyFixpoint#converge}; feeding
|
|
1070
|
+
# that `nil` back into the body re-evaluation would leak it past a condition-form assignment the engine does not
|
|
1071
|
+
# thread into the branch (`if exps.size > (count = 3)`), false-firing `+`/nil-receiver on the guarded use.
|
|
1207
1072
|
def loop_body_exit_bindings(node, post_pred, bindings, names, body_first)
|
|
1208
1073
|
overlaid = bindings.except(*body_first)
|
|
1209
1074
|
entry = overlaid.reduce(post_pred) { |acc, (name, type)| acc.with_local(name, type) }
|
|
@@ -1213,16 +1078,12 @@ module Rigor
|
|
|
1213
1078
|
names.to_h { |name| [name, exit_scope.local(name)] }
|
|
1214
1079
|
end
|
|
1215
1080
|
|
|
1216
|
-
# `for index in collection; body; end`. Unlike `each {}` blocks,
|
|
1217
|
-
#
|
|
1218
|
-
#
|
|
1219
|
-
# scope
|
|
1220
|
-
#
|
|
1221
|
-
#
|
|
1222
|
-
# with half-bound names degraded to `T | nil` via nil-injection.
|
|
1223
|
-
# The loop expression itself types as `Constant[nil]` (the common
|
|
1224
|
-
# case where no `break VALUE` is observed), matching the policy
|
|
1225
|
-
# `eval_loop` uses for `while` / `until`.
|
|
1081
|
+
# `for index in collection; body; end`. Unlike `each {}` blocks, `for` does NOT create a new variable scope: the
|
|
1082
|
+
# index variable AND every local written in the body leak to the surrounding scope. The collection is evaluated
|
|
1083
|
+
# once; the body runs zero or more times, so the post-loop scope is the join of the no-iteration scope (just
|
|
1084
|
+
# `post_collection`) and the body scope, with half-bound names degraded to `T | nil` via nil-injection. The loop
|
|
1085
|
+
# expression itself types as `Constant[nil]` (the common case where no `break VALUE` is observed), matching the
|
|
1086
|
+
# policy `eval_loop` uses for `while` / `until`.
|
|
1226
1087
|
def eval_for(node)
|
|
1227
1088
|
coll_type, post_coll = sub_eval(node.collection, scope)
|
|
1228
1089
|
element_type = for_iteration_element_type(coll_type)
|
|
@@ -1232,10 +1093,9 @@ module Rigor
|
|
|
1232
1093
|
return [Type::Combinator.constant_of(nil), join_with_nil_injection(post_coll, body_entry)]
|
|
1233
1094
|
end
|
|
1234
1095
|
|
|
1235
|
-
# Run the body pass under a break sink so a `break`-path binding the
|
|
1236
|
-
#
|
|
1237
|
-
#
|
|
1238
|
-
# single-pass join is the only continuation).
|
|
1096
|
+
# Run the body pass under a break sink so a `break`-path binding the fall-through drops is recovered into the
|
|
1097
|
+
# continuation (the `for` sibling of `eval_loop`'s break join; `for` has no fixpoint, so the single-pass join is
|
|
1098
|
+
# the only continuation).
|
|
1239
1099
|
break_targets, break_sink, body_scope = capture_loop_body_breaks(node.statements, body_entry)
|
|
1240
1100
|
continuation = join_with_nil_injection(post_coll, body_scope)
|
|
1241
1101
|
pre_existing, body_first = loop_body_local_writes(node.statements, post_coll)
|
|
@@ -1243,15 +1103,11 @@ module Rigor
|
|
|
1243
1103
|
[Type::Combinator.constant_of(nil), continuation]
|
|
1244
1104
|
end
|
|
1245
1105
|
|
|
1246
|
-
# `for x in coll` is semantically `coll.each { |x| ... }`. We
|
|
1247
|
-
#
|
|
1248
|
-
#
|
|
1249
|
-
#
|
|
1250
|
-
#
|
|
1251
|
-
# `Hash[K, V]#each` yields `[K, V]` even when the receiver is
|
|
1252
|
-
# not a literal Hash carrier in our local lattice). When the
|
|
1253
|
-
# dispatcher returns nothing (no signature, unknown receiver)
|
|
1254
|
-
# we fall back to the structural extractor.
|
|
1106
|
+
# `for x in coll` is semantically `coll.each { |x| ... }`. We ask the method dispatcher for `coll.each`'s expected
|
|
1107
|
+
# block parameter types — that path consults RBS and the iterator dispatch table, which is more precise than the
|
|
1108
|
+
# structural `collection_element_type` fallback (it knows, e.g., that `Hash[K, V]#each` yields `[K, V]` even when
|
|
1109
|
+
# the receiver is not a literal Hash carrier in our local lattice). When the dispatcher returns nothing (no
|
|
1110
|
+
# signature, unknown receiver) we fall back to the structural extractor.
|
|
1255
1111
|
def for_iteration_element_type(coll_type)
|
|
1256
1112
|
structural = collection_element_type(coll_type)
|
|
1257
1113
|
return structural unless structural.equal?(Type::Combinator.untyped)
|
|
@@ -1269,11 +1125,9 @@ module Rigor
|
|
|
1269
1125
|
Type::Combinator.untyped
|
|
1270
1126
|
end
|
|
1271
1127
|
|
|
1272
|
-
# Binds the `for` index variable(s) into `scope`. A single
|
|
1273
|
-
# `
|
|
1274
|
-
#
|
|
1275
|
-
# (`for a, b in pairs`) delegates to {MultiTargetBinder}, which
|
|
1276
|
-
# decomposes a tuple-shaped element into the inner slots.
|
|
1128
|
+
# Binds the `for` index variable(s) into `scope`. A single `LocalVariableTargetNode` is bound to `element_type`
|
|
1129
|
+
# (the per-iteration value the collection yields). A `MultiTargetNode` (`for a, b in pairs`) delegates to
|
|
1130
|
+
# {MultiTargetBinder}, which decomposes a tuple-shaped element into the inner slots.
|
|
1277
1131
|
def bind_for_index(index_node, element_type, scope)
|
|
1278
1132
|
case index_node
|
|
1279
1133
|
when Prism::LocalVariableTargetNode
|
|
@@ -1286,13 +1140,10 @@ module Rigor
|
|
|
1286
1140
|
end
|
|
1287
1141
|
end
|
|
1288
1142
|
|
|
1289
|
-
# Extracts the per-iteration element type from a collection
|
|
1290
|
-
#
|
|
1291
|
-
#
|
|
1292
|
-
#
|
|
1293
|
-
# `[key, value]` pairs); `IntegerRange` yields `Integer`;
|
|
1294
|
-
# `Constant<Range>` reads the literal range's element class.
|
|
1295
|
-
# Anything else falls back to `untyped`.
|
|
1143
|
+
# Extracts the per-iteration element type from a collection carrier. `Tuple[T1..Tn]` yields the union of its
|
|
1144
|
+
# elements; `Nominal[Array, [T]]` and `Nominal[Range, [T]]` yield `T`; `Nominal[Hash, [K, V]]` yields `Tuple[K,
|
|
1145
|
+
# V]` (Hash#each yields `[key, value]` pairs); `IntegerRange` yields `Integer`; `Constant<Range>` reads the
|
|
1146
|
+
# literal range's element class. Anything else falls back to `untyped`.
|
|
1296
1147
|
def collection_element_type(type)
|
|
1297
1148
|
case type
|
|
1298
1149
|
when Type::Tuple
|
|
@@ -1337,27 +1188,18 @@ module Rigor
|
|
|
1337
1188
|
end
|
|
1338
1189
|
end
|
|
1339
1190
|
|
|
1340
|
-
# `a && b` / `a || b`. The LHS always runs, the RHS only
|
|
1341
|
-
#
|
|
1342
|
-
# `a
|
|
1343
|
-
#
|
|
1344
|
-
#
|
|
1345
|
-
#
|
|
1346
|
-
# the RHS still degrade to `T | nil`. The result type is
|
|
1347
|
-
# edge-aware: `a && b` can only produce the falsey fragment of
|
|
1348
|
-
# `a` when the RHS is skipped, while `a || b` can only produce
|
|
1349
|
-
# the truthy fragment of `a` when the RHS is skipped.
|
|
1191
|
+
# `a && b` / `a || b`. The LHS always runs, the RHS only sometimes runs. Slice 6 phase 1 narrows the RHS
|
|
1192
|
+
# evaluation: `a && b` evaluates `b` under the truthy edge of `a`, and `a || b` evaluates `b` under the falsey
|
|
1193
|
+
# edge of `a`. The narrowed RHS post-scope is joined with the LHS post-scope (RHS skipped) using nil-injection so
|
|
1194
|
+
# half-bound names from the RHS still degrade to `T | nil`. The result type is edge-aware: `a && b` can only
|
|
1195
|
+
# produce the falsey fragment of `a` when the RHS is skipped, while `a || b` can only produce the truthy fragment
|
|
1196
|
+
# of `a` when the RHS is skipped.
|
|
1350
1197
|
#
|
|
1351
|
-
# When the RHS is a terminating branch — it `raise`s /
|
|
1352
|
-
#
|
|
1353
|
-
#
|
|
1354
|
-
#
|
|
1355
|
-
# `
|
|
1356
|
-
# LHS-skipped edge alone: `a or raise` only survives when `a`
|
|
1357
|
-
# was truthy, so subsequent statements observe `a` narrowed to
|
|
1358
|
-
# its truthy fragment; the symmetric `a and raise` survives
|
|
1359
|
-
# only when `a` was falsey. Same shape as the `eval_if` /
|
|
1360
|
-
# `eval_unless` early-return narrowing.
|
|
1198
|
+
# When the RHS is a terminating branch — it `raise`s / `return`s / `throw`s / `exit`s / `break`s / `next`s, OR its
|
|
1199
|
+
# inferred type is `Bot` (ADR-24 WD6: a divergent helper such as `a or fail_with_message(...)`, recognised via
|
|
1200
|
+
# `branch_terminates?`) — the post-OR / post-AND scope is the LHS-skipped edge alone: `a or raise` only survives
|
|
1201
|
+
# when `a` was truthy, so subsequent statements observe `a` narrowed to its truthy fragment; the symmetric `a and
|
|
1202
|
+
# raise` survives only when `a` was falsey. Same shape as the `eval_if` / `eval_unless` early-return narrowing.
|
|
1361
1203
|
def eval_and_or(node)
|
|
1362
1204
|
left_type, left_scope = sub_eval(node.left, scope)
|
|
1363
1205
|
truthy_left, falsey_left = Narrowing.predicate_scopes(node.left, left_scope)
|
|
@@ -1365,8 +1207,7 @@ module Rigor
|
|
|
1365
1207
|
right_type, right_scope = sub_eval(node.right, rhs_entry)
|
|
1366
1208
|
|
|
1367
1209
|
if branch_terminates?(node.right, right_type)
|
|
1368
|
-
# Control never reaches any statement after `a or raise`
|
|
1369
|
-
# via the RHS edge — the RHS scope is discarded.
|
|
1210
|
+
# Control never reaches any statement after `a or raise` via the RHS edge — the RHS scope is discarded.
|
|
1370
1211
|
surviving_type =
|
|
1371
1212
|
if node.is_a?(Prism::AndNode)
|
|
1372
1213
|
Narrowing.narrow_falsey(left_type)
|
|
@@ -1389,23 +1230,18 @@ module Rigor
|
|
|
1389
1230
|
]
|
|
1390
1231
|
end
|
|
1391
1232
|
|
|
1392
|
-
# `(body)`. Threads scope through the inner expression so
|
|
1393
|
-
# `(x = 1; x + 2)` binds `x` and produces `Constant[3]`.
|
|
1233
|
+
# `(body)`. Threads scope through the inner expression so `(x = 1; x + 2)` binds `x` and produces `Constant[3]`.
|
|
1394
1234
|
def eval_parentheses(node)
|
|
1395
1235
|
return [Type::Combinator.constant_of(nil), scope] if node.body.nil?
|
|
1396
1236
|
|
|
1397
1237
|
sub_eval(node.body, scope)
|
|
1398
1238
|
end
|
|
1399
1239
|
|
|
1400
|
-
# `class Foo; body; end` and `module Foo; body; end`. The class
|
|
1401
|
-
#
|
|
1402
|
-
#
|
|
1403
|
-
#
|
|
1404
|
-
#
|
|
1405
|
-
# definition does not bind any local in the enclosing scope.
|
|
1406
|
-
# The class body's value is the value of its last statement
|
|
1407
|
-
# (`Constant[nil]` for an empty body); we discard the body's
|
|
1408
|
-
# post-scope.
|
|
1240
|
+
# `class Foo; body; end` and `module Foo; body; end`. The class body runs in a fresh scope (Ruby's class scope
|
|
1241
|
+
# does not see the outer locals), and the StatementEvaluator pushes a new `ClassFrame` so nested `def`s know their
|
|
1242
|
+
# lexical owner. The outer scope is unchanged on exit because Ruby's class definition does not bind any local in
|
|
1243
|
+
# the enclosing scope. The class body's value is the value of its last statement (`Constant[nil]` for an empty
|
|
1244
|
+
# body); we discard the body's post-scope.
|
|
1409
1245
|
def eval_class_or_module(node)
|
|
1410
1246
|
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
1411
1247
|
new_context = @class_context + [ClassFrame.new(name: name, singleton: false)]
|
|
@@ -1413,45 +1249,30 @@ module Rigor
|
|
|
1413
1249
|
[body_type, scope]
|
|
1414
1250
|
end
|
|
1415
1251
|
|
|
1416
|
-
# `class << expr; body; end`. When `expr` is `self`, the body
|
|
1417
|
-
#
|
|
1418
|
-
#
|
|
1419
|
-
#
|
|
1420
|
-
# `instance_method`. For non-`self` expressions we cannot
|
|
1421
|
-
# statically resolve the receiver, so we keep the existing
|
|
1422
|
-
# context and accept that nested defs degrade to the
|
|
1423
|
-
# `Dynamic[Top]` default.
|
|
1252
|
+
# `class << expr; body; end`. When `expr` is `self`, the body defines class methods on the immediate enclosing
|
|
1253
|
+
# class — the innermost frame flips to `singleton: true` so a nested `def foo` resolves through `singleton_method`
|
|
1254
|
+
# rather than `instance_method`. For non-`self` expressions we cannot statically resolve the receiver, so we keep
|
|
1255
|
+
# the existing context and accept that nested defs degrade to the `Dynamic[Top]` default.
|
|
1424
1256
|
def eval_singleton_class(node)
|
|
1425
1257
|
new_context = singleton_context_for(node)
|
|
1426
1258
|
body_type, _body_scope = eval_class_body(node, new_context)
|
|
1427
1259
|
[body_type, scope]
|
|
1428
1260
|
end
|
|
1429
1261
|
|
|
1430
|
-
# `def name(params); body; end`. Builds the method-entry scope
|
|
1431
|
-
#
|
|
1432
|
-
# `
|
|
1433
|
-
#
|
|
1434
|
-
# scope is left unchanged: a `def` does not introduce a binding
|
|
1435
|
-
# in its enclosing scope. Ruby evaluates `def` to the method's
|
|
1436
|
-
# name as a Symbol, so the produced type is `Constant[:name]`.
|
|
1262
|
+
# `def name(params); body; end`. Builds the method-entry scope by binding the parameter list (RBS-driven where
|
|
1263
|
+
# available, or `Dynamic[Top]` for the slice 3 phase 2 fallback) into a fresh scope, then evaluates the body under
|
|
1264
|
+
# that scope. The outer scope is left unchanged: a `def` does not introduce a binding in its enclosing scope. Ruby
|
|
1265
|
+
# evaluates `def` to the method's name as a Symbol, so the produced type is `Constant[:name]`.
|
|
1437
1266
|
def eval_def(node)
|
|
1438
1267
|
body_scope = build_method_entry_scope(node)
|
|
1439
|
-
# Parameter default value expressions (e.g. `self.x` in
|
|
1440
|
-
# `def
|
|
1441
|
-
#
|
|
1442
|
-
#
|
|
1443
|
-
#
|
|
1444
|
-
#
|
|
1445
|
-
#
|
|
1446
|
-
#
|
|
1447
|
-
# `self_type` is `singleton(C)`), making `self.foo` look
|
|
1448
|
-
# like a singleton-side call. Observed surfacing 915 false
|
|
1449
|
-
# positives in `prism-1.9.0`'s auto-generated `copy`
|
|
1450
|
-
# methods alone.
|
|
1451
|
-
# A nested `def` is a return barrier: its body's `return`s belong to
|
|
1452
|
-
# the inner method, not the one currently being inferred. Suspend the
|
|
1453
|
-
# return sink across the nested body so `eval_return` does not record
|
|
1454
|
-
# them into the outer method's return type.
|
|
1268
|
+
# Parameter default value expressions (e.g. `self.x` in `def copy(x: self.x)`) execute when the method is
|
|
1269
|
+
# *invoked*, not when the `def` is read; their `self` is the instance receiver, not the surrounding class body.
|
|
1270
|
+
# Walk the parameters subtree under `body_scope` so the scope-index records the instance `self_type` for every
|
|
1271
|
+
# node inside parameter defaults. `propagate` would otherwise drop them to the outer class-body scope (where
|
|
1272
|
+
# `self_type` is `singleton(C)`), making `self.foo` look like a singleton-side call. Observed surfacing 915
|
|
1273
|
+
# false positives in `prism-1.9.0`'s auto-generated `copy` methods alone. A nested `def` is a return barrier:
|
|
1274
|
+
# its body's `return`s belong to the inner method, not the one currently being inferred. Suspend the return sink
|
|
1275
|
+
# across the nested body so `eval_return` does not record them into the outer method's return type.
|
|
1455
1276
|
outer_sink = Thread.current[RETURN_SINK_KEY]
|
|
1456
1277
|
Thread.current[RETURN_SINK_KEY] = nil
|
|
1457
1278
|
begin
|
|
@@ -1463,145 +1284,98 @@ module Rigor
|
|
|
1463
1284
|
[Type::Combinator.constant_of(node.name), scope]
|
|
1464
1285
|
end
|
|
1465
1286
|
|
|
1466
|
-
# `recv.foo(args) { |params| body }` and friends. The call
|
|
1467
|
-
#
|
|
1468
|
-
#
|
|
1469
|
-
#
|
|
1470
|
-
# scope from the receiving method's RBS signature, types the
|
|
1471
|
-
# block body, and threads the body's type into
|
|
1472
|
-
# `MethodDispatcher.dispatch`'s `block_type:` so generic
|
|
1473
|
-
# methods like `Array#map { |n| n.to_s }` resolve to
|
|
1287
|
+
# `recv.foo(args) { |params| body }` and friends. The call type comes from `Scope#type_of` (which routes through
|
|
1288
|
+
# `ExpressionTyper#call_type_for` and is itself block-aware since Slice 6 phase C sub-phase 2: it builds the
|
|
1289
|
+
# block-entry scope from the receiving method's RBS signature, types the block body, and threads the body's type
|
|
1290
|
+
# into `MethodDispatcher.dispatch`'s `block_type:` so generic methods like `Array#map { |n| n.to_s }` resolve to
|
|
1474
1291
|
# `Array[String]`).
|
|
1475
1292
|
#
|
|
1476
|
-
# The handler still re-evaluates the block under its entry
|
|
1477
|
-
#
|
|
1478
|
-
#
|
|
1479
|
-
#
|
|
1480
|
-
# inside the block body. The receiver and arguments still
|
|
1481
|
-
# observe the outer scope, matching Ruby evaluation order.
|
|
1293
|
+
# The handler still re-evaluates the block under its entry scope so the per-node scope index sees the bindings on
|
|
1294
|
+
# the `on_enter` callback path. Block effects do NOT leak into the post-call scope: a block-local write is
|
|
1295
|
+
# observed only inside the block body. The receiver and arguments still observe the outer scope, matching Ruby
|
|
1296
|
+
# evaluation order.
|
|
1482
1297
|
def eval_call(node)
|
|
1483
1298
|
call_type = scope.type_of(node, tracer: tracer)
|
|
1484
|
-
# ADR-56 slice C (B3) — `each_with_object(memo) { |x, acc| acc << … }`
|
|
1485
|
-
#
|
|
1486
|
-
#
|
|
1487
|
-
# the memo block-param and adopt it as the call's return type.
|
|
1299
|
+
# ADR-56 slice C (B3) — `each_with_object(memo) { |x, acc| acc << … }` returns the memo; the engine otherwise
|
|
1300
|
+
# types the call `Dynamic[top]`. Compute the joined memo type from the block's content mutations of the memo
|
|
1301
|
+
# block-param and adopt it as the call's return type.
|
|
1488
1302
|
call_type = each_with_object_return(node, call_type)
|
|
1489
1303
|
evaluate_block_if_present(node)
|
|
1490
|
-
# `ruby2_keywords def foo(...)` (and similar wrappers like
|
|
1491
|
-
#
|
|
1492
|
-
#
|
|
1493
|
-
#
|
|
1494
|
-
# `
|
|
1495
|
-
#
|
|
1496
|
-
# scope
|
|
1497
|
-
# `self_type = singleton(C)` from `ScopeIndexer.propagate`,
|
|
1498
|
-
# so `self.helper` inside reports `undefined method 'helper'
|
|
1499
|
-
# for singleton(C)`. Walking each argument-position def under
|
|
1500
|
-
# the current evaluator (not a sub_eval — the def's effects
|
|
1501
|
-
# do not bind into the surrounding scope) populates the
|
|
1502
|
-
# scope index with the correct instance / singleton
|
|
1503
|
-
# `self_type` for the def's body.
|
|
1304
|
+
# `ruby2_keywords def foo(...)` (and similar wrappers like `private def`, `public def`, `module_function def`)
|
|
1305
|
+
# parse the def as the call's positional argument; the ExpressionTyper#type_of_def handler types it as
|
|
1306
|
+
# `Constant[:foo]` without walking the body. Without explicitly evaluating the argument-position def, the body's
|
|
1307
|
+
# scope-index entries inherit the outer class-body `self_type = singleton(C)` from `ScopeIndexer.propagate`, so
|
|
1308
|
+
# `self.helper` inside reports `undefined method 'helper' for singleton(C)`. Walking each argument-position def
|
|
1309
|
+
# under the current evaluator (not a sub_eval — the def's effects do not bind into the surrounding scope)
|
|
1310
|
+
# populates the scope index with the correct instance / singleton `self_type` for the def's body.
|
|
1504
1311
|
evaluate_def_arguments(node)
|
|
1505
1312
|
post_scope = record_closure_escape_if_any(node)
|
|
1506
|
-
# ADR-56 slice A — non-escaping block captured-local write-back.
|
|
1507
|
-
#
|
|
1508
|
-
#
|
|
1509
|
-
#
|
|
1510
|
-
# § "Fact stability and mutation" names captured locals a
|
|
1511
|
-
# first-class invalidation category. (The escaping / unknown path
|
|
1512
|
-
# already widened to Dynamic[top] via `record_closure_escape_if_any`.)
|
|
1313
|
+
# ADR-56 slice A — non-escaping block captured-local write-back. A `:non_escaping` block (each / times / upto /
|
|
1314
|
+
# map …) that rebinds an outer local must not leave that local's pre-call binding unmodified in the continuation
|
|
1315
|
+
# scope; the spec MUST in § "Fact stability and mutation" names captured locals a first-class invalidation
|
|
1316
|
+
# category. (The escaping / unknown path already widened to Dynamic[top] via `record_closure_escape_if_any`.)
|
|
1513
1317
|
post_scope = write_back_block_captures(node, post_scope)
|
|
1514
1318
|
post_scope = apply_rbs_extended_assertions(node, post_scope)
|
|
1515
1319
|
post_scope = apply_plugin_assertions(node, post_scope)
|
|
1516
1320
|
post_scope = apply_rspec_matcher_narrowing(node, post_scope)
|
|
1517
|
-
# Flow-folding G1 / G2 — widen a local- or instance-variable
|
|
1518
|
-
#
|
|
1519
|
-
#
|
|
1520
|
-
# carrier (`Tuple` / `HashShape`) from outliving its
|
|
1521
|
-
# justification when the value is mutated. Always-safe
|
|
1522
|
-
# (loses precision, never invents facts).
|
|
1321
|
+
# Flow-folding G1 / G2 — widen a local- or instance-variable binding when the call is an in-place mutator on it
|
|
1322
|
+
# (e.g. `arms << x`, `@tags << hashtag`). Stops a literal-shape carrier (`Tuple` / `HashShape`) from outliving
|
|
1323
|
+
# its justification when the value is mutated. Always-safe (loses precision, never invents facts).
|
|
1523
1324
|
post_scope = MutationWidening.widen_after_call(call_node: node, current_scope: post_scope)
|
|
1524
|
-
# ADR-57 slice 3 work-item 1 (cross-method-boundary variant). When a
|
|
1525
|
-
#
|
|
1526
|
-
#
|
|
1527
|
-
#
|
|
1528
|
-
# `
|
|
1529
|
-
# the
|
|
1530
|
-
#
|
|
1531
|
-
# seed and `options.fetch(:mode)` folds to a wrong constant. Precise:
|
|
1532
|
-
# fires only when the resolved callee actually escape-mutates that
|
|
1533
|
-
# parameter (not for every self-call), and sound — only loses
|
|
1534
|
-
# precision on the floored argument.
|
|
1325
|
+
# ADR-57 slice 3 work-item 1 (cross-method-boundary variant). When a self-call resolves to a user method that
|
|
1326
|
+
# CONTENT-mutates one of its parameters inside an escaping block (the `build_option_parser(opts)` idiom — the
|
|
1327
|
+
# callee returns an `OptionParser` whose `opts.on { o[:k] = v }` blocks close over the passed-in hash), floor
|
|
1328
|
+
# the matching caller-argument local. The callee's escape is invisible across the boundary, so without this the
|
|
1329
|
+
# caller's `options` keeps its seed and `options.fetch(:mode)` folds to a wrong constant. Precise: fires only
|
|
1330
|
+
# when the resolved callee actually escape-mutates that parameter (not for every self-call), and sound — only
|
|
1331
|
+
# loses precision on the floored argument.
|
|
1535
1332
|
post_scope = widen_callee_escaped_argument_captures(node, post_scope)
|
|
1536
|
-
# Same always-safe rationale as `widen_after_call` above —
|
|
1537
|
-
#
|
|
1538
|
-
# mutations (`items.each { |x| arr << x }`).
|
|
1333
|
+
# Same always-safe rationale as `widen_after_call` above — propagates outer-scope local / ivar widening from
|
|
1334
|
+
# block body mutations (`items.each { |x| arr << x }`).
|
|
1539
1335
|
post_scope = MutationWidening.widen_after_block(call_node: node, outer_scope: post_scope)
|
|
1540
|
-
# ADR-56 slice C — receiver-content element-type join. Joins
|
|
1541
|
-
#
|
|
1542
|
-
#
|
|
1543
|
-
# types `Array[0 | Integer]`, not `Array[0]`. Same always-safe
|
|
1544
|
-
# rationale (only widens).
|
|
1336
|
+
# ADR-56 slice C — receiver-content element-type join. Joins appended / stored element / key / value types into
|
|
1337
|
+
# the continuation collection so `out = [0]; arr.each { |x| out << x }` types `Array[0 | Integer]`, not
|
|
1338
|
+
# `Array[0]`. Same always-safe rationale (only widens).
|
|
1545
1339
|
post_scope = content_writeback_block_captures(node, post_scope)
|
|
1546
|
-
# Indexed-collection narrowing — drop any
|
|
1547
|
-
# `
|
|
1548
|
-
#
|
|
1549
|
-
# same slot or any other mutator runs against the
|
|
1550
|
-
# receiver. Always-safe (only forgets; never invents).
|
|
1340
|
+
# Indexed-collection narrowing — drop any `receiver[key] ||= default` narrowing the analyzer recorded earlier
|
|
1341
|
+
# when an intervening `[]=` writes the same slot or any other mutator runs against the receiver. Always-safe
|
|
1342
|
+
# (only forgets; never invents).
|
|
1551
1343
|
post_scope = IndexedNarrowing.invalidate_after_call(call_node: node, current_scope: post_scope)
|
|
1552
|
-
# Single-hop method-chain narrowing — drop every
|
|
1553
|
-
#
|
|
1554
|
-
# outer
|
|
1555
|
-
#
|
|
1556
|
-
# notes). Calls whose outer receiver is itself a chain
|
|
1557
|
-
# node (e.g. `x.last << y`) do NOT drop narrowings
|
|
1558
|
-
# keyed on `x` — only direct calls against the root
|
|
1559
|
-
# variable invalidate the chain.
|
|
1344
|
+
# Single-hop method-chain narrowing — drop every `(receiver, *)` chain narrowing rooted at the call's outer
|
|
1345
|
+
# stable receiver (any-call-against-the-root invalidation rule, B2 from the slice's design notes). Calls whose
|
|
1346
|
+
# outer receiver is itself a chain node (e.g. `x.last << y`) do NOT drop narrowings keyed on `x` — only direct
|
|
1347
|
+
# calls against the root variable invalidate the chain.
|
|
1560
1348
|
post_scope = IndexedNarrowing.invalidate_chain_after_call(call_node: node, current_scope: post_scope)
|
|
1561
|
-
# B2.2 — intervening method call ivar invalidation.
|
|
1562
|
-
#
|
|
1563
|
-
# ivar
|
|
1564
|
-
#
|
|
1565
|
-
#
|
|
1566
|
-
# seed back to the seed itself, so a subsequent
|
|
1567
|
-
# `if @flag` predicate observes the seed's union (not the
|
|
1568
|
-
# pre-call narrowed value). Always-safe (only widens; no
|
|
1569
|
-
# new facts). See [`docs/CURRENT_WORK.md`](../../../docs/CURRENT_WORK.md)
|
|
1570
|
-
# § "Flow-folding" — G2 intervening-call case.
|
|
1349
|
+
# B2.2 — intervening method call ivar invalidation. An implicit-self / self-receiver call could mutate any ivar
|
|
1350
|
+
# of the enclosing class (we cannot prove purity without an effect system). Reset each ivar whose current local
|
|
1351
|
+
# binding has narrowed below the class-ivar seed back to the seed itself, so a subsequent `if @flag` predicate
|
|
1352
|
+
# observes the seed's union (not the pre-call narrowed value). Always-safe (only widens; no new facts). See
|
|
1353
|
+
# [`docs/CURRENT_WORK.md`](../../../docs/CURRENT_WORK.md) § "Flow-folding" — G2 intervening-call case.
|
|
1571
1354
|
post_scope = invalidate_ivars_for_intervening_call(node, post_scope)
|
|
1572
|
-
# C1 — regex match-data globals (`$~`, `$1..$9`, `$&`, …) are
|
|
1573
|
-
#
|
|
1574
|
-
#
|
|
1575
|
-
#
|
|
1576
|
-
#
|
|
1577
|
-
# unknown-receiver call whose body we cannot prove match-free).
|
|
1578
|
-
# A call provably match-free on a known receiver — `$3.to_i`,
|
|
1579
|
-
# `year < 50` — does NOT clobber, so the multi-statement
|
|
1580
|
-
# `m = /…/ =~ s; …; use($2)` stdlib idiom keeps its precision
|
|
1355
|
+
# C1 — regex match-data globals (`$~`, `$1..$9`, `$&`, …) are narrowed to non-nil on a successful-match edge; a
|
|
1356
|
+
# later call that itself runs a regex match rebinds them, so the narrowed facts must be dropped. We forget them
|
|
1357
|
+
# only when the call is match-CAPABLE (a regex-matching method, or an implicit-self / unknown-receiver call
|
|
1358
|
+
# whose body we cannot prove match-free). A call provably match-free on a known receiver — `$3.to_i`, `year <
|
|
1359
|
+
# 50` — does NOT clobber, so the multi-statement `m = /…/ =~ s; …; use($2)` stdlib idiom keeps its precision
|
|
1581
1360
|
# while a genuinely interposed match still invalidates.
|
|
1582
1361
|
post_scope = post_scope.forget_match_globals if match_capable_call?(node)
|
|
1583
1362
|
[call_type, post_scope]
|
|
1584
1363
|
end
|
|
1585
1364
|
|
|
1586
|
-
# Method names that (may) run a regex match and therefore rebind
|
|
1587
|
-
#
|
|
1588
|
-
# globals only with a Regexp argument, but we do not inspect args.
|
|
1365
|
+
# Method names that (may) run a regex match and therefore rebind the `$~` family. Conservative over-approximation
|
|
1366
|
+
# — a few set globals only with a Regexp argument, but we do not inspect args.
|
|
1589
1367
|
MATCH_CAPABLE_METHODS = %i[
|
|
1590
1368
|
=~ match match? gsub gsub! sub sub! scan split slice slice!
|
|
1591
1369
|
[] partition rpartition index rindex === grep grep_v
|
|
1592
1370
|
].freeze
|
|
1593
1371
|
private_constant :MATCH_CAPABLE_METHODS
|
|
1594
1372
|
|
|
1595
|
-
# True when `node` could rebind the regex match-data globals:
|
|
1596
|
-
#
|
|
1597
|
-
#
|
|
1598
|
-
#
|
|
1599
|
-
#
|
|
1600
|
-
#
|
|
1601
|
-
# narrowed globals. The over-approximation is one-directional: a
|
|
1602
|
-
# user method that secretly matches on an explicit receiver is the
|
|
1603
|
-
# only escape, and re-narrowing on the next real guard recovers —
|
|
1604
|
-
# weighed against the false-positive cost, precision wins here.
|
|
1373
|
+
# True when `node` could rebind the regex match-data globals: a known regex-matching method by name, or an
|
|
1374
|
+
# implicit-self / self-receiver call whose body we cannot inspect for an internal match. An explicit-receiver call
|
|
1375
|
+
# to a non-matching method (`$3.to_i`, `year < 50`, `buf << c`) is treated as match-free so the multi-statement `m
|
|
1376
|
+
# = /…/ =~ s; …; use($2)` idiom keeps the narrowed globals. The over-approximation is one-directional: a user
|
|
1377
|
+
# method that secretly matches on an explicit receiver is the only escape, and re-narrowing on the next real guard
|
|
1378
|
+
# recovers — weighed against the false-positive cost, precision wins here.
|
|
1605
1379
|
def match_capable_call?(node)
|
|
1606
1380
|
return true unless node.is_a?(Prism::CallNode)
|
|
1607
1381
|
return true if MATCH_CAPABLE_METHODS.include?(node.name)
|
|
@@ -1610,12 +1384,9 @@ module Rigor
|
|
|
1610
1384
|
receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
1611
1385
|
end
|
|
1612
1386
|
|
|
1613
|
-
# Returns a scope with each ivar's narrowed local binding
|
|
1614
|
-
#
|
|
1615
|
-
#
|
|
1616
|
-
# class (implicit-self or explicit `self.foo`). External-
|
|
1617
|
-
# receiver calls (`obj.method`) cannot reach the caller's
|
|
1618
|
-
# ivars; they pass through unchanged.
|
|
1387
|
+
# Returns a scope with each ivar's narrowed local binding widened back to its class-ivar seed value when the call
|
|
1388
|
+
# is one that could plausibly mutate ivars on the enclosing class (implicit-self or explicit `self.foo`).
|
|
1389
|
+
# External- receiver calls (`obj.method`) cannot reach the caller's ivars; they pass through unchanged.
|
|
1619
1390
|
def invalidate_ivars_for_intervening_call(call_node, current_scope)
|
|
1620
1391
|
return current_scope unless intervening_call_candidate?(call_node)
|
|
1621
1392
|
|
|
@@ -1655,11 +1426,9 @@ module Rigor
|
|
|
1655
1426
|
end
|
|
1656
1427
|
end
|
|
1657
1428
|
|
|
1658
|
-
# v0.0.3 — recognises a small catalogue of RSpec
|
|
1659
|
-
#
|
|
1660
|
-
#
|
|
1661
|
-
# matched purely on AST shape; no RBS for RSpec is
|
|
1662
|
-
# required (and none is shipped today).
|
|
1429
|
+
# v0.0.3 — recognises a small catalogue of RSpec matcher patterns as assert-shaped narrows on the local passed to
|
|
1430
|
+
# `expect(...)`. The pattern is matched purely on AST shape; no RBS for RSpec is required (and none is shipped
|
|
1431
|
+
# today).
|
|
1663
1432
|
#
|
|
1664
1433
|
# Recognised today:
|
|
1665
1434
|
#
|
|
@@ -1674,11 +1443,9 @@ module Rigor
|
|
|
1674
1443
|
# `be_an_instance_of`, subtype-permitting
|
|
1675
1444
|
# otherwise).
|
|
1676
1445
|
#
|
|
1677
|
-
# Anything else is silently passed through. Symmetric
|
|
1678
|
-
#
|
|
1679
|
-
#
|
|
1680
|
-
# modelled: they are rarely useful in practice and
|
|
1681
|
-
# risk masking bugs if the assertion later fails.
|
|
1446
|
+
# Anything else is silently passed through. Symmetric negative class assertions (`not_to be_a(C)`) and narrowing
|
|
1447
|
+
# TO `NilClass` are intentionally NOT modelled: they are rarely useful in practice and risk masking bugs if the
|
|
1448
|
+
# assertion later fails.
|
|
1682
1449
|
def apply_rspec_matcher_narrowing(call_node, current_scope)
|
|
1683
1450
|
narrow = rspec_matcher_narrowing_request(call_node)
|
|
1684
1451
|
return current_scope if narrow.nil?
|
|
@@ -1691,10 +1458,8 @@ module Rigor
|
|
|
1691
1458
|
current_scope.with_local(local_name, narrowed)
|
|
1692
1459
|
end
|
|
1693
1460
|
|
|
1694
|
-
# Decodes an `expect(x).<chain>` outer call into a
|
|
1695
|
-
#
|
|
1696
|
-
# not recognised. The hash carries `:local` (the local
|
|
1697
|
-
# name being narrowed) plus the narrowing parameters.
|
|
1461
|
+
# Decodes an `expect(x).<chain>` outer call into a narrowing request hash, or `nil` when the shape is not
|
|
1462
|
+
# recognised. The hash carries `:local` (the local name being narrowed) plus the narrowing parameters.
|
|
1698
1463
|
def rspec_matcher_narrowing_request(call_node)
|
|
1699
1464
|
local_name = rspec_expectation_target(call_node)
|
|
1700
1465
|
return nil if local_name.nil?
|
|
@@ -1725,8 +1490,7 @@ module Rigor
|
|
|
1725
1490
|
end
|
|
1726
1491
|
end
|
|
1727
1492
|
|
|
1728
|
-
# `be_a` / `be_kind_of` / `be_an_instance_of` accept a
|
|
1729
|
-
# single class argument — either a `ConstantReadNode`
|
|
1493
|
+
# `be_a` / `be_kind_of` / `be_an_instance_of` accept a single class argument — either a `ConstantReadNode`
|
|
1730
1494
|
# (`Integer`) or a `ConstantPathNode` (`Rigor::Type::Nominal`).
|
|
1731
1495
|
def rspec_be_a_narrow(matcher, local_name, exact:)
|
|
1732
1496
|
args = matcher.arguments&.arguments || []
|
|
@@ -1749,11 +1513,8 @@ module Rigor
|
|
|
1749
1513
|
end
|
|
1750
1514
|
end
|
|
1751
1515
|
|
|
1752
|
-
# Returns the local name passed to `expect(...)` when
|
|
1753
|
-
#
|
|
1754
|
-
# or nil otherwise. Centralised so each per-matcher
|
|
1755
|
-
# decoder can short-circuit on a non-matching outer
|
|
1756
|
-
# call.
|
|
1516
|
+
# Returns the local name passed to `expect(...)` when the receiver chain matches `expect(<local>)` exactly, or nil
|
|
1517
|
+
# otherwise. Centralised so each per-matcher decoder can short-circuit on a non-matching outer call.
|
|
1757
1518
|
def rspec_expectation_target(call_node)
|
|
1758
1519
|
receiver = call_node.receiver
|
|
1759
1520
|
return nil unless receiver.is_a?(Prism::CallNode) && receiver.name == :expect
|
|
@@ -1776,10 +1537,8 @@ module Rigor
|
|
|
1776
1537
|
matcher
|
|
1777
1538
|
end
|
|
1778
1539
|
|
|
1779
|
-
# True when `call_node`'s sole argument is an
|
|
1780
|
-
#
|
|
1781
|
-
# no positional arguments — used by the no-arg
|
|
1782
|
-
# matchers (`be_nil`).
|
|
1540
|
+
# True when `call_node`'s sole argument is an implicit-self matcher call with the given name and no positional
|
|
1541
|
+
# arguments — used by the no-arg matchers (`be_nil`).
|
|
1783
1542
|
def rspec_matcher_argument?(call_node, matcher_name)
|
|
1784
1543
|
matcher = rspec_matcher_node(call_node)
|
|
1785
1544
|
return false if matcher.nil?
|
|
@@ -1788,16 +1547,11 @@ module Rigor
|
|
|
1788
1547
|
matcher.arguments.nil? || matcher.arguments.arguments.empty?
|
|
1789
1548
|
end
|
|
1790
1549
|
|
|
1791
|
-
# Slice 4b-2 (ADR-7 § "Slice 4-A/4-B") — applies the
|
|
1792
|
-
#
|
|
1793
|
-
# `
|
|
1794
|
-
# `
|
|
1795
|
-
#
|
|
1796
|
-
# rows for `:always` assert directives (the slice-4a
|
|
1797
|
-
# routing places conditional asserts on `truthy_facts` /
|
|
1798
|
-
# `falsey_facts`, which `Narrowing.predicate_scopes`
|
|
1799
|
-
# consumes). Plugin `:always` assertions are handled by
|
|
1800
|
-
# the sibling `apply_plugin_assertions`, not this path.
|
|
1550
|
+
# Slice 4b-2 (ADR-7 § "Slice 4-A/4-B") — applies the post-return facts the merger produces for an
|
|
1551
|
+
# `RBS::Extended`-annotated call. Reads through `RbsExtended.read_flow_contribution` so the bundle carries the
|
|
1552
|
+
# canonical `Rigor::FlowContribution::Fact` rows for `:always` assert directives (the slice-4a routing places
|
|
1553
|
+
# conditional asserts on `truthy_facts` / `falsey_facts`, which `Narrowing.predicate_scopes` consumes). Plugin
|
|
1554
|
+
# `:always` assertions are handled by the sibling `apply_plugin_assertions`, not this path.
|
|
1801
1555
|
def apply_rbs_extended_assertions(call_node, current_scope)
|
|
1802
1556
|
method_def = resolve_call_method(call_node, current_scope)
|
|
1803
1557
|
return current_scope if method_def.nil?
|
|
@@ -1814,31 +1568,20 @@ module Rigor
|
|
|
1814
1568
|
end
|
|
1815
1569
|
end
|
|
1816
1570
|
|
|
1817
|
-
# ADR-7 § "Slice 4-A" / T.bind priority slice 2 — applies
|
|
1818
|
-
# the
|
|
1819
|
-
#
|
|
1820
|
-
#
|
|
1821
|
-
# downstream narrowing path (`apply_post_return_fact` →
|
|
1822
|
-
# `apply_self_post_return_fact`) are the same; only the
|
|
1823
|
-
# *source* of the bundle changes (RBS::Extended vs the
|
|
1824
|
-
# registered plugins' `flow_contribution_for`).
|
|
1571
|
+
# ADR-7 § "Slice 4-A" / T.bind priority slice 2 — applies the post-return facts plugin contributions produce. This
|
|
1572
|
+
# is the sibling of {apply_rbs_extended_assertions}: the carrier (`Rigor::FlowContribution::Fact`) and the
|
|
1573
|
+
# downstream narrowing path (`apply_post_return_fact` → `apply_self_post_return_fact`) are the same; only the
|
|
1574
|
+
# *source* of the bundle changes (RBS::Extended vs the registered plugins' `flow_contribution_for`).
|
|
1825
1575
|
#
|
|
1826
|
-
# `:self`-targeted facts narrow `scope.self_type` for the
|
|
1827
|
-
#
|
|
1828
|
-
#
|
|
1829
|
-
# to the rest of the block — exactly the contract Sorbet's
|
|
1830
|
-
# `T.bind(self, T)` commits to.
|
|
1576
|
+
# `:self`-targeted facts narrow `scope.self_type` for the surrounding scope. In a block body, the surrounding
|
|
1577
|
+
# scope is the block's own scope, so the narrowing applies to the rest of the block — exactly the contract
|
|
1578
|
+
# Sorbet's `T.bind(self, T)` commits to.
|
|
1831
1579
|
#
|
|
1832
|
-
# `:parameter`-targeted facts only land when the called
|
|
1833
|
-
#
|
|
1834
|
-
#
|
|
1835
|
-
#
|
|
1836
|
-
#
|
|
1837
|
-
# plugin's own diagnostics_for_file path covers those
|
|
1838
|
-
# cases. The full plugin-side parameter-targeting story
|
|
1839
|
-
# (PHPStan-style Type-Specifying Extensions on
|
|
1840
|
-
# plugin-recognised calls) lives behind a follow-up slice
|
|
1841
|
-
# that introduces `:local` / `:argument_at` target kinds.
|
|
1580
|
+
# `:parameter`-targeted facts only land when the called method has an authoritative RBS sig (via
|
|
1581
|
+
# `resolve_call_method`); plugins recognising their own synthetic call shapes (e.g. `T.assert_type!`) have no
|
|
1582
|
+
# method_def and the parameter facts silently skip — the plugin's own diagnostics_for_file path covers those
|
|
1583
|
+
# cases. The full plugin-side parameter-targeting story (PHPStan-style Type-Specifying Extensions on
|
|
1584
|
+
# plugin-recognised calls) lives behind a follow-up slice that introduces `:local` / `:argument_at` target kinds.
|
|
1842
1585
|
def apply_plugin_assertions(call_node, current_scope)
|
|
1843
1586
|
registry = current_scope.environment&.plugin_registry
|
|
1844
1587
|
return current_scope if registry.nil? || registry.empty?
|
|
@@ -1856,16 +1599,14 @@ module Rigor
|
|
|
1856
1599
|
end
|
|
1857
1600
|
end
|
|
1858
1601
|
|
|
1859
|
-
# ADR-37 slice 2 / ADR-52 WD3 — gathers each plugin's post-return
|
|
1860
|
-
#
|
|
1861
|
-
#
|
|
1862
|
-
# exceptions so a buggy plugin can't abort the assertion path.
|
|
1602
|
+
# ADR-37 slice 2 / ADR-52 WD3 — gathers each plugin's post-return narrowing from the method-gated
|
|
1603
|
+
# `narrowing_facts` DSL, wrapped as a facts-only `FlowContribution`, swallowing per-plugin exceptions so a buggy
|
|
1604
|
+
# plugin can't abort the assertion path.
|
|
1863
1605
|
EMPTY_CONTRIBUTIONS = [].freeze
|
|
1864
1606
|
private_constant :EMPTY_CONTRIBUTIONS
|
|
1865
1607
|
|
|
1866
|
-
# Fast-exit guard: skip if no plugin declares a `narrowing_facts` rule, or if
|
|
1867
|
-
#
|
|
1868
|
-
# `collect_gated_statement_contributions` for the full consultation.
|
|
1608
|
+
# Fast-exit guard: skip if no plugin declares a `narrowing_facts` rule, or if no registered method-name gate
|
|
1609
|
+
# matches the call. See `collect_gated_statement_contributions` for the full consultation.
|
|
1869
1610
|
def collect_plugin_contributions(registry, call_node, current_scope)
|
|
1870
1611
|
index = registry.contribution_index
|
|
1871
1612
|
relevant = index.for_statement
|
|
@@ -1877,10 +1618,9 @@ module Rigor
|
|
|
1877
1618
|
collect_gated_statement_contributions(index, relevant, name, call_node, current_scope)
|
|
1878
1619
|
end
|
|
1879
1620
|
|
|
1880
|
-
# ADR-37 slice 2 / ADR-52 WD1 — post-gate walk in registry order.
|
|
1881
|
-
#
|
|
1882
|
-
#
|
|
1883
|
-
# case is a single lookup. Accumulates lazily; caller is read-only.
|
|
1621
|
+
# ADR-37 slice 2 / ADR-52 WD1 — post-gate walk in registry order. Visits only plugins in `for_statement` (declare
|
|
1622
|
+
# a `narrowing_facts` rule), further gated by the method-name Set probe so the common no-candidate case is a
|
|
1623
|
+
# single lookup. Accumulates lazily; caller is read-only.
|
|
1884
1624
|
def collect_gated_statement_contributions(index, relevant, name, call_node, current_scope)
|
|
1885
1625
|
result = nil
|
|
1886
1626
|
relevant.each do |plugin|
|
|
@@ -1923,19 +1663,13 @@ module Rigor
|
|
|
1923
1663
|
end
|
|
1924
1664
|
end
|
|
1925
1665
|
|
|
1926
|
-
# Slice 4b-2 — applies a single post-return Fact to the
|
|
1927
|
-
#
|
|
1928
|
-
#
|
|
1929
|
-
# narrowing logic via `Narrowing.narrow_for_fact` so the
|
|
1930
|
-
# predicate / assert / plugin paths all converge on the
|
|
1931
|
-
# same hierarchy-aware narrowing rules.
|
|
1666
|
+
# Slice 4b-2 — applies a single post-return Fact to the scope. Mirrors `Narrowing#apply_fact_to_scope` (Fact
|
|
1667
|
+
# variant of the v0.0.2 `apply_assert_effect`); shares the narrowing logic via `Narrowing.narrow_for_fact` so the
|
|
1668
|
+
# predicate / assert / plugin paths all converge on the same hierarchy-aware narrowing rules.
|
|
1932
1669
|
#
|
|
1933
|
-
# v0.1.8 Pillar 2 Slice 1 added the `:local` target_kind
|
|
1934
|
-
#
|
|
1935
|
-
#
|
|
1936
|
-
# local in the surrounding scope, bypassing the
|
|
1937
|
-
# parameter-name lookup that requires an authoritative RBS
|
|
1938
|
-
# sig on the called method (which RSpec matchers lack).
|
|
1670
|
+
# v0.1.8 Pillar 2 Slice 1 added the `:local` target_kind branch so plugins recognising bespoke call shapes
|
|
1671
|
+
# (`expect(x).to be_a(T)`) can directly narrow a named local in the surrounding scope, bypassing the
|
|
1672
|
+
# parameter-name lookup that requires an authoritative RBS sig on the called method (which RSpec matchers lack).
|
|
1939
1673
|
def apply_post_return_fact(fact, call_node, current_scope, method_def)
|
|
1940
1674
|
return apply_local_post_return_fact(fact, current_scope) if fact.target_kind == :local
|
|
1941
1675
|
|
|
@@ -1951,12 +1685,9 @@ module Rigor
|
|
|
1951
1685
|
current_scope.with_local(local_name, narrowed)
|
|
1952
1686
|
end
|
|
1953
1687
|
|
|
1954
|
-
# v0.1.8 Pillar 2 Slice 1 — narrows the named local directly
|
|
1955
|
-
#
|
|
1956
|
-
#
|
|
1957
|
-
# source. Silently no-ops when the local is unbound in the
|
|
1958
|
-
# current scope (the plugin's named local may have already
|
|
1959
|
-
# gone out of scope when the contribution fires).
|
|
1688
|
+
# v0.1.8 Pillar 2 Slice 1 — narrows the named local directly without consulting the call node's argument list. The
|
|
1689
|
+
# fact's `target_name` is the local-variable name as written in source. Silently no-ops when the local is unbound
|
|
1690
|
+
# in the current scope (the plugin's named local may have already gone out of scope when the contribution fires).
|
|
1960
1691
|
def apply_local_post_return_fact(fact, current_scope)
|
|
1961
1692
|
local_name = fact.target_name
|
|
1962
1693
|
current_type = current_scope.local(local_name)
|
|
@@ -1966,9 +1697,8 @@ module Rigor
|
|
|
1966
1697
|
current_scope.with_local(local_name, narrowed)
|
|
1967
1698
|
end
|
|
1968
1699
|
|
|
1969
|
-
# v0.1.1 Track 1 slice 3 — `assert self is T` post-return
|
|
1970
|
-
#
|
|
1971
|
-
# `Narrowing#apply_self_fact`).
|
|
1700
|
+
# v0.1.1 Track 1 slice 3 — `assert self is T` post-return narrowing for the four supported receiver shapes
|
|
1701
|
+
# (mirrors `Narrowing#apply_self_fact`).
|
|
1972
1702
|
def apply_self_post_return_fact(fact, receiver_node, current_scope)
|
|
1973
1703
|
case receiver_node
|
|
1974
1704
|
when nil, Prism::SelfNode
|
|
@@ -1994,8 +1724,7 @@ module Rigor
|
|
|
1994
1724
|
end
|
|
1995
1725
|
end
|
|
1996
1726
|
|
|
1997
|
-
# `:self` routes to the call receiver; otherwise we look
|
|
1998
|
-
# up the matching positional argument by parameter name.
|
|
1727
|
+
# `:self` routes to the call receiver; otherwise we look up the matching positional argument by parameter name.
|
|
1999
1728
|
def fact_target_node(fact, call_node, method_def)
|
|
2000
1729
|
if fact.target_kind == :self
|
|
2001
1730
|
call_node.receiver
|
|
@@ -2005,12 +1734,9 @@ module Rigor
|
|
|
2005
1734
|
end
|
|
2006
1735
|
|
|
2007
1736
|
def lookup_post_return_arg(call_node, method_def, target_name)
|
|
2008
|
-
# Plugin-source contributions arrive without an
|
|
2009
|
-
#
|
|
2010
|
-
#
|
|
2011
|
-
# to "no narrow" in that case — the wider plugin-side
|
|
2012
|
-
# parameter mapping (`:local` / `:argument_at`) is a
|
|
2013
|
-
# follow-up slice.
|
|
1737
|
+
# Plugin-source contributions arrive without an authoritative method_def (the plugin recognised the call shape
|
|
1738
|
+
# directly). Parameter-targeting falls back to "no narrow" in that case — the wider plugin-side parameter
|
|
1739
|
+
# mapping (`:local` / `:argument_at`) is a follow-up slice.
|
|
2014
1740
|
return nil if method_def.nil?
|
|
2015
1741
|
|
|
2016
1742
|
arguments = call_node.arguments&.arguments || []
|
|
@@ -2030,8 +1756,8 @@ module Rigor
|
|
|
2030
1756
|
sub_eval(block, block_entry)
|
|
2031
1757
|
end
|
|
2032
1758
|
|
|
2033
|
-
# Slice 6 phase C sub-phase 3b/3c. When the call carries a
|
|
2034
|
-
#
|
|
1759
|
+
# Slice 6 phase C sub-phase 3b/3c. When the call carries a block whose receiving method is NOT proven
|
|
1760
|
+
# non-escaping:
|
|
2035
1761
|
#
|
|
2036
1762
|
# - 3b: attach a `dynamic_origin` `closure_escape` fact to the
|
|
2037
1763
|
# post-call scope so consumers can see that the closure may
|
|
@@ -2045,19 +1771,15 @@ module Rigor
|
|
|
2045
1771
|
# only reads (without writing) are also untouched: read-only
|
|
2046
1772
|
# captures cannot rebind the outer variable.
|
|
2047
1773
|
#
|
|
2048
|
-
# A `:non_escaping` classification (or any block-less call)
|
|
2049
|
-
# leaves the post-call scope unchanged.
|
|
1774
|
+
# A `:non_escaping` classification (or any block-less call) leaves the post-call scope unchanged.
|
|
2050
1775
|
def record_closure_escape_if_any(node)
|
|
2051
|
-
# ADR-57 slice 3 work-item 1: an escaping block can also be attached
|
|
2052
|
-
# to
|
|
2053
|
-
#
|
|
2054
|
-
#
|
|
2055
|
-
#
|
|
2056
|
-
#
|
|
2057
|
-
#
|
|
2058
|
-
# Fold each escaping receiver-chain block's content widening into the
|
|
2059
|
-
# continuation here so the captured collection is floored regardless of
|
|
2060
|
-
# how deep in the receiver chain its mutating block lives.
|
|
1776
|
+
# ADR-57 slice 3 work-item 1: an escaping block can also be attached to a RECEIVER call in a chain rather than
|
|
1777
|
+
# to `node` itself — the canonical `OptionParser.new do |opts| opts.on { o[:k] = v } end .parse!(argv)` idiom,
|
|
1778
|
+
# where the content-mutating block hangs off `OptionParser.new` but the statement-level call node is the chained
|
|
1779
|
+
# `.parse!`. A receiver call is evaluated as an expression, never as a statement, so its block never reaches
|
|
1780
|
+
# this escape handler on its own. Fold each escaping receiver-chain block's content widening into the
|
|
1781
|
+
# continuation here so the captured collection is floored regardless of how deep in the receiver chain its
|
|
1782
|
+
# mutating block lives.
|
|
2061
1783
|
post_scope = widen_escaping_receiver_chain_captures(node, scope)
|
|
2062
1784
|
|
|
2063
1785
|
return post_scope unless node.block.is_a?(Prism::BlockNode)
|
|
@@ -2078,15 +1800,13 @@ module Rigor
|
|
|
2078
1800
|
)
|
|
2079
1801
|
end
|
|
2080
1802
|
|
|
2081
|
-
# Floor each caller-argument local whose matching parameter the resolved
|
|
2082
|
-
#
|
|
2083
|
-
#
|
|
2084
|
-
# "which parameters escape-mutate" set is memoised on the def node.
|
|
1803
|
+
# Floor each caller-argument local whose matching parameter the resolved callee escape-mutates (see the call-site
|
|
1804
|
+
# comment). Only self-dispatch calls resolving to a discovered user def are considered; the per-def "which
|
|
1805
|
+
# parameters escape-mutate" set is memoised on the def node.
|
|
2085
1806
|
def widen_callee_escaped_argument_captures(node, base_scope)
|
|
2086
1807
|
# Apply to the statement call AND every call in its receiver chain: the
|
|
2087
|
-
# `build_option_parser(options).parse!(argv)` idiom puts the escape-
|
|
2088
|
-
#
|
|
2089
|
-
# never the statement node's own argument.
|
|
1808
|
+
# `build_option_parser(options).parse!(argv)` idiom puts the escape- mutating helper call in the RECEIVER
|
|
1809
|
+
# position, where its argument is never the statement node's own argument.
|
|
2090
1810
|
acc = floor_callee_escaped_args_for_call(node, base_scope)
|
|
2091
1811
|
receiver = node.receiver
|
|
2092
1812
|
while receiver.is_a?(Prism::CallNode)
|
|
@@ -2098,9 +1818,8 @@ module Rigor
|
|
|
2098
1818
|
|
|
2099
1819
|
def floor_callee_escaped_args_for_call(node, base_scope)
|
|
2100
1820
|
return base_scope unless self_dispatch_call?(node)
|
|
2101
|
-
# Fast path — the floor only ever touches a local passed as an
|
|
2102
|
-
#
|
|
2103
|
-
# def resolution + body scan entirely (the overwhelming common case).
|
|
1821
|
+
# Fast path — the floor only ever touches a local passed as an argument, so a call with no arguments cannot
|
|
1822
|
+
# floor anything. Skip the def resolution + body scan entirely (the overwhelming common case).
|
|
2104
1823
|
return base_scope unless call_passes_local_argument?(node)
|
|
2105
1824
|
|
|
2106
1825
|
def_node = resolve_self_callee_def(node)
|
|
@@ -2112,11 +1831,9 @@ module Rigor
|
|
|
2112
1831
|
floor_arguments_at_positions(node, escaped, base_scope)
|
|
2113
1832
|
end
|
|
2114
1833
|
|
|
2115
|
-
# The user def a self-dispatch `node` resolves to in the enclosing class,
|
|
2116
|
-
#
|
|
2117
|
-
#
|
|
2118
|
-
# off the hot path for the overwhelming majority of self-calls that
|
|
2119
|
-
# resolve to nothing escaping.
|
|
1834
|
+
# The user def a self-dispatch `node` resolves to in the enclosing class, or nil. Reuses the discovery index
|
|
1835
|
+
# `Scope#user_def_for` reads; no ancestor walk (the boundary-escape idiom is same-class), keeping this off the hot
|
|
1836
|
+
# path for the overwhelming majority of self-calls that resolve to nothing escaping.
|
|
2120
1837
|
def resolve_self_callee_def(node)
|
|
2121
1838
|
class_name = enclosing_class_name_for(scope.self_type)
|
|
2122
1839
|
return scope.top_level_def_for(node.name) if class_name.nil?
|
|
@@ -2130,11 +1847,10 @@ module Rigor
|
|
|
2130
1847
|
node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
2131
1848
|
end
|
|
2132
1849
|
|
|
2133
|
-
# The set of `[name, position]` parameters of `def_node` whose content a
|
|
2134
|
-
#
|
|
2135
|
-
#
|
|
2136
|
-
#
|
|
2137
|
-
# inside a block whose receiving call is not proven non-escaping.
|
|
1850
|
+
# The set of `[name, position]` parameters of `def_node` whose content a block in the body escape-mutates.
|
|
1851
|
+
# Memoised per def node (the body walk is otherwise repeated at every call site). A parameter is "escape- mutated"
|
|
1852
|
+
# when a `param[k] = v` / `param << x` mutation on it appears inside a block whose receiving call is not proven
|
|
1853
|
+
# non-escaping.
|
|
2138
1854
|
def escaped_content_parameters(def_node)
|
|
2139
1855
|
cache = (@escaped_param_cache ||= {}.compare_by_identity)
|
|
2140
1856
|
cache[def_node] ||= compute_escaped_content_parameters(def_node)
|
|
@@ -2156,15 +1872,12 @@ module Rigor
|
|
|
2156
1872
|
positions.slice(*mutated)
|
|
2157
1873
|
end
|
|
2158
1874
|
|
|
2159
|
-
# A receiver-independent over-approximation of `ClosureEscapeAnalyzer`'s
|
|
2160
|
-
#
|
|
2161
|
-
#
|
|
2162
|
-
#
|
|
2163
|
-
#
|
|
2164
|
-
#
|
|
2165
|
-
# `define_method`, an unknown DSL hook) is treated as escaping — sound,
|
|
2166
|
-
# since mis-classifying a truly-non-escaping call only floors an argument
|
|
2167
|
-
# that was about to be precise.
|
|
1875
|
+
# A receiver-independent over-approximation of `ClosureEscapeAnalyzer`'s non-escaping verdict, used when scanning
|
|
1876
|
+
# a callee body where the block- owning call's receiver TYPE is not available. A call whose method name is a known
|
|
1877
|
+
# structural iterator (`each` / `map` / `tap` / …) runs its block synchronously and does not retain it, so its
|
|
1878
|
+
# captured mutations are not a cross-boundary escape. Any other name (`on`, `subscribe`, `define_method`, an
|
|
1879
|
+
# unknown DSL hook) is treated as escaping — sound, since mis-classifying a truly-non-escaping call only floors an
|
|
1880
|
+
# argument that was about to be precise.
|
|
2168
1881
|
SYNTACTIC_NON_ESCAPING_BLOCK_METHODS = (
|
|
2169
1882
|
ClosureEscapeAnalyzer::ENUMERABLE_NON_ESCAPING +
|
|
2170
1883
|
ClosureEscapeAnalyzer::OBJECT_NON_ESCAPING +
|
|
@@ -2179,9 +1892,8 @@ module Rigor
|
|
|
2179
1892
|
SYNTACTIC_NON_ESCAPING_BLOCK_METHODS.include?(call_node.name)
|
|
2180
1893
|
end
|
|
2181
1894
|
|
|
2182
|
-
# `{ name => position }` for the required / optional positional
|
|
2183
|
-
# parameters
|
|
2184
|
-
# the boundary-escape idiom passes a plain positional collection.
|
|
1895
|
+
# `{ name => position }` for the required / optional positional parameters of a def. Keyword / rest / block
|
|
1896
|
+
# parameters are skipped — the boundary-escape idiom passes a plain positional collection.
|
|
2185
1897
|
def positional_parameter_positions(def_node)
|
|
2186
1898
|
params = def_node.parameters
|
|
2187
1899
|
return {} if params.nil?
|
|
@@ -2194,10 +1906,9 @@ module Rigor
|
|
|
2194
1906
|
positions
|
|
2195
1907
|
end
|
|
2196
1908
|
|
|
2197
|
-
# True when at least one argument of `node` is a bare local-variable read
|
|
2198
|
-
#
|
|
2199
|
-
#
|
|
2200
|
-
# could actually floor something.
|
|
1909
|
+
# True when at least one argument of `node` is a bare local-variable read (positional or keyword value) bound in
|
|
1910
|
+
# the current scope — a cheap pre-filter so the def resolution / body scan only runs for calls that could actually
|
|
1911
|
+
# floor something.
|
|
2201
1912
|
def call_passes_local_argument?(node)
|
|
2202
1913
|
args = node.arguments
|
|
2203
1914
|
return false unless args.respond_to?(:arguments)
|
|
@@ -2232,12 +1943,10 @@ module Rigor
|
|
|
2232
1943
|
end
|
|
2233
1944
|
end
|
|
2234
1945
|
|
|
2235
|
-
# Walk the receiver chain of `node` and fold the escaping-content
|
|
2236
|
-
#
|
|
2237
|
-
# `
|
|
2238
|
-
#
|
|
2239
|
-
# non-escaping write-back (which the receiver expression evaluation
|
|
2240
|
-
# already drives), so we only floor the escaping / unknown ones here.
|
|
1946
|
+
# Walk the receiver chain of `node` and fold the escaping-content widening of every block-bearing, escaping
|
|
1947
|
+
# receiver call into `base_scope`. Only receiver calls are walked — `node` itself is handled by the caller. A
|
|
1948
|
+
# `:non_escaping` receiver block is left to slice C's non-escaping write-back (which the receiver expression
|
|
1949
|
+
# evaluation already drives), so we only floor the escaping / unknown ones here.
|
|
2241
1950
|
def widen_escaping_receiver_chain_captures(node, base_scope)
|
|
2242
1951
|
receiver = node.receiver
|
|
2243
1952
|
acc = base_scope
|
|
@@ -2251,28 +1960,23 @@ module Rigor
|
|
|
2251
1960
|
acc
|
|
2252
1961
|
end
|
|
2253
1962
|
|
|
2254
|
-
# ADR-57 slice 2 (ADR-56 mechanisms 2 / 8 extended to escaping blocks).
|
|
2255
|
-
#
|
|
2256
|
-
# local
|
|
2257
|
-
#
|
|
2258
|
-
#
|
|
2259
|
-
# survived and its element fold (`options[:format]` -> `"text"`,
|
|
2260
|
-
# `s.empty?` -> `true`) was unsoundly precise.
|
|
1963
|
+
# ADR-57 slice 2 (ADR-56 mechanisms 2 / 8 extended to escaping blocks). An escaping / unknown block that
|
|
1964
|
+
# CONTENT-mutates a captured outer local (`options[:k] = v` in an `OptionParser#on` block, `s << x` in a stored
|
|
1965
|
+
# proc) previously left that local's content untouched — only its narrowing was dropped, so a constant seed
|
|
1966
|
+
# (`options = {}`, `s = ""`) survived and its element fold (`options[:format]` -> `"text"`, `s.empty?` -> `true`)
|
|
1967
|
+
# was unsoundly precise.
|
|
2261
1968
|
#
|
|
2262
|
-
# An escaping block may run later and any number of times, so joining a
|
|
2263
|
-
#
|
|
2264
|
-
#
|
|
2265
|
-
#
|
|
2266
|
-
#
|
|
2267
|
-
# the carrier survives. Read-only captures and locals the block merely
|
|
2268
|
-
# rebinds (already floored by `drop_captured_narrowing`) are untouched.
|
|
1969
|
+
# An escaping block may run later and any number of times, so joining a bounded evidence set is not sound (unlike
|
|
1970
|
+
# slice C's non-escaping join): the sound continuation is the bare-collection floor — Array ->
|
|
1971
|
+
# `Array[Dynamic[top]]`, Hash -> `Hash[untyped, untyped]`, String -> `String`. The seed's element/key/value
|
|
1972
|
+
# precision is forgotten; only the carrier survives. Read-only captures and locals the block merely rebinds
|
|
1973
|
+
# (already floored by `drop_captured_narrowing`) are untouched.
|
|
2269
1974
|
def widen_escaping_content_captures(block_node, post_scope)
|
|
2270
1975
|
body = block_node.body
|
|
2271
1976
|
return post_scope if body.nil?
|
|
2272
1977
|
|
|
2273
|
-
# Transitive case first: the body may content-mutate a captured local
|
|
2274
|
-
#
|
|
2275
|
-
# `collect_content_mutations` cannot see (see below).
|
|
1978
|
+
# Transitive case first: the body may content-mutate a captured local through a self-call rather than a direct
|
|
1979
|
+
# `local[k] = v` write, which `collect_content_mutations` cannot see (see below).
|
|
2276
1980
|
post_scope = floor_block_body_callee_escaped_args(body, post_scope)
|
|
2277
1981
|
|
|
2278
1982
|
mutations = collect_content_mutations(body)
|
|
@@ -2284,20 +1988,15 @@ module Rigor
|
|
|
2284
1988
|
end
|
|
2285
1989
|
end
|
|
2286
1990
|
|
|
2287
|
-
# Inside an ESCAPING block body, a captured outer local can be content-
|
|
2288
|
-
#
|
|
2289
|
-
#
|
|
2290
|
-
#
|
|
2291
|
-
#
|
|
2292
|
-
#
|
|
2293
|
-
#
|
|
2294
|
-
#
|
|
2295
|
-
#
|
|
2296
|
-
# `options[:mutation]` guard to an always-falsey constant). Reuse the
|
|
2297
|
-
# cross-method-boundary callee-escaped-argument floor (the same gate the
|
|
2298
|
-
# receiver-chain path uses at the call site) on every self-call in the
|
|
2299
|
-
# body. Sound — only ever floors a captured local passed as an argument
|
|
2300
|
-
# to a callee that demonstrably escape-mutates the matching parameter.
|
|
1991
|
+
# Inside an ESCAPING block body, a captured outer local can be content- mutated transitively: the body is (or
|
|
1992
|
+
# contains) a self-call that escape-mutates one of its arguments. The canonical shape is the CLI's own
|
|
1993
|
+
# `OptionParser.new { |opts| define_options(opts, options) }` — the block body is a bare `define_options(opts,
|
|
1994
|
+
# options)` whose `options` parameter is escape-mutated inside ITS nested `opts.on { options[:k] = v }` blocks.
|
|
1995
|
+
# `collect_content_mutations` only sees direct `local[k] = v` writes in THIS body, so it misses the transitive
|
|
1996
|
+
# write and the captured Hash keeps its literal-false seed (folding the caller's `options[:mutation]` guard to an
|
|
1997
|
+
# always-falsey constant). Reuse the cross-method-boundary callee-escaped-argument floor (the same gate the
|
|
1998
|
+
# receiver-chain path uses at the call site) on every self-call in the body. Sound — only ever floors a captured
|
|
1999
|
+
# local passed as an argument to a callee that demonstrably escape-mutates the matching parameter.
|
|
2301
2000
|
def floor_block_body_callee_escaped_args(body, post_scope)
|
|
2302
2001
|
acc = post_scope
|
|
2303
2002
|
Source::NodeWalker.each(body) do |descendant|
|
|
@@ -2306,9 +2005,8 @@ module Rigor
|
|
|
2306
2005
|
acc
|
|
2307
2006
|
end
|
|
2308
2007
|
|
|
2309
|
-
# The Dynamic-floor carrier for a content-mutated escaping capture, or
|
|
2310
|
-
#
|
|
2311
|
-
# it alone — e.g. an already-`Dynamic` binding or an unknown shape).
|
|
2008
|
+
# The Dynamic-floor carrier for a content-mutated escaping capture, or nil when the pre-state is not a recognised
|
|
2009
|
+
# mutable collection (leave it alone — e.g. an already-`Dynamic` binding or an unknown shape).
|
|
2312
2010
|
def content_floor_for(type)
|
|
2313
2011
|
return nil if type.nil?
|
|
2314
2012
|
|
|
@@ -2334,14 +2032,10 @@ module Rigor
|
|
|
2334
2032
|
:unknown
|
|
2335
2033
|
end
|
|
2336
2034
|
|
|
2337
|
-
# Sub-phase 3c. Replace the outer-local types that the block
|
|
2338
|
-
#
|
|
2339
|
-
#
|
|
2340
|
-
#
|
|
2341
|
-
# synthesise the union of the block's write types (which the
|
|
2342
|
-
# current pass does not yet expose), we discard the narrowed
|
|
2343
|
-
# binding altogether. A future sub-phase MAY refine this to
|
|
2344
|
-
# the union of the block's actual writes.
|
|
2035
|
+
# Sub-phase 3c. Replace the outer-local types that the block body can rebind with `Dynamic[Top]`. The conservative
|
|
2036
|
+
# drop matches the spec line "facts about locals it can write become unstable after the escape point": rather than
|
|
2037
|
+
# synthesise the union of the block's write types (which the current pass does not yet expose), we discard the
|
|
2038
|
+
# narrowed binding altogether. A future sub-phase MAY refine this to the union of the block's actual writes.
|
|
2345
2039
|
def drop_captured_narrowing(block_node, base_scope)
|
|
2346
2040
|
names = captured_local_writes(block_node, base_scope)
|
|
2347
2041
|
return base_scope if names.empty?
|
|
@@ -2349,13 +2043,11 @@ module Rigor
|
|
|
2349
2043
|
names.reduce(base_scope) { |acc, name| acc.with_local(name, Type::Combinator.untyped) }
|
|
2350
2044
|
end
|
|
2351
2045
|
|
|
2352
|
-
# Names of outer locals the block body can REBIND, across every
|
|
2353
|
-
#
|
|
2354
|
-
#
|
|
2355
|
-
#
|
|
2356
|
-
#
|
|
2357
|
-
# names not bound in the outer scope are excluded — a write to either
|
|
2358
|
-
# is not a captured rebind of an outer variable.
|
|
2046
|
+
# Names of outer locals the block body can REBIND, across every local-write form: plain `=`
|
|
2047
|
+
# (`LocalVariableWriteNode`), the operator / `||=` / `&&=` compound forms, and a multi-assign target (`x, y = ...`
|
|
2048
|
+
# → `LocalVariableTargetNode` under `MultiWriteNode`). Block-introduced names (parameters, numbered params,
|
|
2049
|
+
# `;`-locals) and names not bound in the outer scope are excluded — a write to either is not a captured rebind of
|
|
2050
|
+
# an outer variable.
|
|
2359
2051
|
LOCAL_WRITE_NODES = [
|
|
2360
2052
|
Prism::LocalVariableWriteNode,
|
|
2361
2053
|
Prism::LocalVariableOperatorWriteNode,
|
|
@@ -2381,18 +2073,14 @@ module Rigor
|
|
|
2381
2073
|
outer_writes.uniq
|
|
2382
2074
|
end
|
|
2383
2075
|
|
|
2384
|
-
# ADR-56 slice A. For a `:non_escaping` block, fold the continuation
|
|
2385
|
-
# binding
|
|
2386
|
-
#
|
|
2387
|
-
#
|
|
2388
|
-
# joined with the pre-call binding (kept as a constituent so the
|
|
2389
|
-
# 0-iteration path — `[].each { … }` — stays sound), value-pinned-
|
|
2390
|
-
# widened on the final permitted iteration, and floored to
|
|
2076
|
+
# ADR-56 slice A. For a `:non_escaping` block, fold the continuation binding of every outer local the body can
|
|
2077
|
+
# rebind back into `post_scope`. The binding is a capped fixpoint (cap 3) over the block body re-evaluated under
|
|
2078
|
+
# the running per-name assumption, joined with the pre-call binding (kept as a constituent so the 0-iteration path
|
|
2079
|
+
# — `[].each { … }` — stays sound), value-pinned- widened on the final permitted iteration, and floored to
|
|
2391
2080
|
# `Dynamic[top]` on non-convergence (matching `drop_captured_narrowing`).
|
|
2392
2081
|
#
|
|
2393
|
-
# Fast path: a block writing no outer local leaves `post_scope`
|
|
2394
|
-
#
|
|
2395
|
-
# one extra `captured_local_writes` walk and nothing else.
|
|
2082
|
+
# Fast path: a block writing no outer local leaves `post_scope` byte-identical (the overwhelming majority of
|
|
2083
|
+
# blocks), so this costs one extra `captured_local_writes` walk and nothing else.
|
|
2396
2084
|
def write_back_block_captures(call_node, post_scope)
|
|
2397
2085
|
block = call_node.block
|
|
2398
2086
|
return post_scope unless block.is_a?(Prism::BlockNode)
|
|
@@ -2412,20 +2100,16 @@ module Rigor
|
|
|
2412
2100
|
result.reduce(post_scope) { |acc, (name, type)| acc.with_local(name, type) }
|
|
2413
2101
|
end
|
|
2414
2102
|
|
|
2415
|
-
# ADR-56 slice C — receiver-content element-type join. After the
|
|
2416
|
-
#
|
|
2417
|
-
#
|
|
2418
|
-
#
|
|
2419
|
-
#
|
|
2420
|
-
# `out = [0]; arr.each { |x| out << x }` types `out` as
|
|
2421
|
-
# `Array[0 | Integer]` (sound) rather than `Array[0]` (the B1
|
|
2422
|
-
# under-approximation: the runtime array is `[0, 1, 2, 3]`).
|
|
2103
|
+
# ADR-56 slice C — receiver-content element-type join. After the rebind write-back and
|
|
2104
|
+
# `MutationWidening.widen_after_block` (which forgets a content-mutated collection's literal arity but keeps only
|
|
2105
|
+
# the SEED's element types), join the appended/stored element / key / value types INTO the continuation
|
|
2106
|
+
# collection's parameter, so `out = [0]; arr.each { |x| out << x }` types `out` as `Array[0 | Integer]` (sound)
|
|
2107
|
+
# rather than `Array[0]` (the B1 under-approximation: the runtime array is `[0, 1, 2, 3]`).
|
|
2423
2108
|
#
|
|
2424
|
-
# Pre-state is read from `post_scope` so a local that is BOTH rebound
|
|
2425
|
-
#
|
|
2426
|
-
#
|
|
2427
|
-
#
|
|
2428
|
-
# sound empty-seed behaviour). Always sound — only ever widens.
|
|
2109
|
+
# Pre-state is read from `post_scope` so a local that is BOTH rebound and content-mutated composes: the rebind
|
|
2110
|
+
# fixpoint result feeds the content join. The block body is typed once for argument evidence; the floor is
|
|
2111
|
+
# `Array[Dynamic[top]]` / `Hash[untyped, untyped]` (the sound empty-seed behaviour). Always sound — only ever
|
|
2112
|
+
# widens.
|
|
2429
2113
|
def content_writeback_block_captures(call_node, post_scope)
|
|
2430
2114
|
block = call_node.block
|
|
2431
2115
|
return post_scope unless block.is_a?(Prism::BlockNode)
|
|
@@ -2444,13 +2128,11 @@ module Rigor
|
|
|
2444
2128
|
end
|
|
2445
2129
|
end
|
|
2446
2130
|
|
|
2447
|
-
# ADR-56 slice C (B3). For `recv.each_with_object(memo) { |x, acc| … }`
|
|
2448
|
-
# the
|
|
2449
|
-
#
|
|
2450
|
-
#
|
|
2451
|
-
#
|
|
2452
|
-
# param. Returns `call_type` unchanged for any other call, a missing
|
|
2453
|
-
# block, or a memo whose pre-state is not a collection.
|
|
2131
|
+
# ADR-56 slice C (B3). For `recv.each_with_object(memo) { |x, acc| … }` the return is the memo object after the
|
|
2132
|
+
# block has mutated it through the `acc` alias. Compute the joined memo type the same way captured- local content
|
|
2133
|
+
# mutations are joined: pre-state = the memo argument's type, added evidence = the content-mutator args on the
|
|
2134
|
+
# memo block param. Returns `call_type` unchanged for any other call, a missing block, or a memo whose pre-state
|
|
2135
|
+
# is not a collection.
|
|
2454
2136
|
def each_with_object_return(call_node, call_type)
|
|
2455
2137
|
return call_type unless call_node.name == :each_with_object
|
|
2456
2138
|
|
|
@@ -2466,8 +2148,8 @@ module Rigor
|
|
|
2466
2148
|
body = block.body
|
|
2467
2149
|
return call_type if body.nil?
|
|
2468
2150
|
|
|
2469
|
-
# The memo alias is a block-local (depth 0) — collect content
|
|
2470
|
-
#
|
|
2151
|
+
# The memo alias is a block-local (depth 0) — collect content mutations on it directly rather than via the
|
|
2152
|
+
# captured-local walk.
|
|
2471
2153
|
calls = body_content_mutations_on(body, memo_param)
|
|
2472
2154
|
return call_type if calls.empty?
|
|
2473
2155
|
|
|
@@ -2477,9 +2159,8 @@ module Rigor
|
|
|
2477
2159
|
joined || call_type
|
|
2478
2160
|
end
|
|
2479
2161
|
|
|
2480
|
-
# The name of the memo block parameter (the SECOND positional param of
|
|
2481
|
-
#
|
|
2482
|
-
# second positional param.
|
|
2162
|
+
# The name of the memo block parameter (the SECOND positional param of an `each_with_object` block), or nil when
|
|
2163
|
+
# the block does not bind a second positional param.
|
|
2483
2164
|
def each_with_object_memo_param(block)
|
|
2484
2165
|
params_root = block.parameters
|
|
2485
2166
|
return nil unless params_root.is_a?(Prism::BlockParametersNode)
|
|
@@ -2494,8 +2175,7 @@ module Rigor
|
|
|
2494
2175
|
second.respond_to?(:name) ? second.name : nil
|
|
2495
2176
|
end
|
|
2496
2177
|
|
|
2497
|
-
# Content-mutator calls on a block-local receiver `var_name`
|
|
2498
|
-
# (depth 0) within `body`.
|
|
2178
|
+
# Content-mutator calls on a block-local receiver `var_name` (depth 0) within `body`.
|
|
2499
2179
|
def body_content_mutations_on(body, var_name)
|
|
2500
2180
|
calls = []
|
|
2501
2181
|
Source::NodeWalker.each(body) do |descendant|
|
|
@@ -2511,15 +2191,14 @@ module Rigor
|
|
|
2511
2191
|
calls
|
|
2512
2192
|
end
|
|
2513
2193
|
|
|
2514
|
-
# Joins content evidence for a memo / param given its pre-state and a
|
|
2515
|
-
#
|
|
2194
|
+
# Joins content evidence for a memo / param given its pre-state and a list of mutator calls, dispatching Array vs
|
|
2195
|
+
# Hash by the mutator set.
|
|
2516
2196
|
def join_content_for_param(calls, pre_state, block_entry)
|
|
2517
2197
|
return nil if pre_state.nil?
|
|
2518
2198
|
|
|
2519
2199
|
if stringish?(pre_state)
|
|
2520
|
-
# String carries no element parameter; mutating `<<`/`concat`
|
|
2521
|
-
#
|
|
2522
|
-
# `"a…"`), so widen to the nominal base. Sound — only widens.
|
|
2200
|
+
# String carries no element parameter; mutating `<<`/`concat` makes the constant value unsound (`s = "a"; s <<
|
|
2201
|
+
# x` → runtime `"a…"`), so widen to the nominal base. Sound — only widens.
|
|
2523
2202
|
Type::Combinator.nominal_of("String")
|
|
2524
2203
|
elsif hashish?(pre_state) || (hash_mutations?(calls) && !arrayish?(pre_state))
|
|
2525
2204
|
join_hash_param(calls, pre_state, block_entry)
|
|
@@ -2539,9 +2218,8 @@ module Rigor
|
|
|
2539
2218
|
return nil unless arrayish?(pre_state)
|
|
2540
2219
|
|
|
2541
2220
|
added = calls.flat_map do |c|
|
|
2542
|
-
# Index-write on an array (`a[i] += v`) introduces no new element
|
|
2543
|
-
#
|
|
2544
|
-
# already widened the binding; contribute nothing.
|
|
2221
|
+
# Index-write on an array (`a[i] += v`) introduces no new element evidence we can cheaply attribute — the
|
|
2222
|
+
# array-arity forget already widened the binding; contribute nothing.
|
|
2545
2223
|
next [] if index_write?(c)
|
|
2546
2224
|
|
|
2547
2225
|
MutationWidening.array_added_elements(c.name, content_arg_types(c, block_entry))
|
|
@@ -2549,11 +2227,9 @@ module Rigor
|
|
|
2549
2227
|
MutationWidening.join_array_content(pre_state, added)
|
|
2550
2228
|
end
|
|
2551
2229
|
|
|
2552
|
-
# Walks the block body for content-mutator calls (`<<`, `push`,
|
|
2553
|
-
#
|
|
2554
|
-
#
|
|
2555
|
-
# `MutationWidening.widen_after_block` walk (descends into nested
|
|
2556
|
-
# blocks; the depth check keeps nested block-locals out).
|
|
2230
|
+
# Walks the block body for content-mutator calls (`<<`, `push`, `[]=`, …) whose receiver is a captured outer local
|
|
2231
|
+
# (depth >= 1), returning `{ name => [call_node, ...] }`. Mirrors the `MutationWidening.widen_after_block` walk
|
|
2232
|
+
# (descends into nested blocks; the depth check keeps nested block-locals out).
|
|
2557
2233
|
def collect_content_mutations(body)
|
|
2558
2234
|
mutations = Hash.new { |h, k| h[k] = [] }
|
|
2559
2235
|
Source::NodeWalker.each(body) do |descendant|
|
|
@@ -2563,11 +2239,9 @@ module Rigor
|
|
|
2563
2239
|
mutations
|
|
2564
2240
|
end
|
|
2565
2241
|
|
|
2566
|
-
# Index-write forms (`h[k] ||= v`, `h[k] += v`, `h[k] = v` via a
|
|
2567
|
-
#
|
|
2568
|
-
#
|
|
2569
|
-
# OrWrite even though the appended values land on the nested array —
|
|
2570
|
-
# leaving `h` an empty `{}` is unsound (`h.empty?` folds to `true`).
|
|
2242
|
+
# Index-write forms (`h[k] ||= v`, `h[k] += v`, `h[k] = v` via a multi-assign target) that mutate a collection's
|
|
2243
|
+
# CONTENT without a `[]=` CallNode. `h[k] ||= []; h[k] << v` mutates `h` through the OrWrite even though the
|
|
2244
|
+
# appended values land on the nested array — leaving `h` an empty `{}` is unsound (`h.empty?` folds to `true`).
|
|
2571
2245
|
INDEX_WRITE_NODES = [
|
|
2572
2246
|
Prism::IndexOrWriteNode,
|
|
2573
2247
|
Prism::IndexAndWriteNode,
|
|
@@ -2576,10 +2250,8 @@ module Rigor
|
|
|
2576
2250
|
].freeze
|
|
2577
2251
|
private_constant :INDEX_WRITE_NODES
|
|
2578
2252
|
|
|
2579
|
-
# `[receiver_name, node]` when `node` is a content mutation whose
|
|
2580
|
-
#
|
|
2581
|
-
# else `[nil, nil]`. Covers `[]=`-style CallNode mutators and the
|
|
2582
|
-
# index-write node forms.
|
|
2253
|
+
# `[receiver_name, node]` when `node` is a content mutation whose receiver is a local variable satisfying `accept`
|
|
2254
|
+
# (depth predicate), else `[nil, nil]`. Covers `[]=`-style CallNode mutators and the index-write node forms.
|
|
2583
2255
|
def content_mutation_target(node)
|
|
2584
2256
|
is_call_mutator = node.is_a?(Prism::CallNode) && MutationWidening::CONTENT_ADDERS.include?(node.name)
|
|
2585
2257
|
return [nil, nil] unless is_call_mutator || index_write?(node)
|
|
@@ -2591,11 +2263,9 @@ module Rigor
|
|
|
2591
2263
|
[receiver.name, node]
|
|
2592
2264
|
end
|
|
2593
2265
|
|
|
2594
|
-
# Computes the joined continuation collection type for one captured
|
|
2595
|
-
#
|
|
2596
|
-
#
|
|
2597
|
-
# e.g. a String accumulator, whose `<<` carries no element parameter
|
|
2598
|
-
# and whose binding already types as `String`.
|
|
2266
|
+
# Computes the joined continuation collection type for one captured local from its content-mutator calls. Returns
|
|
2267
|
+
# `nil` (no overlay) when the pre-state is neither an Array-ish nor a Hash-ish binding — e.g. a String
|
|
2268
|
+
# accumulator, whose `<<` carries no element parameter and whose binding already types as `String`.
|
|
2599
2269
|
def join_content_for_local(name, calls, post_scope, block_entry)
|
|
2600
2270
|
join_content_for_param(calls, post_scope.local(name), block_entry)
|
|
2601
2271
|
end
|
|
@@ -2633,11 +2303,9 @@ module Rigor
|
|
|
2633
2303
|
(type.is_a?(Type::Nominal) && type.class_name == "String")
|
|
2634
2304
|
end
|
|
2635
2305
|
|
|
2636
|
-
# `[key_type, value_type]` for a `h[k] = v` / `h.store(k, v)` call or
|
|
2637
|
-
# an index-write
|
|
2638
|
-
#
|
|
2639
|
-
# often land on a NESTED collection via `h[k] << v`), so the value is
|
|
2640
|
-
# floored to `untyped` — sound: it only ever widens the value param.
|
|
2306
|
+
# `[key_type, value_type]` for a `h[k] = v` / `h.store(k, v)` call or an index-write node (`h[k] ||= v`), typed in
|
|
2307
|
+
# the block-entry scope. For an index-write the stored value is opaque (the appended values often land on a NESTED
|
|
2308
|
+
# collection via `h[k] << v`), so the value is floored to `untyped` — sound: it only ever widens the value param.
|
|
2641
2309
|
# Returns `[]` for other forms.
|
|
2642
2310
|
def hash_pair_types(node, block_entry)
|
|
2643
2311
|
if index_write?(node)
|
|
@@ -2664,10 +2332,9 @@ module Rigor
|
|
|
2664
2332
|
nil
|
|
2665
2333
|
end
|
|
2666
2334
|
|
|
2667
|
-
# Argument types for a content-mutator call, typed against the
|
|
2668
|
-
#
|
|
2669
|
-
#
|
|
2670
|
-
# `;`-locals without leaking into the outer scope.
|
|
2335
|
+
# Argument types for a content-mutator call, typed against the block-entry scope (block params bound). A
|
|
2336
|
+
# sub-evaluator over `block_entry` keeps the argument typing flow-correct for params / `;`-locals without leaking
|
|
2337
|
+
# into the outer scope.
|
|
2671
2338
|
def content_arg_types(call_node, block_entry)
|
|
2672
2339
|
arguments = call_node.arguments
|
|
2673
2340
|
return [] if arguments.nil?
|
|
@@ -2677,10 +2344,9 @@ module Rigor
|
|
|
2677
2344
|
[]
|
|
2678
2345
|
end
|
|
2679
2346
|
|
|
2680
|
-
# Evaluates `block`'s body once with each written outer local bound to
|
|
2681
|
-
#
|
|
2682
|
-
#
|
|
2683
|
-
# the `BodyFixpoint` body-evaluator.
|
|
2347
|
+
# Evaluates `block`'s body once with each written outer local bound to the supplied `bindings` (block params /
|
|
2348
|
+
# `;`-locals re-bound as usual) and returns the per-name exit binding for `names`. Used as the `BodyFixpoint`
|
|
2349
|
+
# body-evaluator.
|
|
2684
2350
|
def block_exit_bindings(call_node, block, bindings, names)
|
|
2685
2351
|
entry = build_block_entry_scope(call_node, block)
|
|
2686
2352
|
entry = bindings.reduce(entry) { |acc, (name, type)| acc.with_local(name, type) }
|
|
@@ -2688,11 +2354,9 @@ module Rigor
|
|
|
2688
2354
|
names.to_h { |name| [name, exit_scope.local(name)] }
|
|
2689
2355
|
end
|
|
2690
2356
|
|
|
2691
|
-
# Names introduced by the block itself (parameters, numbered
|
|
2692
|
-
#
|
|
2693
|
-
#
|
|
2694
|
-
# to these names are local to the block and MUST NOT be
|
|
2695
|
-
# treated as captured rebinds of an outer local.
|
|
2357
|
+
# Names introduced by the block itself (parameters, numbered parameters via `BlockParameterBinder`, plus explicit
|
|
2358
|
+
# `;`-prefixed block-locals on `BlockParametersNode`). Writes to these names are local to the block and MUST NOT
|
|
2359
|
+
# be treated as captured rebinds of an outer local.
|
|
2696
2360
|
def block_introduced_locals(block_node)
|
|
2697
2361
|
introduced = Set.new(BlockParameterBinder.new.bind(block_node).keys)
|
|
2698
2362
|
params_root = block_node.parameters
|
|
@@ -2700,34 +2364,24 @@ module Rigor
|
|
|
2700
2364
|
introduced
|
|
2701
2365
|
end
|
|
2702
2366
|
|
|
2703
|
-
# `Prism::BlockNode` is reached through {#eval_call}; the
|
|
2704
|
-
#
|
|
2705
|
-
#
|
|
2706
|
-
#
|
|
2707
|
-
# caller's scope unchanged), but the body's local writes are
|
|
2708
|
-
# threaded through subsequent statements *inside* the block so
|
|
2709
|
-
# `each { |x| sum = x; sum.succ }` types `sum.succ` under the
|
|
2710
|
-
# `sum: x` binding.
|
|
2367
|
+
# `Prism::BlockNode` is reached through {#eval_call}; the handler runs the body under `scope`, which the caller
|
|
2368
|
+
# has already augmented with the block's parameter bindings. Effects do not leak past the block (the outer
|
|
2369
|
+
# eval_call returns the caller's scope unchanged), but the body's local writes are threaded through subsequent
|
|
2370
|
+
# statements *inside* the block so `each { |x| sum = x; sum.succ }` types `sum.succ` under the `sum: x` binding.
|
|
2711
2371
|
def eval_block(node)
|
|
2712
2372
|
return [Type::Combinator.constant_of(nil), scope] if node.body.nil?
|
|
2713
2373
|
|
|
2714
2374
|
sub_eval(node.body, scope)
|
|
2715
2375
|
end
|
|
2716
2376
|
|
|
2717
|
-
# Builds the entry scope for a block body. The block sees the
|
|
2718
|
-
#
|
|
2719
|
-
#
|
|
2720
|
-
# types come from the receiving method's RBS signature when
|
|
2721
|
-
# one is available; the rest default to `Dynamic[Top]`.
|
|
2377
|
+
# Builds the entry scope for a block body. The block sees the outer scope's locals (Ruby's lexical scoping rule)
|
|
2378
|
+
# and adds bindings for every named block parameter on top. Parameter types come from the receiving method's RBS
|
|
2379
|
+
# signature when one is available; the rest default to `Dynamic[Top]`.
|
|
2722
2380
|
#
|
|
2723
|
-
# `;`-prefixed block-locals (`do |i; x|`) are bound to
|
|
2724
|
-
#
|
|
2725
|
-
# `x
|
|
2726
|
-
#
|
|
2727
|
-
# Without this shadow, an inner `x.even?` before the first
|
|
2728
|
-
# write would type-check against the OUTER `x` (e.g.
|
|
2729
|
-
# `Integer`) when the runtime would actually `NoMethodError`
|
|
2730
|
-
# on `nil`.
|
|
2381
|
+
# `;`-prefixed block-locals (`do |i; x|`) are bound to `Constant[nil]` so the inner read shadows any outer `x` per
|
|
2382
|
+
# Ruby's semantics — at runtime the block-local is a fresh nil-valued variable on every block invocation. Without
|
|
2383
|
+
# this shadow, an inner `x.even?` before the first write would type-check against the OUTER `x` (e.g. `Integer`)
|
|
2384
|
+
# when the runtime would actually `NoMethodError` on `nil`.
|
|
2731
2385
|
def build_block_entry_scope(call_node, block_node)
|
|
2732
2386
|
expected = expected_block_param_types_for(call_node)
|
|
2733
2387
|
bindings = BlockParameterBinder.new(expected_param_types: expected).bind(block_node)
|
|
@@ -2771,12 +2425,10 @@ module Rigor
|
|
|
2771
2425
|
def eval_class_body(node, new_context)
|
|
2772
2426
|
return [Type::Combinator.constant_of(nil), scope] if node.body.nil?
|
|
2773
2427
|
|
|
2774
|
-
# Class/module bodies run in a fresh scope: the outer scope's
|
|
2775
|
-
#
|
|
2776
|
-
# the
|
|
2777
|
-
#
|
|
2778
|
-
# class body is the class object itself, so we set
|
|
2779
|
-
# `self_type` to `Singleton[<qualified>]`.
|
|
2428
|
+
# Class/module bodies run in a fresh scope: the outer scope's locals are NOT visible inside `class Foo; ...
|
|
2429
|
+
# end`. We keep the same Environment so RBS lookups continue to work, and simply drop the locals. Slice
|
|
2430
|
+
# A-engine: `self` inside a class body is the class object itself, so we set `self_type` to
|
|
2431
|
+
# `Singleton[<qualified>]`.
|
|
2780
2432
|
fresh = build_fresh_body_scope
|
|
2781
2433
|
body_self = self_type_for_class_body(new_context)
|
|
2782
2434
|
fresh = fresh.with_self_type(body_self) if body_self
|
|
@@ -2792,43 +2444,49 @@ module Rigor
|
|
|
2792
2444
|
source_path: scope.source_path
|
|
2793
2445
|
)
|
|
2794
2446
|
bindings = binder.bind(def_node)
|
|
2795
|
-
# ADR-67 WD3 — override an undeclared parameter with its call-site
|
|
2796
|
-
#
|
|
2797
|
-
#
|
|
2798
|
-
#
|
|
2799
|
-
# parameter-boundary diagnostic (WD1, satisfied by construction).
|
|
2447
|
+
# ADR-67 WD3 — override an undeclared parameter with its call-site inferred type (precision-additive; an
|
|
2448
|
+
# RBS-declared parameter wins, the table is empty on a normal `check` run). The inferred type lives only as a
|
|
2449
|
+
# body local, never as an RBS contract, so it cannot fire a parameter-boundary diagnostic (WD1, satisfied by
|
|
2450
|
+
# construction).
|
|
2800
2451
|
bindings = seed_inferred_param_types(bindings, def_node, singleton)
|
|
2801
2452
|
|
|
2802
|
-
# Method bodies do NOT see the outer scope's locals. They start
|
|
2803
|
-
#
|
|
2804
|
-
# the
|
|
2805
|
-
# seed their `ivars` map from the class-level accumulator so
|
|
2806
|
-
# `def get; @x; end` reads the type that a sibling
|
|
2807
|
-
# `def init; @x = 1; end` wrote.
|
|
2453
|
+
# Method bodies do NOT see the outer scope's locals. They start from a fresh scope with the same environment,
|
|
2454
|
+
# then receive the parameter bindings. Slice 7 phase 2: instance defs ALSO seed their `ivars` map from the
|
|
2455
|
+
# class-level accumulator so `def get; @x; end` reads the type that a sibling `def init; @x = 1; end` wrote.
|
|
2808
2456
|
fresh = build_fresh_body_scope
|
|
2809
2457
|
body_self = self_type_for_method_body(singleton: singleton)
|
|
2810
2458
|
fresh = fresh.with_self_type(body_self) if body_self
|
|
2811
2459
|
fresh = seed_instance_ivars(fresh, singleton: singleton)
|
|
2812
2460
|
fresh = seed_class_cvars(fresh)
|
|
2813
2461
|
fresh = seed_program_globals(fresh)
|
|
2814
|
-
# ADR-48 Struct slice 3 — install the method body's fold-safe-local set
|
|
2815
|
-
#
|
|
2816
|
-
# walk (the call-return inference path is seeded separately).
|
|
2462
|
+
# ADR-48 Struct slice 3 — install the method body's fold-safe-local set so a member read off a mutation-free
|
|
2463
|
+
# local folds during the in-body walk (the call-return inference path is seeded separately).
|
|
2817
2464
|
fresh = fresh.with_struct_fold_safe(
|
|
2818
2465
|
StructFoldSafety.fold_safe_locals(
|
|
2819
2466
|
def_node.body, ->(name) { scope.struct_member_layout(name)&.[](:members) }
|
|
2820
2467
|
)
|
|
2821
2468
|
)
|
|
2822
|
-
bindings.reduce(fresh) { |acc, (name, type)| acc
|
|
2469
|
+
bindings.reduce(fresh) { |acc, (name, type)| bind_param(acc, name, type) }
|
|
2470
|
+
end
|
|
2471
|
+
|
|
2472
|
+
# ADR-82 root-enrichment — bind a method parameter, and for an *undeclared* (untyped) parameter seed its
|
|
2473
|
+
# provenance to `inferred_return_untyped`. An untyped param is the archetypal inference gap ([ADR-67](
|
|
2474
|
+
# docs/adr/67-parameter-type-inference.md): no call-site type flows in), so a `x.foo` receiver on it should
|
|
2475
|
+
# route to parameter inference rather than reporting no cause at all. Reuses the WD1 `local_origins`
|
|
2476
|
+
# channel, so WD6 then carries the cause through any chain rooted at the parameter (`x.foo.bar`). An
|
|
2477
|
+
# RBS-declared / call-site-inferred parameter (a non-untyped binding) keeps no origin — it is not a hole.
|
|
2478
|
+
def bind_param(acc, name, type)
|
|
2479
|
+
bound = acc.with_local(name, type)
|
|
2480
|
+
return bound unless untyped_binding?(type)
|
|
2481
|
+
|
|
2482
|
+
bound.with_local_origin(name, DynamicOrigin::INFERRED_RETURN_UNTYPED)
|
|
2823
2483
|
end
|
|
2824
2484
|
|
|
2825
|
-
# ADR-67 WD3 — consult the call-site parameter-inference table for this
|
|
2826
|
-
#
|
|
2827
|
-
#
|
|
2828
|
-
#
|
|
2829
|
-
#
|
|
2830
|
-
# parameter (a non-untyped binding) always wins. No-op when the table is
|
|
2831
|
-
# empty (the normal `check` path), so the seed is byte-identical there.
|
|
2485
|
+
# ADR-67 WD3 — consult the call-site parameter-inference table for this `def` and replace each undeclared
|
|
2486
|
+
# (untyped) parameter binding with its inferred type. Keyed by `[class_name, method_name, kind]`, reconstructed
|
|
2487
|
+
# from the lexical class path — the same triple {Inference::ParameterInferenceCollector} records. An RBS-declared
|
|
2488
|
+
# parameter (a non-untyped binding) always wins. No-op when the table is empty (the normal `check` path), so the
|
|
2489
|
+
# seed is byte-identical there.
|
|
2832
2490
|
def seed_inferred_param_types(bindings, def_node, singleton)
|
|
2833
2491
|
inferred = scope.param_inferred_types
|
|
2834
2492
|
return bindings if inferred.empty?
|
|
@@ -2846,8 +2504,8 @@ module Rigor
|
|
|
2846
2504
|
merged
|
|
2847
2505
|
end
|
|
2848
2506
|
|
|
2849
|
-
# True for the `Dynamic[Top]` carrier `MethodParameterBinder` leaves on an
|
|
2850
|
-
#
|
|
2507
|
+
# True for the `Dynamic[Top]` carrier `MethodParameterBinder` leaves on an undeclared parameter — the only
|
|
2508
|
+
# bindings ADR-67 WD3 overrides.
|
|
2851
2509
|
def untyped_binding?(type)
|
|
2852
2510
|
type.is_a?(Type::Dynamic) && type.static_facet.is_a?(Type::Top)
|
|
2853
2511
|
end
|
|
@@ -2861,20 +2519,17 @@ module Rigor
|
|
|
2861
2519
|
seeded = scope.class_ivars_for(path)
|
|
2862
2520
|
return body_scope if seeded.empty?
|
|
2863
2521
|
|
|
2864
|
-
# ADR-58 WD1 — the class-ivar index unions every `@x = …` write across
|
|
2865
|
-
#
|
|
2866
|
-
#
|
|
2867
|
-
#
|
|
2868
|
-
#
|
|
2869
|
-
# on the cross-method invariant unless a method-local write or
|
|
2870
|
-
# narrowing makes the nil flow-live (which drops the mark).
|
|
2522
|
+
# ADR-58 WD1 — the class-ivar index unions every `@x = …` write across the class flow-insensitively, so a ctor
|
|
2523
|
+
# `@x = nil` seed makes a read in a *different* method type `T | nil`. That `nil` is declaration-sourced, not
|
|
2524
|
+
# flow-live, so `seed_declaration_sourced_ivar` marks each seeded ivar: `possible-nil-receiver` then declines to
|
|
2525
|
+
# fire on the cross-method invariant unless a method-local write or narrowing makes the nil flow-live (which
|
|
2526
|
+
# drops the mark).
|
|
2871
2527
|
seeded.reduce(body_scope) { |acc, (name, type)| acc.seed_declaration_sourced_ivar(name, type) }
|
|
2872
2528
|
end
|
|
2873
2529
|
|
|
2874
|
-
# Cvars are visible from BOTH instance and singleton method
|
|
2875
|
-
#
|
|
2876
|
-
#
|
|
2877
|
-
# the accumulator is empty and the seed is a no-op.
|
|
2530
|
+
# Cvars are visible from BOTH instance and singleton method bodies of the enclosing class, so this seed is
|
|
2531
|
+
# unconditional (no `singleton:` gate). At the top-level (no class context) the accumulator is empty and the seed
|
|
2532
|
+
# is a no-op.
|
|
2878
2533
|
def seed_class_cvars(body_scope)
|
|
2879
2534
|
path = current_class_path
|
|
2880
2535
|
return body_scope if path.nil?
|
|
@@ -2885,11 +2540,9 @@ module Rigor
|
|
|
2885
2540
|
seeded.reduce(body_scope) { |acc, (name, type)| acc.with_cvar(name, type) }
|
|
2886
2541
|
end
|
|
2887
2542
|
|
|
2888
|
-
# Globals are process-wide. The body scope already inherited
|
|
2889
|
-
# the
|
|
2890
|
-
#
|
|
2891
|
-
# `globals` map so reads observe a precise type without
|
|
2892
|
-
# consulting the accumulator on every lookup.
|
|
2543
|
+
# Globals are process-wide. The body scope already inherited the program-globals accumulator through
|
|
2544
|
+
# `with_program_globals`; seeding here just materialises each entry into the body's `globals` map so reads observe
|
|
2545
|
+
# a precise type without consulting the accumulator on every lookup.
|
|
2893
2546
|
def seed_program_globals(body_scope)
|
|
2894
2547
|
seeded = scope.program_globals
|
|
2895
2548
|
return body_scope if seeded.empty?
|
|
@@ -2897,19 +2550,14 @@ module Rigor
|
|
|
2897
2550
|
seeded.reduce(body_scope) { |acc, (name, type)| acc.with_global(name, type) }
|
|
2898
2551
|
end
|
|
2899
2552
|
|
|
2900
|
-
# Slice A-declarations. Class- and method-bodies start from a
|
|
2901
|
-
#
|
|
2902
|
-
# `
|
|
2903
|
-
# ScopeIndexer-populated declaration overrides
|
|
2904
|
-
# (`Prism::ConstantReadNode` for `module Foo` headers, etc.)
|
|
2905
|
-
# remain reachable from inside nested bodies.
|
|
2553
|
+
# Slice A-declarations. Class- and method-bodies start from a fresh local-empty scope, but they MUST keep the
|
|
2554
|
+
# `declared_types` table visible at the outer scope so the ScopeIndexer-populated declaration overrides
|
|
2555
|
+
# (`Prism::ConstantReadNode` for `module Foo` headers, etc.) remain reachable from inside nested bodies.
|
|
2906
2556
|
def build_fresh_body_scope
|
|
2907
|
-
# Single allocation instead of a deep `with_*` chain — this runs
|
|
2908
|
-
#
|
|
2909
|
-
#
|
|
2910
|
-
#
|
|
2911
|
-
# reference (ADR-53 Track A), so a table added to the index can no
|
|
2912
|
-
# longer be dropped here by a missed per-field copy.
|
|
2557
|
+
# Single allocation instead of a deep `with_*` chain — this runs per class/method body on the main walk, so the
|
|
2558
|
+
# chain's throwaway intermediate Scopes were a top `Scope#rebuild` source (ADR-44). Local-empty by design; the
|
|
2559
|
+
# discovery index is inherited whole by reference (ADR-53 Track A), so a table added to the index can no longer
|
|
2560
|
+
# be dropped here by a missed per-field copy.
|
|
2913
2561
|
Scope.new(
|
|
2914
2562
|
environment: scope.environment,
|
|
2915
2563
|
locals: {}.freeze,
|
|
@@ -2925,16 +2573,11 @@ module Rigor
|
|
|
2925
2573
|
def_receiver_targets_lexical_self?(def_node.receiver)
|
|
2926
2574
|
end
|
|
2927
2575
|
|
|
2928
|
-
# `def Foo.bar` inside `module Foo` (or `def Meta.init` inside
|
|
2929
|
-
# `
|
|
2930
|
-
#
|
|
2931
|
-
#
|
|
2932
|
-
#
|
|
2933
|
-
# `def OpenURI.x` form (single segment) and the
|
|
2934
|
-
# `def OpenURI::Meta.x` form (qualified path). Cross-class
|
|
2935
|
-
# receivers (`def Bar.baz` inside `module Foo` where the
|
|
2936
|
-
# receiver names a different constant) are not promoted to
|
|
2937
|
-
# singleton at this slice.
|
|
2576
|
+
# `def Foo.bar` inside `module Foo` (or `def Meta.init` inside `module Meta`) — explicit-receiver def that
|
|
2577
|
+
# semantically equals `def self.bar` because the receiver constant resolves to `self` at the def-site. Matched
|
|
2578
|
+
# against the current class context's tail to cover both the `def OpenURI.x` form (single segment) and the `def
|
|
2579
|
+
# OpenURI::Meta.x` form (qualified path). Cross-class receivers (`def Bar.baz` inside `module Foo` where the
|
|
2580
|
+
# receiver names a different constant) are not promoted to singleton at this slice.
|
|
2938
2581
|
def def_receiver_targets_lexical_self?(receiver)
|
|
2939
2582
|
return false if @class_context.empty?
|
|
2940
2583
|
|
|
@@ -2953,18 +2596,16 @@ module Rigor
|
|
|
2953
2596
|
end
|
|
2954
2597
|
end
|
|
2955
2598
|
|
|
2956
|
-
# Slice A-engine. Inside a class body `class Foo; ...; end`,
|
|
2957
|
-
#
|
|
2958
|
-
# at the top level (no enclosing class).
|
|
2599
|
+
# Slice A-engine. Inside a class body `class Foo; ...; end`, `self` is the class object — `Singleton[Foo]`.
|
|
2600
|
+
# Returns nil at the top level (no enclosing class).
|
|
2959
2601
|
def self_type_for_class_body(class_context)
|
|
2960
2602
|
return nil if class_context.empty?
|
|
2961
2603
|
|
|
2962
2604
|
Type::Combinator.singleton_of(class_context.map(&:name).join("::"))
|
|
2963
2605
|
end
|
|
2964
2606
|
|
|
2965
|
-
# Slice A-engine. Inside a method body, `self` depends on
|
|
2966
|
-
#
|
|
2967
|
-
# surrounding class:
|
|
2607
|
+
# Slice A-engine. Inside a method body, `self` depends on whether the def is on the singleton or instance side of
|
|
2608
|
+
# the surrounding class:
|
|
2968
2609
|
#
|
|
2969
2610
|
# - `def self.foo` or any def inside `class << self`: self is
|
|
2970
2611
|
# the class object → `Singleton[Foo]`.
|
|
@@ -2995,16 +2636,11 @@ module Rigor
|
|
|
2995
2636
|
target = singleton_constant_target(node.expression)
|
|
2996
2637
|
return @class_context unless target
|
|
2997
2638
|
|
|
2998
|
-
# `class << Foo` inside `class Foo` (the canonical
|
|
2999
|
-
#
|
|
3000
|
-
# `
|
|
3001
|
-
# singleton frame
|
|
3002
|
-
#
|
|
3003
|
-
# When the target names a different constant (rare
|
|
3004
|
-
# cross-class form), append a fresh singleton frame
|
|
3005
|
-
# tagged with the target FQN; the bodies are scoped to
|
|
3006
|
-
# that target rather than to the lexical enclosing
|
|
3007
|
-
# class.
|
|
2639
|
+
# `class << Foo` inside `class Foo` (the canonical pattern in Ruby's own time.rb) is semantically `class <<
|
|
2640
|
+
# self` — replace the enclosing frame with a singleton frame for the same name so method registration and
|
|
2641
|
+
# `self_type` lookup land on Foo. When the target names a different constant (rare cross-class form), append a
|
|
2642
|
+
# fresh singleton frame tagged with the target FQN; the bodies are scoped to that target rather than to the
|
|
2643
|
+
# lexical enclosing class.
|
|
3008
2644
|
if !@class_context.empty? && @class_context.last.name == target
|
|
3009
2645
|
outer = @class_context[0..-2]
|
|
3010
2646
|
outer + [ClassFrame.new(name: target, singleton: true)]
|
|
@@ -3025,10 +2661,8 @@ module Rigor
|
|
|
3025
2661
|
end
|
|
3026
2662
|
end
|
|
3027
2663
|
|
|
3028
|
-
# The qualified name of the immediately-enclosing class (joining
|
|
3029
|
-
#
|
|
3030
|
-
# top-level def with no enclosing class, which routes the
|
|
3031
|
-
# parameter binder past RBS lookup.
|
|
2664
|
+
# The qualified name of the immediately-enclosing class (joining every nested `ClassFrame` with `::`). Returns
|
|
2665
|
+
# `nil` for a top-level def with no enclosing class, which routes the parameter binder past RBS lookup.
|
|
3032
2666
|
def current_class_path
|
|
3033
2667
|
return nil if @class_context.empty?
|
|
3034
2668
|
|
|
@@ -3041,27 +2675,23 @@ module Rigor
|
|
|
3041
2675
|
|
|
3042
2676
|
# ----- helpers -----
|
|
3043
2677
|
|
|
3044
|
-
# Explicit `return value` (including `return` inside a block, which in
|
|
3045
|
-
#
|
|
3046
|
-
# is
|
|
3047
|
-
#
|
|
3048
|
-
#
|
|
3049
|
-
#
|
|
3050
|
-
# the sink around the nested body, so this handler only ever appends a
|
|
3051
|
-
# return that genuinely exits the method currently being inferred.
|
|
2678
|
+
# Explicit `return value` (including `return` inside a block, which in Ruby returns from the *enclosing method*).
|
|
2679
|
+
# The control-transfer value is `Bot` — a `return` produces no value at its own position — but the returned
|
|
2680
|
+
# expression's type is recorded into the active return sink so the method-return inference joins it with the
|
|
2681
|
+
# body's tail type. Returns inside a nested `def`/lambda are barriers: `eval_def` clears the sink around the
|
|
2682
|
+
# nested body, so this handler only ever appends a return that genuinely exits the method currently being
|
|
2683
|
+
# inferred.
|
|
3052
2684
|
def eval_return(node)
|
|
3053
2685
|
sink = Thread.current[RETURN_SINK_KEY]
|
|
3054
2686
|
record_return_value(node, sink) if sink
|
|
3055
2687
|
[Type::Combinator.bot, scope]
|
|
3056
2688
|
end
|
|
3057
2689
|
|
|
3058
|
-
# A `break` transfers control to the loop exit (its flow value is `Bot`,
|
|
3059
|
-
#
|
|
3060
|
-
# break
|
|
3061
|
-
#
|
|
3062
|
-
#
|
|
3063
|
-
# loop body (a block targeting a method, or top-level) — left to the
|
|
3064
|
-
# existing escaping-block / no-op handling.
|
|
2690
|
+
# A `break` transfers control to the loop exit (its flow value is `Bot`, like `return`). It records the current
|
|
2691
|
+
# scope into the active loop's break sink so the loop join can recover a `break`-path binding the fall-through
|
|
2692
|
+
# would drop (`flag = true; break` -> `flag` is `false | true` after the loop). nil sink = a `break` not inside an
|
|
2693
|
+
# inferred loop body (a block targeting a method, or top-level) — left to the existing escaping-block / no-op
|
|
2694
|
+
# handling.
|
|
3065
2695
|
def eval_break(node)
|
|
3066
2696
|
sink = Thread.current[BREAK_SINK_KEY]
|
|
3067
2697
|
sink << [node, scope] if sink
|
|
@@ -3070,10 +2700,9 @@ module Rigor
|
|
|
3070
2700
|
|
|
3071
2701
|
def record_return_value(node, sink)
|
|
3072
2702
|
args = node.arguments&.arguments || []
|
|
3073
|
-
# `return` with no argument returns nil; `return a` records the
|
|
3074
|
-
#
|
|
3075
|
-
#
|
|
3076
|
-
# return contributes the corresponding Tuple element-by-element.
|
|
2703
|
+
# `return` with no argument returns nil; `return a` records the argument's type; `return a, b, c` packs a Tuple
|
|
2704
|
+
# — in Ruby a multi-value return yields the array `[a, b, c]`, so the inferred return contributes the
|
|
2705
|
+
# corresponding Tuple element-by-element.
|
|
3077
2706
|
if args.empty?
|
|
3078
2707
|
sink << Type::Combinator.constant_of(nil)
|
|
3079
2708
|
elsif args.size == 1
|
|
@@ -3095,16 +2724,12 @@ module Rigor
|
|
|
3095
2724
|
).evaluate(node)
|
|
3096
2725
|
end
|
|
3097
2726
|
|
|
3098
|
-
# Slice 7 phase 14 — branch exit detection. Returns true
|
|
3099
|
-
#
|
|
3100
|
-
# surrounding
|
|
3101
|
-
#
|
|
3102
|
-
# to narrow the post-scope: when one branch exits, the
|
|
3103
|
-
# surrounding scope can carry the OTHER branch's edge
|
|
3104
|
-
# forward without nil-injection.
|
|
2727
|
+
# Slice 7 phase 14 — branch exit detection. Returns true when the branch's body unconditionally exits the
|
|
2728
|
+
# surrounding control flow through a `return`, `next`, `break`, or `raise`. Used by `eval_if` / `eval_unless` to
|
|
2729
|
+
# narrow the post-scope: when one branch exits, the surrounding scope can carry the OTHER branch's edge forward
|
|
2730
|
+
# without nil-injection.
|
|
3105
2731
|
#
|
|
3106
|
-
# The detection is intentionally conservative — it
|
|
3107
|
-
# recognises only the most common patterns:
|
|
2732
|
+
# The detection is intentionally conservative — it recognises only the most common patterns:
|
|
3108
2733
|
# - A `Prism::ReturnNode`, `NextNode`, `BreakNode`.
|
|
3109
2734
|
# - A `Prism::CallNode` whose name is `:raise` or `:throw`.
|
|
3110
2735
|
# - A `Prism::StatementsNode`, `Prism::ParenthesesNode`, or
|
|
@@ -3141,18 +2766,12 @@ module Rigor
|
|
|
3141
2766
|
end
|
|
3142
2767
|
end
|
|
3143
2768
|
|
|
3144
|
-
# ADR-24 WD6 / slice 3 — generalised terminating-branch
|
|
3145
|
-
#
|
|
3146
|
-
#
|
|
3147
|
-
#
|
|
3148
|
-
#
|
|
3149
|
-
#
|
|
3150
|
-
# it — regardless of how it is spelled. The canonical
|
|
3151
|
-
# case is a resolved guard helper (`fail_with_message(...)`)
|
|
3152
|
-
# whose body always raises: ADR-24 slice 1 types the call
|
|
3153
|
-
# `bot`, and this OR-test makes `helper(...) if x.nil?`
|
|
3154
|
-
# narrow exactly like `raise ... if x.nil?`. The branch
|
|
3155
|
-
# type is already computed by `eval_if` / `eval_unless`.
|
|
2769
|
+
# ADR-24 WD6 / slice 3 — generalised terminating-branch detection. `branch_unconditionally_exits?` recognises a
|
|
2770
|
+
# branch SYNTACTICALLY (return / next / break / a call named raise / throw / exit / abort / fail). A branch whose
|
|
2771
|
+
# *inferred type is `Bot`* also terminates — it cannot produce a value, so control never falls through it —
|
|
2772
|
+
# regardless of how it is spelled. The canonical case is a resolved guard helper (`fail_with_message(...)`) whose
|
|
2773
|
+
# body always raises: ADR-24 slice 1 types the call `bot`, and this OR-test makes `helper(...) if x.nil?` narrow
|
|
2774
|
+
# exactly like `raise ... if x.nil?`. The branch type is already computed by `eval_if` / `eval_unless`.
|
|
3156
2775
|
def branch_terminates?(branch_node, branch_type)
|
|
3157
2776
|
branch_unconditionally_exits?(branch_node) ||
|
|
3158
2777
|
branch_type.is_a?(Type::Bot)
|
|
@@ -3164,12 +2783,9 @@ module Rigor
|
|
|
3164
2783
|
sub_eval(branch_node, branch_scope)
|
|
3165
2784
|
end
|
|
3166
2785
|
|
|
3167
|
-
# Joins two branch scopes at a control-flow merge point. Names
|
|
3168
|
-
#
|
|
3169
|
-
#
|
|
3170
|
-
# them outright. This implements the contract the Slice 3 phase 1
|
|
3171
|
-
# `Scope#join` documentation defers to the statement-level
|
|
3172
|
-
# evaluator.
|
|
2786
|
+
# Joins two branch scopes at a control-flow merge point. Names bound in only one branch are nil-injected into the
|
|
2787
|
+
# other side so the joined scope sees them as `T | nil` rather than dropping them outright. This implements the
|
|
2788
|
+
# contract the Slice 3 phase 1 `Scope#join` documentation defers to the statement-level evaluator.
|
|
3173
2789
|
def join_with_nil_injection(scope_a, scope_b)
|
|
3174
2790
|
nil_const = Type::Combinator.constant_of(nil)
|
|
3175
2791
|
a_keys = scope_a.locals.keys
|
|
@@ -3182,21 +2798,17 @@ module Rigor
|
|
|
3182
2798
|
aug_a.join(aug_b)
|
|
3183
2799
|
end
|
|
3184
2800
|
|
|
3185
|
-
# Generalises {#join_with_nil_injection} to N branches (case/when,
|
|
3186
|
-
#
|
|
3187
|
-
# result because nil-injection commutes with union under
|
|
3188
|
-
# `Scope#join`.
|
|
2801
|
+
# Generalises {#join_with_nil_injection} to N branches (case/when, begin/rescue chain). The reduce order does not
|
|
2802
|
+
# affect the result because nil-injection commutes with union under `Scope#join`.
|
|
3189
2803
|
def reduce_scopes_with_nil_injection(scopes)
|
|
3190
2804
|
scopes.reduce { |a, b| join_with_nil_injection(a, b) }
|
|
3191
2805
|
end
|
|
3192
2806
|
|
|
3193
|
-
# ---------------------------------------------------------------
|
|
3194
|
-
# rescue variable binding helpers
|
|
2807
|
+
# --------------------------------------------------------------- rescue variable binding helpers
|
|
3195
2808
|
# ---------------------------------------------------------------
|
|
3196
2809
|
|
|
3197
|
-
# Returns `scope` extended with the rescue reference variable bound
|
|
3198
|
-
#
|
|
3199
|
-
# node carries no reference (bare `rescue` without `=> var`).
|
|
2810
|
+
# Returns `scope` extended with the rescue reference variable bound to the exception instance type. Leaves scope
|
|
2811
|
+
# unchanged when the node carries no reference (bare `rescue` without `=> var`).
|
|
3200
2812
|
def bind_rescue_reference(rescue_node, scope)
|
|
3201
2813
|
ref = rescue_node.reference
|
|
3202
2814
|
return scope unless ref.is_a?(Prism::LocalVariableTargetNode)
|
|
@@ -3204,11 +2816,9 @@ module Rigor
|
|
|
3204
2816
|
scope.with_local(ref.name, rescue_exception_type(rescue_node, scope))
|
|
3205
2817
|
end
|
|
3206
2818
|
|
|
3207
|
-
# Derives the exception instance type for a `RescueNode`. When the
|
|
3208
|
-
#
|
|
3209
|
-
# `StandardError
|
|
3210
|
-
# types are unioned. Falls back to `StandardError` for any class
|
|
3211
|
-
# that cannot be resolved to a `Singleton` type.
|
|
2819
|
+
# Derives the exception instance type for a `RescueNode`. When the exceptions list is empty (bare `rescue`) the
|
|
2820
|
+
# type is `StandardError`. When one or more exception classes are named the types are unioned. Falls back to
|
|
2821
|
+
# `StandardError` for any class that cannot be resolved to a `Singleton` type.
|
|
3212
2822
|
def rescue_exception_type(rescue_node, scope)
|
|
3213
2823
|
exceptions = rescue_node.exceptions
|
|
3214
2824
|
if exceptions.empty?
|
|
@@ -3222,20 +2832,18 @@ module Rigor
|
|
|
3222
2832
|
end
|
|
3223
2833
|
end
|
|
3224
2834
|
|
|
3225
|
-
# ---------------------------------------------------------------
|
|
3226
|
-
# `case/in` pattern variable binding helpers
|
|
2835
|
+
# --------------------------------------------------------------- `case/in` pattern variable binding helpers
|
|
3227
2836
|
# ---------------------------------------------------------------
|
|
3228
2837
|
|
|
3229
|
-
# Builds the entry scope for an `in` branch by injecting every
|
|
3230
|
-
#
|
|
2838
|
+
# Builds the entry scope for an `in` branch by injecting every variable captured by the pattern as a local
|
|
2839
|
+
# binding.
|
|
3231
2840
|
def apply_in_pattern_bindings(subject, pattern, scope)
|
|
3232
2841
|
bindings = collect_in_pattern_bindings(subject, pattern, scope)
|
|
3233
2842
|
bindings.reduce(scope) { |s, (name, type)| s.with_local(name, type) }
|
|
3234
2843
|
end
|
|
3235
2844
|
|
|
3236
|
-
# Returns an array of `[Symbol, Rigor::Type]` pairs for every
|
|
3237
|
-
#
|
|
3238
|
-
# contribute no bindings (fail-soft).
|
|
2845
|
+
# Returns an array of `[Symbol, Rigor::Type]` pairs for every variable captured by `pattern`. Unrecognised pattern
|
|
2846
|
+
# nodes contribute no bindings (fail-soft).
|
|
3239
2847
|
def collect_in_pattern_bindings(subject, pattern, scope)
|
|
3240
2848
|
case pattern
|
|
3241
2849
|
when Prism::CapturePatternNode
|
|
@@ -3288,11 +2896,9 @@ module Rigor
|
|
|
3288
2896
|
bindings
|
|
3289
2897
|
end
|
|
3290
2898
|
|
|
3291
|
-
# `[..., *rest, ...]` / `[*pre, x, *post]` capture an Array of
|
|
3292
|
-
#
|
|
3293
|
-
#
|
|
3294
|
-
# subject-aware element-type extraction (the binder doesn't see
|
|
3295
|
-
# the case subject).
|
|
2899
|
+
# `[..., *rest, ...]` / `[*pre, x, *post]` capture an Array of the unmatched elements; bind `rest` to
|
|
2900
|
+
# `Array[untyped]` rather than the previous bare `untyped`. Per-element typing waits on subject-aware element-type
|
|
2901
|
+
# extraction (the binder doesn't see the case subject).
|
|
3296
2902
|
def append_array_splat_binding(bindings, splat)
|
|
3297
2903
|
return unless splat.is_a?(Prism::SplatNode)
|
|
3298
2904
|
|
|
@@ -3302,10 +2908,8 @@ module Rigor
|
|
|
3302
2908
|
bindings << [target.name, Type::Combinator.nominal_of("Array", type_args: [Type::Combinator.untyped])]
|
|
3303
2909
|
end
|
|
3304
2910
|
|
|
3305
|
-
# `{ key:, **rest }` binds `rest` to a Hash whose keys are
|
|
3306
|
-
#
|
|
3307
|
-
# whose values are untyped (the binder can't see the subject's
|
|
3308
|
-
# value type).
|
|
2911
|
+
# `{ key:, **rest }` binds `rest` to a Hash whose keys are Symbols (the only legal key shape for a hash pattern)
|
|
2912
|
+
# and whose values are untyped (the binder can't see the subject's value type).
|
|
3309
2913
|
def hash_pattern_rest_type
|
|
3310
2914
|
Type::Combinator.nominal_of(
|
|
3311
2915
|
"Hash",
|
|
@@ -3313,14 +2917,12 @@ module Rigor
|
|
|
3313
2917
|
)
|
|
3314
2918
|
end
|
|
3315
2919
|
|
|
3316
|
-
# ---------------------------------------------------------------
|
|
3317
|
-
# named-capture regex binding (`MatchWriteNode`)
|
|
2920
|
+
# --------------------------------------------------------------- named-capture regex binding (`MatchWriteNode`)
|
|
3318
2921
|
# ---------------------------------------------------------------
|
|
3319
2922
|
|
|
3320
|
-
# `/(?<year>\d+)/ =~ str` — Prism emits a `MatchWriteNode` that
|
|
3321
|
-
#
|
|
3322
|
-
#
|
|
3323
|
-
# when the pattern doesn't match or the group didn't participate).
|
|
2923
|
+
# `/(?<year>\d+)/ =~ str` — Prism emits a `MatchWriteNode` that wraps the `=~` call and lists the named-capture
|
|
2924
|
+
# targets. Each target is bound to `String | nil` (the capture is absent as nil when the pattern doesn't match or
|
|
2925
|
+
# the group didn't participate).
|
|
3324
2926
|
def eval_match_write(node)
|
|
3325
2927
|
match_type, post_scope = sub_eval(node.call, scope)
|
|
3326
2928
|
string_or_nil = Type::Combinator.union(
|
|
@@ -3335,23 +2937,19 @@ module Rigor
|
|
|
3335
2937
|
[match_type, bound_scope]
|
|
3336
2938
|
end
|
|
3337
2939
|
|
|
3338
|
-
# ---------------------------------------------------------------
|
|
3339
|
-
# shared type conversion helper
|
|
2940
|
+
# --------------------------------------------------------------- shared type conversion helper
|
|
3340
2941
|
# ---------------------------------------------------------------
|
|
3341
2942
|
|
|
3342
|
-
# Converts a `Singleton[ClassName]` (the class object) to the
|
|
3343
|
-
#
|
|
3344
|
-
#
|
|
3345
|
-
# when the class could not be resolved).
|
|
2943
|
+
# Converts a `Singleton[ClassName]` (the class object) to the corresponding `Nominal[ClassName]` (an instance).
|
|
2944
|
+
# Falls back to `untyped` for carriers that are not Singleton (e.g. Dynamic[Top] when the class could not be
|
|
2945
|
+
# resolved).
|
|
3346
2946
|
def singleton_to_nominal(type)
|
|
3347
2947
|
type.is_a?(Type::Singleton) ? Type::Combinator.nominal_of(type.class_name) : Type::Combinator.untyped
|
|
3348
2948
|
end
|
|
3349
2949
|
|
|
3350
|
-
# Returns the type to bind for a `CapturePatternNode`'s target.
|
|
3351
|
-
#
|
|
3352
|
-
# `
|
|
3353
|
-
# alternate's resolved type. Anything else falls back to
|
|
3354
|
-
# `untyped` (the conservative legacy behaviour).
|
|
2950
|
+
# Returns the type to bind for a `CapturePatternNode`'s target. Plain class references collapse to the matching
|
|
2951
|
+
# `Nominal[T]`; `AlternationPatternNode` (`Integer | String => x`) unions every alternate's resolved type.
|
|
2952
|
+
# Anything else falls back to `untyped` (the conservative legacy behaviour).
|
|
3355
2953
|
def pattern_capture_type(value_node, scope)
|
|
3356
2954
|
if value_node.is_a?(Prism::AlternationPatternNode)
|
|
3357
2955
|
left = pattern_capture_type(value_node.left, scope)
|
|
@@ -3362,13 +2960,10 @@ module Rigor
|
|
|
3362
2960
|
end
|
|
3363
2961
|
end
|
|
3364
2962
|
|
|
3365
|
-
# `in PatternA | PatternB` — Ruby requires both alternates to
|
|
3366
|
-
#
|
|
3367
|
-
#
|
|
3368
|
-
#
|
|
3369
|
-
# Names that only one alternate contributes still surface (the
|
|
3370
|
-
# parser would have rejected the case at compile time, so by the
|
|
3371
|
-
# time we see it the user's intent is the merged set).
|
|
2963
|
+
# `in PatternA | PatternB` — Ruby requires both alternates to bind the same names, but the binder runs against the
|
|
2964
|
+
# AST and cannot enforce that. We collect bindings from each side and merge by name, unioning types when both
|
|
2965
|
+
# alternates contribute. Names that only one alternate contributes still surface (the parser would have rejected
|
|
2966
|
+
# the case at compile time, so by the time we see it the user's intent is the merged set).
|
|
3372
2967
|
def collect_alternation_pattern_bindings(subject, pattern, scope)
|
|
3373
2968
|
left = collect_in_pattern_bindings(subject, pattern.left, scope)
|
|
3374
2969
|
right = collect_in_pattern_bindings(subject, pattern.right, scope)
|