rigortype 0.3.5 → 0.3.7
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 +5 -5
- data/data/builtins/ruby_core/date.yml +6 -3
- data/data/builtins/ruby_core/enumerable.yml +1 -0
- data/data/builtins/ruby_core/exception.yml +2 -1
- data/data/builtins/ruby_core/file.yml +28 -14
- data/data/builtins/ruby_core/hash.yml +5 -2
- data/data/builtins/ruby_core/io.yml +33 -16
- data/data/builtins/ruby_core/random.yml +2 -1
- data/data/builtins/ruby_core/re.yml +6 -3
- data/data/builtins/ruby_core/struct.yml +2 -0
- data/data/builtins/ruby_core/time.yml +2 -1
- data/data/gem_overlay/activesupport/core_ext.rbs +442 -11
- data/docs/handbook/02-everyday-types.md +1 -1
- data/docs/handbook/04-tuples-and-shapes.md +1 -1
- data/docs/handbook/08-understanding-errors.md +1 -1
- data/docs/handbook/09-plugins.md +2 -2
- data/docs/handbook/10-sorbet.md +1 -1
- data/docs/handbook/README.md +2 -2
- data/docs/handbook/appendix-go.md +1 -1
- data/docs/handbook/appendix-java-csharp.md +2 -2
- data/docs/handbook/appendix-mypy.md +1 -1
- data/docs/handbook/appendix-protocols-and-structural-typing.md +2 -2
- data/docs/handbook/appendix-rust.md +1 -1
- data/docs/handbook/appendix-type-theory.md +2 -2
- data/docs/handbook/appendix-typescript.md +4 -4
- data/docs/manual/02-cli-reference.md +29 -16
- data/docs/manual/03-configuration.md +1 -1
- data/docs/manual/04-diagnostics.md +12 -0
- data/docs/manual/05-inspecting-types.md +20 -8
- data/docs/manual/07-plugins.md +4 -0
- data/docs/manual/08-skills.md +1 -1
- data/docs/manual/11-ci.md +1 -1
- data/docs/manual/12-caching.md +1 -1
- data/docs/manual/15-type-protection-coverage.md +1 -1
- data/docs/manual/18-removing-dead-code.md +13 -4
- data/docs/manual/19-effect-labels.md +19 -12
- data/docs/manual/README.md +2 -2
- data/docs/manual/plugins/rigor-actionmailer.md +4 -4
- data/docs/manual/plugins/rigor-actionpack.md +61 -0
- data/docs/manual/plugins/rigor-activejob.md +3 -3
- data/docs/manual/plugins/rigor-activerecord.md +78 -5
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
- data/docs/manual/plugins/rigor-rails-i18n.md +5 -5
- data/docs/manual/plugins/rigor-rspec.md +6 -2
- data/docs/manual/plugins/rigor-sidekiq.md +22 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +12 -0
- data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +98 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -3
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +199 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +1 -2
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -2
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +6 -12
- data/lib/rigor/analysis/check_rules.rb +383 -48
- data/lib/rigor/analysis/crash_signature.rb +116 -0
- data/lib/rigor/analysis/dependency_recorder.rb +18 -3
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/diagnostic.rb +17 -6
- data/lib/rigor/analysis/effects_cache_probe.rb +132 -0
- data/lib/rigor/analysis/incremental.rb +28 -0
- data/lib/rigor/analysis/incremental_session.rb +44 -9
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +1 -2
- data/lib/rigor/analysis/reachability/graph.rb +44 -16
- data/lib/rigor/analysis/reachability/plugin_roots.rb +11 -8
- data/lib/rigor/analysis/reachability/scan.rb +27 -7
- data/lib/rigor/analysis/reachability/scan_cache.rb +130 -0
- data/lib/rigor/analysis/result.rb +32 -0
- data/lib/rigor/analysis/run_cache_key.rb +53 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +93 -1
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +14 -8
- data/lib/rigor/analysis/runner/pool_coordinator.rb +34 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +15 -11
- data/lib/rigor/analysis/runner/run_snapshots.rb +7 -2
- data/lib/rigor/analysis/runner.rb +298 -38
- data/lib/rigor/analysis/worker_session.rb +10 -1
- data/lib/rigor/bleeding_edge.rb +7 -6
- data/lib/rigor/builtins/hkt_builtins.rb +45 -6
- data/lib/rigor/builtins/predefined_constant_refinements.rb +108 -59
- data/lib/rigor/builtins/static_return_refinements.rb +20 -1
- data/lib/rigor/cache/descriptor.rb +42 -2
- data/lib/rigor/cache/file_digest.rb +20 -2
- data/lib/rigor/cache/incremental_snapshot.rb +40 -3
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +31 -8
- data/lib/rigor/cache/store.rb +10 -1
- data/lib/rigor/cli/check_command.rb +67 -48
- data/lib/rigor/cli/coverage_command.rb +28 -24
- data/lib/rigor/cli/coverage_mutation.rb +13 -17
- data/lib/rigor/cli/coverage_scan.rb +47 -6
- data/lib/rigor/cli/doc_links.rb +100 -0
- data/lib/rigor/cli/docs_command.rb +32 -2
- data/lib/rigor/cli/effects_command.rb +27 -2
- data/lib/rigor/cli/effects_diff_renderer.rb +82 -12
- data/lib/rigor/cli/effects_explain_renderer.rb +6 -3
- data/lib/rigor/cli/effects_snapshot_command.rb +52 -7
- data/lib/rigor/cli/fused_protection_renderer.rb +19 -2
- data/lib/rigor/cli/fused_protection_report.rb +23 -2
- data/lib/rigor/cli/measurement_integrity_warning.rb +57 -0
- data/lib/rigor/cli/mutation_protection_renderer.rb +22 -2
- data/lib/rigor/cli/mutation_protection_report.rb +31 -2
- data/lib/rigor/cli/sig_gen_command.rb +18 -0
- data/lib/rigor/cli/type_of_command.rb +188 -41
- data/lib/rigor/cli/type_of_renderer.rb +72 -3
- data/lib/rigor/cli/type_scan_command.rb +10 -1
- data/lib/rigor/cli/unused_command.rb +65 -16
- data/lib/rigor/cli.rb +22 -9
- data/lib/rigor/configuration.rb +7 -4
- data/lib/rigor/effects/definition_lines.rb +100 -0
- data/lib/rigor/effects/envelope_check.rb +18 -1
- data/lib/rigor/effects/liskov_check.rb +17 -8
- data/lib/rigor/effects/plugin_facts.rb +1 -1
- data/lib/rigor/effects/scanner.rb +2 -3
- data/lib/rigor/effects/signature_sources.rb +13 -2
- data/lib/rigor/effects/snapshot.rb +53 -21
- data/lib/rigor/effects/snapshot_diff.rb +26 -3
- data/lib/rigor/environment/bundle_sig_discovery.rb +64 -13
- data/lib/rigor/environment/lockfile_resolver.rb +15 -5
- data/lib/rigor/environment/missing_gem_constant_index.rb +45 -4
- data/lib/rigor/environment/rbs_hierarchy.rb +16 -22
- data/lib/rigor/environment/rbs_loader.rb +525 -46
- data/lib/rigor/environment.rb +38 -15
- data/lib/rigor/inference/acceptance.rb +172 -20
- data/lib/rigor/inference/captured_locals.rb +62 -0
- data/lib/rigor/inference/content_join.rb +347 -0
- data/lib/rigor/inference/def_handle.rb +15 -3
- data/lib/rigor/inference/def_node_resolver.rb +62 -3
- data/lib/rigor/inference/expression_typer.rb +1020 -141
- data/lib/rigor/inference/fork_map.rb +6 -1
- data/lib/rigor/inference/hkt_reducer.rb +12 -1
- data/lib/rigor/inference/hkt_registry.rb +46 -0
- data/lib/rigor/inference/hkt_sugar_translator.rb +93 -0
- data/lib/rigor/inference/index_write_widening.rb +48 -0
- data/lib/rigor/inference/indexed_narrowing.rb +14 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +7 -5
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +10 -0
- data/lib/rigor/inference/method_dispatcher/json_folding.rb +58 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +81 -38
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +54 -17
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -2
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +63 -10
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +104 -0
- data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +81 -0
- data/lib/rigor/inference/method_dispatcher.rb +155 -14
- data/lib/rigor/inference/method_parameter_binder.rb +3 -3
- data/lib/rigor/inference/mutation_widening.rb +244 -174
- data/lib/rigor/inference/narrowing.rb +331 -27
- data/lib/rigor/inference/parameter_inference_collector.rb +1 -0
- data/lib/rigor/inference/pre_eval_constants.rb +15 -0
- data/lib/rigor/inference/precision_scanner.rb +5 -2
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +89 -39
- data/lib/rigor/inference/scope_indexer.rb +1493 -140
- data/lib/rigor/inference/statement_evaluator.rb +276 -91
- data/lib/rigor/inference/struct_fold_safety.rb +186 -27
- data/lib/rigor/inference/synthetic_method_scanner.rb +7 -0
- data/lib/rigor/inference/version_guard.rb +229 -0
- data/lib/rigor/language_server/selection_range_provider.rb +1 -1
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/inflector.rb +14 -5
- data/lib/rigor/plugin/io_boundary.rb +107 -4
- data/lib/rigor/plugin/loader.rb +14 -0
- data/lib/rigor/protection/analysis_guard.rb +60 -0
- data/lib/rigor/protection/closure_kill_oracle.rb +43 -38
- data/lib/rigor/protection/diagnostic_oracle.rb +12 -4
- data/lib/rigor/protection/discovery_seed.rb +1 -1
- data/lib/rigor/protection/measurement_integrity.rb +36 -0
- data/lib/rigor/protection/mutation_scanner.rb +61 -6
- data/lib/rigor/protection/mutator.rb +121 -18
- data/lib/rigor/rbs_extended/envelope_scanner.rb +9 -0
- data/lib/rigor/reflection.rb +182 -30
- data/lib/rigor/scope/discovery_index.rb +39 -1
- data/lib/rigor/scope.rb +385 -4
- data/lib/rigor/sig_gen/classification.rb +12 -1
- data/lib/rigor/sig_gen/generator.rb +254 -5
- data/lib/rigor/sig_gen/renderer.rb +1 -6
- data/lib/rigor/sig_gen/writer.rb +3 -0
- data/lib/rigor/source/constant_path.rb +79 -2
- data/lib/rigor/triage/catalogue.rb +1 -1
- data/lib/rigor/type/combinator.rb +10 -0
- data/lib/rigor/type/maybe.rb +47 -0
- data/lib/rigor/type/refined.rb +1 -2
- data/lib/rigor/type/result.rb +53 -0
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +3 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +58 -8
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +15 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +4 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +3 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +39 -14
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +36 -3
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +21 -5
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +47 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +13 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +242 -4
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +3 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +7 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +62 -11
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +15 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +3 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +5 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +798 -22
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +156 -13
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +140 -40
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -2
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +20 -6
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +14 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -3
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +23 -6
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +264 -9
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +876 -23
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +99 -0
- data/plugins/rigor-dry-monads/lib/rigor-dry-monads.rb +5 -0
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +34 -0
- data/plugins/rigor-ethon/lib/rigor-ethon.rb +3 -0
- data/plugins/rigor-ethon/sig/ethon.rbs +27 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +198 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/binding_recognizer.rb +75 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/catalog.rb +64 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +173 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +46 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +161 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +121 -0
- data/plugins/rigor-ffi/lib/rigor-ffi.rb +3 -0
- data/plugins/rigor-ffi/sig/ffi.rbs +67 -0
- data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +19 -0
- data/plugins/rigor-ffi-rzmq/lib/rigor-ffi-rzmq.rb +3 -0
- data/plugins/rigor-ffi-rzmq/sig/ffi_rzmq.rbs +29 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +2 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +2 -1
- data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +156 -11
- data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +30 -0
- data/plugins/rigor-rbnacl/lib/rigor-rbnacl.rb +3 -0
- data/plugins/rigor-rbnacl/sig/rbnacl.rbs +22 -0
- data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +55 -0
- data/plugins/rigor-sassc/lib/rigor-sassc.rb +3 -0
- data/plugins/rigor-sassc/sig/sassc.rbs +24 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +32 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +3 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +63 -10
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +15 -2
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +61 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +4 -2
- data/sig/rigor/analysis/baseline.rbs +1 -1
- data/sig/rigor/environment.rbs +3 -0
- data/sig/rigor/inference.rbs +4 -1
- data/sig/rigor/plugin/io_boundary.rbs +2 -0
- data/sig/rigor/reflection.rbs +3 -1
- data/sig/rigor/scope.rbs +25 -1
- data/sig/rigor/type.rbs +95 -1
- data/sig/rigor.rbs +16 -0
- data/skills/rigor-ci-setup/SKILL.md +2 -2
- data/skills/rigor-editor-setup/SKILL.md +2 -2
- data/skills/rigor-mcp-setup/SKILL.md +2 -2
- data/skills/rigor-monkeypatch-resolve/SKILL.md +1 -1
- data/skills/rigor-plugin-review/SKILL.md +3 -3
- metadata +73 -3
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
3
5
|
require_relative "../type"
|
|
4
6
|
require_relative "../source/node_children"
|
|
7
|
+
require_relative "content_join"
|
|
5
8
|
require_relative "receiver_alias"
|
|
6
9
|
|
|
7
10
|
module Rigor
|
|
@@ -91,6 +94,12 @@ module Rigor
|
|
|
91
94
|
replace
|
|
92
95
|
].to_set.freeze
|
|
93
96
|
|
|
97
|
+
# Every method name {#widen_for_mutator} responds to — the one set a body scan asks "could an
|
|
98
|
+
# in-place call on this name change its binding?" against (issue #587: the block-return
|
|
99
|
+
# threading gate). Derived from the two tables above rather than spelled out, so the scan and
|
|
100
|
+
# the widening it predicts cannot drift apart.
|
|
101
|
+
SHAPE_MUTATORS = (ARRAY_MUTATORS | HASH_MUTATORS).freeze
|
|
102
|
+
|
|
94
103
|
# Methods that return the receiver (or a shallow copy) and cannot mutate it. They must not
|
|
95
104
|
# trigger widening or any other receiver-fact invalidation. The list is intentionally
|
|
96
105
|
# narrow — only methods whose purity is unconditional and whose return value is the
|
|
@@ -111,17 +120,45 @@ module Rigor
|
|
|
111
120
|
# `non-empty-hash`) AND whose call name is a known in-place mutator for that shape.
|
|
112
121
|
# Returns `current_scope` unchanged otherwise.
|
|
113
122
|
#
|
|
123
|
+
# `arg_types` carries the mutator call's argument types, already typed by the caller in the
|
|
124
|
+
# scope the arguments are evaluated in. They are what the widened carrier JOINS its added
|
|
125
|
+
# content evidence from (issue #560); an empty list means "no evidence", which reproduces the
|
|
126
|
+
# pre-join behaviour exactly.
|
|
127
|
+
NO_ARG_TYPES = [].freeze
|
|
128
|
+
|
|
114
129
|
# @param call_node [Prism::CallNode]
|
|
115
130
|
# @param current_scope [Rigor::Scope]
|
|
131
|
+
# @param arg_types [Array<Rigor::Type::Base>]
|
|
116
132
|
# @return [Rigor::Scope]
|
|
117
|
-
def widen_after_call(call_node:, current_scope:)
|
|
133
|
+
def widen_after_call(call_node:, current_scope:, arg_types: NO_ARG_TYPES)
|
|
118
134
|
return current_scope if pure_self_returner?(call_node.name)
|
|
119
135
|
|
|
120
|
-
receiver
|
|
136
|
+
widen_receiver_aliases(call_node.receiver, call_node.name, current_scope, arg_types: arg_types)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# True when `receiver` names at least one variable whose CURRENT binding is a literal-shape
|
|
140
|
+
# carrier — the only pre-state {#widen_for_mutator} joins into. Callers use it to skip typing a
|
|
141
|
+
# mutator's arguments when nothing will consume them: `buf << x` on a String and `arr << x` on an
|
|
142
|
+
# already-nominal Array are the common cases, and `Scope#type_of` memoizes nothing.
|
|
143
|
+
def joinable_receiver?(receiver, scope)
|
|
144
|
+
return false if receiver.nil?
|
|
145
|
+
|
|
146
|
+
ReceiverAlias.candidates(receiver).any? do |read|
|
|
147
|
+
current =
|
|
148
|
+
case read
|
|
149
|
+
when Prism::LocalVariableReadNode then scope.local(read.name)
|
|
150
|
+
when Prism::InstanceVariableReadNode then scope.ivar(read.name)
|
|
151
|
+
end
|
|
152
|
+
current.is_a?(Type::Tuple) || current.is_a?(Type::HashShape)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Widens every variable `receiver` can evaluate to, against `method_name`'s mutator table.
|
|
157
|
+
def widen_receiver_aliases(receiver, method_name, current_scope, arg_types: NO_ARG_TYPES)
|
|
121
158
|
return current_scope if receiver.nil?
|
|
122
159
|
|
|
123
160
|
ReceiverAlias.candidates(receiver).reduce(current_scope) do |acc, read|
|
|
124
|
-
widen_alias_read(
|
|
161
|
+
widen_alias_read(method_name, read, acc, arg_types: arg_types)
|
|
125
162
|
end
|
|
126
163
|
end
|
|
127
164
|
|
|
@@ -179,55 +216,223 @@ module Rigor
|
|
|
179
216
|
# name against the OUTER scope would hit an unrelated same-named binding.
|
|
180
217
|
next acc if read.is_a?(Prism::LocalVariableReadNode) && read.depth.zero?
|
|
181
218
|
|
|
182
|
-
|
|
219
|
+
# `values: :keep` — this is the block-capture path, and the slice-C content join that runs
|
|
220
|
+
# after it re-adds the appended values' types onto the widened seed, so the seed's own value
|
|
221
|
+
# pinning is still justified (`out = [0]; arr.each { out << x }` never rewrites slot 0). The
|
|
222
|
+
# straight-line paths have no such join, which is why their default is `:widen` (issue #560).
|
|
223
|
+
widen_alias_read(call_node.name, read, acc, values: :keep)
|
|
183
224
|
end
|
|
184
225
|
end
|
|
185
226
|
|
|
186
|
-
def widen_alias_read(method_name, read, scope)
|
|
227
|
+
def widen_alias_read(method_name, read, scope, values: :widen, arg_types: NO_ARG_TYPES)
|
|
187
228
|
case read
|
|
188
|
-
when Prism::LocalVariableReadNode
|
|
189
|
-
|
|
229
|
+
when Prism::LocalVariableReadNode
|
|
230
|
+
widen_local(method_name, read.name, scope, values: values, arg_types: arg_types)
|
|
231
|
+
when Prism::InstanceVariableReadNode
|
|
232
|
+
widen_ivar(method_name, read.name, scope, values: values, arg_types: arg_types)
|
|
190
233
|
else scope
|
|
191
234
|
end
|
|
192
235
|
end
|
|
193
236
|
|
|
194
|
-
def widen_local(method_name, var_name, current_scope)
|
|
237
|
+
def widen_local(method_name, var_name, current_scope, values: :widen, arg_types: NO_ARG_TYPES)
|
|
195
238
|
current = current_scope.local(var_name)
|
|
196
|
-
widened = widen_for_mutator(current, method_name)
|
|
239
|
+
widened = widen_for_mutator(current, method_name, values: values, arg_types: arg_types)
|
|
197
240
|
return current_scope if widened.nil?
|
|
198
241
|
|
|
199
242
|
current_scope.with_local(var_name, widened)
|
|
200
243
|
end
|
|
201
244
|
|
|
202
|
-
def widen_ivar(method_name, var_name, current_scope)
|
|
245
|
+
def widen_ivar(method_name, var_name, current_scope, values: :widen, arg_types: NO_ARG_TYPES)
|
|
203
246
|
current = current_scope.ivar(var_name)
|
|
204
|
-
widened = widen_for_mutator(current, method_name)
|
|
247
|
+
widened = widen_for_mutator(current, method_name, values: values, arg_types: arg_types)
|
|
205
248
|
return current_scope if widened.nil?
|
|
206
249
|
|
|
207
250
|
current_scope.with_ivar(var_name, widened)
|
|
208
251
|
end
|
|
209
252
|
|
|
253
|
+
# Mutators that can land a NEW value in an EXISTING slot, falsifying that slot's value pinning
|
|
254
|
+
# (`t[0] += 5` holds 6 where `Constant[1]` was — issue #560). Adders, removers, and reorderers
|
|
255
|
+
# leave every surviving slot's value intact, so their widening keeps the pinning; what an ADDER
|
|
256
|
+
# introduces is covered by the {#join_added_elements} join instead, which is why widening the
|
|
257
|
+
# SEED's pinning under `<<` (the shape haml's hand-written sigs pin) is not needed either.
|
|
258
|
+
VALUE_REWRITING_MUTATORS = %i[[]= fill map! collect! replace store merge! update transform_values!].to_set.freeze
|
|
259
|
+
private_constant :VALUE_REWRITING_MUTATORS
|
|
260
|
+
|
|
210
261
|
# Returns the widened type for a binding whose receiver is about to be mutated by
|
|
211
262
|
# `method_name`, or `nil` when no widening applies (binding is not a literal-shape
|
|
212
263
|
# carrier, OR the method is not a mutator for that shape, OR the binding is already a
|
|
213
264
|
# nominal — no precision to lose).
|
|
214
|
-
def widen_for_mutator(type, method_name)
|
|
265
|
+
def widen_for_mutator(type, method_name, values: :widen, arg_types: NO_ARG_TYPES)
|
|
266
|
+
values = :keep unless VALUE_REWRITING_MUTATORS.include?(method_name)
|
|
267
|
+
|
|
215
268
|
return nil if type.nil?
|
|
216
269
|
|
|
217
270
|
case type
|
|
218
271
|
when Type::Tuple
|
|
219
272
|
return nil unless ARRAY_MUTATORS.include?(method_name)
|
|
220
273
|
|
|
221
|
-
widen_tuple(type)
|
|
274
|
+
join_added_elements(widen_tuple(type, values: values), method_name, arg_types, type.elements)
|
|
222
275
|
when Type::HashShape
|
|
223
276
|
return nil unless HASH_MUTATORS.include?(method_name)
|
|
224
277
|
|
|
225
|
-
widen_hash_shape(type)
|
|
278
|
+
join_added_pairs(widen_hash_shape(type, values: values), method_name, arg_types,
|
|
279
|
+
ContentJoin.hash_shape_key_values(type))
|
|
226
280
|
when Type::Difference
|
|
227
281
|
widen_difference(type, method_name)
|
|
228
282
|
end
|
|
229
283
|
end
|
|
230
284
|
|
|
285
|
+
# Joins the element evidence the mutator's own ARGUMENTS introduce into the already-widened
|
|
286
|
+
# Array carrier (issue #560). Without it the widening keeps only the SEED's elements, which
|
|
287
|
+
# under-covers precisely the value the mutation added: `u = [1, 2]; u.push(6)` left
|
|
288
|
+
# `Array[1 | 2]`, so `u.last == 6` constant-folded to false and fired a false always-falsey on
|
|
289
|
+
# correct code. {ContentJoin} owns the algebra — including the seed-admissibility gate that
|
|
290
|
+
# keeps a heterogeneous accumulator from growing a member a hand-written signature rejects.
|
|
291
|
+
#
|
|
292
|
+
# The added evidence is **value-pin widened** first, so the join never manufactures a NEW
|
|
293
|
+
# constant fold in place of the one it removes: `opts[:mode] = :fast` must not leave
|
|
294
|
+
# `Constant[:fast]` as the whole value bound and let a later `opts[:mode] == :fast` fold to
|
|
295
|
+
# `Constant[true]` — that would trade an always-falsey FP for an always-truthy one on the same
|
|
296
|
+
# shape. Widening it is also what makes the seed's class set the right admissibility test.
|
|
297
|
+
#
|
|
298
|
+
# Non-adders (removers, reorderers) introduce no element evidence and return the widened
|
|
299
|
+
# carrier untouched.
|
|
300
|
+
#
|
|
301
|
+
# `seed_elements` is the PRE-STATE literal's own element list, not the widened carrier's —
|
|
302
|
+
# `[]` and `[x]` on an untyped `x` widen to the SAME `Array[Dynamic[top]]`, and only the
|
|
303
|
+
# literal distinguishes "no evidence" from "one slot holds something unknown".
|
|
304
|
+
#
|
|
305
|
+
# **The join records what it saw; it never CLOSES the parameter.** That is
|
|
306
|
+
# {#gradual_floor}'s job, and the reason is the seam this method sits on rather than anything
|
|
307
|
+
# about Array or Hash.
|
|
308
|
+
def join_added_elements(widened, method_name, arg_types, seed_elements)
|
|
309
|
+
return widened unless ContentJoin::ARRAY_CONTENT_ADDERS.include?(method_name)
|
|
310
|
+
|
|
311
|
+
added = value_pin_widened(ContentJoin.array_added_elements(method_name, arg_types))
|
|
312
|
+
return widened if added.empty? && arg_types.empty?
|
|
313
|
+
|
|
314
|
+
admitted = added.empty? ? NO_ADDED_EVIDENCE : ContentJoin.admissible_evidence(seed_elements, added)
|
|
315
|
+
ContentJoin.join_array_content(widened, gradual_floor(admitted))
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Straight-line evidence is ALWAYS incomplete, so a straight-line join may never close the
|
|
319
|
+
# parameter it contributes to.
|
|
320
|
+
#
|
|
321
|
+
# This seam sees ONE store — the call being evaluated. The widening it feeds is a one-way
|
|
322
|
+
# door: it replaces the literal carrier with a `Nominal`, and {#widen_for_mutator} declines a
|
|
323
|
+
# `Nominal` outright, so the NEXT store is invisible. Closing over one sample of a growing
|
|
324
|
+
# population is therefore a wrong type, not an imprecise one:
|
|
325
|
+
#
|
|
326
|
+
# a = []
|
|
327
|
+
# a.push(1) # joins -> Array[Integer]
|
|
328
|
+
# a.push("s") # DECLINED -- pre-state is a Nominal now
|
|
329
|
+
# a.last.upcase # correct Ruby, prints "S"
|
|
330
|
+
#
|
|
331
|
+
# closed to `Array[Integer]` and drew `undefined method 'upcase' for Integer` on code that
|
|
332
|
+
# runs fine. `mail`'s `Message#to_yaml` is the same defect one carrier over — `hash = {}`,
|
|
333
|
+
# `hash['headers'] = {}`, then `hash['multipart_body'] = []` appended through the read, where
|
|
334
|
+
# the value parameter kept the first store's Hash arm and had dropped the Array arm.
|
|
335
|
+
#
|
|
336
|
+
# An earlier reading of this blamed the carrier — Array element unions are over positions and
|
|
337
|
+
# survive a missed store, Hash value unions are over keys and do not. The probe above refutes
|
|
338
|
+
# it: `a.last` selects a position exactly as `hash[k]` selects a key, and a dropped arm is a
|
|
339
|
+
# wrong answer either way. The real line is **how much the joining path saw**, and it puts the
|
|
340
|
+
# BLOCK path (ADR-56 slice C) on the other side: `content_writeback_block_captures` scans the
|
|
341
|
+
# whole body and joins every mutator call in it before writing back, so its evidence IS
|
|
342
|
+
# complete for that body and its precise join stays justified — `acc = []; xs.each { |x|
|
|
343
|
+
# acc.push(x) }` keeps reading `Array[Integer]`.
|
|
344
|
+
#
|
|
345
|
+
# `Array[Integer | Dynamic[top]]` still does everything issue #560 needs: a union carrying
|
|
346
|
+
# `Dynamic` cannot constant-fold, so every stale always-falsey the join was written to remove
|
|
347
|
+
# stays removed.
|
|
348
|
+
def gradual_floor(types)
|
|
349
|
+
types + [Type::Combinator.untyped]
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# The Hash-side twin of {#join_added_elements}: `h[k] = v` / `h.store(k, v)` join the stored
|
|
353
|
+
# key and value into the widened `Hash[K, V]` carrier, each admitted against its OWN side's
|
|
354
|
+
# seed evidence (a foreign key does not make the value gradual, or the reverse).
|
|
355
|
+
def join_added_pairs(widened, method_name, arg_types, seed_pairs)
|
|
356
|
+
return widened unless ContentJoin::HASH_CONTENT_ADDERS.include?(method_name)
|
|
357
|
+
return widened if arg_types.size < 2
|
|
358
|
+
|
|
359
|
+
added = value_pin_widened([arg_types.first, arg_types.last])
|
|
360
|
+
return widened unless added.size == 2
|
|
361
|
+
|
|
362
|
+
seed_keys, seed_values = seed_pairs
|
|
363
|
+
key = admitted_union(seed_keys, added.first)
|
|
364
|
+
value = admitted_union(seed_values, added.last)
|
|
365
|
+
ContentJoin.join_hash_content(widened, [[key, value]])
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# One carrier for a stored key or value. A `Hash` pair has a single slot per side, so the
|
|
369
|
+
# admissible list folds to a union rather than being truncated — and it takes the same
|
|
370
|
+
# {#gradual_floor} the element side takes, for the same reason: this seam saw one store.
|
|
371
|
+
def admitted_union(seed_members, added)
|
|
372
|
+
admitted = ContentJoin.admissible_evidence(seed_members, [added])
|
|
373
|
+
Type::Combinator.union(*gradual_floor(admitted))
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# A content adder whose arguments the extractor could read NOTHING out of is still one store: the
|
|
377
|
+
# mutation ran, and what it appended is unknown. It therefore takes the ordinary one-store treatment
|
|
378
|
+
# — no admitted evidence, and {#gradual_floor}'s `Dynamic[top]` arm — rather than an answer of its
|
|
379
|
+
# own.
|
|
380
|
+
#
|
|
381
|
+
# An EMPTY `arg_types` is a different case and keeps the pre-join carrier untouched: no argument
|
|
382
|
+
# machinery reached this call at all. The ADR-56 block-capture path is the one that matters — it
|
|
383
|
+
# passes none because its slice-C join re-adds the appended types afterwards, and flooring there
|
|
384
|
+
# would strip seed pinning it keeps on purpose — but it is not the only producer. A genuinely
|
|
385
|
+
# zero-arg adder (`m.concat`) and `content_arg_types`' own rescue land here too; both are runtime
|
|
386
|
+
# no-ops or unanalyzable, so leaving the carrier alone is right for them as well.
|
|
387
|
+
#
|
|
388
|
+
# Getting this wrong once is instructive enough to keep. An earlier cut replaced the carrier with a
|
|
389
|
+
# CLOSED `Array[<seed base>]`, reasoning that the retained constants were falsified so the honest
|
|
390
|
+
# answer was their nominal base. The first half is right — `m = [1, 2]; m.concat(xs)` really can
|
|
391
|
+
# leave `6` at the end, and keeping `Array[1 | 2]` folds `m.last == 6` to false on correct code. The
|
|
392
|
+
# second half broke this file's own rule two methods down (a seam that sees one store may never
|
|
393
|
+
# CLOSE the parameter) and cost exactly what that rule protects: `Array[Symbol]` under haml's
|
|
394
|
+
# hand-written `-> Array[:multi]` brought back the #561 `def.return-type-mismatch`, and
|
|
395
|
+
# `m.last.upcase` after the concat drew `undefined method` on code that is correct when `xs` holds
|
|
396
|
+
# strings.
|
|
397
|
+
#
|
|
398
|
+
# `Array[1 | 2 | Dynamic[top]]` settles all three: the `Dynamic` arm stops the fold, the surviving
|
|
399
|
+
# pinning keeps the accepting form the signature gate documents (`Array[:multi | Dynamic[top]]`), and
|
|
400
|
+
# a union carrying `Dynamic` dispatches quietly.
|
|
401
|
+
#
|
|
402
|
+
# Known and accepted false negative: `m.concat([])` is a runtime no-op, so `m.last == 6` there really
|
|
403
|
+
# is always false, and the `Dynamic` arm suppresses a CORRECT always-falsey. Telling an empty literal
|
|
404
|
+
# argument from an unreadable one is possible in principle; a false negative on a no-op call is a far
|
|
405
|
+
# better trade than the two false positives above.
|
|
406
|
+
NO_ADDED_EVIDENCE = [].freeze
|
|
407
|
+
|
|
408
|
+
# Normalizes the types the mutator's arguments contribute, before they join.
|
|
409
|
+
#
|
|
410
|
+
# `widen_value_pinned` erases a constant's VALUE. A stored literal collection needs the same
|
|
411
|
+
# treatment for its literal SHAPE, and for the same reason one step removed: the program keeps
|
|
412
|
+
# a reference to what it stored and mutates it through the slot —
|
|
413
|
+
#
|
|
414
|
+
# params[:f] ||= []
|
|
415
|
+
# params[:f] << :status
|
|
416
|
+
#
|
|
417
|
+
# is Redmine's `Query#as_params` idiom, six times over. The `<<` mutates the nested array, and
|
|
418
|
+
# nothing writes that back through the outer Hash's value parameter, so joining the literal
|
|
419
|
+
# `Tuple[]` would pin `Hash[Symbol, []]` on a hash whose slot really holds `[:status]` — a
|
|
420
|
+
# WRONG precise type, and `params[:f].empty?` would fold to `true` off it. That is the same
|
|
421
|
+
# class of stale fold this whole change exists to remove, so the shape goes with the value:
|
|
422
|
+
# `[]` joins as `Array[untyped]`, `{}` as `Hash[untyped, untyped]`. Both are true of the slot
|
|
423
|
+
# no matter what the program does to the object afterwards.
|
|
424
|
+
def value_pin_widened(types)
|
|
425
|
+
types.compact.map { |type| shape_erased(Type::Combinator.widen_value_pinned(type)) }
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def shape_erased(type)
|
|
429
|
+
case type
|
|
430
|
+
when Type::Tuple then widen_tuple(type, values: :widen)
|
|
431
|
+
when Type::HashShape then widen_hash_shape(type, values: :widen)
|
|
432
|
+
else type
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
231
436
|
# `non-empty-array[T]` / `non-empty-hash[K, V]` → the bare base nominal. These refinement
|
|
232
437
|
# carriers are what `empty?` / `any?` narrowing writes (ADR-47 §4-4), and they are just as
|
|
233
438
|
# invalidated by an in-place mutator as a `Tuple` is: `arr.clear` makes `arr` empty, so a
|
|
@@ -250,24 +455,30 @@ module Rigor
|
|
|
250
455
|
end
|
|
251
456
|
end
|
|
252
457
|
|
|
253
|
-
# `Tuple[A, B, C]` → `Nominal[Array, [union(A, B, C)]]
|
|
254
|
-
#
|
|
255
|
-
# `
|
|
256
|
-
|
|
458
|
+
# `Tuple[A, B, C]` → `Nominal[Array, [union(A, B, C)]]`; under `values: :widen`, each element's
|
|
459
|
+
# VALUE pinning widens to its class nominal (`1 | 2` → `Integer`): a slot-rewriting mutator
|
|
460
|
+
# falsifies values along with the shape — `t = [1, 2]; t[0] += 5` holds `6` at slot 0, so a
|
|
461
|
+
# surviving `Array[1 | 2]` feeds the constant-comparison fold and fires a false always-falsey
|
|
462
|
+
# on `t[0] == 6` (issue #560). An empty tuple has no element evidence, so the widened form
|
|
463
|
+
# carries `untyped` element bound — matches `BlockFolding`'s `tuple_to_array` widening.
|
|
464
|
+
def widen_tuple(tuple, values: :widen)
|
|
257
465
|
element_type =
|
|
258
466
|
if tuple.elements.empty?
|
|
259
467
|
Type::Combinator.untyped
|
|
260
|
-
elsif tuple.elements.size == 1
|
|
261
|
-
tuple.elements.first
|
|
262
468
|
else
|
|
263
|
-
|
|
469
|
+
elements = tuple.elements
|
|
470
|
+
elements = elements.map { |e| Type::Combinator.widen_value_pinned(e) } if values == :widen
|
|
471
|
+
elements.size == 1 ? elements.first : Type::Combinator.union(*elements)
|
|
264
472
|
end
|
|
265
473
|
Type::Combinator.nominal_of("Array", type_args: [element_type])
|
|
266
474
|
end
|
|
267
475
|
|
|
268
476
|
# `HashShape` (closed or open) → `Nominal[Hash, [Kunion, Vunion]]`. Empty / extra-keys-only
|
|
269
|
-
# shapes degrade to a fully-untyped Hash.
|
|
270
|
-
|
|
477
|
+
# shapes degrade to a fully-untyped Hash. Values widen their pinning the same way
|
|
478
|
+
# {#widen_tuple}'s elements do (issue #560): `opts = {headers: false}` then
|
|
479
|
+
# `opts[:encoding] = v` must not keep `false` as the whole value bound — redmine's
|
|
480
|
+
# `import.rb:274` read the stored key back through it and drew a false always-falsey.
|
|
481
|
+
def widen_hash_shape(shape, values: :widen)
|
|
271
482
|
if shape.pairs.empty?
|
|
272
483
|
return Type::Combinator.nominal_of("Hash",
|
|
273
484
|
type_args: [Type::Combinator.untyped,
|
|
@@ -275,160 +486,19 @@ module Rigor
|
|
|
275
486
|
end
|
|
276
487
|
|
|
277
488
|
key_type = key_union_for(shape.pairs.keys)
|
|
278
|
-
|
|
279
|
-
Type::Combinator.
|
|
489
|
+
value_types = shape.pairs.values
|
|
490
|
+
value_types = value_types.map { |v| Type::Combinator.widen_value_pinned(v) } if values == :widen
|
|
491
|
+
Type::Combinator.nominal_of("Hash", type_args: [key_type, Type::Combinator.union(*value_types)])
|
|
280
492
|
end
|
|
281
493
|
|
|
282
|
-
#
|
|
283
|
-
#
|
|
284
|
-
# keep their constant carrier (the constant IS the class's whole value set, and `nil` reads better
|
|
285
|
-
# than `NilClass` in a widened `Hash[K, V]`). We deliberately do NOT fold the widenable kinds to a
|
|
286
|
-
# `Constant<:k1> | Constant<:k2>` union — that would be a precision improvement that complicates
|
|
287
|
-
# the widening contract; the goal here is to LOSE precision, not to record a new fact set.
|
|
288
|
-
def key_union_for(keys)
|
|
289
|
-
carriers = keys.map { |k| key_widening_carrier(k) }.uniq
|
|
290
|
-
carriers.size == 1 ? carriers.first : Type::Combinator.union(*carriers)
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
def key_widening_carrier(key)
|
|
294
|
-
case key
|
|
295
|
-
when true, false, nil then Type::Combinator.constant_of(key)
|
|
296
|
-
else Type::Combinator.nominal_of(key.class.name)
|
|
297
|
-
end
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
# ----------------------------------------------------------------
|
|
301
|
-
# ADR-56 slice C — receiver-content element-type JOIN.
|
|
494
|
+
# ADR-56 slice C's content JOIN — the other half of this module — lives in {ContentJoin}, which
|
|
495
|
+
# both the block-capture path and the straight-line path above share.
|
|
302
496
|
#
|
|
303
|
-
# `
|
|
304
|
-
#
|
|
305
|
-
#
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
# parameter, so the result is `Array[0 | Integer]` rather than `Array[0]`.
|
|
309
|
-
#
|
|
310
|
-
# Array content-mutators that append/store ELEMENTS. The appended element type is the
|
|
311
|
-
# call's argument type(s); `[]=`'s value is its LAST argument (the keys precede it).
|
|
312
|
-
# Subset of `ARRAY_MUTATORS`: only the element-INTRODUCING methods (removers / reorderers
|
|
313
|
-
# add no new element evidence and are already covered by the arity-forget).
|
|
314
|
-
ARRAY_CONTENT_ADDERS = %i[
|
|
315
|
-
<< push append prepend unshift concat insert []= fill replace
|
|
316
|
-
].to_set.freeze
|
|
317
|
-
|
|
318
|
-
# Hash content-mutators that store a key→value pair. For `[]=` / `store` the key is the
|
|
319
|
-
# first argument and the value the last.
|
|
320
|
-
HASH_CONTENT_ADDERS = %i[[]= store].to_set.freeze
|
|
321
|
-
|
|
322
|
-
# String content-mutators that append to the buffer. String carries no element parameter,
|
|
323
|
-
# so these contribute nothing to a join — they are listed so the orchestrator recognises
|
|
324
|
-
# them as content mutators (the binding already widens to `String` via normal typing);
|
|
325
|
-
# the join helpers below short-circuit on a non-collection pre-state.
|
|
326
|
-
STRING_CONTENT_ADDERS = %i[<< concat prepend insert replace].to_set.freeze
|
|
327
|
-
|
|
328
|
-
# Every method name that mutates a captured local's CONTENT — the union the orchestrator
|
|
329
|
-
# scans the block body for.
|
|
330
|
-
CONTENT_ADDERS = (ARRAY_CONTENT_ADDERS | HASH_CONTENT_ADDERS | STRING_CONTENT_ADDERS).freeze
|
|
331
|
-
|
|
332
|
-
# The element types a single content-mutator call introduces into an Array, given the
|
|
333
|
-
# per-argument types (already typed in the block body scope). `concat`/`replace` take
|
|
334
|
-
# collection arguments, so their element evidence is the arguments' OWN element types
|
|
335
|
-
# unioned; the rest append the argument values directly. Returns `[]` when no element
|
|
336
|
-
# evidence (e.g. a `<<` with no resolvable arg).
|
|
337
|
-
def array_added_elements(method_name, arg_types)
|
|
338
|
-
return [] if arg_types.empty?
|
|
339
|
-
|
|
340
|
-
case method_name
|
|
341
|
-
when :concat, :replace
|
|
342
|
-
arg_types.flat_map { |t| collection_element_types(t) }
|
|
343
|
-
when :insert
|
|
344
|
-
# `insert(index, *objs)` — first arg is the position.
|
|
345
|
-
arg_types.drop(1)
|
|
346
|
-
when :[]=
|
|
347
|
-
# `arr[i] = v` / `arr[i, n] = v` — value is the last argument.
|
|
348
|
-
[arg_types.last]
|
|
349
|
-
when :fill
|
|
350
|
-
# `fill(value)` — only the no-block single-value form adds a
|
|
351
|
-
# concrete element; block / range forms are conservatively
|
|
352
|
-
# ignored (the arity-forget already widened the binding).
|
|
353
|
-
arg_types.size == 1 ? arg_types : []
|
|
354
|
-
else # << push append prepend unshift
|
|
355
|
-
arg_types
|
|
356
|
-
end
|
|
357
|
-
end
|
|
358
|
-
|
|
359
|
-
# Builds the continuation Array type from the pre-state binding and the appended element
|
|
360
|
-
# types. The floor is `Array[Dynamic[top]]` (the sound empty-seed behaviour) when there is
|
|
361
|
-
# no element evidence at all.
|
|
362
|
-
def join_array_content(pre_state, added_elements)
|
|
363
|
-
seed_elements = collection_element_types(pre_state)
|
|
364
|
-
added = added_elements.compact
|
|
365
|
-
# The empty-seed floor element is `Dynamic[top]` (no element evidence). When real
|
|
366
|
-
# appended evidence exists that floor carries nothing, so drop it — an empty accumulator
|
|
367
|
-
# built by `out << x*2` reads `Array[Integer]`, not `Array[Integer | Dynamic[top]]`.
|
|
368
|
-
seed_elements = drop_dynamic(seed_elements) unless added.empty?
|
|
369
|
-
elements = seed_elements + added
|
|
370
|
-
return Type::Combinator.nominal_of("Array", type_args: [Type::Combinator.untyped]) if elements.empty?
|
|
371
|
-
|
|
372
|
-
Type::Combinator.nominal_of("Array", type_args: [Type::Combinator.union(*elements)])
|
|
373
|
-
end
|
|
374
|
-
|
|
375
|
-
# Builds the continuation Hash type from the pre-state binding and a list of `[key_type,
|
|
376
|
-
# value_type]` pairs stored by `[]=` / `store`.
|
|
377
|
-
def join_hash_content(pre_state, added_pairs)
|
|
378
|
-
seed_keys, seed_values = hash_shape_key_values(pre_state)
|
|
379
|
-
added_keys = added_pairs.map(&:first).compact
|
|
380
|
-
added_values = added_pairs.map(&:last).compact
|
|
381
|
-
seed_keys = drop_dynamic(seed_keys) unless added_keys.empty?
|
|
382
|
-
seed_values = drop_dynamic(seed_values) unless added_values.empty?
|
|
383
|
-
keys = seed_keys + added_keys
|
|
384
|
-
values = seed_values + added_values
|
|
385
|
-
key_t = keys.empty? ? Type::Combinator.untyped : Type::Combinator.union(*keys)
|
|
386
|
-
value_t = values.empty? ? Type::Combinator.untyped : Type::Combinator.union(*values)
|
|
387
|
-
Type::Combinator.nominal_of("Hash", type_args: [key_t, value_t])
|
|
388
|
-
end
|
|
389
|
-
|
|
390
|
-
# Drops `Dynamic` (incl. `untyped`) constituents from a type list.
|
|
391
|
-
def drop_dynamic(types)
|
|
392
|
-
types.grep_v(Type::Dynamic)
|
|
393
|
-
end
|
|
394
|
-
|
|
395
|
-
# Element types carried by a collection binding, regardless of which carrier holds them: a
|
|
396
|
-
# `Tuple` lists them, a `Nominal[Array, [E]]` has one element param, a bare `Array` /
|
|
397
|
-
# anything else yields none.
|
|
398
|
-
def collection_element_types(type)
|
|
399
|
-
case type
|
|
400
|
-
when Type::Tuple
|
|
401
|
-
type.elements
|
|
402
|
-
when Type::Nominal
|
|
403
|
-
type.class_name == "Array" ? type.type_args : []
|
|
404
|
-
when Type::Union
|
|
405
|
-
# A loop's single-pass join can union the widened collection with its un-widened
|
|
406
|
-
# literal seed (`Array[0] | [0]`); pull element evidence from every Array-ish member.
|
|
407
|
-
type.members.flat_map { |m| collection_element_types(m) }
|
|
408
|
-
else
|
|
409
|
-
[]
|
|
410
|
-
end
|
|
411
|
-
end
|
|
412
|
-
|
|
413
|
-
# `[keys, values]` evidence from a Hash-ish pre-state binding — a `HashShape` (literal
|
|
414
|
-
# pairs) or a `Nominal[Hash, [K, V]]`.
|
|
415
|
-
def hash_shape_key_values(type)
|
|
416
|
-
case type
|
|
417
|
-
when Type::HashShape
|
|
418
|
-
return [[], []] if type.pairs.empty?
|
|
419
|
-
|
|
420
|
-
[[key_union_for(type.pairs.keys)], type.pairs.values]
|
|
421
|
-
when Type::Nominal
|
|
422
|
-
type.class_name == "Hash" && type.type_args.size == 2 ? [[type.type_args[0]], [type.type_args[1]]] : [[], []]
|
|
423
|
-
when Type::Union
|
|
424
|
-
type.members.each_with_object([[], []]) do |m, (ks, vs)|
|
|
425
|
-
mk, mv = hash_shape_key_values(m)
|
|
426
|
-
ks.concat(mk)
|
|
427
|
-
vs.concat(mv)
|
|
428
|
-
end
|
|
429
|
-
else
|
|
430
|
-
[[], []]
|
|
431
|
-
end
|
|
497
|
+
# `key_union_for` is delegated rather than duplicated: {#widen_hash_shape} and
|
|
498
|
+
# `ContentJoin.hash_shape_key_values` must map a literal key set the SAME way, or a widened
|
|
499
|
+
# carrier and the join that reads it back disagree about the key parameter.
|
|
500
|
+
def key_union_for(keys)
|
|
501
|
+
ContentJoin.key_union_for(keys)
|
|
432
502
|
end
|
|
433
503
|
end
|
|
434
504
|
end
|