rigortype 0.1.19 → 0.2.1
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 +41 -6
- data/data/core_overlay/numeric.rbs +33 -0
- data/data/core_overlay/pathname.rbs +25 -0
- data/data/core_overlay/string_scanner.rbs +28 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +473 -0
- data/data/vendored_gem_sigs/ast/ast.rbs +130 -0
- data/data/vendored_gem_sigs/bcrypt/bcrypt.rbs +47 -0
- data/data/vendored_gem_sigs/bundler/bundler.rbs +238 -0
- data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +34 -0
- data/data/vendored_gem_sigs/did_you_mean/did_you_mean_extras.rbs +34 -0
- data/data/vendored_gem_sigs/idn-ruby/idn.rbs +54 -0
- data/data/vendored_gem_sigs/mysql2/client.rbs +55 -0
- data/data/vendored_gem_sigs/mysql2/error.rbs +5 -0
- data/data/vendored_gem_sigs/mysql2/result.rbs +31 -0
- data/data/vendored_gem_sigs/mysql2/statement.rbs +5 -0
- data/data/vendored_gem_sigs/nokogiri/nokogiri.rbs +2332 -0
- data/data/vendored_gem_sigs/nokogiri/nokogiri_html5.rbs +47 -0
- data/data/vendored_gem_sigs/pg/pg.rbs +212 -0
- data/data/vendored_gem_sigs/prism/prism_supplement.rbs +44 -0
- data/data/vendored_gem_sigs/redis/errors.rbs +50 -0
- data/data/vendored_gem_sigs/redis/future.rbs +5 -0
- data/data/vendored_gem_sigs/redis/redis.rbs +348 -0
- data/data/vendored_gem_sigs/redis/redis_extras.rbs +130 -0
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +226 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -23
- data/lib/rigor/analysis/check_rules/rule_walk.rb +3 -21
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
- data/lib/rigor/analysis/check_rules.rb +492 -71
- 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/fact_store.rb +5 -4
- data/lib/rigor/analysis/rule_catalog.rb +153 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +17 -17
- data/lib/rigor/analysis/runner/project_pre_passes.rb +9 -8
- data/lib/rigor/analysis/runner.rb +17 -6
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
- data/lib/rigor/analysis/worker_session.rb +10 -14
- data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
- data/lib/rigor/cache/store.rb +5 -3
- data/lib/rigor/cli/annotate_command.rb +28 -7
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/check_command.rb +138 -16
- data/lib/rigor/cli/coverage_command.rb +138 -31
- data/lib/rigor/cli/coverage_mutation.rb +149 -0
- data/lib/rigor/cli/coverage_scan.rb +57 -0
- data/lib/rigor/cli/explain_command.rb +2 -0
- data/lib/rigor/cli/fused_protection_renderer.rb +67 -0
- data/lib/rigor/cli/fused_protection_report.rb +76 -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 +2 -1
- data/lib/rigor/cli/protection_renderer.rb +63 -0
- data/lib/rigor/cli/protection_report.rb +68 -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 +2 -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 +3 -2
- data/lib/rigor/config_audit.rb +152 -0
- data/lib/rigor/configuration/dependencies.rb +2 -4
- data/lib/rigor/configuration.rb +57 -7
- 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 +76 -5
- data/lib/rigor/environment.rb +66 -8
- 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/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 +20 -28
- 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/method_dispatcher/call_context.rb +1 -4
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +169 -24
- 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/shape_dispatch.rb +90 -15
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
- data/lib/rigor/inference/method_dispatcher.rb +40 -48
- data/lib/rigor/inference/mutation_widening.rb +5 -11
- data/lib/rigor/inference/narrowing.rb +14 -16
- 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 +129 -55
- data/lib/rigor/inference/statement_evaluator.rb +271 -114
- data/lib/rigor/inference/struct_fold_safety.rb +181 -0
- data/lib/rigor/inference/synthetic_method.rb +7 -7
- 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 +10 -8
- data/lib/rigor/plugin/macro/block_as_method.rb +3 -4
- data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
- data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
- data/lib/rigor/plugin/macro.rb +4 -5
- data/lib/rigor/plugin/manifest.rb +45 -66
- data/lib/rigor/plugin/registry.rb +6 -7
- data/lib/rigor/plugin/type_node_resolver.rb +6 -8
- data/lib/rigor/protection/diagnostic_oracle.rb +51 -0
- data/lib/rigor/protection/mutation_scanner.rb +180 -0
- data/lib/rigor/protection/mutator.rb +267 -0
- data/lib/rigor/protection/test_suite_oracle.rb +68 -0
- data/lib/rigor/rbs_extended.rb +24 -36
- data/lib/rigor/reflection.rb +4 -7
- data/lib/rigor/scope/discovery_index.rb +14 -2
- data/lib/rigor/scope.rb +54 -11
- data/lib/rigor/sig_gen/observed_call.rb +3 -3
- data/lib/rigor/sig_gen/writer.rb +40 -2
- data/lib/rigor/signature_path_audit.rb +92 -0
- data/lib/rigor/source/constant_path.rb +62 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/bound_method.rb +2 -11
- data/lib/rigor/type/combinator.rb +16 -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 +3 -3
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +7 -10
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +6 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -2
- 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 +7 -9
- 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 +3 -3
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
- 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 +1 -1
- 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 +5 -5
- 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 +19 -14
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
- 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 +28 -41
- data/sig/rigor/scope.rbs +9 -1
- data/sig/rigor/type.rbs +36 -1
- metadata +49 -1
|
@@ -40,7 +40,7 @@ module Rigor
|
|
|
40
40
|
# receiver/argument combination.
|
|
41
41
|
#
|
|
42
42
|
# Anything else returns `nil`, signalling "no rule matched" so the
|
|
43
|
-
# caller (`
|
|
43
|
+
# caller (`MethodDispatcher`) falls back to `Dynamic[Top]` and records a
|
|
44
44
|
# fail-soft event. Slice 4 (RBS-backed) layers another dispatch tier
|
|
45
45
|
# behind this rule book, but the constant-folding semantics defined
|
|
46
46
|
# here MUST NOT regress: any value reachable by literal arithmetic at
|
|
@@ -51,7 +51,7 @@ module Rigor
|
|
|
51
51
|
NUMERIC_BINARY = Set[
|
|
52
52
|
:+, :-, :*, :/, :%, :**, :&, :|, :^, :<<, :>>,
|
|
53
53
|
:<, :<=, :>, :>=, :==, :!=, :<=>,
|
|
54
|
-
:gcd, :lcm, :fdiv
|
|
54
|
+
:gcd, :lcm, :fdiv, :quo, :ceildiv, :[]
|
|
55
55
|
].freeze
|
|
56
56
|
STRING_BINARY = Set[
|
|
57
57
|
:+, :*, :==, :!=, :<, :<=, :>, :>=, :<=>,
|
|
@@ -60,12 +60,31 @@ module Rigor
|
|
|
60
60
|
:match?, :index, :rindex, :center, :ljust, :rjust,
|
|
61
61
|
# 1-arg pure transforms/queries whose output never exceeds the
|
|
62
62
|
# input: `delete`/`squeeze` shrink the string, `count` → Integer.
|
|
63
|
-
:delete, :count, :squeeze
|
|
63
|
+
:delete, :count, :squeeze,
|
|
64
|
+
# ASCII / Unicode-case-fold comparison — deterministic, no
|
|
65
|
+
# locale read: `casecmp` → -1/0/1, `casecmp?` → bool/nil.
|
|
66
|
+
:casecmp, :casecmp?
|
|
64
67
|
].freeze
|
|
65
68
|
SYMBOL_BINARY = Set[:==, :!=, :<=>, :<, :<=, :>, :>=].freeze
|
|
66
69
|
BOOL_BINARY = Set[:&, :|, :^, :==, :!=, :===].freeze
|
|
67
70
|
NIL_BINARY = Set[:==, :!=].freeze
|
|
68
|
-
|
|
71
|
+
# Rational arithmetic / ordering are exact and pure. Division
|
|
72
|
+
# (`/`) and `**` may return a `Float`/`Complex` for some operands,
|
|
73
|
+
# all of which are foldable `Constant` value classes. `==` / `!=`
|
|
74
|
+
# are deliberately EXCLUDED: `Rational#==` (`nurat_eqeq_p`) routes
|
|
75
|
+
# through `rb_funcall(:==)` on the operands — user-redefinable —
|
|
76
|
+
# so the catalog classifies it `:dispatch` and the equality stays
|
|
77
|
+
# the RBS `bool`. (The set would otherwise bypass that gate.)
|
|
78
|
+
RATIONAL_BINARY = Set[
|
|
79
|
+
:+, :-, :*, :/, :**, :<=>, :<, :<=, :>, :>=,
|
|
80
|
+
:div, :modulo, :%, :remainder, :fdiv, :quo
|
|
81
|
+
].freeze
|
|
82
|
+
# Complex arithmetic. `ops_for` gains a `Complex` branch so these
|
|
83
|
+
# reach the binary fold path (Complex was previously unary-only).
|
|
84
|
+
# `/` and `**` stay foldable (Complex result). `==` / `!=` are
|
|
85
|
+
# excluded for the same reason as Rational (`nucomp_eqeq_p`
|
|
86
|
+
# delegates to operand `==`); ordering is undefined for Complex.
|
|
87
|
+
COMPLEX_BINARY = Set[:+, :-, :*, :/, :**].freeze
|
|
69
88
|
|
|
70
89
|
# v0.0.3 C — pure unary catalogue. Each method must:
|
|
71
90
|
# - take zero arguments,
|
|
@@ -83,20 +102,35 @@ module Rigor
|
|
|
83
102
|
# user-defined `def is_odd(n) = n.odd?` so
|
|
84
103
|
# `Parity.new.is_odd(3)` types as `Constant[true]`
|
|
85
104
|
# rather than the RBS-widened `bool`.
|
|
105
|
+
# NOTE: `:hash` is deliberately NOT in any of these sets.
|
|
106
|
+
# `Object#hash` (and the `String`/`Symbol`/`Integer`/`Float`
|
|
107
|
+
# overrides) is salted with a per-process SipHash seed, so
|
|
108
|
+
# `"abc".hash` returns a different Integer in every Ruby
|
|
109
|
+
# process. Folding it to a `Constant` would bake one process's
|
|
110
|
+
# value into the type (and the on-disk cache), making the
|
|
111
|
+
# result non-deterministic across runs — a violation of the
|
|
112
|
+
# purity contract this catalogue rests on. A literal's `.hash`
|
|
113
|
+
# therefore stays the RBS-widened `Integer`. The deterministic
|
|
114
|
+
# siblings `:inspect` / `:to_s` remain folded.
|
|
86
115
|
INTEGER_UNARY = Set[
|
|
87
116
|
:odd?, :even?, :zero?, :positive?, :negative?,
|
|
117
|
+
# `finite?` / `infinite?` are total on Integer (`true` / `nil`
|
|
118
|
+
# always) and round out the numeric predicate family — the Float
|
|
119
|
+
# sibling already folds them. `nonzero?` returns `self` (non-zero)
|
|
120
|
+
# or `nil`, both foldable Constants.
|
|
121
|
+
:finite?, :infinite?, :nonzero?,
|
|
88
122
|
:succ, :pred, :next, :abs, :magnitude,
|
|
89
123
|
:bit_length, :to_s, :to_i, :to_int, :to_f,
|
|
90
124
|
:floor, :ceil, :round, :truncate, :chr,
|
|
91
|
-
:inspect,
|
|
125
|
+
:inspect, :-@, :+@, :~, :to_r, :to_c
|
|
92
126
|
].freeze
|
|
93
127
|
FLOAT_UNARY = Set[
|
|
94
|
-
:zero?, :positive?, :negative?,
|
|
95
|
-
:nan?, :finite?, :infinite?,
|
|
128
|
+
:zero?, :positive?, :negative?, :nonzero?,
|
|
129
|
+
:nan?, :finite?, :infinite?, :integer?,
|
|
96
130
|
:abs, :magnitude, :floor, :ceil, :round, :truncate,
|
|
97
131
|
:next_float, :prev_float,
|
|
98
|
-
:to_s, :to_i, :to_int, :to_f,
|
|
99
|
-
:inspect,
|
|
132
|
+
:to_s, :to_i, :to_int, :to_f, :to_r, :rationalize,
|
|
133
|
+
:inspect, :-@, :+@
|
|
100
134
|
].freeze
|
|
101
135
|
STRING_UNARY = Set[
|
|
102
136
|
:upcase, :downcase, :capitalize, :swapcase,
|
|
@@ -104,20 +138,33 @@ module Rigor
|
|
|
104
138
|
:empty?, :strip, :lstrip, :rstrip, :chomp, :chop, :squeeze,
|
|
105
139
|
:to_s, :to_str, :to_sym, :intern,
|
|
106
140
|
:to_i, :to_f, :ord, :chr, :hex, :oct, :succ, :next,
|
|
107
|
-
:
|
|
141
|
+
:sum, :inspect
|
|
108
142
|
].freeze
|
|
109
143
|
SYMBOL_UNARY = Set[
|
|
110
144
|
:to_s, :to_sym, :to_proc, :length, :size,
|
|
111
145
|
:empty?, :upcase, :downcase, :capitalize,
|
|
112
|
-
:swapcase, :
|
|
146
|
+
:swapcase, :succ, :next, :inspect,
|
|
147
|
+
# `name` (the frozen-string accessor), `id2name` (alias of
|
|
148
|
+
# `to_s`), and `intern` (alias of `to_sym`) are pure reads of the
|
|
149
|
+
# symbol's text — siblings of the already-folded `to_s` / `to_sym`.
|
|
150
|
+
:name, :id2name, :intern
|
|
113
151
|
].freeze
|
|
114
|
-
BOOL_UNARY = Set[:!, :to_s, :inspect,
|
|
115
|
-
NIL_UNARY = Set[:nil?, :!, :to_s, :to_a, :to_h, :inspect
|
|
152
|
+
BOOL_UNARY = Set[:!, :to_s, :inspect, :&, :|, :^].freeze
|
|
153
|
+
NIL_UNARY = Set[:nil?, :!, :to_s, :to_a, :to_h, :inspect].freeze
|
|
116
154
|
RATIONAL_UNARY = Set[
|
|
117
155
|
:zero?, :integer?, :real, :abs2,
|
|
118
|
-
:conj, :conjugate, :nonzero
|
|
156
|
+
:conj, :conjugate, :nonzero?,
|
|
157
|
+
:numerator, :denominator, :abs, :magnitude,
|
|
158
|
+
:to_f, :to_i, :to_int, :to_r, :rationalize,
|
|
159
|
+
:floor, :ceil, :round, :truncate,
|
|
160
|
+
:-@, :+@
|
|
161
|
+
].freeze
|
|
162
|
+
COMPLEX_UNARY = Set[
|
|
163
|
+
:zero?, :nonzero?,
|
|
164
|
+
:abs, :magnitude, :abs2, :arg, :angle, :phase,
|
|
165
|
+
:conjugate, :conj, :real, :imaginary, :imag,
|
|
166
|
+
:to_c, :-@, :+@
|
|
119
167
|
].freeze
|
|
120
|
-
COMPLEX_UNARY = Set[:zero?, :nonzero?].freeze
|
|
121
168
|
|
|
122
169
|
STRING_FOLD_BYTE_LIMIT = 4096
|
|
123
170
|
|
|
@@ -386,9 +433,15 @@ module Rigor
|
|
|
386
433
|
# Only fires on a single-receiver Range with finite integer
|
|
387
434
|
# endpoints; mixed unions fall through so the existing
|
|
388
435
|
# union-of-Constants path keeps the rest of the arms.
|
|
389
|
-
RANGE_FOLD_METHODS = Set[:to_a, :first, :last, :min, :max, :count, :size, :length, :entries, :minmax
|
|
436
|
+
RANGE_FOLD_METHODS = Set[:to_a, :first, :last, :min, :max, :count, :size, :length, :entries, :minmax,
|
|
437
|
+
:sum].freeze
|
|
438
|
+
# 1-arg head/tail projections on a `Constant<Range>`. `first(n)` /
|
|
439
|
+
# `take(n)` return the first `n` elements, `last(n)` the final `n` —
|
|
440
|
+
# each lifts to a per-position `Tuple[Constant[Integer]…]`. The
|
|
441
|
+
# no-arg `first` / `last` stay on the unary path (single Integer).
|
|
442
|
+
RANGE_FOLD_BINARY_METHODS = Set[:first, :last, :take].freeze
|
|
390
443
|
RANGE_TO_A_LIMIT = 16
|
|
391
|
-
private_constant :RANGE_FOLD_METHODS, :RANGE_TO_A_LIMIT
|
|
444
|
+
private_constant :RANGE_FOLD_METHODS, :RANGE_FOLD_BINARY_METHODS, :RANGE_TO_A_LIMIT
|
|
392
445
|
|
|
393
446
|
def try_fold_range_constant_unary(receiver_values, method_name)
|
|
394
447
|
return nil unless RANGE_FOLD_METHODS.include?(method_name)
|
|
@@ -408,6 +461,11 @@ module Rigor
|
|
|
408
461
|
when :last, :max then range_endpoint_constant(range, :last)
|
|
409
462
|
when :count, :size, :length then Type::Combinator.constant_of(range.to_a.size)
|
|
410
463
|
when :minmax then range_minmax_tuple(range)
|
|
464
|
+
# `range.sum` is closed-form (Gauss) for an integer range, so a
|
|
465
|
+
# huge range still costs O(1) and yields a single Integer — no
|
|
466
|
+
# materialisation, no cap needed. Endless ranges are already
|
|
467
|
+
# excluded by the Integer-endpoint guard in the caller.
|
|
468
|
+
when :sum then Type::Combinator.constant_of(range.sum)
|
|
411
469
|
end
|
|
412
470
|
end
|
|
413
471
|
|
|
@@ -441,10 +499,46 @@ module Rigor
|
|
|
441
499
|
)
|
|
442
500
|
end
|
|
443
501
|
|
|
502
|
+
# `(1..10).first(3)` / `.take(3)` / `.last(3)` — the 1-arg head /
|
|
503
|
+
# tail forms. `first`/`last` already fold no-arg through the unary
|
|
504
|
+
# path; this is the n-arg sibling, mirroring the Tuple carrier's
|
|
505
|
+
# `first(n)`/`take(n)` handlers. Lifts to `Tuple[Constant…]`.
|
|
506
|
+
def try_fold_range_constant_binary(receiver_values, method_name, arg_values)
|
|
507
|
+
return nil unless RANGE_FOLD_BINARY_METHODS.include?(method_name)
|
|
508
|
+
return nil unless receiver_values.size == 1 && arg_values.size == 1
|
|
509
|
+
|
|
510
|
+
range = receiver_values.first
|
|
511
|
+
return nil unless range.is_a?(Range)
|
|
512
|
+
return nil unless range.begin.is_a?(Integer) && range.end.is_a?(Integer)
|
|
513
|
+
|
|
514
|
+
range_take_tuple(range, method_name, arg_values.first)
|
|
515
|
+
rescue StandardError
|
|
516
|
+
nil
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def range_take_tuple(range, method_name, count)
|
|
520
|
+
return nil unless count.is_a?(Integer) && !count.negative?
|
|
521
|
+
# `first(n)`/`last(n)`/`take(n)` materialise at most `min(n, size)`
|
|
522
|
+
# elements; cap that count so a huge `n` (or range) never blows up
|
|
523
|
+
# the Constant. `Range#size` is O(1) for integer endpoints.
|
|
524
|
+
return nil if [count, range.size].min > RANGE_TO_A_LIMIT
|
|
525
|
+
|
|
526
|
+
values = method_name == :last ? range.last(count) : range.first(count)
|
|
527
|
+
return Type::Combinator.tuple_of if values.empty?
|
|
528
|
+
|
|
529
|
+
Type::Combinator.tuple_of(*values.map { |v| Type::Combinator.constant_of(v) })
|
|
530
|
+
end
|
|
531
|
+
|
|
444
532
|
def try_fold_binary_set(receiver_values, method_name, arg_values)
|
|
533
|
+
range_lift = try_fold_range_constant_binary(receiver_values, method_name, arg_values)
|
|
534
|
+
return range_lift if range_lift
|
|
535
|
+
|
|
445
536
|
string_lift = try_fold_string_array_binary(receiver_values, method_name, arg_values)
|
|
446
537
|
return string_lift if string_lift
|
|
447
538
|
|
|
539
|
+
integer_lift = try_fold_integer_array_binary(receiver_values, method_name, arg_values)
|
|
540
|
+
return integer_lift if integer_lift
|
|
541
|
+
|
|
448
542
|
pathname_lift = try_fold_pathname_binary(receiver_values, method_name, arg_values)
|
|
449
543
|
return pathname_lift if pathname_lift
|
|
450
544
|
|
|
@@ -456,15 +550,21 @@ module Rigor
|
|
|
456
550
|
end
|
|
457
551
|
build_constant_type(results, source: receiver_values + arg_values)
|
|
458
552
|
end
|
|
459
|
-
# v0.0.7 — `Constant<String>#chars` / `bytes` / `
|
|
460
|
-
# `split` (no-arg) return a Ruby
|
|
461
|
-
# scalars; `foldable_constant_value?` rejects Array
|
|
553
|
+
# v0.0.7 — `Constant<String>#chars` / `bytes` / `codepoints` /
|
|
554
|
+
# `grapheme_clusters` / `lines` / `split` (no-arg) return a Ruby
|
|
555
|
+
# Array of foldable scalars; `foldable_constant_value?` rejects Array
|
|
462
556
|
# results, so the standard unary path declines. Lift the
|
|
463
557
|
# Array to a per-position `Tuple[Constant…]` directly,
|
|
464
558
|
# capped at `STRING_ARRAY_LIFT_LIMIT` to keep the result
|
|
465
|
-
# bounded for long strings.
|
|
466
|
-
|
|
467
|
-
|
|
559
|
+
# bounded for long strings. (`codepoints` yields per-character
|
|
560
|
+
# Integer codepoints, the sibling of the byte-valued `bytes`;
|
|
561
|
+
# `grapheme_clusters` is the extended-grapheme sibling of `chars`.)
|
|
562
|
+
STRING_ARRAY_UNARY_METHODS = Set[:chars, :bytes, :codepoints, :grapheme_clusters, :lines, :split].freeze
|
|
563
|
+
# `partition` / `rpartition` always return a fixed 3-element
|
|
564
|
+
# `[head, separator, tail]` Array whose members are substrings of
|
|
565
|
+
# the receiver (bounded by the input), so they lift to a precise
|
|
566
|
+
# 3-slot `Tuple[Constant…]`.
|
|
567
|
+
STRING_ARRAY_BINARY_METHODS = Set[:split, :scan, :partition, :rpartition].freeze
|
|
468
568
|
STRING_ARRAY_LIFT_LIMIT = 32
|
|
469
569
|
private_constant :STRING_ARRAY_UNARY_METHODS,
|
|
470
570
|
:STRING_ARRAY_BINARY_METHODS,
|
|
@@ -494,6 +594,14 @@ module Rigor
|
|
|
494
594
|
INTEGER_ARRAY_UNARY_METHODS = Set[:digits].freeze
|
|
495
595
|
private_constant :INTEGER_ARRAY_UNARY_METHODS
|
|
496
596
|
|
|
597
|
+
# 1-arg Integer methods that return an Array of foldable
|
|
598
|
+
# Integers: `digits(base)` (base-n place values; raises on a
|
|
599
|
+
# negative receiver or base < 2 → declines) and `gcdlcm(other)`
|
|
600
|
+
# (the fixed `[gcd, lcm]` pair). Both are pure arithmetic; the
|
|
601
|
+
# result lifts to a `Tuple[Constant[Integer]…]`.
|
|
602
|
+
INTEGER_ARRAY_BINARY_METHODS = Set[:digits, :gcdlcm].freeze
|
|
603
|
+
private_constant :INTEGER_ARRAY_BINARY_METHODS
|
|
604
|
+
|
|
497
605
|
# v0.0.7 — `Constant<Pathname>` delegates to a curated set
|
|
498
606
|
# of pure path-manipulation methods. Pathname is immutable
|
|
499
607
|
# in Ruby (per its docstring) and the catalog classifies
|
|
@@ -613,6 +721,25 @@ module Rigor
|
|
|
613
721
|
nil
|
|
614
722
|
end
|
|
615
723
|
|
|
724
|
+
# `Constant<Integer>#digits(base)` / `#gcdlcm(other)` — the
|
|
725
|
+
# 1-arg Array-returning Integer methods. `digits(base)` declines
|
|
726
|
+
# on a negative receiver (the unary path's guard); other domain
|
|
727
|
+
# errors (base < 2) raise and are rescued. `gcdlcm` is total over
|
|
728
|
+
# Integer args.
|
|
729
|
+
def try_fold_integer_array_binary(receiver_values, method_name, arg_values)
|
|
730
|
+
return nil unless INTEGER_ARRAY_BINARY_METHODS.include?(method_name)
|
|
731
|
+
return nil unless receiver_values.size == 1 && arg_values.size == 1
|
|
732
|
+
|
|
733
|
+
receiver = receiver_values.first
|
|
734
|
+
arg = arg_values.first
|
|
735
|
+
return nil unless receiver.is_a?(Integer) && arg.is_a?(Integer)
|
|
736
|
+
return nil if method_name == :digits && receiver.negative?
|
|
737
|
+
|
|
738
|
+
lift_array_result(receiver.public_send(method_name, arg))
|
|
739
|
+
rescue StandardError
|
|
740
|
+
nil
|
|
741
|
+
end
|
|
742
|
+
|
|
616
743
|
# `Constant<Complex>#rect` / `#rectangular` — lifts `[real, imaginary]`
|
|
617
744
|
# to `Tuple[Constant[re], Constant[im]]`. Both components are always
|
|
618
745
|
# numeric (Integer or Float for literal complexes), so they satisfy
|
|
@@ -1334,7 +1461,24 @@ module Rigor
|
|
|
1334
1461
|
private_constant :FOLDABLE_CONSTANT_CLASSES
|
|
1335
1462
|
|
|
1336
1463
|
def foldable_constant_value?(value)
|
|
1337
|
-
FOLDABLE_CONSTANT_CLASSES.any? { |klass| value.is_a?(klass) }
|
|
1464
|
+
return false unless FOLDABLE_CONSTANT_CLASSES.any? { |klass| value.is_a?(klass) }
|
|
1465
|
+
|
|
1466
|
+
# A NaN result (`0.0 / 0.0`, `Float::NAN`-propagating arithmetic,
|
|
1467
|
+
# or a NaN-bearing Complex) is non-reflexive under `==`, so a
|
|
1468
|
+
# `Constant[NaN]` would break the `==` / `eql?` / `hash` contract
|
|
1469
|
+
# `build_constant_type` relies on for union dedup. Decline the
|
|
1470
|
+
# fold and let the RBS tier answer with the widened class.
|
|
1471
|
+
return false if value.is_a?(Float) && value.nan?
|
|
1472
|
+
return false if value.is_a?(Complex) && complex_nan?(value)
|
|
1473
|
+
|
|
1474
|
+
true
|
|
1475
|
+
end
|
|
1476
|
+
|
|
1477
|
+
# True when either component of a Complex is NaN.
|
|
1478
|
+
def complex_nan?(value)
|
|
1479
|
+
real = value.real
|
|
1480
|
+
imag = value.imaginary
|
|
1481
|
+
(real.is_a?(Float) && real.nan?) || (imag.is_a?(Float) && imag.nan?)
|
|
1338
1482
|
end
|
|
1339
1483
|
|
|
1340
1484
|
def safe?(receiver_value, method_name, arg_value)
|
|
@@ -1355,6 +1499,7 @@ module Rigor
|
|
|
1355
1499
|
when true, false then BOOL_BINARY
|
|
1356
1500
|
when nil then NIL_BINARY
|
|
1357
1501
|
when Rational then RATIONAL_BINARY
|
|
1502
|
+
when Complex then COMPLEX_BINARY
|
|
1358
1503
|
else Set.new
|
|
1359
1504
|
end
|
|
1360
1505
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../../type"
|
|
4
4
|
require_relative "singleton_folding"
|
|
5
|
+
require_relative "member_shape_projection"
|
|
5
6
|
|
|
6
7
|
module Rigor
|
|
7
8
|
module Inference
|
|
@@ -30,6 +31,10 @@ module Rigor
|
|
|
30
31
|
module DataFolding
|
|
31
32
|
module_function
|
|
32
33
|
|
|
34
|
+
# The `[]` / `to_h` / `deconstruct` / `members` / `with` projections
|
|
35
|
+
# and the reader-redefinition guard are shared with {StructFolding}.
|
|
36
|
+
extend MemberShapeProjection
|
|
37
|
+
|
|
33
38
|
# @return [Rigor::Type, nil] the folded result, or nil to defer.
|
|
34
39
|
def try_dispatch(context)
|
|
35
40
|
receiver = context.receiver
|
|
@@ -165,81 +170,12 @@ module Rigor
|
|
|
165
170
|
when :deconstruct then instance_deconstruct(instance)
|
|
166
171
|
when :deconstruct_keys then instance_deconstruct_keys(instance, args)
|
|
167
172
|
when :members then instance_members(instance)
|
|
168
|
-
when :with
|
|
173
|
+
when :with
|
|
174
|
+
instance_with(instance, args) do |members, class_name|
|
|
175
|
+
Type::Combinator.data_instance_of(members: members, class_name: class_name)
|
|
176
|
+
end
|
|
169
177
|
end
|
|
170
178
|
end
|
|
171
|
-
|
|
172
|
-
# A `Data.define` class body (the `class Point < Data.define(:x);
|
|
173
|
-
# def x; …; end; end` subclass body, or a `Const = Data.define(:x) do
|
|
174
|
-
# def x; …; end; end` block) can redefine a member's synthesised
|
|
175
|
-
# reader. When it does, `inst.x` runs that `def`, NOT the member, so
|
|
176
|
-
# folding the read to the member type would be unsound (a downstream
|
|
177
|
-
# FP). Both named forms register the override as a real `def` node
|
|
178
|
-
# under the class name, so an entry in the project def-node table is
|
|
179
|
-
# the discriminator (the synthesised reader has no def node). The
|
|
180
|
-
# value accessors `[]` / `to_h` / `deconstruct` bypass the reader and
|
|
181
|
-
# stay foldable, so this gate is on the bare member read only.
|
|
182
|
-
def reader_overridden?(instance, method_name, scope)
|
|
183
|
-
class_name = instance.class_name
|
|
184
|
-
return false if class_name.nil? || scope.nil?
|
|
185
|
-
|
|
186
|
-
!scope.user_def_for(class_name, method_name).nil?
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
def instance_index(instance, args)
|
|
190
|
-
return nil unless args.size == 1
|
|
191
|
-
|
|
192
|
-
arg = args.first
|
|
193
|
-
return nil unless arg.is_a?(Type::Constant)
|
|
194
|
-
|
|
195
|
-
key = arg.value
|
|
196
|
-
case key
|
|
197
|
-
when Symbol
|
|
198
|
-
instance.members[key]
|
|
199
|
-
when Integer
|
|
200
|
-
values = instance.members.values
|
|
201
|
-
idx = key.negative? ? key + values.size : key
|
|
202
|
-
values[idx] if idx && idx >= 0 && idx < values.size
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
def instance_to_h(instance)
|
|
207
|
-
Type::Combinator.hash_shape_of(instance.members.dup)
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
def instance_deconstruct(instance)
|
|
211
|
-
Type::Combinator.tuple_of(*instance.members.values)
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
# `deconstruct_keys(nil)` / `deconstruct_keys([:x])` both yield a
|
|
215
|
-
# subset of the member map; the conservative, always-correct answer
|
|
216
|
-
# is the full closed member shape.
|
|
217
|
-
def instance_deconstruct_keys(instance, args)
|
|
218
|
-
return nil unless args.size <= 1
|
|
219
|
-
|
|
220
|
-
Type::Combinator.hash_shape_of(instance.members.dup)
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
def instance_members(instance)
|
|
224
|
-
Type::Combinator.tuple_of(*instance.member_names.map { |name| Type::Combinator.constant_of(name) })
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
# `Data#with(x: 9)` returns a new frozen copy with the named members
|
|
228
|
-
# overridden. Only a closed keyword `HashShape` whose keys are a
|
|
229
|
-
# subset of the members folds; anything else defers (RBS resolves
|
|
230
|
-
# `with` to `self`, returning the unchanged instance type).
|
|
231
|
-
def instance_with(instance, args)
|
|
232
|
-
return instance if args.empty?
|
|
233
|
-
return nil unless args.size == 1
|
|
234
|
-
|
|
235
|
-
shape = args.first
|
|
236
|
-
return nil unless shape.is_a?(Type::HashShape) && shape.closed?
|
|
237
|
-
return nil unless shape.optional_keys.empty?
|
|
238
|
-
return nil unless shape.pairs.keys.all? { |key| instance.members.key?(key) }
|
|
239
|
-
|
|
240
|
-
merged = instance.members.merge(shape.pairs)
|
|
241
|
-
Type::Combinator.data_instance_of(members: merged, class_name: instance.class_name)
|
|
242
|
-
end
|
|
243
179
|
end
|
|
244
180
|
end
|
|
245
181
|
end
|
|
@@ -54,11 +54,10 @@ module Rigor
|
|
|
54
54
|
# *correctness-preservingly* proved" excludes Constants whose
|
|
55
55
|
# value is host-specific.
|
|
56
56
|
module FileFolding
|
|
57
|
-
# File class methods
|
|
58
|
-
#
|
|
59
|
-
# platform-sensitive
|
|
60
|
-
#
|
|
61
|
-
# opt-in flag for any of them to fire.
|
|
57
|
+
# File class methods the analyzer can fold when the opt-in
|
|
58
|
+
# flag is set. Currently identical to PLATFORM_DEPENDENT_METHODS
|
|
59
|
+
# — separated for a future non-platform-sensitive tier that
|
|
60
|
+
# can fold without the opt-in flag.
|
|
62
61
|
FILE_PURE_CLASS_METHODS = Set[
|
|
63
62
|
:basename,
|
|
64
63
|
:dirname,
|
|
@@ -72,8 +71,8 @@ module Rigor
|
|
|
72
71
|
# Methods whose result depends on host directory-separator
|
|
73
72
|
# semantics (`/` on POSIX, `/` AND `\` on Windows, drive
|
|
74
73
|
# letters, UNC paths). Folding these would bake the
|
|
75
|
-
# analyzer-host's platform into the inferred type. The opt-
|
|
76
|
-
#
|
|
74
|
+
# analyzer-host's platform into the inferred type. The opt-in
|
|
75
|
+
# flag below controls whether to do it anyway.
|
|
77
76
|
PLATFORM_DEPENDENT_METHODS = Set[
|
|
78
77
|
:basename, :dirname, :extname, :join, :split, :absolute_path?
|
|
79
78
|
].freeze
|
|
@@ -175,24 +175,18 @@ module Rigor
|
|
|
175
175
|
type.is_a?(Type::Constant) && type.value.is_a?(Symbol)
|
|
176
176
|
end
|
|
177
177
|
|
|
178
|
-
# Element-yielding Enumerable methods covered as a
|
|
179
|
-
#
|
|
180
|
-
#
|
|
181
|
-
#
|
|
182
|
-
#
|
|
183
|
-
#
|
|
184
|
-
# `Tuple[K, V]` pair rather than the projected
|
|
178
|
+
# Element-yielding Enumerable methods covered as a placeholder.
|
|
179
|
+
# RBS already binds the block parameter correctly for plain
|
|
180
|
+
# `Array[T]` / `Set[T]` / `Range[T]` receivers via generic
|
|
181
|
+
# substitution; this tier exists so Tuple- and HashShape-shaped
|
|
182
|
+
# receivers reach the block body with the precise per-position
|
|
183
|
+
# element union / `Tuple[K, V]` pair rather than the projected
|
|
185
184
|
# `Array[union]` / `Hash[K, V]` widening.
|
|
186
185
|
#
|
|
187
|
-
# NOTE (
|
|
188
|
-
#
|
|
189
|
-
#
|
|
190
|
-
#
|
|
191
|
-
# after PHPStan's extension API (ADR-2). The placeholders
|
|
192
|
-
# below stay until the plugin surface is in place; once it
|
|
193
|
-
# ships, this dispatcher loses these arms and the
|
|
194
|
-
# equivalent rules move into a built-in plugin loaded at
|
|
195
|
-
# boot.
|
|
186
|
+
# NOTE: `Plugin::NodeRuleWalk` (ADR-52 WD4) is now in place as
|
|
187
|
+
# the intended migration target for these Enumerable projections.
|
|
188
|
+
# The four methods (group_by, partition, each_slice, each_cons)
|
|
189
|
+
# remain here pending that migration.
|
|
196
190
|
def single_element_block_params(receiver)
|
|
197
191
|
element = element_type_of(receiver)
|
|
198
192
|
return nil if element.nil?
|
|
@@ -43,14 +43,21 @@ module Rigor
|
|
|
43
43
|
private_constant :NUMERIC_CONSTRUCTORS
|
|
44
44
|
|
|
45
45
|
# `Kernel#Integer(s)` predicate-aware refinement set
|
|
46
|
-
# (v0.1.1 Track 1 slice 2b).
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
|
|
46
|
+
# (v0.1.1 Track 1 slice 2b). `decimal-int-string` is the
|
|
47
|
+
# only string refinement whose every inhabitant `Integer(s)`
|
|
48
|
+
# parses without remainder, so the result is a plain
|
|
49
|
+
# `Integer` — but NOT `non-negative-int`: the predicate
|
|
50
|
+
# `/\A-?\d+\z/` admits a leading sign, so `"-7"` is a valid
|
|
51
|
+
# decimal-int-string and `Integer("-7") == -7 < 0`. The
|
|
52
|
+
# narrowing is total (every inhabitant parses) but not `>= 0`,
|
|
53
|
+
# so it lands on `universal_int`. `numeric-string` is
|
|
54
|
+
# deliberately NOT in this set at all: since it was widened to
|
|
55
|
+
# the full Ruby numeric-literal grammar (floats, hex, rational,
|
|
56
|
+
# imaginary, signs), `Integer(numeric_string)` would raise for
|
|
57
|
+
# a `"1.5"` / `"2i"` inhabitant — not even total — so it falls
|
|
58
|
+
# through to RBS `Integer`. The `Integer(s, base)` overload is
|
|
59
|
+
# left for a later slice.
|
|
60
|
+
INTEGER_REFINEMENT_PREDICATES = Set[:decimal_int].freeze
|
|
54
61
|
private_constant :INTEGER_REFINEMENT_PREDICATES
|
|
55
62
|
|
|
56
63
|
def try_dispatch(context)
|
|
@@ -70,7 +77,7 @@ module Rigor
|
|
|
70
77
|
# paths, tried in order:
|
|
71
78
|
#
|
|
72
79
|
# 1. A `Refined[String, predicate]` argument whose predicate
|
|
73
|
-
# is a
|
|
80
|
+
# is a total-parse carrier narrows to `universal_int`
|
|
74
81
|
# (see {try_integer_from_refinement}).
|
|
75
82
|
# 2. A `Constant` String or Numeric argument — optionally
|
|
76
83
|
# with a `Constant[Integer]` base — runs the actual
|
|
@@ -120,9 +127,14 @@ module Rigor
|
|
|
120
127
|
# `Kernel#Integer(s)` over a `Refined[String, predicate]`
|
|
121
128
|
# whose predicate is in {INTEGER_REFINEMENT_PREDICATES}.
|
|
122
129
|
# Mirrors the `String#to_i` projection in `ShapeDispatch`
|
|
123
|
-
# (v0.1.1 slice 2a) — the result is
|
|
124
|
-
# `non-negative-int
|
|
125
|
-
# so the
|
|
130
|
+
# (v0.1.1 slice 2a) — the result is `universal_int`, NOT
|
|
131
|
+
# `non-negative-int`: a decimal-int-string admits a leading
|
|
132
|
+
# sign (`"-7"`), so the parsed Integer can be negative. The
|
|
133
|
+
# carrier stays an `IntegerRange` (rather than declining to
|
|
134
|
+
# the RBS `Nominal[Integer]`) so downstream range narrowing
|
|
135
|
+
# still has a range to intersect. Returns nil for any other
|
|
136
|
+
# arg shape so the RBS tier handles the generic `Integer(arg)`
|
|
137
|
+
# case.
|
|
126
138
|
def try_integer_from_refinement(args)
|
|
127
139
|
return nil unless args.size == 1
|
|
128
140
|
|
|
@@ -133,7 +145,7 @@ module Rigor
|
|
|
133
145
|
return nil unless base.is_a?(Type::Nominal) && base.class_name == "String"
|
|
134
146
|
return nil unless INTEGER_REFINEMENT_PREDICATES.include?(arg.predicate_id)
|
|
135
147
|
|
|
136
|
-
Type::Combinator.
|
|
148
|
+
Type::Combinator.universal_int
|
|
137
149
|
end
|
|
138
150
|
|
|
139
151
|
def try_array(args)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module MethodDispatcher
|
|
8
|
+
# The member-shape projections shared by {DataFolding} and
|
|
9
|
+
# {StructFolding}. A `DataInstance` and a `StructInstance` expose the
|
|
10
|
+
# same surface — an ordered `members` map, `member_names`, and a
|
|
11
|
+
# `class_name` — so the value projections off that surface
|
|
12
|
+
# (`[]` / `to_h` / `deconstruct` / `deconstruct_keys` / `members` /
|
|
13
|
+
# `with`) and the reader-redefinition guard are identical between the
|
|
14
|
+
# two folders. Only `#with`'s carrier constructor differs
|
|
15
|
+
# (`data_instance_of` vs `struct_instance_of`), so it takes a block
|
|
16
|
+
# that builds the new instance from the merged member map.
|
|
17
|
+
#
|
|
18
|
+
# Both folders `extend` this module so the projections resolve as
|
|
19
|
+
# their own module functions (matching their `module_function` style).
|
|
20
|
+
module MemberShapeProjection
|
|
21
|
+
# A Data/Struct subclass body can redefine a member's synthesised
|
|
22
|
+
# reader (`def x`); when it does, `inst.x` runs that `def`, not the
|
|
23
|
+
# member, so folding the read would be unsound. A real `def` node
|
|
24
|
+
# under the class name is the discriminator (the synthesised reader
|
|
25
|
+
# has none), so an entry in the project def-node table gates the
|
|
26
|
+
# bare member read off.
|
|
27
|
+
def reader_overridden?(instance, method_name, scope)
|
|
28
|
+
class_name = instance.class_name
|
|
29
|
+
return false if class_name.nil? || scope.nil?
|
|
30
|
+
|
|
31
|
+
!scope.user_def_for(class_name, method_name).nil?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def instance_index(instance, args)
|
|
35
|
+
return nil unless args.size == 1
|
|
36
|
+
|
|
37
|
+
arg = args.first
|
|
38
|
+
return nil unless arg.is_a?(Type::Constant)
|
|
39
|
+
|
|
40
|
+
key = arg.value
|
|
41
|
+
case key
|
|
42
|
+
when Symbol
|
|
43
|
+
instance.members[key]
|
|
44
|
+
when Integer
|
|
45
|
+
values = instance.members.values
|
|
46
|
+
idx = key.negative? ? key + values.size : key
|
|
47
|
+
values[idx] if idx && idx >= 0 && idx < values.size
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def instance_to_h(instance)
|
|
52
|
+
Type::Combinator.hash_shape_of(instance.members.dup)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def instance_deconstruct(instance)
|
|
56
|
+
Type::Combinator.tuple_of(*instance.members.values)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# `deconstruct_keys(nil)` / `deconstruct_keys([:x])` both yield a
|
|
60
|
+
# subset of the member map; the conservative, always-correct answer
|
|
61
|
+
# is the full closed member shape.
|
|
62
|
+
def instance_deconstruct_keys(instance, args)
|
|
63
|
+
return nil unless args.size <= 1
|
|
64
|
+
|
|
65
|
+
Type::Combinator.hash_shape_of(instance.members.dup)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def instance_members(instance)
|
|
69
|
+
Type::Combinator.tuple_of(*instance.member_names.map { |name| Type::Combinator.constant_of(name) })
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# `#with(x: 9)` returns a new copy with the named members
|
|
73
|
+
# overridden. Only a closed keyword `HashShape` whose keys are a
|
|
74
|
+
# subset of the members folds; anything else defers (RBS resolves
|
|
75
|
+
# `with` to `self`, returning the unchanged instance type). The
|
|
76
|
+
# carrier constructor differs per folder, so the caller supplies it
|
|
77
|
+
# as a block taking the merged member map and the class name.
|
|
78
|
+
def instance_with(instance, args)
|
|
79
|
+
return instance if args.empty?
|
|
80
|
+
return nil unless args.size == 1
|
|
81
|
+
|
|
82
|
+
shape = args.first
|
|
83
|
+
return nil unless shape.is_a?(Type::HashShape) && shape.closed?
|
|
84
|
+
return nil unless shape.optional_keys.empty?
|
|
85
|
+
return nil unless shape.pairs.keys.all? { |key| instance.members.key?(key) }
|
|
86
|
+
|
|
87
|
+
merged = instance.members.merge(shape.pairs)
|
|
88
|
+
yield(merged, instance.class_name)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -25,9 +25,7 @@ module Rigor
|
|
|
25
25
|
# so without this preference, an alias-typed overload like
|
|
26
26
|
# `Array#[](::int) -> Elem` would beat the strict
|
|
27
27
|
# `Array#[](Range) -> Array[Elem]?` overload for a Range
|
|
28
|
-
# argument.
|
|
29
|
-
# "Interface-strictness on overload selection" item in
|
|
30
|
-
# `docs/ROADMAP.md`.)
|
|
28
|
+
# argument.
|
|
31
29
|
# 3. **Pass 2 — gradual fall-back.** If no fully strict overload
|
|
32
30
|
# matches, accept the first arity-and-gradual-accept match
|
|
33
31
|
# (the v0.1.1 behaviour). Alias / Interface / Intersection
|