rigortype 0.1.18 → 0.2.0
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 +159 -224
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -3
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +32 -23
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +151 -23
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +9 -3
- data/lib/rigor/analysis/check_rules.rb +756 -132
- data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
- data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
- data/lib/rigor/analysis/diagnostic.rb +8 -0
- data/lib/rigor/analysis/fact_store.rb +5 -4
- data/lib/rigor/analysis/rule_catalog.rb +153 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +19 -18
- data/lib/rigor/analysis/runner/project_pre_passes.rb +13 -9
- data/lib/rigor/analysis/runner.rb +75 -27
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
- data/lib/rigor/analysis/worker_session.rb +31 -25
- data/lib/rigor/bleeding_edge.rb +123 -0
- data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
- data/lib/rigor/cache/descriptor.rb +86 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +5 -3
- data/lib/rigor/cli/annotate_command.rb +122 -16
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/check_command.rb +118 -16
- data/lib/rigor/cli/coverage_command.rb +148 -16
- data/lib/rigor/cli/coverage_scan.rb +57 -0
- data/lib/rigor/cli/explain_command.rb +2 -0
- data/lib/rigor/cli/lsp_command.rb +3 -7
- data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
- data/lib/rigor/cli/mutation_protection_report.rb +73 -0
- data/lib/rigor/cli/options.rb +9 -0
- data/lib/rigor/cli/plugins_command.rb +4 -5
- data/lib/rigor/cli/plugins_renderer.rb +0 -2
- data/lib/rigor/cli/protection_renderer.rb +63 -0
- data/lib/rigor/cli/protection_report.rb +68 -0
- data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
- data/lib/rigor/cli/sig_gen_command.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +2 -1
- data/lib/rigor/cli/triage_command.rb +8 -4
- data/lib/rigor/cli/triage_renderer.rb +15 -1
- data/lib/rigor/cli/type_of_command.rb +1 -1
- data/lib/rigor/cli/type_scan_command.rb +2 -1
- data/lib/rigor/cli.rb +12 -3
- data/lib/rigor/configuration/dependencies.rb +2 -4
- data/lib/rigor/configuration/severity_profile.rb +13 -1
- data/lib/rigor/configuration.rb +100 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
- data/lib/rigor/environment/class_registry.rb +4 -3
- data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
- data/lib/rigor/environment/lockfile_resolver.rb +1 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
- data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
- data/lib/rigor/environment/rbs_loader.rb +74 -5
- data/lib/rigor/environment.rb +17 -7
- data/lib/rigor/flow_contribution/fact.rb +1 -1
- data/lib/rigor/flow_contribution.rb +3 -5
- data/lib/rigor/inference/acceptance.rb +17 -9
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/body_fixpoint.rb +89 -0
- data/lib/rigor/inference/budget_trace.rb +29 -2
- data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
- data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
- data/lib/rigor/inference/expression_typer.rb +1072 -71
- data/lib/rigor/inference/hkt_body.rb +8 -11
- data/lib/rigor/inference/hkt_body_parser.rb +10 -12
- data/lib/rigor/inference/hkt_registry.rb +10 -11
- data/lib/rigor/inference/macro_block_self_type.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
- data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +210 -35
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +237 -24
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
- data/lib/rigor/inference/method_dispatcher.rb +112 -49
- data/lib/rigor/inference/method_parameter_binder.rb +56 -2
- data/lib/rigor/inference/multi_target_binder.rb +46 -3
- data/lib/rigor/inference/mutation_widening.rb +147 -11
- data/lib/rigor/inference/narrowing.rb +284 -53
- data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
- data/lib/rigor/inference/project_patched_methods.rb +4 -7
- data/lib/rigor/inference/project_patched_scanner.rb +2 -13
- data/lib/rigor/inference/protection_scanner.rb +86 -0
- data/lib/rigor/inference/scope_indexer.rb +821 -76
- data/lib/rigor/inference/statement_evaluator.rb +1179 -102
- data/lib/rigor/inference/struct_fold_safety.rb +181 -0
- data/lib/rigor/inference/synthetic_method.rb +7 -7
- data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
- data/lib/rigor/language_server/completion_provider.rb +6 -12
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
- data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
- data/lib/rigor/language_server/hover_provider.rb +2 -3
- data/lib/rigor/language_server/hover_renderer.rb +2 -11
- data/lib/rigor/language_server/server.rb +9 -17
- data/lib/rigor/language_server.rb +4 -5
- data/lib/rigor/plugin/base.rb +245 -87
- data/lib/rigor/plugin/macro/block_as_method.rb +25 -25
- data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
- data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
- data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
- data/lib/rigor/plugin/macro.rb +6 -8
- data/lib/rigor/plugin/manifest.rb +49 -90
- data/lib/rigor/plugin/node_rule_walk.rb +59 -14
- data/lib/rigor/plugin/registry.rb +18 -18
- data/lib/rigor/plugin/type_node_resolver.rb +6 -8
- data/lib/rigor/protection/mutation_scanner.rb +120 -0
- data/lib/rigor/protection/mutator.rb +246 -0
- data/lib/rigor/rbs_extended.rb +24 -36
- data/lib/rigor/reflection.rb +4 -7
- data/lib/rigor/scope/discovery_index.rb +16 -2
- data/lib/rigor/scope.rb +185 -16
- data/lib/rigor/sig_gen/generator.rb +8 -0
- data/lib/rigor/sig_gen/observed_call.rb +3 -3
- data/lib/rigor/sig_gen/writer.rb +40 -2
- data/lib/rigor/source/constant_path.rb +62 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/triage/catalogue.rb +4 -19
- data/lib/rigor/triage.rb +69 -1
- data/lib/rigor/type/bound_method.rb +2 -11
- data/lib/rigor/type/combinator.rb +45 -3
- data/lib/rigor/type/constant.rb +2 -11
- data/lib/rigor/type/data_class.rb +2 -11
- data/lib/rigor/type/data_instance.rb +2 -11
- data/lib/rigor/type/hash_shape.rb +2 -11
- data/lib/rigor/type/integer_range.rb +2 -11
- data/lib/rigor/type/intersection.rb +2 -11
- data/lib/rigor/type/nominal.rb +2 -11
- data/lib/rigor/type/plain_lattice.rb +37 -0
- data/lib/rigor/type/refined.rb +72 -13
- data/lib/rigor/type/singleton.rb +2 -11
- data/lib/rigor/type/struct_class.rb +75 -0
- data/lib/rigor/type/struct_instance.rb +93 -0
- data/lib/rigor/type/tuple.rb +5 -15
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +16 -32
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +34 -100
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +26 -27
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +9 -8
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +18 -49
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +4 -4
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +22 -35
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +16 -23
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -4
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +21 -27
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +52 -40
- data/sig/rigor/analysis/fact_store.rbs +3 -0
- data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
- data/sig/rigor/plugin/base.rbs +5 -2
- data/sig/rigor/plugin/manifest.rbs +1 -2
- data/sig/rigor/scope.rbs +18 -1
- data/sig/rigor/type.rbs +37 -1
- data/sig/rigor.rbs +1 -1
- data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
- data/skills/rigor-plugin-author/SKILL.md +6 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
- metadata +25 -2
- data/lib/rigor/plugin/macro/external_file.rb +0 -143
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
require_relative "call_context"
|
|
5
|
+
require_relative "iterator_dispatch"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Inference
|
|
9
|
+
module MethodDispatcher
|
|
10
|
+
# Symbol-form `reduce` / `inject` return-type tier.
|
|
11
|
+
#
|
|
12
|
+
# `IteratorDispatch.inject_block_params` deliberately declines the
|
|
13
|
+
# Symbol-call shapes (`(1..n).reduce(1, :*)`, `[1,2,3].reduce(:+)`)
|
|
14
|
+
# because they carry no block to bind parameters for — the decline
|
|
15
|
+
# of *block-param typing* is correct. What that decline leaves on
|
|
16
|
+
# the floor is the *return type*: with no block and no precise tier,
|
|
17
|
+
# the call falls to `Enumerable#reduce`'s RBS overload
|
|
18
|
+
# `(untyped, Symbol) -> untyped`, so the whole fold widens to
|
|
19
|
+
# `Dynamic[top]`.
|
|
20
|
+
#
|
|
21
|
+
# This tier recovers a precise return type for the Symbol-operand
|
|
22
|
+
# forms by dispatching the named operator on the accumulated type:
|
|
23
|
+
#
|
|
24
|
+
# - `(seed, :op)` (2-arg) — operand starts at the seed type `S`;
|
|
25
|
+
# the result type is `dispatch(:op, widen(S) ∪ widen(E), widen(E))`
|
|
26
|
+
# joined with the seed (the seed is returned unchanged when the
|
|
27
|
+
# collection is empty). `E` is the receiver's element type.
|
|
28
|
+
# - `(:op)` (1-arg) — no seed: the first element seeds the memo, so
|
|
29
|
+
# the operand type is `E` and the result is
|
|
30
|
+
# `dispatch(:op, widen(E), widen(E))`. RBS models this overload as
|
|
31
|
+
# `() { (E, E) -> E } -> E` (no nil), so the tier returns the
|
|
32
|
+
# operator result without manufacturing a nil — staying consistent
|
|
33
|
+
# with the declared type and Rigor's false-positive discipline
|
|
34
|
+
# (the empty-collection `nil` runtime case is not modelled by RBS
|
|
35
|
+
# and adding it here would only pressure callers into defensive
|
|
36
|
+
# nil-handling for code that works).
|
|
37
|
+
#
|
|
38
|
+
# The tier is precision-additive: it declines (returns nil, today's
|
|
39
|
+
# `Dynamic[top]` behaviour) for every shape it cannot prove —
|
|
40
|
+
# unknown element type, Dynamic / Top receiver, a non-`Constant`
|
|
41
|
+
# Symbol operand, or an operator the engine cannot dispatch on the
|
|
42
|
+
# widened operand types.
|
|
43
|
+
#
|
|
44
|
+
# `widen_value_pinned` (ADR-55/56) collapses `Constant`/`IntegerRange`
|
|
45
|
+
# operands to their nominal base before dispatch so the result is the
|
|
46
|
+
# operator's nominal return (`Integer`) rather than a constant-folded
|
|
47
|
+
# `Constant[120]` — full constant folding of the reduction is out of
|
|
48
|
+
# scope, the precision target is the carrier (`Integer`), not the value.
|
|
49
|
+
module ReduceFolding
|
|
50
|
+
module_function
|
|
51
|
+
|
|
52
|
+
REDUCE_METHODS = %i[reduce inject].freeze
|
|
53
|
+
private_constant :REDUCE_METHODS
|
|
54
|
+
|
|
55
|
+
# Allow-listed pure, side-effect-free fold operators. Each is
|
|
56
|
+
# safe on the foldable constant operand classes (Integer / Float
|
|
57
|
+
# / Rational); division / modulo by zero is caught by the rescue
|
|
58
|
+
# harness in `execute_constant_reduce` and declines to the
|
|
59
|
+
# nominal fold. `:gcd` / `:lcm` are valid binary Integer methods
|
|
60
|
+
# (`acc.public_send(:gcd, elem)`); `:min` / `:max` are *not* (no
|
|
61
|
+
# `Integer#min`/`#max` — they would raise and decline), so they
|
|
62
|
+
# are deliberately omitted.
|
|
63
|
+
CONSTANT_FOLD_OPERATORS = %i[+ * - / % & | ^ gcd lcm].freeze
|
|
64
|
+
private_constant :CONSTANT_FOLD_OPERATORS
|
|
65
|
+
|
|
66
|
+
# Hard caps (ADR-41 WD4 style). The element count is checked
|
|
67
|
+
# BEFORE the receiver is enumerated, so a `(1..1_000_000)` range
|
|
68
|
+
# never materialises. The bit-length cap rejects factorial-style
|
|
69
|
+
# blow-up (`(1..64).reduce(:*)` is a ~296-bit Integer) so the
|
|
70
|
+
# folded value can never become an analyzer-heavy bignum.
|
|
71
|
+
CONSTANT_FOLD_ELEMENT_CAP = 64
|
|
72
|
+
CONSTANT_FOLD_BIT_CAP = 256
|
|
73
|
+
private_constant :CONSTANT_FOLD_ELEMENT_CAP, :CONSTANT_FOLD_BIT_CAP
|
|
74
|
+
|
|
75
|
+
# @return [Rigor::Type, nil]
|
|
76
|
+
def try_dispatch(context)
|
|
77
|
+
return nil unless REDUCE_METHODS.include?(context.method_name)
|
|
78
|
+
return nil if context.block_type
|
|
79
|
+
|
|
80
|
+
args = context.args
|
|
81
|
+
operator, seed = operator_and_seed(args)
|
|
82
|
+
return nil if operator.nil?
|
|
83
|
+
|
|
84
|
+
# Precision pre-check: when the receiver is a fully-constant
|
|
85
|
+
# collection (a `Constant[Range]` with foldable endpoints or a
|
|
86
|
+
# `Tuple` of `Constant` elements) and the operator is an
|
|
87
|
+
# allow-listed pure op, treat the reduction as a pure function
|
|
88
|
+
# and execute it on the real values, capped. Any decline (size
|
|
89
|
+
# cap, non-constant member, magnitude, exception) falls through
|
|
90
|
+
# to the carrier-precise nominal fold below — byte-identical to
|
|
91
|
+
# pre-fold behaviour.
|
|
92
|
+
folded = try_constant_reduce(context.receiver, operator, seed)
|
|
93
|
+
return folded if folded
|
|
94
|
+
|
|
95
|
+
element = IteratorDispatch.element_type_of(context.receiver)
|
|
96
|
+
return nil if element.nil?
|
|
97
|
+
|
|
98
|
+
fold_result(operator, seed, element, context.environment)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Executes the reduction on real constant values, or returns nil
|
|
102
|
+
# to decline. The caller falls through to the nominal fold on a
|
|
103
|
+
# nil return, so every guard here is precision-additive only.
|
|
104
|
+
#
|
|
105
|
+
# @param seed [Rigor::Type, nil] the optional seed type; only a
|
|
106
|
+
# foldable `Constant` seed is honoured, any other seed declines.
|
|
107
|
+
# @return [Rigor::Type::Constant, nil]
|
|
108
|
+
def try_constant_reduce(receiver, operator, seed)
|
|
109
|
+
return nil unless CONSTANT_FOLD_OPERATORS.include?(operator)
|
|
110
|
+
|
|
111
|
+
members = constant_members(receiver)
|
|
112
|
+
return nil if members.nil?
|
|
113
|
+
|
|
114
|
+
seed_value, seed_present = constant_seed(seed)
|
|
115
|
+
return nil if !seed.nil? && !seed_present
|
|
116
|
+
|
|
117
|
+
execute_constant_reduce(members, operator, seed_value, seed_present)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Extracts the receiver's elements as a Ruby Array of foldable
|
|
121
|
+
# constant values, or nil to decline. Checks the size cap BEFORE
|
|
122
|
+
# enumerating so an unbounded / huge `Constant[Range]` (e.g.
|
|
123
|
+
# `(1..1_000_000)`) never calls `to_a`.
|
|
124
|
+
#
|
|
125
|
+
# @return [Array, nil]
|
|
126
|
+
def constant_members(receiver)
|
|
127
|
+
case receiver
|
|
128
|
+
when Type::Constant
|
|
129
|
+
constant_range_members(receiver.value)
|
|
130
|
+
when Type::Tuple
|
|
131
|
+
tuple_members(receiver.elements)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def constant_range_members(value)
|
|
136
|
+
return nil unless value.is_a?(Range)
|
|
137
|
+
|
|
138
|
+
first = value.begin
|
|
139
|
+
last = value.end
|
|
140
|
+
return nil unless foldable?(first) && foldable?(last)
|
|
141
|
+
# Reject endless / beginless ranges (`(1..)`, `(..5)`).
|
|
142
|
+
return nil if first.nil? || last.nil?
|
|
143
|
+
|
|
144
|
+
# Size BEFORE enumeration — `Range#size` is O(1) for numeric
|
|
145
|
+
# ranges and never materialises the elements.
|
|
146
|
+
size = value.size
|
|
147
|
+
return nil unless size.is_a?(Integer)
|
|
148
|
+
return nil if size > CONSTANT_FOLD_ELEMENT_CAP
|
|
149
|
+
|
|
150
|
+
value.to_a
|
|
151
|
+
rescue StandardError
|
|
152
|
+
nil
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def tuple_members(elements)
|
|
156
|
+
return nil if elements.size > CONSTANT_FOLD_ELEMENT_CAP
|
|
157
|
+
return nil unless elements.all? { |e| e.is_a?(Type::Constant) && foldable?(e.value) }
|
|
158
|
+
|
|
159
|
+
elements.map(&:value)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# @return [Array(Object, Boolean)] `[seed_value, present?]`. A nil
|
|
163
|
+
# seed type yields `[nil, false]` (no-seed form). A foldable
|
|
164
|
+
# `Constant` seed yields `[value, true]`. Any other seed yields
|
|
165
|
+
# `[nil, false]` so the caller can decline.
|
|
166
|
+
def constant_seed(seed)
|
|
167
|
+
return [nil, false] if seed.nil?
|
|
168
|
+
return [nil, false] unless seed.is_a?(Type::Constant) && foldable?(seed.value)
|
|
169
|
+
|
|
170
|
+
[seed.value, true]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Runs the actual reduction, guarded by the rescue harness and
|
|
174
|
+
# the magnitude cap. Empty-collection semantics match Ruby:
|
|
175
|
+
# `[].reduce(s, :op) == s` (seed form) and `[].reduce(:op) == nil`
|
|
176
|
+
# (no-seed form → declines so the nominal fold's no-nil contract
|
|
177
|
+
# stands; see `fold_result`).
|
|
178
|
+
#
|
|
179
|
+
# @return [Rigor::Type::Constant, nil]
|
|
180
|
+
def execute_constant_reduce(members, operator, seed_value, seed_present)
|
|
181
|
+
result =
|
|
182
|
+
if seed_present
|
|
183
|
+
members.reduce(seed_value) { |acc, e| acc.public_send(operator, e) }
|
|
184
|
+
else
|
|
185
|
+
# No-seed empty collection reduces to nil; decline so the
|
|
186
|
+
# nominal no-nil contract is preserved rather than folding
|
|
187
|
+
# to `Constant[nil]`.
|
|
188
|
+
return nil if members.empty?
|
|
189
|
+
|
|
190
|
+
members.reduce { |acc, e| acc.public_send(operator, e) }
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
return nil unless foldable?(result)
|
|
194
|
+
return nil if magnitude_too_large?(result)
|
|
195
|
+
|
|
196
|
+
Type::Combinator.constant_of(result)
|
|
197
|
+
rescue StandardError
|
|
198
|
+
# Division by zero, type mismatch, or any operator error: decline.
|
|
199
|
+
nil
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
FOLDABLE_FOLD_CLASSES = [Integer, Float, Rational].freeze
|
|
203
|
+
private_constant :FOLDABLE_FOLD_CLASSES
|
|
204
|
+
|
|
205
|
+
def foldable?(value)
|
|
206
|
+
FOLDABLE_FOLD_CLASSES.any? { |klass| value.is_a?(klass) }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Rejects bignum blow-up. A folded Integer wider than the bit cap
|
|
210
|
+
# (factorial, repeated multiplication) declines to the nominal
|
|
211
|
+
# `Integer` carrier rather than parking a heavy literal in the
|
|
212
|
+
# type graph. Float / Rational carry no comparable blow-up risk.
|
|
213
|
+
def magnitude_too_large?(result)
|
|
214
|
+
result.is_a?(Integer) && result.bit_length > CONSTANT_FOLD_BIT_CAP
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Splits the call's positional arguments into the operator Symbol
|
|
218
|
+
# and the optional seed. Returns `[nil, nil]` for any non-Symbol-
|
|
219
|
+
# operand shape so `try_dispatch` declines.
|
|
220
|
+
#
|
|
221
|
+
# - `[:op]` -> operator `:op`, no seed
|
|
222
|
+
# - `[seed, :op]` -> operator `:op`, seed `seed`
|
|
223
|
+
def operator_and_seed(args)
|
|
224
|
+
case args.size
|
|
225
|
+
when 1
|
|
226
|
+
sym = symbol_value(args[0])
|
|
227
|
+
sym ? [sym, nil] : [nil, nil]
|
|
228
|
+
when 2
|
|
229
|
+
sym = symbol_value(args[1])
|
|
230
|
+
sym ? [sym, args[0]] : [nil, nil]
|
|
231
|
+
else
|
|
232
|
+
[nil, nil]
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def symbol_value(type)
|
|
237
|
+
return nil unless type.is_a?(Type::Constant)
|
|
238
|
+
|
|
239
|
+
type.value.is_a?(Symbol) ? type.value : nil
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Dispatches the operator on the widened operand types. With a
|
|
243
|
+
# seed the memo type spans `seed | element` (the first iteration's
|
|
244
|
+
# memo is the seed, every later iteration's memo is a previous
|
|
245
|
+
# operator result); without a seed the memo and operand are both
|
|
246
|
+
# the element type.
|
|
247
|
+
def fold_result(operator, seed, element, environment)
|
|
248
|
+
widened_element = Type::Combinator.widen_value_pinned(element)
|
|
249
|
+
memo = if seed.nil?
|
|
250
|
+
widened_element
|
|
251
|
+
else
|
|
252
|
+
Type::Combinator.union(
|
|
253
|
+
Type::Combinator.widen_value_pinned(seed), widened_element
|
|
254
|
+
)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
result = dispatch_operator(memo, operator, widened_element, environment)
|
|
258
|
+
return nil if result.nil?
|
|
259
|
+
|
|
260
|
+
# The seed itself is the result when the collection is empty
|
|
261
|
+
# (`[].reduce(s, :op) == s`), so a 2-arg fold's static type is
|
|
262
|
+
# the operator result joined with the seed. The seed is widened
|
|
263
|
+
# (`Constant[0]` -> `Integer`) for the join so the carrier stays
|
|
264
|
+
# the precision target rather than leaking a value-pinned member
|
|
265
|
+
# (`0 | Integer`) — full constant folding of the fold is out of
|
|
266
|
+
# scope.
|
|
267
|
+
return result if seed.nil?
|
|
268
|
+
|
|
269
|
+
Type::Combinator.union(Type::Combinator.widen_value_pinned(seed), result)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def dispatch_operator(memo, operator, operand, environment)
|
|
273
|
+
MethodDispatcher.dispatch(
|
|
274
|
+
receiver_type: memo, method_name: operator,
|
|
275
|
+
arg_types: [operand], environment: environment
|
|
276
|
+
)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
@@ -38,10 +38,81 @@ module Rigor
|
|
|
38
38
|
return nil unless SingletonFolding.receiver?(receiver, "Regexp")
|
|
39
39
|
return fold_escape(args) if REGEXP_ESCAPE_METHODS.include?(method_name)
|
|
40
40
|
return fold_new(args) if method_name == :new
|
|
41
|
+
return fold_last_match(context) if method_name == :last_match
|
|
41
42
|
|
|
42
43
|
nil
|
|
43
44
|
end
|
|
44
45
|
|
|
46
|
+
# `Regexp.last_match` reads the same match-data slot the perlish
|
|
47
|
+
# `$~` global tracks. On a proven-match edge — the truthy branch
|
|
48
|
+
# of `if str =~ /re/`, the surviving path of `unless /re/ =~ s;
|
|
49
|
+
# raise; end`, a `case`/`when` regex arm — the flow engine has
|
|
50
|
+
# already narrowed `$~` to a non-nil `MatchData` (see
|
|
51
|
+
# `Narrowing#regex_match_predicate_scopes`). Upstream RBS types
|
|
52
|
+
# `Regexp.last_match` as `() -> MatchData?` / `(int) -> String?`,
|
|
53
|
+
# so without this consult the call drops back to the nilable
|
|
54
|
+
# return even where the surrounding flow has *proven* the match
|
|
55
|
+
# succeeded — re-introducing the `possible nil receiver` the `$~`
|
|
56
|
+
# narrowing exists to remove the moment the code reaches for the
|
|
57
|
+
# match through the method rather than the global.
|
|
58
|
+
#
|
|
59
|
+
# Mirrors the global's narrowing exactly, so it rides the same
|
|
60
|
+
# `Scope#forget_match_globals` invalidation (an intervening call
|
|
61
|
+
# that can re-run a match clears `$~`, and this consult clears
|
|
62
|
+
# with it):
|
|
63
|
+
#
|
|
64
|
+
# * 0-arg -> `MatchData` (the narrowed `$~`)
|
|
65
|
+
# * `(N)` -> `String` when capture group N is unconditional on
|
|
66
|
+
# every successful match (the same set the `$N`
|
|
67
|
+
# globals narrow), else `String?` (an optional /
|
|
68
|
+
# alternation-reachable group is nil at runtime even
|
|
69
|
+
# on a successful match).
|
|
70
|
+
# * `(:name)` / `(0)` and other forms defer to RBS.
|
|
71
|
+
#
|
|
72
|
+
# Declines (returns nil -> RBS) whenever `$~` is NOT proven
|
|
73
|
+
# non-nil: no match edge established it, or a falsey edge bound it
|
|
74
|
+
# to `Constant[nil]`. Narrowing only on the proven edge keeps the
|
|
75
|
+
# bare `m = Regexp.last_match; m[...]` (no preceding match) firing
|
|
76
|
+
# exactly as today — this never invents a match.
|
|
77
|
+
def fold_last_match(context)
|
|
78
|
+
scope = context.scope
|
|
79
|
+
return nil if scope.nil?
|
|
80
|
+
|
|
81
|
+
match_data = scope.global(:$~)
|
|
82
|
+
return nil unless proven_match?(match_data)
|
|
83
|
+
|
|
84
|
+
args = context.args
|
|
85
|
+
return Type::Combinator.nominal_of("MatchData") if args.empty?
|
|
86
|
+
return nil unless args.size == 1
|
|
87
|
+
|
|
88
|
+
fold_last_match_group(args.first, scope)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Narrowed `$~` is a non-nil `Nominal[MatchData]`. A bare
|
|
92
|
+
# `MatchData?` / `nil` / Dynamic binding is NOT a proven match.
|
|
93
|
+
def proven_match?(match_data)
|
|
94
|
+
match_data.is_a?(Type::Nominal) && match_data.class_name == "MatchData"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# `Regexp.last_match(N)` for a value-pinned positive Integer N.
|
|
98
|
+
# Track the matching `$N` global the predicate edge already
|
|
99
|
+
# narrowed: present (`String`) means the group is unconditional,
|
|
100
|
+
# absent means it is optional / alternation-reachable (nil at
|
|
101
|
+
# runtime on a success), so we surface `String?`. A non-constant
|
|
102
|
+
# or non-positive index defers to RBS.
|
|
103
|
+
def fold_last_match_group(arg, scope)
|
|
104
|
+
return nil unless arg.is_a?(Type::Constant)
|
|
105
|
+
|
|
106
|
+
index = arg.value
|
|
107
|
+
return nil unless index.is_a?(Integer) && index.positive?
|
|
108
|
+
|
|
109
|
+
string_t = Type::Combinator.nominal_of("String")
|
|
110
|
+
group_global = scope.global(:"$#{index}")
|
|
111
|
+
return string_t if group_global.is_a?(Type::Nominal) && group_global.class_name == "String"
|
|
112
|
+
|
|
113
|
+
Type::Combinator.union(string_t, Type::Combinator.constant_of(nil))
|
|
114
|
+
end
|
|
115
|
+
|
|
45
116
|
# `Regexp.escape(str)` / `.quote(str)` — one String arg.
|
|
46
117
|
def fold_escape(args)
|
|
47
118
|
return nil unless args.size == 1
|