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