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
|
@@ -24,27 +24,21 @@ require_relative "../builtins/exception_catalog"
|
|
|
24
24
|
module Rigor
|
|
25
25
|
module Inference
|
|
26
26
|
module MethodDispatcher
|
|
27
|
-
# Slice 2 rule book that folds method calls on `Rigor::Type::Constant`
|
|
28
|
-
#
|
|
29
|
-
# `Union[Constant, …]`) whenever:
|
|
27
|
+
# Slice 2 rule book that folds method calls on `Rigor::Type::Constant` receivers (and unions of them)
|
|
28
|
+
# into another `Constant` (or a small `Union[Constant, …]`) whenever:
|
|
30
29
|
#
|
|
31
|
-
# * the receiver is a recognised scalar literal, OR a `Union` whose
|
|
32
|
-
# members are all `Constant`,
|
|
30
|
+
# * the receiver is a recognised scalar literal, OR a `Union` whose members are all `Constant`,
|
|
33
31
|
# * arguments (zero or one) are likewise `Constant` or `Union[Constant…]`,
|
|
34
32
|
# * the method name is in the curated whitelist for the receiver's class,
|
|
35
|
-
# * the operation cannot accidentally explode the analyzer (we cap
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
# * the actual Ruby invocation does not raise on at least one
|
|
40
|
-
# receiver/argument combination.
|
|
33
|
+
# * the operation cannot accidentally explode the analyzer (we cap string-fold output at
|
|
34
|
+
# `STRING_FOLD_BYTE_LIMIT` bytes, the input cartesian product at `UNION_FOLD_INPUT_LIMIT`, and the
|
|
35
|
+
# deduped output union at `UNION_FOLD_OUTPUT_LIMIT`), and
|
|
36
|
+
# * the actual Ruby invocation does not raise on at least one receiver/argument combination.
|
|
41
37
|
#
|
|
42
|
-
# Anything else returns `nil`, signalling "no rule matched" so the
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
# here MUST NOT regress: any value reachable by literal arithmetic at
|
|
47
|
-
# parse time is meant to be foldable independent of RBS data.
|
|
38
|
+
# Anything else returns `nil`, signalling "no rule matched" so the caller (`MethodDispatcher`) falls
|
|
39
|
+
# back to `Dynamic[Top]` and records a fail-soft event. Slice 4 (RBS-backed) layers another dispatch
|
|
40
|
+
# tier behind this rule book, but the constant-folding semantics defined here MUST NOT regress: any
|
|
41
|
+
# value reachable by literal arithmetic at parse time is meant to be foldable independent of RBS data.
|
|
48
42
|
module ConstantFolding # rubocop:disable Metrics/ModuleLength
|
|
49
43
|
module_function
|
|
50
44
|
|
|
@@ -52,12 +46,10 @@ module Rigor
|
|
|
52
46
|
:+, :-, :*, :/, :%, :**, :&, :|, :^, :<<, :>>,
|
|
53
47
|
:<, :<=, :>, :>=, :==, :!=, :<=>,
|
|
54
48
|
:gcd, :lcm, :fdiv, :quo, :ceildiv, :[],
|
|
55
|
-
# Integer bit-test predicates (`(self & mask) <=> mask|0`). The
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
# bit-reference `:[]`. Integer-only, but Float-safe to list: a Float
|
|
60
|
-
# receiver has no such method, so `invoke_binary` rescues the
|
|
49
|
+
# Integer bit-test predicates (`(self & mask) <=> mask|0`). The catalog marks them `:dispatch` only
|
|
50
|
+
# because a non-Integer mask would route through `to_int`; a concrete Integer literal never does,
|
|
51
|
+
# so the fold is pure here — the sibling of the already-folded bit-reference `:[]`. Integer-only,
|
|
52
|
+
# but Float-safe to list: a Float receiver has no such method, so `invoke_binary` rescues the
|
|
61
53
|
# `NoMethodError` to nil and the RBS tier answers.
|
|
62
54
|
:allbits?, :anybits?, :nobits?
|
|
63
55
|
].freeze
|
|
@@ -66,76 +58,60 @@ module Rigor
|
|
|
66
58
|
:start_with?, :end_with?, :include?,
|
|
67
59
|
:delete_prefix, :delete_suffix,
|
|
68
60
|
:match?, :index, :rindex, :center, :ljust, :rjust,
|
|
69
|
-
# 1-arg pure transforms/queries whose output never exceeds the
|
|
70
|
-
#
|
|
61
|
+
# 1-arg pure transforms/queries whose output never exceeds the input: `delete`/`squeeze` shrink
|
|
62
|
+
# the string, `count` → Integer.
|
|
71
63
|
:delete, :count, :squeeze,
|
|
72
|
-
# ASCII / Unicode-case-fold comparison — deterministic, no
|
|
73
|
-
#
|
|
64
|
+
# ASCII / Unicode-case-fold comparison — deterministic, no locale read: `casecmp` → -1/0/1,
|
|
65
|
+
# `casecmp?` → bool/nil.
|
|
74
66
|
:casecmp, :casecmp?
|
|
75
67
|
].freeze
|
|
76
68
|
SYMBOL_BINARY = Set[:==, :!=, :<=>, :<, :<=, :>, :>=].freeze
|
|
77
69
|
BOOL_BINARY = Set[:&, :|, :^, :==, :!=, :===].freeze
|
|
78
70
|
NIL_BINARY = Set[:==, :!=].freeze
|
|
79
|
-
# Rational arithmetic / ordering are exact and pure. Division
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
# through `rb_funcall(:==)` on the operands — user-redefinable —
|
|
84
|
-
# so the catalog classifies it `:dispatch` and the equality stays
|
|
71
|
+
# Rational arithmetic / ordering are exact and pure. Division (`/`) and `**` may return a
|
|
72
|
+
# `Float`/`Complex` for some operands, all of which are foldable `Constant` value classes. `==` /
|
|
73
|
+
# `!=` are deliberately EXCLUDED: `Rational#==` (`nurat_eqeq_p`) routes through `rb_funcall(:==)` on
|
|
74
|
+
# the operands — user-redefinable — so the catalog classifies it `:dispatch` and the equality stays
|
|
85
75
|
# the RBS `bool`. (The set would otherwise bypass that gate.)
|
|
86
76
|
RATIONAL_BINARY = Set[
|
|
87
77
|
:+, :-, :*, :/, :**, :<=>, :<, :<=, :>, :>=,
|
|
88
78
|
:div, :modulo, :%, :remainder, :fdiv, :quo
|
|
89
79
|
].freeze
|
|
90
|
-
# Complex arithmetic. `ops_for` gains a `Complex` branch so these
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
# delegates to operand `==`); ordering is undefined for Complex.
|
|
80
|
+
# Complex arithmetic. `ops_for` gains a `Complex` branch so these reach the binary fold path
|
|
81
|
+
# (Complex was previously unary-only). `/` and `**` stay foldable (Complex result). `==` / `!=` are
|
|
82
|
+
# excluded for the same reason as Rational (`nucomp_eqeq_p` delegates to operand `==`); ordering is
|
|
83
|
+
# undefined for Complex.
|
|
95
84
|
COMPLEX_BINARY = Set[:+, :-, :*, :/, :**].freeze
|
|
96
85
|
|
|
97
|
-
# `Set#&` and its alias `Set#intersection` are leaf-pure for a
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
# is the pure core method, so the fold is sound; the hand-rolled
|
|
104
|
-
# allow-list is the right tool, mirroring the bit-test predicates.
|
|
105
|
-
# (The other binary set ops keep folding through the catalog.)
|
|
86
|
+
# `Set#&` and its alias `Set#intersection` are leaf-pure for a concrete Set operand exactly like
|
|
87
|
+
# their siblings `|` / `-` / `^` (all `:leaf` in the catalog), but the catalog flags
|
|
88
|
+
# `set_i_intersection`'s C body `block_dependent` — it drives Set's own internal iterator — so the
|
|
89
|
+
# catalog tier declines and the intersection alone fails to fold. A concrete Set argument's `each`
|
|
90
|
+
# is the pure core method, so the fold is sound; the hand-rolled allow-list is the right tool,
|
|
91
|
+
# mirroring the bit-test predicates. (The other binary set ops keep folding through the catalog.)
|
|
106
92
|
SET_BINARY = Set[:&, :intersection].freeze
|
|
107
93
|
|
|
108
94
|
# v0.0.3 C — pure unary catalogue. Each method must:
|
|
109
95
|
# - take zero arguments,
|
|
110
96
|
# - have no side effects,
|
|
111
|
-
# - never raise on the type's full domain (or be
|
|
112
|
-
#
|
|
113
|
-
# - return a value safe to materialise as a
|
|
114
|
-
# `Constant` (no large strings, no host objects).
|
|
97
|
+
# - never raise on the type's full domain (or be guarded by `safe?` below),
|
|
98
|
+
# - return a value safe to materialise as a `Constant` (no large strings, no host objects).
|
|
115
99
|
#
|
|
116
|
-
# The catalogue is the prerequisite for aggressive
|
|
117
|
-
#
|
|
118
|
-
#
|
|
119
|
-
#
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
# `
|
|
123
|
-
#
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
#
|
|
127
|
-
# `"abc".hash` returns a different Integer in every Ruby
|
|
128
|
-
# process. Folding it to a `Constant` would bake one process's
|
|
129
|
-
# value into the type (and the on-disk cache), making the
|
|
130
|
-
# result non-deterministic across runs — a violation of the
|
|
131
|
-
# purity contract this catalogue rests on. A literal's `.hash`
|
|
132
|
-
# therefore stays the RBS-widened `Integer`. The deterministic
|
|
133
|
-
# siblings `:inspect` / `:to_s` remain folded.
|
|
100
|
+
# The catalogue is the prerequisite for aggressive constant folding through user methods: once
|
|
101
|
+
# `Constant[3].odd?` folds to `Constant[true]`, the inter-procedural inference path landed in
|
|
102
|
+
# v0.0.2 #5 carries the constant through the body of a user-defined `def is_odd(n) = n.odd?` so
|
|
103
|
+
# `Parity.new.is_odd(3)` types as `Constant[true]` rather than the RBS-widened `bool`.
|
|
104
|
+
# NOTE: `:hash` is deliberately NOT in any of these sets. `Object#hash` (and the
|
|
105
|
+
# `String`/`Symbol`/`Integer`/`Float` overrides) is salted with a per-process SipHash seed, so
|
|
106
|
+
# `"abc".hash` returns a different Integer in every Ruby process. Folding it to a `Constant` would
|
|
107
|
+
# bake one process's value into the type (and the on-disk cache), making the result
|
|
108
|
+
# non-deterministic across runs — a violation of the purity contract this catalogue rests on. A
|
|
109
|
+
# literal's `.hash` therefore stays the RBS-widened `Integer`. The deterministic siblings
|
|
110
|
+
# `:inspect` / `:to_s` remain folded.
|
|
134
111
|
INTEGER_UNARY = Set[
|
|
135
112
|
:odd?, :even?, :zero?, :positive?, :negative?,
|
|
136
|
-
# `finite?` / `infinite?` are total on Integer (`true` / `nil`
|
|
137
|
-
#
|
|
138
|
-
# sibling already folds them. `nonzero?` returns `self` (non-zero)
|
|
113
|
+
# `finite?` / `infinite?` are total on Integer (`true` / `nil` always) and round out the numeric
|
|
114
|
+
# predicate family — the Float sibling already folds them. `nonzero?` returns `self` (non-zero)
|
|
139
115
|
# or `nil`, both foldable Constants.
|
|
140
116
|
:finite?, :infinite?, :nonzero?,
|
|
141
117
|
:succ, :pred, :next, :abs, :magnitude,
|
|
@@ -149,18 +125,15 @@ module Rigor
|
|
|
149
125
|
:abs, :magnitude, :floor, :ceil, :round, :truncate,
|
|
150
126
|
:next_float, :prev_float,
|
|
151
127
|
:to_s, :to_i, :to_int, :to_f, :to_r, :rationalize,
|
|
152
|
-
# `numerator` / `denominator` expose the rational
|
|
153
|
-
#
|
|
154
|
-
#
|
|
155
|
-
#
|
|
156
|
-
#
|
|
157
|
-
# `.denominator → 1` fold to the same value Ruby returns, and
|
|
158
|
-
# `NaN.numerator → NaN` is declined by `foldable_constant_value?`.
|
|
128
|
+
# `numerator` / `denominator` expose the rational decomposition of the float (`2.5.numerator → 5`,
|
|
129
|
+
# `.denominator → 2`) — pure arithmetic, the Float siblings of the already-folded Rational
|
|
130
|
+
# accessors. The non-finite edges stay sound: `Infinity.numerator → Infinity` /
|
|
131
|
+
# `.denominator → 1` fold to the same value Ruby returns, and `NaN.numerator → NaN` is declined by
|
|
132
|
+
# `foldable_constant_value?`.
|
|
159
133
|
:numerator, :denominator,
|
|
160
|
-
# `arg` / `angle` / `phase` (aliases) return the complex
|
|
161
|
-
#
|
|
162
|
-
#
|
|
163
|
-
# receiver yields NaN which `foldable_constant_value?` declines.
|
|
134
|
+
# `arg` / `angle` / `phase` (aliases) return the complex argument of the real number: `0` for
|
|
135
|
+
# `self >= 0`, `Math::PI` for `self < 0`. Pure sign test, deterministic; a NaN receiver yields NaN
|
|
136
|
+
# which `foldable_constant_value?` declines.
|
|
164
137
|
:arg, :angle, :phase,
|
|
165
138
|
:inspect, :-@, :+@
|
|
166
139
|
].freeze
|
|
@@ -171,19 +144,17 @@ module Rigor
|
|
|
171
144
|
:to_s, :to_str, :to_sym, :intern,
|
|
172
145
|
:to_i, :to_f, :ord, :chr, :hex, :oct, :succ, :next,
|
|
173
146
|
:sum, :inspect,
|
|
174
|
-
# `shellescape` is the String-receiver twin of the already-folded
|
|
175
|
-
#
|
|
176
|
-
#
|
|
177
|
-
# `shellwords_folding`, so the method is always defined here.
|
|
147
|
+
# `shellescape` is the String-receiver twin of the already-folded `Shellwords.escape` —
|
|
148
|
+
# deterministic shell-quoting, no global state. The `shellwords` library is loaded process-wide
|
|
149
|
+
# via `shellwords_folding`, so the method is always defined here.
|
|
178
150
|
:shellescape
|
|
179
151
|
].freeze
|
|
180
152
|
SYMBOL_UNARY = Set[
|
|
181
153
|
:to_s, :to_sym, :to_proc, :length, :size,
|
|
182
154
|
:empty?, :upcase, :downcase, :capitalize,
|
|
183
155
|
:swapcase, :succ, :next, :inspect,
|
|
184
|
-
# `name` (the frozen-string accessor), `id2name` (alias of
|
|
185
|
-
# `
|
|
186
|
-
# symbol's text — siblings of the already-folded `to_s` / `to_sym`.
|
|
156
|
+
# `name` (the frozen-string accessor), `id2name` (alias of `to_s`), and `intern` (alias of
|
|
157
|
+
# `to_sym`) are pure reads of the symbol's text — siblings of the already-folded `to_s` / `to_sym`.
|
|
187
158
|
:name, :id2name, :intern
|
|
188
159
|
].freeze
|
|
189
160
|
BOOL_UNARY = Set[:!, :to_s, :inspect, :&, :|, :^].freeze
|
|
@@ -205,28 +176,23 @@ module Rigor
|
|
|
205
176
|
|
|
206
177
|
STRING_FOLD_BYTE_LIMIT = 4096
|
|
207
178
|
|
|
208
|
-
# Input cartesian product hard cap. Keeps fold cost bounded even
|
|
209
|
-
#
|
|
210
|
-
#
|
|
211
|
-
# facing payoff (a precise small enum) drops off fast past this
|
|
212
|
-
# range and CRuby method invocation cost adds up.
|
|
179
|
+
# Input cartesian product hard cap. Keeps fold cost bounded even when the receiver and argument are
|
|
180
|
+
# both `Union[Constant…]`. 5 × 5 = 25 inputs is permitted; 6 × 6 = 36 is not. The user-facing payoff
|
|
181
|
+
# (a precise small enum) drops off fast past this range and CRuby method invocation cost adds up.
|
|
213
182
|
UNION_FOLD_INPUT_LIMIT = 32
|
|
214
183
|
|
|
215
|
-
# Output cardinality cap on the deduped result union. A single
|
|
216
|
-
#
|
|
217
|
-
#
|
|
218
|
-
# is what ultimately limits how wide an inferred type gets.
|
|
184
|
+
# Output cardinality cap on the deduped result union. A single binary op on a small range can
|
|
185
|
+
# collapse: `[1,2,3] + [2,4,6]` produces 9 raw pairs but only 7 distinct sums. The output cap is
|
|
186
|
+
# what ultimately limits how wide an inferred type gets.
|
|
219
187
|
UNION_FOLD_OUTPUT_LIMIT = 8
|
|
220
188
|
|
|
221
189
|
# ADR-78 — reflexive over-fold guard.
|
|
222
|
-
# Reflective dispatch (`public_send` / `send` / `__send__`) must
|
|
223
|
-
#
|
|
224
|
-
#
|
|
225
|
-
#
|
|
226
|
-
#
|
|
227
|
-
#
|
|
228
|
-
# preservation tier (ADR-76 WD2) cannot surface an over-fold as a
|
|
229
|
-
# spurious `flow.always-truthy-condition`.
|
|
190
|
+
# Reflective dispatch (`public_send` / `send` / `__send__`) must NOT constant-fold unless the
|
|
191
|
+
# method-name argument is itself a value-pinned literal `Constant[Symbol]`. With a runtime-variable
|
|
192
|
+
# method name the dispatched method is not statically determined, so the call degrades to the RBS
|
|
193
|
+
# result (`untyped`) — exactly as it does without the guard, but explicit so a later shape-carrier
|
|
194
|
+
# preservation tier (ADR-76 WD2) cannot surface an over-fold as a spurious
|
|
195
|
+
# `flow.always-truthy-condition`.
|
|
230
196
|
REFLECTIVE_SEND_METHODS = %i[public_send send __send__].to_set.freeze
|
|
231
197
|
|
|
232
198
|
# @return [Rigor::Type::Constant, Rigor::Type::Union, Rigor::Type::IntegerRange, nil]
|
|
@@ -239,13 +205,10 @@ module Rigor
|
|
|
239
205
|
first_arg = args.first
|
|
240
206
|
return nil unless first_arg.is_a?(Type::Constant) && first_arg.value.is_a?(Symbol)
|
|
241
207
|
end
|
|
242
|
-
# v0.0.7 — `String#%` against a `Tuple` / `HashShape`
|
|
243
|
-
#
|
|
244
|
-
#
|
|
245
|
-
#
|
|
246
|
-
# arguments because they are not scalar-Constant
|
|
247
|
-
# carriers, so the special-case sits ahead of the
|
|
248
|
-
# numeric path.
|
|
208
|
+
# v0.0.7 — `String#%` against a `Tuple` / `HashShape` argument runs Ruby's format-string engine
|
|
209
|
+
# when both sides are statically constant. The standard `numeric_set_of` path bails on Tuple /
|
|
210
|
+
# HashShape arguments because they are not scalar-Constant carriers, so the special-case sits
|
|
211
|
+
# ahead of the numeric path.
|
|
249
212
|
format_lift = try_fold_string_format(receiver, method_name, args)
|
|
250
213
|
return format_lift if format_lift
|
|
251
214
|
|
|
@@ -258,20 +221,14 @@ module Rigor
|
|
|
258
221
|
dispatch_by_arity(receiver_set, method_name, arg_sets)
|
|
259
222
|
end
|
|
260
223
|
|
|
261
|
-
# `Constant<String> % …` — runs the actual `String#%`
|
|
262
|
-
#
|
|
263
|
-
#
|
|
264
|
-
#
|
|
265
|
-
#
|
|
266
|
-
#
|
|
267
|
-
#
|
|
268
|
-
#
|
|
269
|
-
# Materialises the elements as a Ruby Array and runs
|
|
270
|
-
# the format.
|
|
271
|
-
# - A `Type::HashShape` with no optional keys whose
|
|
272
|
-
# values are all `Constant`. Materialises a Ruby Hash
|
|
273
|
-
# and runs the format. Symbol keys are kept as
|
|
274
|
-
# Symbols (matching Ruby's `%{key}` resolution).
|
|
224
|
+
# `Constant<String> % …` — runs the actual `String#%` operation when both sides are statically
|
|
225
|
+
# known. The argument may be:
|
|
226
|
+
# - A `Type::Constant` whose value is a scalar (Integer / Float / String / Symbol). Already handled
|
|
227
|
+
# by the numeric path; this method declines so the standard binary path picks it up.
|
|
228
|
+
# - A `Type::Tuple` whose elements are all `Constant`. Materialises the elements as a Ruby Array
|
|
229
|
+
# and runs the format.
|
|
230
|
+
# - A `Type::HashShape` with no optional keys whose values are all `Constant`. Materialises a Ruby
|
|
231
|
+
# Hash and runs the format. Symbol keys are kept as Symbols (matching Ruby's `%{key}` resolution).
|
|
275
232
|
# Anything else declines so the RBS tier widens.
|
|
276
233
|
def try_fold_string_format(receiver, method_name, args)
|
|
277
234
|
return nil unless method_name == :%
|
|
@@ -318,8 +275,8 @@ module Rigor
|
|
|
318
275
|
end
|
|
319
276
|
|
|
320
277
|
# Normalises an input type into one of:
|
|
321
|
-
# - `Array<Object>` for a `Constant` (1-element) or
|
|
322
|
-
#
|
|
278
|
+
# - `Array<Object>` for a `Constant` (1-element) or `Union[Constant…]` (n-element) — concrete values
|
|
279
|
+
# to enumerate.
|
|
323
280
|
# - `Type::IntegerRange` — bounded interval.
|
|
324
281
|
# - `nil` — the input shape is not foldable.
|
|
325
282
|
def numeric_set_of(type)
|
|
@@ -328,20 +285,18 @@ module Rigor
|
|
|
328
285
|
when Type::Union
|
|
329
286
|
return type.members.map(&:value) if type.members.all?(Type::Constant)
|
|
330
287
|
|
|
331
|
-
# A union that mixes `Constant<Integer>` and `IntegerRange`
|
|
332
|
-
#
|
|
333
|
-
#
|
|
334
|
-
#
|
|
335
|
-
# the result an `IntegerRange` instead of bailing to Dynamic.
|
|
288
|
+
# A union that mixes `Constant<Integer>` and `IntegerRange` members (e.g. an accumulator's
|
|
289
|
+
# running fixpoint assumption `1 | int<1, 6>`) folds as the bounding interval. The
|
|
290
|
+
# range-arithmetic path (`try_fold_binary_range`) then keeps the result an `IntegerRange`
|
|
291
|
+
# instead of bailing to Dynamic.
|
|
336
292
|
union_integer_bounds(type)
|
|
337
293
|
when Type::IntegerRange then type
|
|
338
294
|
end
|
|
339
295
|
end
|
|
340
296
|
|
|
341
|
-
# Returns the bounding `IntegerRange` over a union whose members
|
|
342
|
-
#
|
|
343
|
-
#
|
|
344
|
-
# so precision is never silently lost).
|
|
297
|
+
# Returns the bounding `IntegerRange` over a union whose members are each an Integer `Constant` or
|
|
298
|
+
# an `IntegerRange`; `nil` otherwise (a Float constant or any non-numeric member declines, so
|
|
299
|
+
# precision is never silently lost).
|
|
345
300
|
def union_integer_bounds(union)
|
|
346
301
|
lowers = []
|
|
347
302
|
uppers = []
|
|
@@ -359,9 +314,8 @@ module Rigor
|
|
|
359
314
|
return nil
|
|
360
315
|
end
|
|
361
316
|
end
|
|
362
|
-
# `IntegerRange#lower`/`#upper` surface an unbounded edge as
|
|
363
|
-
#
|
|
364
|
-
# so map the extremum back.
|
|
317
|
+
# `IntegerRange#lower`/`#upper` surface an unbounded edge as `±Float::INFINITY`; `integer_range`
|
|
318
|
+
# wants the `±∞` *sentinel*, so map the extremum back.
|
|
365
319
|
Type::Combinator.integer_range(infinity_to_sentinel(lowers.min),
|
|
366
320
|
infinity_to_sentinel(uppers.max))
|
|
367
321
|
end
|
|
@@ -391,13 +345,10 @@ module Rigor
|
|
|
391
345
|
end
|
|
392
346
|
end
|
|
393
347
|
|
|
394
|
-
# `Integer#divmod` and `Float#divmod` return a 2-element array
|
|
395
|
-
# `[
|
|
396
|
-
#
|
|
397
|
-
#
|
|
398
|
-
# widened per-position: each tuple slot carries the union of
|
|
399
|
-
# quotients (resp. remainders) over every safe input pair.
|
|
400
|
-
# Range inputs are not yet folded — they bail to nil.
|
|
348
|
+
# `Integer#divmod` and `Float#divmod` return a 2-element array `[quotient, remainder]`. We project
|
|
349
|
+
# that into `Tuple[Constant[q], Constant[r]]` so downstream rules see the precise element types.
|
|
350
|
+
# Union/range receivers are widened per-position: each tuple slot carries the union of quotients
|
|
351
|
+
# (resp. remainders) over every safe input pair. Range inputs are not yet folded — they bail to nil.
|
|
401
352
|
def try_fold_divmod(left, right)
|
|
402
353
|
pairs = collect_divmod_pairs(left, right)
|
|
403
354
|
return nil unless pairs && !pairs.empty?
|
|
@@ -418,10 +369,9 @@ module Rigor
|
|
|
418
369
|
end
|
|
419
370
|
end
|
|
420
371
|
|
|
421
|
-
# Returns `[[quotient, remainder]]` (single-element array
|
|
422
|
-
#
|
|
423
|
-
#
|
|
424
|
-
# use `flat_map` and not lose legitimate 0/false elements.
|
|
372
|
+
# Returns `[[quotient, remainder]]` (single-element array wrapping the tuple) on success; `nil` to
|
|
373
|
+
# signal "skip this pair". The wrapping mirrors `invoke_binary` so we can use `flat_map` and not
|
|
374
|
+
# lose legitimate 0/false elements.
|
|
425
375
|
def invoke_divmod(receiver_value, arg_value)
|
|
426
376
|
return nil unless receiver_value.is_a?(Numeric) && arg_value.is_a?(Numeric)
|
|
427
377
|
|
|
@@ -442,12 +392,10 @@ module Rigor
|
|
|
442
392
|
special = try_fold_unary_special(receiver_values, method_name)
|
|
443
393
|
return special if special
|
|
444
394
|
|
|
445
|
-
# Type-level allow check on every receiver. If one member's
|
|
446
|
-
#
|
|
447
|
-
#
|
|
448
|
-
#
|
|
449
|
-
# Silently dropping the unsafe member would lie about the
|
|
450
|
-
# remaining receivers' behaviour.
|
|
395
|
+
# Type-level allow check on every receiver. If one member's type does not have the method in its
|
|
396
|
+
# allow list (e.g. `Union[String, nil].nil?` — `:nil?` is not in `STRING_UNARY`), bail the fold so
|
|
397
|
+
# the RBS tier answers. Silently dropping the unsafe member would lie about the remaining
|
|
398
|
+
# receivers' behaviour.
|
|
451
399
|
return nil unless receiver_values.all? { |rv| unary_method_allowed?(rv, method_name) }
|
|
452
400
|
|
|
453
401
|
results = receiver_values.flat_map do |rv|
|
|
@@ -456,12 +404,10 @@ module Rigor
|
|
|
456
404
|
build_constant_type(results, source: receiver_values)
|
|
457
405
|
end
|
|
458
406
|
|
|
459
|
-
# The carrier-specific unary lifts — Range-to-Tuple, the
|
|
460
|
-
#
|
|
461
|
-
#
|
|
462
|
-
# the
|
|
463
|
-
# nil means none applied and the caller falls through to the
|
|
464
|
-
# scalar allow-list path.
|
|
407
|
+
# The carrier-specific unary lifts — Range-to-Tuple, the Array-returning String / Pathname / Regexp
|
|
408
|
+
# / Set / Integer / Numeric folds — that produce a precise structural type before the generic
|
|
409
|
+
# scalar `invoke_unary` path. The first match wins; nil means none applied and the caller falls
|
|
410
|
+
# through to the scalar allow-list path.
|
|
465
411
|
def try_fold_unary_special(receiver_values, method_name)
|
|
466
412
|
try_fold_range_constant_unary(receiver_values, method_name) ||
|
|
467
413
|
try_fold_string_array_unary(receiver_values, method_name) ||
|
|
@@ -472,28 +418,22 @@ module Rigor
|
|
|
472
418
|
try_fold_integer_array_unary(receiver_values, method_name) ||
|
|
473
419
|
try_fold_numeric_array_unary(receiver_values, method_name)
|
|
474
420
|
end
|
|
475
|
-
# v0.0.7 — `Constant<Range>#to_a` and the no-arg
|
|
476
|
-
#
|
|
477
|
-
#
|
|
478
|
-
#
|
|
479
|
-
#
|
|
480
|
-
#
|
|
481
|
-
# - `first` / `last` / `min` / `max` are catalog-classified
|
|
482
|
-
# `:block_dependent` because of the optional-block forms,
|
|
483
|
-
# but the no-arg form is pure for finite integer ranges.
|
|
421
|
+
# v0.0.7 — `Constant<Range>#to_a` and the no-arg `first` / `last` / `min` / `max` short-circuit
|
|
422
|
+
# through a Range-specific arm that catalog dispatch cannot reach:
|
|
423
|
+
# - `to_a` returns an Array (not foldable through `foldable_constant_value?`) — lift to
|
|
424
|
+
# `Tuple[Constant…]` when the cardinality fits within `RANGE_TO_A_LIMIT`.
|
|
425
|
+
# - `first` / `last` / `min` / `max` are catalog-classified `:block_dependent` because of the
|
|
426
|
+
# optional-block forms, but the no-arg form is pure for finite integer ranges.
|
|
484
427
|
#
|
|
485
|
-
# Only fires on a single-receiver Range with finite integer
|
|
486
|
-
#
|
|
487
|
-
# union-of-Constants path keeps the rest of the arms.
|
|
428
|
+
# Only fires on a single-receiver Range with finite integer endpoints; mixed unions fall through so
|
|
429
|
+
# the existing union-of-Constants path keeps the rest of the arms.
|
|
488
430
|
RANGE_FOLD_METHODS = Set[:to_a, :first, :last, :min, :max, :count, :size, :length, :entries, :minmax,
|
|
489
431
|
:sum].freeze
|
|
490
|
-
# 1-arg head/tail projections on a `Constant<Range>`. `first(n)` /
|
|
491
|
-
# `
|
|
492
|
-
#
|
|
493
|
-
#
|
|
494
|
-
#
|
|
495
|
-
# no-arg `first` / `last` / `min` / `max` stay on the unary path
|
|
496
|
-
# (single Integer endpoint).
|
|
432
|
+
# 1-arg head/tail projections on a `Constant<Range>`. `first(n)` / `take(n)` return the first `n`
|
|
433
|
+
# elements, `last(n)` the final `n`, and `min(n)` / `max(n)` the n smallest / largest (for an
|
|
434
|
+
# ascending integer range `min(n) == first(n)` and `max(n) == last(n).reverse`) — each lifts to a
|
|
435
|
+
# per-position `Tuple[Constant[Integer]…]`. The no-arg `first` / `last` / `min` / `max` stay on the
|
|
436
|
+
# unary path (single Integer endpoint).
|
|
497
437
|
RANGE_FOLD_BINARY_METHODS = Set[:first, :last, :take, :min, :max].freeze
|
|
498
438
|
RANGE_TO_A_LIMIT = 16
|
|
499
439
|
private_constant :RANGE_FOLD_METHODS, :RANGE_FOLD_BINARY_METHODS, :RANGE_TO_A_LIMIT
|
|
@@ -516,10 +456,9 @@ module Rigor
|
|
|
516
456
|
when :last, :max then range_endpoint_constant(range, :last)
|
|
517
457
|
when :count, :size, :length then Type::Combinator.constant_of(range.to_a.size)
|
|
518
458
|
when :minmax then range_minmax_tuple(range)
|
|
519
|
-
# `range.sum` is closed-form (Gauss) for an integer range, so a
|
|
520
|
-
#
|
|
521
|
-
#
|
|
522
|
-
# excluded by the Integer-endpoint guard in the caller.
|
|
459
|
+
# `range.sum` is closed-form (Gauss) for an integer range, so a huge range still costs O(1) and
|
|
460
|
+
# yields a single Integer — no materialisation, no cap needed. Endless ranges are already excluded
|
|
461
|
+
# by the Integer-endpoint guard in the caller.
|
|
523
462
|
when :sum then Type::Combinator.constant_of(range.sum)
|
|
524
463
|
end
|
|
525
464
|
end
|
|
@@ -554,9 +493,8 @@ module Rigor
|
|
|
554
493
|
)
|
|
555
494
|
end
|
|
556
495
|
|
|
557
|
-
# `(1..10).first(3)` / `.take(3)` / `.last(3)` — the 1-arg head /
|
|
558
|
-
#
|
|
559
|
-
# path; this is the n-arg sibling, mirroring the Tuple carrier's
|
|
496
|
+
# `(1..10).first(3)` / `.take(3)` / `.last(3)` — the 1-arg head / tail forms. `first`/`last` already
|
|
497
|
+
# fold no-arg through the unary path; this is the n-arg sibling, mirroring the Tuple carrier's
|
|
560
498
|
# `first(n)`/`take(n)` handlers. Lifts to `Tuple[Constant…]`.
|
|
561
499
|
def try_fold_range_constant_binary(receiver_values, method_name, arg_values)
|
|
562
500
|
return nil unless RANGE_FOLD_BINARY_METHODS.include?(method_name)
|
|
@@ -573,11 +511,9 @@ module Rigor
|
|
|
573
511
|
|
|
574
512
|
def range_take_tuple(range, method_name, count)
|
|
575
513
|
return nil unless count.is_a?(Integer) && !count.negative?
|
|
576
|
-
# `first(n)`/`last(n)`/`take(n)`/`min(n)`/`max(n)` materialise at
|
|
577
|
-
#
|
|
578
|
-
#
|
|
579
|
-
# tail projections are O(n) for integer endpoints (no full
|
|
580
|
-
# materialisation).
|
|
514
|
+
# `first(n)`/`last(n)`/`take(n)`/`min(n)`/`max(n)` materialise at most `min(n, size)` elements; cap
|
|
515
|
+
# that count so a huge `n` (or range) never blows up the Constant. `Range#size` and the head/tail
|
|
516
|
+
# projections are O(n) for integer endpoints (no full materialisation).
|
|
581
517
|
return nil if [count, range.size].min > RANGE_TO_A_LIMIT
|
|
582
518
|
|
|
583
519
|
values = range_head_tail(range, method_name, count)
|
|
@@ -586,10 +522,9 @@ module Rigor
|
|
|
586
522
|
Type::Combinator.tuple_of(*values.map { |v| Type::Combinator.constant_of(v) })
|
|
587
523
|
end
|
|
588
524
|
|
|
589
|
-
# The n elements a head/tail projection selects, in Ruby's order.
|
|
590
|
-
#
|
|
591
|
-
#
|
|
592
|
-
# so neither needs the full sort `Array#min`/`#max` would do.
|
|
525
|
+
# The n elements a head/tail projection selects, in Ruby's order. For an ascending integer range
|
|
526
|
+
# `min(n)` is the leading `n` (`first(n)`) and `max(n)` the trailing `n` reversed (descending), so
|
|
527
|
+
# neither needs the full sort `Array#min`/`#max` would do.
|
|
593
528
|
def range_head_tail(range, method_name, count)
|
|
594
529
|
case method_name
|
|
595
530
|
when :last then range.last(count)
|
|
@@ -619,77 +554,62 @@ module Rigor
|
|
|
619
554
|
end
|
|
620
555
|
build_constant_type(results, source: receiver_values + arg_values)
|
|
621
556
|
end
|
|
622
|
-
# v0.0.7 — `Constant<String>#chars` / `bytes` / `codepoints` /
|
|
623
|
-
# `
|
|
624
|
-
# Array
|
|
625
|
-
#
|
|
626
|
-
#
|
|
627
|
-
#
|
|
628
|
-
#
|
|
629
|
-
#
|
|
630
|
-
# `
|
|
631
|
-
#
|
|
632
|
-
# `Shellwords.split` — lifts the token Array to a Tuple. Raises
|
|
633
|
-
# `ArgumentError` on unmatched quotes, which `try_fold_string_array_unary`
|
|
634
|
-
# rescues to nil (RBS tier widens). `shellwords` is loaded process-wide
|
|
635
|
-
# via `shellwords_folding`.
|
|
557
|
+
# v0.0.7 — `Constant<String>#chars` / `bytes` / `codepoints` / `grapheme_clusters` / `lines` /
|
|
558
|
+
# `split` (no-arg) return a Ruby Array of foldable scalars; `foldable_constant_value?` rejects
|
|
559
|
+
# Array results, so the standard unary path declines. Lift the Array to a per-position
|
|
560
|
+
# `Tuple[Constant…]` directly, capped at `STRING_ARRAY_LIFT_LIMIT` to keep the result bounded for
|
|
561
|
+
# long strings. (`codepoints` yields per-character Integer codepoints, the sibling of the
|
|
562
|
+
# byte-valued `bytes`; `grapheme_clusters` is the extended-grapheme sibling of `chars`.)
|
|
563
|
+
# `shellsplit` is the String-receiver twin of the already-folded `Shellwords.split` — lifts the
|
|
564
|
+
# token Array to a Tuple. Raises `ArgumentError` on unmatched quotes, which
|
|
565
|
+
# `try_fold_string_array_unary` rescues to nil (RBS tier widens). `shellwords` is loaded
|
|
566
|
+
# process-wide via `shellwords_folding`.
|
|
636
567
|
STRING_ARRAY_UNARY_METHODS = Set[:chars, :bytes, :codepoints, :grapheme_clusters,
|
|
637
568
|
:lines, :split, :shellsplit].freeze
|
|
638
|
-
# `partition` / `rpartition` always return a fixed 3-element
|
|
639
|
-
#
|
|
640
|
-
#
|
|
641
|
-
# 3-slot `Tuple[Constant…]`.
|
|
569
|
+
# `partition` / `rpartition` always return a fixed 3-element `[head, separator, tail]` Array whose
|
|
570
|
+
# members are substrings of the receiver (bounded by the input), so they lift to a precise 3-slot
|
|
571
|
+
# `Tuple[Constant…]`.
|
|
642
572
|
STRING_ARRAY_BINARY_METHODS = Set[:split, :scan, :partition, :rpartition].freeze
|
|
643
573
|
STRING_ARRAY_LIFT_LIMIT = 32
|
|
644
574
|
private_constant :STRING_ARRAY_UNARY_METHODS,
|
|
645
575
|
:STRING_ARRAY_BINARY_METHODS,
|
|
646
576
|
:STRING_ARRAY_LIFT_LIMIT
|
|
647
577
|
|
|
648
|
-
# `Constant<Regexp>#names` returns an Array of capture-group name
|
|
649
|
-
#
|
|
650
|
-
# per-element types. The catalog classifies the C body as `:leaf`
|
|
578
|
+
# `Constant<Regexp>#names` returns an Array of capture-group name strings. Lifted to a Tuple so
|
|
579
|
+
# downstream narrowing can project per-element types. The catalog classifies the C body as `:leaf`
|
|
651
580
|
# so it is safe to evaluate at fold time; no `$~` side effect.
|
|
652
581
|
REGEXP_ARRAY_UNARY_METHODS = Set[:names].freeze
|
|
653
582
|
private_constant :REGEXP_ARRAY_UNARY_METHODS
|
|
654
583
|
|
|
655
|
-
# `Constant<Set>#to_a` returns an Array of the set's elements.
|
|
656
|
-
#
|
|
657
|
-
#
|
|
658
|
-
#
|
|
659
|
-
# internal hash), so this dedicated handler bypasses the catalog gate.
|
|
584
|
+
# `Constant<Set>#to_a` returns an Array of the set's elements. Ruby 3.2+ Set is C-implemented with a
|
|
585
|
+
# Hash as its backing store, so element ordering is deterministic (insertion order). The catalog
|
|
586
|
+
# marks `to_a` as `:dispatch` (it calls through to the internal hash), so this dedicated handler
|
|
587
|
+
# bypasses the catalog gate.
|
|
660
588
|
SET_ARRAY_UNARY_METHODS = Set[:to_a, :entries].freeze
|
|
661
589
|
private_constant :SET_ARRAY_UNARY_METHODS
|
|
662
590
|
|
|
663
|
-
# `Constant<Integer>#digits` returns the base-10 (or base-n with
|
|
664
|
-
#
|
|
665
|
-
#
|
|
666
|
-
#
|
|
667
|
-
# `digits` raises `Math::DomainError` on a negative receiver,
|
|
668
|
-
# so the negative case bails to the RBS tier.
|
|
591
|
+
# `Constant<Integer>#digits` returns the base-10 (or base-n with an argument — only the no-arg form
|
|
592
|
+
# is folded here) place values as a little-endian Array of Integers. Lifted to a Tuple so downstream
|
|
593
|
+
# rules see the precise per-position type. `digits` raises `Math::DomainError` on a negative
|
|
594
|
+
# receiver, so the negative case bails to the RBS tier.
|
|
669
595
|
INTEGER_ARRAY_UNARY_METHODS = Set[:digits].freeze
|
|
670
596
|
private_constant :INTEGER_ARRAY_UNARY_METHODS
|
|
671
597
|
|
|
672
|
-
# 1-arg Integer methods that return an Array of foldable
|
|
673
|
-
#
|
|
674
|
-
#
|
|
675
|
-
# (the fixed `[gcd, lcm]` pair). Both are pure arithmetic; the
|
|
676
|
-
# result lifts to a `Tuple[Constant[Integer]…]`.
|
|
598
|
+
# 1-arg Integer methods that return an Array of foldable Integers: `digits(base)` (base-n place
|
|
599
|
+
# values; raises on a negative receiver or base < 2 → declines) and `gcdlcm(other)` (the fixed
|
|
600
|
+
# `[gcd, lcm]` pair). Both are pure arithmetic; the result lifts to a `Tuple[Constant[Integer]…]`.
|
|
677
601
|
INTEGER_ARRAY_BINARY_METHODS = Set[:digits, :gcdlcm].freeze
|
|
678
602
|
private_constant :INTEGER_ARRAY_BINARY_METHODS
|
|
679
603
|
|
|
680
|
-
# v0.0.7 — `Constant<Pathname>` delegates to a curated set
|
|
681
|
-
#
|
|
682
|
-
#
|
|
683
|
-
#
|
|
684
|
-
#
|
|
685
|
-
# filesystem-independent — they read only `@path` — so
|
|
686
|
-
# invoking them at fold time produces a deterministic
|
|
687
|
-
# result regardless of the host filesystem state.
|
|
604
|
+
# v0.0.7 — `Constant<Pathname>` delegates to a curated set of pure path-manipulation methods.
|
|
605
|
+
# Pathname is immutable in Ruby (per its docstring) and the catalog classifies most methods
|
|
606
|
+
# `:dispatch` because the C body delegates to File / Dir / FileTest. The methods listed here are
|
|
607
|
+
# filesystem-independent — they read only `@path` — so invoking them at fold time produces a
|
|
608
|
+
# deterministic result regardless of the host filesystem state.
|
|
688
609
|
#
|
|
689
|
-
# Filesystem-touching methods (`exist?`, `file?`, `read`,
|
|
690
|
-
#
|
|
691
|
-
#
|
|
692
|
-
# neither stable nor relevant to the analyzed program.
|
|
610
|
+
# Filesystem-touching methods (`exist?`, `file?`, `read`, `stat`, …) are intentionally NOT folded:
|
|
611
|
+
# their answer depends on the analysis machine's filesystem, which is neither stable nor relevant to
|
|
612
|
+
# the analyzed program.
|
|
693
613
|
PATHNAME_PURE_UNARY = Set[
|
|
694
614
|
:to_s, :to_path, :to_str,
|
|
695
615
|
:basename, :dirname, :extname, :cleanpath,
|
|
@@ -699,22 +619,19 @@ module Rigor
|
|
|
699
619
|
PATHNAME_PURE_BINARY = Set[
|
|
700
620
|
:+, :join, :sub_ext, :<=>, :==, :eql?, :===,
|
|
701
621
|
:relative_path_from,
|
|
702
|
-
# `/` is the exact alias of `+` (`def /(other) = self + other`),
|
|
703
|
-
#
|
|
704
|
-
#
|
|
705
|
-
#
|
|
706
|
-
# both are pure `@path` string manipulation, no filesystem read.
|
|
622
|
+
# `/` is the exact alias of `+` (`def /(other) = self + other`), the idiomatic path-join operator
|
|
623
|
+
# (`dir / "file"`). `basename`'s 1-arg suffix-stripping form (`path.basename(".rb")` → the stem)
|
|
624
|
+
# is the binary sibling of the already-folded no-arg `basename` — both are pure `@path` string
|
|
625
|
+
# manipulation, no filesystem read.
|
|
707
626
|
:/, :basename
|
|
708
627
|
].freeze
|
|
709
628
|
private_constant :PATHNAME_PURE_UNARY, :PATHNAME_PURE_BINARY
|
|
710
629
|
|
|
711
|
-
# `Constant<Pathname>#split` returns the fixed 2-element
|
|
712
|
-
#
|
|
713
|
-
#
|
|
714
|
-
#
|
|
715
|
-
#
|
|
716
|
-
# Array-returning sibling of the scalar `basename` / `dirname`
|
|
717
|
-
# folds (which `try_fold_pathname_unary` already covers).
|
|
630
|
+
# `Constant<Pathname>#split` returns the fixed 2-element `[dirname, basename]` pair (both Pathname),
|
|
631
|
+
# the path-string split of `File.split`. Lifted to `Tuple[Constant[Pathname], Constant[Pathname]]`.
|
|
632
|
+
# Filesystem-independent — reads only `@path` — so it is deterministic at fold time, the
|
|
633
|
+
# Array-returning sibling of the scalar `basename` / `dirname` folds (which
|
|
634
|
+
# `try_fold_pathname_unary` already covers).
|
|
718
635
|
PATHNAME_ARRAY_UNARY_METHODS = Set[:split].freeze
|
|
719
636
|
private_constant :PATHNAME_ARRAY_UNARY_METHODS
|
|
720
637
|
|
|
@@ -750,10 +667,9 @@ module Rigor
|
|
|
750
667
|
nil
|
|
751
668
|
end
|
|
752
669
|
|
|
753
|
-
# `Constant<Pathname>#split` — lift the `[dirname, basename]`
|
|
754
|
-
#
|
|
755
|
-
#
|
|
756
|
-
# elements are Pathname, a foldable Constant class.
|
|
670
|
+
# `Constant<Pathname>#split` — lift the `[dirname, basename]` Pathname pair to a
|
|
671
|
+
# Tuple[Constant[Pathname], Constant[Pathname]]. Pure path-string manipulation (no filesystem
|
|
672
|
+
# read); both elements are Pathname, a foldable Constant class.
|
|
757
673
|
def try_fold_pathname_array_unary(receiver_values, method_name)
|
|
758
674
|
return nil unless PATHNAME_ARRAY_UNARY_METHODS.include?(method_name)
|
|
759
675
|
return nil unless receiver_values.size == 1
|
|
@@ -778,10 +694,9 @@ module Rigor
|
|
|
778
694
|
nil
|
|
779
695
|
end
|
|
780
696
|
|
|
781
|
-
# `Constant<Regexp>#names` — lift the Array[String] of named-capture
|
|
782
|
-
#
|
|
783
|
-
#
|
|
784
|
-
# writes no global state, and always returns an Array of frozen Strings.
|
|
697
|
+
# `Constant<Regexp>#names` — lift the Array[String] of named-capture group names to a
|
|
698
|
+
# Tuple[Constant[String]…]. Safe to evaluate at fold time: the C body reads only the regexp's
|
|
699
|
+
# internal names table, writes no global state, and always returns an Array of frozen Strings.
|
|
785
700
|
def try_fold_regexp_array_unary(receiver_values, method_name)
|
|
786
701
|
return nil unless REGEXP_ARRAY_UNARY_METHODS.include?(method_name)
|
|
787
702
|
return nil unless receiver_values.size == 1
|
|
@@ -794,10 +709,9 @@ module Rigor
|
|
|
794
709
|
nil
|
|
795
710
|
end
|
|
796
711
|
|
|
797
|
-
# `Constant<Set>#to_a` / `#entries` — lift the Array of set elements
|
|
798
|
-
#
|
|
799
|
-
#
|
|
800
|
-
# (insertion order), so the result is stable across invocations.
|
|
712
|
+
# `Constant<Set>#to_a` / `#entries` — lift the Array of set elements to a Tuple[Constant[…]…] when
|
|
713
|
+
# every element is a foldable scalar. Ruby 3.2+ Set is C-implemented; element order is
|
|
714
|
+
# deterministic (insertion order), so the result is stable across invocations.
|
|
801
715
|
def try_fold_set_array_unary(receiver_values, method_name)
|
|
802
716
|
return nil unless SET_ARRAY_UNARY_METHODS.include?(method_name)
|
|
803
717
|
return nil unless receiver_values.size == 1
|
|
@@ -810,11 +724,10 @@ module Rigor
|
|
|
810
724
|
nil
|
|
811
725
|
end
|
|
812
726
|
|
|
813
|
-
# `Constant<Integer>#digits` — lift the Array of base-10 place
|
|
814
|
-
#
|
|
815
|
-
#
|
|
816
|
-
#
|
|
817
|
-
# answers with `Array[Integer]`.
|
|
727
|
+
# `Constant<Integer>#digits` — lift the Array of base-10 place values to a
|
|
728
|
+
# Tuple[Constant[Integer]…]. Safe to evaluate at fold time: the C body is pure arithmetic. Negative
|
|
729
|
+
# receivers raise `Math::DomainError`; the fold declines so the RBS tier answers with
|
|
730
|
+
# `Array[Integer]`.
|
|
818
731
|
def try_fold_integer_array_unary(receiver_values, method_name)
|
|
819
732
|
return nil unless INTEGER_ARRAY_UNARY_METHODS.include?(method_name)
|
|
820
733
|
return nil unless receiver_values.size == 1
|
|
@@ -828,11 +741,9 @@ module Rigor
|
|
|
828
741
|
nil
|
|
829
742
|
end
|
|
830
743
|
|
|
831
|
-
# `Constant<Integer>#digits(base)` / `#gcdlcm(other)` — the
|
|
832
|
-
#
|
|
833
|
-
#
|
|
834
|
-
# errors (base < 2) raise and are rescued. `gcdlcm` is total over
|
|
835
|
-
# Integer args.
|
|
744
|
+
# `Constant<Integer>#digits(base)` / `#gcdlcm(other)` — the 1-arg Array-returning Integer methods.
|
|
745
|
+
# `digits(base)` declines on a negative receiver (the unary path's guard); other domain errors
|
|
746
|
+
# (base < 2) raise and are rescued. `gcdlcm` is total over Integer args.
|
|
836
747
|
def try_fold_integer_array_binary(receiver_values, method_name, arg_values)
|
|
837
748
|
return nil unless INTEGER_ARRAY_BINARY_METHODS.include?(method_name)
|
|
838
749
|
return nil unless receiver_values.size == 1 && arg_values.size == 1
|
|
@@ -847,18 +758,16 @@ module Rigor
|
|
|
847
758
|
nil
|
|
848
759
|
end
|
|
849
760
|
|
|
850
|
-
# `Constant<Complex>#rect` / `#rectangular` — lifts `[real, imaginary]`
|
|
851
|
-
#
|
|
852
|
-
#
|
|
853
|
-
# `foldable_constant_value?`.
|
|
761
|
+
# `Constant<Complex>#rect` / `#rectangular` — lifts `[real, imaginary]` to
|
|
762
|
+
# `Tuple[Constant[re], Constant[im]]`. Both components are always numeric (Integer or Float for
|
|
763
|
+
# literal complexes), so they satisfy `foldable_constant_value?`.
|
|
854
764
|
#
|
|
855
|
-
# `Constant<Complex>#polar` — lifts `[abs, arg]` to
|
|
856
|
-
#
|
|
857
|
-
# via `Complex#polar` (which calls `Math.hypot` and `Math.atan2`).
|
|
765
|
+
# `Constant<Complex>#polar` — lifts `[abs, arg]` to `Tuple[Constant[Float], Constant[Float]]`.
|
|
766
|
+
# Evaluated at fold time via `Complex#polar` (which calls `Math.hypot` and `Math.atan2`).
|
|
858
767
|
# Deterministic: reads only the receiver's real and imaginary parts.
|
|
859
768
|
#
|
|
860
|
-
# Rational receivers also support `rect` / `rectangular` / `polar`:
|
|
861
|
-
# `
|
|
769
|
+
# Rational receivers also support `rect` / `rectangular` / `polar`: `Rational(r,1).rect` →
|
|
770
|
+
# `[r, 0]`, `Rational(r,1).polar` → `[abs, arg]`.
|
|
862
771
|
NUMERIC_ARRAY_UNARY_METHODS = Set[:rect, :rectangular, :polar].freeze
|
|
863
772
|
private_constant :NUMERIC_ARRAY_UNARY_METHODS
|
|
864
773
|
|
|
@@ -874,9 +783,8 @@ module Rigor
|
|
|
874
783
|
nil
|
|
875
784
|
end
|
|
876
785
|
|
|
877
|
-
# `Constant<String>#split(arg)` / `#scan(arg)` — lift the
|
|
878
|
-
#
|
|
879
|
-
# known and the cardinality fits.
|
|
786
|
+
# `Constant<String>#split(arg)` / `#scan(arg)` — lift the Array result to a Tuple when both sides
|
|
787
|
+
# are statically known and the cardinality fits.
|
|
880
788
|
def try_fold_string_array_binary(receiver_values, method_name, arg_values)
|
|
881
789
|
return nil unless STRING_ARRAY_BINARY_METHODS.include?(method_name)
|
|
882
790
|
return nil unless receiver_values.size == 1 && arg_values.size == 1
|
|
@@ -899,17 +807,14 @@ module Rigor
|
|
|
899
807
|
Type::Combinator.tuple_of(*result.map { |v| Type::Combinator.constant_of(v) })
|
|
900
808
|
end
|
|
901
809
|
|
|
902
|
-
# 2-arg fold dispatch. Used by `Comparable#between?(min, max)`,
|
|
903
|
-
# `
|
|
904
|
-
#
|
|
905
|
-
# 0/1-arg switch could not reach.
|
|
810
|
+
# 2-arg fold dispatch. Used by `Comparable#between?(min, max)`, `Comparable#clamp(min, max)`, and
|
|
811
|
+
# `Integer#pow(exp, mod)` — methods the catalog classifies `:leaf` but that the prior 0/1-arg switch
|
|
812
|
+
# could not reach.
|
|
906
813
|
#
|
|
907
|
-
# v0.0.6 — IntegerRange-shaped receivers participate in
|
|
908
|
-
# `Comparable#
|
|
909
|
-
# `int<a,b>.
|
|
910
|
-
#
|
|
911
|
-
# narrows the receiver's bounds against the bracket. Other
|
|
912
|
-
# ternary methods over IntegerRange operands still decline.
|
|
814
|
+
# v0.0.6 — IntegerRange-shaped receivers participate in `Comparable#between?` and
|
|
815
|
+
# `Comparable#clamp` folds. `int<a,b>.between?(min, max)` decides three-valued via the receiver's
|
|
816
|
+
# bounds against scalar args; `int<a,b>.clamp` narrows the receiver's bounds against the bracket.
|
|
817
|
+
# Other ternary methods over IntegerRange operands still decline.
|
|
913
818
|
def try_fold_ternary(receiver_set, method_name, arg_sets)
|
|
914
819
|
return try_fold_ternary_range(receiver_set, method_name, arg_sets) if receiver_set.is_a?(Type::IntegerRange)
|
|
915
820
|
return nil if arg_sets.any?(Type::IntegerRange)
|
|
@@ -917,11 +822,9 @@ module Rigor
|
|
|
917
822
|
try_fold_ternary_set(receiver_set, method_name, arg_sets)
|
|
918
823
|
end
|
|
919
824
|
|
|
920
|
-
# Receiver IntegerRange + two scalar `Constant<Integer>`
|
|
921
|
-
#
|
|
922
|
-
#
|
|
923
|
-
# `clamp` returns the intersected IntegerRange (or a
|
|
924
|
-
# collapsed Constant if the result pins a single point).
|
|
825
|
+
# Receiver IntegerRange + two scalar `Constant<Integer>` args — the only IntegerRange-aware ternary
|
|
826
|
+
# fold today. `between?` returns Trinary truthiness over the bracket; `clamp` returns the
|
|
827
|
+
# intersected IntegerRange (or a collapsed Constant if the result pins a single point).
|
|
925
828
|
def try_fold_ternary_range(range, method_name, arg_sets)
|
|
926
829
|
return nil unless arg_sets.all?(Array)
|
|
927
830
|
|
|
@@ -957,12 +860,9 @@ module Rigor
|
|
|
957
860
|
|
|
958
861
|
# `int<a,b>.clamp(min, max)`:
|
|
959
862
|
# - new_lower = max(a, min), new_upper = min(b, max).
|
|
960
|
-
# - When new_lower > new_upper the bracket excluded the
|
|
961
|
-
#
|
|
962
|
-
#
|
|
963
|
-
# precise here than Ruby — decline so the RBS tier
|
|
964
|
-
# widens to plain Integer rather than the dispatcher
|
|
965
|
-
# inventing a value.
|
|
863
|
+
# - When new_lower > new_upper the bracket excluded the range entirely; the call still returns one
|
|
864
|
+
# of the bracket bounds at runtime, but Rigor is strictly less precise here than Ruby — decline
|
|
865
|
+
# so the RBS tier widens to plain Integer rather than the dispatcher inventing a value.
|
|
966
866
|
def range_clamp(range, min_arg, max_arg)
|
|
967
867
|
new_lower = clamp_lower_bound(range.lower, min_arg)
|
|
968
868
|
new_upper = clamp_upper_bound(range.upper, max_arg)
|
|
@@ -1017,15 +917,12 @@ module Rigor
|
|
|
1017
917
|
catalog_allows?(receiver_value, method_name)
|
|
1018
918
|
end
|
|
1019
919
|
|
|
1020
|
-
# Builds a Constant or Union[Constant…] from a flat list of
|
|
1021
|
-
#
|
|
1022
|
-
# `
|
|
1023
|
-
#
|
|
1024
|
-
#
|
|
1025
|
-
#
|
|
1026
|
-
# a hint that the result set's "Integer-ness" was already
|
|
1027
|
-
# implied by the inputs (so the widening fallback only fires
|
|
1028
|
-
# for arithmetic over integers).
|
|
920
|
+
# Builds a Constant or Union[Constant…] from a flat list of Ruby values. When the deduped set
|
|
921
|
+
# exceeds `UNION_FOLD_OUTPUT_LIMIT` and every result is an Integer, widens to the bounding
|
|
922
|
+
# `IntegerRange` instead of returning nil — that is the graceful escape valve for additions over
|
|
923
|
+
# disjoint integer ranges. The `source` array is used only as a hint that the result set's
|
|
924
|
+
# "Integer-ness" was already implied by the inputs (so the widening fallback only fires for
|
|
925
|
+
# arithmetic over integers).
|
|
1029
926
|
def build_constant_type(values, source: nil)
|
|
1030
927
|
return nil if values.empty?
|
|
1031
928
|
|
|
@@ -1041,11 +938,9 @@ module Rigor
|
|
|
1041
938
|
constants.size == 1 ? constants.first : Type::Combinator.union(*constants)
|
|
1042
939
|
end
|
|
1043
940
|
|
|
1044
|
-
# Widening fallback: when every successful result is an
|
|
1045
|
-
#
|
|
1046
|
-
#
|
|
1047
|
-
# the input set being all-integers, so a fold whose results
|
|
1048
|
-
# happen to land on integers but whose receivers were Floats
|
|
941
|
+
# Widening fallback: when every successful result is an Integer, return the bounding `IntegerRange`
|
|
942
|
+
# rather than losing the answer entirely. The fallback is also gated on the input set being
|
|
943
|
+
# all-integers, so a fold whose results happen to land on integers but whose receivers were Floats
|
|
1049
944
|
# does not silently change shape.
|
|
1050
945
|
def widen_to_integer_range(values, source)
|
|
1051
946
|
return nil unless values.all?(Integer)
|
|
@@ -1054,9 +949,8 @@ module Rigor
|
|
|
1054
949
|
Type::Combinator.integer_range(values.min, values.max)
|
|
1055
950
|
end
|
|
1056
951
|
|
|
1057
|
-
# Reserved hook: present so future `:strict` modes can raise
|
|
1058
|
-
#
|
|
1059
|
-
# nil so behaviour is unchanged.
|
|
952
|
+
# Reserved hook: present so future `:strict` modes can raise rather than silently returning nil.
|
|
953
|
+
# Today it always returns nil so behaviour is unchanged.
|
|
1060
954
|
def raise_if_strict
|
|
1061
955
|
nil
|
|
1062
956
|
end
|
|
@@ -1068,9 +962,8 @@ module Rigor
|
|
|
1068
962
|
RANGE_ADDITIVE = Set[:+, :-].freeze
|
|
1069
963
|
RANGE_COMPARISON = Set[:<, :<=, :>, :>=, :==, :!=].freeze
|
|
1070
964
|
|
|
1071
|
-
# Per-operator dispatch table for binary range ops. Each
|
|
1072
|
-
#
|
|
1073
|
-
# `(left, right)` and returning a `Type` or `nil`.
|
|
965
|
+
# Per-operator dispatch table for binary range ops. Each value is a method symbol on
|
|
966
|
+
# `ConstantFolding` taking `(left, right)` and returning a `Type` or `nil`.
|
|
1074
967
|
BINARY_RANGE_HANDLERS = {
|
|
1075
968
|
:* => :range_multiply,
|
|
1076
969
|
:/ => :range_divide,
|
|
@@ -1089,11 +982,9 @@ module Rigor
|
|
|
1089
982
|
nil
|
|
1090
983
|
end
|
|
1091
984
|
|
|
1092
|
-
# Promotes an array-of-values input to an `IntegerRange` when
|
|
1093
|
-
#
|
|
1094
|
-
#
|
|
1095
|
-
# arithmetic. Returns `nil` for non-Integer arrays so a
|
|
1096
|
-
# `Constant[Float]` does not silently degrade.
|
|
985
|
+
# Promotes an array-of-values input to an `IntegerRange` when every value is an `Integer`. Used so a
|
|
986
|
+
# mixed `Constant + IntegerRange` call can be reduced to range × range arithmetic. Returns `nil` for
|
|
987
|
+
# non-Integer arrays so a `Constant[Float]` does not silently degrade.
|
|
1097
988
|
def ensure_integer_range(operand)
|
|
1098
989
|
case operand
|
|
1099
990
|
when Type::IntegerRange then operand
|
|
@@ -1113,10 +1004,9 @@ module Rigor
|
|
|
1113
1004
|
build_integer_range(lower, upper)
|
|
1114
1005
|
end
|
|
1115
1006
|
|
|
1116
|
-
# Range × Range. Computes the four corner products with
|
|
1117
|
-
#
|
|
1118
|
-
#
|
|
1119
|
-
# range elements are integers, never literal infinity.
|
|
1007
|
+
# Range × Range. Computes the four corner products with `safe_mul` so that `0 × ±∞` is treated as 0
|
|
1008
|
+
# rather than NaN — that captures the algebraic truth that the actual range elements are integers,
|
|
1009
|
+
# never literal infinity.
|
|
1120
1010
|
def range_multiply(left, right)
|
|
1121
1011
|
corners = [
|
|
1122
1012
|
safe_mul(left.lower, right.lower),
|
|
@@ -1127,21 +1017,18 @@ module Rigor
|
|
|
1127
1017
|
build_integer_range(corners.min, corners.max)
|
|
1128
1018
|
end
|
|
1129
1019
|
|
|
1130
|
-
# 0 dominates: 0 × anything (including ±∞) is 0. Without this
|
|
1131
|
-
#
|
|
1132
|
-
# would corrupt `min`/`max`.
|
|
1020
|
+
# 0 dominates: 0 × anything (including ±∞) is 0. Without this special case Ruby's
|
|
1021
|
+
# `0 * Float::INFINITY` is `NaN`, which would corrupt `min`/`max`.
|
|
1133
1022
|
def safe_mul(left, right)
|
|
1134
1023
|
return 0 if left.zero? || right.zero?
|
|
1135
1024
|
|
|
1136
1025
|
left * right
|
|
1137
1026
|
end
|
|
1138
1027
|
|
|
1139
|
-
# Range ÷ Range using Ruby's integer floor division. If the
|
|
1140
|
-
#
|
|
1141
|
-
#
|
|
1142
|
-
#
|
|
1143
|
-
# compute the four corner quotients; the universal-on-one-side
|
|
1144
|
-
# case is handled by treating ±∞ ÷ n as ±∞ and n ÷ ±∞ as 0.
|
|
1028
|
+
# Range ÷ Range using Ruby's integer floor division. If the right range covers 0 the operation may
|
|
1029
|
+
# raise `ZeroDivisionError`, so the fold bails (caller falls back to RBS-widened `Integer`). When
|
|
1030
|
+
# both inputs are finite we compute the four corner quotients; the universal-on-one-side case is
|
|
1031
|
+
# handled by treating ±∞ ÷ n as ±∞ and n ÷ ±∞ as 0.
|
|
1145
1032
|
def range_divide(left, right)
|
|
1146
1033
|
return nil if right.covers?(0)
|
|
1147
1034
|
|
|
@@ -1166,10 +1053,9 @@ module Rigor
|
|
|
1166
1053
|
numer.to_i.div(denom.to_i).to_f
|
|
1167
1054
|
end
|
|
1168
1055
|
|
|
1169
|
-
# Range % Range. Only the `(any range) % (positive constant n)`
|
|
1170
|
-
#
|
|
1171
|
-
#
|
|
1172
|
-
# to `int<n+1, 0>`. Other shapes fall back to nil.
|
|
1056
|
+
# Range % Range. Only the `(any range) % (positive constant n)` and `(any range) % (negative
|
|
1057
|
+
# constant n)` cases are folded precisely — the former narrows to `int<0, n-1>`, the latter to
|
|
1058
|
+
# `int<n+1, 0>`. Other shapes fall back to nil.
|
|
1173
1059
|
def range_modulo(_left, right)
|
|
1174
1060
|
return nil unless right.finite? && right.min == right.max
|
|
1175
1061
|
|
|
@@ -1183,10 +1069,9 @@ module Rigor
|
|
|
1183
1069
|
end
|
|
1184
1070
|
end
|
|
1185
1071
|
|
|
1186
|
-
# Builds an `IntegerRange` from numeric `lower`/`upper`
|
|
1187
|
-
#
|
|
1188
|
-
#
|
|
1189
|
-
# specific carrier) see the most precise result.
|
|
1072
|
+
# Builds an `IntegerRange` from numeric `lower`/`upper` endpoints. Collapses single-point finite
|
|
1073
|
+
# ranges to a `Constant` so downstream rules (which prefer the more specific carrier) see the most
|
|
1074
|
+
# precise result.
|
|
1190
1075
|
def build_integer_range(lower, upper)
|
|
1191
1076
|
min = lower == -Float::INFINITY ? Type::IntegerRange::NEG_INFINITY : Integer(lower)
|
|
1192
1077
|
max = upper == Float::INFINITY ? Type::IntegerRange::POS_INFINITY : Integer(upper)
|
|
@@ -1260,10 +1145,9 @@ module Rigor
|
|
|
1260
1145
|
RANGE_UNARY_SHIFTS = Set[:succ, :next, :pred].freeze
|
|
1261
1146
|
RANGE_UNARY_PARITY = Set[:even?, :odd?].freeze
|
|
1262
1147
|
|
|
1263
|
-
# `(method_name) -> handler symbol` for the unary range
|
|
1264
|
-
#
|
|
1265
|
-
#
|
|
1266
|
-
# because they share dispatch logic.
|
|
1148
|
+
# `(method_name) -> handler symbol` for the unary range surface that does not need extra context.
|
|
1149
|
+
# The grouped categories (predicates / shifts / parity) stay separate because they share dispatch
|
|
1150
|
+
# logic.
|
|
1267
1151
|
UNARY_RANGE_DIRECT = {
|
|
1268
1152
|
abs: :range_unary_abs,
|
|
1269
1153
|
magnitude: :range_unary_abs,
|
|
@@ -1295,10 +1179,8 @@ module Rigor
|
|
|
1295
1179
|
range
|
|
1296
1180
|
end
|
|
1297
1181
|
|
|
1298
|
-
# `even?`/`odd?` on a single-point range collapses to an
|
|
1299
|
-
#
|
|
1300
|
-
# contains both an even and an odd value, so the result is
|
|
1301
|
-
# `Union[true, false]`.
|
|
1182
|
+
# `even?`/`odd?` on a single-point range collapses to an exact `Constant[bool]`. Any range spanning
|
|
1183
|
+
# ≥ 2 integers contains both an even and an odd value, so the result is `Union[true, false]`.
|
|
1302
1184
|
def range_unary_parity(range, method_name)
|
|
1303
1185
|
if range.finite? && range.min == range.max
|
|
1304
1186
|
value = range.min.public_send(method_name)
|
|
@@ -1308,11 +1190,10 @@ module Rigor
|
|
|
1308
1190
|
end
|
|
1309
1191
|
end
|
|
1310
1192
|
|
|
1311
|
-
# Integer#bit_length is non-negative and bounded by the
|
|
1312
|
-
#
|
|
1313
|
-
#
|
|
1314
|
-
#
|
|
1315
|
-
# `~n` semantics; using the magnitude is a safe upper bound.
|
|
1193
|
+
# Integer#bit_length is non-negative and bounded by the bit_length of the wider endpoint. For
|
|
1194
|
+
# half-open ranges the upper bound is unknown (any large integer is reachable), so we widen to
|
|
1195
|
+
# non_negative_int. Negative endpoints map via `~n` semantics; using the magnitude is a safe upper
|
|
1196
|
+
# bound.
|
|
1316
1197
|
def range_unary_bit_length(range)
|
|
1317
1198
|
return Type::Combinator.non_negative_int unless range.finite?
|
|
1318
1199
|
|
|
@@ -1377,9 +1258,8 @@ module Rigor
|
|
|
1377
1258
|
|
|
1378
1259
|
# ----------------------------------------------------------------
|
|
1379
1260
|
|
|
1380
|
-
# Returns `[value]` on success, `nil` to signal "skip this pair".
|
|
1381
|
-
#
|
|
1382
|
-
# `false`/`nil` fold from a skipped pair when chaining via
|
|
1261
|
+
# Returns `[value]` on success, `nil` to signal "skip this pair". The 1-element-array shape lets
|
|
1262
|
+
# callers distinguish a successful `false`/`nil` fold from a skipped pair when chaining via
|
|
1383
1263
|
# `flat_map`.
|
|
1384
1264
|
def invoke_binary(receiver_value, method_name, arg_value)
|
|
1385
1265
|
return nil unless safe?(receiver_value, method_name, arg_value)
|
|
@@ -1390,10 +1270,9 @@ module Rigor
|
|
|
1390
1270
|
nil
|
|
1391
1271
|
end
|
|
1392
1272
|
|
|
1393
|
-
# Returns `[value]` on success, `nil` to signal "skip this triple".
|
|
1394
|
-
#
|
|
1395
|
-
#
|
|
1396
|
-
# legitimate `false`/`nil` folds.
|
|
1273
|
+
# Returns `[value]` on success, `nil` to signal "skip this triple". Mirrors `invoke_binary` but for
|
|
1274
|
+
# the 2-argument shape; the wrap convention lets callers `flat_map` without losing legitimate
|
|
1275
|
+
# `false`/`nil` folds.
|
|
1397
1276
|
def invoke_ternary(receiver_value, method_name, av0, av1)
|
|
1398
1277
|
return nil unless ternary_method_allowed?(receiver_value, method_name)
|
|
1399
1278
|
|
|
@@ -1403,8 +1282,7 @@ module Rigor
|
|
|
1403
1282
|
nil
|
|
1404
1283
|
end
|
|
1405
1284
|
|
|
1406
|
-
# Returns `[value]` on success, `nil` to signal "skip". See
|
|
1407
|
-
# `invoke_binary` for why we wrap.
|
|
1285
|
+
# Returns `[value]` on success, `nil` to signal "skip". See `invoke_binary` for why we wrap.
|
|
1408
1286
|
def invoke_unary(receiver_value, method_name)
|
|
1409
1287
|
return nil unless unary_safe?(receiver_value, method_name)
|
|
1410
1288
|
return nil if string_unary_blow_up?(receiver_value, method_name)
|
|
@@ -1421,29 +1299,21 @@ module Rigor
|
|
|
1421
1299
|
catalog_allows?(receiver_value, method_name)
|
|
1422
1300
|
end
|
|
1423
1301
|
|
|
1424
|
-
# Consults the offline numeric catalog (data/builtins/ruby_core/
|
|
1425
|
-
#
|
|
1426
|
-
#
|
|
1427
|
-
#
|
|
1428
|
-
# implementation does not call back into user-redefinable
|
|
1429
|
-
# Ruby methods, so executing them on a literal Integer/Float
|
|
1430
|
-
# is safe regardless of monkey-patching.
|
|
1302
|
+
# Consults the offline numeric catalog (data/builtins/ruby_core/numeric.yml) as a superset of the
|
|
1303
|
+
# hand-rolled unary/binary allow lists. The catalog's `leaf` / `trivial` / `leaf_when_numeric`
|
|
1304
|
+
# entries promise the underlying CRuby implementation does not call back into user-redefinable Ruby
|
|
1305
|
+
# methods, so executing them on a literal Integer/Float is safe regardless of monkey-patching.
|
|
1431
1306
|
#
|
|
1432
1307
|
# Resolution order:
|
|
1433
1308
|
#
|
|
1434
|
-
# 1. Primary class catalog (e.g. NUMERIC_CATALOG for an
|
|
1435
|
-
#
|
|
1436
|
-
#
|
|
1437
|
-
#
|
|
1438
|
-
#
|
|
1439
|
-
# catalog
|
|
1440
|
-
#
|
|
1441
|
-
#
|
|
1442
|
-
# when the primary catalog has NO entry for the method
|
|
1443
|
-
# — typically because the method is inherited purely
|
|
1444
|
-
# through `include Comparable` / `include Enumerable`
|
|
1445
|
-
# (e.g. `Integer#between?` / `Integer#clamp` are not in
|
|
1446
|
-
# numeric.yml because the Init block does not
|
|
1309
|
+
# 1. Primary class catalog (e.g. NUMERIC_CATALOG for an Integer receiver). When the catalog has an
|
|
1310
|
+
# entry — even one classified `:dispatch` — that answer wins. The class's direct
|
|
1311
|
+
# `rb_define_method` registration is authoritative; we MUST NOT fall through to a module catalog
|
|
1312
|
+
# and risk over-folding.
|
|
1313
|
+
# 2. Module catalogs (Comparable, Enumerable, …) that the receiver's class includes by ancestry.
|
|
1314
|
+
# Reached only when the primary catalog has NO entry for the method — typically because the
|
|
1315
|
+
# method is inherited purely through `include Comparable` / `include Enumerable` (e.g.
|
|
1316
|
+
# `Integer#between?` / `Integer#clamp` are not in numeric.yml because the Init block does not
|
|
1447
1317
|
# `rb_define_method` them on Integer).
|
|
1448
1318
|
def catalog_allows?(receiver_value, method_name)
|
|
1449
1319
|
catalog, class_name = catalog_for(receiver_value)
|
|
@@ -1455,11 +1325,9 @@ module Rigor
|
|
|
1455
1325
|
end
|
|
1456
1326
|
end
|
|
1457
1327
|
|
|
1458
|
-
# `(Module, catalog, class_name)` triples consulted as a
|
|
1459
|
-
#
|
|
1460
|
-
#
|
|
1461
|
-
# class's ancestor chain at lookup time; the catalog
|
|
1462
|
-
# corresponds to the module-mode YAML at
|
|
1328
|
+
# `(Module, catalog, class_name)` triples consulted as a fallthrough when the primary class catalog
|
|
1329
|
+
# has no entry. Each triple's Module is matched against the receiver class's ancestor chain at
|
|
1330
|
+
# lookup time; the catalog corresponds to the module-mode YAML at
|
|
1463
1331
|
# `data/builtins/ruby_core/<topic>.yml`.
|
|
1464
1332
|
MODULE_CATALOGS = Ractor.make_shareable([
|
|
1465
1333
|
[Comparable, Builtins::COMPARABLE_CATALOG, "Comparable"],
|
|
@@ -1467,10 +1335,9 @@ module Rigor
|
|
|
1467
1335
|
])
|
|
1468
1336
|
private_constant :MODULE_CATALOGS
|
|
1469
1337
|
|
|
1470
|
-
# Returns the `(catalog, class_name)` pairs for every
|
|
1471
|
-
#
|
|
1472
|
-
#
|
|
1473
|
-
# cached by Ruby; the `Set` membership check is cheap.
|
|
1338
|
+
# Returns the `(catalog, class_name)` pairs for every registered module that is in the receiver's
|
|
1339
|
+
# ancestor chain. The receiver's class's `Module#ancestors` is cached by Ruby; the `Set` membership
|
|
1340
|
+
# check is cheap.
|
|
1474
1341
|
def module_catalogs_for(receiver_value)
|
|
1475
1342
|
ancestors = Set.new(receiver_value.class.ancestors)
|
|
1476
1343
|
MODULE_CATALOGS.filter_map do |mod, catalog, class_name|
|
|
@@ -1478,18 +1345,13 @@ module Rigor
|
|
|
1478
1345
|
end
|
|
1479
1346
|
end
|
|
1480
1347
|
|
|
1481
|
-
# `(catalog, class_name)` per receiver class. The class_name
|
|
1482
|
-
#
|
|
1483
|
-
#
|
|
1484
|
-
#
|
|
1485
|
-
# before
|
|
1486
|
-
#
|
|
1487
|
-
#
|
|
1488
|
-
# Subclass-before-superclass ordering: `DateTime < Date`,
|
|
1489
|
-
# so the `DateTime` row MUST come before the `Date` row.
|
|
1490
|
-
# Otherwise a `DateTime` receiver would match the `Date`
|
|
1491
|
-
# arm first and the catalog would consult the Date entry
|
|
1492
|
-
# in `DATE_CATALOG` for the wrong class.
|
|
1348
|
+
# `(catalog, class_name)` per receiver class. The class_name is what each catalog's RBS-rooted
|
|
1349
|
+
# entries are keyed by. `catalog_for` walks this table in declaration order so subclasses
|
|
1350
|
+
# (Symbol < String) hit their dedicated entry before any base-class fallback would, and adding a
|
|
1351
|
+
# new class is a one-line addition rather than another `when` arm on a growing case statement.
|
|
1352
|
+
# Subclass-before-superclass ordering: `DateTime < Date`, so the `DateTime` row MUST come before the
|
|
1353
|
+
# `Date` row. Otherwise a `DateTime` receiver would match the `Date` arm first and the catalog would
|
|
1354
|
+
# consult the Date entry in `DATE_CATALOG` for the wrong class.
|
|
1493
1355
|
CATALOG_BY_CLASS = Ractor.make_shareable([
|
|
1494
1356
|
[Integer, [Builtins::NUMERIC_CATALOG, "Integer"]],
|
|
1495
1357
|
[Float, [Builtins::NUMERIC_CATALOG, "Float"]],
|
|
@@ -1517,8 +1379,7 @@ module Rigor
|
|
|
1517
1379
|
])
|
|
1518
1380
|
private_constant :CATALOG_BY_CLASS
|
|
1519
1381
|
|
|
1520
|
-
# Returns `[catalog, class_name]` for receivers we have a
|
|
1521
|
-
# catalog for; nil otherwise.
|
|
1382
|
+
# Returns `[catalog, class_name]` for receivers we have a catalog for; nil otherwise.
|
|
1522
1383
|
def catalog_for(receiver_value)
|
|
1523
1384
|
CATALOG_BY_CLASS.each do |klass, entry|
|
|
1524
1385
|
return entry if receiver_value.is_a?(klass)
|
|
@@ -1540,26 +1401,20 @@ module Rigor
|
|
|
1540
1401
|
end
|
|
1541
1402
|
end
|
|
1542
1403
|
|
|
1543
|
-
# `String#reverse` / `#swapcase` / `#succ` etc. produce a string
|
|
1544
|
-
#
|
|
1545
|
-
#
|
|
1546
|
-
#
|
|
1547
|
-
#
|
|
1548
|
-
#
|
|
1549
|
-
# no precision worth the bytes. Non-String receivers never blow up
|
|
1550
|
-
# through a unary op, so they pass.
|
|
1404
|
+
# `String#reverse` / `#swapcase` / `#succ` etc. produce a string at least as large as the receiver.
|
|
1405
|
+
# The binary `:+` / `:*` paths have their own `string_blow_up?` output guard; this is the unary
|
|
1406
|
+
# analogue — decline to fold a unary String op whose receiver is already at or beyond
|
|
1407
|
+
# `STRING_FOLD_BYTE_LIMIT`, since the folded output would be just as large and constant-materialising
|
|
1408
|
+
# it buys no precision worth the bytes. Non-String receivers never blow up through a unary op, so
|
|
1409
|
+
# they pass.
|
|
1551
1410
|
def string_unary_blow_up?(receiver_value, _method_name)
|
|
1552
1411
|
receiver_value.is_a?(String) && receiver_value.bytesize >= STRING_FOLD_BYTE_LIMIT
|
|
1553
1412
|
end
|
|
1554
1413
|
|
|
1555
|
-
# Scalar / String / Symbol values fold; everything
|
|
1556
|
-
#
|
|
1557
|
-
#
|
|
1558
|
-
#
|
|
1559
|
-
# downstream calls. `Range`, `Array`, and friends
|
|
1560
|
-
# have their own shape carriers; this method picks
|
|
1561
|
-
# the conservative envelope of "values that already
|
|
1562
|
-
# round-trip through `Type::Combinator.constant_of`".
|
|
1414
|
+
# Scalar / String / Symbol values fold; everything else (Array, Hash, Proc, Range, ...) is held back
|
|
1415
|
+
# because `Type::Constant` does not model those carriers and surfacing one would mis-type downstream
|
|
1416
|
+
# calls. `Range`, `Array`, and friends have their own shape carriers; this method picks the
|
|
1417
|
+
# conservative envelope of "values that already round-trip through `Type::Combinator.constant_of`".
|
|
1563
1418
|
FOLDABLE_CONSTANT_CLASSES = [
|
|
1564
1419
|
Integer, Float, Rational, Complex, String, Symbol,
|
|
1565
1420
|
Regexp, Pathname, ::Set, Date, Time,
|
|
@@ -1570,11 +1425,10 @@ module Rigor
|
|
|
1570
1425
|
def foldable_constant_value?(value)
|
|
1571
1426
|
return false unless FOLDABLE_CONSTANT_CLASSES.any? { |klass| value.is_a?(klass) }
|
|
1572
1427
|
|
|
1573
|
-
# A NaN result (`0.0 / 0.0`, `Float::NAN`-propagating arithmetic,
|
|
1574
|
-
#
|
|
1575
|
-
# `
|
|
1576
|
-
#
|
|
1577
|
-
# fold and let the RBS tier answer with the widened class.
|
|
1428
|
+
# A NaN result (`0.0 / 0.0`, `Float::NAN`-propagating arithmetic, or a NaN-bearing Complex) is
|
|
1429
|
+
# non-reflexive under `==`, so a `Constant[NaN]` would break the `==` / `eql?` / `hash` contract
|
|
1430
|
+
# `build_constant_type` relies on for union dedup. Decline the fold and let the RBS tier answer
|
|
1431
|
+
# with the widened class.
|
|
1578
1432
|
return false if value.is_a?(Float) && value.nan?
|
|
1579
1433
|
return false if value.is_a?(Complex) && complex_nan?(value)
|
|
1580
1434
|
|
|
@@ -1612,8 +1466,8 @@ module Rigor
|
|
|
1612
1466
|
end
|
|
1613
1467
|
end
|
|
1614
1468
|
|
|
1615
|
-
# Integer / 0 and Integer % 0 raise; Float / 0 and Float / 0.0 return
|
|
1616
|
-
#
|
|
1469
|
+
# Integer / 0 and Integer % 0 raise; Float / 0 and Float / 0.0 return Float::INFINITY or NaN, which
|
|
1470
|
+
# are valid `Constant[Float]` values.
|
|
1617
1471
|
def integer_division_by_zero?(receiver_value, method_name, arg_value)
|
|
1618
1472
|
return false unless %i[/ %].include?(method_name)
|
|
1619
1473
|
return false unless receiver_value.is_a?(Integer)
|
|
@@ -1632,10 +1486,9 @@ module Rigor
|
|
|
1632
1486
|
end
|
|
1633
1487
|
end
|
|
1634
1488
|
|
|
1635
|
-
# `"x".center(width)` / `#ljust` / `#rjust` produce a string
|
|
1636
|
-
#
|
|
1637
|
-
#
|
|
1638
|
-
# cap it at the same byte limit the concat / repeat paths use.
|
|
1489
|
+
# `"x".center(width)` / `#ljust` / `#rjust` produce a string of `max(width, len)` characters. A
|
|
1490
|
+
# literal `width` far larger than the receiver would materialise a huge Constant; cap it at the same
|
|
1491
|
+
# byte limit the concat / repeat paths use.
|
|
1639
1492
|
def string_pad_blow_up?(arg_value)
|
|
1640
1493
|
arg_value.is_a?(Integer) && arg_value > STRING_FOLD_BYTE_LIMIT
|
|
1641
1494
|
end
|