rigortype 0.2.6 → 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 +4 -3
- data/docs/manual/02-cli-reference.md +2 -1
- data/docs/manual/04-diagnostics.md +7 -4
- data/docs/manual/06-baseline.md +5 -2
- data/docs/manual/08-skills.md +21 -0
- 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 +45 -20
- 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 +70 -36
- 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 +433 -503
- 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 -1115
- 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 +208 -309
- data/lib/rigor/sig_gen/layout_index.rb +12 -20
- data/lib/rigor/sig_gen/method_candidate.rb +15 -15
- 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 +141 -173
- 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 +107 -83
- 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 -236
- 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 +20 -36
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +23 -30
- 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 +182 -328
- 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
- data/skills/rigor-ask/SKILL.md +21 -1
- data/skills/rigor-baseline-reduce/SKILL.md +16 -0
- data/skills/rigor-ci-setup/SKILL.md +96 -249
- data/skills/rigor-doctor/SKILL.md +39 -49
- data/skills/rigor-doctor/references/01-checks.md +52 -0
- data/skills/rigor-editor-setup/SKILL.md +14 -0
- data/skills/rigor-mcp-setup/SKILL.md +14 -0
- data/skills/rigor-monkeypatch-resolve/SKILL.md +15 -0
- data/skills/rigor-plugin-author/SKILL.md +16 -0
- data/skills/rigor-plugin-review/SKILL.md +174 -0
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +214 -0
- data/skills/rigor-plugin-tune/SKILL.md +21 -2
- data/skills/rigor-project-init/SKILL.md +16 -0
- data/skills/rigor-protection-uplift/SKILL.md +15 -0
- data/skills/rigor-rbs-setup/SKILL.md +15 -0
- data/skills/rigor-upgrade/SKILL.md +16 -0
- metadata +8 -4
|
@@ -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,15 +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.
|
|
785
|
-
def eval_begin_primary(node)
|
|
786
|
-
eval_begin_primary_under(node, scope)
|
|
787
|
-
end
|
|
788
|
-
|
|
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.
|
|
789
704
|
def eval_begin_primary_under(node, entry_scope)
|
|
790
705
|
body_type, body_scope =
|
|
791
706
|
if node.statements
|
|
@@ -802,17 +717,12 @@ module Rigor
|
|
|
802
717
|
end
|
|
803
718
|
end
|
|
804
719
|
|
|
805
|
-
# B2.1 — return a widened entry scope when at least one
|
|
806
|
-
#
|
|
807
|
-
#
|
|
808
|
-
# to the original entry. Returns nil when no widening is
|
|
809
|
-
# needed (no retry, or no rebinds reachable across the
|
|
810
|
-
# 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).
|
|
811
723
|
#
|
|
812
|
-
# Always-safe: the widening can only LOSE precision; it
|
|
813
|
-
#
|
|
814
|
-
# the Constant / shape carrier it widens from). Convergent
|
|
815
|
-
# 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
|
|
816
726
|
# widened form against the engine's current carrier set.
|
|
817
727
|
def widen_entry_for_retry(entry_scope, rescue_chain)
|
|
818
728
|
widened = nil
|
|
@@ -831,9 +741,8 @@ module Rigor
|
|
|
831
741
|
def arm_contains_retry?(node)
|
|
832
742
|
return false unless node.is_a?(Prism::Node)
|
|
833
743
|
return true if node.is_a?(Prism::RetryNode)
|
|
834
|
-
# Don't descend into nested blocks / defs / classes /
|
|
835
|
-
#
|
|
836
|
-
# 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.
|
|
837
746
|
return false if node.is_a?(Prism::DefNode) ||
|
|
838
747
|
node.is_a?(Prism::ClassNode) ||
|
|
839
748
|
node.is_a?(Prism::ModuleNode) ||
|
|
@@ -844,8 +753,7 @@ module Rigor
|
|
|
844
753
|
|
|
845
754
|
def absorb_retry_rebinds(accumulator, entry_scope, arm_post_scope)
|
|
846
755
|
scope_acc = accumulator
|
|
847
|
-
# Walk every local visible in either side, compare types,
|
|
848
|
-
# widen to Nominal envelope on a difference.
|
|
756
|
+
# Walk every local visible in either side, compare types, widen to Nominal envelope on a difference.
|
|
849
757
|
local_keys = arm_post_scope.locals.keys | entry_scope.locals.keys
|
|
850
758
|
local_keys.each do |name|
|
|
851
759
|
pre = entry_scope.local(name)
|
|
@@ -868,8 +776,7 @@ module Rigor
|
|
|
868
776
|
end
|
|
869
777
|
|
|
870
778
|
def retry_widened_type(pre, post)
|
|
871
|
-
# `pre` is nil when the local was introduced inside the
|
|
872
|
-
# 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
|
|
873
780
|
# primary body's entry — widen the post type itself.
|
|
874
781
|
envelope = nominal_envelope_for(post)
|
|
875
782
|
return envelope if pre.nil?
|
|
@@ -877,11 +784,9 @@ module Rigor
|
|
|
877
784
|
nominal_envelope_for(Type::Combinator.union(pre, envelope))
|
|
878
785
|
end
|
|
879
786
|
|
|
880
|
-
# Nominal envelope of a value type: widens Constant /
|
|
881
|
-
#
|
|
882
|
-
#
|
|
883
|
-
# `Union` of non-shape members, `Top`, `Dynamic`, `Bot`).
|
|
884
|
-
# 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.
|
|
885
790
|
def nominal_envelope_for(type)
|
|
886
791
|
members = type.is_a?(Type::Union) ? type.members : [type]
|
|
887
792
|
widened = members.map { |m| nominal_envelope_member(m) }
|
|
@@ -920,92 +825,75 @@ module Rigor
|
|
|
920
825
|
eval_branch_or_nil(node.statements, scope)
|
|
921
826
|
end
|
|
922
827
|
|
|
923
|
-
# `while pred; body; end` / `until pred; body; end`. The body
|
|
924
|
-
#
|
|
925
|
-
#
|
|
926
|
-
# types as `Constant[nil]` (Slice 3 phase 1), reflecting the
|
|
927
|
-
# 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.
|
|
928
831
|
def eval_loop(node)
|
|
929
832
|
_pred_type, post_pred = sub_eval(node.predicate, scope)
|
|
930
833
|
return [Type::Combinator.constant_of(nil), narrow_loop_exit_edge(node, post_pred)] if node.statements.nil?
|
|
931
834
|
|
|
932
|
-
# The historical single body pass joined with the pre-loop scope.
|
|
933
|
-
#
|
|
934
|
-
#
|
|
935
|
-
# widens `buf`'s Tuple), body-introduced locals' nil-injection, and
|
|
936
|
-
# 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
|
|
937
838
|
# rebound-local bindings it corrects.
|
|
938
839
|
#
|
|
939
|
-
# The pass runs under a break sink so a `break`-path binding
|
|
940
|
-
#
|
|
941
|
-
# 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.
|
|
942
842
|
break_targets, break_sink, body_scope = capture_loop_body_breaks(node.statements, post_pred)
|
|
943
843
|
base_scope = join_with_nil_injection(post_pred, body_scope)
|
|
944
844
|
|
|
945
845
|
rebound, body_first = loop_body_local_writes(node.statements, post_pred)
|
|
946
846
|
names = rebound + body_first
|
|
947
847
|
|
|
948
|
-
# Fast path: a loop whose body rebinds no local skips the rebind
|
|
949
|
-
#
|
|
950
|
-
#
|
|
951
|
-
# 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.
|
|
952
851
|
if names.empty?
|
|
953
852
|
fast = loop_content_writeback(node.statements, base_scope)
|
|
954
853
|
return [Type::Combinator.constant_of(nil), narrow_loop_exit_edge(node, fast)]
|
|
955
854
|
end
|
|
956
855
|
|
|
957
856
|
post_loop = converged_loop_scope(node, post_pred, base_scope, names, body_first)
|
|
958
|
-
# Recover `break`-path bindings the fall-through dropped (`flag = true;
|
|
959
|
-
#
|
|
857
|
+
# Recover `break`-path bindings the fall-through dropped (`flag = true; break` -> `flag` is `false | true`, not
|
|
858
|
+
# the stale `false`).
|
|
960
859
|
post_loop = join_break_scopes(post_loop, break_sink, break_targets, names)
|
|
961
860
|
post_loop = narrow_loop_exit_edge(node, post_loop)
|
|
962
861
|
[Type::Combinator.constant_of(nil), post_loop]
|
|
963
862
|
end
|
|
964
863
|
|
|
965
|
-
# The continuation scope for a loop whose body rebinds locals: the
|
|
966
|
-
#
|
|
967
|
-
# 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.
|
|
968
866
|
def converged_loop_scope(node, post_pred, base_scope, names, body_first)
|
|
969
|
-
# ADR-56 slice B — loop-body fixpoint. The body runs 0..N times and
|
|
970
|
-
#
|
|
971
|
-
#
|
|
972
|
-
#
|
|
973
|
-
#
|
|
974
|
-
#
|
|
975
|
-
# a pre-existing local seeds with its post-predicate binding; a local
|
|
976
|
-
# FIRST assigned inside the body seeds with `nil` so the 0-iteration
|
|
977
|
-
# 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.
|
|
978
873
|
result = loop_rebind_fixpoint(node, post_pred, names, body_first)
|
|
979
|
-
# Display-path re-record: the fixpoint's body re-evaluations fire
|
|
980
|
-
#
|
|
981
|
-
#
|
|
982
|
-
# stale pre-convergence constants. One extra pass from the converged
|
|
983
|
-
# 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.
|
|
984
877
|
record_converged_loop_body(node, post_pred, result, names, body_first)
|
|
985
878
|
post_loop = result.reduce(base_scope) { |acc, (name, type)| acc.with_local(name, type) }
|
|
986
|
-
# ADR-56 slice C — loop-body receiver-content element-type join. A loop
|
|
987
|
-
#
|
|
988
|
-
#
|
|
989
|
-
#
|
|
990
|
-
# `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).
|
|
991
883
|
loop_content_writeback(node.statements, post_loop)
|
|
992
884
|
end
|
|
993
885
|
|
|
994
|
-
# Item 4 — loop-exit predicate narrowing. A `while pred` / `until pred`
|
|
995
|
-
#
|
|
996
|
-
#
|
|
997
|
-
#
|
|
998
|
-
# io.gets; …; end; line.foo` reads `line` non-nil because the loop ran
|
|
999
|
-
# 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
|
|
1000
890
|
# `Narrowing.predicate_scopes` to the continuation scope.
|
|
1001
891
|
#
|
|
1002
|
-
# Guarded against `break`: a `break` exits the loop WITHOUT the predicate
|
|
1003
|
-
#
|
|
1004
|
-
#
|
|
1005
|
-
#
|
|
1006
|
-
#
|
|
1007
|
-
# nested-loop `break` is rare in predicate-assignment loops and the
|
|
1008
|
-
# 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.
|
|
1009
897
|
def narrow_loop_exit_edge(node, post_loop)
|
|
1010
898
|
return post_loop if loop_body_breaks?(node.statements)
|
|
1011
899
|
|
|
@@ -1013,10 +901,9 @@ module Rigor
|
|
|
1013
901
|
node.is_a?(Prism::UntilNode) ? truthy_scope : falsey_scope
|
|
1014
902
|
end
|
|
1015
903
|
|
|
1016
|
-
# True when the loop body can `break` out of THIS loop. Conservatively
|
|
1017
|
-
#
|
|
1018
|
-
#
|
|
1019
|
-
# 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).
|
|
1020
907
|
def loop_body_breaks?(statements)
|
|
1021
908
|
return false if statements.nil?
|
|
1022
909
|
|
|
@@ -1027,10 +914,8 @@ module Rigor
|
|
|
1027
914
|
found
|
|
1028
915
|
end
|
|
1029
916
|
|
|
1030
|
-
# A `break` inside one of these nested constructs targets the inner
|
|
1031
|
-
#
|
|
1032
|
-
# lexical loop — so the directly-targeting break scan does not descend
|
|
1033
|
-
# 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.
|
|
1034
919
|
BREAK_BOUNDARY_NODES = [
|
|
1035
920
|
Prism::ForNode, Prism::WhileNode, Prism::UntilNode,
|
|
1036
921
|
Prism::BlockNode, Prism::LambdaNode, Prism::DefNode,
|
|
@@ -1038,11 +923,9 @@ module Rigor
|
|
|
1038
923
|
].freeze
|
|
1039
924
|
private_constant :BREAK_BOUNDARY_NODES
|
|
1040
925
|
|
|
1041
|
-
# The `BreakNode`s that lexically target THIS loop — reachable from the
|
|
1042
|
-
#
|
|
1043
|
-
#
|
|
1044
|
-
# break scopes (the thread-local sink also collects breaks from nested
|
|
1045
|
-
# 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).
|
|
1046
929
|
def directly_targeting_breaks(statements)
|
|
1047
930
|
found = {}.compare_by_identity
|
|
1048
931
|
collect_direct_breaks(statements, found)
|
|
@@ -1060,10 +943,9 @@ module Rigor
|
|
|
1060
943
|
end
|
|
1061
944
|
end
|
|
1062
945
|
|
|
1063
|
-
# Installs a fresh thread-local break sink around `yield` (a loop-body
|
|
1064
|
-
#
|
|
1065
|
-
#
|
|
1066
|
-
# 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.
|
|
1067
949
|
def collect_break_scopes
|
|
1068
950
|
previous = Thread.current[BREAK_SINK_KEY]
|
|
1069
951
|
sink = []
|
|
@@ -1076,22 +958,19 @@ module Rigor
|
|
|
1076
958
|
[sink, result]
|
|
1077
959
|
end
|
|
1078
960
|
|
|
1079
|
-
# Runs a loop body's single pass under a break sink. Returns the
|
|
1080
|
-
#
|
|
1081
|
-
#
|
|
1082
|
-
# {#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`.
|
|
1083
964
|
def capture_loop_body_breaks(statements, entry)
|
|
1084
965
|
targets = directly_targeting_breaks(statements)
|
|
1085
966
|
sink, (_type, body_scope) = collect_break_scopes { sub_eval(statements, entry) }
|
|
1086
967
|
[targets, sink, body_scope]
|
|
1087
968
|
end
|
|
1088
969
|
|
|
1089
|
-
# Joins each directly-targeting break's body-written local bindings into
|
|
1090
|
-
# the
|
|
1091
|
-
#
|
|
1092
|
-
#
|
|
1093
|
-
# unions to itself; a break-only-written local is already present via the
|
|
1094
|
-
# 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.
|
|
1095
974
|
def join_break_scopes(continuation, sink, targeting, names)
|
|
1096
975
|
return continuation if sink.empty? || names.empty?
|
|
1097
976
|
|
|
@@ -1108,14 +987,11 @@ module Rigor
|
|
|
1108
987
|
end
|
|
1109
988
|
end
|
|
1110
989
|
|
|
1111
|
-
# Joins loop-body content mutations into the continuation collection
|
|
1112
|
-
#
|
|
1113
|
-
#
|
|
1114
|
-
#
|
|
1115
|
-
#
|
|
1116
|
-
# would be captured, an unsound under-approximation). Pre-state is
|
|
1117
|
-
# read from `post_loop` too. A loop body shares the surrounding scope,
|
|
1118
|
-
# 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).
|
|
1119
995
|
def loop_content_writeback(statements, post_loop)
|
|
1120
996
|
return post_loop if statements.nil?
|
|
1121
997
|
|
|
@@ -1132,12 +1008,9 @@ module Rigor
|
|
|
1132
1008
|
end
|
|
1133
1009
|
end
|
|
1134
1010
|
|
|
1135
|
-
# Re-evaluates the loop body once from the converged fixpoint
|
|
1136
|
-
#
|
|
1137
|
-
# the body
|
|
1138
|
-
# display-path-only `converged_loop_recording` flag so the check
|
|
1139
|
-
# path neither pays the extra body evaluation nor risks any
|
|
1140
|
-
# 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.
|
|
1141
1014
|
def record_converged_loop_body(node, post_pred, bindings, names, body_first)
|
|
1142
1015
|
return unless @converged_loop_recording && @on_enter
|
|
1143
1016
|
|
|
@@ -1145,11 +1018,10 @@ module Rigor
|
|
|
1145
1018
|
nil
|
|
1146
1019
|
end
|
|
1147
1020
|
|
|
1148
|
-
# Runs the slice-B loop-body rebind fixpoint, returning the per-name
|
|
1149
|
-
#
|
|
1150
|
-
#
|
|
1151
|
-
#
|
|
1152
|
-
# 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.
|
|
1153
1025
|
def loop_rebind_fixpoint(node, post_pred, names, body_first)
|
|
1154
1026
|
nil_const = Type::Combinator.constant_of(nil)
|
|
1155
1027
|
seed = names.to_h { |name| [name, post_pred.local(name) || nil_const] }
|
|
@@ -1161,13 +1033,10 @@ module Rigor
|
|
|
1161
1033
|
)
|
|
1162
1034
|
end
|
|
1163
1035
|
|
|
1164
|
-
# Names of locals the loop body can rebind, partitioned into those
|
|
1165
|
-
#
|
|
1166
|
-
#
|
|
1167
|
-
#
|
|
1168
|
-
# introduces no new binding scope — every write leaks to the
|
|
1169
|
-
# surrounding scope — so unlike a block there is no introduced-name
|
|
1170
|
-
# 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.
|
|
1171
1040
|
def loop_body_local_writes(statements, base_scope)
|
|
1172
1041
|
pre_existing = []
|
|
1173
1042
|
body_first = []
|
|
@@ -1184,30 +1053,22 @@ module Rigor
|
|
|
1184
1053
|
[pre_existing.uniq, body_first.uniq - pre_existing.uniq]
|
|
1185
1054
|
end
|
|
1186
1055
|
|
|
1187
|
-
# Evaluates the loop body once with each fixpoint-tracked local bound
|
|
1188
|
-
#
|
|
1189
|
-
# 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`.
|
|
1190
1058
|
#
|
|
1191
|
-
# The body runs from `post_pred` overlaid with the assumptions, then
|
|
1192
|
-
#
|
|
1193
|
-
#
|
|
1194
|
-
#
|
|
1195
|
-
#
|
|
1196
|
-
#
|
|
1197
|
-
#
|
|
1198
|
-
# and false-fire `possible nil receiver` on the guarded re-read. The
|
|
1199
|
-
# historical single body pass (which seeds these locals from their
|
|
1200
|
-
# never-nil pre-loop binding) did not need this; the fixpoint, which
|
|
1201
|
-
# 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.
|
|
1202
1066
|
#
|
|
1203
|
-
# A body-FIRST local (no pre-loop binding) is deliberately NOT overlaid
|
|
1204
|
-
#
|
|
1205
|
-
#
|
|
1206
|
-
#
|
|
1207
|
-
#
|
|
1208
|
-
# back into the body re-evaluation would leak it past a condition-form
|
|
1209
|
-
# assignment the engine does not thread into the branch (`if exps.size
|
|
1210
|
-
# > (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.
|
|
1211
1072
|
def loop_body_exit_bindings(node, post_pred, bindings, names, body_first)
|
|
1212
1073
|
overlaid = bindings.except(*body_first)
|
|
1213
1074
|
entry = overlaid.reduce(post_pred) { |acc, (name, type)| acc.with_local(name, type) }
|
|
@@ -1217,16 +1078,12 @@ module Rigor
|
|
|
1217
1078
|
names.to_h { |name| [name, exit_scope.local(name)] }
|
|
1218
1079
|
end
|
|
1219
1080
|
|
|
1220
|
-
# `for index in collection; body; end`. Unlike `each {}` blocks,
|
|
1221
|
-
#
|
|
1222
|
-
#
|
|
1223
|
-
# scope
|
|
1224
|
-
#
|
|
1225
|
-
#
|
|
1226
|
-
# with half-bound names degraded to `T | nil` via nil-injection.
|
|
1227
|
-
# The loop expression itself types as `Constant[nil]` (the common
|
|
1228
|
-
# case where no `break VALUE` is observed), matching the policy
|
|
1229
|
-
# `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`.
|
|
1230
1087
|
def eval_for(node)
|
|
1231
1088
|
coll_type, post_coll = sub_eval(node.collection, scope)
|
|
1232
1089
|
element_type = for_iteration_element_type(coll_type)
|
|
@@ -1236,10 +1093,9 @@ module Rigor
|
|
|
1236
1093
|
return [Type::Combinator.constant_of(nil), join_with_nil_injection(post_coll, body_entry)]
|
|
1237
1094
|
end
|
|
1238
1095
|
|
|
1239
|
-
# Run the body pass under a break sink so a `break`-path binding the
|
|
1240
|
-
#
|
|
1241
|
-
#
|
|
1242
|
-
# 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).
|
|
1243
1099
|
break_targets, break_sink, body_scope = capture_loop_body_breaks(node.statements, body_entry)
|
|
1244
1100
|
continuation = join_with_nil_injection(post_coll, body_scope)
|
|
1245
1101
|
pre_existing, body_first = loop_body_local_writes(node.statements, post_coll)
|
|
@@ -1247,15 +1103,11 @@ module Rigor
|
|
|
1247
1103
|
[Type::Combinator.constant_of(nil), continuation]
|
|
1248
1104
|
end
|
|
1249
1105
|
|
|
1250
|
-
# `for x in coll` is semantically `coll.each { |x| ... }`. We
|
|
1251
|
-
#
|
|
1252
|
-
#
|
|
1253
|
-
#
|
|
1254
|
-
#
|
|
1255
|
-
# `Hash[K, V]#each` yields `[K, V]` even when the receiver is
|
|
1256
|
-
# not a literal Hash carrier in our local lattice). When the
|
|
1257
|
-
# dispatcher returns nothing (no signature, unknown receiver)
|
|
1258
|
-
# 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.
|
|
1259
1111
|
def for_iteration_element_type(coll_type)
|
|
1260
1112
|
structural = collection_element_type(coll_type)
|
|
1261
1113
|
return structural unless structural.equal?(Type::Combinator.untyped)
|
|
@@ -1273,11 +1125,9 @@ module Rigor
|
|
|
1273
1125
|
Type::Combinator.untyped
|
|
1274
1126
|
end
|
|
1275
1127
|
|
|
1276
|
-
# Binds the `for` index variable(s) into `scope`. A single
|
|
1277
|
-
# `
|
|
1278
|
-
#
|
|
1279
|
-
# (`for a, b in pairs`) delegates to {MultiTargetBinder}, which
|
|
1280
|
-
# 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.
|
|
1281
1131
|
def bind_for_index(index_node, element_type, scope)
|
|
1282
1132
|
case index_node
|
|
1283
1133
|
when Prism::LocalVariableTargetNode
|
|
@@ -1290,13 +1140,10 @@ module Rigor
|
|
|
1290
1140
|
end
|
|
1291
1141
|
end
|
|
1292
1142
|
|
|
1293
|
-
# Extracts the per-iteration element type from a collection
|
|
1294
|
-
#
|
|
1295
|
-
#
|
|
1296
|
-
#
|
|
1297
|
-
# `[key, value]` pairs); `IntegerRange` yields `Integer`;
|
|
1298
|
-
# `Constant<Range>` reads the literal range's element class.
|
|
1299
|
-
# 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`.
|
|
1300
1147
|
def collection_element_type(type)
|
|
1301
1148
|
case type
|
|
1302
1149
|
when Type::Tuple
|
|
@@ -1341,27 +1188,18 @@ module Rigor
|
|
|
1341
1188
|
end
|
|
1342
1189
|
end
|
|
1343
1190
|
|
|
1344
|
-
# `a && b` / `a || b`. The LHS always runs, the RHS only
|
|
1345
|
-
#
|
|
1346
|
-
# `a
|
|
1347
|
-
#
|
|
1348
|
-
#
|
|
1349
|
-
#
|
|
1350
|
-
# the RHS still degrade to `T | nil`. The result type is
|
|
1351
|
-
# edge-aware: `a && b` can only produce the falsey fragment of
|
|
1352
|
-
# `a` when the RHS is skipped, while `a || b` can only produce
|
|
1353
|
-
# 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.
|
|
1354
1197
|
#
|
|
1355
|
-
# When the RHS is a terminating branch — it `raise`s /
|
|
1356
|
-
#
|
|
1357
|
-
#
|
|
1358
|
-
#
|
|
1359
|
-
# `
|
|
1360
|
-
# LHS-skipped edge alone: `a or raise` only survives when `a`
|
|
1361
|
-
# was truthy, so subsequent statements observe `a` narrowed to
|
|
1362
|
-
# its truthy fragment; the symmetric `a and raise` survives
|
|
1363
|
-
# only when `a` was falsey. Same shape as the `eval_if` /
|
|
1364
|
-
# `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.
|
|
1365
1203
|
def eval_and_or(node)
|
|
1366
1204
|
left_type, left_scope = sub_eval(node.left, scope)
|
|
1367
1205
|
truthy_left, falsey_left = Narrowing.predicate_scopes(node.left, left_scope)
|
|
@@ -1369,8 +1207,7 @@ module Rigor
|
|
|
1369
1207
|
right_type, right_scope = sub_eval(node.right, rhs_entry)
|
|
1370
1208
|
|
|
1371
1209
|
if branch_terminates?(node.right, right_type)
|
|
1372
|
-
# Control never reaches any statement after `a or raise`
|
|
1373
|
-
# 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.
|
|
1374
1211
|
surviving_type =
|
|
1375
1212
|
if node.is_a?(Prism::AndNode)
|
|
1376
1213
|
Narrowing.narrow_falsey(left_type)
|
|
@@ -1393,23 +1230,18 @@ module Rigor
|
|
|
1393
1230
|
]
|
|
1394
1231
|
end
|
|
1395
1232
|
|
|
1396
|
-
# `(body)`. Threads scope through the inner expression so
|
|
1397
|
-
# `(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]`.
|
|
1398
1234
|
def eval_parentheses(node)
|
|
1399
1235
|
return [Type::Combinator.constant_of(nil), scope] if node.body.nil?
|
|
1400
1236
|
|
|
1401
1237
|
sub_eval(node.body, scope)
|
|
1402
1238
|
end
|
|
1403
1239
|
|
|
1404
|
-
# `class Foo; body; end` and `module Foo; body; end`. The class
|
|
1405
|
-
#
|
|
1406
|
-
#
|
|
1407
|
-
#
|
|
1408
|
-
#
|
|
1409
|
-
# definition does not bind any local in the enclosing scope.
|
|
1410
|
-
# The class body's value is the value of its last statement
|
|
1411
|
-
# (`Constant[nil]` for an empty body); we discard the body's
|
|
1412
|
-
# 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.
|
|
1413
1245
|
def eval_class_or_module(node)
|
|
1414
1246
|
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
1415
1247
|
new_context = @class_context + [ClassFrame.new(name: name, singleton: false)]
|
|
@@ -1417,45 +1249,30 @@ module Rigor
|
|
|
1417
1249
|
[body_type, scope]
|
|
1418
1250
|
end
|
|
1419
1251
|
|
|
1420
|
-
# `class << expr; body; end`. When `expr` is `self`, the body
|
|
1421
|
-
#
|
|
1422
|
-
#
|
|
1423
|
-
#
|
|
1424
|
-
# `instance_method`. For non-`self` expressions we cannot
|
|
1425
|
-
# statically resolve the receiver, so we keep the existing
|
|
1426
|
-
# context and accept that nested defs degrade to the
|
|
1427
|
-
# `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.
|
|
1428
1256
|
def eval_singleton_class(node)
|
|
1429
1257
|
new_context = singleton_context_for(node)
|
|
1430
1258
|
body_type, _body_scope = eval_class_body(node, new_context)
|
|
1431
1259
|
[body_type, scope]
|
|
1432
1260
|
end
|
|
1433
1261
|
|
|
1434
|
-
# `def name(params); body; end`. Builds the method-entry scope
|
|
1435
|
-
#
|
|
1436
|
-
# `
|
|
1437
|
-
#
|
|
1438
|
-
# scope is left unchanged: a `def` does not introduce a binding
|
|
1439
|
-
# in its enclosing scope. Ruby evaluates `def` to the method's
|
|
1440
|
-
# 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]`.
|
|
1441
1266
|
def eval_def(node)
|
|
1442
1267
|
body_scope = build_method_entry_scope(node)
|
|
1443
|
-
# Parameter default value expressions (e.g. `self.x` in
|
|
1444
|
-
# `def
|
|
1445
|
-
#
|
|
1446
|
-
#
|
|
1447
|
-
#
|
|
1448
|
-
#
|
|
1449
|
-
#
|
|
1450
|
-
#
|
|
1451
|
-
# `self_type` is `singleton(C)`), making `self.foo` look
|
|
1452
|
-
# like a singleton-side call. Observed surfacing 915 false
|
|
1453
|
-
# positives in `prism-1.9.0`'s auto-generated `copy`
|
|
1454
|
-
# methods alone.
|
|
1455
|
-
# A nested `def` is a return barrier: its body's `return`s belong to
|
|
1456
|
-
# the inner method, not the one currently being inferred. Suspend the
|
|
1457
|
-
# return sink across the nested body so `eval_return` does not record
|
|
1458
|
-
# 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.
|
|
1459
1276
|
outer_sink = Thread.current[RETURN_SINK_KEY]
|
|
1460
1277
|
Thread.current[RETURN_SINK_KEY] = nil
|
|
1461
1278
|
begin
|
|
@@ -1467,145 +1284,98 @@ module Rigor
|
|
|
1467
1284
|
[Type::Combinator.constant_of(node.name), scope]
|
|
1468
1285
|
end
|
|
1469
1286
|
|
|
1470
|
-
# `recv.foo(args) { |params| body }` and friends. The call
|
|
1471
|
-
#
|
|
1472
|
-
#
|
|
1473
|
-
#
|
|
1474
|
-
# scope from the receiving method's RBS signature, types the
|
|
1475
|
-
# block body, and threads the body's type into
|
|
1476
|
-
# `MethodDispatcher.dispatch`'s `block_type:` so generic
|
|
1477
|
-
# 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
|
|
1478
1291
|
# `Array[String]`).
|
|
1479
1292
|
#
|
|
1480
|
-
# The handler still re-evaluates the block under its entry
|
|
1481
|
-
#
|
|
1482
|
-
#
|
|
1483
|
-
#
|
|
1484
|
-
# inside the block body. The receiver and arguments still
|
|
1485
|
-
# 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.
|
|
1486
1297
|
def eval_call(node)
|
|
1487
1298
|
call_type = scope.type_of(node, tracer: tracer)
|
|
1488
|
-
# ADR-56 slice C (B3) — `each_with_object(memo) { |x, acc| acc << … }`
|
|
1489
|
-
#
|
|
1490
|
-
#
|
|
1491
|
-
# 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.
|
|
1492
1302
|
call_type = each_with_object_return(node, call_type)
|
|
1493
1303
|
evaluate_block_if_present(node)
|
|
1494
|
-
# `ruby2_keywords def foo(...)` (and similar wrappers like
|
|
1495
|
-
#
|
|
1496
|
-
#
|
|
1497
|
-
#
|
|
1498
|
-
# `
|
|
1499
|
-
#
|
|
1500
|
-
# scope
|
|
1501
|
-
# `self_type = singleton(C)` from `ScopeIndexer.propagate`,
|
|
1502
|
-
# so `self.helper` inside reports `undefined method 'helper'
|
|
1503
|
-
# for singleton(C)`. Walking each argument-position def under
|
|
1504
|
-
# the current evaluator (not a sub_eval — the def's effects
|
|
1505
|
-
# do not bind into the surrounding scope) populates the
|
|
1506
|
-
# scope index with the correct instance / singleton
|
|
1507
|
-
# `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.
|
|
1508
1311
|
evaluate_def_arguments(node)
|
|
1509
1312
|
post_scope = record_closure_escape_if_any(node)
|
|
1510
|
-
# ADR-56 slice A — non-escaping block captured-local write-back.
|
|
1511
|
-
#
|
|
1512
|
-
#
|
|
1513
|
-
#
|
|
1514
|
-
# § "Fact stability and mutation" names captured locals a
|
|
1515
|
-
# first-class invalidation category. (The escaping / unknown path
|
|
1516
|
-
# 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`.)
|
|
1517
1317
|
post_scope = write_back_block_captures(node, post_scope)
|
|
1518
1318
|
post_scope = apply_rbs_extended_assertions(node, post_scope)
|
|
1519
1319
|
post_scope = apply_plugin_assertions(node, post_scope)
|
|
1520
1320
|
post_scope = apply_rspec_matcher_narrowing(node, post_scope)
|
|
1521
|
-
# Flow-folding G1 / G2 — widen a local- or instance-variable
|
|
1522
|
-
#
|
|
1523
|
-
#
|
|
1524
|
-
# carrier (`Tuple` / `HashShape`) from outliving its
|
|
1525
|
-
# justification when the value is mutated. Always-safe
|
|
1526
|
-
# (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).
|
|
1527
1324
|
post_scope = MutationWidening.widen_after_call(call_node: node, current_scope: post_scope)
|
|
1528
|
-
# ADR-57 slice 3 work-item 1 (cross-method-boundary variant). When a
|
|
1529
|
-
#
|
|
1530
|
-
#
|
|
1531
|
-
#
|
|
1532
|
-
# `
|
|
1533
|
-
# the
|
|
1534
|
-
#
|
|
1535
|
-
# seed and `options.fetch(:mode)` folds to a wrong constant. Precise:
|
|
1536
|
-
# fires only when the resolved callee actually escape-mutates that
|
|
1537
|
-
# parameter (not for every self-call), and sound — only loses
|
|
1538
|
-
# 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.
|
|
1539
1332
|
post_scope = widen_callee_escaped_argument_captures(node, post_scope)
|
|
1540
|
-
# Same always-safe rationale as `widen_after_call` above —
|
|
1541
|
-
#
|
|
1542
|
-
# 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 }`).
|
|
1543
1335
|
post_scope = MutationWidening.widen_after_block(call_node: node, outer_scope: post_scope)
|
|
1544
|
-
# ADR-56 slice C — receiver-content element-type join. Joins
|
|
1545
|
-
#
|
|
1546
|
-
#
|
|
1547
|
-
# types `Array[0 | Integer]`, not `Array[0]`. Same always-safe
|
|
1548
|
-
# 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).
|
|
1549
1339
|
post_scope = content_writeback_block_captures(node, post_scope)
|
|
1550
|
-
# Indexed-collection narrowing — drop any
|
|
1551
|
-
# `
|
|
1552
|
-
#
|
|
1553
|
-
# same slot or any other mutator runs against the
|
|
1554
|
-
# 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).
|
|
1555
1343
|
post_scope = IndexedNarrowing.invalidate_after_call(call_node: node, current_scope: post_scope)
|
|
1556
|
-
# Single-hop method-chain narrowing — drop every
|
|
1557
|
-
#
|
|
1558
|
-
# outer
|
|
1559
|
-
#
|
|
1560
|
-
# notes). Calls whose outer receiver is itself a chain
|
|
1561
|
-
# node (e.g. `x.last << y`) do NOT drop narrowings
|
|
1562
|
-
# keyed on `x` — only direct calls against the root
|
|
1563
|
-
# 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.
|
|
1564
1348
|
post_scope = IndexedNarrowing.invalidate_chain_after_call(call_node: node, current_scope: post_scope)
|
|
1565
|
-
# B2.2 — intervening method call ivar invalidation.
|
|
1566
|
-
#
|
|
1567
|
-
# ivar
|
|
1568
|
-
#
|
|
1569
|
-
#
|
|
1570
|
-
# seed back to the seed itself, so a subsequent
|
|
1571
|
-
# `if @flag` predicate observes the seed's union (not the
|
|
1572
|
-
# pre-call narrowed value). Always-safe (only widens; no
|
|
1573
|
-
# new facts). See [`docs/CURRENT_WORK.md`](../../../docs/CURRENT_WORK.md)
|
|
1574
|
-
# § "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.
|
|
1575
1354
|
post_scope = invalidate_ivars_for_intervening_call(node, post_scope)
|
|
1576
|
-
# C1 — regex match-data globals (`$~`, `$1..$9`, `$&`, …) are
|
|
1577
|
-
#
|
|
1578
|
-
#
|
|
1579
|
-
#
|
|
1580
|
-
#
|
|
1581
|
-
# unknown-receiver call whose body we cannot prove match-free).
|
|
1582
|
-
# A call provably match-free on a known receiver — `$3.to_i`,
|
|
1583
|
-
# `year < 50` — does NOT clobber, so the multi-statement
|
|
1584
|
-
# `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
|
|
1585
1360
|
# while a genuinely interposed match still invalidates.
|
|
1586
1361
|
post_scope = post_scope.forget_match_globals if match_capable_call?(node)
|
|
1587
1362
|
[call_type, post_scope]
|
|
1588
1363
|
end
|
|
1589
1364
|
|
|
1590
|
-
# Method names that (may) run a regex match and therefore rebind
|
|
1591
|
-
#
|
|
1592
|
-
# 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.
|
|
1593
1367
|
MATCH_CAPABLE_METHODS = %i[
|
|
1594
1368
|
=~ match match? gsub gsub! sub sub! scan split slice slice!
|
|
1595
1369
|
[] partition rpartition index rindex === grep grep_v
|
|
1596
1370
|
].freeze
|
|
1597
1371
|
private_constant :MATCH_CAPABLE_METHODS
|
|
1598
1372
|
|
|
1599
|
-
# True when `node` could rebind the regex match-data globals:
|
|
1600
|
-
#
|
|
1601
|
-
#
|
|
1602
|
-
#
|
|
1603
|
-
#
|
|
1604
|
-
#
|
|
1605
|
-
# narrowed globals. The over-approximation is one-directional: a
|
|
1606
|
-
# user method that secretly matches on an explicit receiver is the
|
|
1607
|
-
# only escape, and re-narrowing on the next real guard recovers —
|
|
1608
|
-
# 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.
|
|
1609
1379
|
def match_capable_call?(node)
|
|
1610
1380
|
return true unless node.is_a?(Prism::CallNode)
|
|
1611
1381
|
return true if MATCH_CAPABLE_METHODS.include?(node.name)
|
|
@@ -1614,12 +1384,9 @@ module Rigor
|
|
|
1614
1384
|
receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
1615
1385
|
end
|
|
1616
1386
|
|
|
1617
|
-
# Returns a scope with each ivar's narrowed local binding
|
|
1618
|
-
#
|
|
1619
|
-
#
|
|
1620
|
-
# class (implicit-self or explicit `self.foo`). External-
|
|
1621
|
-
# receiver calls (`obj.method`) cannot reach the caller's
|
|
1622
|
-
# 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.
|
|
1623
1390
|
def invalidate_ivars_for_intervening_call(call_node, current_scope)
|
|
1624
1391
|
return current_scope unless intervening_call_candidate?(call_node)
|
|
1625
1392
|
|
|
@@ -1659,11 +1426,9 @@ module Rigor
|
|
|
1659
1426
|
end
|
|
1660
1427
|
end
|
|
1661
1428
|
|
|
1662
|
-
# v0.0.3 — recognises a small catalogue of RSpec
|
|
1663
|
-
#
|
|
1664
|
-
#
|
|
1665
|
-
# matched purely on AST shape; no RBS for RSpec is
|
|
1666
|
-
# 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).
|
|
1667
1432
|
#
|
|
1668
1433
|
# Recognised today:
|
|
1669
1434
|
#
|
|
@@ -1678,11 +1443,9 @@ module Rigor
|
|
|
1678
1443
|
# `be_an_instance_of`, subtype-permitting
|
|
1679
1444
|
# otherwise).
|
|
1680
1445
|
#
|
|
1681
|
-
# Anything else is silently passed through. Symmetric
|
|
1682
|
-
#
|
|
1683
|
-
#
|
|
1684
|
-
# modelled: they are rarely useful in practice and
|
|
1685
|
-
# 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.
|
|
1686
1449
|
def apply_rspec_matcher_narrowing(call_node, current_scope)
|
|
1687
1450
|
narrow = rspec_matcher_narrowing_request(call_node)
|
|
1688
1451
|
return current_scope if narrow.nil?
|
|
@@ -1695,10 +1458,8 @@ module Rigor
|
|
|
1695
1458
|
current_scope.with_local(local_name, narrowed)
|
|
1696
1459
|
end
|
|
1697
1460
|
|
|
1698
|
-
# Decodes an `expect(x).<chain>` outer call into a
|
|
1699
|
-
#
|
|
1700
|
-
# not recognised. The hash carries `:local` (the local
|
|
1701
|
-
# 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.
|
|
1702
1463
|
def rspec_matcher_narrowing_request(call_node)
|
|
1703
1464
|
local_name = rspec_expectation_target(call_node)
|
|
1704
1465
|
return nil if local_name.nil?
|
|
@@ -1729,8 +1490,7 @@ module Rigor
|
|
|
1729
1490
|
end
|
|
1730
1491
|
end
|
|
1731
1492
|
|
|
1732
|
-
# `be_a` / `be_kind_of` / `be_an_instance_of` accept a
|
|
1733
|
-
# single class argument — either a `ConstantReadNode`
|
|
1493
|
+
# `be_a` / `be_kind_of` / `be_an_instance_of` accept a single class argument — either a `ConstantReadNode`
|
|
1734
1494
|
# (`Integer`) or a `ConstantPathNode` (`Rigor::Type::Nominal`).
|
|
1735
1495
|
def rspec_be_a_narrow(matcher, local_name, exact:)
|
|
1736
1496
|
args = matcher.arguments&.arguments || []
|
|
@@ -1753,11 +1513,8 @@ module Rigor
|
|
|
1753
1513
|
end
|
|
1754
1514
|
end
|
|
1755
1515
|
|
|
1756
|
-
# Returns the local name passed to `expect(...)` when
|
|
1757
|
-
#
|
|
1758
|
-
# or nil otherwise. Centralised so each per-matcher
|
|
1759
|
-
# decoder can short-circuit on a non-matching outer
|
|
1760
|
-
# 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.
|
|
1761
1518
|
def rspec_expectation_target(call_node)
|
|
1762
1519
|
receiver = call_node.receiver
|
|
1763
1520
|
return nil unless receiver.is_a?(Prism::CallNode) && receiver.name == :expect
|
|
@@ -1780,10 +1537,8 @@ module Rigor
|
|
|
1780
1537
|
matcher
|
|
1781
1538
|
end
|
|
1782
1539
|
|
|
1783
|
-
# True when `call_node`'s sole argument is an
|
|
1784
|
-
#
|
|
1785
|
-
# no positional arguments — used by the no-arg
|
|
1786
|
-
# 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`).
|
|
1787
1542
|
def rspec_matcher_argument?(call_node, matcher_name)
|
|
1788
1543
|
matcher = rspec_matcher_node(call_node)
|
|
1789
1544
|
return false if matcher.nil?
|
|
@@ -1792,16 +1547,11 @@ module Rigor
|
|
|
1792
1547
|
matcher.arguments.nil? || matcher.arguments.arguments.empty?
|
|
1793
1548
|
end
|
|
1794
1549
|
|
|
1795
|
-
# Slice 4b-2 (ADR-7 § "Slice 4-A/4-B") — applies the
|
|
1796
|
-
#
|
|
1797
|
-
# `
|
|
1798
|
-
# `
|
|
1799
|
-
#
|
|
1800
|
-
# rows for `:always` assert directives (the slice-4a
|
|
1801
|
-
# routing places conditional asserts on `truthy_facts` /
|
|
1802
|
-
# `falsey_facts`, which `Narrowing.predicate_scopes`
|
|
1803
|
-
# consumes). Plugin `:always` assertions are handled by
|
|
1804
|
-
# 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.
|
|
1805
1555
|
def apply_rbs_extended_assertions(call_node, current_scope)
|
|
1806
1556
|
method_def = resolve_call_method(call_node, current_scope)
|
|
1807
1557
|
return current_scope if method_def.nil?
|
|
@@ -1818,31 +1568,20 @@ module Rigor
|
|
|
1818
1568
|
end
|
|
1819
1569
|
end
|
|
1820
1570
|
|
|
1821
|
-
# ADR-7 § "Slice 4-A" / T.bind priority slice 2 — applies
|
|
1822
|
-
# the
|
|
1823
|
-
#
|
|
1824
|
-
#
|
|
1825
|
-
# downstream narrowing path (`apply_post_return_fact` →
|
|
1826
|
-
# `apply_self_post_return_fact`) are the same; only the
|
|
1827
|
-
# *source* of the bundle changes (RBS::Extended vs the
|
|
1828
|
-
# 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`).
|
|
1829
1575
|
#
|
|
1830
|
-
# `:self`-targeted facts narrow `scope.self_type` for the
|
|
1831
|
-
#
|
|
1832
|
-
#
|
|
1833
|
-
# to the rest of the block — exactly the contract Sorbet's
|
|
1834
|
-
# `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.
|
|
1835
1579
|
#
|
|
1836
|
-
# `:parameter`-targeted facts only land when the called
|
|
1837
|
-
#
|
|
1838
|
-
#
|
|
1839
|
-
#
|
|
1840
|
-
#
|
|
1841
|
-
# plugin's own diagnostics_for_file path covers those
|
|
1842
|
-
# cases. The full plugin-side parameter-targeting story
|
|
1843
|
-
# (PHPStan-style Type-Specifying Extensions on
|
|
1844
|
-
# plugin-recognised calls) lives behind a follow-up slice
|
|
1845
|
-
# 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.
|
|
1846
1585
|
def apply_plugin_assertions(call_node, current_scope)
|
|
1847
1586
|
registry = current_scope.environment&.plugin_registry
|
|
1848
1587
|
return current_scope if registry.nil? || registry.empty?
|
|
@@ -1860,16 +1599,14 @@ module Rigor
|
|
|
1860
1599
|
end
|
|
1861
1600
|
end
|
|
1862
1601
|
|
|
1863
|
-
# ADR-37 slice 2 / ADR-52 WD3 — gathers each plugin's post-return
|
|
1864
|
-
#
|
|
1865
|
-
#
|
|
1866
|
-
# 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.
|
|
1867
1605
|
EMPTY_CONTRIBUTIONS = [].freeze
|
|
1868
1606
|
private_constant :EMPTY_CONTRIBUTIONS
|
|
1869
1607
|
|
|
1870
|
-
# Fast-exit guard: skip if no plugin declares a `narrowing_facts` rule, or if
|
|
1871
|
-
#
|
|
1872
|
-
# `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.
|
|
1873
1610
|
def collect_plugin_contributions(registry, call_node, current_scope)
|
|
1874
1611
|
index = registry.contribution_index
|
|
1875
1612
|
relevant = index.for_statement
|
|
@@ -1881,10 +1618,9 @@ module Rigor
|
|
|
1881
1618
|
collect_gated_statement_contributions(index, relevant, name, call_node, current_scope)
|
|
1882
1619
|
end
|
|
1883
1620
|
|
|
1884
|
-
# ADR-37 slice 2 / ADR-52 WD1 — post-gate walk in registry order.
|
|
1885
|
-
#
|
|
1886
|
-
#
|
|
1887
|
-
# 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.
|
|
1888
1624
|
def collect_gated_statement_contributions(index, relevant, name, call_node, current_scope)
|
|
1889
1625
|
result = nil
|
|
1890
1626
|
relevant.each do |plugin|
|
|
@@ -1927,19 +1663,13 @@ module Rigor
|
|
|
1927
1663
|
end
|
|
1928
1664
|
end
|
|
1929
1665
|
|
|
1930
|
-
# Slice 4b-2 — applies a single post-return Fact to the
|
|
1931
|
-
#
|
|
1932
|
-
#
|
|
1933
|
-
# narrowing logic via `Narrowing.narrow_for_fact` so the
|
|
1934
|
-
# predicate / assert / plugin paths all converge on the
|
|
1935
|
-
# 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.
|
|
1936
1669
|
#
|
|
1937
|
-
# v0.1.8 Pillar 2 Slice 1 added the `:local` target_kind
|
|
1938
|
-
#
|
|
1939
|
-
#
|
|
1940
|
-
# local in the surrounding scope, bypassing the
|
|
1941
|
-
# parameter-name lookup that requires an authoritative RBS
|
|
1942
|
-
# 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).
|
|
1943
1673
|
def apply_post_return_fact(fact, call_node, current_scope, method_def)
|
|
1944
1674
|
return apply_local_post_return_fact(fact, current_scope) if fact.target_kind == :local
|
|
1945
1675
|
|
|
@@ -1955,12 +1685,9 @@ module Rigor
|
|
|
1955
1685
|
current_scope.with_local(local_name, narrowed)
|
|
1956
1686
|
end
|
|
1957
1687
|
|
|
1958
|
-
# v0.1.8 Pillar 2 Slice 1 — narrows the named local directly
|
|
1959
|
-
#
|
|
1960
|
-
#
|
|
1961
|
-
# source. Silently no-ops when the local is unbound in the
|
|
1962
|
-
# current scope (the plugin's named local may have already
|
|
1963
|
-
# 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).
|
|
1964
1691
|
def apply_local_post_return_fact(fact, current_scope)
|
|
1965
1692
|
local_name = fact.target_name
|
|
1966
1693
|
current_type = current_scope.local(local_name)
|
|
@@ -1970,9 +1697,8 @@ module Rigor
|
|
|
1970
1697
|
current_scope.with_local(local_name, narrowed)
|
|
1971
1698
|
end
|
|
1972
1699
|
|
|
1973
|
-
# v0.1.1 Track 1 slice 3 — `assert self is T` post-return
|
|
1974
|
-
#
|
|
1975
|
-
# `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`).
|
|
1976
1702
|
def apply_self_post_return_fact(fact, receiver_node, current_scope)
|
|
1977
1703
|
case receiver_node
|
|
1978
1704
|
when nil, Prism::SelfNode
|
|
@@ -1998,8 +1724,7 @@ module Rigor
|
|
|
1998
1724
|
end
|
|
1999
1725
|
end
|
|
2000
1726
|
|
|
2001
|
-
# `:self` routes to the call receiver; otherwise we look
|
|
2002
|
-
# 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.
|
|
2003
1728
|
def fact_target_node(fact, call_node, method_def)
|
|
2004
1729
|
if fact.target_kind == :self
|
|
2005
1730
|
call_node.receiver
|
|
@@ -2009,12 +1734,9 @@ module Rigor
|
|
|
2009
1734
|
end
|
|
2010
1735
|
|
|
2011
1736
|
def lookup_post_return_arg(call_node, method_def, target_name)
|
|
2012
|
-
# Plugin-source contributions arrive without an
|
|
2013
|
-
#
|
|
2014
|
-
#
|
|
2015
|
-
# to "no narrow" in that case — the wider plugin-side
|
|
2016
|
-
# parameter mapping (`:local` / `:argument_at`) is a
|
|
2017
|
-
# 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.
|
|
2018
1740
|
return nil if method_def.nil?
|
|
2019
1741
|
|
|
2020
1742
|
arguments = call_node.arguments&.arguments || []
|
|
@@ -2034,8 +1756,8 @@ module Rigor
|
|
|
2034
1756
|
sub_eval(block, block_entry)
|
|
2035
1757
|
end
|
|
2036
1758
|
|
|
2037
|
-
# Slice 6 phase C sub-phase 3b/3c. When the call carries a
|
|
2038
|
-
#
|
|
1759
|
+
# Slice 6 phase C sub-phase 3b/3c. When the call carries a block whose receiving method is NOT proven
|
|
1760
|
+
# non-escaping:
|
|
2039
1761
|
#
|
|
2040
1762
|
# - 3b: attach a `dynamic_origin` `closure_escape` fact to the
|
|
2041
1763
|
# post-call scope so consumers can see that the closure may
|
|
@@ -2049,19 +1771,15 @@ module Rigor
|
|
|
2049
1771
|
# only reads (without writing) are also untouched: read-only
|
|
2050
1772
|
# captures cannot rebind the outer variable.
|
|
2051
1773
|
#
|
|
2052
|
-
# A `:non_escaping` classification (or any block-less call)
|
|
2053
|
-
# leaves the post-call scope unchanged.
|
|
1774
|
+
# A `:non_escaping` classification (or any block-less call) leaves the post-call scope unchanged.
|
|
2054
1775
|
def record_closure_escape_if_any(node)
|
|
2055
|
-
# ADR-57 slice 3 work-item 1: an escaping block can also be attached
|
|
2056
|
-
# to
|
|
2057
|
-
#
|
|
2058
|
-
#
|
|
2059
|
-
#
|
|
2060
|
-
#
|
|
2061
|
-
#
|
|
2062
|
-
# Fold each escaping receiver-chain block's content widening into the
|
|
2063
|
-
# continuation here so the captured collection is floored regardless of
|
|
2064
|
-
# 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.
|
|
2065
1783
|
post_scope = widen_escaping_receiver_chain_captures(node, scope)
|
|
2066
1784
|
|
|
2067
1785
|
return post_scope unless node.block.is_a?(Prism::BlockNode)
|
|
@@ -2082,15 +1800,13 @@ module Rigor
|
|
|
2082
1800
|
)
|
|
2083
1801
|
end
|
|
2084
1802
|
|
|
2085
|
-
# Floor each caller-argument local whose matching parameter the resolved
|
|
2086
|
-
#
|
|
2087
|
-
#
|
|
2088
|
-
# "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.
|
|
2089
1806
|
def widen_callee_escaped_argument_captures(node, base_scope)
|
|
2090
1807
|
# Apply to the statement call AND every call in its receiver chain: the
|
|
2091
|
-
# `build_option_parser(options).parse!(argv)` idiom puts the escape-
|
|
2092
|
-
#
|
|
2093
|
-
# 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.
|
|
2094
1810
|
acc = floor_callee_escaped_args_for_call(node, base_scope)
|
|
2095
1811
|
receiver = node.receiver
|
|
2096
1812
|
while receiver.is_a?(Prism::CallNode)
|
|
@@ -2102,9 +1818,8 @@ module Rigor
|
|
|
2102
1818
|
|
|
2103
1819
|
def floor_callee_escaped_args_for_call(node, base_scope)
|
|
2104
1820
|
return base_scope unless self_dispatch_call?(node)
|
|
2105
|
-
# Fast path — the floor only ever touches a local passed as an
|
|
2106
|
-
#
|
|
2107
|
-
# 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).
|
|
2108
1823
|
return base_scope unless call_passes_local_argument?(node)
|
|
2109
1824
|
|
|
2110
1825
|
def_node = resolve_self_callee_def(node)
|
|
@@ -2116,11 +1831,9 @@ module Rigor
|
|
|
2116
1831
|
floor_arguments_at_positions(node, escaped, base_scope)
|
|
2117
1832
|
end
|
|
2118
1833
|
|
|
2119
|
-
# The user def a self-dispatch `node` resolves to in the enclosing class,
|
|
2120
|
-
#
|
|
2121
|
-
#
|
|
2122
|
-
# off the hot path for the overwhelming majority of self-calls that
|
|
2123
|
-
# 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.
|
|
2124
1837
|
def resolve_self_callee_def(node)
|
|
2125
1838
|
class_name = enclosing_class_name_for(scope.self_type)
|
|
2126
1839
|
return scope.top_level_def_for(node.name) if class_name.nil?
|
|
@@ -2134,11 +1847,10 @@ module Rigor
|
|
|
2134
1847
|
node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
2135
1848
|
end
|
|
2136
1849
|
|
|
2137
|
-
# The set of `[name, position]` parameters of `def_node` whose content a
|
|
2138
|
-
#
|
|
2139
|
-
#
|
|
2140
|
-
#
|
|
2141
|
-
# 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.
|
|
2142
1854
|
def escaped_content_parameters(def_node)
|
|
2143
1855
|
cache = (@escaped_param_cache ||= {}.compare_by_identity)
|
|
2144
1856
|
cache[def_node] ||= compute_escaped_content_parameters(def_node)
|
|
@@ -2160,15 +1872,12 @@ module Rigor
|
|
|
2160
1872
|
positions.slice(*mutated)
|
|
2161
1873
|
end
|
|
2162
1874
|
|
|
2163
|
-
# A receiver-independent over-approximation of `ClosureEscapeAnalyzer`'s
|
|
2164
|
-
#
|
|
2165
|
-
#
|
|
2166
|
-
#
|
|
2167
|
-
#
|
|
2168
|
-
#
|
|
2169
|
-
# `define_method`, an unknown DSL hook) is treated as escaping — sound,
|
|
2170
|
-
# since mis-classifying a truly-non-escaping call only floors an argument
|
|
2171
|
-
# 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.
|
|
2172
1881
|
SYNTACTIC_NON_ESCAPING_BLOCK_METHODS = (
|
|
2173
1882
|
ClosureEscapeAnalyzer::ENUMERABLE_NON_ESCAPING +
|
|
2174
1883
|
ClosureEscapeAnalyzer::OBJECT_NON_ESCAPING +
|
|
@@ -2183,9 +1892,8 @@ module Rigor
|
|
|
2183
1892
|
SYNTACTIC_NON_ESCAPING_BLOCK_METHODS.include?(call_node.name)
|
|
2184
1893
|
end
|
|
2185
1894
|
|
|
2186
|
-
# `{ name => position }` for the required / optional positional
|
|
2187
|
-
# parameters
|
|
2188
|
-
# 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.
|
|
2189
1897
|
def positional_parameter_positions(def_node)
|
|
2190
1898
|
params = def_node.parameters
|
|
2191
1899
|
return {} if params.nil?
|
|
@@ -2198,10 +1906,9 @@ module Rigor
|
|
|
2198
1906
|
positions
|
|
2199
1907
|
end
|
|
2200
1908
|
|
|
2201
|
-
# True when at least one argument of `node` is a bare local-variable read
|
|
2202
|
-
#
|
|
2203
|
-
#
|
|
2204
|
-
# 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.
|
|
2205
1912
|
def call_passes_local_argument?(node)
|
|
2206
1913
|
args = node.arguments
|
|
2207
1914
|
return false unless args.respond_to?(:arguments)
|
|
@@ -2236,12 +1943,10 @@ module Rigor
|
|
|
2236
1943
|
end
|
|
2237
1944
|
end
|
|
2238
1945
|
|
|
2239
|
-
# Walk the receiver chain of `node` and fold the escaping-content
|
|
2240
|
-
#
|
|
2241
|
-
# `
|
|
2242
|
-
#
|
|
2243
|
-
# non-escaping write-back (which the receiver expression evaluation
|
|
2244
|
-
# 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.
|
|
2245
1950
|
def widen_escaping_receiver_chain_captures(node, base_scope)
|
|
2246
1951
|
receiver = node.receiver
|
|
2247
1952
|
acc = base_scope
|
|
@@ -2255,28 +1960,23 @@ module Rigor
|
|
|
2255
1960
|
acc
|
|
2256
1961
|
end
|
|
2257
1962
|
|
|
2258
|
-
# ADR-57 slice 2 (ADR-56 mechanisms 2 / 8 extended to escaping blocks).
|
|
2259
|
-
#
|
|
2260
|
-
# local
|
|
2261
|
-
#
|
|
2262
|
-
#
|
|
2263
|
-
# survived and its element fold (`options[:format]` -> `"text"`,
|
|
2264
|
-
# `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.
|
|
2265
1968
|
#
|
|
2266
|
-
# An escaping block may run later and any number of times, so joining a
|
|
2267
|
-
#
|
|
2268
|
-
#
|
|
2269
|
-
#
|
|
2270
|
-
#
|
|
2271
|
-
# the carrier survives. Read-only captures and locals the block merely
|
|
2272
|
-
# 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.
|
|
2273
1974
|
def widen_escaping_content_captures(block_node, post_scope)
|
|
2274
1975
|
body = block_node.body
|
|
2275
1976
|
return post_scope if body.nil?
|
|
2276
1977
|
|
|
2277
|
-
# Transitive case first: the body may content-mutate a captured local
|
|
2278
|
-
#
|
|
2279
|
-
# `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).
|
|
2280
1980
|
post_scope = floor_block_body_callee_escaped_args(body, post_scope)
|
|
2281
1981
|
|
|
2282
1982
|
mutations = collect_content_mutations(body)
|
|
@@ -2288,20 +1988,15 @@ module Rigor
|
|
|
2288
1988
|
end
|
|
2289
1989
|
end
|
|
2290
1990
|
|
|
2291
|
-
# Inside an ESCAPING block body, a captured outer local can be content-
|
|
2292
|
-
#
|
|
2293
|
-
#
|
|
2294
|
-
#
|
|
2295
|
-
#
|
|
2296
|
-
#
|
|
2297
|
-
#
|
|
2298
|
-
#
|
|
2299
|
-
#
|
|
2300
|
-
# `options[:mutation]` guard to an always-falsey constant). Reuse the
|
|
2301
|
-
# cross-method-boundary callee-escaped-argument floor (the same gate the
|
|
2302
|
-
# receiver-chain path uses at the call site) on every self-call in the
|
|
2303
|
-
# body. Sound — only ever floors a captured local passed as an argument
|
|
2304
|
-
# 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.
|
|
2305
2000
|
def floor_block_body_callee_escaped_args(body, post_scope)
|
|
2306
2001
|
acc = post_scope
|
|
2307
2002
|
Source::NodeWalker.each(body) do |descendant|
|
|
@@ -2310,9 +2005,8 @@ module Rigor
|
|
|
2310
2005
|
acc
|
|
2311
2006
|
end
|
|
2312
2007
|
|
|
2313
|
-
# The Dynamic-floor carrier for a content-mutated escaping capture, or
|
|
2314
|
-
#
|
|
2315
|
-
# 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).
|
|
2316
2010
|
def content_floor_for(type)
|
|
2317
2011
|
return nil if type.nil?
|
|
2318
2012
|
|
|
@@ -2338,14 +2032,10 @@ module Rigor
|
|
|
2338
2032
|
:unknown
|
|
2339
2033
|
end
|
|
2340
2034
|
|
|
2341
|
-
# Sub-phase 3c. Replace the outer-local types that the block
|
|
2342
|
-
#
|
|
2343
|
-
#
|
|
2344
|
-
#
|
|
2345
|
-
# synthesise the union of the block's write types (which the
|
|
2346
|
-
# current pass does not yet expose), we discard the narrowed
|
|
2347
|
-
# binding altogether. A future sub-phase MAY refine this to
|
|
2348
|
-
# 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.
|
|
2349
2039
|
def drop_captured_narrowing(block_node, base_scope)
|
|
2350
2040
|
names = captured_local_writes(block_node, base_scope)
|
|
2351
2041
|
return base_scope if names.empty?
|
|
@@ -2353,13 +2043,11 @@ module Rigor
|
|
|
2353
2043
|
names.reduce(base_scope) { |acc, name| acc.with_local(name, Type::Combinator.untyped) }
|
|
2354
2044
|
end
|
|
2355
2045
|
|
|
2356
|
-
# Names of outer locals the block body can REBIND, across every
|
|
2357
|
-
#
|
|
2358
|
-
#
|
|
2359
|
-
#
|
|
2360
|
-
#
|
|
2361
|
-
# names not bound in the outer scope are excluded — a write to either
|
|
2362
|
-
# 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.
|
|
2363
2051
|
LOCAL_WRITE_NODES = [
|
|
2364
2052
|
Prism::LocalVariableWriteNode,
|
|
2365
2053
|
Prism::LocalVariableOperatorWriteNode,
|
|
@@ -2385,18 +2073,14 @@ module Rigor
|
|
|
2385
2073
|
outer_writes.uniq
|
|
2386
2074
|
end
|
|
2387
2075
|
|
|
2388
|
-
# ADR-56 slice A. For a `:non_escaping` block, fold the continuation
|
|
2389
|
-
# binding
|
|
2390
|
-
#
|
|
2391
|
-
#
|
|
2392
|
-
# joined with the pre-call binding (kept as a constituent so the
|
|
2393
|
-
# 0-iteration path — `[].each { … }` — stays sound), value-pinned-
|
|
2394
|
-
# 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
|
|
2395
2080
|
# `Dynamic[top]` on non-convergence (matching `drop_captured_narrowing`).
|
|
2396
2081
|
#
|
|
2397
|
-
# Fast path: a block writing no outer local leaves `post_scope`
|
|
2398
|
-
#
|
|
2399
|
-
# 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.
|
|
2400
2084
|
def write_back_block_captures(call_node, post_scope)
|
|
2401
2085
|
block = call_node.block
|
|
2402
2086
|
return post_scope unless block.is_a?(Prism::BlockNode)
|
|
@@ -2416,20 +2100,16 @@ module Rigor
|
|
|
2416
2100
|
result.reduce(post_scope) { |acc, (name, type)| acc.with_local(name, type) }
|
|
2417
2101
|
end
|
|
2418
2102
|
|
|
2419
|
-
# ADR-56 slice C — receiver-content element-type join. After the
|
|
2420
|
-
#
|
|
2421
|
-
#
|
|
2422
|
-
#
|
|
2423
|
-
#
|
|
2424
|
-
# `out = [0]; arr.each { |x| out << x }` types `out` as
|
|
2425
|
-
# `Array[0 | Integer]` (sound) rather than `Array[0]` (the B1
|
|
2426
|
-
# 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]`).
|
|
2427
2108
|
#
|
|
2428
|
-
# Pre-state is read from `post_scope` so a local that is BOTH rebound
|
|
2429
|
-
#
|
|
2430
|
-
#
|
|
2431
|
-
#
|
|
2432
|
-
# 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.
|
|
2433
2113
|
def content_writeback_block_captures(call_node, post_scope)
|
|
2434
2114
|
block = call_node.block
|
|
2435
2115
|
return post_scope unless block.is_a?(Prism::BlockNode)
|
|
@@ -2448,13 +2128,11 @@ module Rigor
|
|
|
2448
2128
|
end
|
|
2449
2129
|
end
|
|
2450
2130
|
|
|
2451
|
-
# ADR-56 slice C (B3). For `recv.each_with_object(memo) { |x, acc| … }`
|
|
2452
|
-
# the
|
|
2453
|
-
#
|
|
2454
|
-
#
|
|
2455
|
-
#
|
|
2456
|
-
# param. Returns `call_type` unchanged for any other call, a missing
|
|
2457
|
-
# 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.
|
|
2458
2136
|
def each_with_object_return(call_node, call_type)
|
|
2459
2137
|
return call_type unless call_node.name == :each_with_object
|
|
2460
2138
|
|
|
@@ -2470,8 +2148,8 @@ module Rigor
|
|
|
2470
2148
|
body = block.body
|
|
2471
2149
|
return call_type if body.nil?
|
|
2472
2150
|
|
|
2473
|
-
# The memo alias is a block-local (depth 0) — collect content
|
|
2474
|
-
#
|
|
2151
|
+
# The memo alias is a block-local (depth 0) — collect content mutations on it directly rather than via the
|
|
2152
|
+
# captured-local walk.
|
|
2475
2153
|
calls = body_content_mutations_on(body, memo_param)
|
|
2476
2154
|
return call_type if calls.empty?
|
|
2477
2155
|
|
|
@@ -2481,9 +2159,8 @@ module Rigor
|
|
|
2481
2159
|
joined || call_type
|
|
2482
2160
|
end
|
|
2483
2161
|
|
|
2484
|
-
# The name of the memo block parameter (the SECOND positional param of
|
|
2485
|
-
#
|
|
2486
|
-
# 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.
|
|
2487
2164
|
def each_with_object_memo_param(block)
|
|
2488
2165
|
params_root = block.parameters
|
|
2489
2166
|
return nil unless params_root.is_a?(Prism::BlockParametersNode)
|
|
@@ -2498,8 +2175,7 @@ module Rigor
|
|
|
2498
2175
|
second.respond_to?(:name) ? second.name : nil
|
|
2499
2176
|
end
|
|
2500
2177
|
|
|
2501
|
-
# Content-mutator calls on a block-local receiver `var_name`
|
|
2502
|
-
# (depth 0) within `body`.
|
|
2178
|
+
# Content-mutator calls on a block-local receiver `var_name` (depth 0) within `body`.
|
|
2503
2179
|
def body_content_mutations_on(body, var_name)
|
|
2504
2180
|
calls = []
|
|
2505
2181
|
Source::NodeWalker.each(body) do |descendant|
|
|
@@ -2515,15 +2191,14 @@ module Rigor
|
|
|
2515
2191
|
calls
|
|
2516
2192
|
end
|
|
2517
2193
|
|
|
2518
|
-
# Joins content evidence for a memo / param given its pre-state and a
|
|
2519
|
-
#
|
|
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.
|
|
2520
2196
|
def join_content_for_param(calls, pre_state, block_entry)
|
|
2521
2197
|
return nil if pre_state.nil?
|
|
2522
2198
|
|
|
2523
2199
|
if stringish?(pre_state)
|
|
2524
|
-
# String carries no element parameter; mutating `<<`/`concat`
|
|
2525
|
-
#
|
|
2526
|
-
# `"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.
|
|
2527
2202
|
Type::Combinator.nominal_of("String")
|
|
2528
2203
|
elsif hashish?(pre_state) || (hash_mutations?(calls) && !arrayish?(pre_state))
|
|
2529
2204
|
join_hash_param(calls, pre_state, block_entry)
|
|
@@ -2543,9 +2218,8 @@ module Rigor
|
|
|
2543
2218
|
return nil unless arrayish?(pre_state)
|
|
2544
2219
|
|
|
2545
2220
|
added = calls.flat_map do |c|
|
|
2546
|
-
# Index-write on an array (`a[i] += v`) introduces no new element
|
|
2547
|
-
#
|
|
2548
|
-
# 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.
|
|
2549
2223
|
next [] if index_write?(c)
|
|
2550
2224
|
|
|
2551
2225
|
MutationWidening.array_added_elements(c.name, content_arg_types(c, block_entry))
|
|
@@ -2553,11 +2227,9 @@ module Rigor
|
|
|
2553
2227
|
MutationWidening.join_array_content(pre_state, added)
|
|
2554
2228
|
end
|
|
2555
2229
|
|
|
2556
|
-
# Walks the block body for content-mutator calls (`<<`, `push`,
|
|
2557
|
-
#
|
|
2558
|
-
#
|
|
2559
|
-
# `MutationWidening.widen_after_block` walk (descends into nested
|
|
2560
|
-
# 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).
|
|
2561
2233
|
def collect_content_mutations(body)
|
|
2562
2234
|
mutations = Hash.new { |h, k| h[k] = [] }
|
|
2563
2235
|
Source::NodeWalker.each(body) do |descendant|
|
|
@@ -2567,11 +2239,9 @@ module Rigor
|
|
|
2567
2239
|
mutations
|
|
2568
2240
|
end
|
|
2569
2241
|
|
|
2570
|
-
# Index-write forms (`h[k] ||= v`, `h[k] += v`, `h[k] = v` via a
|
|
2571
|
-
#
|
|
2572
|
-
#
|
|
2573
|
-
# OrWrite even though the appended values land on the nested array —
|
|
2574
|
-
# 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`).
|
|
2575
2245
|
INDEX_WRITE_NODES = [
|
|
2576
2246
|
Prism::IndexOrWriteNode,
|
|
2577
2247
|
Prism::IndexAndWriteNode,
|
|
@@ -2580,10 +2250,8 @@ module Rigor
|
|
|
2580
2250
|
].freeze
|
|
2581
2251
|
private_constant :INDEX_WRITE_NODES
|
|
2582
2252
|
|
|
2583
|
-
# `[receiver_name, node]` when `node` is a content mutation whose
|
|
2584
|
-
#
|
|
2585
|
-
# else `[nil, nil]`. Covers `[]=`-style CallNode mutators and the
|
|
2586
|
-
# 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.
|
|
2587
2255
|
def content_mutation_target(node)
|
|
2588
2256
|
is_call_mutator = node.is_a?(Prism::CallNode) && MutationWidening::CONTENT_ADDERS.include?(node.name)
|
|
2589
2257
|
return [nil, nil] unless is_call_mutator || index_write?(node)
|
|
@@ -2595,11 +2263,9 @@ module Rigor
|
|
|
2595
2263
|
[receiver.name, node]
|
|
2596
2264
|
end
|
|
2597
2265
|
|
|
2598
|
-
# Computes the joined continuation collection type for one captured
|
|
2599
|
-
#
|
|
2600
|
-
#
|
|
2601
|
-
# e.g. a String accumulator, whose `<<` carries no element parameter
|
|
2602
|
-
# 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`.
|
|
2603
2269
|
def join_content_for_local(name, calls, post_scope, block_entry)
|
|
2604
2270
|
join_content_for_param(calls, post_scope.local(name), block_entry)
|
|
2605
2271
|
end
|
|
@@ -2637,11 +2303,9 @@ module Rigor
|
|
|
2637
2303
|
(type.is_a?(Type::Nominal) && type.class_name == "String")
|
|
2638
2304
|
end
|
|
2639
2305
|
|
|
2640
|
-
# `[key_type, value_type]` for a `h[k] = v` / `h.store(k, v)` call or
|
|
2641
|
-
# an index-write
|
|
2642
|
-
#
|
|
2643
|
-
# often land on a NESTED collection via `h[k] << v`), so the value is
|
|
2644
|
-
# 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.
|
|
2645
2309
|
# Returns `[]` for other forms.
|
|
2646
2310
|
def hash_pair_types(node, block_entry)
|
|
2647
2311
|
if index_write?(node)
|
|
@@ -2668,10 +2332,9 @@ module Rigor
|
|
|
2668
2332
|
nil
|
|
2669
2333
|
end
|
|
2670
2334
|
|
|
2671
|
-
# Argument types for a content-mutator call, typed against the
|
|
2672
|
-
#
|
|
2673
|
-
#
|
|
2674
|
-
# `;`-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.
|
|
2675
2338
|
def content_arg_types(call_node, block_entry)
|
|
2676
2339
|
arguments = call_node.arguments
|
|
2677
2340
|
return [] if arguments.nil?
|
|
@@ -2681,10 +2344,9 @@ module Rigor
|
|
|
2681
2344
|
[]
|
|
2682
2345
|
end
|
|
2683
2346
|
|
|
2684
|
-
# Evaluates `block`'s body once with each written outer local bound to
|
|
2685
|
-
#
|
|
2686
|
-
#
|
|
2687
|
-
# 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.
|
|
2688
2350
|
def block_exit_bindings(call_node, block, bindings, names)
|
|
2689
2351
|
entry = build_block_entry_scope(call_node, block)
|
|
2690
2352
|
entry = bindings.reduce(entry) { |acc, (name, type)| acc.with_local(name, type) }
|
|
@@ -2692,11 +2354,9 @@ module Rigor
|
|
|
2692
2354
|
names.to_h { |name| [name, exit_scope.local(name)] }
|
|
2693
2355
|
end
|
|
2694
2356
|
|
|
2695
|
-
# Names introduced by the block itself (parameters, numbered
|
|
2696
|
-
#
|
|
2697
|
-
#
|
|
2698
|
-
# to these names are local to the block and MUST NOT be
|
|
2699
|
-
# 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.
|
|
2700
2360
|
def block_introduced_locals(block_node)
|
|
2701
2361
|
introduced = Set.new(BlockParameterBinder.new.bind(block_node).keys)
|
|
2702
2362
|
params_root = block_node.parameters
|
|
@@ -2704,34 +2364,24 @@ module Rigor
|
|
|
2704
2364
|
introduced
|
|
2705
2365
|
end
|
|
2706
2366
|
|
|
2707
|
-
# `Prism::BlockNode` is reached through {#eval_call}; the
|
|
2708
|
-
#
|
|
2709
|
-
#
|
|
2710
|
-
#
|
|
2711
|
-
# caller's scope unchanged), but the body's local writes are
|
|
2712
|
-
# threaded through subsequent statements *inside* the block so
|
|
2713
|
-
# `each { |x| sum = x; sum.succ }` types `sum.succ` under the
|
|
2714
|
-
# `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.
|
|
2715
2371
|
def eval_block(node)
|
|
2716
2372
|
return [Type::Combinator.constant_of(nil), scope] if node.body.nil?
|
|
2717
2373
|
|
|
2718
2374
|
sub_eval(node.body, scope)
|
|
2719
2375
|
end
|
|
2720
2376
|
|
|
2721
|
-
# Builds the entry scope for a block body. The block sees the
|
|
2722
|
-
#
|
|
2723
|
-
#
|
|
2724
|
-
# types come from the receiving method's RBS signature when
|
|
2725
|
-
# 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]`.
|
|
2726
2380
|
#
|
|
2727
|
-
# `;`-prefixed block-locals (`do |i; x|`) are bound to
|
|
2728
|
-
#
|
|
2729
|
-
# `x
|
|
2730
|
-
#
|
|
2731
|
-
# Without this shadow, an inner `x.even?` before the first
|
|
2732
|
-
# write would type-check against the OUTER `x` (e.g.
|
|
2733
|
-
# `Integer`) when the runtime would actually `NoMethodError`
|
|
2734
|
-
# 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`.
|
|
2735
2385
|
def build_block_entry_scope(call_node, block_node)
|
|
2736
2386
|
expected = expected_block_param_types_for(call_node)
|
|
2737
2387
|
bindings = BlockParameterBinder.new(expected_param_types: expected).bind(block_node)
|
|
@@ -2775,12 +2425,10 @@ module Rigor
|
|
|
2775
2425
|
def eval_class_body(node, new_context)
|
|
2776
2426
|
return [Type::Combinator.constant_of(nil), scope] if node.body.nil?
|
|
2777
2427
|
|
|
2778
|
-
# Class/module bodies run in a fresh scope: the outer scope's
|
|
2779
|
-
#
|
|
2780
|
-
# the
|
|
2781
|
-
#
|
|
2782
|
-
# class body is the class object itself, so we set
|
|
2783
|
-
# `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>]`.
|
|
2784
2432
|
fresh = build_fresh_body_scope
|
|
2785
2433
|
body_self = self_type_for_class_body(new_context)
|
|
2786
2434
|
fresh = fresh.with_self_type(body_self) if body_self
|
|
@@ -2796,43 +2444,49 @@ module Rigor
|
|
|
2796
2444
|
source_path: scope.source_path
|
|
2797
2445
|
)
|
|
2798
2446
|
bindings = binder.bind(def_node)
|
|
2799
|
-
# ADR-67 WD3 — override an undeclared parameter with its call-site
|
|
2800
|
-
#
|
|
2801
|
-
#
|
|
2802
|
-
#
|
|
2803
|
-
# 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).
|
|
2804
2451
|
bindings = seed_inferred_param_types(bindings, def_node, singleton)
|
|
2805
2452
|
|
|
2806
|
-
# Method bodies do NOT see the outer scope's locals. They start
|
|
2807
|
-
#
|
|
2808
|
-
# the
|
|
2809
|
-
# seed their `ivars` map from the class-level accumulator so
|
|
2810
|
-
# `def get; @x; end` reads the type that a sibling
|
|
2811
|
-
# `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.
|
|
2812
2456
|
fresh = build_fresh_body_scope
|
|
2813
2457
|
body_self = self_type_for_method_body(singleton: singleton)
|
|
2814
2458
|
fresh = fresh.with_self_type(body_self) if body_self
|
|
2815
2459
|
fresh = seed_instance_ivars(fresh, singleton: singleton)
|
|
2816
2460
|
fresh = seed_class_cvars(fresh)
|
|
2817
2461
|
fresh = seed_program_globals(fresh)
|
|
2818
|
-
# ADR-48 Struct slice 3 — install the method body's fold-safe-local set
|
|
2819
|
-
#
|
|
2820
|
-
# 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).
|
|
2821
2464
|
fresh = fresh.with_struct_fold_safe(
|
|
2822
2465
|
StructFoldSafety.fold_safe_locals(
|
|
2823
2466
|
def_node.body, ->(name) { scope.struct_member_layout(name)&.[](:members) }
|
|
2824
2467
|
)
|
|
2825
2468
|
)
|
|
2826
|
-
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)
|
|
2827
2483
|
end
|
|
2828
2484
|
|
|
2829
|
-
# ADR-67 WD3 — consult the call-site parameter-inference table for this
|
|
2830
|
-
#
|
|
2831
|
-
#
|
|
2832
|
-
#
|
|
2833
|
-
#
|
|
2834
|
-
# parameter (a non-untyped binding) always wins. No-op when the table is
|
|
2835
|
-
# 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.
|
|
2836
2490
|
def seed_inferred_param_types(bindings, def_node, singleton)
|
|
2837
2491
|
inferred = scope.param_inferred_types
|
|
2838
2492
|
return bindings if inferred.empty?
|
|
@@ -2850,8 +2504,8 @@ module Rigor
|
|
|
2850
2504
|
merged
|
|
2851
2505
|
end
|
|
2852
2506
|
|
|
2853
|
-
# True for the `Dynamic[Top]` carrier `MethodParameterBinder` leaves on an
|
|
2854
|
-
#
|
|
2507
|
+
# True for the `Dynamic[Top]` carrier `MethodParameterBinder` leaves on an undeclared parameter — the only
|
|
2508
|
+
# bindings ADR-67 WD3 overrides.
|
|
2855
2509
|
def untyped_binding?(type)
|
|
2856
2510
|
type.is_a?(Type::Dynamic) && type.static_facet.is_a?(Type::Top)
|
|
2857
2511
|
end
|
|
@@ -2865,20 +2519,17 @@ module Rigor
|
|
|
2865
2519
|
seeded = scope.class_ivars_for(path)
|
|
2866
2520
|
return body_scope if seeded.empty?
|
|
2867
2521
|
|
|
2868
|
-
# ADR-58 WD1 — the class-ivar index unions every `@x = …` write across
|
|
2869
|
-
#
|
|
2870
|
-
#
|
|
2871
|
-
#
|
|
2872
|
-
#
|
|
2873
|
-
# on the cross-method invariant unless a method-local write or
|
|
2874
|
-
# 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).
|
|
2875
2527
|
seeded.reduce(body_scope) { |acc, (name, type)| acc.seed_declaration_sourced_ivar(name, type) }
|
|
2876
2528
|
end
|
|
2877
2529
|
|
|
2878
|
-
# Cvars are visible from BOTH instance and singleton method
|
|
2879
|
-
#
|
|
2880
|
-
#
|
|
2881
|
-
# 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.
|
|
2882
2533
|
def seed_class_cvars(body_scope)
|
|
2883
2534
|
path = current_class_path
|
|
2884
2535
|
return body_scope if path.nil?
|
|
@@ -2889,11 +2540,9 @@ module Rigor
|
|
|
2889
2540
|
seeded.reduce(body_scope) { |acc, (name, type)| acc.with_cvar(name, type) }
|
|
2890
2541
|
end
|
|
2891
2542
|
|
|
2892
|
-
# Globals are process-wide. The body scope already inherited
|
|
2893
|
-
# the
|
|
2894
|
-
#
|
|
2895
|
-
# `globals` map so reads observe a precise type without
|
|
2896
|
-
# 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.
|
|
2897
2546
|
def seed_program_globals(body_scope)
|
|
2898
2547
|
seeded = scope.program_globals
|
|
2899
2548
|
return body_scope if seeded.empty?
|
|
@@ -2901,19 +2550,14 @@ module Rigor
|
|
|
2901
2550
|
seeded.reduce(body_scope) { |acc, (name, type)| acc.with_global(name, type) }
|
|
2902
2551
|
end
|
|
2903
2552
|
|
|
2904
|
-
# Slice A-declarations. Class- and method-bodies start from a
|
|
2905
|
-
#
|
|
2906
|
-
# `
|
|
2907
|
-
# ScopeIndexer-populated declaration overrides
|
|
2908
|
-
# (`Prism::ConstantReadNode` for `module Foo` headers, etc.)
|
|
2909
|
-
# 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.
|
|
2910
2556
|
def build_fresh_body_scope
|
|
2911
|
-
# Single allocation instead of a deep `with_*` chain — this runs
|
|
2912
|
-
#
|
|
2913
|
-
#
|
|
2914
|
-
#
|
|
2915
|
-
# reference (ADR-53 Track A), so a table added to the index can no
|
|
2916
|
-
# 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.
|
|
2917
2561
|
Scope.new(
|
|
2918
2562
|
environment: scope.environment,
|
|
2919
2563
|
locals: {}.freeze,
|
|
@@ -2929,16 +2573,11 @@ module Rigor
|
|
|
2929
2573
|
def_receiver_targets_lexical_self?(def_node.receiver)
|
|
2930
2574
|
end
|
|
2931
2575
|
|
|
2932
|
-
# `def Foo.bar` inside `module Foo` (or `def Meta.init` inside
|
|
2933
|
-
# `
|
|
2934
|
-
#
|
|
2935
|
-
#
|
|
2936
|
-
#
|
|
2937
|
-
# `def OpenURI.x` form (single segment) and the
|
|
2938
|
-
# `def OpenURI::Meta.x` form (qualified path). Cross-class
|
|
2939
|
-
# receivers (`def Bar.baz` inside `module Foo` where the
|
|
2940
|
-
# receiver names a different constant) are not promoted to
|
|
2941
|
-
# 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.
|
|
2942
2581
|
def def_receiver_targets_lexical_self?(receiver)
|
|
2943
2582
|
return false if @class_context.empty?
|
|
2944
2583
|
|
|
@@ -2957,18 +2596,16 @@ module Rigor
|
|
|
2957
2596
|
end
|
|
2958
2597
|
end
|
|
2959
2598
|
|
|
2960
|
-
# Slice A-engine. Inside a class body `class Foo; ...; end`,
|
|
2961
|
-
#
|
|
2962
|
-
# 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).
|
|
2963
2601
|
def self_type_for_class_body(class_context)
|
|
2964
2602
|
return nil if class_context.empty?
|
|
2965
2603
|
|
|
2966
2604
|
Type::Combinator.singleton_of(class_context.map(&:name).join("::"))
|
|
2967
2605
|
end
|
|
2968
2606
|
|
|
2969
|
-
# Slice A-engine. Inside a method body, `self` depends on
|
|
2970
|
-
#
|
|
2971
|
-
# 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:
|
|
2972
2609
|
#
|
|
2973
2610
|
# - `def self.foo` or any def inside `class << self`: self is
|
|
2974
2611
|
# the class object → `Singleton[Foo]`.
|
|
@@ -2999,16 +2636,11 @@ module Rigor
|
|
|
2999
2636
|
target = singleton_constant_target(node.expression)
|
|
3000
2637
|
return @class_context unless target
|
|
3001
2638
|
|
|
3002
|
-
# `class << Foo` inside `class Foo` (the canonical
|
|
3003
|
-
#
|
|
3004
|
-
# `
|
|
3005
|
-
# singleton frame
|
|
3006
|
-
#
|
|
3007
|
-
# When the target names a different constant (rare
|
|
3008
|
-
# cross-class form), append a fresh singleton frame
|
|
3009
|
-
# tagged with the target FQN; the bodies are scoped to
|
|
3010
|
-
# that target rather than to the lexical enclosing
|
|
3011
|
-
# 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.
|
|
3012
2644
|
if !@class_context.empty? && @class_context.last.name == target
|
|
3013
2645
|
outer = @class_context[0..-2]
|
|
3014
2646
|
outer + [ClassFrame.new(name: target, singleton: true)]
|
|
@@ -3029,10 +2661,8 @@ module Rigor
|
|
|
3029
2661
|
end
|
|
3030
2662
|
end
|
|
3031
2663
|
|
|
3032
|
-
# The qualified name of the immediately-enclosing class (joining
|
|
3033
|
-
#
|
|
3034
|
-
# top-level def with no enclosing class, which routes the
|
|
3035
|
-
# 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.
|
|
3036
2666
|
def current_class_path
|
|
3037
2667
|
return nil if @class_context.empty?
|
|
3038
2668
|
|
|
@@ -3045,27 +2675,23 @@ module Rigor
|
|
|
3045
2675
|
|
|
3046
2676
|
# ----- helpers -----
|
|
3047
2677
|
|
|
3048
|
-
# Explicit `return value` (including `return` inside a block, which in
|
|
3049
|
-
#
|
|
3050
|
-
# is
|
|
3051
|
-
#
|
|
3052
|
-
#
|
|
3053
|
-
#
|
|
3054
|
-
# the sink around the nested body, so this handler only ever appends a
|
|
3055
|
-
# 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.
|
|
3056
2684
|
def eval_return(node)
|
|
3057
2685
|
sink = Thread.current[RETURN_SINK_KEY]
|
|
3058
2686
|
record_return_value(node, sink) if sink
|
|
3059
2687
|
[Type::Combinator.bot, scope]
|
|
3060
2688
|
end
|
|
3061
2689
|
|
|
3062
|
-
# A `break` transfers control to the loop exit (its flow value is `Bot`,
|
|
3063
|
-
#
|
|
3064
|
-
# break
|
|
3065
|
-
#
|
|
3066
|
-
#
|
|
3067
|
-
# loop body (a block targeting a method, or top-level) — left to the
|
|
3068
|
-
# 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.
|
|
3069
2695
|
def eval_break(node)
|
|
3070
2696
|
sink = Thread.current[BREAK_SINK_KEY]
|
|
3071
2697
|
sink << [node, scope] if sink
|
|
@@ -3074,10 +2700,9 @@ module Rigor
|
|
|
3074
2700
|
|
|
3075
2701
|
def record_return_value(node, sink)
|
|
3076
2702
|
args = node.arguments&.arguments || []
|
|
3077
|
-
# `return` with no argument returns nil; `return a` records the
|
|
3078
|
-
#
|
|
3079
|
-
#
|
|
3080
|
-
# 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.
|
|
3081
2706
|
if args.empty?
|
|
3082
2707
|
sink << Type::Combinator.constant_of(nil)
|
|
3083
2708
|
elsif args.size == 1
|
|
@@ -3099,16 +2724,12 @@ module Rigor
|
|
|
3099
2724
|
).evaluate(node)
|
|
3100
2725
|
end
|
|
3101
2726
|
|
|
3102
|
-
# Slice 7 phase 14 — branch exit detection. Returns true
|
|
3103
|
-
#
|
|
3104
|
-
# surrounding
|
|
3105
|
-
#
|
|
3106
|
-
# to narrow the post-scope: when one branch exits, the
|
|
3107
|
-
# surrounding scope can carry the OTHER branch's edge
|
|
3108
|
-
# 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.
|
|
3109
2731
|
#
|
|
3110
|
-
# The detection is intentionally conservative — it
|
|
3111
|
-
# recognises only the most common patterns:
|
|
2732
|
+
# The detection is intentionally conservative — it recognises only the most common patterns:
|
|
3112
2733
|
# - A `Prism::ReturnNode`, `NextNode`, `BreakNode`.
|
|
3113
2734
|
# - A `Prism::CallNode` whose name is `:raise` or `:throw`.
|
|
3114
2735
|
# - A `Prism::StatementsNode`, `Prism::ParenthesesNode`, or
|
|
@@ -3145,18 +2766,12 @@ module Rigor
|
|
|
3145
2766
|
end
|
|
3146
2767
|
end
|
|
3147
2768
|
|
|
3148
|
-
# ADR-24 WD6 / slice 3 — generalised terminating-branch
|
|
3149
|
-
#
|
|
3150
|
-
#
|
|
3151
|
-
#
|
|
3152
|
-
#
|
|
3153
|
-
#
|
|
3154
|
-
# it — regardless of how it is spelled. The canonical
|
|
3155
|
-
# case is a resolved guard helper (`fail_with_message(...)`)
|
|
3156
|
-
# whose body always raises: ADR-24 slice 1 types the call
|
|
3157
|
-
# `bot`, and this OR-test makes `helper(...) if x.nil?`
|
|
3158
|
-
# narrow exactly like `raise ... if x.nil?`. The branch
|
|
3159
|
-
# 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`.
|
|
3160
2775
|
def branch_terminates?(branch_node, branch_type)
|
|
3161
2776
|
branch_unconditionally_exits?(branch_node) ||
|
|
3162
2777
|
branch_type.is_a?(Type::Bot)
|
|
@@ -3168,12 +2783,9 @@ module Rigor
|
|
|
3168
2783
|
sub_eval(branch_node, branch_scope)
|
|
3169
2784
|
end
|
|
3170
2785
|
|
|
3171
|
-
# Joins two branch scopes at a control-flow merge point. Names
|
|
3172
|
-
#
|
|
3173
|
-
#
|
|
3174
|
-
# them outright. This implements the contract the Slice 3 phase 1
|
|
3175
|
-
# `Scope#join` documentation defers to the statement-level
|
|
3176
|
-
# 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.
|
|
3177
2789
|
def join_with_nil_injection(scope_a, scope_b)
|
|
3178
2790
|
nil_const = Type::Combinator.constant_of(nil)
|
|
3179
2791
|
a_keys = scope_a.locals.keys
|
|
@@ -3186,21 +2798,17 @@ module Rigor
|
|
|
3186
2798
|
aug_a.join(aug_b)
|
|
3187
2799
|
end
|
|
3188
2800
|
|
|
3189
|
-
# Generalises {#join_with_nil_injection} to N branches (case/when,
|
|
3190
|
-
#
|
|
3191
|
-
# result because nil-injection commutes with union under
|
|
3192
|
-
# `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`.
|
|
3193
2803
|
def reduce_scopes_with_nil_injection(scopes)
|
|
3194
2804
|
scopes.reduce { |a, b| join_with_nil_injection(a, b) }
|
|
3195
2805
|
end
|
|
3196
2806
|
|
|
3197
|
-
# ---------------------------------------------------------------
|
|
3198
|
-
# rescue variable binding helpers
|
|
2807
|
+
# --------------------------------------------------------------- rescue variable binding helpers
|
|
3199
2808
|
# ---------------------------------------------------------------
|
|
3200
2809
|
|
|
3201
|
-
# Returns `scope` extended with the rescue reference variable bound
|
|
3202
|
-
#
|
|
3203
|
-
# 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`).
|
|
3204
2812
|
def bind_rescue_reference(rescue_node, scope)
|
|
3205
2813
|
ref = rescue_node.reference
|
|
3206
2814
|
return scope unless ref.is_a?(Prism::LocalVariableTargetNode)
|
|
@@ -3208,11 +2816,9 @@ module Rigor
|
|
|
3208
2816
|
scope.with_local(ref.name, rescue_exception_type(rescue_node, scope))
|
|
3209
2817
|
end
|
|
3210
2818
|
|
|
3211
|
-
# Derives the exception instance type for a `RescueNode`. When the
|
|
3212
|
-
#
|
|
3213
|
-
# `StandardError
|
|
3214
|
-
# types are unioned. Falls back to `StandardError` for any class
|
|
3215
|
-
# 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.
|
|
3216
2822
|
def rescue_exception_type(rescue_node, scope)
|
|
3217
2823
|
exceptions = rescue_node.exceptions
|
|
3218
2824
|
if exceptions.empty?
|
|
@@ -3226,20 +2832,18 @@ module Rigor
|
|
|
3226
2832
|
end
|
|
3227
2833
|
end
|
|
3228
2834
|
|
|
3229
|
-
# ---------------------------------------------------------------
|
|
3230
|
-
# `case/in` pattern variable binding helpers
|
|
2835
|
+
# --------------------------------------------------------------- `case/in` pattern variable binding helpers
|
|
3231
2836
|
# ---------------------------------------------------------------
|
|
3232
2837
|
|
|
3233
|
-
# Builds the entry scope for an `in` branch by injecting every
|
|
3234
|
-
#
|
|
2838
|
+
# Builds the entry scope for an `in` branch by injecting every variable captured by the pattern as a local
|
|
2839
|
+
# binding.
|
|
3235
2840
|
def apply_in_pattern_bindings(subject, pattern, scope)
|
|
3236
2841
|
bindings = collect_in_pattern_bindings(subject, pattern, scope)
|
|
3237
2842
|
bindings.reduce(scope) { |s, (name, type)| s.with_local(name, type) }
|
|
3238
2843
|
end
|
|
3239
2844
|
|
|
3240
|
-
# Returns an array of `[Symbol, Rigor::Type]` pairs for every
|
|
3241
|
-
#
|
|
3242
|
-
# 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).
|
|
3243
2847
|
def collect_in_pattern_bindings(subject, pattern, scope)
|
|
3244
2848
|
case pattern
|
|
3245
2849
|
when Prism::CapturePatternNode
|
|
@@ -3292,11 +2896,9 @@ module Rigor
|
|
|
3292
2896
|
bindings
|
|
3293
2897
|
end
|
|
3294
2898
|
|
|
3295
|
-
# `[..., *rest, ...]` / `[*pre, x, *post]` capture an Array of
|
|
3296
|
-
#
|
|
3297
|
-
#
|
|
3298
|
-
# subject-aware element-type extraction (the binder doesn't see
|
|
3299
|
-
# 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).
|
|
3300
2902
|
def append_array_splat_binding(bindings, splat)
|
|
3301
2903
|
return unless splat.is_a?(Prism::SplatNode)
|
|
3302
2904
|
|
|
@@ -3306,10 +2908,8 @@ module Rigor
|
|
|
3306
2908
|
bindings << [target.name, Type::Combinator.nominal_of("Array", type_args: [Type::Combinator.untyped])]
|
|
3307
2909
|
end
|
|
3308
2910
|
|
|
3309
|
-
# `{ key:, **rest }` binds `rest` to a Hash whose keys are
|
|
3310
|
-
#
|
|
3311
|
-
# whose values are untyped (the binder can't see the subject's
|
|
3312
|
-
# 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).
|
|
3313
2913
|
def hash_pattern_rest_type
|
|
3314
2914
|
Type::Combinator.nominal_of(
|
|
3315
2915
|
"Hash",
|
|
@@ -3317,14 +2917,12 @@ module Rigor
|
|
|
3317
2917
|
)
|
|
3318
2918
|
end
|
|
3319
2919
|
|
|
3320
|
-
# ---------------------------------------------------------------
|
|
3321
|
-
# named-capture regex binding (`MatchWriteNode`)
|
|
2920
|
+
# --------------------------------------------------------------- named-capture regex binding (`MatchWriteNode`)
|
|
3322
2921
|
# ---------------------------------------------------------------
|
|
3323
2922
|
|
|
3324
|
-
# `/(?<year>\d+)/ =~ str` — Prism emits a `MatchWriteNode` that
|
|
3325
|
-
#
|
|
3326
|
-
#
|
|
3327
|
-
# 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).
|
|
3328
2926
|
def eval_match_write(node)
|
|
3329
2927
|
match_type, post_scope = sub_eval(node.call, scope)
|
|
3330
2928
|
string_or_nil = Type::Combinator.union(
|
|
@@ -3339,23 +2937,19 @@ module Rigor
|
|
|
3339
2937
|
[match_type, bound_scope]
|
|
3340
2938
|
end
|
|
3341
2939
|
|
|
3342
|
-
# ---------------------------------------------------------------
|
|
3343
|
-
# shared type conversion helper
|
|
2940
|
+
# --------------------------------------------------------------- shared type conversion helper
|
|
3344
2941
|
# ---------------------------------------------------------------
|
|
3345
2942
|
|
|
3346
|
-
# Converts a `Singleton[ClassName]` (the class object) to the
|
|
3347
|
-
#
|
|
3348
|
-
#
|
|
3349
|
-
# 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).
|
|
3350
2946
|
def singleton_to_nominal(type)
|
|
3351
2947
|
type.is_a?(Type::Singleton) ? Type::Combinator.nominal_of(type.class_name) : Type::Combinator.untyped
|
|
3352
2948
|
end
|
|
3353
2949
|
|
|
3354
|
-
# Returns the type to bind for a `CapturePatternNode`'s target.
|
|
3355
|
-
#
|
|
3356
|
-
# `
|
|
3357
|
-
# alternate's resolved type. Anything else falls back to
|
|
3358
|
-
# `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).
|
|
3359
2953
|
def pattern_capture_type(value_node, scope)
|
|
3360
2954
|
if value_node.is_a?(Prism::AlternationPatternNode)
|
|
3361
2955
|
left = pattern_capture_type(value_node.left, scope)
|
|
@@ -3366,13 +2960,10 @@ module Rigor
|
|
|
3366
2960
|
end
|
|
3367
2961
|
end
|
|
3368
2962
|
|
|
3369
|
-
# `in PatternA | PatternB` — Ruby requires both alternates to
|
|
3370
|
-
#
|
|
3371
|
-
#
|
|
3372
|
-
#
|
|
3373
|
-
# Names that only one alternate contributes still surface (the
|
|
3374
|
-
# parser would have rejected the case at compile time, so by the
|
|
3375
|
-
# 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).
|
|
3376
2967
|
def collect_alternation_pattern_bindings(subject, pattern, scope)
|
|
3377
2968
|
left = collect_in_pattern_bindings(subject, pattern.left, scope)
|
|
3378
2969
|
right = collect_in_pattern_bindings(subject, pattern.right, scope)
|