rigortype 0.2.8 → 0.3.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 +2 -2
- data/data/core_overlay/csv.rbs +28 -0
- data/data/core_overlay/psych.rbs +22 -0
- data/docs/handbook/01-getting-started.md +9 -1
- data/docs/handbook/02-everyday-types.md +4 -1
- data/docs/handbook/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +18 -9
- data/docs/handbook/08-understanding-errors.md +4 -3
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +99 -4
- data/docs/manual/02-cli-reference.md +65 -19
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +23 -0
- data/docs/manual/07-plugins.md +5 -5
- data/docs/manual/09-editor-integration.md +4 -4
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- data/docs/manual/14-rails-quickstart.md +4 -2
- data/docs/manual/15-type-protection-coverage.md +21 -0
- data/docs/manual/plugins/rigor-actionpack.md +1 -1
- data/docs/manual/plugins/rigor-activerecord.md +12 -5
- data/docs/manual/plugins/rigor-rails-routes.md +11 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +93 -0
- data/lib/rigor/analysis/check_rules.rb +557 -95
- data/lib/rigor/analysis/dependency_recorder.rb +93 -9
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +456 -51
- data/lib/rigor/analysis/path_expansion.rb +42 -0
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
- data/lib/rigor/analysis/rule_catalog.rb +226 -1
- data/lib/rigor/analysis/run_cache_key.rb +68 -0
- data/lib/rigor/analysis/run_cache_probe.rb +72 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
- data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
- data/lib/rigor/analysis/runner.rb +343 -68
- data/lib/rigor/analysis/severity_stamp.rb +43 -0
- data/lib/rigor/analysis/worker_session.rb +30 -2
- data/lib/rigor/bleeding_edge.rb +37 -8
- data/lib/rigor/builtins/regex_refinement.rb +36 -1
- data/lib/rigor/cache/descriptor.rb +88 -28
- data/lib/rigor/cache/file_digest.rb +158 -0
- data/lib/rigor/cache/incremental_snapshot.rb +41 -5
- data/lib/rigor/cache/rbs_descriptor.rb +56 -10
- data/lib/rigor/cache/store.rb +207 -60
- data/lib/rigor/ci_detector.rb +90 -0
- data/lib/rigor/cli/annotate_command.rb +15 -9
- data/lib/rigor/cli/baseline_command.rb +4 -1
- data/lib/rigor/cli/check_command.rb +212 -27
- data/lib/rigor/cli/coverage_command.rb +56 -30
- data/lib/rigor/cli/diagnostic_formats.rb +4 -1
- data/lib/rigor/cli/docs_command.rb +0 -30
- data/lib/rigor/cli/doctor_command.rb +163 -23
- data/lib/rigor/cli/lsp_command.rb +5 -0
- data/lib/rigor/cli/mcp_command.rb +5 -0
- data/lib/rigor/cli/plugins_command.rb +36 -6
- data/lib/rigor/cli/plugins_renderer.rb +44 -12
- data/lib/rigor/cli/probe_environment.rb +85 -0
- data/lib/rigor/cli/protection_fork_scan.rb +55 -0
- data/lib/rigor/cli/protection_report.rb +7 -1
- data/lib/rigor/cli/sig_gen_command.rb +36 -9
- data/lib/rigor/cli/skill_command.rb +1 -32
- data/lib/rigor/cli/trace_command.rb +5 -6
- data/lib/rigor/cli/type_of_command.rb +9 -8
- data/lib/rigor/cli/type_scan_command.rb +8 -8
- data/lib/rigor/cli.rb +15 -4
- data/lib/rigor/config_audit.rb +48 -2
- data/lib/rigor/configuration/severity_profile.rb +31 -0
- data/lib/rigor/configuration.rb +156 -8
- data/lib/rigor/environment/default_libraries.rb +35 -0
- data/lib/rigor/environment/missing_gem_constant_index.rb +128 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +71 -43
- data/lib/rigor/inference/budget_trace.rb +77 -2
- data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
- data/lib/rigor/inference/def_handle.rb +23 -0
- data/lib/rigor/inference/def_node_resolver.rb +90 -0
- data/lib/rigor/inference/def_return_typer.rb +2 -1
- data/lib/rigor/inference/expression_typer.rb +379 -113
- data/lib/rigor/inference/fork_map.rb +87 -0
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +11 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
- data/lib/rigor/inference/method_dispatcher.rb +57 -1
- data/lib/rigor/inference/mutation_widening.rb +42 -8
- data/lib/rigor/inference/narrowing.rb +245 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +70 -10
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +8 -1
- data/lib/rigor/inference/scope_indexer.rb +460 -49
- data/lib/rigor/inference/statement_evaluator.rb +111 -17
- data/lib/rigor/inference/struct_fold_safety.rb +48 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
- data/lib/rigor/inference/void_origin.rb +25 -0
- data/lib/rigor/inference/void_tail_summary.rb +220 -0
- data/lib/rigor/language_server/completion_provider.rb +2 -1
- data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
- data/lib/rigor/language_server/folding_range_provider.rb +2 -1
- data/lib/rigor/language_server/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/plugin/base.rb +9 -22
- data/lib/rigor/plugin/inflector.rb +12 -3
- data/lib/rigor/plugin/io_boundary.rb +5 -1
- data/lib/rigor/plugin/isolation.rb +81 -11
- data/lib/rigor/plugin/load_error.rb +10 -1
- data/lib/rigor/plugin/loader.rb +96 -14
- data/lib/rigor/plugin/node_rule_walk.rb +5 -3
- data/lib/rigor/plugin/registry.rb +32 -23
- data/lib/rigor/plugin.rb +26 -0
- data/lib/rigor/protection/mutator.rb +3 -2
- data/lib/rigor/reflection.rb +64 -0
- data/lib/rigor/runtime/jit.rb +128 -0
- data/lib/rigor/scope/discovery_index.rb +12 -2
- data/lib/rigor/scope.rb +122 -28
- data/lib/rigor/sig_gen/classification.rb +5 -1
- data/lib/rigor/sig_gen/generator.rb +38 -6
- data/lib/rigor/sig_gen/observation_collector.rb +4 -3
- data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
- data/lib/rigor/sig_gen/renderer.rb +10 -0
- data/lib/rigor/sig_gen/write_result.rb +9 -4
- data/lib/rigor/sig_gen/writer.rb +24 -2
- data/lib/rigor/source/node_children.rb +116 -0
- data/lib/rigor/source/node_locator.rb +3 -1
- data/lib/rigor/source/node_walker.rb +4 -2
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/difference.rb +28 -24
- data/lib/rigor/type/hash_shape.rb +34 -10
- data/lib/rigor/version.rb +1 -1
- data/lib/rigortype.rb +24 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +23 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +21 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +13 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +65 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +20 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +172 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +22 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +32 -0
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
- data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +191 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +19 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +48 -15
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +42 -12
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
- data/sig/prism_node_children.rbs +9 -0
- data/sig/rigor/environment.rbs +1 -0
- data/sig/rigor/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +2 -3
- data/sig/rigor/reflection.rbs +1 -0
- data/sig/rigor/scope.rbs +12 -1
- data/skills/rigor-ci-setup/SKILL.md +10 -0
- data/skills/rigor-editor-setup/SKILL.md +11 -0
- data/skills/rigor-next-steps/SKILL.md +23 -2
- data/skills/rigor-plugin-author/SKILL.md +3 -3
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
- data/skills/rigor-plugin-review/SKILL.md +2 -2
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
- data/skills/rigor-project-init/SKILL.md +29 -3
- data/skills/rigor-project-init/references/01-detect.md +1 -1
- data/skills/rigor-project-init/references/02-configure.md +39 -5
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
- data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
- metadata +49 -24
- data/lib/rigor/cli/ci_detector.rb +0 -89
|
@@ -4,6 +4,7 @@ require_relative "../../reflection"
|
|
|
4
4
|
require_relative "../../type"
|
|
5
5
|
require_relative "../../rbs_extended"
|
|
6
6
|
require_relative "../rbs_type_translator"
|
|
7
|
+
require_relative "../void_origin"
|
|
7
8
|
require_relative "overload_selector"
|
|
8
9
|
|
|
9
10
|
module Rigor
|
|
@@ -96,7 +97,8 @@ module Rigor
|
|
|
96
97
|
block_type: context.block_type,
|
|
97
98
|
self_type_override: context.self_type_override,
|
|
98
99
|
public_only: context.public_only,
|
|
99
|
-
scope: context.scope
|
|
100
|
+
scope: context.scope,
|
|
101
|
+
call_node: context.call_node
|
|
100
102
|
)
|
|
101
103
|
end
|
|
102
104
|
|
|
@@ -135,23 +137,23 @@ module Rigor
|
|
|
135
137
|
private
|
|
136
138
|
|
|
137
139
|
def dispatch_for(receiver:, method_name:, args:, environment:, block_type:, self_type_override: nil, # rubocop:disable Metrics/ParameterLists
|
|
138
|
-
public_only: false, scope: nil)
|
|
140
|
+
public_only: false, scope: nil, call_node: nil)
|
|
139
141
|
args ||= []
|
|
140
142
|
case receiver
|
|
141
143
|
when Type::Union
|
|
142
144
|
dispatch_union(receiver, method_name, args, environment, block_type, self_type_override,
|
|
143
|
-
public_only: public_only, scope: scope)
|
|
145
|
+
public_only: public_only, scope: scope, call_node: call_node)
|
|
144
146
|
else
|
|
145
147
|
dispatch_one(receiver, method_name, args, environment, block_type, self_type_override,
|
|
146
|
-
public_only: public_only, scope: scope)
|
|
148
|
+
public_only: public_only, scope: scope, call_node: call_node)
|
|
147
149
|
end
|
|
148
150
|
end
|
|
149
151
|
|
|
150
152
|
def dispatch_union(receiver, method_name, args, environment, block_type, self_type_override = nil, # rubocop:disable Metrics/ParameterLists
|
|
151
|
-
public_only: false, scope: nil)
|
|
153
|
+
public_only: false, scope: nil, call_node: nil)
|
|
152
154
|
results = receiver.members.map do |member|
|
|
153
155
|
dispatch_one(member, method_name, args, environment, block_type, self_type_override,
|
|
154
|
-
public_only: public_only, scope: scope)
|
|
156
|
+
public_only: public_only, scope: scope, call_node: call_node)
|
|
155
157
|
end
|
|
156
158
|
return nil if results.any?(&:nil?)
|
|
157
159
|
|
|
@@ -159,7 +161,7 @@ module Rigor
|
|
|
159
161
|
end
|
|
160
162
|
|
|
161
163
|
def dispatch_one(receiver, method_name, args, environment, block_type, self_type_override = nil, # rubocop:disable Metrics/ParameterLists
|
|
162
|
-
public_only: false, scope: nil)
|
|
164
|
+
public_only: false, scope: nil, call_node: nil)
|
|
163
165
|
descriptor = receiver_descriptor(receiver)
|
|
164
166
|
return nil unless descriptor
|
|
165
167
|
|
|
@@ -173,11 +175,14 @@ module Rigor
|
|
|
173
175
|
method_definition,
|
|
174
176
|
class_name: class_name,
|
|
175
177
|
kind: kind,
|
|
178
|
+
method_name: method_name,
|
|
176
179
|
args: args,
|
|
177
180
|
type_vars: type_vars,
|
|
178
181
|
block_type: block_type,
|
|
179
182
|
environment: environment,
|
|
180
|
-
self_type_override: self_type_override
|
|
183
|
+
self_type_override: self_type_override,
|
|
184
|
+
scope: scope,
|
|
185
|
+
call_node: call_node
|
|
181
186
|
)
|
|
182
187
|
rescue StandardError
|
|
183
188
|
# Defensive: if RBS' definition builder raises on a broken hierarchy (e.g., partially loaded
|
|
@@ -318,7 +323,8 @@ module Rigor
|
|
|
318
323
|
|
|
319
324
|
# rubocop:disable Metrics/ParameterLists
|
|
320
325
|
def translate_return_type(method_definition, class_name:, kind:, args:, type_vars:, block_type:,
|
|
321
|
-
environment: nil, self_type_override: nil
|
|
326
|
+
method_name: nil, environment: nil, self_type_override: nil,
|
|
327
|
+
scope: nil, call_node: nil)
|
|
322
328
|
# rubocop:enable Metrics/ParameterLists
|
|
323
329
|
# Slice 4b-3 (ADR-7 § "Slice 4-A/4-B") — read the return-type override through the merger so
|
|
324
330
|
# future plugin / `:rbs_extended` bundles that also assert a `return_type` slot at this call
|
|
@@ -348,6 +354,15 @@ module Rigor
|
|
|
348
354
|
)
|
|
349
355
|
return nil unless method_type
|
|
350
356
|
|
|
357
|
+
# ADR-100 WD2/WD3 — the return-typing tier is where `void → top` widens, so it is the one place
|
|
358
|
+
# that still knows the RBS return was an author-declared `-> void` before the translator erases
|
|
359
|
+
# it to a plain `top`. Record the recovery on the scope's `void_origins` side-table, keyed by the
|
|
360
|
+
# call node, so the `static.value-use.void` check rule can fire when this `top` is used in value
|
|
361
|
+
# context. Recording is gated on `scope` && `call_node` being present, which naturally scopes it
|
|
362
|
+
# to the *direct*-RBS dispatch (the receiver's own resolvable class): the user-class / Object
|
|
363
|
+
# ancestor fallback nils both out (WD4 defers that, murkier, surface).
|
|
364
|
+
record_void_recovery(method_type, scope, call_node, [class_name, method_name, kind])
|
|
365
|
+
|
|
351
366
|
full_type_vars = compose_block_type_vars(method_type, type_vars, block_type)
|
|
352
367
|
|
|
353
368
|
RbsTypeTranslator.translate(
|
|
@@ -358,6 +373,24 @@ module Rigor
|
|
|
358
373
|
)
|
|
359
374
|
end
|
|
360
375
|
|
|
376
|
+
# Record the `void → top` recovery when the selected overload declares `-> void` and both `scope` and
|
|
377
|
+
# `call_node` are present (the direct-dispatch path). `void_site` is the `[class_name, method_name,
|
|
378
|
+
# kind]` triple the {VoidOrigin} carries.
|
|
379
|
+
def record_void_recovery(method_type, scope, call_node, void_site)
|
|
380
|
+
return unless scope && call_node && void_return?(method_type)
|
|
381
|
+
|
|
382
|
+
class_name, method_name, kind = void_site
|
|
383
|
+
scope.record_void_origin(
|
|
384
|
+
call_node,
|
|
385
|
+
VoidOrigin.new(class_name: class_name, method_name: method_name, kind: kind)
|
|
386
|
+
)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def void_return?(method_type)
|
|
390
|
+
fun = method_type.type
|
|
391
|
+
fun.respond_to?(:return_type) && fun.return_type.is_a?(RBS::Types::Bases::Void)
|
|
392
|
+
end
|
|
393
|
+
|
|
361
394
|
# ADR-7 § "Slice 4-A/4-B" — folds the `RBS::Extended` `return:` directive (and any other
|
|
362
395
|
# `return_type`-bearing contribution future slices add at this call site) through the merger
|
|
363
396
|
# before consuming. Returns the merged return type or nil when no contribution overrides the
|
|
@@ -41,8 +41,13 @@ module Rigor
|
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
# `RBS::Types::UntypedFunction` (a `(?)` method type, e.g. core's `Proc#call`) declares no
|
|
45
|
+
# parameters and exposes none of the per-arity accessors. It has no param classes to compare
|
|
46
|
+
# against the receiver, so it carries no affinity either way.
|
|
44
47
|
def overload_param_classes_in_ancestry?(method_type, self_class_name, environment)
|
|
45
48
|
fun = method_type.type
|
|
49
|
+
return false unless fun.respond_to?(:required_positionals)
|
|
50
|
+
|
|
46
51
|
params = fun.required_positionals + fun.optional_positionals + fun.trailing_positionals
|
|
47
52
|
return false if params.empty?
|
|
48
53
|
|
|
@@ -27,13 +27,14 @@ module Rigor
|
|
|
27
27
|
# the chain collapses to `Constant[nil]` so the carrier surfaces through downstream narrowing. A
|
|
28
28
|
# non-shape intermediate falls through to the projection answer.
|
|
29
29
|
# - HashShape#`size`/`length`: no-arg.
|
|
30
|
-
# - HashShape#`[]`, HashShape#`fetch`, HashShape#`dig` with a single `Constant
|
|
31
|
-
# argument
|
|
30
|
+
# - HashShape#`[]`, HashShape#`fetch`, HashShape#`dig` with a single `Constant` scalar-key
|
|
31
|
+
# argument (Symbol / String / Integer / Float / true / false / nil) matching one of the declared
|
|
32
|
+
# keys. `[]` and `dig` resolve missing keys to
|
|
32
33
|
# `Constant[nil]`; `fetch` (no default, no block) falls through on a miss because Ruby would raise
|
|
33
34
|
# `KeyError` and the analyzer prefers the conservative projection answer.
|
|
34
35
|
# - HashShape#`dig` with multi-arg chains (Slice 5 phase 2 sub-phase 2). Same chaining semantics as
|
|
35
36
|
# Tuple#`dig`.
|
|
36
|
-
# - HashShape#`values_at` with a list of `Constant
|
|
37
|
+
# - HashShape#`values_at` with a list of `Constant` scalar-key arguments (Slice 5 phase 2
|
|
37
38
|
# sub-phase 2). The result is a `Tuple` whose elements are the per-key values (`Constant[nil]` for
|
|
38
39
|
# missing keys, mirroring Ruby's runtime behaviour).
|
|
39
40
|
#
|
|
@@ -244,6 +245,14 @@ module Rigor
|
|
|
244
245
|
carrier
|
|
245
246
|
end
|
|
246
247
|
|
|
248
|
+
# True when `key` is a value the `HashShape` carrier can hold as a key (the value-pinned scalar
|
|
249
|
+
# set — Symbol, String, Integer, Float, true, false, nil). Key identity in every handler below
|
|
250
|
+
# is Ruby's own `eql?` via the shape's native `pairs` Hash, so `1` and `1.0` are distinct keys
|
|
251
|
+
# while `1.0` and `1.00` collide — exactly the runtime's `Hash` semantics.
|
|
252
|
+
def static_shape_key?(key)
|
|
253
|
+
Type::HashShape::ALLOWED_KEY_CLASSES.any? { |klass| key.is_a?(klass) }
|
|
254
|
+
end
|
|
255
|
+
|
|
247
256
|
def dispatch_nominal_size(nominal, method_name, args)
|
|
248
257
|
projection = nominal_projection(nominal, method_name, args)
|
|
249
258
|
return projection if projection
|
|
@@ -424,7 +433,7 @@ module Rigor
|
|
|
424
433
|
base = difference.base
|
|
425
434
|
return nil unless base.is_a?(Type::Nominal)
|
|
426
435
|
|
|
427
|
-
if removes_empty_witness?
|
|
436
|
+
if difference.removes_empty_witness?
|
|
428
437
|
precise = empty_removal_projection(difference, method_name, args)
|
|
429
438
|
return precise if precise
|
|
430
439
|
end
|
|
@@ -432,23 +441,6 @@ module Rigor
|
|
|
432
441
|
dispatch_nominal_size(base, method_name, args)
|
|
433
442
|
end
|
|
434
443
|
|
|
435
|
-
EMPTY_WITNESS_PREDICATES = {
|
|
436
|
-
"String" => ->(removed) { removed.is_a?(Type::Constant) && removed.value == "" },
|
|
437
|
-
"Integer" => lambda { |removed|
|
|
438
|
-
removed.is_a?(Type::Constant) && removed.value.is_a?(Integer) && removed.value.zero?
|
|
439
|
-
},
|
|
440
|
-
"Array" => ->(removed) { removed.is_a?(Type::Tuple) && removed.elements.empty? },
|
|
441
|
-
"Hash" => ->(removed) { removed.is_a?(Type::HashShape) && removed.pairs.empty? }
|
|
442
|
-
}.freeze
|
|
443
|
-
private_constant :EMPTY_WITNESS_PREDICATES
|
|
444
|
-
|
|
445
|
-
def removes_empty_witness?(difference)
|
|
446
|
-
return false unless difference.base.is_a?(Type::Nominal)
|
|
447
|
-
|
|
448
|
-
predicate = EMPTY_WITNESS_PREDICATES[difference.base.class_name]
|
|
449
|
-
!!(predicate && predicate.call(difference.removed))
|
|
450
|
-
end
|
|
451
|
-
|
|
452
444
|
# Methods on a non-empty String that preserve non-emptiness (they transform characters but never
|
|
453
445
|
# reduce the string to "").
|
|
454
446
|
NON_EMPTY_STRING_PRESERVING_UNARY = Set[:upcase, :downcase, :capitalize, :swapcase, :reverse].freeze
|
|
@@ -1308,7 +1300,7 @@ module Rigor
|
|
|
1308
1300
|
return nil unless arg.is_a?(Type::Constant)
|
|
1309
1301
|
|
|
1310
1302
|
key = arg.value
|
|
1311
|
-
return nil unless
|
|
1303
|
+
return nil unless static_shape_key?(key)
|
|
1312
1304
|
return nil unless shape.pairs.key?(key)
|
|
1313
1305
|
|
|
1314
1306
|
values << shape.pairs[key]
|
|
@@ -1327,7 +1319,7 @@ module Rigor
|
|
|
1327
1319
|
return nil unless arg.is_a?(Type::Constant)
|
|
1328
1320
|
|
|
1329
1321
|
key = arg.value
|
|
1330
|
-
return nil unless
|
|
1322
|
+
return nil unless static_shape_key?(key)
|
|
1331
1323
|
return Type::Combinator.constant_of(nil) unless shape.pairs.key?(key)
|
|
1332
1324
|
|
|
1333
1325
|
Type::Combinator.tuple_of(Type::Combinator.constant_of(key), shape.pairs[key])
|
|
@@ -1436,7 +1428,7 @@ module Rigor
|
|
|
1436
1428
|
end
|
|
1437
1429
|
|
|
1438
1430
|
# `shape.has_key?(k)` / `key?(k)` / `member?(k)` / `include?(k)` — folds to `Constant[true/false]`
|
|
1439
|
-
# when the argument is a `Constant
|
|
1431
|
+
# when the argument is a `Constant` scalar key and the shape is closed with no optional
|
|
1440
1432
|
# keys.
|
|
1441
1433
|
def hash_has_key?(shape, _method_name, args)
|
|
1442
1434
|
return nil unless args.size == 1
|
|
@@ -1447,7 +1439,7 @@ module Rigor
|
|
|
1447
1439
|
return nil unless arg.is_a?(Type::Constant)
|
|
1448
1440
|
|
|
1449
1441
|
key = arg.value
|
|
1450
|
-
return nil unless
|
|
1442
|
+
return nil unless static_shape_key?(key)
|
|
1451
1443
|
|
|
1452
1444
|
Type::Combinator.constant_of(shape.pairs.key?(key))
|
|
1453
1445
|
end
|
|
@@ -1496,7 +1488,8 @@ module Rigor
|
|
|
1496
1488
|
end
|
|
1497
1489
|
|
|
1498
1490
|
# `shape.invert` — swaps keys and values. Folds when every value is a `Constant` whose value is a
|
|
1499
|
-
# Symbol
|
|
1491
|
+
# scalar the carrier accepts as a key (Symbol / String / Integer / Float / true / false / nil;
|
|
1492
|
+
# see {#static_shape_key?}). Duplicate values
|
|
1500
1493
|
# would alias under inversion, so Rigor declines on collisions rather than silently dropping
|
|
1501
1494
|
# entries.
|
|
1502
1495
|
def hash_invert(shape, _method_name, args)
|
|
@@ -1504,7 +1497,7 @@ module Rigor
|
|
|
1504
1497
|
return nil unless shape.closed?
|
|
1505
1498
|
return nil unless shape.optional_keys.empty?
|
|
1506
1499
|
return nil unless shape.pairs.values.all?(Type::Constant)
|
|
1507
|
-
return nil unless shape.pairs.values.all? { |v|
|
|
1500
|
+
return nil unless shape.pairs.values.all? { |v| static_shape_key?(v.value) }
|
|
1508
1501
|
|
|
1509
1502
|
inverted = shape.pairs.each_with_object({}) do |(k, v), acc|
|
|
1510
1503
|
return nil if acc.key?(v.value)
|
|
@@ -1565,7 +1558,7 @@ module Rigor
|
|
|
1565
1558
|
Type::Combinator.hash_shape_of(shape.pairs.merge(other.pairs))
|
|
1566
1559
|
end
|
|
1567
1560
|
|
|
1568
|
-
# `shape[k]` and `shape.fetch(k)` for a
|
|
1561
|
+
# `shape[k]` and `shape.fetch(k)` for a value-pinned scalar key. Missing-key resolution depends
|
|
1569
1562
|
# on the method:
|
|
1570
1563
|
#
|
|
1571
1564
|
# - `[]` returns `nil` at runtime; we surface `Constant[nil]` so the carrier is visible to
|
|
@@ -1599,13 +1592,13 @@ module Rigor
|
|
|
1599
1592
|
end
|
|
1600
1593
|
|
|
1601
1594
|
# Returns the per-step value type for a HashShape lookup (or `Constant[nil]` for a known-missing
|
|
1602
|
-
# key). Returns `nil` when the argument is not a
|
|
1595
|
+
# key). Returns `nil` when the argument is not a value-pinned scalar key so the caller can fall
|
|
1603
1596
|
# through to the projection answer.
|
|
1604
1597
|
def hash_dig_step(shape, arg)
|
|
1605
1598
|
return nil unless arg.is_a?(Type::Constant)
|
|
1606
1599
|
|
|
1607
1600
|
key = arg.value
|
|
1608
|
-
return nil unless
|
|
1601
|
+
return nil unless static_shape_key?(key)
|
|
1609
1602
|
|
|
1610
1603
|
if shape.pairs.key?(key)
|
|
1611
1604
|
value = shape.pairs[key]
|
|
@@ -1631,7 +1624,7 @@ module Rigor
|
|
|
1631
1624
|
|
|
1632
1625
|
# `shape.values_at(:a, :b, ...)` with a list of static keys. Returns a `Tuple` whose per-position
|
|
1633
1626
|
# values are the per-key value types (`Constant[nil]` for missing keys, mirroring Ruby's runtime
|
|
1634
|
-
# behaviour). Falls through when any argument is not a
|
|
1627
|
+
# behaviour). Falls through when any argument is not a value-pinned scalar key.
|
|
1635
1628
|
def hash_values_at(shape, _method_name, args)
|
|
1636
1629
|
return nil if args.empty?
|
|
1637
1630
|
|
|
@@ -1647,7 +1640,7 @@ module Rigor
|
|
|
1647
1640
|
end
|
|
1648
1641
|
|
|
1649
1642
|
# `shape.slice(:a, :b, ...)` — returns a sub-HashShape containing only the specified keys. All
|
|
1650
|
-
# arguments must be `Constant
|
|
1643
|
+
# arguments must be `Constant` scalar keys. Keys not present in the shape are silently
|
|
1651
1644
|
# omitted (matching Ruby's runtime semantics — no nil padding). Declines on open shapes or when
|
|
1652
1645
|
# any argument is not a static key.
|
|
1653
1646
|
def hash_slice(shape, _method_name, args)
|
|
@@ -1660,7 +1653,7 @@ module Rigor
|
|
|
1660
1653
|
return nil unless arg.is_a?(Type::Constant)
|
|
1661
1654
|
|
|
1662
1655
|
key = arg.value
|
|
1663
|
-
return nil unless
|
|
1656
|
+
return nil unless static_shape_key?(key)
|
|
1664
1657
|
|
|
1665
1658
|
requested << key
|
|
1666
1659
|
end
|
|
@@ -1669,7 +1662,7 @@ module Rigor
|
|
|
1669
1662
|
end
|
|
1670
1663
|
|
|
1671
1664
|
# `shape.except(:a, :b, ...)` — returns a sub-HashShape with the specified keys removed. All
|
|
1672
|
-
# arguments must be `Constant
|
|
1665
|
+
# arguments must be `Constant` scalar keys. Keys not present in the shape are silently
|
|
1673
1666
|
# ignored. Declines on open shapes or when any argument is not a static key.
|
|
1674
1667
|
def hash_except(shape, _method_name, args)
|
|
1675
1668
|
return nil if args.empty?
|
|
@@ -1681,7 +1674,7 @@ module Rigor
|
|
|
1681
1674
|
return nil unless arg.is_a?(Type::Constant)
|
|
1682
1675
|
|
|
1683
1676
|
key = arg.value
|
|
1684
|
-
return nil unless
|
|
1677
|
+
return nil unless static_shape_key?(key)
|
|
1685
1678
|
|
|
1686
1679
|
excluded[key] = true
|
|
1687
1680
|
end
|
|
@@ -12,13 +12,12 @@ module Rigor
|
|
|
12
12
|
# `Data` path does not need: **mutation soundness.**
|
|
13
13
|
#
|
|
14
14
|
# A `Struct` instance is mutable (`s.x = v`, `s[:x] = v`, escape), so a member map bound to a
|
|
15
|
-
# variable can be invalidated by a later write.
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
# re-typing is slice 4.
|
|
15
|
+
# variable can be invalidated by a later write. A member read folds off a **fresh** instance (the
|
|
16
|
+
# transient receiver of a `.new(...).x` / `.with(...).x` chain, unmutatable between materialisation
|
|
17
|
+
# and the read — slices 1+2), off a **fold-safe stored local** the {Inference::StructFoldSafety} scan
|
|
18
|
+
# proved is never mutated / aliased / escaped (slice 3), or off a **setter-mutated fold-safe local**
|
|
19
|
+
# whose bindings {#apply_setter_writeback} keeps current (slice 4). Any other stored receiver degrades
|
|
20
|
+
# to `Dynamic[top]` rather than fold a possibly-stale value.
|
|
22
21
|
#
|
|
23
22
|
# Responsibilities:
|
|
24
23
|
#
|
|
@@ -272,7 +271,8 @@ module Rigor
|
|
|
272
271
|
end
|
|
273
272
|
|
|
274
273
|
# A fold-safe stored receiver is a local-variable read whose name the body's fold-safe set (on the
|
|
275
|
-
# scope) marks as never
|
|
274
|
+
# scope) marks as safe to fold — never aliased / escaped, and any mutation a straight-line member
|
|
275
|
+
# setter the write-back keeps the binding current for.
|
|
276
276
|
def fold_safe_local_receiver?(context)
|
|
277
277
|
node = context.call_node
|
|
278
278
|
receiver = node&.receiver
|
|
@@ -281,6 +281,38 @@ module Rigor
|
|
|
281
281
|
|
|
282
282
|
scope.struct_fold_safe?(receiver.name)
|
|
283
283
|
end
|
|
284
|
+
|
|
285
|
+
# ADR-48 slice 4 — precise mutated-member re-typing. After a `local.member = v` setter on a
|
|
286
|
+
# fold-safe `StructInstance` local, rebind the local to a `StructInstance` with that member replaced
|
|
287
|
+
# by the assigned type, so a later `local.member` read folds to the assigned value (and a sibling
|
|
288
|
+
# `local.other` stays precise). Called from `eval_call`'s post-call scope, mirroring
|
|
289
|
+
# `MutationWidening.widen_after_call`. Sound ONLY for a fold-safe local: the fold-safe scan proves it
|
|
290
|
+
# is never aliased / escaped and its setters are straight-line, so the binding this installs is the
|
|
291
|
+
# local's true member state at every later read on the path. A non-fold-safe local is left untouched
|
|
292
|
+
# (its reads do not fold, so the binding is never consulted for folding).
|
|
293
|
+
#
|
|
294
|
+
# @param call_node [Prism::CallNode] the `local.member = v` call
|
|
295
|
+
# @param assigned_type [Rigor::Type, nil] the setter's assigned value type (the call's own result)
|
|
296
|
+
# @param scope [Rigor::Scope, nil]
|
|
297
|
+
# @return [Rigor::Scope] the (possibly) rebound scope
|
|
298
|
+
def apply_setter_writeback(call_node:, assigned_type:, scope:)
|
|
299
|
+
return scope if scope.nil? || assigned_type.nil?
|
|
300
|
+
|
|
301
|
+
receiver = call_node.receiver
|
|
302
|
+
return scope unless receiver.is_a?(Prism::LocalVariableReadNode)
|
|
303
|
+
return scope unless scope.struct_fold_safe?(receiver.name)
|
|
304
|
+
|
|
305
|
+
current = scope.local(receiver.name)
|
|
306
|
+
return scope unless current.is_a?(Type::StructInstance)
|
|
307
|
+
|
|
308
|
+
member = member_setter_target(call_node.name, current.members)
|
|
309
|
+
return scope if member.nil?
|
|
310
|
+
|
|
311
|
+
rebound = Type::Combinator.struct_instance_of(
|
|
312
|
+
members: current.members.merge(member => assigned_type), class_name: current.class_name
|
|
313
|
+
)
|
|
314
|
+
scope.with_local(receiver.name, rebound)
|
|
315
|
+
end
|
|
284
316
|
end
|
|
285
317
|
end
|
|
286
318
|
end
|
|
@@ -8,6 +8,7 @@ require_relative "../builtins/hkt_builtins"
|
|
|
8
8
|
require_relative "../builtins/static_return_refinements"
|
|
9
9
|
require_relative "dynamic_origin"
|
|
10
10
|
require_relative "flow_tracer"
|
|
11
|
+
require_relative "void_tail_summary"
|
|
11
12
|
require_relative "method_dispatcher/call_context"
|
|
12
13
|
require_relative "method_dispatcher/constant_folding"
|
|
13
14
|
require_relative "method_dispatcher/literal_string_folding"
|
|
@@ -70,6 +71,14 @@ module Rigor
|
|
|
70
71
|
block_type: block_type, environment: environment,
|
|
71
72
|
call_node: call_node, scope: scope
|
|
72
73
|
)
|
|
74
|
+
# ADR-100 WD4 — the transitive-void consult, HERE in the wrapper rather than in any result
|
|
75
|
+
# tier: which tier answers the intermediate's call depends on where the leaf's RBS came from
|
|
76
|
+
# (`RbsDispatch` on the ADR-93 synthesized `untyped` skeleton, or the post-dispatch
|
|
77
|
+
# `ExpressionTyper` body-inference tiers on a partial hand-written `sig/`), and both answers
|
|
78
|
+
# are correct types that must not change. Recording is therefore result-independent and
|
|
79
|
+
# type-blind; a statement-position record stays inert because the collector only reads value
|
|
80
|
+
# positions.
|
|
81
|
+
record_transitive_void_origin(receiver_type, method_name, call_node, scope)
|
|
73
82
|
# `rigor trace` — record the dispatch outcome (resolved type, or the fail-soft `nil` the
|
|
74
83
|
# caller widens to `Dynamic[Top]`).
|
|
75
84
|
if FlowTracer.active?
|
|
@@ -225,6 +234,36 @@ module Rigor
|
|
|
225
234
|
fallback_result
|
|
226
235
|
end
|
|
227
236
|
|
|
237
|
+
# ADR-100 WD4 — records the transitive `-> void` provenance when the called method is a
|
|
238
|
+
# discovered project `def` whose {VoidTailSummary} chain ends in an author-declared `-> void`
|
|
239
|
+
# leaf on the same owner. Guards cheapest-first: the internal dispatcher callers nil `scope` /
|
|
240
|
+
# `call_node` out; the receiver must project through {#discovered_method_lookup}
|
|
241
|
+
# (`Nominal` / `Singleton` only — unions and `Dynamic` receivers stay out); and the
|
|
242
|
+
# `discovered_method?` pre-filter (two pure hash reads) keeps the hot path at ~zero cost —
|
|
243
|
+
# only a call that provably targets a discovered def of the right kind resolves the node and
|
|
244
|
+
# consults the summary. The `Prism::CallNode` guard mirrors `collect_plugin_contributions`:
|
|
245
|
+
# the `&:symbol` block path dispatches with the `Prism::BlockArgumentNode` itself, which is
|
|
246
|
+
# never a collector value position, so recording on it would be dead weight.
|
|
247
|
+
def record_transitive_void_origin(receiver_type, method_name, call_node, scope)
|
|
248
|
+
return if scope.nil? || call_node.nil?
|
|
249
|
+
return unless call_node.is_a?(Prism::CallNode)
|
|
250
|
+
|
|
251
|
+
class_name, kind = discovered_method_lookup(receiver_type)
|
|
252
|
+
return if class_name.nil?
|
|
253
|
+
return unless scope.discovered_method?(class_name, method_name, kind)
|
|
254
|
+
|
|
255
|
+
def_node =
|
|
256
|
+
if kind == :singleton
|
|
257
|
+
scope.singleton_def_for(class_name, method_name)
|
|
258
|
+
else
|
|
259
|
+
scope.user_def_for(class_name, method_name)
|
|
260
|
+
end
|
|
261
|
+
return if def_node.nil?
|
|
262
|
+
|
|
263
|
+
origin = VoidTailSummary.new(scope).origin_for(def_node, class_name, kind)
|
|
264
|
+
scope.record_void_origin(call_node, origin) if origin
|
|
265
|
+
end
|
|
266
|
+
|
|
228
267
|
# v0.1.3 — discovered-method dispatch tier. `scope` carries the `discovered_methods` table
|
|
229
268
|
# built once per program by `ScopeIndexer` (a `Hash[String, Hash[Symbol, :instance |
|
|
230
269
|
# :singleton]]`). When the receiver names a discovered class AND the requested method is
|
|
@@ -701,7 +740,7 @@ module Rigor
|
|
|
701
740
|
private_constant :STDLIB_SINGLETON_FOLDERS
|
|
702
741
|
|
|
703
742
|
PRECISE_TIERS_TAIL = Ractor.make_shareable([
|
|
704
|
-
|
|
743
|
+
MethodFolding, ReduceFolding, ArrayToHFolding, BlockFolding
|
|
705
744
|
].freeze)
|
|
706
745
|
private_constant :PRECISE_TIERS_TAIL
|
|
707
746
|
|
|
@@ -735,6 +774,9 @@ module Rigor
|
|
|
735
774
|
return result if result
|
|
736
775
|
end
|
|
737
776
|
|
|
777
|
+
kernel_result = dispatch_kernel_intrinsic(context)
|
|
778
|
+
return kernel_result if kernel_result
|
|
779
|
+
|
|
738
780
|
PRECISE_TIERS_TAIL.each do |tier|
|
|
739
781
|
result = tier.try_dispatch(context)
|
|
740
782
|
return result if result
|
|
@@ -742,6 +784,20 @@ module Rigor
|
|
|
742
784
|
nil
|
|
743
785
|
end
|
|
744
786
|
|
|
787
|
+
# ADR-91 WD1 — the single dispatcher-held ownership gate for the Kernel module-function surface
|
|
788
|
+
# (gate-by-held-key, the same move STDLIB_SINGLETON_FOLDERS made above). The tier runs only when
|
|
789
|
+
# the method name is in the compiled `INTRINSIC_NAMES` table AND the call is attributable to
|
|
790
|
+
# Kernel itself (implicit self / `Kernel.` receiver, no discovered user redefinition). Because the
|
|
791
|
+
# gate lives HERE and nowhere else, a fold inside KernelDispatch that "forgets" its ownership
|
|
792
|
+
# guard is unrepresentable — the whole tier is unreachable for a foreign receiver. Unit probes
|
|
793
|
+
# calling `KernelDispatch.try_dispatch` directly keep the caller-vouches-for-ownership contract.
|
|
794
|
+
def dispatch_kernel_intrinsic(context)
|
|
795
|
+
return nil unless KernelDispatch::INTRINSIC_NAMES.include?(context.method_name)
|
|
796
|
+
return nil unless KernelDispatch.kernel_owned_call?(context)
|
|
797
|
+
|
|
798
|
+
KernelDispatch.try_dispatch(context)
|
|
799
|
+
end
|
|
800
|
+
|
|
745
801
|
def try_user_class_fallback(receiver_type, environment, call_node, context)
|
|
746
802
|
return nil if environment.nil?
|
|
747
803
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../type"
|
|
4
|
+
require_relative "../source/node_children"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
module Inference
|
|
@@ -101,7 +102,8 @@ module Rigor
|
|
|
101
102
|
|
|
102
103
|
# Returns a scope with the call's receiver widened, when the receiver is a
|
|
103
104
|
# local-/instance-variable read whose current binding is a literal-shape carrier
|
|
104
|
-
# (`Tuple` / `HashShape`)
|
|
105
|
+
# (`Tuple` / `HashShape`) or an empty-witness refinement (`non-empty-array` /
|
|
106
|
+
# `non-empty-hash`) AND the call name is a known in-place mutator for that shape.
|
|
105
107
|
# Returns `current_scope` unchanged otherwise.
|
|
106
108
|
#
|
|
107
109
|
# @param call_node [Prism::CallNode]
|
|
@@ -160,7 +162,7 @@ module Rigor
|
|
|
160
162
|
# scope's locals) trigger widening, so descending into nested blocks is safe and
|
|
161
163
|
# necessary for the hkt_registry-shape case (an outer collection mutated inside an
|
|
162
164
|
# iterator block whose body is itself inside another block).
|
|
163
|
-
node.
|
|
165
|
+
node.rigor_each_child do |child|
|
|
164
166
|
scope = walk_for_outer_mutations(child, scope)
|
|
165
167
|
end
|
|
166
168
|
scope
|
|
@@ -216,6 +218,30 @@ module Rigor
|
|
|
216
218
|
return nil unless HASH_MUTATORS.include?(method_name)
|
|
217
219
|
|
|
218
220
|
widen_hash_shape(type)
|
|
221
|
+
when Type::Difference
|
|
222
|
+
widen_difference(type, method_name)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# `non-empty-array[T]` / `non-empty-hash[K, V]` → the bare base nominal. These refinement
|
|
227
|
+
# carriers are what `empty?` / `any?` narrowing writes (ADR-47 §4-4), and they are just as
|
|
228
|
+
# invalidated by an in-place mutator as a `Tuple` is: `arr.clear` makes `arr` empty, so a
|
|
229
|
+
# surviving `Difference[Array[T], Tuple[]]` would project `arr.size` to `positive-int` and
|
|
230
|
+
# fold `arr.size == 0` to `Constant[false]` — a false `flow.always-falsey-condition` on
|
|
231
|
+
# correct code.
|
|
232
|
+
#
|
|
233
|
+
# Only the EMPTY-witness differences over Array / Hash are widened, and only for that
|
|
234
|
+
# base's mutator table. The other catalogued refinements are unreachable from these tables
|
|
235
|
+
# by construction: `non-empty-string` and `non-zero-int` bind String / Integer receivers,
|
|
236
|
+
# whose mutators (`String#<<` and friends) appear in neither `ARRAY_MUTATORS` nor
|
|
237
|
+
# `HASH_MUTATORS` — and none of them can empty a non-empty string anyway.
|
|
238
|
+
def widen_difference(difference, method_name)
|
|
239
|
+
return nil unless difference.removes_empty_witness?
|
|
240
|
+
|
|
241
|
+
base = difference.base
|
|
242
|
+
case base.class_name
|
|
243
|
+
when "Array" then ARRAY_MUTATORS.include?(method_name) ? base : nil
|
|
244
|
+
when "Hash" then HASH_MUTATORS.include?(method_name) ? base : nil
|
|
219
245
|
end
|
|
220
246
|
end
|
|
221
247
|
|
|
@@ -248,16 +274,24 @@ module Rigor
|
|
|
248
274
|
Type::Combinator.nominal_of("Hash", type_args: [key_type, value_type])
|
|
249
275
|
end
|
|
250
276
|
|
|
251
|
-
# Maps the literal Ruby key set
|
|
252
|
-
#
|
|
253
|
-
#
|
|
254
|
-
#
|
|
277
|
+
# Maps the literal Ruby key set to a union of the corresponding type carriers. Symbol / String /
|
|
278
|
+
# Integer / Float keys widen to their class nominal; the `true` / `false` / `nil` singleton keys
|
|
279
|
+
# keep their constant carrier (the constant IS the class's whole value set, and `nil` reads better
|
|
280
|
+
# than `NilClass` in a widened `Hash[K, V]`). We deliberately do NOT fold the widenable kinds to a
|
|
281
|
+
# `Constant<:k1> | Constant<:k2>` union — that would be a precision improvement that complicates
|
|
282
|
+
# the widening contract; the goal here is to LOSE precision, not to record a new fact set.
|
|
255
283
|
def key_union_for(keys)
|
|
256
|
-
|
|
257
|
-
carriers = kinds.map { |name| Type::Combinator.nominal_of(name) }
|
|
284
|
+
carriers = keys.map { |k| key_widening_carrier(k) }.uniq
|
|
258
285
|
carriers.size == 1 ? carriers.first : Type::Combinator.union(*carriers)
|
|
259
286
|
end
|
|
260
287
|
|
|
288
|
+
def key_widening_carrier(key)
|
|
289
|
+
case key
|
|
290
|
+
when true, false, nil then Type::Combinator.constant_of(key)
|
|
291
|
+
else Type::Combinator.nominal_of(key.class.name)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
261
295
|
# ----------------------------------------------------------------
|
|
262
296
|
# ADR-56 slice C — receiver-content element-type JOIN.
|
|
263
297
|
#
|