rigortype 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/data/core_overlay/csv.rbs +28 -0
- data/data/core_overlay/psych.rbs +22 -0
- data/docs/handbook/01-getting-started.md +9 -1
- data/docs/handbook/02-everyday-types.md +4 -1
- data/docs/handbook/08-understanding-errors.md +3 -3
- data/docs/manual/01-installation.md +1 -0
- data/docs/manual/02-cli-reference.md +16 -7
- data/docs/manual/04-diagnostics.md +7 -4
- data/docs/manual/06-baseline.md +5 -2
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/14-rails-quickstart.md +4 -2
- data/docs/manual/15-type-protection-coverage.md +27 -4
- data/docs/manual/plugins/rigor-actionpack.md +1 -1
- data/docs/manual/plugins/rigor-activerecord.md +12 -5
- data/docs/manual/plugins/rigor-rails-routes.md +11 -0
- data/lib/rigor/analysis/baseline.rb +56 -96
- data/lib/rigor/analysis/buffer_binding.rb +13 -20
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +29 -48
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +35 -54
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +23 -37
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +16 -23
- data/lib/rigor/analysis/check_rules/rule_walk.rb +48 -71
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +19 -28
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +48 -67
- data/lib/rigor/analysis/check_rules.rb +174 -274
- data/lib/rigor/analysis/dependency_recorder.rb +26 -38
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +13 -25
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +17 -30
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +15 -26
- data/lib/rigor/analysis/dependency_source_inference/index.rb +45 -76
- data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +23 -40
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +50 -83
- data/lib/rigor/analysis/dependency_source_inference.rb +6 -10
- data/lib/rigor/analysis/diagnostic.rb +48 -75
- data/lib/rigor/analysis/erb_template_detector.rb +13 -18
- data/lib/rigor/analysis/fact_store.rb +12 -22
- data/lib/rigor/analysis/incremental.rb +47 -63
- data/lib/rigor/analysis/incremental_session.rb +74 -100
- data/lib/rigor/analysis/project_scan.rb +12 -23
- data/lib/rigor/analysis/result.rb +3 -5
- data/lib/rigor/analysis/rule_catalog.rb +67 -95
- data/lib/rigor/analysis/run_stats.rb +38 -61
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +101 -160
- data/lib/rigor/analysis/runner/pool_coordinator.rb +121 -188
- data/lib/rigor/analysis/runner/project_pre_passes.rb +62 -96
- data/lib/rigor/analysis/runner/run_snapshots.rb +12 -18
- data/lib/rigor/analysis/runner.rb +220 -333
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +30 -43
- data/lib/rigor/analysis/worker_session.rb +64 -102
- data/lib/rigor/ast/type_node.rb +6 -8
- data/lib/rigor/ast.rb +6 -10
- data/lib/rigor/bleeding_edge.rb +27 -35
- data/lib/rigor/builtins/hkt_builtins.rb +7 -14
- data/lib/rigor/builtins/imported_refinements.rb +106 -149
- data/lib/rigor/builtins/predefined_constant_refinements.rb +51 -65
- data/lib/rigor/builtins/regex_refinement.rb +32 -43
- data/lib/rigor/builtins/static_return_refinements.rb +57 -81
- data/lib/rigor/cache/descriptor.rb +58 -94
- data/lib/rigor/cache/incremental_snapshot.rb +32 -48
- data/lib/rigor/cache/rbs_cache_producer.rb +10 -15
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +10 -16
- data/lib/rigor/cache/rbs_class_type_param_names.rb +10 -16
- data/lib/rigor/cache/rbs_constant_table.rb +8 -13
- data/lib/rigor/cache/rbs_descriptor.rb +8 -13
- data/lib/rigor/cache/rbs_environment.rb +11 -19
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +11 -19
- data/lib/rigor/cache/rbs_known_class_names.rb +8 -12
- data/lib/rigor/cache/store.rb +248 -186
- data/lib/rigor/cli/annotate_command.rb +51 -88
- data/lib/rigor/cli/baseline_command.rb +10 -17
- data/lib/rigor/cli/check_command.rb +121 -199
- data/lib/rigor/cli/check_runner_factory.rb +8 -14
- data/lib/rigor/cli/ci_detector.rb +9 -14
- data/lib/rigor/cli/command.rb +8 -14
- data/lib/rigor/cli/coverage_command.rb +62 -48
- data/lib/rigor/cli/coverage_mutation.rb +14 -19
- data/lib/rigor/cli/coverage_report.rb +0 -2
- data/lib/rigor/cli/coverage_scan.rb +7 -11
- data/lib/rigor/cli/diagnostic_formats.rb +46 -62
- data/lib/rigor/cli/diff_command.rb +10 -20
- data/lib/rigor/cli/docs_command.rb +26 -40
- data/lib/rigor/cli/doctor_command.rb +8 -9
- data/lib/rigor/cli/explain_command.rb +4 -7
- data/lib/rigor/cli/fused_protection_renderer.rb +4 -5
- data/lib/rigor/cli/fused_protection_report.rb +6 -8
- data/lib/rigor/cli/lsp_command.rb +11 -19
- data/lib/rigor/cli/mcp_command.rb +4 -6
- data/lib/rigor/cli/mutation_protection_renderer.rb +3 -4
- data/lib/rigor/cli/mutation_protection_report.rb +6 -9
- data/lib/rigor/cli/options.rb +11 -19
- data/lib/rigor/cli/plugin_command.rb +18 -30
- data/lib/rigor/cli/plugins_command.rb +29 -51
- data/lib/rigor/cli/plugins_renderer.rb +12 -20
- data/lib/rigor/cli/prism_colorizer.rb +13 -19
- data/lib/rigor/cli/protection_fork_scan.rb +55 -0
- data/lib/rigor/cli/protection_renderer.rb +5 -7
- data/lib/rigor/cli/protection_report.rb +34 -18
- data/lib/rigor/cli/renderable.rb +4 -6
- data/lib/rigor/cli/show_bleedingedge_command.rb +5 -8
- data/lib/rigor/cli/sig_gen_command.rb +14 -26
- data/lib/rigor/cli/skill_command.rb +30 -47
- data/lib/rigor/cli/skill_describe.rb +40 -64
- data/lib/rigor/cli/trace_command.rb +9 -16
- data/lib/rigor/cli/trace_renderer.rb +35 -51
- data/lib/rigor/cli/triage_command.rb +6 -10
- data/lib/rigor/cli/triage_renderer.rb +1 -2
- data/lib/rigor/cli/type_of_command.rb +14 -23
- data/lib/rigor/cli/type_of_renderer.rb +3 -5
- data/lib/rigor/cli/type_scan_command.rb +6 -10
- data/lib/rigor/cli/type_scan_renderer.rb +3 -5
- data/lib/rigor/cli/type_scan_report.rb +2 -3
- data/lib/rigor/cli/upgrade_command.rb +2 -3
- data/lib/rigor/cli.rb +16 -28
- data/lib/rigor/config_audit.rb +35 -44
- data/lib/rigor/configuration/dependencies.rb +41 -70
- data/lib/rigor/configuration/severity_profile.rb +28 -42
- data/lib/rigor/configuration.rb +150 -241
- data/lib/rigor/environment/bundle_sig_discovery.rb +74 -118
- data/lib/rigor/environment/class_registry.rb +19 -26
- data/lib/rigor/environment/constant_type_cache_holder.rb +14 -21
- data/lib/rigor/environment/hkt_registry_holder.rb +6 -12
- data/lib/rigor/environment/lockfile_resolver.rb +31 -47
- data/lib/rigor/environment/missing_gem_constant_index.rb +128 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +39 -63
- data/lib/rigor/environment/rbs_coverage_report.rb +24 -42
- data/lib/rigor/environment/rbs_loader.rb +420 -518
- data/lib/rigor/environment/reflection.rb +28 -53
- data/lib/rigor/environment/reporters.rb +13 -24
- data/lib/rigor/environment.rb +225 -304
- data/lib/rigor/flow_contribution/conflict.rb +16 -27
- data/lib/rigor/flow_contribution/element.rb +7 -12
- data/lib/rigor/flow_contribution/fact.rb +33 -57
- data/lib/rigor/flow_contribution/merge_result.rb +6 -9
- data/lib/rigor/flow_contribution/merger.rb +32 -47
- data/lib/rigor/flow_contribution.rb +37 -55
- data/lib/rigor/inference/acceptance.rb +133 -219
- data/lib/rigor/inference/block_parameter_binder.rb +52 -82
- data/lib/rigor/inference/body_fixpoint.rb +22 -30
- data/lib/rigor/inference/budget_trace.rb +43 -63
- data/lib/rigor/inference/builtins/array_catalog.rb +4 -6
- data/lib/rigor/inference/builtins/comparable_catalog.rb +4 -6
- data/lib/rigor/inference/builtins/complex_catalog.rb +13 -19
- data/lib/rigor/inference/builtins/date_catalog.rb +38 -67
- data/lib/rigor/inference/builtins/encoding_catalog.rb +22 -36
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +4 -6
- data/lib/rigor/inference/builtins/exception_catalog.rb +38 -57
- data/lib/rigor/inference/builtins/hash_catalog.rb +9 -14
- data/lib/rigor/inference/builtins/method_catalog.rb +29 -48
- data/lib/rigor/inference/builtins/numeric_catalog.rb +13 -18
- data/lib/rigor/inference/builtins/pathname_catalog.rb +8 -13
- data/lib/rigor/inference/builtins/proc_catalog.rb +38 -62
- data/lib/rigor/inference/builtins/random_catalog.rb +21 -32
- data/lib/rigor/inference/builtins/range_catalog.rb +12 -21
- data/lib/rigor/inference/builtins/rational_catalog.rb +9 -15
- data/lib/rigor/inference/builtins/re_catalog.rb +29 -48
- data/lib/rigor/inference/builtins/set_catalog.rb +18 -27
- data/lib/rigor/inference/builtins/string_catalog.rb +12 -19
- data/lib/rigor/inference/builtins/struct_catalog.rb +18 -30
- data/lib/rigor/inference/builtins/time_catalog.rb +28 -44
- data/lib/rigor/inference/closure_escape_analyzer.rb +44 -67
- data/lib/rigor/inference/coverage_scanner.rb +13 -18
- data/lib/rigor/inference/def_return_typer.rb +8 -14
- data/lib/rigor/inference/dynamic_origin.rb +23 -20
- data/lib/rigor/inference/expression_typer.rb +794 -1154
- data/lib/rigor/inference/fallback.rb +8 -12
- data/lib/rigor/inference/fallback_tracer.rb +4 -10
- data/lib/rigor/inference/flow_tracer.rb +25 -36
- data/lib/rigor/inference/fork_map.rb +87 -0
- data/lib/rigor/inference/hkt_body.rb +45 -68
- data/lib/rigor/inference/hkt_body_parser.rb +33 -52
- data/lib/rigor/inference/hkt_reducer.rb +38 -59
- data/lib/rigor/inference/hkt_registry.rb +50 -76
- data/lib/rigor/inference/indexed_narrowing.rb +55 -81
- data/lib/rigor/inference/macro_block_self_type.rb +21 -34
- data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +11 -18
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +49 -83
- data/lib/rigor/inference/method_dispatcher/call_context.rb +16 -23
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +13 -22
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +307 -454
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +24 -34
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +22 -37
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +52 -81
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +51 -78
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +60 -97
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +23 -34
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +18 -26
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +37 -71
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +114 -182
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +122 -193
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +15 -24
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +64 -93
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +31 -45
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -9
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +356 -511
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +22 -32
- data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +15 -21
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +66 -82
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +10 -15
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -13
- data/lib/rigor/inference/method_dispatcher.rb +378 -524
- data/lib/rigor/inference/method_parameter_binder.rb +81 -114
- data/lib/rigor/inference/multi_target_binder.rb +51 -68
- data/lib/rigor/inference/mutation_widening.rb +115 -158
- data/lib/rigor/inference/narrowing.rb +627 -727
- data/lib/rigor/inference/origin_lookup.rb +38 -0
- data/lib/rigor/inference/parameter_inference_collector.rb +154 -109
- data/lib/rigor/inference/precision_scanner.rb +26 -35
- data/lib/rigor/inference/project_patched_methods.rb +20 -32
- data/lib/rigor/inference/project_patched_scanner.rb +23 -37
- data/lib/rigor/inference/protection_scanner.rb +16 -18
- data/lib/rigor/inference/rbs_type_translator.rb +49 -66
- data/lib/rigor/inference/scope_indexer.rb +473 -821
- data/lib/rigor/inference/statement_evaluator.rb +754 -1114
- data/lib/rigor/inference/struct_fold_safety.rb +34 -48
- data/lib/rigor/inference/synthetic_method.rb +9 -16
- data/lib/rigor/inference/synthetic_method_index.rb +20 -35
- data/lib/rigor/inference/synthetic_method_scanner.rb +63 -103
- data/lib/rigor/language_server/buffer_resolution.rb +6 -9
- data/lib/rigor/language_server/buffer_table.rb +11 -18
- data/lib/rigor/language_server/completion_provider.rb +69 -116
- data/lib/rigor/language_server/debouncer.rb +16 -25
- data/lib/rigor/language_server/diagnostic_publisher.rb +26 -40
- data/lib/rigor/language_server/document_symbol_provider.rb +14 -19
- data/lib/rigor/language_server/folding_range_provider.rb +11 -19
- data/lib/rigor/language_server/hover_provider.rb +8 -11
- data/lib/rigor/language_server/hover_renderer.rb +56 -93
- data/lib/rigor/language_server/loop.rb +14 -19
- data/lib/rigor/language_server/project_context.rb +46 -74
- data/lib/rigor/language_server/selection_range_provider.rb +9 -12
- data/lib/rigor/language_server/server.rb +55 -83
- data/lib/rigor/language_server/signature_help_provider.rb +29 -46
- data/lib/rigor/language_server/synchronized_writer.rb +4 -7
- data/lib/rigor/language_server/uri.rb +8 -13
- data/lib/rigor/language_server.rb +4 -6
- data/lib/rigor/mcp/loop.rb +2 -3
- data/lib/rigor/mcp/server.rb +4 -7
- data/lib/rigor/mcp.rb +3 -6
- data/lib/rigor/plugin/access_denied_error.rb +5 -8
- data/lib/rigor/plugin/additional_initializer.rb +21 -31
- data/lib/rigor/plugin/base.rb +335 -518
- data/lib/rigor/plugin/blueprint.rb +14 -23
- data/lib/rigor/plugin/box.rb +18 -29
- data/lib/rigor/plugin/fact_store.rb +16 -26
- data/lib/rigor/plugin/inflector.rb +37 -53
- data/lib/rigor/plugin/io_boundary.rb +33 -56
- data/lib/rigor/plugin/isolation.rb +42 -55
- data/lib/rigor/plugin/load_error.rb +10 -15
- data/lib/rigor/plugin/loader.rb +30 -49
- data/lib/rigor/plugin/macro/block_as_method.rb +20 -32
- data/lib/rigor/plugin/macro/heredoc_template.rb +35 -58
- data/lib/rigor/plugin/macro/nested_class_template.rb +22 -36
- data/lib/rigor/plugin/macro/trait_registry.rb +34 -51
- data/lib/rigor/plugin/macro.rb +10 -15
- data/lib/rigor/plugin/manifest.rb +85 -144
- data/lib/rigor/plugin/node_context.rb +14 -22
- data/lib/rigor/plugin/node_rule_walk.rb +49 -74
- data/lib/rigor/plugin/protocol_contract.rb +25 -39
- data/lib/rigor/plugin/registry.rb +132 -205
- data/lib/rigor/plugin/services.rb +21 -33
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -19
- data/lib/rigor/plugin/trust_policy.rb +24 -38
- data/lib/rigor/plugin/type_node_resolver.rb +15 -24
- data/lib/rigor/protection/diagnostic_oracle.rb +11 -13
- data/lib/rigor/protection/mutation_scanner.rb +27 -35
- data/lib/rigor/protection/mutator.rb +50 -70
- data/lib/rigor/protection/test_suite_oracle.rb +20 -27
- data/lib/rigor/rbs_extended/conformance_checker.rb +48 -68
- data/lib/rigor/rbs_extended/hkt_directives.rb +28 -54
- data/lib/rigor/rbs_extended/reporter.rb +24 -40
- data/lib/rigor/rbs_extended.rb +107 -197
- data/lib/rigor/reflection.rb +68 -86
- data/lib/rigor/scope/discovery_index.rb +14 -19
- data/lib/rigor/scope.rb +255 -310
- data/lib/rigor/sig_gen/classification.rb +6 -10
- data/lib/rigor/sig_gen/generator.rb +197 -323
- data/lib/rigor/sig_gen/layout_index.rb +12 -20
- data/lib/rigor/sig_gen/method_candidate.rb +12 -17
- data/lib/rigor/sig_gen/observation_collector.rb +38 -70
- data/lib/rigor/sig_gen/observed_call.rb +13 -23
- data/lib/rigor/sig_gen/path_mapper.rb +17 -29
- data/lib/rigor/sig_gen/renderer.rb +7 -13
- data/lib/rigor/sig_gen/type_elaborator.rb +15 -28
- data/lib/rigor/sig_gen/write_result.rb +8 -16
- data/lib/rigor/sig_gen/writer.rb +95 -174
- data/lib/rigor/sig_gen.rb +3 -6
- data/lib/rigor/signature_path_audit.rb +24 -30
- data/lib/rigor/source/constant_path.rb +10 -14
- data/lib/rigor/source/literals.rb +31 -45
- data/lib/rigor/source/node_locator.rb +9 -11
- data/lib/rigor/source/node_walker.rb +9 -13
- data/lib/rigor/source.rb +3 -4
- data/lib/rigor/testing.rb +16 -20
- data/lib/rigor/triage/catalogue.rb +38 -62
- data/lib/rigor/triage.rb +31 -52
- data/lib/rigor/trinary.rb +9 -13
- data/lib/rigor/type/acceptance_router.rb +4 -6
- data/lib/rigor/type/accepts_result.rb +10 -14
- data/lib/rigor/type/app.rb +19 -27
- data/lib/rigor/type/bot.rb +4 -6
- data/lib/rigor/type/bound_method.rb +10 -15
- data/lib/rigor/type/combinator.rb +165 -257
- data/lib/rigor/type/constant.rb +23 -34
- data/lib/rigor/type/data_class.rb +10 -15
- data/lib/rigor/type/data_instance.rb +14 -20
- data/lib/rigor/type/difference.rb +21 -32
- data/lib/rigor/type/dynamic.rb +3 -5
- data/lib/rigor/type/hash_shape.rb +32 -18
- data/lib/rigor/type/integer_range.rb +11 -16
- data/lib/rigor/type/intersection.rb +27 -42
- data/lib/rigor/type/nominal.rb +10 -15
- data/lib/rigor/type/plain_lattice.rb +9 -13
- data/lib/rigor/type/refined.rb +67 -114
- data/lib/rigor/type/singleton.rb +4 -6
- data/lib/rigor/type/struct_class.rb +11 -16
- data/lib/rigor/type/struct_instance.rb +15 -21
- data/lib/rigor/type/tuple.rb +14 -19
- data/lib/rigor/type/union.rb +30 -42
- data/lib/rigor/type_node/generic.rb +14 -26
- data/lib/rigor/type_node/identifier.rb +12 -19
- data/lib/rigor/type_node.rb +3 -12
- data/lib/rigor/value_semantics.rb +16 -21
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +18 -30
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +16 -29
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +12 -23
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +22 -39
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +26 -46
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +51 -87
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +10 -17
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +26 -39
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +141 -184
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +30 -51
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +57 -92
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +92 -108
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +13 -22
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +15 -25
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +8 -13
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +16 -24
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +40 -48
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +140 -129
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +56 -59
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +27 -41
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +16 -24
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +172 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +168 -241
- data/plugins/rigor-activerecord/lib/rigor-activerecord.rb +3 -5
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +8 -14
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +9 -15
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +4 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +23 -41
- data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +3 -5
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +6 -10
- data/plugins/rigor-activesupport-core-ext/lib/rigor-activesupport-core-ext.rb +8 -11
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +32 -0
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +30 -45
- data/plugins/rigor-devise/lib/rigor-devise.rb +3 -5
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +43 -69
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +26 -35
- data/plugins/rigor-dry-schema/lib/rigor-dry-schema.rb +3 -5
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +30 -47
- data/plugins/rigor-dry-struct/lib/rigor-dry-struct.rb +3 -5
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +67 -113
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +29 -46
- data/plugins/rigor-dry-types/lib/rigor-dry-types.rb +3 -5
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -20
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +14 -23
- data/plugins/rigor-dry-validation/lib/rigor-dry-validation.rb +2 -3
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +22 -37
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +28 -46
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +13 -17
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -47
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +52 -80
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +21 -31
- data/plugins/rigor-graphql/lib/rigor-graphql.rb +3 -5
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +11 -17
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +20 -34
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +45 -75
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +22 -36
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +22 -41
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +17 -32
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +6 -12
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +4 -8
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +17 -32
- data/plugins/rigor-rails/lib/rigor-rails.rb +11 -18
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +58 -92
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +20 -34
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +20 -31
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +49 -82
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +55 -92
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +51 -90
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +17 -30
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +189 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +38 -70
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +61 -75
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +319 -505
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +91 -96
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +32 -54
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +8 -13
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +9 -16
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +24 -39
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +13 -22
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +34 -56
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +22 -42
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +43 -75
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +27 -39
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +14 -19
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +20 -34
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +28 -47
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +21 -37
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +13 -22
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +18 -31
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +6 -10
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +14 -29
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +23 -41
- data/plugins/rigor-sinatra/lib/rigor-sinatra.rb +3 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +23 -38
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +57 -96
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +22 -35
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +41 -69
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +6 -10
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +24 -41
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +29 -50
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +55 -88
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +159 -262
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +21 -29
- data/plugins/rigor-statesman/lib/rigor-statesman.rb +3 -5
- data/plugins/rigor-typescript-utility-types/lib/rigor/plugin/typescript_utility_types.rb +23 -37
- data/plugins/rigor-typescript-utility-types/lib/rigor-typescript-utility-types.rb +4 -6
- data/sig/rigor/environment.rbs +1 -0
- data/sig/rigor/scope.rbs +6 -0
- metadata +9 -1
|
@@ -10,6 +10,7 @@ require_relative "block_parameter_binder"
|
|
|
10
10
|
require_relative "body_fixpoint"
|
|
11
11
|
require_relative "budget_trace"
|
|
12
12
|
require_relative "dynamic_origin"
|
|
13
|
+
require_relative "origin_lookup"
|
|
13
14
|
require_relative "fallback"
|
|
14
15
|
require_relative "flow_tracer"
|
|
15
16
|
require_relative "indexed_narrowing"
|
|
@@ -20,52 +21,41 @@ require_relative "struct_fold_safety"
|
|
|
20
21
|
|
|
21
22
|
module Rigor
|
|
22
23
|
module Inference
|
|
23
|
-
# Translates AST nodes into Rigor::Type values, consulting the surrounding
|
|
24
|
-
#
|
|
25
|
-
#
|
|
24
|
+
# Translates AST nodes into Rigor::Type values, consulting the surrounding Rigor::Scope for local-variable
|
|
25
|
+
# bindings and the environment registry for nominal-type resolution. Pure: never mutates the receiver
|
|
26
|
+
# scope.
|
|
26
27
|
#
|
|
27
|
-
# Accepts both real Prism nodes and synthetic Rigor::AST::Node
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
# without building a real Prism expression.
|
|
28
|
+
# Accepts both real Prism nodes and synthetic Rigor::AST::Node instances; the synthetic family lets
|
|
29
|
+
# callers and plugins ask "what would the analyzer infer if a value of type T appeared here?" without
|
|
30
|
+
# building a real Prism expression.
|
|
31
31
|
#
|
|
32
|
-
# Slice 1 recognises literal expressions, local-variable reads/writes,
|
|
33
|
-
#
|
|
34
|
-
# Prism::
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
# global-variable, and self positions. Many of those handlers return
|
|
41
|
-
# Dynamic[Top] silently because they are non-value or out-of-scope
|
|
42
|
-
# positions for Slice 2; later slices refine them in place.
|
|
32
|
+
# Slice 1 recognises literal expressions, local-variable reads/writes, shallow Array literals, and
|
|
33
|
+
# Rigor::AST::TypeNode. Slice 2 adds Prism::CallNode (routed through Rigor::Inference::MethodDispatcher),
|
|
34
|
+
# Prism::ArgumentsNode (a non-value position whose children are typed individually by the CallNode
|
|
35
|
+
# handler), constant references resolved through Rigor::Environment::ClassRegistry, hash and interpolated
|
|
36
|
+
# string/symbol literals, definition expressions (def/class/module), and explicit handlers for parameter,
|
|
37
|
+
# block, splat, instance/class/global-variable, and self positions. Many of those handlers return
|
|
38
|
+
# Dynamic[Top] silently because they are non-value or out-of-scope positions for Slice 2; later slices
|
|
39
|
+
# refine them in place.
|
|
43
40
|
#
|
|
44
|
-
# Slice 4 phase 2b types bare-constant references (`Foo`, `Foo::Bar`)
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
# corresponding instance type is reachable through `Foo.new` and the
|
|
48
|
-
# value-lattice projections.
|
|
41
|
+
# Slice 4 phase 2b types bare-constant references (`Foo`, `Foo::Bar`) as `Singleton[Foo]` rather than
|
|
42
|
+
# `Nominal[Foo]`, so that method dispatch on the constant correctly looks up *class* methods. The
|
|
43
|
+
# corresponding instance type is reachable through `Foo.new` and the value-lattice projections.
|
|
49
44
|
#
|
|
50
|
-
# Every other node falls back to Dynamic[Top] per the fail-soft
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
# each fallback; the tracer MUST NOT change the return value of
|
|
55
|
-
# type_of.
|
|
45
|
+
# Every other node falls back to Dynamic[Top] per the fail-soft policy in
|
|
46
|
+
# docs/internal-spec/inference-engine.md. The optional tracer is a Rigor::Inference::FallbackTracer (or
|
|
47
|
+
# any object answering #record_fallback) that receives a Fallback event for each fallback; the tracer
|
|
48
|
+
# MUST NOT change the return value of type_of.
|
|
56
49
|
# rubocop:disable Metrics/ClassLength
|
|
57
50
|
class ExpressionTyper
|
|
58
|
-
# Hash-based dispatch keeps `type_of` linear and lets future slices add
|
|
59
|
-
#
|
|
60
|
-
# cyclomatic budget. Anonymous Prism subclasses are not expected.
|
|
51
|
+
# Hash-based dispatch keeps `type_of` linear and lets future slices add node kinds without growing a
|
|
52
|
+
# single case statement past RuboCop's cyclomatic budget. Anonymous Prism subclasses are not expected.
|
|
61
53
|
PRISM_DISPATCH = {
|
|
62
54
|
# Literals
|
|
63
55
|
Prism::IntegerNode => :type_of_literal_value,
|
|
64
56
|
Prism::FloatNode => :type_of_literal_value,
|
|
65
|
-
# `1i` / `2.5ri` lift via `node.value` which is already a
|
|
66
|
-
#
|
|
67
|
-
# value is a `Rational`. `Type::Constant` accepts both
|
|
68
|
-
# via `SCALAR_CLASSES`.
|
|
57
|
+
# `1i` / `2.5ri` lift via `node.value` which is already a `Complex` Ruby value; same for `1r` / `1.5r`
|
|
58
|
+
# whose value is a `Rational`. `Type::Constant` accepts both via `SCALAR_CLASSES`.
|
|
69
59
|
Prism::ImaginaryNode => :type_of_literal_value,
|
|
70
60
|
Prism::RationalNode => :type_of_literal_value,
|
|
71
61
|
Prism::SymbolNode => :symbol_type_for,
|
|
@@ -125,12 +115,10 @@ module Rigor
|
|
|
125
115
|
Prism::IndexOrWriteNode => :type_of_assignment_write,
|
|
126
116
|
Prism::IndexAndWriteNode => :type_of_assignment_write,
|
|
127
117
|
Prism::MultiWriteNode => :type_of_assignment_write,
|
|
128
|
-
# LHS-only target nodes (destructuring assignment, pattern matching,
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
# into the scope is the StatementEvaluator / MultiTargetBinder /
|
|
133
|
-
# BlockParameterBinder side's concern.
|
|
118
|
+
# LHS-only target nodes (destructuring assignment, pattern matching, `for x in xs`, block parameter
|
|
119
|
+
# `|a, (b, c)|`). They have no value to extract — the type-of pass acknowledges the node class so the
|
|
120
|
+
# coverage scanner stops flagging it; binding the inner names back into the scope is the
|
|
121
|
+
# StatementEvaluator / MultiTargetBinder / BlockParameterBinder side's concern.
|
|
134
122
|
Prism::LocalVariableTargetNode => :type_of_non_value,
|
|
135
123
|
Prism::MultiTargetNode => :type_of_non_value,
|
|
136
124
|
Prism::InstanceVariableTargetNode => :type_of_non_value,
|
|
@@ -203,9 +191,8 @@ module Rigor
|
|
|
203
191
|
Prism::ForwardingArgumentsNode => :type_of_non_value,
|
|
204
192
|
Prism::WhileNode => :type_of_loop,
|
|
205
193
|
Prism::UntilNode => :type_of_loop,
|
|
206
|
-
# `for` matches `eval_for`'s statement-path policy: the loop
|
|
207
|
-
#
|
|
208
|
-
# same as `while` / `until` — annotating a `for`'s `end` line as
|
|
194
|
+
# `for` matches `eval_for`'s statement-path policy: the loop expression types `Constant[nil]` (no
|
|
195
|
+
# `break VALUE` observed), same as `while` / `until` — annotating a `for`'s `end` line as
|
|
209
196
|
# `Dynamic[top]` was a display artifact of the old mapping.
|
|
210
197
|
Prism::ForNode => :type_of_loop,
|
|
211
198
|
Prism::DefinedNode => :type_of_defined,
|
|
@@ -242,13 +229,10 @@ module Rigor
|
|
|
242
229
|
end
|
|
243
230
|
|
|
244
231
|
def untraced_type_of(node)
|
|
245
|
-
# Slice A-declarations. ScopeIndexer pre-fills
|
|
246
|
-
# `
|
|
247
|
-
#
|
|
248
|
-
#
|
|
249
|
-
# through to `Dynamic[Top]`. The override is consulted
|
|
250
|
-
# before any other dispatch and bypasses fail-soft
|
|
251
|
-
# tracing on a recognised match.
|
|
232
|
+
# Slice A-declarations. ScopeIndexer pre-fills `scope.declared_types` for declaration-position nodes
|
|
233
|
+
# (`module Foo` / `class Bar` headers) with the qualified `Singleton` type so the header itself does
|
|
234
|
+
# not fall through to `Dynamic[Top]`. The override is consulted before any other dispatch and
|
|
235
|
+
# bypasses fail-soft tracing on a recognised match.
|
|
252
236
|
declared = scope.declared_types[node]
|
|
253
237
|
return declared if declared
|
|
254
238
|
|
|
@@ -284,26 +268,29 @@ module Rigor
|
|
|
284
268
|
Type::Combinator.constant_of(nil)
|
|
285
269
|
end
|
|
286
270
|
|
|
287
|
-
# All `*WriteNode` flavours expose a `.value` rvalue child. Their type
|
|
288
|
-
#
|
|
289
|
-
#
|
|
290
|
-
# never of `type_of` itself.
|
|
271
|
+
# All `*WriteNode` flavours expose a `.value` rvalue child. Their type is the type of that rvalue.
|
|
272
|
+
# Binding the result back into the scope is the responsibility of the statement-level evaluator
|
|
273
|
+
# (Slice 3), never of `type_of` itself.
|
|
291
274
|
def type_of_assignment_write(node)
|
|
292
275
|
type_of(node.value)
|
|
293
276
|
end
|
|
294
277
|
|
|
295
|
-
# Slice 7 phase 1 — instance/class/global variable reads.
|
|
296
|
-
#
|
|
297
|
-
#
|
|
298
|
-
#
|
|
299
|
-
#
|
|
300
|
-
#
|
|
301
|
-
# a follow-up slice; the read handlers MUST NOT raise on a
|
|
302
|
-
# missing binding and MUST NOT record a fallback event in
|
|
303
|
-
# either branch — the absence of a binding is a recognised
|
|
304
|
-
# semantic outcome, not a fail-soft compromise.
|
|
278
|
+
# Slice 7 phase 1 — instance/class/global variable reads. Each lookup returns the type currently bound
|
|
279
|
+
# in the surrounding scope's per-kind binding map (populated by `StatementEvaluator` write handlers
|
|
280
|
+
# within the same method body), falling through to `Dynamic[Top]` when no binding is recorded.
|
|
281
|
+
# Cross-method ivar/cvar inference is a follow-up slice; the read handlers MUST NOT raise on a missing
|
|
282
|
+
# binding and MUST NOT record a fallback event in either branch — the absence of a binding is a
|
|
283
|
+
# recognised semantic outcome, not a fail-soft compromise.
|
|
305
284
|
def type_of_instance_variable_read(node)
|
|
306
|
-
scope.ivar(node.name)
|
|
285
|
+
bound = scope.ivar(node.name)
|
|
286
|
+
return bound if bound
|
|
287
|
+
|
|
288
|
+
# ADR-82 — an unbound instance-variable read is dynamic because the engine does not track this
|
|
289
|
+
# field's type (it is assigned in another method, or never seen). Route it to ivar-field typing
|
|
290
|
+
# (ADR-58) rather than reporting no cause, and let WD6 carry it through a `@x.foo.bar` chain. This
|
|
291
|
+
# records provenance only (still `dynamic_top`), never a fallback / tracer event.
|
|
292
|
+
scope.record_dynamic_origin(node, DynamicOrigin::INFERRED_RETURN_UNTYPED)
|
|
293
|
+
dynamic_top
|
|
307
294
|
end
|
|
308
295
|
|
|
309
296
|
def type_of_class_variable_read(node)
|
|
@@ -322,22 +309,18 @@ module Rigor
|
|
|
322
309
|
statements_type_for(node.statements)
|
|
323
310
|
end
|
|
324
311
|
|
|
325
|
-
# Recognised position that does not produce a value: parameter lists
|
|
326
|
-
#
|
|
327
|
-
#
|
|
328
|
-
#
|
|
329
|
-
# off the unrecognised list without faking a value type.
|
|
312
|
+
# Recognised position that does not produce a value: parameter lists and individual parameter
|
|
313
|
+
# declarations, splats inside argument lists, key-value pairs in hashes, and the implicit-rest token
|
|
314
|
+
# inside destructuring. Returning Dynamic[Top] silently keeps these off the unrecognised list without
|
|
315
|
+
# faking a value type.
|
|
330
316
|
def type_of_non_value(_node)
|
|
331
317
|
dynamic_top
|
|
332
318
|
end
|
|
333
319
|
|
|
334
|
-
# `Prism::SelfNode` resolves to the scope's
|
|
335
|
-
# `
|
|
336
|
-
# `
|
|
337
|
-
#
|
|
338
|
-
# `self` is `Singleton[<class>]`; instance-method `self` is
|
|
339
|
-
# `Nominal[<class>]`; singleton-method `self` is
|
|
340
|
-
# `Singleton[<class>]`.
|
|
320
|
+
# `Prism::SelfNode` resolves to the scope's `self_type` when one has been injected (by
|
|
321
|
+
# `StatementEvaluator` at class-body and method-body boundaries) or `Dynamic[Top]` at the top level.
|
|
322
|
+
# Class-body `self` is `Singleton[<class>]`; instance-method `self` is `Nominal[<class>]`;
|
|
323
|
+
# singleton-method `self` is `Singleton[<class>]`.
|
|
341
324
|
def type_of_self_node(_node)
|
|
342
325
|
scope.self_type || dynamic_top
|
|
343
326
|
end
|
|
@@ -346,11 +329,9 @@ module Rigor
|
|
|
346
329
|
dynamic_top
|
|
347
330
|
end
|
|
348
331
|
|
|
349
|
-
# `defined?(expr)` returns `String | nil` per Ruby semantics —
|
|
350
|
-
#
|
|
351
|
-
#
|
|
352
|
-
# is not evaluated (it is statically inspected by the runtime),
|
|
353
|
-
# so the typer does not recurse into it.
|
|
332
|
+
# `defined?(expr)` returns `String | nil` per Ruby semantics — a description of the expression's
|
|
333
|
+
# category (`"local-variable"`, `"method"`, ...) when defined, or `nil` when not. The argument is not
|
|
334
|
+
# evaluated (it is statically inspected by the runtime), so the typer does not recurse into it.
|
|
354
335
|
def type_of_defined(_node)
|
|
355
336
|
Type::Combinator.union(
|
|
356
337
|
Type::Combinator.nominal_of("String"),
|
|
@@ -358,10 +339,9 @@ module Rigor
|
|
|
358
339
|
)
|
|
359
340
|
end
|
|
360
341
|
|
|
361
|
-
# `$1`, `$&`, `$'`, `$+`, `$\`` — the regex back-reference and
|
|
362
|
-
#
|
|
363
|
-
#
|
|
364
|
-
# which back-reference shape Prism emitted.
|
|
342
|
+
# `$1`, `$&`, `$'`, `$+`, `$\`` — the regex back-reference and numbered-capture globals each carry
|
|
343
|
+
# `String | nil`. They share the typer because the typing rule is identical regardless of which
|
|
344
|
+
# back-reference shape Prism emitted.
|
|
365
345
|
def type_of_string_or_nil(_node)
|
|
366
346
|
Type::Combinator.union(
|
|
367
347
|
Type::Combinator.nominal_of("String"),
|
|
@@ -369,23 +349,20 @@ module Rigor
|
|
|
369
349
|
)
|
|
370
350
|
end
|
|
371
351
|
|
|
372
|
-
# `$1` / `$2` / ... — numbered match-data globals. When the
|
|
373
|
-
#
|
|
374
|
-
#
|
|
375
|
-
# /(\d+)/ =~ s; raise; end`), prefer the scope-bound type.
|
|
376
|
-
# Falls back to the default `String | nil`.
|
|
352
|
+
# `$1` / `$2` / ... — numbered match-data globals. When the narrowing tier has bound a tighter type for
|
|
353
|
+
# this number (typically `String` after a `=~`-success guard like `unless /(\d+)/ =~ s; raise; end`),
|
|
354
|
+
# prefer the scope-bound type. Falls back to the default `String | nil`.
|
|
377
355
|
def type_of_numbered_reference(node)
|
|
378
356
|
scope.global(:"$#{node.number}") || type_of_string_or_nil(node)
|
|
379
357
|
end
|
|
380
358
|
|
|
381
|
-
# `$&` / `$'` / `$\`` / `$+` — symbolic back-references. Same
|
|
382
|
-
# narrowing model as numbered references.
|
|
359
|
+
# `$&` / `$'` / `$\`` / `$+` — symbolic back-references. Same narrowing model as numbered references.
|
|
383
360
|
def type_of_back_reference(node)
|
|
384
361
|
scope.global(node.name) || type_of_string_or_nil(node)
|
|
385
362
|
end
|
|
386
363
|
|
|
387
|
-
# `expr in pattern` — pattern-match predicate. Returns `true`
|
|
388
|
-
#
|
|
364
|
+
# `expr in pattern` — pattern-match predicate. Returns `true` when the pattern matches, `false`
|
|
365
|
+
# otherwise.
|
|
389
366
|
def type_of_match_predicate(_node)
|
|
390
367
|
Type::Combinator.union(
|
|
391
368
|
Type::Combinator.constant_of(true),
|
|
@@ -393,42 +370,54 @@ module Rigor
|
|
|
393
370
|
)
|
|
394
371
|
end
|
|
395
372
|
|
|
396
|
-
# `expr => pattern` — one-line pattern-match assertion. Raises
|
|
397
|
-
#
|
|
398
|
-
# itself evaluates to `nil`.
|
|
373
|
+
# `expr => pattern` — one-line pattern-match assertion. Raises `NoMatchingPatternError` on mismatch; on
|
|
374
|
+
# success the expression itself evaluates to `nil`.
|
|
399
375
|
def type_of_match_required(_node)
|
|
400
376
|
Type::Combinator.constant_of(nil)
|
|
401
377
|
end
|
|
402
378
|
|
|
403
|
-
# The expression `Foo` evaluates to the *class object* `Foo`, not
|
|
404
|
-
#
|
|
405
|
-
#
|
|
406
|
-
# that receiver looks up class methods (`Foo.new`, `Foo.bar`, ...).
|
|
379
|
+
# The expression `Foo` evaluates to the *class object* `Foo`, not an instance. From Slice 4 phase 2b on
|
|
380
|
+
# we therefore type a bare-constant reference as `Singleton[Foo]`; method dispatch on that receiver
|
|
381
|
+
# looks up class methods (`Foo.new`, `Foo.bar`, ...).
|
|
407
382
|
#
|
|
408
|
-
# Slice A constant-walk: when the literal name does not resolve,
|
|
409
|
-
#
|
|
410
|
-
#
|
|
411
|
-
# `Inference::FallbackTracer
|
|
412
|
-
# resolves to `Rigor::Inference::FallbackTracer`.
|
|
383
|
+
# Slice A constant-walk: when the literal name does not resolve, we try a lexical walk based on the
|
|
384
|
+
# surrounding class context exposed through `scope.self_type` so a reference like
|
|
385
|
+
# `Inference::FallbackTracer` from inside `Rigor::CLI::Foo` resolves to
|
|
386
|
+
# `Rigor::Inference::FallbackTracer`.
|
|
413
387
|
def type_of_constant_read(node)
|
|
414
|
-
resolve_constant_name(node.name.to_s) ||
|
|
388
|
+
resolve_constant_name(node.name.to_s) || unresolved_constant_fallback(node, node.name.to_s)
|
|
415
389
|
end
|
|
416
390
|
|
|
417
391
|
def type_of_constant_path(node)
|
|
418
392
|
full_name = Source::ConstantPath.qualified_name_or_nil(node)
|
|
419
393
|
return fallback_for(node, family: :prism) if full_name.nil?
|
|
420
394
|
|
|
421
|
-
resolve_constant_name(full_name) ||
|
|
395
|
+
resolve_constant_name(full_name) || unresolved_constant_fallback(node, full_name)
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# ADR-82 WD9 — an unresolved constant whose root name a locked, RBS-less gem declares carries the
|
|
399
|
+
# `external_gem_without_rbs` cause instead of the generic `unsupported_syntax`. The constant read is
|
|
400
|
+
# where the class name is last visible (a no-RBS gem's receiver never types Nominal, so the dispatch
|
|
401
|
+
# tiers that record this cause under ADR-10 / `pre_eval:` opt-ins can't see it); WD6 chain inheritance
|
|
402
|
+
# then carries the cause through `Faraday.new.get(...)`. Side-channel only — the type stays the same
|
|
403
|
+
# `Dynamic[top]`, and an unindexed constant (project typo, unanalyzed project path) keeps the generic
|
|
404
|
+
# cause: the fail-open direction is a missing label, never a wrong one.
|
|
405
|
+
def unresolved_constant_fallback(node, full_name)
|
|
406
|
+
root = full_name.delete_prefix("::").split("::").first
|
|
407
|
+
owner = root && scope.environment.missing_rbs_gem_owner(root)
|
|
408
|
+
return fallback_for(node, family: :prism) unless owner
|
|
409
|
+
|
|
410
|
+
inner = dynamic_top
|
|
411
|
+
record_fallback(node, family: :prism, inner_type: inner, origin: DynamicOrigin::EXTERNAL_GEM_WITHOUT_RBS)
|
|
412
|
+
scope.record_dynamic_origin(node, DynamicOrigin::EXTERNAL_GEM_WITHOUT_RBS)
|
|
413
|
+
inner
|
|
422
414
|
end
|
|
423
415
|
|
|
424
|
-
# Try the literal name first, then walk Ruby's lexical lookup by
|
|
425
|
-
#
|
|
426
|
-
#
|
|
427
|
-
#
|
|
428
|
-
#
|
|
429
|
-
# constant value such as `BUCKETS: Array[Symbol]`).
|
|
430
|
-
# Returns the matched `Rigor::Type` or nil; the caller decides
|
|
431
|
-
# whether to fall back.
|
|
416
|
+
# Try the literal name first, then walk Ruby's lexical lookup by progressively prefixing the surrounding
|
|
417
|
+
# class path (peeled one `::segment` at a time). For each candidate the lookup consults
|
|
418
|
+
# `Environment#singleton_for_name` (a class object) and then `Environment#constant_for_name` (a
|
|
419
|
+
# non-class constant value such as `BUCKETS: Array[Symbol]`). Returns the matched `Rigor::Type` or nil;
|
|
420
|
+
# the caller decides whether to fall back.
|
|
432
421
|
def resolve_constant_name(name)
|
|
433
422
|
env = scope.environment
|
|
434
423
|
discovered = scope.discovered_classes
|
|
@@ -440,9 +429,8 @@ module Rigor
|
|
|
440
429
|
in_source_class = discovered[candidate]
|
|
441
430
|
return in_source_class if in_source_class
|
|
442
431
|
|
|
443
|
-
# In-source value-bearing constants take precedence
|
|
444
|
-
#
|
|
445
|
-
# authoritative source for its own constants.
|
|
432
|
+
# In-source value-bearing constants take precedence over RBS constant decls because user code is
|
|
433
|
+
# the authoritative source for its own constants.
|
|
446
434
|
in_source_value = in_source[candidate]
|
|
447
435
|
return in_source_value if in_source_value
|
|
448
436
|
|
|
@@ -452,20 +440,17 @@ module Rigor
|
|
|
452
440
|
nil
|
|
453
441
|
end
|
|
454
442
|
|
|
455
|
-
# The candidate qualified names to try, in Ruby's lexical
|
|
456
|
-
#
|
|
457
|
-
#
|
|
458
|
-
# the bare `name`. Top-level scopes (no `self_type`) yield
|
|
459
|
-
# only `[name]`, preserving the pre-walk behaviour.
|
|
443
|
+
# The candidate qualified names to try, in Ruby's lexical order: most-qualified first (the surrounding
|
|
444
|
+
# class path joined to `name`), then progressively less-qualified, then the bare `name`. Top-level
|
|
445
|
+
# scopes (no `self_type`) yield only `[name]`, preserving the pre-walk behaviour.
|
|
460
446
|
def lexical_constant_candidates(name)
|
|
461
447
|
prefix = enclosing_class_path
|
|
462
448
|
candidates = []
|
|
463
449
|
while prefix && !prefix.empty?
|
|
464
450
|
candidates << "#{prefix}::#{name}"
|
|
465
|
-
# Strip the last `::` segment without `rpartition`'s throwaway
|
|
466
|
-
#
|
|
467
|
-
#
|
|
468
|
-
# profile): `rindex` + slice gives the same prefix, or nil.
|
|
451
|
+
# Strip the last `::` segment without `rpartition`'s throwaway 3-element array + extra substrings
|
|
452
|
+
# (this loop is the sole caller of the `String#rpartition` allocation seen in the profile): `rindex`
|
|
453
|
+
# + slice gives the same prefix, or nil.
|
|
469
454
|
idx = prefix.rindex("::")
|
|
470
455
|
prefix = idx ? prefix[0, idx] : nil
|
|
471
456
|
end
|
|
@@ -473,10 +458,8 @@ module Rigor
|
|
|
473
458
|
candidates
|
|
474
459
|
end
|
|
475
460
|
|
|
476
|
-
# Pulls the enclosing qualified class name out of
|
|
477
|
-
# `
|
|
478
|
-
# `Singleton[T]` both expose `class_name`. Returns nil when
|
|
479
|
-
# no class context is available (top-level).
|
|
461
|
+
# Pulls the enclosing qualified class name out of `scope.self_type` when one is set. `Nominal[T]` and
|
|
462
|
+
# `Singleton[T]` both expose `class_name`. Returns nil when no class context is available (top-level).
|
|
480
463
|
def enclosing_class_path
|
|
481
464
|
st = scope.self_type
|
|
482
465
|
case st
|
|
@@ -484,23 +467,16 @@ module Rigor
|
|
|
484
467
|
end
|
|
485
468
|
end
|
|
486
469
|
|
|
487
|
-
# Slice 5 phase 1 upgrades hash literals to `HashShape{...}`
|
|
488
|
-
#
|
|
489
|
-
#
|
|
490
|
-
#
|
|
491
|
-
# `Hash
|
|
492
|
-
# (`{ **other }`) and dynamic keys widen to the underlying
|
|
493
|
-
# `Hash[K, V]` form by unioning the types each entry exposes;
|
|
494
|
-
# when no concrete pair survives we fall back to the raw `Hash`
|
|
495
|
-
# so callers stay backward compatible.
|
|
470
|
+
# Slice 5 phase 1 upgrades hash literals to `HashShape{...}` when every entry is a static `AssocNode`
|
|
471
|
+
# whose key is a `SymbolNode` or `StringNode` with a known value (covering the `{ a: 1, "b" => 2 }`
|
|
472
|
+
# pattern and falling back to the generic `Hash[K, V]` form otherwise). Splatted entries (`{ **other }`)
|
|
473
|
+
# and dynamic keys widen to the underlying `Hash[K, V]` form by unioning the types each entry exposes;
|
|
474
|
+
# when no concrete pair survives we fall back to the raw `Hash` so callers stay backward compatible.
|
|
496
475
|
def type_of_hash(node)
|
|
497
476
|
elements = node.respond_to?(:elements) ? node.elements : []
|
|
498
|
-
# v0.0.7 — `{}` resolves to the empty `HashShape{}` carrier
|
|
499
|
-
#
|
|
500
|
-
#
|
|
501
|
-
# but `HashShape{}` pins the literal's known size (zero)
|
|
502
|
-
# so HashShape projections (`empty?`, `first`, `count`,
|
|
503
|
-
# …) fold against it.
|
|
477
|
+
# v0.0.7 — `{}` resolves to the empty `HashShape{}` carrier rather than `Nominal[Hash]`, mirroring the
|
|
478
|
+
# v0.0.6 empty-array literal change. Both forms erase to plain `Hash`, but `HashShape{}` pins the
|
|
479
|
+
# literal's known size (zero) so HashShape projections (`empty?`, `first`, `count`, …) fold against it.
|
|
504
480
|
return Type::Combinator.hash_shape_of({}) if elements.empty?
|
|
505
481
|
|
|
506
482
|
shape = static_hash_shape_for(elements)
|
|
@@ -515,9 +491,8 @@ module Rigor
|
|
|
515
491
|
)
|
|
516
492
|
end
|
|
517
493
|
|
|
518
|
-
# Builds `HashShape{...}` when every entry is an `AssocNode`
|
|
519
|
-
#
|
|
520
|
-
# otherwise so the caller falls back to the generic shape.
|
|
494
|
+
# Builds `HashShape{...}` when every entry is an `AssocNode` whose key is a static Symbol or String
|
|
495
|
+
# literal. Returns nil otherwise so the caller falls back to the generic shape.
|
|
521
496
|
def static_hash_shape_for(elements)
|
|
522
497
|
pairs = {}
|
|
523
498
|
elements.each do |entry|
|
|
@@ -534,10 +509,9 @@ module Rigor
|
|
|
534
509
|
Type::Combinator.hash_shape_of(pairs)
|
|
535
510
|
end
|
|
536
511
|
|
|
537
|
-
# Returns the static (Symbol|String) literal carried by a hash
|
|
538
|
-
#
|
|
539
|
-
#
|
|
540
|
-
# are non-nil (interpolation produces a nil unescaped).
|
|
512
|
+
# Returns the static (Symbol|String) literal carried by a hash key node, or nil when the key is
|
|
513
|
+
# dynamic. We only treat SymbolNode#value and StringNode#unescaped as static when they are non-nil
|
|
514
|
+
# (interpolation produces a nil unescaped).
|
|
541
515
|
def static_hash_key(node)
|
|
542
516
|
case node
|
|
543
517
|
when Prism::SymbolNode
|
|
@@ -560,13 +534,10 @@ module Rigor
|
|
|
560
534
|
[keys, values]
|
|
561
535
|
end
|
|
562
536
|
|
|
563
|
-
# An interpolated string `"#{a}b#{c}"` is `literal-string`
|
|
564
|
-
#
|
|
565
|
-
#
|
|
566
|
-
#
|
|
567
|
-
# compatible (a `Constant<String>`, the `literal-string`
|
|
568
|
-
# carrier, an `Intersection` containing it, or a `Union`
|
|
569
|
-
# whose members all qualify). Otherwise the result widens to
|
|
537
|
+
# An interpolated string `"#{a}b#{c}"` is `literal-string` when every part contributes literal-bearing
|
|
538
|
+
# material: plain text segments are literal by construction, embedded expressions count when their type
|
|
539
|
+
# is itself literal-string-compatible (a `Constant<String>`, the `literal-string` carrier, an
|
|
540
|
+
# `Intersection` containing it, or a `Union` whose members all qualify). Otherwise the result widens to
|
|
570
541
|
# plain `Nominal[String]` as before.
|
|
571
542
|
def type_of_interpolated_string(node)
|
|
572
543
|
return Type::Combinator.literal_string if interpolation_parts_literal?(node.parts)
|
|
@@ -601,11 +572,9 @@ module Rigor
|
|
|
601
572
|
Type::Combinator.constant_of(node.name)
|
|
602
573
|
end
|
|
603
574
|
|
|
604
|
-
# `class Foo; body; end`, `module Foo; body; end`, and `class << x;
|
|
605
|
-
# body
|
|
606
|
-
#
|
|
607
|
-
# scope yet, so the body is typed in the surrounding scope and
|
|
608
|
-
# that result is returned.
|
|
575
|
+
# `class Foo; body; end`, `module Foo; body; end`, and `class << x; body; end` evaluate to the value of
|
|
576
|
+
# the body's last expression, or `nil` when the body is empty. We do not track class/module scope yet,
|
|
577
|
+
# so the body is typed in the surrounding scope and that result is returned.
|
|
609
578
|
def type_of_class_or_module(node)
|
|
610
579
|
body = node.body
|
|
611
580
|
return Type::Combinator.constant_of(nil) if body.nil?
|
|
@@ -613,35 +582,29 @@ module Rigor
|
|
|
613
582
|
type_of(body)
|
|
614
583
|
end
|
|
615
584
|
|
|
616
|
-
# `alias x y`, `alias $x $y`, and `undef foo` all evaluate to nil at
|
|
617
|
-
#
|
|
585
|
+
# `alias x y`, `alias $x $y`, and `undef foo` all evaluate to nil at runtime; the constant carrier
|
|
586
|
+
# captures that exactly.
|
|
618
587
|
def type_of_nil_value(_node)
|
|
619
588
|
Type::Combinator.constant_of(nil)
|
|
620
589
|
end
|
|
621
590
|
|
|
622
|
-
# `if c; t; (elsif c2; ...; )* else; e; end`. Prism nests `elsif`
|
|
623
|
-
# branches
|
|
624
|
-
#
|
|
625
|
-
#
|
|
626
|
-
# Without an else clause the branch's implicit value is nil, which
|
|
627
|
-
# is included in the union.
|
|
591
|
+
# `if c; t; (elsif c2; ...; )* else; e; end`. Prism nests `elsif` branches as `IfNode#subsequent`. Slice
|
|
592
|
+
# 3 phase 1 types both branches in the receiver scope and returns their union; scope rebinding is the
|
|
593
|
+
# StatementEvaluator's job (Slice 3 phase 2). Without an else clause the branch's implicit value is nil,
|
|
594
|
+
# which is included in the union.
|
|
628
595
|
#
|
|
629
|
-
# v0.0.6 — when the predicate folds to a `Type::Constant` whose
|
|
630
|
-
#
|
|
631
|
-
# branch
|
|
632
|
-
#
|
|
633
|
-
# `StatementEvaluator#eval_if`; this handler covers the
|
|
634
|
-
# expression-position ternary form (`a ? b : c`) and any
|
|
635
|
-
# `if`/`unless` reached through `type_of`.
|
|
596
|
+
# v0.0.6 — when the predicate folds to a `Type::Constant` whose value is Ruby-truthy (resp.
|
|
597
|
+
# Ruby-falsey), the unreachable branch is elided so the if-expression's type is the live branch alone.
|
|
598
|
+
# Statement-level branch elision lives in `StatementEvaluator#eval_if`; this handler covers the
|
|
599
|
+
# expression-position ternary form (`a ? b : c`) and any `if`/`unless` reached through `type_of`.
|
|
636
600
|
def type_of_if(node)
|
|
637
601
|
then_type = statements_or_nil(node.statements)
|
|
638
602
|
else_type = if_else_type(node.subsequent)
|
|
639
603
|
elide_or_union(node.predicate, then_type, else_type)
|
|
640
604
|
end
|
|
641
605
|
|
|
642
|
-
# `unless c; t; else; e; end`. Prism uses `else_clause` here (no
|
|
643
|
-
# `
|
|
644
|
-
# inverted: a truthy predicate selects the else branch.
|
|
606
|
+
# `unless c; t; else; e; end`. Prism uses `else_clause` here (no `elsif` chain). Branch-elision logic
|
|
607
|
+
# mirrors `type_of_if`, inverted: a truthy predicate selects the else branch.
|
|
645
608
|
def type_of_unless(node)
|
|
646
609
|
then_type = statements_or_nil(node.statements)
|
|
647
610
|
else_type = if_else_type(node.else_clause)
|
|
@@ -654,11 +617,9 @@ module Rigor
|
|
|
654
617
|
type_of(subsequent)
|
|
655
618
|
end
|
|
656
619
|
|
|
657
|
-
# Routes the predicate's typed value through branch elision.
|
|
658
|
-
#
|
|
659
|
-
#
|
|
660
|
-
# match `IfNode` semantics directly and invert at the
|
|
661
|
-
# `type_of_unless` call site.
|
|
620
|
+
# Routes the predicate's typed value through branch elision. `live_when_truthy` and `live_when_falsey`
|
|
621
|
+
# are the branch types selected by the predicate's polarity; the names match `IfNode` semantics
|
|
622
|
+
# directly and invert at the `type_of_unless` call site.
|
|
662
623
|
def elide_or_union(predicate, live_when_truthy, live_when_falsey)
|
|
663
624
|
case constant_predicate_polarity(predicate)
|
|
664
625
|
when :truthy then live_when_truthy
|
|
@@ -667,14 +628,10 @@ module Rigor
|
|
|
667
628
|
end
|
|
668
629
|
end
|
|
669
630
|
|
|
670
|
-
# Returns `:truthy`, `:falsey`, or `nil` for an arbitrary
|
|
671
|
-
#
|
|
672
|
-
#
|
|
673
|
-
# one `
|
|
674
|
-
# scope side): `Nominal[Integer]` (always truthy in Ruby),
|
|
675
|
-
# `Constant[nil]`, and `Constant[false]` fold one branch;
|
|
676
|
-
# `Union[true, false]`, `Dynamic[T]`, and `Top` keep both
|
|
677
|
-
# branches live.
|
|
631
|
+
# Returns `:truthy`, `:falsey`, or `nil` for an arbitrary predicate expression under three-valued logic.
|
|
632
|
+
# {Narrowing.predicate_certainty} owns the judgment (the same one `StatementEvaluator#live_branch_for_if`
|
|
633
|
+
# reads on the scope side): `Nominal[Integer]` (always truthy in Ruby), `Constant[nil]`, and
|
|
634
|
+
# `Constant[false]` fold one branch; `Union[true, false]`, `Dynamic[T]`, and `Top` keep both branches live.
|
|
678
635
|
def constant_predicate_polarity(predicate)
|
|
679
636
|
return nil if predicate.nil?
|
|
680
637
|
|
|
@@ -685,27 +642,22 @@ module Rigor
|
|
|
685
642
|
statements_or_nil(node.statements)
|
|
686
643
|
end
|
|
687
644
|
|
|
688
|
-
# `a && b` and `a || b` short-circuit at the value level:
|
|
689
|
-
# `a
|
|
690
|
-
# `a || b` returns `a` when `a` is truthy, else `b`.
|
|
645
|
+
# `a && b` and `a || b` short-circuit at the value level: `a && b` returns `a` when `a` is falsey, else
|
|
646
|
+
# `b`. `a || b` returns `a` when `a` is truthy, else `b`.
|
|
691
647
|
#
|
|
692
|
-
# v0.0.6 — when the left operand folds to a `Type::Constant`,
|
|
693
|
-
#
|
|
694
|
-
# is
|
|
695
|
-
# union-of-both-operands fallback is preserved.
|
|
648
|
+
# v0.0.6 — when the left operand folds to a `Type::Constant`, we know which side actually flows
|
|
649
|
+
# through, so the result is one operand's type instead of a union. Otherwise the union-of-both-operands
|
|
650
|
+
# fallback is preserved.
|
|
696
651
|
def type_of_and_or(node)
|
|
697
652
|
left_type = type_of(node.left)
|
|
698
653
|
polarity = constant_value_polarity(left_type)
|
|
699
654
|
return short_circuit_for(node, left_type, polarity) if polarity
|
|
700
655
|
|
|
701
|
-
# The left operand only flows through on the edge that short-
|
|
702
|
-
#
|
|
703
|
-
#
|
|
704
|
-
#
|
|
705
|
-
#
|
|
706
|
-
# union so `s || full` (with `s : String?`) types `String |
|
|
707
|
-
# <full>` rather than re-admitting the stripped `nil`. Mirrors
|
|
708
|
-
# `StatementEvaluator#eval_and_or`'s `skipped_type`.
|
|
656
|
+
# The left operand only flows through on the edge that short-circuits: `a || b` yields `a` solely
|
|
657
|
+
# when `a` is truthy, so its falsey constituents (`nil` / `false`) can never be the value of the
|
|
658
|
+
# OrNode (they hand off to `b`); `a && b` yields `a` solely when `a` is falsey. Narrow the surviving
|
|
659
|
+
# left edge before the union so `s || full` (with `s : String?`) types `String | <full>` rather than
|
|
660
|
+
# re-admitting the stripped `nil`. Mirrors `StatementEvaluator#eval_and_or`'s `skipped_type`.
|
|
709
661
|
surviving_left =
|
|
710
662
|
if node.is_a?(Prism::AndNode)
|
|
711
663
|
Narrowing.narrow_falsey(left_type)
|
|
@@ -724,37 +676,30 @@ module Rigor
|
|
|
724
676
|
end
|
|
725
677
|
end
|
|
726
678
|
|
|
727
|
-
# Returns `:truthy` / `:falsey` for a `Type::Constant`,
|
|
728
|
-
#
|
|
729
|
-
#
|
|
730
|
-
# than a Prism node, so the same predicate analysis can
|
|
731
|
-
# be reused in both contexts.
|
|
679
|
+
# Returns `:truthy` / `:falsey` for a `Type::Constant`, nil otherwise. Mirrors
|
|
680
|
+
# `constant_predicate_polarity` but operates on a typed value (already-type-of'd) rather than a Prism
|
|
681
|
+
# node, so the same predicate analysis can be reused in both contexts.
|
|
732
682
|
def constant_value_polarity(type)
|
|
733
683
|
return nil unless type.is_a?(Type::Constant)
|
|
734
684
|
|
|
735
685
|
type.value ? :truthy : :falsey
|
|
736
686
|
end
|
|
737
687
|
|
|
738
|
-
# Three-valued evaluation of `case predicate when pattern`
|
|
739
|
-
#
|
|
740
|
-
#
|
|
741
|
-
# definitely not match (`:no`), or possibly match (`:maybe`)?
|
|
742
|
-
# Walking in source order:
|
|
688
|
+
# Three-valued evaluation of `case predicate when pattern` dispatch. For each `when` clause we ask:
|
|
689
|
+
# under static types, does `pattern === predicate` definitely match (`:yes`), definitely not match
|
|
690
|
+
# (`:no`), or possibly match (`:maybe`)? Walking in source order:
|
|
743
691
|
#
|
|
744
|
-
# - `:yes` — this branch fires, subsequent branches are
|
|
745
|
-
#
|
|
746
|
-
# `:yes` branch).
|
|
692
|
+
# - `:yes` — this branch fires, subsequent branches are unreachable. Result = union(prior `:maybe`
|
|
693
|
+
# branches, this `:yes` branch).
|
|
747
694
|
# - `:no` — branch dropped.
|
|
748
695
|
# - `:maybe` — branch is a candidate, continue.
|
|
749
696
|
#
|
|
750
|
-
# If no `:yes` was reached, the else clause (or `Constant[nil]`
|
|
751
|
-
#
|
|
697
|
+
# If no `:yes` was reached, the else clause (or `Constant[nil]` when absent) is added to the candidate
|
|
698
|
+
# set.
|
|
752
699
|
#
|
|
753
|
-
# The `case ... in` pattern-matching form (`CaseMatchNode`) and
|
|
754
|
-
# the
|
|
755
|
-
#
|
|
756
|
-
# predicate-less `case` reduces to a `if c1; ...; elsif c2`
|
|
757
|
-
# chain that statement-level narrowing already handles.
|
|
700
|
+
# The `case ... in` pattern-matching form (`CaseMatchNode`) and the predicate-less form (`case; when
|
|
701
|
+
# c1; ...`) bypass the `===` analysis: pattern matching has richer semantics, and a predicate-less
|
|
702
|
+
# `case` reduces to a `if c1; ...; elsif c2` chain that statement-level narrowing already handles.
|
|
758
703
|
def type_of_case(node)
|
|
759
704
|
return type_of_case_simple_union(node) if node.is_a?(Prism::CaseMatchNode) || node.predicate.nil?
|
|
760
705
|
|
|
@@ -789,10 +734,8 @@ module Rigor
|
|
|
789
734
|
type_of(node.else_clause)
|
|
790
735
|
end
|
|
791
736
|
|
|
792
|
-
# Combines per-pattern certainty across a `when` clause's
|
|
793
|
-
#
|
|
794
|
-
# `:yes` if any pattern is `:yes`; `:no` if all are `:no`;
|
|
795
|
-
# `:maybe` otherwise.
|
|
737
|
+
# Combines per-pattern certainty across a `when` clause's conditions (`when a, b, c` ≡ `a === s || b
|
|
738
|
+
# === s || c === s`). `:yes` if any pattern is `:yes`; `:no` if all are `:no`; `:maybe` otherwise.
|
|
796
739
|
def case_when_branch_certainty(subject_type, when_node)
|
|
797
740
|
return :maybe unless when_node.respond_to?(:conditions)
|
|
798
741
|
|
|
@@ -804,23 +747,17 @@ module Rigor
|
|
|
804
747
|
:maybe
|
|
805
748
|
end
|
|
806
749
|
|
|
807
|
-
# Static three-valued certainty for `pattern === subject`.
|
|
808
|
-
# Specialises two pattern shapes:
|
|
750
|
+
# Static three-valued certainty for `pattern === subject`. Specialises two pattern shapes:
|
|
809
751
|
#
|
|
810
|
-
# - **Class / Module reference** (`Integer`, `Foo::Bar`):
|
|
811
|
-
#
|
|
812
|
-
# `
|
|
813
|
-
#
|
|
814
|
-
#
|
|
815
|
-
#
|
|
816
|
-
# - **Value-equality literal** (numeric / String / Symbol /
|
|
817
|
-
# true / false / nil) against a `Constant[c]` subject:
|
|
818
|
-
# the static comparison `pattern_value === c` is exact.
|
|
819
|
-
# Other subject carriers stay `:maybe` because the
|
|
820
|
-
# runtime value isn't pinned.
|
|
752
|
+
# - **Class / Module reference** (`Integer`, `Foo::Bar`): reduce to `subject.is_a?(class)` via
|
|
753
|
+
# `Narrowing.narrow_class` / `narrow_not_class`. A Bot truthy fragment means no inhabitant matches
|
|
754
|
+
# (`:no`); a Bot falsey fragment means every inhabitant matches (`:yes`).
|
|
755
|
+
# - **Value-equality literal** (numeric / String / Symbol / true / false / nil) against a
|
|
756
|
+
# `Constant[c]` subject: the static comparison `pattern_value === c` is exact. Other subject
|
|
757
|
+
# carriers stay `:maybe` because the runtime value isn't pinned.
|
|
821
758
|
#
|
|
822
|
-
# Other pattern shapes (Range, Regexp, custom `===`) stay
|
|
823
|
-
#
|
|
759
|
+
# Other pattern shapes (Range, Regexp, custom `===`) stay `:maybe` — the existing union fallback
|
|
760
|
+
# handles them.
|
|
824
761
|
def case_when_pattern_certainty(subject_type, pattern_node)
|
|
825
762
|
class_name = Source::ConstantPath.qualified_name_or_nil(pattern_node)
|
|
826
763
|
return Narrowing.class_pattern_certainty(subject_type, class_name, environment: scope.environment) if class_name
|
|
@@ -831,11 +768,9 @@ module Rigor
|
|
|
831
768
|
:maybe
|
|
832
769
|
end
|
|
833
770
|
|
|
834
|
-
# Returns `{ value: v }` when `pattern_node` types to a
|
|
835
|
-
# `
|
|
836
|
-
#
|
|
837
|
-
# `nil` / `false` value doesn't collide with the "no literal"
|
|
838
|
-
# signal.
|
|
771
|
+
# Returns `{ value: v }` when `pattern_node` types to a `Constant[v]` of a value-equality-safe class
|
|
772
|
+
# (so `===` reduces to `==`), else nil. Wrapped in a hash so a literal `nil` / `false` value doesn't
|
|
773
|
+
# collide with the "no literal" signal.
|
|
839
774
|
def literal_pattern_value(pattern_node)
|
|
840
775
|
type = type_of(pattern_node)
|
|
841
776
|
return nil unless type.is_a?(Type::Constant)
|
|
@@ -844,19 +779,16 @@ module Rigor
|
|
|
844
779
|
{ value: type.value }
|
|
845
780
|
end
|
|
846
781
|
|
|
847
|
-
# `when` clauses for `case` and `in` clauses for `case ... in` have
|
|
848
|
-
#
|
|
849
|
-
# classes.
|
|
782
|
+
# `when` clauses for `case` and `in` clauses for `case ... in` have the same body shape; we reuse one
|
|
783
|
+
# handler for both Prism node classes.
|
|
850
784
|
def type_of_when_or_in(node)
|
|
851
785
|
statements_or_nil(node.statements)
|
|
852
786
|
end
|
|
853
787
|
|
|
854
|
-
# `begin; body; rescue R => e; r1; rescue; r2; else; e; ensure; f; end`.
|
|
855
|
-
#
|
|
856
|
-
#
|
|
857
|
-
#
|
|
858
|
-
# rescue chain. The ensure clause runs but does not contribute to
|
|
859
|
-
# the begin's value.
|
|
788
|
+
# `begin; body; rescue R => e; r1; rescue; r2; else; e; ensure; f; end`. The result is the union of
|
|
789
|
+
# every value-producing branch: the body (or the else-clause when present, since it replaces the body's
|
|
790
|
+
# value when no exception fires), plus each rescue body in the rescue chain. The ensure clause runs but
|
|
791
|
+
# does not contribute to the begin's value.
|
|
860
792
|
def type_of_begin(node)
|
|
861
793
|
rescue_clause = node.rescue_clause
|
|
862
794
|
else_clause = node.else_clause
|
|
@@ -888,10 +820,8 @@ module Rigor
|
|
|
888
820
|
statements_or_nil(node.statements)
|
|
889
821
|
end
|
|
890
822
|
|
|
891
|
-
# `expr rescue fallback` is RescueModifierNode in Prism. The result
|
|
892
|
-
# is `
|
|
893
|
-
# type otherwise; both paths are reachable, so the result is their
|
|
894
|
-
# union.
|
|
823
|
+
# `expr rescue fallback` is RescueModifierNode in Prism. The result is `expr`'s type when no exception
|
|
824
|
+
# is raised and `fallback`'s type otherwise; both paths are reachable, so the result is their union.
|
|
895
825
|
def type_of_rescue_modifier(node)
|
|
896
826
|
Type::Combinator.union(type_of(node.expression), type_of(node.rescue_expression))
|
|
897
827
|
end
|
|
@@ -900,20 +830,16 @@ module Rigor
|
|
|
900
830
|
statements_or_nil(node.statements)
|
|
901
831
|
end
|
|
902
832
|
|
|
903
|
-
# `return`, `break`, `next`, `retry`, and `redo` all transfer
|
|
904
|
-
#
|
|
905
|
-
#
|
|
906
|
-
# `Constant[1] | Bot == Constant[1]`), so the surrounding
|
|
907
|
-
# control-flow handlers collapse correctly when one branch jumps.
|
|
833
|
+
# `return`, `break`, `next`, `retry`, and `redo` all transfer control instead of producing a value.
|
|
834
|
+
# Their type is Bot, the empty type that absorbs cleanly under union (e.g. `Constant[1] | Bot ==
|
|
835
|
+
# Constant[1]`), so the surrounding control-flow handlers collapse correctly when one branch jumps.
|
|
908
836
|
def type_of_jump(_node)
|
|
909
837
|
Type::Combinator.bot
|
|
910
838
|
end
|
|
911
839
|
|
|
912
|
-
# `while` and `until` loops produce nil unless interrupted by
|
|
913
|
-
# `break VALUE
|
|
914
|
-
#
|
|
915
|
-
# Returning Constant[nil] is safe and matches Ruby semantics for
|
|
916
|
-
# the common case.
|
|
840
|
+
# `while` and `until` loops produce nil unless interrupted by `break VALUE`; the expression value of
|
|
841
|
+
# `break VALUE` is not yet modeled (scope break-path propagation landed in `eval_loop`). Returning
|
|
842
|
+
# Constant[nil] is safe and matches Ruby semantics for the common case.
|
|
917
843
|
def type_of_loop(_node)
|
|
918
844
|
Type::Combinator.constant_of(nil)
|
|
919
845
|
end
|
|
@@ -930,12 +856,10 @@ module Rigor
|
|
|
930
856
|
nominal_range_for_endpoints(node.left, node.right)
|
|
931
857
|
end
|
|
932
858
|
|
|
933
|
-
# Derives `Nominal[Range, [T]]` from the endpoint expression
|
|
934
|
-
#
|
|
935
|
-
#
|
|
936
|
-
#
|
|
937
|
-
# what `Range#each` would yield). Falls back to bare
|
|
938
|
-
# `Nominal[Range]` when no endpoint contributes a typable shape.
|
|
859
|
+
# Derives `Nominal[Range, [T]]` from the endpoint expression types when at least one endpoint is
|
|
860
|
+
# statically typeable. The element parameter is the union of the endpoint types (lifted from
|
|
861
|
+
# `Constant<v>` to `Nominal<v.class>` so the carrier matches what `Range#each` would yield). Falls back
|
|
862
|
+
# to bare `Nominal[Range]` when no endpoint contributes a typable shape.
|
|
939
863
|
def nominal_range_for_endpoints(left_node, right_node)
|
|
940
864
|
endpoints = [left_node, right_node].compact.map { |n| range_endpoint_element_type(n) }
|
|
941
865
|
endpoints.reject! { |t| t.equal?(Type::Combinator.untyped) }
|
|
@@ -959,12 +883,10 @@ module Rigor
|
|
|
959
883
|
end
|
|
960
884
|
end
|
|
961
885
|
|
|
962
|
-
# v0.0.7 — non-interpolated regex literals lift to
|
|
963
|
-
#
|
|
964
|
-
#
|
|
965
|
-
#
|
|
966
|
-
# second `Prism::InterpolatedRegularExpressionNode` arm
|
|
967
|
-
# which keeps the conservative `Nominal[Regexp]` answer.
|
|
886
|
+
# v0.0.7 — non-interpolated regex literals lift to `Constant<Regexp>` so `Constant<String>#scan(/regex/)`
|
|
887
|
+
# / `#match(/regex/)` etc. can fold through the catalog tier. Interpolated regexes (`/foo#{x}/`) reach
|
|
888
|
+
# the second `Prism::InterpolatedRegularExpressionNode` arm which keeps the conservative
|
|
889
|
+
# `Nominal[Regexp]` answer.
|
|
968
890
|
def type_of_regexp(node)
|
|
969
891
|
return Type::Combinator.nominal_of(Regexp) unless node.is_a?(Prism::RegularExpressionNode)
|
|
970
892
|
|
|
@@ -974,15 +896,12 @@ module Rigor
|
|
|
974
896
|
Type::Combinator.nominal_of(Regexp)
|
|
975
897
|
end
|
|
976
898
|
|
|
977
|
-
# A range endpoint folds to a static value when it is a literal
|
|
978
|
-
#
|
|
979
|
-
#
|
|
980
|
-
#
|
|
981
|
-
#
|
|
982
|
-
#
|
|
983
|
-
# node is a beginless/endless boundary: keep today's static-nil
|
|
984
|
-
# behaviour (which yields `Constant<Range>` only when the *other*
|
|
985
|
-
# end is also static, preserving today's beginless/endless path).
|
|
899
|
+
# A range endpoint folds to a static value when it is a literal (`IntegerNode` / `StringNode`) or when
|
|
900
|
+
# its *evaluated* type is a `Constant<v>` carrying a range-able value (Integer / Float / String —
|
|
901
|
+
# matching the literal-path value kinds). The evaluated arm lets `(1..n)` fold to `Constant<Range>`
|
|
902
|
+
# when per-call body inference has pinned `n` to a constant (fact2 chain). A `nil` node is a
|
|
903
|
+
# beginless/endless boundary: keep today's static-nil behaviour (which yields `Constant<Range>` only
|
|
904
|
+
# when the *other* end is also static, preserving today's beginless/endless path).
|
|
986
905
|
def static_range_endpoint(node)
|
|
987
906
|
return [true, nil] if node.nil?
|
|
988
907
|
return [true, node.value] if node.is_a?(Prism::IntegerNode)
|
|
@@ -997,10 +916,9 @@ module Rigor
|
|
|
997
916
|
[false, nil]
|
|
998
917
|
end
|
|
999
918
|
|
|
1000
|
-
# Helper for the many control-flow handlers that read a body
|
|
1001
|
-
#
|
|
1002
|
-
#
|
|
1003
|
-
# missing bodies in many node kinds.
|
|
919
|
+
# Helper for the many control-flow handlers that read a body `Prism::StatementsNode` or treat its
|
|
920
|
+
# absence as nil. Note that Prism uses nil (rather than an empty `StatementsNode`) for missing bodies
|
|
921
|
+
# in many node kinds.
|
|
1004
922
|
def statements_or_nil(statements_node)
|
|
1005
923
|
return Type::Combinator.constant_of(nil) if statements_node.nil?
|
|
1006
924
|
|
|
@@ -1050,41 +968,34 @@ module Rigor
|
|
|
1050
968
|
Type::Combinator.constant_of(unescaped)
|
|
1051
969
|
end
|
|
1052
970
|
|
|
1053
|
-
# Backtick (`cmd`) and `%x{cmd}` invoke Kernel#` and always return a
|
|
1054
|
-
# String
|
|
1055
|
-
#
|
|
1056
|
-
# subprocess output, not the source text.
|
|
971
|
+
# Backtick (`cmd`) and `%x{cmd}` invoke Kernel#` and always return a String. Even when the content is
|
|
972
|
+
# statically known, we widen to Nominal[String] because the runtime value depends on the subprocess
|
|
973
|
+
# output, not the source text.
|
|
1057
974
|
def type_of_xstring(_node)
|
|
1058
975
|
Type::Combinator.nominal_of(String)
|
|
1059
976
|
end
|
|
1060
977
|
|
|
1061
|
-
# __FILE__ is the source file path. Always non-empty when
|
|
1062
|
-
#
|
|
1063
|
-
#
|
|
1064
|
-
#
|
|
1065
|
-
# `non-empty-string` instead of `Nominal[String]` so
|
|
1066
|
-
# downstream String-emptiness checks know the value cannot
|
|
1067
|
-
# be `""`.
|
|
978
|
+
# __FILE__ is the source file path. Always non-empty when parsing a real file (the path resolver gives
|
|
979
|
+
# the buffer name, which is at minimum `"(stdin)"` / `"-e"` / a real path — never the empty String).
|
|
980
|
+
# Widened to `non-empty-string` instead of `Nominal[String]` so downstream String-emptiness checks know
|
|
981
|
+
# the value cannot be `""`.
|
|
1068
982
|
def type_of_source_file(_node)
|
|
1069
983
|
Type::Combinator.non_empty_string
|
|
1070
984
|
end
|
|
1071
985
|
|
|
1072
|
-
# __LINE__ is the line of the source literal. Ruby line
|
|
1073
|
-
#
|
|
1074
|
-
# 1 — `positive-int` (Integer in `[1, +Inf)`) is the
|
|
1075
|
-
# canonical refinement.
|
|
986
|
+
# __LINE__ is the line of the source literal. Ruby line numbers are 1-indexed, so `__LINE__` is always
|
|
987
|
+
# at least 1 — `positive-int` (Integer in `[1, +Inf)`) is the canonical refinement.
|
|
1076
988
|
def type_of_source_line(_node)
|
|
1077
989
|
Type::Combinator.positive_int
|
|
1078
990
|
end
|
|
1079
991
|
|
|
1080
|
-
# `# shareable_constant_value:` magic comment wraps the next
|
|
1081
|
-
#
|
|
992
|
+
# `# shareable_constant_value:` magic comment wraps the next constant write. Type is the wrapped
|
|
993
|
+
# write's value.
|
|
1082
994
|
def type_of_shareable_constant(node)
|
|
1083
995
|
type_of(node.write)
|
|
1084
996
|
end
|
|
1085
997
|
|
|
1086
|
-
# `{ x: }` shorthand hash. The implicit value is the call to
|
|
1087
|
-
# `x` (or a local read of `x`). Delegate.
|
|
998
|
+
# `{ x: }` shorthand hash. The implicit value is the call to `x` (or a local read of `x`). Delegate.
|
|
1088
999
|
def type_of_implicit(node)
|
|
1089
1000
|
type_of(node.value)
|
|
1090
1001
|
end
|
|
@@ -1093,25 +1004,20 @@ module Rigor
|
|
|
1093
1004
|
scope.local(node.name) || dynamic_top
|
|
1094
1005
|
end
|
|
1095
1006
|
|
|
1096
|
-
# `it` (Ruby 3.4) — `ItLocalVariableReadNode` carries no `name`
|
|
1097
|
-
#
|
|
1098
|
-
# `BlockParameterBinder` installs for `Prism::ItParametersNode`.
|
|
1007
|
+
# `it` (Ruby 3.4) — `ItLocalVariableReadNode` carries no `name` field; the implicit name is always
|
|
1008
|
+
# `:it`, matching the binding `BlockParameterBinder` installs for `Prism::ItParametersNode`.
|
|
1099
1009
|
def it_read(_node)
|
|
1100
1010
|
scope.local(:it) || dynamic_top
|
|
1101
1011
|
end
|
|
1102
1012
|
|
|
1103
|
-
# Slice 5 phase 1 upgrades array literals to `Tuple[T1..Tn]`
|
|
1104
|
-
#
|
|
1105
|
-
#
|
|
1106
|
-
# the contributed element types and emit
|
|
1107
|
-
# `Nominal[Array, [union]]`.
|
|
1013
|
+
# Slice 5 phase 1 upgrades array literals to `Tuple[T1..Tn]` when every element is a non-splat value.
|
|
1014
|
+
# Splatted entries (`[*xs, 1]`) preserve the Slice 4 phase 2d behavior: we union the contributed
|
|
1015
|
+
# element types and emit `Nominal[Array, [union]]`.
|
|
1108
1016
|
#
|
|
1109
|
-
# v0.0.6 — the empty literal `[]` resolves to the empty
|
|
1110
|
-
# `
|
|
1111
|
-
#
|
|
1112
|
-
#
|
|
1113
|
-
# per-element block fold concatenate across all-empty
|
|
1114
|
-
# positions like `[1, 2].flat_map { |_| [] }`.
|
|
1017
|
+
# v0.0.6 — the empty literal `[]` resolves to the empty `Tuple[]` carrier rather than the raw
|
|
1018
|
+
# `Nominal[Array]`. Both carriers erase to RBS `Array`, but `Tuple[]` pins the literal's known arity
|
|
1019
|
+
# (zero), which lets the per-element block fold concatenate across all-empty positions like `[1,
|
|
1020
|
+
# 2].flat_map { |_| [] }`.
|
|
1115
1021
|
def array_type_for(node)
|
|
1116
1022
|
elements = node.elements
|
|
1117
1023
|
return Type::Combinator.tuple_of if elements.empty?
|
|
@@ -1141,26 +1047,20 @@ module Rigor
|
|
|
1141
1047
|
type_of(body.last)
|
|
1142
1048
|
end
|
|
1143
1049
|
|
|
1144
|
-
# Indexed-collection narrowing — `receiver[key]` after a
|
|
1145
|
-
#
|
|
1146
|
-
#
|
|
1147
|
-
#
|
|
1148
|
-
# the standard `Hash#[]` / `Array#[]` answer (which would
|
|
1149
|
-
# fold to `Constant[nil]` for an empty `HashShape{}` or
|
|
1150
|
-
# `Tuple[]`) does not override the narrowing. See
|
|
1050
|
+
# Indexed-collection narrowing — `receiver[key]` after a prior `receiver[key] ||= default` reads the
|
|
1051
|
+
# post-`||=` type when the receiver and key are stable enough to address. Sits ahead of
|
|
1052
|
+
# `MethodDispatcher.dispatch` so the standard `Hash#[]` / `Array#[]` answer (which would fold to
|
|
1053
|
+
# `Constant[nil]` for an empty `HashShape{}` or `Tuple[]`) does not override the narrowing. See
|
|
1151
1054
|
# {Inference::IndexedNarrowing}.
|
|
1152
1055
|
def indexed_narrowing_for(node)
|
|
1153
1056
|
IndexedNarrowing.lookup_for_call(node, scope) || method_chain_narrowing_for(node)
|
|
1154
1057
|
end
|
|
1155
1058
|
|
|
1156
|
-
# Stable single-hop chain narrowing — `receiver.method`
|
|
1157
|
-
#
|
|
1158
|
-
#
|
|
1159
|
-
#
|
|
1160
|
-
#
|
|
1161
|
-
# standard dispatcher. ROADMAP § Future cycles —
|
|
1162
|
-
# "Method-call receiver narrowing across stable
|
|
1163
|
-
# receivers" — Law-of-Demeter-justified single-hop scope.
|
|
1059
|
+
# Stable single-hop chain narrowing — `receiver.method` after an `is_a?` / `kind_of?` / `instance_of?`
|
|
1060
|
+
# predicate established the narrowing on the dominated edge. The call MUST be no-arg + no-block +
|
|
1061
|
+
# rooted at a local-var / ivar read; everything else falls through to the standard dispatcher. ROADMAP
|
|
1062
|
+
# § Future cycles — "Method-call receiver narrowing across stable receivers" — Law-of-Demeter-justified
|
|
1063
|
+
# single-hop scope.
|
|
1164
1064
|
def method_chain_narrowing_for(node)
|
|
1165
1065
|
return nil unless node.is_a?(Prism::CallNode)
|
|
1166
1066
|
return nil unless node.block.nil?
|
|
@@ -1174,14 +1074,10 @@ module Rigor
|
|
|
1174
1074
|
end
|
|
1175
1075
|
end
|
|
1176
1076
|
|
|
1177
|
-
# v0.0.3 A — implicit-self calls prefer a same-named
|
|
1178
|
-
#
|
|
1179
|
-
#
|
|
1180
|
-
#
|
|
1181
|
-
# through `Enumerable#select` / `Object#select` and
|
|
1182
|
-
# the caller observes `Array[Elem]` instead of the
|
|
1183
|
-
# helper's actual return type. The check fires only
|
|
1184
|
-
# for `node.receiver.nil?` (true implicit self), so
|
|
1077
|
+
# v0.0.3 A — implicit-self calls prefer a same-named top-level `def` over RBS dispatch. Without this, a
|
|
1078
|
+
# helper like `def select(...)` defined inside an `RSpec.describe ... do ... end` block mis-routes
|
|
1079
|
+
# through `Enumerable#select` / `Object#select` and the caller observes `Array[Elem]` instead of the
|
|
1080
|
+
# helper's actual return type. The check fires only for `node.receiver.nil?` (true implicit self), so
|
|
1185
1081
|
# explicit-receiver dispatch is unaffected.
|
|
1186
1082
|
def try_local_def_dispatch(node, receiver, arg_types)
|
|
1187
1083
|
local_def = node.receiver.nil? ? scope.top_level_def_for(node.name) : nil
|
|
@@ -1190,24 +1086,19 @@ module Rigor
|
|
|
1190
1086
|
local_inference = infer_top_level_user_method(local_def, receiver, arg_types)
|
|
1191
1087
|
return local_inference if local_inference
|
|
1192
1088
|
|
|
1193
|
-
# The local def matches by name but the inference was
|
|
1194
|
-
#
|
|
1195
|
-
#
|
|
1196
|
-
#
|
|
1197
|
-
#
|
|
1198
|
-
#
|
|
1199
|
-
# dispatch would find), and `Dynamic[Top]` propagates
|
|
1200
|
-
# correctly through downstream call chains without
|
|
1201
|
-
# surfacing misleading false-positive diagnostics.
|
|
1089
|
+
# The local def matches by name but the inference was disqualified — the parameter shape is too
|
|
1090
|
+
# complex for the first-iteration binder (kwargs / optionals / rest), so the body could not be
|
|
1091
|
+
# re-typed. `Dynamic[Top]` is the safest answer: RBS dispatch would be wrong (the method is
|
|
1092
|
+
# user-defined and shadows whatever ancestor method the dispatch would find), and `Dynamic[Top]`
|
|
1093
|
+
# propagates correctly through downstream call chains without surfacing misleading false-positive
|
|
1094
|
+
# diagnostics.
|
|
1202
1095
|
dynamic_top
|
|
1203
1096
|
end
|
|
1204
1097
|
|
|
1205
|
-
# Slice 2 routes call expressions through `MethodDispatcher`. The
|
|
1206
|
-
#
|
|
1207
|
-
#
|
|
1208
|
-
#
|
|
1209
|
-
# their own fallbacks for unrecognised receivers/args, so the tracer
|
|
1210
|
-
# captures both the immediate dispatch miss and the deeper cause).
|
|
1098
|
+
# Slice 2 routes call expressions through `MethodDispatcher`. The receiver and every argument are typed
|
|
1099
|
+
# first, then the dispatcher is asked for a result type. A nil result triggers the fail-soft fallback
|
|
1100
|
+
# for the CallNode itself (the inner type_of calls already record their own fallbacks for unrecognised
|
|
1101
|
+
# receivers/args, so the tracer captures both the immediate dispatch miss and the deeper cause).
|
|
1211
1102
|
def call_type_for(node)
|
|
1212
1103
|
narrowed = indexed_narrowing_for(node)
|
|
1213
1104
|
return narrowed if narrowed
|
|
@@ -1219,15 +1110,11 @@ module Rigor
|
|
|
1219
1110
|
local_def_result = try_local_def_dispatch(node, receiver, arg_types)
|
|
1220
1111
|
return local_def_result if local_def_result
|
|
1221
1112
|
|
|
1222
|
-
# v0.0.6 phase 2 — per-element block fold for Tuple
|
|
1223
|
-
#
|
|
1224
|
-
#
|
|
1225
|
-
#
|
|
1226
|
-
#
|
|
1227
|
-
# and assemble the results into a `Tuple[U_1..U_n]`.
|
|
1228
|
-
# This sits ahead of `MethodDispatcher.dispatch` so
|
|
1229
|
-
# the RBS tier does not re-widen the answer back to
|
|
1230
|
-
# `Array[union]`.
|
|
1113
|
+
# v0.0.6 phase 2 — per-element block fold for Tuple receivers. When `[a, b, c].map { |x| f(x) }` and
|
|
1114
|
+
# the receiver is a `Tuple` carrier with finite elements, type the block body once per position with
|
|
1115
|
+
# the corresponding element bound to the block parameter and assemble the results into a
|
|
1116
|
+
# `Tuple[U_1..U_n]`. This sits ahead of `MethodDispatcher.dispatch` so the RBS tier does not re-widen
|
|
1117
|
+
# the answer back to `Array[union]`.
|
|
1231
1118
|
per_element = try_per_element_block_fold(node, receiver)
|
|
1232
1119
|
return per_element if per_element
|
|
1233
1120
|
|
|
@@ -1248,48 +1135,50 @@ module Rigor
|
|
|
1248
1135
|
)
|
|
1249
1136
|
return result if result
|
|
1250
1137
|
|
|
1251
|
-
# v0.0.2 #5 — inter-procedural inference for
|
|
1252
|
-
# user-
|
|
1253
|
-
#
|
|
1254
|
-
# the body with the call's argument types bound and
|
|
1255
|
-
# return the body's last-expression type.
|
|
1138
|
+
# v0.0.2 #5 — inter-procedural inference for user-defined methods. When dispatch misses but the
|
|
1139
|
+
# receiver is a user class with a `def` body, re-type the body with the call's argument types bound
|
|
1140
|
+
# and return the body's last-expression type.
|
|
1256
1141
|
user_inference = try_user_method_inference(receiver, node, arg_types)
|
|
1257
1142
|
return user_inference if user_inference
|
|
1258
1143
|
|
|
1259
|
-
# Module-singleton call resolution (ADR-57 follow-up) — when the
|
|
1260
|
-
#
|
|
1261
|
-
#
|
|
1262
|
-
#
|
|
1263
|
-
#
|
|
1264
|
-
# foreign / RBS-known singletons (`Math.sqrt`) keep their catalog
|
|
1265
|
-
# answer; only project-defined singleton methods reach here.
|
|
1144
|
+
# Module-singleton call resolution (ADR-57 follow-up) — when the receiver is `Singleton[Foo]` (a
|
|
1145
|
+
# module/class constant or a singleton-method `self`) and `Foo` declares a user-side `def self.x` /
|
|
1146
|
+
# `module_function` body, re-type that body with the call args bound. Sits after the RBS dispatch
|
|
1147
|
+
# tier, so foreign / RBS-known singletons (`Math.sqrt`) keep their catalog answer; only
|
|
1148
|
+
# project-defined singleton methods reach here.
|
|
1266
1149
|
singleton_inference = try_singleton_method_inference(receiver, node, arg_types)
|
|
1267
1150
|
return singleton_inference if singleton_inference
|
|
1268
1151
|
|
|
1269
|
-
# Dynamic-origin propagation: when the receiver is Dynamic[T] and
|
|
1270
|
-
#
|
|
1271
|
-
#
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
#
|
|
1277
|
-
#
|
|
1278
|
-
# dispatch + user-class ancestor walk) and falls through to
|
|
1279
|
-
# `Dynamic[top]`. When the slice-4 recorder is active, capture the
|
|
1280
|
-
# miss so a later slice's closed-class gate can flag it. Off by
|
|
1281
|
-
# default: `active?` is a plain integer read.
|
|
1152
|
+
# Dynamic-origin propagation: when the receiver is Dynamic[T] and no positive rule resolves the call,
|
|
1153
|
+
# the result inherits the dynamic origin. Per the value-lattice algebra, this is a recognised
|
|
1154
|
+
# semantic outcome, not a fail-soft compromise, so it MUST NOT record a tracer event.
|
|
1155
|
+
return inherit_receiver_origin(node) if receiver.is_a?(Type::Dynamic)
|
|
1156
|
+
|
|
1157
|
+
# ADR-24 slice 4a — this is the engine choke-point where an implicit-self call has exhausted every
|
|
1158
|
+
# resolution tier (RBS dispatch + user-class ancestor walk) and falls through to `Dynamic[top]`. When
|
|
1159
|
+
# the slice-4 recorder is active, capture the miss so a later slice's closed-class gate can flag it.
|
|
1160
|
+
# Off by default: `active?` is a plain integer read.
|
|
1282
1161
|
record_unresolved_self_call(node, receiver) if Analysis::SelfCallResolutionRecorder.active?
|
|
1283
1162
|
|
|
1284
1163
|
fallback_for(node, family: :prism)
|
|
1285
1164
|
end
|
|
1286
1165
|
|
|
1287
|
-
# ADR-
|
|
1288
|
-
#
|
|
1289
|
-
#
|
|
1290
|
-
#
|
|
1291
|
-
#
|
|
1292
|
-
#
|
|
1166
|
+
# ADR-82 WD6 — carry the receiver's provenance onto the call it produces (returning the unchanged
|
|
1167
|
+
# `dynamic_top` result), so a specific cause survives a method chain (`x.foo.bar`): without this,
|
|
1168
|
+
# `.foo` on a Dynamic `x` records nothing and `.bar`'s receiver looks causeless. Side-channel only
|
|
1169
|
+
# (the result type is untouched); a nil cause is skipped. This is the lever for the
|
|
1170
|
+
# intermediate-expression / chain receivers that dominate a real app's `coverage --protection`
|
|
1171
|
+
# catch-all.
|
|
1172
|
+
def inherit_receiver_origin(node)
|
|
1173
|
+
inherited = OriginLookup.origin_for(scope, node.receiver)
|
|
1174
|
+
scope.record_dynamic_origin(node, inherited) if inherited
|
|
1175
|
+
dynamic_top
|
|
1176
|
+
end
|
|
1177
|
+
|
|
1178
|
+
# ADR-24 slice 4a — records an unresolved *implicit-self* call (no explicit receiver) whose `self`
|
|
1179
|
+
# types to a concrete user class. Explicit-receiver misses are out of scope (the existing
|
|
1180
|
+
# `call.undefined-method` rule already owns receiver-typed dispatch); a non-`Nominal` self (top-level /
|
|
1181
|
+
# DSL-block `self`, or a `Dynamic` self) is skipped so the gradual guarantee is never touched here.
|
|
1293
1182
|
def record_unresolved_self_call(node, receiver)
|
|
1294
1183
|
return unless node.receiver.nil?
|
|
1295
1184
|
return unless receiver.is_a?(Type::Nominal)
|
|
@@ -1306,24 +1195,18 @@ module Rigor
|
|
|
1306
1195
|
)
|
|
1307
1196
|
end
|
|
1308
1197
|
|
|
1309
|
-
# The recorder must capture *existence* misses, not type misses.
|
|
1310
|
-
#
|
|
1311
|
-
# a
|
|
1312
|
-
#
|
|
1313
|
-
#
|
|
1314
|
-
#
|
|
1315
|
-
#
|
|
1316
|
-
# a
|
|
1317
|
-
#
|
|
1318
|
-
#
|
|
1319
|
-
#
|
|
1320
|
-
#
|
|
1321
|
-
# `module_function` records its defs as `:singleton` (an implicit-self
|
|
1322
|
-
# call inside such a method dispatches to the module's singleton
|
|
1323
|
-
# method), while ordinary instance methods record `:instance`. The
|
|
1324
|
-
# recorder cannot tell the two contexts apart from the call node, so
|
|
1325
|
-
# existence under EITHER kind suppresses recording — the FP-safe
|
|
1326
|
-
# choice, since either means the method genuinely exists.
|
|
1198
|
+
# The recorder must capture *existence* misses, not type misses. Reaching the choke-point means RBS
|
|
1199
|
+
# dispatch produced no result, but a project method can still EXIST without an inferable return type —
|
|
1200
|
+
# a `module_function` sibling whose body the engine can't fully type, an `attr_reader` /
|
|
1201
|
+
# `define_method` / `Data.define` member. Recording those would reproduce the 135 false positives of
|
|
1202
|
+
# slice-4 attempt 1. So skip any name the engine's own existence signals already know: a `def`
|
|
1203
|
+
# resolvable through the ancestor walk, or an own-class entry in the discovered-methods table (`def` /
|
|
1204
|
+
# `attr_*` / `define_method` / alias). This reuses the engine's real resolution — the "collect, don't
|
|
1205
|
+
# recompute" lesson — so only a name that exists nowhere a project signal can see reaches the recorder.
|
|
1206
|
+
# `module_function` records its defs as `:singleton` (an implicit-self call inside such a method
|
|
1207
|
+
# dispatches to the module's singleton method), while ordinary instance methods record `:instance`. The
|
|
1208
|
+
# recorder cannot tell the two contexts apart from the call node, so existence under EITHER kind
|
|
1209
|
+
# suppresses recording — the FP-safe choice, since either means the method genuinely exists.
|
|
1327
1210
|
def self_call_method_known?(class_name, method_name)
|
|
1328
1211
|
return true if resolve_user_def_through_ancestors(class_name, method_name)
|
|
1329
1212
|
|
|
@@ -1331,94 +1214,68 @@ module Rigor
|
|
|
1331
1214
|
scope.discovered_method?(class_name, method_name, :singleton)
|
|
1332
1215
|
end
|
|
1333
1216
|
|
|
1334
|
-
# v0.0.2 #5 — re-types the body of a user-defined
|
|
1335
|
-
#
|
|
1336
|
-
#
|
|
1337
|
-
# last-resort tier after `MethodDispatcher.dispatch`
|
|
1338
|
-
# has exhausted its catalogue (RBS, shape, constant
|
|
1339
|
-
# folding, user-class fallback). Returns nil when:
|
|
1217
|
+
# v0.0.2 #5 — re-types the body of a user-defined instance method with the call site's argument types
|
|
1218
|
+
# bound to the method's parameters. Used as a last-resort tier after `MethodDispatcher.dispatch` has
|
|
1219
|
+
# exhausted its catalogue (RBS, shape, constant folding, user-class fallback). Returns nil when:
|
|
1340
1220
|
#
|
|
1341
1221
|
# - the receiver is not `Nominal[T]` for some T;
|
|
1342
|
-
# - no def_node is recorded for that class/method
|
|
1343
|
-
#
|
|
1344
|
-
# - the
|
|
1345
|
-
#
|
|
1346
|
-
# -
|
|
1347
|
-
#
|
|
1348
|
-
#
|
|
1349
|
-
#
|
|
1350
|
-
#
|
|
1351
|
-
# `TOP_LEVEL_DEF_KEY` sentinel. Mirrors the
|
|
1352
|
-
# `infer_user_method_return` shape but uses the
|
|
1353
|
-
# current `scope.self_type` (or implicit `Object`)
|
|
1354
|
-
# as the receiver carrier so the body's own self is
|
|
1355
|
-
# consistent with the call site's. Returns nil when
|
|
1356
|
-
# the parameter shape disqualifies the def, when the
|
|
1357
|
-
# body is empty, or when a recursion cycle is
|
|
1358
|
-
# detected.
|
|
1222
|
+
# - no def_node is recorded for that class/method (the receiver is foreign or has only an RBS sig);
|
|
1223
|
+
# - the def has no body, or has a parameter shape we cannot bind from the call's positional args;
|
|
1224
|
+
# - the inference is already in progress for this (class, method, signature) tuple — recursion safety
|
|
1225
|
+
# net.
|
|
1226
|
+
# v0.0.3 A — re-types a top-level (or DSL-block-nested) `def` discovered by `ScopeIndexer` under the
|
|
1227
|
+
# `TOP_LEVEL_DEF_KEY` sentinel. Mirrors the `infer_user_method_return` shape but uses the current
|
|
1228
|
+
# `scope.self_type` (or implicit `Object`) as the receiver carrier so the body's own self is
|
|
1229
|
+
# consistent with the call site's. Returns nil when the parameter shape disqualifies the def, when the
|
|
1230
|
+
# body is empty, or when a recursion cycle is detected.
|
|
1359
1231
|
def infer_top_level_user_method(def_node, receiver, arg_types)
|
|
1360
1232
|
infer_user_method_return(def_node, receiver, arg_types)
|
|
1361
1233
|
rescue StandardError
|
|
1362
1234
|
nil
|
|
1363
1235
|
end
|
|
1364
1236
|
|
|
1365
|
-
# ADR-24 slice 1 — implicit-self method-call resolution.
|
|
1366
|
-
# `
|
|
1367
|
-
#
|
|
1368
|
-
#
|
|
1369
|
-
# method body resolves against the enclosing class's own
|
|
1370
|
-
# definitions and the file's top-level defs. Before
|
|
1371
|
-
# slice 1 every such call typed `Dynamic[top]`.
|
|
1237
|
+
# ADR-24 slice 1 — implicit-self method-call resolution. `discovered_def_nodes` is now carried into
|
|
1238
|
+
# method / class body scopes (see `StatementEvaluator#build_fresh_body_scope`), so a call written with
|
|
1239
|
+
# no explicit receiver inside a method body resolves against the enclosing class's own definitions and
|
|
1240
|
+
# the file's top-level defs. Before slice 1 every such call typed `Dynamic[top]`.
|
|
1372
1241
|
#
|
|
1373
|
-
# The resolved return type is adopted UNCONDITIONALLY — a resolved
|
|
1374
|
-
#
|
|
1375
|
-
# toplevel call has since v0.0.3.
|
|
1242
|
+
# The resolved return type is adopted UNCONDITIONALLY — a resolved user-method call site reads the
|
|
1243
|
+
# callee's inferred return, exactly as a toplevel call has since v0.0.3.
|
|
1376
1244
|
#
|
|
1377
|
-
# ADR-24 WD3 originally gated this: inside a class / method body only a
|
|
1378
|
-
#
|
|
1379
|
-
#
|
|
1380
|
-
#
|
|
1381
|
-
#
|
|
1382
|
-
#
|
|
1383
|
-
#
|
|
1384
|
-
#
|
|
1385
|
-
#
|
|
1386
|
-
#
|
|
1387
|
-
#
|
|
1388
|
-
#
|
|
1389
|
-
# tuple-slot destructure (slice 3). With the residual all genuine-or-win,
|
|
1390
|
-
# the gate opened permanently on 2026-06-12 (ADR-57 WD2): the gate-open
|
|
1391
|
-
# `rigor check lib` + plugin self-check delta is zero, and the Mastodon /
|
|
1392
|
-
# haml / kramdown corpora show only adjudicated wins (a more precise error
|
|
1393
|
-
# message; FP removals).
|
|
1245
|
+
# ADR-24 WD3 originally gated this: inside a class / method body only a `Bot` return was adopted,
|
|
1246
|
+
# everything else stayed `Dynamic[top]`, because an early unconditional-adoption experiment regressed
|
|
1247
|
+
# `rigor check lib` by 16 diagnostics. ADR-55 / ADR-56 then chipped the gate open for the
|
|
1248
|
+
# recursive-fixpoint summary and the value-pinned unroll envelope. ADR-57 closed the arc: it re-ran the
|
|
1249
|
+
# gate-open experiment per engine generation and adjudicated every firing as genuine-or-artifact,
|
|
1250
|
+
# fixing the artifacts at their root — the tail-only body evaluator dropping explicit `return` (slice
|
|
1251
|
+
# 1), multi-value returns not contributing a Tuple (slice 1), escaping block-captured content mutation
|
|
1252
|
+
# surviving as a precise seed both inline and across a method boundary (slices 2/3), two over-strict
|
|
1253
|
+
# self-authored RBS signatures (slice 1), and an over-optional tuple-slot destructure (slice 3). With
|
|
1254
|
+
# the residual all genuine-or-win, the gate opened permanently on 2026-06-12 (ADR-57 WD2): the
|
|
1255
|
+
# gate-open `rigor check lib` + plugin self-check delta is zero, and the Mastodon / haml / kramdown
|
|
1256
|
+
# corpora show only adjudicated wins (a more precise error message; FP removals).
|
|
1394
1257
|
#
|
|
1395
|
-
# The historical `adoptable_self_call_result?` predicate (its
|
|
1396
|
-
#
|
|
1397
|
-
#
|
|
1398
|
-
#
|
|
1399
|
-
|
|
1400
|
-
# value
|
|
1401
|
-
|
|
1402
|
-
# An extended (value-keyed) guard frame is `[plain_signature,
|
|
1403
|
-
# value_key]` where `plain_signature` is itself the `[receiver,
|
|
1404
|
-
# method]` pair; a plain frame is that pair directly.
|
|
1258
|
+
# The historical `adoptable_self_call_result?` predicate (its `self_type.nil?` / `Bot` /
|
|
1259
|
+
# fixpoint-summary / unroll special cases) is now subsumed by unconditional adoption and removed;
|
|
1260
|
+
# `try_local_def_dispatch` / `try_user_method_inference` simply return the inferred return.
|
|
1261
|
+
# `clamp_unroll_result` still backstops an untrustworthy unrolled value independently of adoption.
|
|
1262
|
+
|
|
1263
|
+
# An extended (value-keyed) guard frame is `[plain_signature, value_key]` where `plain_signature` is
|
|
1264
|
+
# itself the `[receiver, method]` pair; a plain frame is that pair directly.
|
|
1405
1265
|
def extended_frame?(frame)
|
|
1406
1266
|
frame.is_a?(Array) && frame.size == 2 && frame.first.is_a?(Array)
|
|
1407
1267
|
end
|
|
1408
1268
|
|
|
1409
|
-
# The plain `(receiver, method)` signature carried by a guard frame:
|
|
1410
|
-
#
|
|
1411
|
-
# extended (value-keyed) frame.
|
|
1269
|
+
# The plain `(receiver, method)` signature carried by a guard frame: the frame itself for a plain
|
|
1270
|
+
# frame, or its first element for an extended (value-keyed) frame.
|
|
1412
1271
|
def plain_part(frame)
|
|
1413
1272
|
extended_frame?(frame) ? frame.first : frame
|
|
1414
1273
|
end
|
|
1415
1274
|
|
|
1416
|
-
# True when `type` is a concrete value — a `Type::Constant` or a
|
|
1417
|
-
#
|
|
1418
|
-
#
|
|
1419
|
-
#
|
|
1420
|
-
# returns as `Dynamic[top]`). A concrete value at a call site is
|
|
1421
|
-
# strictly more precise and can never enable an undefined-method or
|
|
1275
|
+
# True when `type` is a concrete value — a `Type::Constant` or a `Type::Tuple` whose elements are
|
|
1276
|
+
# (recursively) all value-pinned. ADR-55 slice 1: a value-pinned self-call result is adopted even
|
|
1277
|
+
# inside a class/method body (where WD3 otherwise keeps non-`Bot` returns as `Dynamic[top]`). A
|
|
1278
|
+
# concrete value at a call site is strictly more precise and can never enable an undefined-method or
|
|
1422
1279
|
# argument-type false positive — it is FP-neutral by construction.
|
|
1423
1280
|
def fully_value_pinned?(type)
|
|
1424
1281
|
case type
|
|
@@ -1442,20 +1299,16 @@ module Rigor
|
|
|
1442
1299
|
nil
|
|
1443
1300
|
end
|
|
1444
1301
|
|
|
1445
|
-
# Module-singleton call resolution (ADR-57 follow-up) — resolves
|
|
1446
|
-
# `Foo
|
|
1447
|
-
#
|
|
1448
|
-
#
|
|
1449
|
-
#
|
|
1450
|
-
# `self_type` is the SAME `Singleton[Foo]` carrier, so an
|
|
1451
|
-
# implicit-self call inside (`def self.via; helper(x); end`)
|
|
1452
|
-
# re-enters this tier and resolves against the same singleton table
|
|
1302
|
+
# Module-singleton call resolution (ADR-57 follow-up) — resolves `Foo.<name>` on a `Singleton[Foo]`
|
|
1303
|
+
# receiver against `Foo`'s user-side singleton defs (`def self.x`, `def Foo.x`, a `class << self`
|
|
1304
|
+
# body, or a `module_function` method) and re-types the body with the call's argument types bound.
|
|
1305
|
+
# The body scope's `self_type` is the SAME `Singleton[Foo]` carrier, so an implicit-self call inside
|
|
1306
|
+
# (`def self.via; helper(x); end`) re-enters this tier and resolves against the same singleton table
|
|
1453
1307
|
# — the symmetric counterpart of the instance-side ancestor walk.
|
|
1454
1308
|
#
|
|
1455
|
-
# Resolution is OWN-class only: the singleton-ancestry chain
|
|
1456
|
-
#
|
|
1457
|
-
#
|
|
1458
|
-
# never a false resolution (ADR-57 follow-up § module-singleton).
|
|
1309
|
+
# Resolution is OWN-class only: the singleton-ancestry chain (`extend`ed modules, inherited
|
|
1310
|
+
# class-method dispatch) is not walked at this slice. A miss degrades to today's `Dynamic[top]`, never
|
|
1311
|
+
# a false resolution (ADR-57 follow-up § module-singleton).
|
|
1459
1312
|
def try_singleton_method_inference(receiver, call_node, arg_types)
|
|
1460
1313
|
return nil unless receiver.is_a?(Type::Singleton)
|
|
1461
1314
|
|
|
@@ -1470,42 +1323,30 @@ module Rigor
|
|
|
1470
1323
|
nil
|
|
1471
1324
|
end
|
|
1472
1325
|
|
|
1473
|
-
# ADR-24 slice 2 — resolves `method_name` against
|
|
1474
|
-
#
|
|
1475
|
-
#
|
|
1476
|
-
# and
|
|
1477
|
-
#
|
|
1478
|
-
# `
|
|
1479
|
-
#
|
|
1480
|
-
# class/module ends that branch. Cross-file: the chain is
|
|
1481
|
-
# followed through `Scope#discovered_superclasses` /
|
|
1482
|
-
# `#discovered_includes` / `#discovered_def_nodes`, which
|
|
1483
|
-
# the runner seeds from the project-wide pre-pass. The walk
|
|
1484
|
-
# is breadth-first, cycle-guarded, and node-count-capped.
|
|
1326
|
+
# ADR-24 slice 2 — resolves `method_name` against `class_name`'s own `def`s, then walks the user-class
|
|
1327
|
+
# ancestor chain: included / prepended modules (transitive) and the superclass chain. RBS-known
|
|
1328
|
+
# ancestors are NOT walked here — the `MethodDispatcher` RBS tier runs before
|
|
1329
|
+
# `try_user_method_inference` and already covers them; an ancestor name that resolves to no
|
|
1330
|
+
# project-discovered class/module ends that branch. Cross-file: the chain is followed through
|
|
1331
|
+
# `Scope#discovered_superclasses` / `#discovered_includes` / `#discovered_def_nodes`, which the runner
|
|
1332
|
+
# seeds from the project-wide pre-pass. The walk is breadth-first, cycle-guarded, and node-count-capped.
|
|
1485
1333
|
ANCESTOR_WALK_LIMIT = 100
|
|
1486
1334
|
private_constant :ANCESTOR_WALK_LIMIT
|
|
1487
1335
|
|
|
1488
1336
|
CLASS_GRAPH_CACHE_KEY = :__rigor_class_graph_cache__
|
|
1489
1337
|
private_constant :CLASS_GRAPH_CACHE_KEY
|
|
1490
1338
|
|
|
1491
|
-
# Run-scoped memo for the static class-graph resolvers below. They
|
|
1492
|
-
#
|
|
1493
|
-
#
|
|
1494
|
-
#
|
|
1495
|
-
# `
|
|
1496
|
-
#
|
|
1497
|
-
#
|
|
1498
|
-
#
|
|
1499
|
-
# memo
|
|
1500
|
-
#
|
|
1501
|
-
# `
|
|
1502
|
-
# `Scope` that swaps an index via `with_discovered_*`, transparently
|
|
1503
|
-
# lands in a fresh bucket while everything sharing the tables shares
|
|
1504
|
-
# the memo. Steady-state cost is three identity-keyed hash reads and
|
|
1505
|
-
# zero allocation — the `||=` chains only allocate on the first miss
|
|
1506
|
-
# of a generation. (Pool mode forks per worker, so the
|
|
1507
|
-
# `Thread.current` store is process-local and never crosses a
|
|
1508
|
-
# project boundary.)
|
|
1339
|
+
# Run-scoped memo for the static class-graph resolvers below. They are pure functions of the *frozen*
|
|
1340
|
+
# project index trio (`discovered_def_nodes` / `discovered_superclasses` / `discovered_includes`) —
|
|
1341
|
+
# `user_def_for` / `superclass_of` / `includes_of` read nothing else, and never touch the current
|
|
1342
|
+
# scope's locals or narrowings — so a result computed for one `(class, method)` is valid for every
|
|
1343
|
+
# `Scope` that shares those tables. `ExpressionTyper` is rebuilt per `Scope#type_of`, so the memo lives
|
|
1344
|
+
# on `Thread.current` rather than on `self`. It is keyed by the *identity* of the three frozen tables
|
|
1345
|
+
# (nested `compare_by_identity` stores): a new analysis generation, or any `Scope` that swaps an index
|
|
1346
|
+
# via `with_discovered_*`, transparently lands in a fresh bucket while everything sharing the tables
|
|
1347
|
+
# shares the memo. Steady-state cost is three identity-keyed hash reads and zero allocation — the `||=`
|
|
1348
|
+
# chains only allocate on the first miss of a generation. (Pool mode forks per worker, so the
|
|
1349
|
+
# `Thread.current` store is process-local and never crosses a project boundary.)
|
|
1509
1350
|
def class_graph_buckets
|
|
1510
1351
|
store = (Thread.current[CLASS_GRAPH_CACHE_KEY] ||= {}.compare_by_identity)
|
|
1511
1352
|
by_def = (store[scope.discovered_def_nodes] ||= {}.compare_by_identity)
|
|
@@ -1517,14 +1358,11 @@ module Rigor
|
|
|
1517
1358
|
resolve_user_def_with_owner(class_name, method_name).first
|
|
1518
1359
|
end
|
|
1519
1360
|
|
|
1520
|
-
# ADR-57 N5 follow-up — resolves the method's def node AND the
|
|
1521
|
-
#
|
|
1522
|
-
#
|
|
1523
|
-
#
|
|
1524
|
-
# owner is
|
|
1525
|
-
# are cached together (the walk is identical to the def-only path it
|
|
1526
|
-
# replaced) and returned as a `[def_node, owner]` pair; `owner` is
|
|
1527
|
-
# nil exactly when `def_node` is nil.
|
|
1361
|
+
# ADR-57 N5 follow-up — resolves the method's def node AND the ancestor that owns it (the class/module
|
|
1362
|
+
# whose own `def` table holds the body, which may differ from `class_name` when the method is
|
|
1363
|
+
# inherited from a superclass or included module). The owner is what the overridable-method adoption
|
|
1364
|
+
# gate keys on. Both are cached together (the walk is identical to the def-only path it replaced) and
|
|
1365
|
+
# returned as a `[def_node, owner]` pair; `owner` is nil exactly when `def_node` is nil.
|
|
1528
1366
|
def resolve_user_def_with_owner(class_name, method_name)
|
|
1529
1367
|
cache = class_graph_buckets[:user_def]
|
|
1530
1368
|
table = (cache[class_name.to_s] ||= {})
|
|
@@ -1557,12 +1395,10 @@ module Rigor
|
|
|
1557
1395
|
[nil, nil]
|
|
1558
1396
|
end
|
|
1559
1397
|
|
|
1560
|
-
# Pushes `current`'s direct ancestors onto the BFS queue:
|
|
1561
|
-
#
|
|
1562
|
-
#
|
|
1563
|
-
#
|
|
1564
|
-
# nesting; names that resolve to no project class/module
|
|
1565
|
-
# are dropped (RBS-known / third-party ancestors).
|
|
1398
|
+
# Pushes `current`'s direct ancestors onto the BFS queue: included / prepended modules first (Ruby
|
|
1399
|
+
# places mixins nearer than the superclass), then the superclass. Each as-written name is resolved
|
|
1400
|
+
# against `current`'s lexical nesting; names that resolve to no project class/module are dropped
|
|
1401
|
+
# (RBS-known / third-party ancestors).
|
|
1566
1402
|
def enqueue_ancestors(current, queue)
|
|
1567
1403
|
scope.includes_of(current).each do |raw|
|
|
1568
1404
|
resolved = resolve_ancestor_class_name(current, raw)
|
|
@@ -1575,13 +1411,10 @@ module Rigor
|
|
|
1575
1411
|
queue.push(resolved_super) if resolved_super
|
|
1576
1412
|
end
|
|
1577
1413
|
|
|
1578
|
-
# Resolves a superclass name AS WRITTEN (`"Base"`, or a
|
|
1579
|
-
#
|
|
1580
|
-
#
|
|
1581
|
-
# the
|
|
1582
|
-
# subclass, innermost first, then bare. Returns nil when
|
|
1583
|
-
# no candidate names a discovered user class (e.g. the
|
|
1584
|
-
# superclass is an RBS-known or third-party class).
|
|
1414
|
+
# Resolves a superclass name AS WRITTEN (`"Base"`, or a qualified `"A::B"`) to a project-discovered
|
|
1415
|
+
# class, following Ruby's `Module.nesting` constant lookup: try the raw name under each enclosing
|
|
1416
|
+
# namespace of the subclass, innermost first, then bare. Returns nil when no candidate names a
|
|
1417
|
+
# discovered user class (e.g. the superclass is an RBS-known or third-party class).
|
|
1585
1418
|
def resolve_ancestor_class_name(subclass_qualified, raw_superclass)
|
|
1586
1419
|
by_subclass = (class_graph_buckets[:name][subclass_qualified] ||= {})
|
|
1587
1420
|
return by_subclass[raw_superclass] if by_subclass.key?(raw_superclass)
|
|
@@ -1605,29 +1438,22 @@ module Rigor
|
|
|
1605
1438
|
scope.discovered_includes.key?(name)
|
|
1606
1439
|
end
|
|
1607
1440
|
|
|
1608
|
-
# ADR-57 N5 — overridable-method adoption gate. A self-call resolved
|
|
1609
|
-
#
|
|
1610
|
-
#
|
|
1611
|
-
#
|
|
1612
|
-
#
|
|
1613
|
-
#
|
|
1614
|
-
#
|
|
1615
|
-
#
|
|
1616
|
-
#
|
|
1617
|
-
# return degrades to `Dynamic[top]`, deliberately re-opening a Dynamic
|
|
1618
|
-
# source ONLY for genuinely-overridden methods. A method with no
|
|
1619
|
-
# discovered override folds exactly as before — over-conservatism must
|
|
1620
|
-
# not re-open Dynamic for final methods.
|
|
1441
|
+
# ADR-57 N5 — overridable-method adoption gate. A self-call resolved to a project `def` whose owner has
|
|
1442
|
+
# a discovered subclass / includer that REDEFINES the same method (same instance-vs-singleton kind) is
|
|
1443
|
+
# a template-method site: the base body's literal return is the *default*, not the value every
|
|
1444
|
+
# receiver sees, so adopting it as a flow constant is unsound (rgl `module Graph; def directed?; false`
|
|
1445
|
+
# folds `unless directed?` always-true, ignoring `DirectedAdjacencyGraph` overriding it to `true` — the
|
|
1446
|
+
# entire rgl warning set, per the 2026-06-13 app/network survey N5 row). On such a hit the precise
|
|
1447
|
+
# return degrades to `Dynamic[top]`, deliberately re-opening a Dynamic source ONLY for
|
|
1448
|
+
# genuinely-overridden methods. A method with no discovered override folds exactly as before —
|
|
1449
|
+
# over-conservatism must not re-open Dynamic for final methods.
|
|
1621
1450
|
#
|
|
1622
|
-
# The gate only inspects a *flow-constant-foldable* result (a
|
|
1623
|
-
#
|
|
1624
|
-
#
|
|
1625
|
-
#
|
|
1626
|
-
#
|
|
1627
|
-
#
|
|
1628
|
-
# harmless and is left untouched — this keeps the override-relation
|
|
1629
|
-
# walk off the hot path for the overwhelming majority of self-calls
|
|
1630
|
-
# (whose return is not a bare constant).
|
|
1451
|
+
# The gate only inspects a *flow-constant-foldable* result (a `Constant`, or a `Tuple` of such): only a
|
|
1452
|
+
# value-pinned return can mislead a downstream `if`/`unless`/`case` into an `always-truthy-condition`
|
|
1453
|
+
# fold, which is exactly the unsoundness the gate exists to remove. A `Nominal` / `Dynamic` / union
|
|
1454
|
+
# return cannot produce a flow constant, so adopting it from an overridden method is harmless and is
|
|
1455
|
+
# left untouched — this keeps the override-relation walk off the hot path for the overwhelming
|
|
1456
|
+
# majority of self-calls (whose return is not a bare constant).
|
|
1631
1457
|
def degrade_if_overridable(result, owner, method_name, kind)
|
|
1632
1458
|
return result if owner.nil?
|
|
1633
1459
|
return result unless fully_value_pinned?(result)
|
|
@@ -1639,14 +1465,11 @@ module Rigor
|
|
|
1639
1465
|
OVERRIDE_GATE_CACHE_KEY = :__rigor_overridable_method_gate__
|
|
1640
1466
|
private_constant :OVERRIDE_GATE_CACHE_KEY
|
|
1641
1467
|
|
|
1642
|
-
# Run-scoped memo for {#overridden_in_project?}, keyed (like
|
|
1643
|
-
#
|
|
1644
|
-
#
|
|
1645
|
-
#
|
|
1646
|
-
#
|
|
1647
|
-
# cache key on the hot path (the gate runs on every adopted self-call
|
|
1648
|
-
# return), so a steady-state hit is three identity hash reads + two
|
|
1649
|
-
# string/symbol hash reads with zero allocation.
|
|
1468
|
+
# Run-scoped memo for {#overridden_in_project?}, keyed (like `class_graph_buckets`) by the identity of
|
|
1469
|
+
# the frozen discovery trio so a new analysis generation lands in a fresh bucket, then nested `kind →
|
|
1470
|
+
# owner → method_name`. The predicate is a pure function of those tables. Nesting avoids allocating a
|
|
1471
|
+
# composite cache key on the hot path (the gate runs on every adopted self-call return), so a
|
|
1472
|
+
# steady-state hit is three identity hash reads + two string/symbol hash reads with zero allocation.
|
|
1650
1473
|
def override_gate_buckets
|
|
1651
1474
|
store = (Thread.current[OVERRIDE_GATE_CACHE_KEY] ||= {}.compare_by_identity)
|
|
1652
1475
|
by_def = (store[scope.discovered_def_nodes] ||= {}.compare_by_identity)
|
|
@@ -1654,12 +1477,10 @@ module Rigor
|
|
|
1654
1477
|
by_super[scope.discovered_includes] ||= { instance: {}, singleton: {} }
|
|
1655
1478
|
end
|
|
1656
1479
|
|
|
1657
|
-
# True when some discovered project class/module — distinct from
|
|
1658
|
-
#
|
|
1659
|
-
#
|
|
1660
|
-
#
|
|
1661
|
-
# an owner module). A same-name reopen of `owner` itself is NOT an
|
|
1662
|
-
# override (monkey-patch reopen shares the owner identity). Memoized
|
|
1480
|
+
# True when some discovered project class/module — distinct from `owner` — redefines `(method_name,
|
|
1481
|
+
# kind)` AND is related to `owner` (a transitive discovered subclass of an owner class, or a
|
|
1482
|
+
# class/module that includes/prepends — extends, for singleton kind — an owner module). A same-name
|
|
1483
|
+
# reopen of `owner` itself is NOT an override (monkey-patch reopen shares the owner identity). Memoized
|
|
1663
1484
|
# per `(owner, method_name, kind)`.
|
|
1664
1485
|
def overridden_in_project?(owner, method_name, kind)
|
|
1665
1486
|
by_owner = (override_gate_buckets[kind][owner] ||= {})
|
|
@@ -1676,16 +1497,13 @@ module Rigor
|
|
|
1676
1497
|
end
|
|
1677
1498
|
end
|
|
1678
1499
|
|
|
1679
|
-
# Every discovered project class/module whose OWN def table redefines
|
|
1680
|
-
# `
|
|
1681
|
-
#
|
|
1682
|
-
#
|
|
1683
|
-
#
|
|
1684
|
-
#
|
|
1685
|
-
#
|
|
1686
|
-
# scan on every `(method_name, kind)` first-miss — the gate runs on
|
|
1687
|
-
# every adopted self-call return, so the full-table `filter_map` it
|
|
1688
|
-
# replaced was the dominant added allocation on a large `lib`.
|
|
1500
|
+
# Every discovered project class/module whose OWN def table redefines `(method_name, kind)`. Instance
|
|
1501
|
+
# kind reads `discovered_def_nodes`, singleton kind reads `discovered_singleton_def_nodes` — both are
|
|
1502
|
+
# genuine project `def` bodies (not RBS / accessor synthesis), so a name's presence is a real
|
|
1503
|
+
# redefinition. Served from a per-generation inverted index (`method_name → [owner names]`) built once
|
|
1504
|
+
# per def table, so the lookup is a single hash read rather than a full-table scan on every
|
|
1505
|
+
# `(method_name, kind)` first-miss — the gate runs on every adopted self-call return, so the full-table
|
|
1506
|
+
# `filter_map` it replaced was the dominant added allocation on a large `lib`.
|
|
1689
1507
|
def redefiners_of(method_name, kind)
|
|
1690
1508
|
method_definers_index(kind)[method_name] || EMPTY_REDEFINERS
|
|
1691
1509
|
end
|
|
@@ -1696,11 +1514,10 @@ module Rigor
|
|
|
1696
1514
|
METHOD_DEFINERS_INDEX_KEY = :__rigor_method_definers_index__
|
|
1697
1515
|
private_constant :METHOD_DEFINERS_INDEX_KEY
|
|
1698
1516
|
|
|
1699
|
-
# Per-generation `method_name (Symbol) → [owner names]` inverted index
|
|
1700
|
-
#
|
|
1701
|
-
#
|
|
1702
|
-
#
|
|
1703
|
-
# has no class ancestry and so can never be an override.
|
|
1517
|
+
# Per-generation `method_name (Symbol) → [owner names]` inverted index over the instance / singleton
|
|
1518
|
+
# def tables, memoised by the identity of the def table it inverts (a new analysis generation lands in
|
|
1519
|
+
# a fresh bucket). The toplevel sentinel is excluded — a toplevel `def` has no class ancestry and so
|
|
1520
|
+
# can never be an override.
|
|
1704
1521
|
def method_definers_index(kind)
|
|
1705
1522
|
table = kind == :singleton ? scope.discovered_singleton_def_nodes : scope.discovered_def_nodes
|
|
1706
1523
|
store = (Thread.current[METHOD_DEFINERS_INDEX_KEY] ||= {}.compare_by_identity)
|
|
@@ -1717,12 +1534,10 @@ module Rigor
|
|
|
1717
1534
|
index
|
|
1718
1535
|
end
|
|
1719
1536
|
|
|
1720
|
-
# True when `candidate`'s transitive ancestor chain (superclasses +
|
|
1721
|
-
#
|
|
1722
|
-
#
|
|
1723
|
-
#
|
|
1724
|
-
# name resolution (lexical nesting, RBS-known-ancestor pruning) is
|
|
1725
|
-
# identical.
|
|
1537
|
+
# True when `candidate`'s transitive ancestor chain (superclasses + included/prepended modules) reaches
|
|
1538
|
+
# `owner` — i.e. `candidate` is a subclass of an owner class or an includer of an owner module. Reuses
|
|
1539
|
+
# the same BFS resolver the method-resolution ancestor walk uses, so name resolution (lexical nesting,
|
|
1540
|
+
# RBS-known-ancestor pruning) is identical.
|
|
1726
1541
|
def related_to_owner?(candidate, owner)
|
|
1727
1542
|
queue = []
|
|
1728
1543
|
enqueue_ancestors(candidate, queue)
|
|
@@ -1749,109 +1564,83 @@ module Rigor
|
|
|
1749
1564
|
INFERENCE_UNROLL_FUEL_KEY = :__rigor_user_method_unroll_fuel__
|
|
1750
1565
|
private_constant :INFERENCE_UNROLL_FUEL_KEY
|
|
1751
1566
|
|
|
1752
|
-
# ADR-55 slice 2 — thread-local fixpoint return-summary table,
|
|
1753
|
-
#
|
|
1754
|
-
#
|
|
1755
|
-
#
|
|
1756
|
-
# `assumption` is the current Kleene iterate (seeded `bot`) and
|
|
1757
|
-
# `consulted` flips true when an in-cycle re-entry returns it.
|
|
1567
|
+
# ADR-55 slice 2 — thread-local fixpoint return-summary table, keyed by the plain `(receiver, method)`
|
|
1568
|
+
# signature (NOT the value-extended signature: extended frames from slice 1 share the same summary).
|
|
1569
|
+
# Each entry is `{ assumption:, consulted: }` where `assumption` is the current Kleene iterate (seeded
|
|
1570
|
+
# `bot`) and `consulted` flips true when an in-cycle re-entry returns it.
|
|
1758
1571
|
INFERENCE_SUMMARY_KEY = :__rigor_user_method_return_summary__
|
|
1759
1572
|
private_constant :INFERENCE_SUMMARY_KEY
|
|
1760
1573
|
|
|
1761
|
-
# Monotonic per-thread counter, bumped once each time `consult_summary`
|
|
1762
|
-
#
|
|
1763
|
-
#
|
|
1764
|
-
#
|
|
1765
|
-
#
|
|
1766
|
-
# some unrelated outermost frame merely *seeded* (but never consulted)
|
|
1767
|
-
# its own entry. See `infer_user_method_return`'s post-hoc memo gate.
|
|
1574
|
+
# Monotonic per-thread counter, bumped once each time `consult_summary` actually reads an in-flight
|
|
1575
|
+
# fixpoint assumption (ADR-55 slice 2). A method return computed across an interval in which this
|
|
1576
|
+
# counter does NOT move depended on no transient Kleene iterate, so it is FINAL and safe to memoise —
|
|
1577
|
+
# even when the `summaries` table is non-empty because some unrelated outermost frame merely *seeded*
|
|
1578
|
+
# (but never consulted) its own entry. See `infer_user_method_return`'s post-hoc memo gate.
|
|
1768
1579
|
SUMMARY_CONSULT_COUNTER_KEY = :__rigor_user_method_summary_consults__
|
|
1769
1580
|
private_constant :SUMMARY_CONSULT_COUNTER_KEY
|
|
1770
1581
|
|
|
1771
|
-
# Per-thread append-only log of the seed depths of every in-flight
|
|
1772
|
-
#
|
|
1773
|
-
#
|
|
1774
|
-
#
|
|
1775
|
-
#
|
|
1776
|
-
#
|
|
1777
|
-
#
|
|
1778
|
-
#
|
|
1779
|
-
#
|
|
1780
|
-
# `untyped` rather than fold one branch's seed into a "final"
|
|
1781
|
-
# constant. Own-signature consults log depth == own depth, and a
|
|
1782
|
-
# nested fixpoint that completes within the evaluation logs depths
|
|
1783
|
-
# > own depth; neither is foreign. Cleared with the summary table
|
|
1784
|
-
# when the guard stack drains to empty.
|
|
1582
|
+
# Per-thread append-only log of the seed depths of every in-flight summary `consult_summary` read
|
|
1583
|
+
# (ADR-55 slice 2 mutual-recursion soundness fix, 2026-06-12). Each fixpoint owner records the guard
|
|
1584
|
+
# stack size at seed time on its entry (`depth:`); a consult appends the consulted entry's depth here.
|
|
1585
|
+
# A fixpoint whose body evaluation logged a depth SHALLOWER than its own seed depth read an ancestor
|
|
1586
|
+
# signature's transient Kleene iterate -- cross-signature mutual recursion (`even?`/`odd?`) -- so its
|
|
1587
|
+
# computed return is entangled with a not-yet-converged foreign assumption and must degrade to
|
|
1588
|
+
# `untyped` rather than fold one branch's seed into a "final" constant. Own-signature consults log
|
|
1589
|
+
# depth == own depth, and a nested fixpoint that completes within the evaluation logs depths > own
|
|
1590
|
+
# depth; neither is foreign. Cleared with the summary table when the guard stack drains to empty.
|
|
1785
1591
|
SUMMARY_CONSULT_DEPTHS_KEY = :__rigor_user_method_summary_consult_depths__
|
|
1786
1592
|
private_constant :SUMMARY_CONSULT_DEPTHS_KEY
|
|
1787
1593
|
|
|
1788
|
-
# ADR-57 follow-up — run-scoped memo for resolved user-method
|
|
1789
|
-
#
|
|
1790
|
-
#
|
|
1791
|
-
#
|
|
1792
|
-
#
|
|
1793
|
-
# whole-`lib` cost grows superlinearly in files-per-process (the
|
|
1794
|
-
# 2026-06-12 Rails survey's whole-`lib` scaling wall).
|
|
1594
|
+
# ADR-57 follow-up — run-scoped memo for resolved user-method return types. The ADR-57 gate-open made
|
|
1595
|
+
# every resolved in-body self-call adopt the callee's inferred return, which re-types the callee body
|
|
1596
|
+
# once per call site. With a project-wide discovery index, file N re-types callees defined in files
|
|
1597
|
+
# 1..N-1, so whole-`lib` cost grows superlinearly in files-per-process (the 2026-06-12 Rails survey's
|
|
1598
|
+
# whole-`lib` scaling wall).
|
|
1795
1599
|
#
|
|
1796
|
-
# `infer_user_method_return` is a pure function of
|
|
1797
|
-
#
|
|
1798
|
-
#
|
|
1799
|
-
#
|
|
1800
|
-
#
|
|
1801
|
-
#
|
|
1802
|
-
# what makes a signature-keyed return memo sound where the
|
|
1803
|
-
# ADR-52 WD5 per-call-NODE contribution cache was not: that cache
|
|
1804
|
-
# keyed scope-sensitive results on the node; this memo keys a
|
|
1805
|
-
# scope-INSENSITIVE result on its real inputs.)
|
|
1600
|
+
# `infer_user_method_return` is a pure function of `(def_node, receiver, arg_types)` PLUS the frozen
|
|
1601
|
+
# project discovery index: `build_user_method_body_scope` binds the args to the params in a FRESH
|
|
1602
|
+
# `Scope` seeded from an empty fact / narrowing store and inherits `scope.discovery` whole by
|
|
1603
|
+
# reference — the caller's narrowing state never enters. (This is what makes a signature-keyed return
|
|
1604
|
+
# memo sound where the ADR-52 WD5 per-call-NODE contribution cache was not: that cache keyed
|
|
1605
|
+
# scope-sensitive results on the node; this memo keys a scope-INSENSITIVE result on its real inputs.)
|
|
1806
1606
|
#
|
|
1807
|
-
# Two dimensions are call-site-varying and so live IN the key:
|
|
1808
|
-
#
|
|
1809
|
-
#
|
|
1810
|
-
#
|
|
1811
|
-
#
|
|
1812
|
-
# the
|
|
1813
|
-
#
|
|
1814
|
-
#
|
|
1815
|
-
# consulted and populated ONLY when the incoming guard stack is
|
|
1816
|
-
# empty (a genuine top-of-stack entry, whose result is final and
|
|
1817
|
-
# cannot be an in-progress assumption or a clamped value). Frames
|
|
1818
|
-
# entered with a non-empty stack bypass the memo entirely and
|
|
1819
|
-
# compute as before.
|
|
1607
|
+
# Two dimensions are call-site-varying and so live IN the key: the receiver carrier
|
|
1608
|
+
# (`describe(:short)`) and the argument-type signature (`describe(:short)` of each arg) — value-pinned
|
|
1609
|
+
# args change folds (`factorial(5)` vs `factorial(6)`), so a coarser key would serve a stale fold. The
|
|
1610
|
+
# third unsafe dimension — the ADR-55 recursion machinery (unroll fuel / fixpoint Kleene
|
|
1611
|
+
# assumption / WD1 clamp) producing a TRANSIENT result rather than a final return — is excluded
|
|
1612
|
+
# structurally: the memo is consulted and populated ONLY when the incoming guard stack is empty (a
|
|
1613
|
+
# genuine top-of-stack entry, whose result is final and cannot be an in-progress assumption or a
|
|
1614
|
+
# clamped value). Frames entered with a non-empty stack bypass the memo entirely and compute as before.
|
|
1820
1615
|
#
|
|
1821
|
-
# Keyed by the identity of the frozen discovery `def_nodes`
|
|
1822
|
-
#
|
|
1823
|
-
#
|
|
1824
|
-
# `
|
|
1825
|
-
#
|
|
1826
|
-
# lives on `Thread.current`; fork-pool workers are separate
|
|
1827
|
-
# processes, so it never crosses a project boundary.
|
|
1616
|
+
# Keyed by the identity of the frozen discovery `def_nodes` table (a new analysis generation lands in
|
|
1617
|
+
# a fresh bucket, mirroring `class_graph_buckets`) then by the identity of the `def_node` and the
|
|
1618
|
+
# `[receiver, *args]` descriptor tuple. `ExpressionTyper` is rebuilt per `Scope#type_of`, so the store
|
|
1619
|
+
# lives on `Thread.current`; fork-pool workers are separate processes, so it never crosses a project
|
|
1620
|
+
# boundary.
|
|
1828
1621
|
RETURN_MEMO_KEY = :__rigor_user_method_return_memo__
|
|
1829
1622
|
private_constant :RETURN_MEMO_KEY
|
|
1830
1623
|
|
|
1831
|
-
# Per-inference recursion context threaded through the guard /
|
|
1832
|
-
#
|
|
1833
|
-
#
|
|
1834
|
-
#
|
|
1835
|
-
# parameter-list budget. `def_node` is carried separately (it is the
|
|
1836
|
-
# body owner, not call context).
|
|
1624
|
+
# Per-inference recursion context threaded through the guard / fixpoint helpers (ADR-55 slice 2).
|
|
1625
|
+
# Bundles the call descriptor (`receiver`, `arg_types`, `plain_signature`), the thread-local summary
|
|
1626
|
+
# table, and the WD1 clamp flag so the helpers stay within the parameter-list budget. `def_node` is
|
|
1627
|
+
# carried separately (it is the body owner, not call context).
|
|
1837
1628
|
RecursionContext = Data.define(
|
|
1838
1629
|
:receiver, :arg_types, :plain_signature, :summaries, :would_have_been_guarded
|
|
1839
1630
|
)
|
|
1840
1631
|
private_constant :RecursionContext
|
|
1841
1632
|
|
|
1842
|
-
# Total body evaluations the fixpoint iteration is permitted per
|
|
1843
|
-
#
|
|
1844
|
-
#
|
|
1633
|
+
# Total body evaluations the fixpoint iteration is permitted per outermost entry for a signature
|
|
1634
|
+
# (ADR-55 WD2). Hard, non-configurable — the iteration cap is part of the termination story (ADR-41
|
|
1635
|
+
# WD4).
|
|
1845
1636
|
RECURSION_FIXPOINT_CAP = 3
|
|
1846
1637
|
private_constant :RECURSION_FIXPOINT_CAP
|
|
1847
1638
|
|
|
1848
|
-
# Hard, non-configurable caps for the ADR-55 slice 1 constant-arg
|
|
1849
|
-
#
|
|
1850
|
-
#
|
|
1851
|
-
#
|
|
1852
|
-
#
|
|
1853
|
-
# guards (ADR-41 WD4) — not measurement-gated precision budgets —
|
|
1854
|
-
# so they ship default-on with no opt-in.
|
|
1639
|
+
# Hard, non-configurable caps for the ADR-55 slice 1 constant-arg unroll. `RECURSION_UNROLL_FUEL`
|
|
1640
|
+
# bounds the number of extended (value-keyed) frames per outermost inference entry;
|
|
1641
|
+
# `RECURSION_VALUE_SIZE_CAP` disqualifies a frame whose pinned argument values are structurally large.
|
|
1642
|
+
# Both are termination guards (ADR-41 WD4) — not measurement-gated precision budgets — so they ship
|
|
1643
|
+
# default-on with no opt-in.
|
|
1855
1644
|
RECURSION_UNROLL_FUEL = 32
|
|
1856
1645
|
private_constant :RECURSION_UNROLL_FUEL
|
|
1857
1646
|
|
|
@@ -1864,44 +1653,31 @@ module Rigor
|
|
|
1864
1653
|
body_scope = build_user_method_body_scope(def_node, receiver, arg_types)
|
|
1865
1654
|
return nil if body_scope.nil?
|
|
1866
1655
|
|
|
1867
|
-
# Recursion-guard signature. Keyed on `(receiver,
|
|
1868
|
-
# method
|
|
1869
|
-
#
|
|
1870
|
-
#
|
|
1871
|
-
#
|
|
1872
|
-
# `
|
|
1873
|
-
# `
|
|
1874
|
-
# recurse unboundedly whenever the carried argument
|
|
1875
|
-
# types differ at each level — observed as a
|
|
1876
|
-
# `SystemStackError` once implicit-self calls began
|
|
1877
|
-
# resolving during the main walk. `describe(:short)`
|
|
1878
|
-
# keeps non-Nominal receivers (the implicit `Object`
|
|
1879
|
-
# carrier for top-level / DSL-block defs) printable.
|
|
1656
|
+
# Recursion-guard signature. Keyed on `(receiver, method)` only — NOT the argument types. ADR-24 WD5:
|
|
1657
|
+
# a method whose summary is still being computed resolves to `Dynamic[top]` for that cycle. Keying on
|
|
1658
|
+
# arg types would let mutual recursion through a `module_function` module (`Acceptance#accepts` →
|
|
1659
|
+
# `accepts_one` → `accepts_dynamic` → `accepts`) recurse unboundedly whenever the carried argument
|
|
1660
|
+
# types differ at each level — observed as a `SystemStackError` once implicit-self calls began
|
|
1661
|
+
# resolving during the main walk. `describe(:short)` keeps non-Nominal receivers (the implicit
|
|
1662
|
+
# `Object` carrier for top-level / DSL-block defs) printable.
|
|
1880
1663
|
plain_signature = [receiver.describe(:short), def_node.name]
|
|
1881
1664
|
stack = (Thread.current[INFERENCE_GUARD_KEY] ||= [])
|
|
1882
1665
|
summaries = (Thread.current[INFERENCE_SUMMARY_KEY] ||= {})
|
|
1883
1666
|
|
|
1884
|
-
# ADR-57 follow-up — return memo. The inferred return is a pure
|
|
1885
|
-
#
|
|
1886
|
-
#
|
|
1887
|
-
#
|
|
1888
|
-
#
|
|
1889
|
-
#
|
|
1890
|
-
#
|
|
1891
|
-
#
|
|
1892
|
-
#
|
|
1893
|
-
#
|
|
1894
|
-
#
|
|
1895
|
-
#
|
|
1896
|
-
#
|
|
1897
|
-
#
|
|
1898
|
-
# whole-`lib` scaling wall: a deep DAG of non-recursive private
|
|
1899
|
-
# readers (ActiveStorage `video_analyzer.rb`) seeded a summary on its
|
|
1900
|
-
# first outermost method and thereafter the old `summaries.empty?`
|
|
1901
|
-
# gate disabled the memo for every nested call, re-walking the shared
|
|
1902
|
-
# sub-readers combinatorially (~932k body evaluations for ~20 tiny
|
|
1903
|
-
# methods). The computation itself lives in
|
|
1904
|
-
# `compute_user_method_return`.
|
|
1667
|
+
# ADR-57 follow-up — return memo. The inferred return is a pure function of `(def_node, receiver,
|
|
1668
|
+
# arg_types)` and the frozen discovery index whenever the computation does NOT depend on a transient
|
|
1669
|
+
# ADR-55 Kleene assumption (an in-flight fixpoint summary). Two structural preconditions decide
|
|
1670
|
+
# whether THIS frame's result is even a memo candidate, both stable across the body walk: the
|
|
1671
|
+
# signature must not already be on the recursion guard stack (else we are inside its own cycle) and
|
|
1672
|
+
# no constant-arg unroll may be in flight (its value-keyed frames are transient). When both hold we
|
|
1673
|
+
# consult the memo, and on a miss we compute, then store the result only if no fixpoint summary was
|
|
1674
|
+
# *consulted* during the computation (the post-hoc consult-counter check) — which is sound
|
|
1675
|
+
# regardless of whether the `summaries` table holds inert *seeded-but-unconsulted* entries left by
|
|
1676
|
+
# unrelated outermost frames. This is the fix for the whole-`lib` scaling wall: a deep DAG of
|
|
1677
|
+
# non-recursive private readers (ActiveStorage `video_analyzer.rb`) seeded a summary on its first
|
|
1678
|
+
# outermost method and thereafter the old `summaries.empty?` gate disabled the memo for every nested
|
|
1679
|
+
# call, re-walking the shared sub-readers combinatorially (~932k body evaluations for ~20 tiny
|
|
1680
|
+
# methods). The computation itself lives in `compute_user_method_return`.
|
|
1905
1681
|
unless memo_candidate?(stack, plain_signature)
|
|
1906
1682
|
return compute_user_method_return(def_node, body_scope, stack, summaries,
|
|
1907
1683
|
receiver, arg_types, plain_signature)
|
|
@@ -1917,28 +1693,23 @@ module Rigor
|
|
|
1917
1693
|
receiver, arg_types, plain_signature)
|
|
1918
1694
|
consults_after = Thread.current[SUMMARY_CONSULT_COUNTER_KEY] || 0
|
|
1919
1695
|
|
|
1920
|
-
# Store only a FINAL result. If a fixpoint summary was consulted
|
|
1921
|
-
#
|
|
1922
|
-
#
|
|
1923
|
-
# shared across call sites.
|
|
1696
|
+
# Store only a FINAL result. If a fixpoint summary was consulted during the computation, `result`
|
|
1697
|
+
# embeds a transient Kleene iterate whose value depends on the iteration in flight, so it must not
|
|
1698
|
+
# be shared across call sites.
|
|
1924
1699
|
memo[memo_key] = result if consults_after == consults_before
|
|
1925
1700
|
result
|
|
1926
1701
|
end
|
|
1927
1702
|
|
|
1928
|
-
# The ADR-55 recursion-guard + value-unroll + fixpoint body of
|
|
1929
|
-
#
|
|
1930
|
-
#
|
|
1931
|
-
# the ADR-57 follow-up; this is unchanged from pre-memo behaviour).
|
|
1703
|
+
# The ADR-55 recursion-guard + value-unroll + fixpoint body of user-method return inference, factored
|
|
1704
|
+
# out so `infer_user_method_return` is a thin memo wrapper (the memo is the ADR-57 follow-up; this is
|
|
1705
|
+
# unchanged from pre-memo behaviour).
|
|
1932
1706
|
def compute_user_method_return(def_node, body_scope, stack, summaries,
|
|
1933
1707
|
receiver, arg_types, plain_signature)
|
|
1934
|
-
# ADR-55 slice 1: when every bound argument is value-pinned,
|
|
1935
|
-
#
|
|
1936
|
-
#
|
|
1937
|
-
#
|
|
1938
|
-
#
|
|
1939
|
-
# entry; exhaustion or value blow-up falls back to the plain
|
|
1940
|
-
# `(receiver, method)` guard — today's behaviour. Non-constant
|
|
1941
|
-
# args never reach this path.
|
|
1708
|
+
# ADR-55 slice 1: when every bound argument is value-pinned, extend the guard key with a stable
|
|
1709
|
+
# descriptor of the argument *values* so distinct constant frames may recurse (e.g. `factorial(5)`
|
|
1710
|
+
# folds to `Constant[120]`). Distinct constant frames are bounded by `RECURSION_UNROLL_FUEL` per
|
|
1711
|
+
# outermost entry; exhaustion or value blow-up falls back to the plain `(receiver, method)` guard —
|
|
1712
|
+
# today's behaviour. Non-constant args never reach this path.
|
|
1942
1713
|
signature = plain_signature
|
|
1943
1714
|
value_key = constant_argument_value_key(arg_types)
|
|
1944
1715
|
extended = value_key && unroll_fuel_remaining(stack).positive?
|
|
@@ -1946,25 +1717,20 @@ module Rigor
|
|
|
1946
1717
|
|
|
1947
1718
|
if stack.include?(signature)
|
|
1948
1719
|
BudgetTrace.hit(BudgetTrace::RECURSION_GUARD)
|
|
1949
|
-
# ADR-55 slice 2: in-cycle re-entries return the current assumed
|
|
1950
|
-
#
|
|
1951
|
-
#
|
|
1952
|
-
#
|
|
1953
|
-
# exists. The WD4 composition: slice 1's clamp/fuel fallbacks
|
|
1954
|
-
# also route here when a summary is active.
|
|
1720
|
+
# ADR-55 slice 2: in-cycle re-entries return the current assumed summary (Kleene iterate, seeded
|
|
1721
|
+
# `bot`) instead of bare `untyped`. The fixpoint loop below seeds the entry on the outermost frame;
|
|
1722
|
+
# if a re-entry beats it here the entry already exists. The WD4 composition: slice 1's clamp/fuel
|
|
1723
|
+
# fallbacks also route here when a summary is active.
|
|
1955
1724
|
return consult_summary(summaries, plain_signature)
|
|
1956
1725
|
end
|
|
1957
1726
|
|
|
1958
|
-
# ADR-55 WD1 clamp (governing rule): the constant-arg unroll may
|
|
1959
|
-
#
|
|
1960
|
-
#
|
|
1961
|
-
#
|
|
1962
|
-
#
|
|
1963
|
-
# plain
|
|
1964
|
-
#
|
|
1965
|
-
# to a non-pinned type, the unroll surfaced a precise value the
|
|
1966
|
-
# plain guard would have masked (and the body evaluator's blind
|
|
1967
|
-
# spots can make that value wrong), so clamp it back to `untyped`.
|
|
1727
|
+
# ADR-55 WD1 clamp (governing rule): the constant-arg unroll may only ever surface a fully
|
|
1728
|
+
# value-pinned result; any other outcome must be byte-identical to the plain guard's `untyped`. A
|
|
1729
|
+
# frame that took the extended (value-keyed) path but whose plain `(receiver, method)` signature is
|
|
1730
|
+
# already on the stack — in plain form or as the plain part of an extended frame — would have been
|
|
1731
|
+
# guarded before slice 1. If such a frame's body folds to a non-pinned type, the unroll surfaced a
|
|
1732
|
+
# precise value the plain guard would have masked (and the body evaluator's blind spots can make
|
|
1733
|
+
# that value wrong), so clamp it back to `untyped`.
|
|
1968
1734
|
would_have_been_guarded =
|
|
1969
1735
|
extended &&
|
|
1970
1736
|
stack.any? { |frame| plain_part(frame) == plain_signature }
|
|
@@ -1976,46 +1742,39 @@ module Rigor
|
|
|
1976
1742
|
evaluate_guarded_user_method_body(def_node, body_scope, stack, signature, context)
|
|
1977
1743
|
end
|
|
1978
1744
|
|
|
1979
|
-
# True when this frame's result is a candidate for the return memo:
|
|
1980
|
-
#
|
|
1981
|
-
#
|
|
1982
|
-
#
|
|
1983
|
-
#
|
|
1984
|
-
#
|
|
1985
|
-
#
|
|
1986
|
-
#
|
|
1987
|
-
#
|
|
1988
|
-
# recursive DAG (the scaling wall). The two preconditions: no constant-
|
|
1989
|
-
# arg unroll in flight (its value-keyed frames are transient) and this
|
|
1990
|
-
# plain signature not itself on the recursion guard stack (else we are
|
|
1991
|
-
# inside its own cycle, returning a Kleene iterate).
|
|
1745
|
+
# True when this frame's result is a candidate for the return memo: the structural preconditions, both
|
|
1746
|
+
# stable across the body walk, that are necessary (but not sufficient) for a FINAL result. Sufficiency
|
|
1747
|
+
# is decided post-hoc in `infer_user_method_return` by the consult-counter check (no transient
|
|
1748
|
+
# fixpoint summary was read during the compute) — so unlike the prior `memoisable_return?` this
|
|
1749
|
+
# deliberately does NOT require an empty `summaries` table: inert seeded-but-unconsulted entries left
|
|
1750
|
+
# by unrelated outermost frames do not contaminate a result, and gating on them disabled the memo for
|
|
1751
|
+
# an entire non-recursive DAG (the scaling wall). The two preconditions: no constant-arg unroll in
|
|
1752
|
+
# flight (its value-keyed frames are transient) and this plain signature not itself on the recursion
|
|
1753
|
+
# guard stack (else we are inside its own cycle, returning a Kleene iterate).
|
|
1992
1754
|
def memo_candidate?(stack, plain_signature)
|
|
1993
|
-
# Read the unroll fuel WITHOUT the decrement side effect of
|
|
1994
|
-
#
|
|
1995
|
-
# thread-local is set and the stack is non-empty (the `ensure` in
|
|
1755
|
+
# Read the unroll fuel WITHOUT the decrement side effect of `unroll_fuel_remaining`: a constant-arg
|
|
1756
|
+
# unroll has begun iff the thread-local is set and the stack is non-empty (the `ensure` in
|
|
1996
1757
|
# `evaluate_guarded_user_method_body` clears it at stack-empty).
|
|
1997
1758
|
unroll_idle = stack.empty? || Thread.current[INFERENCE_UNROLL_FUEL_KEY].nil?
|
|
1998
1759
|
unroll_idle &&
|
|
1999
1760
|
stack.none? { |frame| plain_part(frame) == plain_signature }
|
|
2000
1761
|
end
|
|
2001
1762
|
|
|
2002
|
-
# Run-scoped return-memo bucket for the current discovery
|
|
2003
|
-
# generation
|
|
2004
|
-
#
|
|
2005
|
-
# index) transparently lands in a fresh bucket. See RETURN_MEMO_KEY.
|
|
1763
|
+
# Run-scoped return-memo bucket for the current discovery generation. Keyed by the identity of the
|
|
1764
|
+
# frozen `def_nodes` table so a new analysis generation (or any scope that swaps the index)
|
|
1765
|
+
# transparently lands in a fresh bucket. See RETURN_MEMO_KEY.
|
|
2006
1766
|
def return_memo_bucket
|
|
2007
1767
|
store = (Thread.current[RETURN_MEMO_KEY] ||= {}.compare_by_identity)
|
|
2008
1768
|
store[scope.discovered_def_nodes] ||= {}
|
|
2009
1769
|
end
|
|
2010
1770
|
|
|
2011
|
-
# Pushes the recursion-guard frame, evaluates the body (the outermost
|
|
2012
|
-
#
|
|
2013
|
-
#
|
|
2014
|
-
#
|
|
2015
|
-
# summary tables when the guard stack drains to empty.
|
|
1771
|
+
# Pushes the recursion-guard frame, evaluates the body (the outermost frame for a plain signature runs
|
|
1772
|
+
# the ADR-55 slice 2 fixpoint; nested extended frames evaluate once and let the owner iterate), and on
|
|
1773
|
+
# the way out pops the frame and resets the per-outermost-entry fuel and summary tables when the guard
|
|
1774
|
+
# stack drains to empty.
|
|
2016
1775
|
def evaluate_guarded_user_method_body(def_node, body_scope, stack, signature, context)
|
|
2017
|
-
# The outermost frame for this plain signature owns the summary
|
|
2018
|
-
#
|
|
1776
|
+
# The outermost frame for this plain signature owns the summary entry and runs the fixpoint loop.
|
|
1777
|
+
# ADR-55 WD2.
|
|
2019
1778
|
outermost = stack.none? { |frame| plain_part(frame) == context.plain_signature }
|
|
2020
1779
|
stack.push(signature)
|
|
2021
1780
|
begin
|
|
@@ -2035,19 +1794,16 @@ module Rigor
|
|
|
2035
1794
|
end
|
|
2036
1795
|
end
|
|
2037
1796
|
|
|
2038
|
-
# Evaluates a method body and joins the value types of every explicit
|
|
2039
|
-
#
|
|
1797
|
+
# Evaluates a method body and joins the value types of every explicit `return value` reached during
|
|
1798
|
+
# the walk with the body's tail type.
|
|
2040
1799
|
#
|
|
2041
|
-
# The tail-only evaluator (`statements_type_for` → `type_of(body.last)`)
|
|
2042
|
-
#
|
|
2043
|
-
#
|
|
2044
|
-
#
|
|
2045
|
-
#
|
|
2046
|
-
# `
|
|
2047
|
-
#
|
|
2048
|
-
# collects the returns (nested `def`/lambda are barriers; block-internal
|
|
2049
|
-
# returns correctly bubble to the enclosing method) so the inferred
|
|
2050
|
-
# return is `tail | return_1 | … | return_n`, matching Ruby semantics.
|
|
1800
|
+
# The tail-only evaluator (`statements_type_for` → `type_of(body.last)`) models only the fall-through
|
|
1801
|
+
# value; an early `return false` or a block-internal `return x` produces `Bot` at its own position and
|
|
1802
|
+
# is otherwise invisible to method-return inference. Without this join a predicate helper shaped
|
|
1803
|
+
# `return false unless cond; ...; true` infers `Constant[true]` (the early `return false` dropped),
|
|
1804
|
+
# which folds `if helper` to always-truthy. `StatementEvaluator.with_return_sink` collects the returns
|
|
1805
|
+
# (nested `def`/lambda are barriers; block-internal returns correctly bubble to the enclosing method)
|
|
1806
|
+
# so the inferred return is `tail | return_1 | … | return_n`, matching Ruby semantics.
|
|
2051
1807
|
def evaluate_body_with_returns(body_scope, body)
|
|
2052
1808
|
(type, post_scope), returns = StatementEvaluator.with_return_sink do
|
|
2053
1809
|
body_scope.evaluate(body)
|
|
@@ -2056,16 +1812,13 @@ module Rigor
|
|
|
2056
1812
|
[joined, post_scope]
|
|
2057
1813
|
end
|
|
2058
1814
|
|
|
2059
|
-
# ADR-55 slice 2 — Kleene fixpoint over a recursive method's return
|
|
2060
|
-
#
|
|
2061
|
-
# if the
|
|
2062
|
-
#
|
|
2063
|
-
#
|
|
2064
|
-
#
|
|
2065
|
-
# `
|
|
2066
|
-
# iteration widens value-pinned constituents to their nominal base to
|
|
2067
|
-
# force convergence, and any residual instability collapses to
|
|
2068
|
-
# `untyped` (today's behaviour).
|
|
1815
|
+
# ADR-55 slice 2 — Kleene fixpoint over a recursive method's return summary. Seeds the assumption to
|
|
1816
|
+
# `bot`, evaluates the body, and (only if the summary was actually consulted during evaluation — i.e.
|
|
1817
|
+
# the method really recursed) iterates: if the computed return is subsumed by the assumption the
|
|
1818
|
+
# fixpoint is reached; otherwise the assumption joins in the computed return and the body re-evaluates.
|
|
1819
|
+
# Capped at `RECURSION_FIXPOINT_CAP` total evaluations; the final permitted iteration widens
|
|
1820
|
+
# value-pinned constituents to their nominal base to force convergence, and any residual instability
|
|
1821
|
+
# collapses to `untyped` (today's behaviour).
|
|
2069
1822
|
def fixpoint_user_method_return(def_node, body_scope, context, widened: false)
|
|
2070
1823
|
plain_signature = context.plain_signature
|
|
2071
1824
|
summaries = context.summaries
|
|
@@ -2079,34 +1832,27 @@ module Rigor
|
|
|
2079
1832
|
type, = evaluate_body_with_returns(body_scope, def_node.body)
|
|
2080
1833
|
computed = clamp_unroll_result(type, context.would_have_been_guarded)
|
|
2081
1834
|
|
|
2082
|
-
# Cross-signature mutual recursion (ADR-55 soundness fix,
|
|
2083
|
-
#
|
|
2084
|
-
#
|
|
2085
|
-
# `
|
|
2086
|
-
#
|
|
2087
|
-
# The per-signature iteration below cannot converge such an
|
|
2088
|
-
# entangled pair (each side's iterate is conditioned on the
|
|
2089
|
-
# other's unfinished assumption), so degrade this frame to the
|
|
1835
|
+
# Cross-signature mutual recursion (ADR-55 soundness fix, 2026-06-12): the evaluation consulted an
|
|
1836
|
+
# ANCESTOR signature's in-flight summary (seed depth shallower than this frame's), so `computed`
|
|
1837
|
+
# embeds a transient foreign Kleene iterate -- e.g. `odd?` folding `even?`'s seeded `bot` into
|
|
1838
|
+
# `Constant[false]`. The per-signature iteration below cannot converge such an entangled pair (each
|
|
1839
|
+
# side's iterate is conditioned on the other's unfinished assumption), so degrade this frame to the
|
|
2090
1840
|
# sound `untyped` floor instead of surfacing a one-sided value.
|
|
2091
1841
|
if consult_depths[consult_mark..].any? { |d| d < depth }
|
|
2092
1842
|
return degrade_entangled_fixpoint(summaries, plain_signature)
|
|
2093
1843
|
end
|
|
2094
1844
|
|
|
2095
|
-
# The summary was never consulted — the method did not recurse on
|
|
2096
|
-
#
|
|
2097
|
-
# computed type directly (pre-fixpoint behaviour for non-recursive
|
|
1845
|
+
# The summary was never consulted — the method did not recurse on this evaluation, so there is no
|
|
1846
|
+
# fixpoint to chase. Return the computed type directly (pre-fixpoint behaviour for non-recursive
|
|
2098
1847
|
# bodies that merely share `infer_user_method_return`).
|
|
2099
1848
|
return computed unless summaries.dig(plain_signature, :consulted)
|
|
2100
1849
|
|
|
2101
|
-
# ADR-55 slice 2 bot-collapse fix (2026-06-11). When the recursive
|
|
2102
|
-
#
|
|
2103
|
-
#
|
|
2104
|
-
#
|
|
2105
|
-
#
|
|
2106
|
-
#
|
|
2107
|
-
# `nil`). `bot` means "never returns", which feeds ADR-47
|
|
2108
|
-
# reachability / always-falsey diagnostics, so it must be reserved
|
|
2109
|
-
# for genuinely diverging methods (`spin`).
|
|
1850
|
+
# ADR-55 slice 2 bot-collapse fix (2026-06-11). When the recursive method's only contribution this
|
|
1851
|
+
# evaluation was the seeded `bot` assumption (so `computed` is `bot` even though the body
|
|
1852
|
+
# recursed), the `joined == assumption` check below would trivially converge at the seed and
|
|
1853
|
+
# return `bot` — UNSOUND for a method with a reachable non-recursive exit (`passthrough` returns
|
|
1854
|
+
# `:done`, `pick` returns `nil`). `bot` means "never returns", which feeds ADR-47 reachability /
|
|
1855
|
+
# always-falsey diagnostics, so it must be reserved for genuinely diverging methods (`spin`).
|
|
2110
1856
|
if computed.is_a?(Type::Bot)
|
|
2111
1857
|
resolved = resolve_bot_collapse(def_node, context, widened: widened)
|
|
2112
1858
|
return resolved unless resolved.nil?
|
|
@@ -2117,10 +1863,9 @@ module Rigor
|
|
|
2117
1863
|
end
|
|
2118
1864
|
end
|
|
2119
1865
|
|
|
2120
|
-
# Seeds the thread-local summary entry for a fixpoint owner: the `bot`
|
|
2121
|
-
#
|
|
2122
|
-
#
|
|
2123
|
-
# nested fixpoints can detect a foreign in-flight (ancestor) consult.
|
|
1866
|
+
# Seeds the thread-local summary entry for a fixpoint owner: the `bot` Kleene seed plus the
|
|
1867
|
+
# guard-stack depth at seed time (the frame for this signature is already pushed), which
|
|
1868
|
+
# `consult_summary` logs so nested fixpoints can detect a foreign in-flight (ancestor) consult.
|
|
2124
1869
|
# Returns the seed depth. ADR-55 slice 2.
|
|
2125
1870
|
def seed_fixpoint_summary(summaries, plain_signature)
|
|
2126
1871
|
depth = (Thread.current[INFERENCE_GUARD_KEY] || []).size
|
|
@@ -2130,9 +1875,8 @@ module Rigor
|
|
|
2130
1875
|
depth
|
|
2131
1876
|
end
|
|
2132
1877
|
|
|
2133
|
-
# Degrades an entangled mutual-recursion fixpoint to the sound
|
|
2134
|
-
#
|
|
2135
|
-
# parking `untyped` in the assumption so any consumer that still reads
|
|
1878
|
+
# Degrades an entangled mutual-recursion fixpoint to the sound `untyped` floor (ADR-55 mutual-recursion
|
|
1879
|
+
# soundness fix, 2026-06-12), parking `untyped` in the assumption so any consumer that still reads
|
|
2136
1880
|
# this signature's summary sees the floor, not the stale `bot` seed.
|
|
2137
1881
|
def degrade_entangled_fixpoint(summaries, plain_signature)
|
|
2138
1882
|
BudgetTrace.hit(BudgetTrace::RECURSION_GUARD)
|
|
@@ -2141,12 +1885,10 @@ module Rigor
|
|
|
2141
1885
|
Type::Combinator.untyped
|
|
2142
1886
|
end
|
|
2143
1887
|
|
|
2144
|
-
# One Kleene-iteration step of the fixpoint loop. Joins `computed` into
|
|
2145
|
-
#
|
|
2146
|
-
#
|
|
2147
|
-
#
|
|
2148
|
-
# `:continue` to request another body evaluation, having advanced the
|
|
2149
|
-
# stored assumption. ADR-55 WD2.
|
|
1888
|
+
# One Kleene-iteration step of the fixpoint loop. Joins `computed` into the running assumption
|
|
1889
|
+
# (widening value-pinned constituents on the final permitted iteration to force convergence) and
|
|
1890
|
+
# either returns a final type — convergence, or the capped `untyped` collapse — or `:continue` to
|
|
1891
|
+
# request another body evaluation, having advanced the stored assumption. ADR-55 WD2.
|
|
2150
1892
|
def fixpoint_step(summaries, plain_signature, computed, iteration)
|
|
2151
1893
|
assumption = summaries[plain_signature][:assumption]
|
|
2152
1894
|
last_iteration = iteration == RECURSION_FIXPOINT_CAP - 1
|
|
@@ -2170,36 +1912,31 @@ module Rigor
|
|
|
2170
1912
|
:continue
|
|
2171
1913
|
end
|
|
2172
1914
|
|
|
2173
|
-
# Rebuilds the user-method body scope with every bound positional
|
|
2174
|
-
#
|
|
2175
|
-
# `
|
|
2176
|
-
#
|
|
2177
|
-
#
|
|
2178
|
-
# view under which the base case is reachable. Returns `nil` when the
|
|
2179
|
-
# parameter shape is not inferable (mirrors `build_user_method_body_scope`).
|
|
1915
|
+
# Rebuilds the user-method body scope with every bound positional parameter widened to its nominal
|
|
1916
|
+
# base (`1 | 2 | 3` → `Integer`, `Constant[:x]` → `Symbol`). Used by the bot-collapse retry in
|
|
1917
|
+
# `fixpoint_user_method_return`: call-site argument narrowing can prune a recursive method's base
|
|
1918
|
+
# case, and widening restores the declared-type view under which the base case is reachable. Returns
|
|
1919
|
+
# `nil` when the parameter shape is not inferable (mirrors `build_user_method_body_scope`).
|
|
2180
1920
|
def widened_user_method_body_scope(def_node, receiver, arg_types)
|
|
2181
1921
|
widened_args = arg_types.map { |arg_type| widen_value_pinned(arg_type) }
|
|
2182
1922
|
build_user_method_body_scope(def_node, receiver, widened_args)
|
|
2183
1923
|
end
|
|
2184
1924
|
|
|
2185
|
-
# ADR-55 slice 2 bot-collapse resolution (2026-06-11). Called when a
|
|
2186
|
-
#
|
|
2187
|
-
# hatches keep `bot` reserved for genuinely diverging methods:
|
|
1925
|
+
# ADR-55 slice 2 bot-collapse resolution (2026-06-11). Called when a fixpoint iteration computed `bot`
|
|
1926
|
+
# for a recursive body. Two escape hatches keep `bot` reserved for genuinely diverging methods:
|
|
2188
1927
|
#
|
|
2189
|
-
# 1. Re-run the fixpoint ONCE over a parameter-widened body scope
|
|
2190
|
-
#
|
|
2191
|
-
# a
|
|
2192
|
-
#
|
|
2193
|
-
# constituent (`:done`) surfaces. `passthrough` recovers here.
|
|
1928
|
+
# 1. Re-run the fixpoint ONCE over a parameter-widened body scope (`1 | 2 | 3` → `Integer`):
|
|
1929
|
+
# call-site argument narrowing can prune a base-case *tail* branch (`n <= 0 ? :done : recurse`
|
|
1930
|
+
# with a positive-only `n`), and widening un-prunes it so the base constituent (`:done`) surfaces.
|
|
1931
|
+
# `passthrough` recovers here.
|
|
2194
1932
|
#
|
|
2195
|
-
# 2. If the (possibly widened) body STILL computes `bot` but contains
|
|
2196
|
-
#
|
|
2197
|
-
#
|
|
2198
|
-
#
|
|
2199
|
-
# observable) rather than the unsound `bot`.
|
|
1933
|
+
# 2. If the (possibly widened) body STILL computes `bot` but contains a reachable explicit `return`
|
|
1934
|
+
# — whose value the tail-only body evaluator never folds into the result (`pick`'s `return nil`)
|
|
1935
|
+
# — fall to the conservative `Dynamic[top]` floor (the pre-slice-2 observable) rather than the
|
|
1936
|
+
# unsound `bot`.
|
|
2200
1937
|
#
|
|
2201
|
-
# Returns the resolved type, or `nil` to let the caller's normal
|
|
2202
|
-
#
|
|
1938
|
+
# Returns the resolved type, or `nil` to let the caller's normal fixpoint convergence proceed (genuine
|
|
1939
|
+
# divergence — `spin`).
|
|
2203
1940
|
def resolve_bot_collapse(def_node, context, widened:)
|
|
2204
1941
|
unless widened
|
|
2205
1942
|
widened_scope = widened_user_method_body_scope(def_node, context.receiver, context.arg_types)
|
|
@@ -2211,13 +1948,11 @@ module Rigor
|
|
|
2211
1948
|
nil
|
|
2212
1949
|
end
|
|
2213
1950
|
|
|
2214
|
-
# True when `node` contains a reachable explicit `return` statement —
|
|
2215
|
-
#
|
|
2216
|
-
#
|
|
2217
|
-
#
|
|
2218
|
-
#
|
|
2219
|
-
# looks like it only diverges. This detector is the signal that such a
|
|
2220
|
-
# method has a non-recursive exit, so its bot-collapse must floor to
|
|
1951
|
+
# True when `node` contains a reachable explicit `return` statement — one not nested inside a return
|
|
1952
|
+
# barrier (`def` / lambda / block). The tail-only body evaluator in `infer_user_method_return` never
|
|
1953
|
+
# folds an early-return value into the method result, so a recursive method whose base case is
|
|
1954
|
+
# spelled as `return value` (rather than a tail branch) looks like it only diverges. This detector is
|
|
1955
|
+
# the signal that such a method has a non-recursive exit, so its bot-collapse must floor to
|
|
2221
1956
|
# `Dynamic[top]` rather than `bot` (ADR-55 slice 2, 2026-06-11).
|
|
2222
1957
|
RETURN_BARRIER_NODES = [Prism::DefNode, Prism::LambdaNode, Prism::BlockNode].freeze
|
|
2223
1958
|
private_constant :RETURN_BARRIER_NODES
|
|
@@ -2230,11 +1965,10 @@ module Rigor
|
|
|
2230
1965
|
node.compact_child_nodes.any? { |child| body_has_explicit_return?(child) }
|
|
2231
1966
|
end
|
|
2232
1967
|
|
|
2233
|
-
# Returns the current assumed summary for `plain_signature`, recording
|
|
2234
|
-
#
|
|
2235
|
-
#
|
|
2236
|
-
#
|
|
2237
|
-
# entry, which is the pre-slice-2 observable.
|
|
1968
|
+
# Returns the current assumed summary for `plain_signature`, recording that it was consulted (so the
|
|
1969
|
+
# fixpoint owner knows the body actually recursed). Falls back to `untyped` when no summary is active
|
|
1970
|
+
# — e.g. a nested extended frame guarded before its plain signature seeded an entry, which is the
|
|
1971
|
+
# pre-slice-2 observable.
|
|
2238
1972
|
def consult_summary(summaries, plain_signature)
|
|
2239
1973
|
entry = summaries[plain_signature]
|
|
2240
1974
|
return Type::Combinator.untyped if entry.nil?
|
|
@@ -2244,46 +1978,38 @@ module Rigor
|
|
|
2244
1978
|
entry[:assumption]
|
|
2245
1979
|
end
|
|
2246
1980
|
|
|
2247
|
-
# ADR-55 WD1 governing-rule clamp. When the just-evaluated frame
|
|
2248
|
-
#
|
|
2249
|
-
#
|
|
2250
|
-
#
|
|
2251
|
-
# byte-identical to the plain guard's `untyped` (and counts a
|
|
2252
|
-
# `RECURSION_GUARD` hit, matching the pre-slice-1 observable).
|
|
1981
|
+
# ADR-55 WD1 governing-rule clamp. When the just-evaluated frame took the extended (value-keyed) path
|
|
1982
|
+
# but its plain signature was already guarded (`would_have_been_guarded`), the unroll may only surface
|
|
1983
|
+
# a fully value-pinned result; any other outcome must be byte-identical to the plain guard's `untyped`
|
|
1984
|
+
# (and counts a `RECURSION_GUARD` hit, matching the pre-slice-1 observable).
|
|
2253
1985
|
def clamp_unroll_result(type, would_have_been_guarded)
|
|
2254
1986
|
return type unless would_have_been_guarded && !fully_value_pinned?(type)
|
|
2255
1987
|
|
|
2256
1988
|
BudgetTrace.hit(BudgetTrace::RECURSION_GUARD)
|
|
2257
1989
|
scope.record_dynamic_origin(@typing_node, DynamicOrigin::ANALYZER_BUDGET_CUTOFF) if @typing_node
|
|
2258
|
-
# ADR-55 WD1 clamp: a guarded extended frame whose body is non-pinned
|
|
2259
|
-
#
|
|
2260
|
-
#
|
|
2261
|
-
#
|
|
2262
|
-
#
|
|
2263
|
-
#
|
|
2264
|
-
# summary-composition applies to the in-cycle guard and fuel paths,
|
|
2265
|
-
# which DO return the assumed summary — see `consult_summary`.)
|
|
1990
|
+
# ADR-55 WD1 clamp: a guarded extended frame whose body is non-pinned must be byte-identical to the
|
|
1991
|
+
# plain guard's `untyped`. This path deliberately does NOT route to the in-progress fixpoint summary:
|
|
1992
|
+
# the summary is a Kleene lower bound mid-iteration, while the clamp is a soundness backstop for an
|
|
1993
|
+
# untrustworthy unrolled value, so it must stay the conservative `untyped` upper bound. (WD4's
|
|
1994
|
+
# summary-composition applies to the in-cycle guard and fuel paths, which DO return the assumed
|
|
1995
|
+
# summary — see `consult_summary`.)
|
|
2266
1996
|
Type::Combinator.untyped
|
|
2267
1997
|
end
|
|
2268
1998
|
|
|
2269
|
-
# Widens every value-pinned constituent of `type` to its nominal base
|
|
2270
|
-
#
|
|
2271
|
-
#
|
|
2272
|
-
#
|
|
2273
|
-
# tower of distinct constant iterates collapses to one nominal type.
|
|
1999
|
+
# Widens every value-pinned constituent of `type` to its nominal base (`Constant[1]` → `Integer`,
|
|
2000
|
+
# `Tuple[Constant…]` → its element bases), leaving non-pinned constituents untouched. Used on the
|
|
2001
|
+
# fixpoint's final permitted iteration (ADR-55 WD2) to force convergence — the tower of distinct
|
|
2002
|
+
# constant iterates collapses to one nominal type.
|
|
2274
2003
|
def widen_value_pinned(type)
|
|
2275
2004
|
Type::Combinator.widen_value_pinned(type)
|
|
2276
2005
|
end
|
|
2277
2006
|
|
|
2278
|
-
# Consumes one unit from the thread-local unroll-fuel counter and
|
|
2279
|
-
#
|
|
2280
|
-
#
|
|
2281
|
-
#
|
|
2282
|
-
#
|
|
2283
|
-
#
|
|
2284
|
-
# drains back to empty. On exhaustion (return 0) it records a
|
|
2285
|
-
# `RECURSION_UNROLL_FUEL` hit so the caller keeps the plain
|
|
2286
|
-
# `(receiver, method)` signature — today's behaviour.
|
|
2007
|
+
# Consumes one unit from the thread-local unroll-fuel counter and returns the units that were
|
|
2008
|
+
# available *before* this consumption (so a positive return means the extended value-key may be used).
|
|
2009
|
+
# Fuel is per-outermost inference entry: at the top level (empty guard stack) it seeds to
|
|
2010
|
+
# `RECURSION_UNROLL_FUEL`, and the `ensure` in `infer_user_method_return` clears it once the stack
|
|
2011
|
+
# drains back to empty. On exhaustion (return 0) it records a `RECURSION_UNROLL_FUEL` hit so the caller
|
|
2012
|
+
# keeps the plain `(receiver, method)` signature — today's behaviour.
|
|
2287
2013
|
def unroll_fuel_remaining(stack)
|
|
2288
2014
|
remaining = Thread.current[INFERENCE_UNROLL_FUEL_KEY]
|
|
2289
2015
|
remaining = RECURSION_UNROLL_FUEL if remaining.nil? || stack.empty?
|
|
@@ -2295,12 +2021,11 @@ module Rigor
|
|
|
2295
2021
|
remaining
|
|
2296
2022
|
end
|
|
2297
2023
|
|
|
2298
|
-
# A stable, hashable descriptor of the argument values when EVERY
|
|
2299
|
-
#
|
|
2300
|
-
#
|
|
2301
|
-
#
|
|
2302
|
-
#
|
|
2303
|
-
# exceeds `RECURSION_VALUE_SIZE_CAP` (value blow-up → fall back).
|
|
2024
|
+
# A stable, hashable descriptor of the argument values when EVERY element of `arg_types` is
|
|
2025
|
+
# value-pinned: a `Type::Constant`, or a `Type::Tuple` whose elements are (recursively) all
|
|
2026
|
+
# value-pinned. Returns nil when any argument is not value-pinned (the ordinary type-keyed path) or
|
|
2027
|
+
# when any pinned value's structural size exceeds `RECURSION_VALUE_SIZE_CAP` (value blow-up → fall
|
|
2028
|
+
# back).
|
|
2304
2029
|
def constant_argument_value_key(arg_types)
|
|
2305
2030
|
return nil if arg_types.empty?
|
|
2306
2031
|
|
|
@@ -2316,10 +2041,9 @@ module Rigor
|
|
|
2316
2041
|
keys.map(&:first)
|
|
2317
2042
|
end
|
|
2318
2043
|
|
|
2319
|
-
# Returns `[descriptor, structural_size]` for a value-pinned type,
|
|
2320
|
-
#
|
|
2321
|
-
#
|
|
2322
|
-
# the frame without a deep walk; tuples recurse.
|
|
2044
|
+
# Returns `[descriptor, structural_size]` for a value-pinned type, or nil for anything else. Strings
|
|
2045
|
+
# count by a cheap length proxy (length > 256 ≈ 64+ nodes) so a long built string disqualifies the
|
|
2046
|
+
# frame without a deep walk; tuples recurse.
|
|
2323
2047
|
def pinned_value_descriptor(arg)
|
|
2324
2048
|
case arg
|
|
2325
2049
|
when Type::Constant
|
|
@@ -2340,35 +2064,27 @@ module Rigor
|
|
|
2340
2064
|
end
|
|
2341
2065
|
end
|
|
2342
2066
|
|
|
2343
|
-
# Builds the body scope for a user-defined instance
|
|
2344
|
-
#
|
|
2345
|
-
#
|
|
2346
|
-
#
|
|
2347
|
-
#
|
|
2348
|
-
# caller does), and required positional parameters
|
|
2349
|
-
# bound from the call's `arg_types` by index. Returns
|
|
2350
|
-
# nil when the parameter shape is too complex for the
|
|
2351
|
-
# first-iteration binder (rest args, keyword args,
|
|
2352
|
-
# block params, etc.).
|
|
2067
|
+
# Builds the body scope for a user-defined instance method call: a fresh `Scope` with `self_type` set
|
|
2068
|
+
# to the receiver's nominal type, the project-wide accumulators inherited (so the body sees the same
|
|
2069
|
+
# `discovered_classes` / `class_ivars` / etc. the caller does), and required positional parameters
|
|
2070
|
+
# bound from the call's `arg_types` by index. Returns nil when the parameter shape is too complex for
|
|
2071
|
+
# the first-iteration binder (rest args, keyword args, block params, etc.).
|
|
2353
2072
|
def build_user_method_body_scope(def_node, receiver, arg_types)
|
|
2354
2073
|
params = def_node.parameters
|
|
2355
2074
|
required = params&.requireds || []
|
|
2356
2075
|
return nil unless params.nil? || user_method_param_shape_simple?(params)
|
|
2357
2076
|
return nil unless required.size == arg_types.size
|
|
2358
2077
|
|
|
2359
|
-
# Bind required positionals by index. The body scope starts from an
|
|
2360
|
-
#
|
|
2361
|
-
# narrowing invalidations would be no-ops here — build the locals
|
|
2078
|
+
# Bind required positionals by index. The body scope starts from an empty fact store and narrowing
|
|
2079
|
+
# set, so `with_local`'s fact / narrowing invalidations would be no-ops here — build the locals
|
|
2362
2080
|
# table directly (matching `with_local`'s `name.to_sym` key).
|
|
2363
2081
|
locals = {}
|
|
2364
2082
|
required.each_with_index { |param, index| locals[param.name.to_sym] = arg_types[index] }
|
|
2365
2083
|
|
|
2366
|
-
# Construct the body scope in a SINGLE allocation — the previous
|
|
2367
|
-
#
|
|
2368
|
-
#
|
|
2369
|
-
#
|
|
2370
|
-
# the hand-copied per-field list this replaces had silently dropped
|
|
2371
|
-
# `data_member_layouts` and `discovered_method_visibilities`.
|
|
2084
|
+
# Construct the body scope in a SINGLE allocation — the previous `Scope.empty.with_*.with_*…` chain
|
|
2085
|
+
# allocated a fresh frozen Scope per field, run per user-method-call inference (ADR-44). The
|
|
2086
|
+
# discovery index is inherited whole by reference (ADR-53 Track A); the hand-copied per-field list
|
|
2087
|
+
# this replaces had silently dropped `data_member_layouts` and `discovered_method_visibilities`.
|
|
2372
2088
|
Scope.new(
|
|
2373
2089
|
environment: scope.environment,
|
|
2374
2090
|
locals: locals.freeze,
|
|
@@ -2379,10 +2095,9 @@ module Rigor
|
|
|
2379
2095
|
)
|
|
2380
2096
|
end
|
|
2381
2097
|
|
|
2382
|
-
# ADR-48 Struct slice 3 — the fold-safe-local set for a method body
|
|
2383
|
-
#
|
|
2384
|
-
#
|
|
2385
|
-
# are resolved through the discovery side-table the body scope inherits.
|
|
2098
|
+
# ADR-48 Struct slice 3 — the fold-safe-local set for a method body (runs only on a return-memo miss,
|
|
2099
|
+
# so the per-call cost is bounded — measured perf-neutral). Struct member layouts of constant
|
|
2100
|
+
# receivers are resolved through the discovery side-table the body scope inherits.
|
|
2386
2101
|
def struct_fold_safe_locals_for(body)
|
|
2387
2102
|
StructFoldSafety.fold_safe_locals(
|
|
2388
2103
|
body,
|
|
@@ -2390,10 +2105,8 @@ module Rigor
|
|
|
2390
2105
|
)
|
|
2391
2106
|
end
|
|
2392
2107
|
|
|
2393
|
-
# First iteration accepts only required positional
|
|
2394
|
-
#
|
|
2395
|
-
# keyword params, and block params disqualify the
|
|
2396
|
-
# method from inference (the caller observes
|
|
2108
|
+
# First iteration accepts only required positional parameters: `def foo(a, b, c)`. Optionals, rest,
|
|
2109
|
+
# keyword params, and block params disqualify the method from inference (the caller observes
|
|
2397
2110
|
# `Dynamic[Top]` instead).
|
|
2398
2111
|
def user_method_param_shape_simple?(params)
|
|
2399
2112
|
return false unless params.is_a?(Prism::ParametersNode)
|
|
@@ -2405,16 +2118,12 @@ module Rigor
|
|
|
2405
2118
|
params.block.nil?
|
|
2406
2119
|
end
|
|
2407
2120
|
|
|
2408
|
-
# Slice A-engine. Implicit-self calls (no `node.receiver`)
|
|
2409
|
-
#
|
|
2410
|
-
#
|
|
2411
|
-
#
|
|
2412
|
-
#
|
|
2413
|
-
#
|
|
2414
|
-
# MUST default to `Nominal[Object]` so Kernel intrinsics
|
|
2415
|
-
# like `require`, `require_relative`, `raise`, and `puts`
|
|
2416
|
-
# dispatch through Object/Kernel rather than falling through
|
|
2417
|
-
# to `Dynamic[Top]`.
|
|
2121
|
+
# Slice A-engine. Implicit-self calls (no `node.receiver`) adopt the surrounding scope's `self_type`
|
|
2122
|
+
# as their receiver so calls like `attr_reader_method_name` or `private_helper(...)` inside an
|
|
2123
|
+
# instance method dispatch against the enclosing class. Slice 7 phase 10 — when `self_type` is nil
|
|
2124
|
+
# (top-level program), the receiver MUST default to `Nominal[Object]` so Kernel intrinsics like
|
|
2125
|
+
# `require`, `require_relative`, `raise`, and `puts` dispatch through Object/Kernel rather than
|
|
2126
|
+
# falling through to `Dynamic[Top]`.
|
|
2418
2127
|
def call_receiver_type_for(node)
|
|
2419
2128
|
return type_of(node.receiver) if node.receiver
|
|
2420
2129
|
|
|
@@ -2432,28 +2141,20 @@ module Rigor
|
|
|
2432
2141
|
arguments_node.arguments.map { |argument| type_of(argument) }
|
|
2433
2142
|
end
|
|
2434
2143
|
|
|
2435
|
-
# When the call carries a `Prism::BlockNode`, build the block's
|
|
2436
|
-
#
|
|
2437
|
-
# the
|
|
2438
|
-
#
|
|
2439
|
-
#
|
|
2440
|
-
#
|
|
2441
|
-
#
|
|
2442
|
-
# the call has no block, when the receiver is unknown, or
|
|
2443
|
-
# when typing the body raises (defensive against malformed
|
|
2444
|
-
# subtrees); the dispatcher then runs in its no-block-aware
|
|
2445
|
-
# path.
|
|
2144
|
+
# When the call carries a `Prism::BlockNode`, build the block's entry scope (outer locals plus
|
|
2145
|
+
# parameter bindings driven by the receiving method's RBS signature), type the block body under that
|
|
2146
|
+
# scope, and return the body's value type. The result feeds `MethodDispatcher.dispatch`'s
|
|
2147
|
+
# `block_type:` so generic methods like `Array#map[U] { (Elem) -> U } -> Array[U]` resolve `U` to the
|
|
2148
|
+
# block's return type. Returns `nil` when the call has no block, when the receiver is unknown, or when
|
|
2149
|
+
# typing the body raises (defensive against malformed subtrees); the dispatcher then runs in its
|
|
2150
|
+
# no-block-aware path.
|
|
2446
2151
|
#
|
|
2447
|
-
# ADR-14 gap-#3 (d): a `Prism::BlockArgumentNode` carrying
|
|
2448
|
-
#
|
|
2449
|
-
#
|
|
2450
|
-
#
|
|
2451
|
-
#
|
|
2452
|
-
#
|
|
2453
|
-
# failure step falls back to `Dynamic[Top]` so the
|
|
2454
|
-
# dispatcher still SEES a block — selecting the block-
|
|
2455
|
-
# bearing overload of e.g. `Hash#transform_values` over
|
|
2456
|
-
# the no-block overload that returns `Enumerator`.
|
|
2152
|
+
# ADR-14 gap-#3 (d): a `Prism::BlockArgumentNode` carrying `&:symbol` (the Symbol#to_proc shorthand) is
|
|
2153
|
+
# treated as a block. The block's return type is computed by dispatching `:symbol` on the expected
|
|
2154
|
+
# block param type (per `Symbol#to_proc`'s `{ |x| x.symbol }` semantics). A precise inner dispatch
|
|
2155
|
+
# produces the right return; any failure step falls back to `Dynamic[Top]` so the dispatcher still
|
|
2156
|
+
# SEES a block — selecting the block-bearing overload of e.g. `Hash#transform_values` over the
|
|
2157
|
+
# no-block overload that returns `Enumerator`.
|
|
2457
2158
|
def block_return_type_for(call_node, receiver_type, arg_types)
|
|
2458
2159
|
block_arg = call_node.block
|
|
2459
2160
|
return nil if block_arg.nil?
|
|
@@ -2465,11 +2166,9 @@ module Rigor
|
|
|
2465
2166
|
arg_types: arg_types,
|
|
2466
2167
|
environment: scope.environment
|
|
2467
2168
|
)
|
|
2468
|
-
# ADR-16 Tier A: when a registered plugin's `block_as_methods`
|
|
2469
|
-
#
|
|
2470
|
-
#
|
|
2471
|
-
# type. The narrowing is `nil` for unmatched calls, leaving
|
|
2472
|
-
# the existing scope contract unchanged.
|
|
2169
|
+
# ADR-16 Tier A: when a registered plugin's `block_as_methods` entry matches `(receiver_type,
|
|
2170
|
+
# call_node.name)`, narrow the block body's `self_type` to the receiver class's instance type. The
|
|
2171
|
+
# narrowing is `nil` for unmatched calls, leaving the existing scope contract unchanged.
|
|
2473
2172
|
narrowed_self = MacroBlockSelfType.narrow_self_type_for(
|
|
2474
2173
|
scope: scope, call_node: call_node, receiver_type: receiver_type
|
|
2475
2174
|
)
|
|
@@ -2490,15 +2189,11 @@ module Rigor
|
|
|
2490
2189
|
end
|
|
2491
2190
|
end
|
|
2492
2191
|
|
|
2493
|
-
# `&:symbol` desugars to a one-arg Proc that dispatches
|
|
2494
|
-
#
|
|
2495
|
-
#
|
|
2496
|
-
#
|
|
2497
|
-
#
|
|
2498
|
-
# dispatcher selects the block-bearing overload.
|
|
2499
|
-
# `&proc_local` / `&method(:foo)` and friends — anything
|
|
2500
|
-
# not a bare SymbolNode — still resolve to
|
|
2501
|
-
# `Dynamic[Top]` for the same block-presence signal.
|
|
2192
|
+
# `&:symbol` desugars to a one-arg Proc that dispatches `symbol` against its argument. When the param
|
|
2193
|
+
# type is known and the resulting inner dispatch is precise, this returns the precise carrier;
|
|
2194
|
+
# otherwise it returns `Dynamic[Top]` (still non-nil) so the outer dispatcher selects the
|
|
2195
|
+
# block-bearing overload. `&proc_local` / `&method(:foo)` and friends — anything not a bare
|
|
2196
|
+
# SymbolNode — still resolve to `Dynamic[Top]` for the same block-presence signal.
|
|
2502
2197
|
def symbol_block_return_type(block_arg, expected_param_types)
|
|
2503
2198
|
expression = block_arg.expression
|
|
2504
2199
|
return dynamic_top unless expression.is_a?(Prism::SymbolNode)
|
|
@@ -2525,19 +2220,14 @@ module Rigor
|
|
|
2525
2220
|
block_scope.type_of(body)
|
|
2526
2221
|
end
|
|
2527
2222
|
|
|
2528
|
-
# v0.0.6 phase 2 — per-element block fold for Tuple
|
|
2529
|
-
#
|
|
2530
|
-
# position
|
|
2531
|
-
#
|
|
2532
|
-
# results are assembled into `Tuple[U_1..U_n]`, strictly
|
|
2533
|
-
# tighter than the RBS-projected `Array[union]`.
|
|
2223
|
+
# v0.0.6 phase 2 — per-element block fold for Tuple receivers under `:map` / `:collect`. Walks every
|
|
2224
|
+
# Tuple position, binds the block parameter to that element's type, and re-types the block body. The
|
|
2225
|
+
# per-position results are assembled into `Tuple[U_1..U_n]`, strictly tighter than the RBS-projected
|
|
2226
|
+
# `Array[union]`.
|
|
2534
2227
|
#
|
|
2535
|
-
# Declines (returns nil) when the receiver is not a
|
|
2536
|
-
# `
|
|
2537
|
-
#
|
|
2538
|
-
# supported set, when block typing raises mid-loop, or
|
|
2539
|
-
# when the block has no body. The decline path leaves
|
|
2540
|
-
# the dispatch chain untouched.
|
|
2228
|
+
# Declines (returns nil) when the receiver is not a `Tuple` with at least one element, when the call
|
|
2229
|
+
# has no `Prism::BlockNode`, when the method is outside the supported set, when block typing raises
|
|
2230
|
+
# mid-loop, or when the block has no body. The decline path leaves the dispatch chain untouched.
|
|
2541
2231
|
PER_ELEMENT_TUPLE_METHODS = Set[
|
|
2542
2232
|
:map, :collect, :filter_map, :flat_map,
|
|
2543
2233
|
:select, :filter, :reject,
|
|
@@ -2551,12 +2241,10 @@ module Rigor
|
|
|
2551
2241
|
].freeze
|
|
2552
2242
|
private_constant :HASH_SHAPE_TRANSFORM_METHODS
|
|
2553
2243
|
|
|
2554
|
-
# Cardinality cap for per-element block fold over
|
|
2555
|
-
#
|
|
2556
|
-
#
|
|
2557
|
-
#
|
|
2558
|
-
# only short ranges expand into per-position folds.
|
|
2559
|
-
# Larger ranges decline so the RBS tier widens.
|
|
2244
|
+
# Cardinality cap for per-element block fold over finite-bound `Constant<Range>` receivers. Walking
|
|
2245
|
+
# `(1..1_000_000).map { … }` element-wise would balloon block-typing cost and explode the resulting
|
|
2246
|
+
# Tuple, so only short ranges expand into per-position folds. Larger ranges decline so the RBS tier
|
|
2247
|
+
# widens.
|
|
2560
2248
|
PER_ELEMENT_RANGE_LIMIT = 8
|
|
2561
2249
|
private_constant :PER_ELEMENT_RANGE_LIMIT
|
|
2562
2250
|
|
|
@@ -2573,18 +2261,14 @@ module Rigor
|
|
|
2573
2261
|
assemble_per_element_result(call_node.name, per_position, element_types)
|
|
2574
2262
|
end
|
|
2575
2263
|
|
|
2576
|
-
# Evaluates the call's block once per receiver element.
|
|
2577
|
-
# Two block shapes are supported:
|
|
2264
|
+
# Evaluates the call's block once per receiver element. Two block shapes are supported:
|
|
2578
2265
|
#
|
|
2579
|
-
# - `Prism::BlockNode` — a full `do … end` / `{ … }` block;
|
|
2580
|
-
#
|
|
2581
|
-
#
|
|
2582
|
-
# -
|
|
2583
|
-
# the `&:predicate` shorthand; the symbol is dispatched
|
|
2584
|
-
# as a zero-arg method on each element type.
|
|
2266
|
+
# - `Prism::BlockNode` — a full `do … end` / `{ … }` block; the body is re-typed per position with the
|
|
2267
|
+
# element bound to the block parameter.
|
|
2268
|
+
# - `Prism::BlockArgumentNode` wrapping a `SymbolNode` — the `&:predicate` shorthand; the symbol is
|
|
2269
|
+
# dispatched as a zero-arg method on each element type.
|
|
2585
2270
|
#
|
|
2586
|
-
# Any other shape (`&proc_local`, `&method(:foo)`, no
|
|
2587
|
-
# block) returns `nil` so the fold declines.
|
|
2271
|
+
# Any other shape (`&proc_local`, `&method(:foo)`, no block) returns `nil` so the fold declines.
|
|
2588
2272
|
def per_element_block_results(block, element_types)
|
|
2589
2273
|
case block
|
|
2590
2274
|
when Prism::BlockNode
|
|
@@ -2613,20 +2297,17 @@ module Rigor
|
|
|
2613
2297
|
nil
|
|
2614
2298
|
end
|
|
2615
2299
|
|
|
2616
|
-
# Returns the per-position element types for a finite,
|
|
2617
|
-
#
|
|
2618
|
-
# receiver does not pin a finite element list.
|
|
2300
|
+
# Returns the per-position element types for a finite, statically-known receiver shape — or nil when
|
|
2301
|
+
# the receiver does not pin a finite element list.
|
|
2619
2302
|
#
|
|
2620
2303
|
# `Tuple[A, B, …]` → [A, B, …]
|
|
2621
2304
|
# `Constant<a..b>` → [Constant[a], …, Constant[b]]
|
|
2622
2305
|
# everything else → nil
|
|
2623
2306
|
#
|
|
2624
|
-
# Note: `Type::IntegerRange` is the bounded-Integer
|
|
2625
|
-
#
|
|
2626
|
-
#
|
|
2627
|
-
#
|
|
2628
|
-
# to `Integer#map` / `Integer#find` — neither exists —
|
|
2629
|
-
# so IntegerRange does NOT participate in this fold.
|
|
2307
|
+
# Note: `Type::IntegerRange` is the bounded-Integer carrier (`int<a, b>` represents "an Integer between
|
|
2308
|
+
# a and b"), not a Range value. Calls like `.map` / `.find` on an `IntegerRange` receiver would resolve
|
|
2309
|
+
# to `Integer#map` / `Integer#find` — neither exists — so IntegerRange does NOT participate in this
|
|
2310
|
+
# fold.
|
|
2630
2311
|
def per_element_elements_of(receiver_type)
|
|
2631
2312
|
case receiver_type
|
|
2632
2313
|
when Type::Tuple then receiver_type.elements
|
|
@@ -2647,47 +2328,38 @@ module Rigor
|
|
|
2647
2328
|
INJECT_METHODS = Set[:inject, :reduce].freeze
|
|
2648
2329
|
private_constant :INJECT_METHODS
|
|
2649
2330
|
|
|
2650
|
-
# Cap on the element count for the Part 2 constant-threading
|
|
2651
|
-
#
|
|
2652
|
-
#
|
|
2653
|
-
# without materialising.
|
|
2331
|
+
# Cap on the element count for the Part 2 constant-threading fold — mirrors
|
|
2332
|
+
# `ReduceFolding::CONSTANT_FOLD_ELEMENT_CAP`. The size is checked BEFORE enumeration so `(1..1_000_000)`
|
|
2333
|
+
# declines without materialising.
|
|
2654
2334
|
INJECT_CONSTANT_ELEMENT_CAP = 64
|
|
2655
2335
|
private_constant :INJECT_CONSTANT_ELEMENT_CAP
|
|
2656
2336
|
|
|
2657
|
-
# Magnitude cap on a folded Integer accumulator — mirrors
|
|
2658
|
-
#
|
|
2659
|
-
#
|
|
2660
|
-
# bignum literal in the type graph.
|
|
2337
|
+
# Magnitude cap on a folded Integer accumulator — mirrors `ReduceFolding`'s bit cap so factorial-style
|
|
2338
|
+
# blow-up declines to the Part 1 nominal result rather than parking a heavy bignum literal in the type
|
|
2339
|
+
# graph.
|
|
2661
2340
|
INJECT_CONSTANT_BIT_CAP = 256
|
|
2662
2341
|
private_constant :INJECT_CONSTANT_BIT_CAP
|
|
2663
2342
|
|
|
2664
2343
|
# Block-form `inject` / `reduce` return-type fold.
|
|
2665
2344
|
#
|
|
2666
|
-
# Part 1 (soundness): the accumulator of a block-form fold must
|
|
2667
|
-
#
|
|
2668
|
-
#
|
|
2669
|
-
#
|
|
2670
|
-
# `
|
|
2671
|
-
# runtime is 120 (out of range — unsound). We iterate the
|
|
2672
|
-
# accumulator type to a capped fixpoint (ADR-55/56 `BodyFixpoint`)
|
|
2673
|
-
# so the multiply converges to `Integer`, never a value-bounded
|
|
2345
|
+
# Part 1 (soundness): the accumulator of a block-form fold must reach a fixpoint over an unknown
|
|
2346
|
+
# number of iterations — the RBS tier's generic `(S) { (S, E) -> S } -> S` binds `S` from a SINGLE
|
|
2347
|
+
# block pass (acc=seed, elem=element-join), so `(1..5).inject(1) { |a, i| a * i }` types `int<1, 5>`
|
|
2348
|
+
# while the runtime is 120 (out of range — unsound). We iterate the accumulator type to a capped
|
|
2349
|
+
# fixpoint (ADR-55/56 `BodyFixpoint`) so the multiply converges to `Integer`, never a value-bounded
|
|
2674
2350
|
# interval the runtime escapes.
|
|
2675
2351
|
#
|
|
2676
|
-
# Part 2 (precision): when the receiver is a fully-constant
|
|
2677
|
-
#
|
|
2678
|
-
#
|
|
2679
|
-
# block
|
|
2680
|
-
#
|
|
2681
|
-
# through per-element block evaluation and return the final
|
|
2682
|
-
# `Constant` (`(1..5).inject(1) { |a, i| a * i } -> 120`).
|
|
2352
|
+
# Part 2 (precision): when the receiver is a fully-constant finite collection (`Constant[Range]` /
|
|
2353
|
+
# `Tuple` of `Constant`), the seed is `Constant` (or the no-seed first element), and the block body
|
|
2354
|
+
# folds to a `Constant` on EVERY iteration with the running accumulator + element bound, thread the
|
|
2355
|
+
# accumulator through per-element block evaluation and return the final `Constant` (`(1..5).inject(1)
|
|
2356
|
+
# { |a, i| a * i } -> 120`).
|
|
2683
2357
|
#
|
|
2684
|
-
# The two are layered: Part 2 is attempted first (a constant
|
|
2685
|
-
#
|
|
2686
|
-
#
|
|
2687
|
-
#
|
|
2688
|
-
#
|
|
2689
|
-
# a block that both accumulates and mutates captured state keeps
|
|
2690
|
-
# its write-back regardless of which arm answers here.
|
|
2358
|
+
# The two are layered: Part 2 is attempted first (a constant answer is strictly tighter); on any
|
|
2359
|
+
# decline it falls through to the Part 1 sound nominal fixpoint, and on a Part 1 decline to the RBS
|
|
2360
|
+
# tier. Captured-local write-back (ADR-56) runs at the statement level independent of this return-type
|
|
2361
|
+
# computation, so a block that both accumulates and mutates captured state keeps its write-back
|
|
2362
|
+
# regardless of which arm answers here.
|
|
2691
2363
|
#
|
|
2692
2364
|
# @return [Rigor::Type, nil]
|
|
2693
2365
|
def try_block_inject_fold(call_node, receiver, arg_types)
|
|
@@ -2705,9 +2377,8 @@ module Rigor
|
|
|
2705
2377
|
try_nominal_inject_fixpoint(receiver, block, seed, has_seed)
|
|
2706
2378
|
end
|
|
2707
2379
|
|
|
2708
|
-
# Splits the positional args into the optional seed. A Symbol
|
|
2709
|
-
#
|
|
2710
|
-
# never reaches here (the block guard already failed for it).
|
|
2380
|
+
# Splits the positional args into the optional seed. A Symbol final arg (`inject(seed, :*)`) is the
|
|
2381
|
+
# no-block Symbol form and never reaches here (the block guard already failed for it).
|
|
2711
2382
|
#
|
|
2712
2383
|
# @return [Array(Rigor::Type, nil), Boolean] `[seed, has_seed]`
|
|
2713
2384
|
def inject_seed(arg_types)
|
|
@@ -2717,10 +2388,9 @@ module Rigor
|
|
|
2717
2388
|
end
|
|
2718
2389
|
end
|
|
2719
2390
|
|
|
2720
|
-
# Part 2 — thread the accumulator through per-element block
|
|
2721
|
-
#
|
|
2722
|
-
#
|
|
2723
|
-
# cap, or any per-step result that is not a foldable `Constant`.
|
|
2391
|
+
# Part 2 — thread the accumulator through per-element block evaluation over a fully-constant finite
|
|
2392
|
+
# receiver. Declines (nil) on a non-constant receiver / seed, a size or magnitude cap, or any per-step
|
|
2393
|
+
# result that is not a foldable `Constant`.
|
|
2724
2394
|
def try_constant_inject_fold(receiver, block, seed, has_seed)
|
|
2725
2395
|
members = inject_constant_members(receiver)
|
|
2726
2396
|
return nil if members.nil?
|
|
@@ -2735,8 +2405,7 @@ module Rigor
|
|
|
2735
2405
|
acc
|
|
2736
2406
|
end
|
|
2737
2407
|
|
|
2738
|
-
# Extracts the receiver's foldable constant values, size-capped
|
|
2739
|
-
# before enumeration, or nil to decline.
|
|
2408
|
+
# Extracts the receiver's foldable constant values, size-capped before enumeration, or nil to decline.
|
|
2740
2409
|
def inject_constant_members(receiver)
|
|
2741
2410
|
case receiver
|
|
2742
2411
|
when Type::Constant then inject_constant_range_members(receiver.value)
|
|
@@ -2767,11 +2436,9 @@ module Rigor
|
|
|
2767
2436
|
elements.map(&:value)
|
|
2768
2437
|
end
|
|
2769
2438
|
|
|
2770
|
-
# Seeds the constant accumulator: with a seed the memo starts at
|
|
2771
|
-
#
|
|
2772
|
-
#
|
|
2773
|
-
# The accumulator is carried as a `Constant` type (so the block
|
|
2774
|
-
# body sees a value-pinned param).
|
|
2439
|
+
# Seeds the constant accumulator: with a seed the memo starts at the (foldable) seed value and every
|
|
2440
|
+
# member is folded; without a seed the first member seeds the memo and the rest are folded. The
|
|
2441
|
+
# accumulator is carried as a `Constant` type (so the block body sees a value-pinned param).
|
|
2775
2442
|
#
|
|
2776
2443
|
# @return [Array(Rigor::Type::Constant, nil), Array] `[acc, rest]`
|
|
2777
2444
|
def inject_constant_start(members, seed, has_seed)
|
|
@@ -2786,10 +2453,9 @@ module Rigor
|
|
|
2786
2453
|
end
|
|
2787
2454
|
end
|
|
2788
2455
|
|
|
2789
|
-
# Evaluates the block body once with the running constant
|
|
2790
|
-
#
|
|
2791
|
-
#
|
|
2792
|
-
# within the magnitude cap, else nil to decline the whole fold.
|
|
2456
|
+
# Evaluates the block body once with the running constant accumulator + the next constant element
|
|
2457
|
+
# bound to the block params, returning the result when it is a foldable `Constant` within the
|
|
2458
|
+
# magnitude cap, else nil to decline the whole fold.
|
|
2793
2459
|
def inject_constant_step(block, acc, element_value)
|
|
2794
2460
|
element = Type::Combinator.constant_of(element_value)
|
|
2795
2461
|
result = type_block_body_with_param(block, [acc, element])
|
|
@@ -2811,13 +2477,10 @@ module Rigor
|
|
|
2811
2477
|
value.is_a?(Integer) && value.bit_length > INJECT_CONSTANT_BIT_CAP
|
|
2812
2478
|
end
|
|
2813
2479
|
|
|
2814
|
-
# Part 1 — the sound nominal accumulator fixpoint. Iterates
|
|
2815
|
-
#
|
|
2816
|
-
#
|
|
2817
|
-
#
|
|
2818
|
-
# no-seed form) and binding the element-join to the element param.
|
|
2819
|
-
# Declines (nil) when the element type is unknown so the RBS tier
|
|
2820
|
-
# owns the call.
|
|
2480
|
+
# Part 1 — the sound nominal accumulator fixpoint. Iterates `acc = join(acc, block(acc, element))` to
|
|
2481
|
+
# a capped fixpoint with final `Constant -> Nominal` widening (ADR-55/56 `BodyFixpoint`), seeding `acc`
|
|
2482
|
+
# from the seed type (or the element type for the no-seed form) and binding the element-join to the
|
|
2483
|
+
# element param. Declines (nil) when the element type is unknown so the RBS tier owns the call.
|
|
2821
2484
|
def try_nominal_inject_fixpoint(receiver, block, seed, has_seed)
|
|
2822
2485
|
element = MethodDispatcher::IteratorDispatch.element_type_of(receiver)
|
|
2823
2486
|
return nil if element.nil?
|
|
@@ -2838,9 +2501,8 @@ module Rigor
|
|
|
2838
2501
|
converged[:__inject_acc__] || seed_acc
|
|
2839
2502
|
end
|
|
2840
2503
|
|
|
2841
|
-
# `index(value)` and `find_index(value)` carry a positional
|
|
2842
|
-
#
|
|
2843
|
-
# Decline so the RBS tier owns those forms.
|
|
2504
|
+
# `index(value)` and `find_index(value)` carry a positional argument and search by `==` rather than
|
|
2505
|
+
# running the block. Decline so the RBS tier owns those forms.
|
|
2844
2506
|
def find_family_with_args?(call_node)
|
|
2845
2507
|
return false unless %i[find_index index].include?(call_node.name)
|
|
2846
2508
|
|
|
@@ -2862,19 +2524,14 @@ module Rigor
|
|
|
2862
2524
|
end
|
|
2863
2525
|
end
|
|
2864
2526
|
|
|
2865
|
-
# `select` / `filter` / `reject`: keeps each receiver
|
|
2866
|
-
#
|
|
2867
|
-
#
|
|
2868
|
-
# Ruby-falsey for `reject`. The surviving elements assemble
|
|
2869
|
-
# into a `Tuple`, strictly tighter than the RBS-projected
|
|
2870
|
-
# `Array[Elem]`.
|
|
2527
|
+
# `select` / `filter` / `reject`: keeps each receiver element whose per-position predicate result
|
|
2528
|
+
# folds to a decisive `Constant` — Ruby-truthy for `select` / `filter`, Ruby-falsey for `reject`. The
|
|
2529
|
+
# surviving elements assemble into a `Tuple`, strictly tighter than the RBS-projected `Array[Elem]`.
|
|
2871
2530
|
#
|
|
2872
|
-
# Folds tightly only when EVERY position is a `Constant`:
|
|
2873
|
-
#
|
|
2874
|
-
#
|
|
2875
|
-
#
|
|
2876
|
-
# widens to `Array[Elem]`. `[].select` style empty results
|
|
2877
|
-
# are sound — an empty `Tuple` is the empty-array carrier.
|
|
2531
|
+
# Folds tightly only when EVERY position is a `Constant`: a single non-`Constant` position leaves the
|
|
2532
|
+
# result cardinality unknown (the element might or might not survive), so the dispatcher declines and
|
|
2533
|
+
# the RBS tier widens to `Array[Elem]`. `[].select` style empty results are sound — an empty `Tuple`
|
|
2534
|
+
# is the empty-array carrier.
|
|
2878
2535
|
def assemble_filter_result(per_position, element_types, keep_on_truthy:)
|
|
2879
2536
|
return nil unless per_position.all?(Type::Constant)
|
|
2880
2537
|
|
|
@@ -2884,12 +2541,9 @@ module Rigor
|
|
|
2884
2541
|
Type::Combinator.tuple_of(*kept)
|
|
2885
2542
|
end
|
|
2886
2543
|
|
|
2887
|
-
# `filter_map` folds tightly only when every per-position
|
|
2888
|
-
#
|
|
2889
|
-
#
|
|
2890
|
-
# When any position is non-Constant the dispatcher
|
|
2891
|
-
# declines (returns nil) so the RBS tier widens to
|
|
2892
|
-
# `Array[U]`.
|
|
2544
|
+
# `filter_map` folds tightly only when every per-position result is a `Constant`: positions whose
|
|
2545
|
+
# value is `nil` or `false` drop, the rest survive in declaration order. When any position is
|
|
2546
|
+
# non-Constant the dispatcher declines (returns nil) so the RBS tier widens to `Array[U]`.
|
|
2893
2547
|
def assemble_filter_map_result(per_position)
|
|
2894
2548
|
return nil unless per_position.all?(Type::Constant)
|
|
2895
2549
|
|
|
@@ -2897,18 +2551,14 @@ module Rigor
|
|
|
2897
2551
|
Type::Combinator.tuple_of(*kept)
|
|
2898
2552
|
end
|
|
2899
2553
|
|
|
2900
|
-
# `flat_map` flattens a single level: if the per-position
|
|
2901
|
-
#
|
|
2902
|
-
#
|
|
2903
|
-
#
|
|
2904
|
-
#
|
|
2905
|
-
# those two recognisable shapes — `Nominal[Array[T]]`,
|
|
2906
|
-
# `Union[…]`, and other opaque carriers decline so the
|
|
2907
|
-
# RBS tier widens to `Array[U]`.
|
|
2554
|
+
# `flat_map` flattens a single level: if the per-position result is a `Tuple`, its elements are
|
|
2555
|
+
# concatenated; if it's a non-Array scalar carrier (`Constant<…>` over a non-Array literal) it
|
|
2556
|
+
# contributes one element. We fold tightly only when every per-position result is one of those two
|
|
2557
|
+
# recognisable shapes — `Nominal[Array[T]]`, `Union[…]`, and other opaque carriers decline so the RBS
|
|
2558
|
+
# tier widens to `Array[U]`.
|
|
2908
2559
|
#
|
|
2909
|
-
# `Type::Constant` only ever holds non-Array scalars (the
|
|
2910
|
-
#
|
|
2911
|
-
# safely contributes itself as a single Tuple element.
|
|
2560
|
+
# `Type::Constant` only ever holds non-Array scalars (the carrier rejects Array literals), so a single
|
|
2561
|
+
# `Constant` safely contributes itself as a single Tuple element.
|
|
2912
2562
|
def assemble_flat_map_result(per_position)
|
|
2913
2563
|
flattened = per_position.flat_map { |type| flat_map_contribution(type) }
|
|
2914
2564
|
return nil if flattened.nil? || flattened.any?(&:nil?)
|
|
@@ -2924,16 +2574,13 @@ module Rigor
|
|
|
2924
2574
|
end
|
|
2925
2575
|
end
|
|
2926
2576
|
|
|
2927
|
-
# `find` / `detect`: returns the first receiver element
|
|
2928
|
-
#
|
|
2929
|
-
# position folds to truthy.
|
|
2577
|
+
# `find` / `detect`: returns the first receiver element whose block result is Ruby-truthy, or `nil`
|
|
2578
|
+
# when no position folds to truthy.
|
|
2930
2579
|
#
|
|
2931
|
-
# Folds tightly only when every per-position block result
|
|
2932
|
-
# is
|
|
2933
|
-
#
|
|
2934
|
-
#
|
|
2935
|
-
# the corresponding receiver element. When every position
|
|
2936
|
-
# folds to falsey, the answer is `Constant[nil]`.
|
|
2580
|
+
# Folds tightly only when every per-position block result is a `Type::Constant` — otherwise we cannot
|
|
2581
|
+
# decide which position (if any) is "the first matching one". When the first decisive truthy position
|
|
2582
|
+
# is found, the answer is the corresponding receiver element. When every position folds to falsey,
|
|
2583
|
+
# the answer is `Constant[nil]`.
|
|
2937
2584
|
def assemble_find_result(per_position, element_types)
|
|
2938
2585
|
return nil unless per_position.all?(Type::Constant)
|
|
2939
2586
|
|
|
@@ -2943,8 +2590,8 @@ module Rigor
|
|
|
2943
2590
|
element_types[first_truthy_index]
|
|
2944
2591
|
end
|
|
2945
2592
|
|
|
2946
|
-
# `find_index` / `index`: returns the index of the first
|
|
2947
|
-
#
|
|
2593
|
+
# `find_index` / `index`: returns the index of the first truthy position, or `Constant[nil]` when
|
|
2594
|
+
# nothing matches.
|
|
2948
2595
|
def assemble_find_index_result(per_position)
|
|
2949
2596
|
return nil unless per_position.all?(Type::Constant)
|
|
2950
2597
|
|
|
@@ -2958,28 +2605,22 @@ module Rigor
|
|
|
2958
2605
|
type.is_a?(Type::Constant) && type.value && type.value != false
|
|
2959
2606
|
end
|
|
2960
2607
|
|
|
2961
|
-
# Per-pair block fold for `HashShape#transform_keys` and
|
|
2962
|
-
#
|
|
2608
|
+
# Per-pair block fold for `HashShape#transform_keys` and `HashShape#transform_values` (and their bang
|
|
2609
|
+
# variants).
|
|
2963
2610
|
#
|
|
2964
|
-
# When the receiver is a closed `HashShape` with no optional
|
|
2965
|
-
#
|
|
2966
|
-
#
|
|
2967
|
-
# independently and assembles a new `HashShape`:
|
|
2611
|
+
# When the receiver is a closed `HashShape` with no optional keys, applies the call's block (a
|
|
2612
|
+
# `Prism::BlockNode` or `Prism::BlockArgumentNode`) to each key/value pair independently and
|
|
2613
|
+
# assembles a new `HashShape`:
|
|
2968
2614
|
#
|
|
2969
|
-
# - `transform_values` / `transform_values!`: re-types
|
|
2970
|
-
#
|
|
2971
|
-
#
|
|
2972
|
-
#
|
|
2973
|
-
#
|
|
2974
|
-
# the
|
|
2975
|
-
# key must be a `Constant[Symbol | String]` — otherwise
|
|
2976
|
-
# the tier declines (the new key cannot be used as a
|
|
2977
|
-
# static HashShape index). Collisions (two old keys
|
|
2978
|
-
# mapping to the same new key) also decline.
|
|
2615
|
+
# - `transform_values` / `transform_values!`: re-types each VALUE by binding it to the block
|
|
2616
|
+
# parameter; keys are preserved unchanged.
|
|
2617
|
+
# - `transform_keys` / `transform_keys!`: re-types each KEY by wrapping it in `Constant[k]` and
|
|
2618
|
+
# passing it to the block; values are preserved unchanged. The result key must be a
|
|
2619
|
+
# `Constant[Symbol | String]` — otherwise the tier declines (the new key cannot be used as a static
|
|
2620
|
+
# HashShape index). Collisions (two old keys mapping to the same new key) also decline.
|
|
2979
2621
|
#
|
|
2980
|
-
# Returns `nil` on any decline so the dispatcher falls
|
|
2981
|
-
#
|
|
2982
|
-
# answer.
|
|
2622
|
+
# Returns `nil` on any decline so the dispatcher falls through to `RbsDispatch` and gets the widened
|
|
2623
|
+
# `Hash[K, V]` answer.
|
|
2983
2624
|
def try_hash_shape_block_fold(call_node, receiver_type)
|
|
2984
2625
|
return nil unless HASH_SHAPE_TRANSFORM_METHODS.include?(call_node.name)
|
|
2985
2626
|
return nil unless receiver_type.is_a?(Type::HashShape)
|
|
@@ -3023,9 +2664,8 @@ module Rigor
|
|
|
3023
2664
|
Type::Combinator.hash_shape_of(new_pairs)
|
|
3024
2665
|
end
|
|
3025
2666
|
|
|
3026
|
-
# Applies a single-argument block (either a full BlockNode
|
|
3027
|
-
#
|
|
3028
|
-
# returns the resulting type, or `nil` on failure.
|
|
2667
|
+
# Applies a single-argument block (either a full BlockNode or a `&:symbol` BlockArgumentNode) to
|
|
2668
|
+
# `param_type` and returns the resulting type, or `nil` on failure.
|
|
3029
2669
|
def apply_hash_block(block_arg, param_type)
|
|
3030
2670
|
case block_arg
|
|
3031
2671
|
when Prism::BlockNode
|