rigortype 0.4.7 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +3 -1
- data/data/core_overlay/bigdecimal.rbs +33 -0
- data/data/core_overlay/resolv.rbs +30 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
- data/data/gem_overlay/addressable/addressable.rbs +40 -0
- data/docs/handbook/01-getting-started.md +1 -1
- data/docs/handbook/11-sig-gen.md +35 -0
- data/docs/install.md +20 -0
- data/docs/manual/01-installation.md +67 -26
- data/docs/manual/02-cli-reference.md +307 -91
- data/docs/manual/03-configuration.md +41 -3
- data/docs/manual/04-diagnostics.md +5 -4
- data/docs/manual/08-skills.md +11 -0
- data/docs/manual/09-editor-integration.md +8 -0
- data/docs/manual/14-rails-quickstart.md +1 -1
- data/docs/manual/19-effect-labels.md +3 -4
- data/docs/manual/plugins/rigor-actionpack.md +15 -6
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
- data/docs/manual/plugins/rigor-sorbet.md +17 -3
- data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
- data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
- data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
- data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules.rb +598 -99
- data/lib/rigor/analysis/crash_signature.rb +43 -1
- data/lib/rigor/analysis/dependency_recorder.rb +233 -76
- data/lib/rigor/analysis/incremental.rb +11 -1
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +279 -37
- data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
- data/lib/rigor/analysis/rule_catalog.rb +37 -1
- data/lib/rigor/analysis/run_cache_key.rb +27 -3
- data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
- data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
- data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
- data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
- data/lib/rigor/analysis/runner.rb +222 -39
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
- data/lib/rigor/analysis/worker_session.rb +42 -41
- data/lib/rigor/bleeding_edge.rb +19 -4
- data/lib/rigor/cache/file_digest.rb +39 -31
- data/lib/rigor/cache/incremental_snapshot.rb +66 -7
- data/lib/rigor/cache/rbs_descriptor.rb +37 -2
- data/lib/rigor/cache/rbs_environment.rb +5 -1
- data/lib/rigor/cli/baseline_command.rb +15 -2
- data/lib/rigor/cli/check_command.rb +4 -2
- data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
- data/lib/rigor/cli/lens_command.rb +17 -2
- data/lib/rigor/cli/sig_gen_command.rb +8 -0
- data/lib/rigor/cli/skill_describe.rb +1 -0
- data/lib/rigor/cli.rb +5 -2
- data/lib/rigor/config_audit.rb +32 -18
- data/lib/rigor/configuration/severity_profile.rb +9 -0
- data/lib/rigor/configuration.rb +32 -5
- data/lib/rigor/effects/collector.rb +53 -21
- data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
- data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
- data/lib/rigor/environment/default_libraries.rb +17 -0
- data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
- data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
- data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
- data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
- data/lib/rigor/environment/rbs_loader.rb +312 -63
- data/lib/rigor/environment.rb +131 -35
- data/lib/rigor/inference/acceptance.rb +50 -15
- data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
- data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
- data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
- data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
- data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
- data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
- data/lib/rigor/inference/block_auto_splat.rb +1 -1
- data/lib/rigor/inference/block_run_count.rb +9 -2
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
- data/lib/rigor/inference/captured_locals.rb +2 -0
- data/lib/rigor/inference/census_foreign_block.rb +354 -0
- data/lib/rigor/inference/clause_widening.rb +1 -1
- data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
- data/lib/rigor/inference/construction_escape.rb +56 -15
- data/lib/rigor/inference/declaration_walk.rb +4 -0
- data/lib/rigor/inference/def_node_resolver.rb +32 -14
- data/lib/rigor/inference/def_return_typer.rb +1 -1
- data/lib/rigor/inference/definer_resolution.rb +15 -7
- data/lib/rigor/inference/definers_index.rb +72 -16
- data/lib/rigor/inference/error_info.rb +16 -0
- data/lib/rigor/inference/expression_typer.rb +1459 -287
- data/lib/rigor/inference/flow_tracer.rb +31 -14
- data/lib/rigor/inference/fork_map.rb +3 -0
- data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
- data/lib/rigor/inference/global_write_census.rb +8 -6
- data/lib/rigor/inference/in_effect_refinements.rb +7 -0
- data/lib/rigor/inference/indexed_narrowing.rb +57 -8
- data/lib/rigor/inference/ivar_writer_census.rb +106 -11
- data/lib/rigor/inference/key_presence_guard.rb +51 -25
- data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
- data/lib/rigor/inference/last_line.rb +5 -2
- data/lib/rigor/inference/loop_value_record.rb +63 -15
- data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
- data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
- data/lib/rigor/inference/match_rebinding.rb +21 -8
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
- data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
- data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
- data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
- data/lib/rigor/inference/method_dispatcher.rb +66 -18
- data/lib/rigor/inference/method_parameter_binder.rb +136 -56
- data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
- data/lib/rigor/inference/module_function_state.rb +13 -2
- data/lib/rigor/inference/multi_target_binder.rb +1 -1
- data/lib/rigor/inference/mutation_widening.rb +44 -4
- data/lib/rigor/inference/narrowing.rb +201 -27
- data/lib/rigor/inference/operand_effects.rb +43 -2
- data/lib/rigor/inference/optimistic_origin.rb +133 -8
- data/lib/rigor/inference/origin_lookup.rb +1 -1
- data/lib/rigor/inference/precision_scanner.rb +1 -1
- data/lib/rigor/inference/project_method_ownership.rb +28 -10
- data/lib/rigor/inference/project_patched_methods.rb +65 -13
- data/lib/rigor/inference/project_patched_scanner.rb +120 -25
- data/lib/rigor/inference/project_patched_visibility.rb +39 -3
- data/lib/rigor/inference/rbs_type_translator.rb +68 -15
- data/lib/rigor/inference/receiver_writer_call.rb +67 -0
- data/lib/rigor/inference/recursion_rebinding.rb +157 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
- data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
- data/lib/rigor/inference/refined_dispatch.rb +13 -0
- data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
- data/lib/rigor/inference/remover_holder_scan.rb +76 -22
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
- data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
- data/lib/rigor/inference/scope_indexer.rb +2079 -183
- data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
- data/lib/rigor/inference/self_preserving_block.rb +87 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
- data/lib/rigor/inference/singleton_object_constant.rb +32 -2
- data/lib/rigor/inference/statement_evaluator.rb +970 -250
- data/lib/rigor/inference/string_mutation.rb +14 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
- data/lib/rigor/inference/toplevel_def_census.rb +14 -6
- data/lib/rigor/inference/unknown_store_widening.rb +1 -1
- data/lib/rigor/inference/untyped_rejoin.rb +1 -1
- data/lib/rigor/inference/void_tail_summary.rb +29 -10
- data/lib/rigor/lens/claims/comparison.rb +78 -0
- data/lib/rigor/lens/claims/request.rb +131 -0
- data/lib/rigor/lens/claims/type_reader.rb +216 -0
- data/lib/rigor/lens/claims.rb +208 -0
- data/lib/rigor/lens/contract_reader.rb +5 -0
- data/lib/rigor/lens/document.rb +1 -1
- data/lib/rigor/lens/file_reader.rb +21 -8
- data/lib/rigor/lens/run.rb +8 -0
- data/lib/rigor/lens.rb +1 -1
- data/lib/rigor/library_probe.rb +68 -0
- data/lib/rigor/memo_audit.rb +282 -0
- data/lib/rigor/plugin/base.rb +15 -2
- data/lib/rigor/plugin/io_boundary.rb +25 -3
- data/lib/rigor/plugin/manifest.rb +7 -3
- data/lib/rigor/plugin/registry.rb +13 -2
- data/lib/rigor/plugin/trust_policy.rb +46 -0
- data/lib/rigor/protection/mutation_cache.rb +4 -1
- data/lib/rigor/reflection/constant_ancestors.rb +79 -22
- data/lib/rigor/reflection/declaration_collectors.rb +55 -0
- data/lib/rigor/reflection/declarations.rb +58 -0
- data/lib/rigor/reflection/project_method.rb +177 -0
- data/lib/rigor/reflection.rb +2 -0
- data/lib/rigor/rooted_glob.rb +18 -1
- data/lib/rigor/run_scoped_slots.rb +20 -20
- data/lib/rigor/scope/discovery_index.rb +149 -2
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
- data/lib/rigor/scope/resolution_chain.rb +900 -55
- data/lib/rigor/scope.rb +307 -34
- data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
- data/lib/rigor/sig_gen/classification.rb +14 -1
- data/lib/rigor/sig_gen/generator.rb +222 -13
- data/lib/rigor/sig_gen/method_candidate.rb +14 -0
- data/lib/rigor/sig_gen/observation_collector.rb +11 -2
- data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
- data/lib/rigor/sig_gen/writer.rb +21 -0
- data/lib/rigor/sig_gen.rb +1 -0
- data/lib/rigor/source/forwardable_delegators.rb +122 -0
- data/lib/rigor/source/node_children.rb +13 -0
- data/lib/rigor/type/combinator.rb +164 -8
- data/lib/rigor/type/interface.rb +60 -0
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_of_memo/serving.rb +99 -0
- data/lib/rigor/type_of_memo/stats.rb +43 -0
- data/lib/rigor/type_of_memo.rb +337 -0
- data/lib/rigor/typing_state.rb +1094 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
- data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
- data/sig/rigor/analysis/fact_store.rbs +2 -3
- data/sig/rigor/cache/descriptor.rbs +14 -0
- data/sig/rigor/environment.rbs +4 -1
- data/sig/rigor/plugin/base.rbs +53 -19
- data/sig/rigor/plugin/interfaces.rbs +51 -0
- data/sig/rigor/plugin/io_boundary.rbs +2 -3
- data/sig/rigor/plugin/node_context.rbs +21 -0
- data/sig/rigor/reflection.rbs +37 -0
- data/sig/rigor/scope.rbs +22 -3
- data/sig/rigor/trinary.rbs +1 -0
- data/sig/rigor/type.rbs +9 -3
- data/sig/rigor.rbs +3 -2
- data/skills/rigor-ask/SKILL.md +1 -0
- data/skills/rigor-project-init/references/01-detect.md +1 -0
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
- data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
- data/skills/rigor-rbs-contract/SKILL.md +147 -0
- data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
- data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
- data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
- data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
- data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
- data/skills/rigor-rbs-setup/SKILL.md +3 -1
- data/skills/rigor-type-oracle/SKILL.md +26 -1
- data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
- metadata +48 -2
- data/lib/rigor/inference/memo_audit.rb +0 -55
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../typing_state"
|
|
4
|
+
|
|
3
5
|
module Rigor
|
|
4
6
|
module Inference
|
|
5
7
|
# Thread-local event recorder behind `rigor trace`: while a block runs under {record}, the inference engine
|
|
@@ -13,8 +15,14 @@ module Rigor
|
|
|
13
15
|
# hot paths (`ExpressionTyper#type_of`, `Scope#with_local`, `Type::Combinator.union`, `MethodDispatcher.dispatch`)
|
|
14
16
|
# each guard their emit behind {active?}, so a normal (non-tracing) run pays one integer comparison.
|
|
15
17
|
module FlowTracer
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
# ADR-129 WD1 — the installed recorder is a Frame whose contents are a Resource: each emit into it touches. It is
|
|
19
|
+
# an output the engine never reads back, and the activation count below is a process switch, outside the typing
|
|
20
|
+
# state (WD5). G1′: a tracer switch, under which the `type_of` memo stays shut (WD4).
|
|
21
|
+
RECORDER = TypingState.frame(
|
|
22
|
+
:flow_tracer, contents: :resource, reason: "the recorder a trace installs; each emit records into it",
|
|
23
|
+
memo_shut_while_installed: true
|
|
24
|
+
)
|
|
25
|
+
private_constant :RECORDER
|
|
18
26
|
|
|
19
27
|
# One animation-relevant moment.
|
|
20
28
|
#
|
|
@@ -95,15 +103,16 @@ module Rigor
|
|
|
95
103
|
# Activates recording on the current thread for the duration of the block and returns the frozen event list.
|
|
96
104
|
# Nests safely; restores the previous recorder on exit.
|
|
97
105
|
def record
|
|
98
|
-
previous = Thread.current[KEY]
|
|
99
106
|
recorder = Recorder.new
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
+
TypingState.current.with(RECORDER, recorder) do
|
|
108
|
+
@mutex.synchronize { @active_count += 1 }
|
|
109
|
+
begin
|
|
110
|
+
yield
|
|
111
|
+
recorder.events.freeze
|
|
112
|
+
ensure
|
|
113
|
+
@mutex.synchronize { @active_count -= 1 }
|
|
114
|
+
end
|
|
115
|
+
end
|
|
107
116
|
end
|
|
108
117
|
|
|
109
118
|
# Plain integer read (GVL-atomic) — the disabled fast path.
|
|
@@ -114,20 +123,28 @@ module Rigor
|
|
|
114
123
|
# Brackets one expression-typing recursion. Falls through to the bare block when the current thread is not
|
|
115
124
|
# recording (another thread may have flipped {active?}).
|
|
116
125
|
def trace_node(node, &)
|
|
117
|
-
recorder =
|
|
126
|
+
recorder = recording
|
|
118
127
|
return yield unless recorder
|
|
119
128
|
|
|
120
129
|
recorder.node(node, &)
|
|
121
130
|
end
|
|
122
131
|
|
|
132
|
+
# The recorder installed on this thread, touched because the caller is about to record into it, or nil.
|
|
133
|
+
def recording
|
|
134
|
+
state = TypingState.current
|
|
135
|
+
recorder = state[RECORDER]
|
|
136
|
+
state.touch(RECORDER) if recorder
|
|
137
|
+
recorder
|
|
138
|
+
end
|
|
139
|
+
|
|
123
140
|
# `Scope#with_local` — the moment a local enters the scope.
|
|
124
141
|
def bind(name, type)
|
|
125
|
-
|
|
142
|
+
recording&.emit(:bind, data: { name: name.to_s, type: describe(type) })
|
|
126
143
|
end
|
|
127
144
|
|
|
128
145
|
# `Type::Combinator.union` — the moment branch types merge (including degenerate collapses like `1 | 1 → 1`).
|
|
129
146
|
def union(members, result)
|
|
130
|
-
|
|
147
|
+
recording&.emit(
|
|
131
148
|
:union,
|
|
132
149
|
data: { members: members.map { |m| describe(m) }.freeze, type: describe(result) }
|
|
133
150
|
)
|
|
@@ -136,7 +153,7 @@ module Rigor
|
|
|
136
153
|
# `MethodDispatcher.dispatch` — resolution or the fail-soft `nil` ("no rule matched"; the caller will widen
|
|
137
154
|
# to `Dynamic[Top]`).
|
|
138
155
|
def dispatch(receiver:, method_name:, args:, result:, location: nil)
|
|
139
|
-
recorder =
|
|
156
|
+
recorder = recording
|
|
140
157
|
return unless recorder
|
|
141
158
|
|
|
142
159
|
recorder.emit(
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "tmpdir"
|
|
4
4
|
|
|
5
5
|
require_relative "../runtime/jit"
|
|
6
|
+
require_relative "../type_of_memo"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
module Inference
|
|
@@ -95,6 +96,7 @@ module Rigor
|
|
|
95
96
|
# what is left of the parent's window rather than a fresh one; {Runtime::Jit.rearm_after_fork}
|
|
96
97
|
# carries the mechanism.
|
|
97
98
|
Runtime::Jit.rearm_after_fork
|
|
99
|
+
TypeOfMemo.forked
|
|
98
100
|
# Published before the block runs, so anything it lazily creates for this process alone lands in the
|
|
99
101
|
# parent's tmpdir and dies with it. See {.child_scratch_dir}.
|
|
100
102
|
@child_scratch_dir = File.dirname(out_path)
|
|
@@ -105,6 +107,7 @@ module Rigor
|
|
|
105
107
|
ensure
|
|
106
108
|
@child_scratch_dir = nil
|
|
107
109
|
end
|
|
110
|
+
TypeOfMemo.write_report
|
|
108
111
|
exit!(status)
|
|
109
112
|
end
|
|
110
113
|
|
|
@@ -23,7 +23,8 @@ module Rigor
|
|
|
23
23
|
# what it reads. Unlike a root block it shares that slot, so a match in it still rebinds the definer's `$~`.
|
|
24
24
|
#
|
|
25
25
|
# `Enumerator.new` is neither: its block runs on an internal fiber whose root is not the block, so it reads and
|
|
26
|
-
# rebinds the creator's slot
|
|
26
|
+
# rebinds the creator's slot. It reads that slot when the enumerator is read, though, not where the block is
|
|
27
|
+
# written ({.deferred_body?}, issue #3195).
|
|
27
28
|
#
|
|
28
29
|
# The receiver of a root call must be the literal constant, and must resolve to the core class where a scope is
|
|
29
30
|
# at hand, so a project's own `Thread` (`module Pool; class Thread; …`) keeps the ordinary block rules.
|
|
@@ -115,6 +116,56 @@ module Rigor
|
|
|
115
116
|
scope.forget_error_info.forget_last_status
|
|
116
117
|
end
|
|
117
118
|
|
|
119
|
+
# The core constructors that keep their block and run it on a schedule of their own: an enumerator's generator
|
|
120
|
+
# runs when the enumerator is first read, and a hash's default proc on a missing key.
|
|
121
|
+
DEFERRED_CONSTRUCTORS = Set[:Enumerator, :Hash].freeze
|
|
122
|
+
private_constant :DEFERRED_CONSTRUCTORS
|
|
123
|
+
|
|
124
|
+
# Issue #1371, #3195 — true when `block_node` is a body that runs later than where it is written, on the slot of
|
|
125
|
+
# the frame that wrote it: a proc's ({.proc_body?}), or the block of `Enumerator.new` or `Hash.new` on the core
|
|
126
|
+
# class ({.deferred_constructor?}). Such a body runs whenever the proc is called, the enumerator is read or a
|
|
127
|
+
# key is missing, so the frame-local specials narrowed where it is written prove nothing about the slot it
|
|
128
|
+
# reads then. It enters with `$_` unbound ({LastLine.block_entry}), and with the match globals as
|
|
129
|
+
# {MatchRebinding.block_entry} gives each kind. It still shares the slot, so a match or a reader it runs
|
|
130
|
+
# rebinds the frame's as any block's does. The other calls that keep their block ({StoredBlockCall}) are a
|
|
131
|
+
# definer's or a root's ({.entry}).
|
|
132
|
+
def deferred_body?(block_node, call_node, scope)
|
|
133
|
+
proc_body?(block_node, call_node, scope) || deferred_constructor?(block_node, call_node, scope)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Issue #1371 — true when `block_node` is the body of a proc made where it is written: a lambda literal, or the
|
|
137
|
+
# block of Kernel's `lambda` / `proc` or of `Proc.new` on the core class. It enters with the match globals
|
|
138
|
+
# unbound; a project's own `Proc` keeps the ordinary entry.
|
|
139
|
+
# Only a lambda literal the statement evaluator enters gets this entry: one written as an operand is typed as a
|
|
140
|
+
# value, and the rule walker reads its body in the enclosing scope.
|
|
141
|
+
def proc_body?(block_node, call_node, scope)
|
|
142
|
+
return true if block_node.is_a?(Prism::LambdaNode)
|
|
143
|
+
return false unless call_node.is_a?(Prism::CallNode) && call_node.block.equal?(block_node)
|
|
144
|
+
|
|
145
|
+
receiver = call_node.receiver
|
|
146
|
+
case call_node.name
|
|
147
|
+
when :lambda, :proc then StoredBlockCall.kernel_spelled?(receiver)
|
|
148
|
+
when :new then StoredBlockCall.root_constant_name(receiver) == :Proc && core_class?(receiver, :Proc, scope)
|
|
149
|
+
else false
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Issue #3195 — true when `block_node` is the block of `Enumerator.new` or `Hash.new` on the literal constant
|
|
154
|
+
# naming the core class. The block often runs right away, in the statement that makes it
|
|
155
|
+
# (`Enumerator.new { … }.to_a`, `Hash.new { … }[key]`), and reads the narrowing then; it may as well run after a
|
|
156
|
+
# later match. The analyzer does not follow which, so the body enters with a narrowed match global untyped, as
|
|
157
|
+
# a definer body does ({.entry}): neither narrowed nor flagged. A project's own `Enumerator` or `Hash` keeps
|
|
158
|
+
# the ordinary entry, and so does every other spelling of the core class: a subclass (`Class.new(Hash)`), an
|
|
159
|
+
# alias (`E = Enumerator`), `Object::Enumerator`, or a receiver that is not a constant (`klass.new { … }`).
|
|
160
|
+
def deferred_constructor?(block_node, call_node, scope)
|
|
161
|
+
return false unless call_node.is_a?(Prism::CallNode) && call_node.name == :new &&
|
|
162
|
+
call_node.block.equal?(block_node)
|
|
163
|
+
|
|
164
|
+
receiver = call_node.receiver
|
|
165
|
+
constant = StoredBlockCall.root_constant_name(receiver)
|
|
166
|
+
DEFERRED_CONSTRUCTORS.include?(constant) && core_class?(receiver, constant, scope)
|
|
167
|
+
end
|
|
168
|
+
|
|
118
169
|
def core_class?(receiver, constant, scope)
|
|
119
170
|
type = scope.type_of(receiver)
|
|
120
171
|
type.is_a?(Type::Singleton) && type.class_name == constant.name
|
|
@@ -29,8 +29,8 @@ module Rigor
|
|
|
29
29
|
# - `[:main_mixin, name]` — a module a top-level `include`, `prepend` or `extend` mixes into `Object` (or
|
|
30
30
|
# `main`), as written. A module RBS does not know may carry any method, so it reaches every class.
|
|
31
31
|
# - the instance-variable writer entries of {IvarWriterCensus} (issue #1154): `[:ivar_set, name]`,
|
|
32
|
-
# `[:ivar_set_class, class_name]`, `[:own_write, class_name, name, path]
|
|
33
|
-
# `[:own_write_type, class_name, name, value, path]`.
|
|
32
|
+
# `[:ivar_set_class, class_name]`, `[:own_write, class_name, name, path]`, issue #2079's
|
|
33
|
+
# `[:own_write_type, class_name, name, value, path]` and issue #2983's `[:binds_class, class_key, constant]`.
|
|
34
34
|
# - `[:global_writer, name, method]` — issue #2314: a write of the global `name` inside a method body, keyed by
|
|
35
35
|
# the `def`'s name (or the literal name a `define_method` block defines), or by nil inside a lambda or a block
|
|
36
36
|
# its call keeps to run later ({StoredBlockCall}). A write in a block its call runs at once belongs to the
|
|
@@ -87,7 +87,7 @@ module Rigor
|
|
|
87
87
|
end
|
|
88
88
|
|
|
89
89
|
# #1154 — the {IvarWriterCensus} entries of `census`, for the incremental diff and its readers.
|
|
90
|
-
IVAR_KINDS = %i[ivar_set ivar_set_class own_write own_write_type].to_set.freeze
|
|
90
|
+
IVAR_KINDS = %i[ivar_set ivar_set_class own_write own_write_type binds_class].to_set.freeze
|
|
91
91
|
|
|
92
92
|
def ivar_set_entries(census) = census.select { |entry| IVAR_KINDS.include?(entry.first) }.to_set
|
|
93
93
|
|
|
@@ -97,6 +97,7 @@ module Rigor
|
|
|
97
97
|
when :ivar_set then "ivar_set:#{entry[1]}"
|
|
98
98
|
when :ivar_set_class then "ivar_set_class:#{entry[1]}"
|
|
99
99
|
when :own_write_type then "own_write_type:#{entry[1]}##{entry[2]}"
|
|
100
|
+
when :binds_class then "binds_class:#{entry[1].split(CensusForeignBlock::CANDIDATE_SEPARATOR).last.split('::').last}"
|
|
100
101
|
else "own_write:#{entry[1]}##{entry[2]}"
|
|
101
102
|
end
|
|
102
103
|
end
|
|
@@ -156,9 +157,10 @@ module Rigor
|
|
|
156
157
|
# A node the collector cannot read must not fail the analysis of its file, nor of every file the pre-pass
|
|
157
158
|
# feeds: the census then counts it as a definition of every name, which only declines a report.
|
|
158
159
|
# `owner` is the qualified name of the class whose body the node sits in, for {IvarWriterCensus} (nil where
|
|
159
|
-
# the host walk does not track it, or no class owns an instance there)
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
# the host walk does not track it, or no class owns an instance there); `receiver_owner` the class an
|
|
161
|
+
# explicit-receiver writer call names (issue #2488).
|
|
162
|
+
def visit(node, top_level:, owner: nil, receiver_owner: nil)
|
|
163
|
+
@ivar_writers.visit(node, owner, receiver_owner)
|
|
162
164
|
visit_global_writer(node)
|
|
163
165
|
case node
|
|
164
166
|
when Prism::AliasGlobalVariableNode then visit_global_alias(node)
|
|
@@ -6,6 +6,7 @@ require_relative "../source/constant_path"
|
|
|
6
6
|
require_relative "../source/node_children"
|
|
7
7
|
require_relative "refine_census"
|
|
8
8
|
require_relative "refine_self"
|
|
9
|
+
require_relative "../typing_state"
|
|
9
10
|
require_relative "in_effect_refinements/proc_literals"
|
|
10
11
|
require_relative "in_effect_refinements/scope_reads"
|
|
11
12
|
require_relative "in_effect_refinements/refine_defs"
|
|
@@ -53,6 +54,12 @@ module Rigor
|
|
|
53
54
|
#
|
|
54
55
|
# Built lazily: the walk runs the first time a consumer asks, so a file nobody asks about pays one small object.
|
|
55
56
|
class InEffectRefinements
|
|
57
|
+
# ADR-129 G4 — {#build} fills every one of these once, from the file's root node alone.
|
|
58
|
+
TypingState.memo_field(self, :@built, :@activations, :@refinement_defs, :@refine_defs, :@chained_refined_calls,
|
|
59
|
+
:@class_body_refines, :@refine_contexts, :@literal_bindings, :@refine_offsets,
|
|
60
|
+
:@unresolved_using, :@nesting,
|
|
61
|
+
reason: "built once by #build from the file's root node alone")
|
|
62
|
+
|
|
56
63
|
# The marker an activation contributes when it may put any refinement in effect: a non-constant `using`, a
|
|
57
64
|
# `using` whose include expansion is unknown, or a `refine` block whose module this walk cannot name. A list
|
|
58
65
|
# that carries it answers "any refinement may be in effect"; its position carries no meaning.
|
|
@@ -33,8 +33,10 @@ module Rigor
|
|
|
33
33
|
#
|
|
34
34
|
# **Invalidation.** Three conditions drop a recorded narrowing:
|
|
35
35
|
# - The receiver variable is rebound (handled inside `Scope#with_local` / `Scope#with_ivar`).
|
|
36
|
-
# - An intervening
|
|
37
|
-
#
|
|
36
|
+
# - An intervening store writes the same slot — `receiver[key] = value`, an index target, or a
|
|
37
|
+
# compound `receiver[key] op= value` / `&&=` (issue #1218) — since it could rebind the slot to
|
|
38
|
+
# nil; conservative drop. A store whose index is not one literal key drops every narrowing
|
|
39
|
+
# on the receiver.
|
|
38
40
|
# - An intervening mutator from {MutationWidening::SHAPE_MUTATORS} runs against the receiver
|
|
39
41
|
# (e.g. `params.delete(:f)`, `params.clear`, `params.default = 0`, `buf.delete_prefix!("x")`).
|
|
40
42
|
#
|
|
@@ -138,14 +140,61 @@ module Rigor
|
|
|
138
140
|
MutationWidening::SHAPE_MUTATORS.include?(method_name)
|
|
139
141
|
end
|
|
140
142
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
143
|
+
# Drops the narrowing on the slot a store through `[]=` overwrote. `node` is the `[]=` call, an index target
|
|
144
|
+
# (a multi-assign slot, a `for` index, a rescue reference) or a compound index write (`h[k] += v`,
|
|
145
|
+
# `h[k] &&= v`, `h[k] ||= v`); every form reads only `receiver` and `arguments`, and only a `[]=` call's last
|
|
146
|
+
# argument is the stored value rather than an index.
|
|
147
|
+
#
|
|
148
|
+
# Issue #1218 — a literal single index drops that one slot and leaves the others. Any other index list on a
|
|
149
|
+
# stable receiver — a key that is not a literal (`h[k] = 1` may be `h[:e] = 1`), a multi-index splice
|
|
150
|
+
# (`s[0, 1] = [1, 2]` shifts every later slot), a splat — may write any slot, so it drops every narrowing on
|
|
151
|
+
# the receiver, as {.invalidate_mutator} does. So does a negative Integer index on a receiver not shown to be a
|
|
152
|
+
# `Hash`: on an `Array` `a[-1]` is the slot some non-negative index also names. An unstable receiver records
|
|
153
|
+
# no narrowing to drop.
|
|
154
|
+
def invalidate_indexed_write(node, current_scope)
|
|
155
|
+
receiver = stable_receiver(node.receiver)
|
|
156
|
+
return current_scope if receiver.nil?
|
|
144
157
|
|
|
145
|
-
|
|
146
|
-
|
|
158
|
+
indexes = index_arguments(node)
|
|
159
|
+
address = indexes.size == 1 ? stable_address(node.receiver, indexes.first) : nil
|
|
160
|
+
if address && !(negative_index?(address.last) && !hash_receiver?(current_scope.type_of(node.receiver)))
|
|
161
|
+
return current_scope.without_indexed_narrowing(*address)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
current_scope.without_indexed_narrowings_for(*receiver)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Whether the literal `key` may name the same slot as another literal key: a negative Integer, unless the
|
|
168
|
+
# receiver is a `Hash`, which tells `-1` from every other key.
|
|
169
|
+
def aliasing_index?(key, receiver_type)
|
|
170
|
+
negative_index?(key) && !hash_receiver?(receiver_type)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def negative_index?(key) = key.is_a?(Integer) && key.negative?
|
|
174
|
+
|
|
175
|
+
def hash_receiver?(receiver_type) = carrier_receiver_type?(receiver_type, HASH_CARRIER)
|
|
176
|
+
|
|
177
|
+
VERBATIM_STORE_CARRIERS = %w[Hash Array].freeze
|
|
178
|
+
HASH_CARRIER = %w[Hash].freeze
|
|
179
|
+
private_constant :VERBATIM_STORE_CARRIERS, :HASH_CARRIER
|
|
180
|
+
|
|
181
|
+
# Whether a store through `[]=` on a receiver of `receiver_type` at the literal `key` leaves exactly the stored
|
|
182
|
+
# value at that slot, so a recorded narrowing may claim it. Only a `Hash` or `Array` carrier (a shape, a tuple,
|
|
183
|
+
# a `Hash` / `Array` nominal, refined or not) does, for every member of a union. A `String`'s `[]=` splices
|
|
184
|
+
# (`s[0] = "xy"` leaves `s[0] == "x"`), and a project class's `[]=` may store anything. A negative Integer key
|
|
185
|
+
# aliases a non-negative one on an `Array` ({.aliasing_index?}), so it records only on a `Hash`.
|
|
186
|
+
def verbatim_slot?(receiver_type, key)
|
|
187
|
+
carrier_receiver_type?(receiver_type, VERBATIM_STORE_CARRIERS) && !aliasing_index?(key, receiver_type)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def carrier_receiver_type?(type, classes)
|
|
191
|
+
members = type.is_a?(Type::Union) ? type.members : [type]
|
|
192
|
+
members.all? { |member| classes.include?(UnknownStoreWidening.carrier_class(member)) }
|
|
193
|
+
end
|
|
147
194
|
|
|
148
|
-
|
|
195
|
+
def index_arguments(node)
|
|
196
|
+
args = node.arguments&.arguments || []
|
|
197
|
+
node.is_a?(Prism::CallNode) ? args[0...-1] : args
|
|
149
198
|
end
|
|
150
199
|
|
|
151
200
|
def invalidate_mutator(call_node, current_scope)
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
|
+
require_relative "census_foreign_block"
|
|
6
|
+
require_relative "receiver_writer_call"
|
|
7
|
+
|
|
5
8
|
module Rigor
|
|
6
9
|
module Inference
|
|
7
10
|
# Issue #1154 — the instance-variable writers {GlobalWriteCensus} records for the declared-parameter credit
|
|
@@ -24,9 +27,15 @@ module Rigor
|
|
|
24
27
|
# file writes the same ivar of the same class.
|
|
25
28
|
# - `[:own_write_type, class_name, name, value]` — issues #2079 / #2170: the same write with its coarse value, a
|
|
26
29
|
# literal's as `[:lit, value]` and anything else as {DYNAMIC}, and a class-body `attr_writer` /
|
|
27
|
-
# `attr_accessor` of the name as {DYNAMIC}. The path is appended as for `:own_write`.
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
+
# `attr_accessor` of the name as {DYNAMIC}. The path is appended as for `:own_write`. Issue #2488: an
|
|
31
|
+
# explicit-receiver `E.attr_writer` / `E.attr_accessor` (or one through `send`) records the same {DYNAMIC}
|
|
32
|
+
# entry under `E`, and an `E.define_method` block is an instance body of `E` ({ReceiverWriterCall}). The
|
|
33
|
+
# class-ivar census of a file joins another file's entries, and those of the modules and superclasses on the
|
|
34
|
+
# class's chain, into the ivars it already holds ({ScopeIndexer::IvarContributions}).
|
|
35
|
+
# Issue #2983: a barred `class_eval`-family block on a receiver rooted at another constant files the code in it
|
|
36
|
+
# that runs on an instance under that constant's candidate key ({#remember_root_constant}), a String
|
|
37
|
+
# `class_eval` records a {DYNAMIC} write of {ANY_IVAR} ({#record_string_eval}), and a constant write `C = K` /
|
|
38
|
+
# `C = K.new` records `[:binds_class, key, "C"]` ({#record_binding}), so the join can take `C`'s entries for `K`.
|
|
30
39
|
class IvarWriterCensus
|
|
31
40
|
SEND_CALLS = %i[send __send__ public_send].to_set.freeze
|
|
32
41
|
# The methods a `send` reaching which is an open-world writer.
|
|
@@ -41,6 +50,10 @@ module Rigor
|
|
|
41
50
|
NO_ARGUMENTS = [].freeze
|
|
42
51
|
# Issue #2079 — the coarse value of a write that is not a literal.
|
|
43
52
|
DYNAMIC = :dynamic
|
|
53
|
+
# Issue #2983 — the ivar name of a write whose ivar no literal spells: a string `class_eval`'s code may write any.
|
|
54
|
+
ANY_IVAR = :*
|
|
55
|
+
# The `class_eval`-family calls that take code in a String.
|
|
56
|
+
STRING_EVALS = %i[class_eval module_eval].to_set.freeze
|
|
44
57
|
# The writer declarations a class body records as a {DYNAMIC} write of each name it spells.
|
|
45
58
|
WRITER_DECLARATIONS = %i[attr_writer attr_accessor].to_set.freeze
|
|
46
59
|
|
|
@@ -54,16 +67,22 @@ module Rigor
|
|
|
54
67
|
@owner_names = {}.compare_by_identity
|
|
55
68
|
@own_writes = {}
|
|
56
69
|
@typed_writes = Set.new
|
|
70
|
+
@rooted = Set.new.compare_by_identity
|
|
57
71
|
end
|
|
58
72
|
|
|
59
73
|
# Fed each node of a pre-order descent. `owner` is the class whose body the node sits in, as its qualified name
|
|
60
74
|
# or as the host walk's prefix Array (joined here only when an entry needs it), or nil where no class owns an
|
|
61
|
-
# instance (`class << self`, the top level, an opaque block).
|
|
62
|
-
|
|
75
|
+
# instance (`class << self`, the top level, an opaque block). `receiver_owner` is the class an explicit-receiver
|
|
76
|
+
# writer call ({ReceiverWriterCall}) names, resolved by the host walk as it resolves a `class_eval` receiver.
|
|
77
|
+
def visit(node, owner, receiver_owner = nil)
|
|
63
78
|
case node
|
|
64
|
-
when Prism::DefNode then
|
|
79
|
+
when Prism::DefNode then remember_host(node, owner) if node.receiver.nil?
|
|
80
|
+
when Prism::ConstantWriteNode, Prism::ConstantPathWriteNode then record_binding(node, owner)
|
|
65
81
|
when Prism::SingletonClassNode then visit_singleton_class(node)
|
|
66
|
-
when Prism::CallNode
|
|
82
|
+
when Prism::CallNode
|
|
83
|
+
bar_foreign_block(node)
|
|
84
|
+
visit_call(node, owner) if WATCHED_CALLS.include?(node.name)
|
|
85
|
+
record_receiver_writers(node, receiver_owner) if receiver_owner
|
|
67
86
|
when Prism::InstanceVariableWriteNode, Prism::InstanceVariableOrWriteNode, Prism::InstanceVariableAndWriteNode,
|
|
68
87
|
Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableTargetNode
|
|
69
88
|
record_own_write(node) unless @instance_ranges.empty?
|
|
@@ -111,6 +130,74 @@ module Rigor
|
|
|
111
130
|
[:lit, node.is_a?(Prism::SymbolNode) ? text.to_sym : text.dup.freeze].freeze
|
|
112
131
|
end
|
|
113
132
|
|
|
133
|
+
# Issue #2490 — a block inside an instance body whose `self` is another object ({CensusForeignBlock}) is
|
|
134
|
+
# remembered with no owner, so its writes credit nothing. Pre-order, so an instance body nested in it (an
|
|
135
|
+
# `E.define_method` block, remembered after) still answers for its own class.
|
|
136
|
+
def bar_foreign_block(node)
|
|
137
|
+
return if @instance_ranges.empty? || !CensusForeignBlock::CALLS.include?(node.name)
|
|
138
|
+
|
|
139
|
+
instance = instance_owner(node)
|
|
140
|
+
return unless instance && CensusForeignBlock.foreign?(node, owner_name(instance))
|
|
141
|
+
|
|
142
|
+
remember(node.block.location, nil)
|
|
143
|
+
remember_reentry(node, instance)
|
|
144
|
+
remember_root_constant(node, instance)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Issue #2983 — the code a barred `class_eval`-family block on a receiver rooted at another constant reenters
|
|
148
|
+
# (`Anc.class_eval`, `INST.class.class_eval`, `AliasK.new.class.class_eval`), remembered under that constant's
|
|
149
|
+
# candidate key ({CensusForeignBlock.root_constant}), in place of the host walk's as-written owner. A class
|
|
150
|
+
# whose census joins the entries of the constant the key resolves to, an ancestor's, an alias's or an instance
|
|
151
|
+
# constant's ({ScopeIndexer::IvarContributions}), then sees the write.
|
|
152
|
+
def remember_root_constant(node, instance)
|
|
153
|
+
root, blocks = CensusForeignBlock.rooted_reentry(node, owner_name(instance))
|
|
154
|
+
blocks&.each { |block| remember(block.location, root) }
|
|
155
|
+
@rooted.merge(blocks) if blocks
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# The host walk's `owner` of a `def` or `define_method` block, unless {#remember_root_constant} filed it already.
|
|
159
|
+
def remember_host(node, owner) = (remember(node.location, owner) if owner && !@rooted.include?(node))
|
|
160
|
+
|
|
161
|
+
# Issue #2983 — `C = K` or `C = K.new` (`.allocate`) records `[:binds_class, key, "C"]`, `K`'s candidate key and
|
|
162
|
+
# `C`'s qualified name ({CensusForeignBlock.binding_entry}), so a class's census also joins the entries filed
|
|
163
|
+
# under an alias or an instance constant of it (`AliasK.class_eval { … }`, `INST.class.class_eval { … }`).
|
|
164
|
+
def record_binding(node, owner)
|
|
165
|
+
entry = CensusForeignBlock.binding_entry(node, nesting(owner))
|
|
166
|
+
@census << entry if entry
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# The lexical class and module names `owner` spells: the host walk's prefix Array, or a resolved name.
|
|
170
|
+
def nesting(owner) = owner.nil? ? NO_ARGUMENTS : owner_name(owner).split("::")
|
|
171
|
+
|
|
172
|
+
# Issue #2983 — `class_eval` / `module_eval` given a String runs code the census cannot read, which may define
|
|
173
|
+
# a method writing any ivar: a {DYNAMIC} write of {ANY_IVAR} is recorded for the module the receiver may be. That
|
|
174
|
+
# is the class owning the body for an implicit or `self` receiver in a class body, the instance's class for a
|
|
175
|
+
# receiver in an instance body that {CensusForeignBlock.foreign_module?} does not bar, and otherwise the constant
|
|
176
|
+
# the receiver chain is rooted at.
|
|
177
|
+
def record_string_eval(node, owner)
|
|
178
|
+
return unless STRING_EVALS.include?(node.name) && node.block.nil? && node.arguments
|
|
179
|
+
|
|
180
|
+
instance = instance_owner(node) unless @instance_ranges.empty?
|
|
181
|
+
target = CensusForeignBlock.string_eval_target(node, owner_name(instance), owner_name(owner))
|
|
182
|
+
record_typed_write(target, ANY_IVAR, DYNAMIC) if target
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# The `define_method` blocks in a barred body that run on the instance again, remembered after the bar so they
|
|
186
|
+
# answer for the instance's class ({CensusForeignBlock.reentry_blocks}).
|
|
187
|
+
def remember_reentry(node, instance)
|
|
188
|
+
CensusForeignBlock.reentry_blocks(node, owner_name(instance)).each do |block|
|
|
189
|
+
remember(block.location, instance)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Issue #2488 — `E.attr_writer :w` declares a writer of `@w` on `E`, and `E.define_method(:m) { … }` installs an
|
|
194
|
+
# instance body of `E`, wherever the call sits.
|
|
195
|
+
def record_receiver_writers(node, class_name)
|
|
196
|
+
ReceiverWriterCall.writer_ivars(node).each { |ivar| record_typed_write(class_name, ivar, DYNAMIC) }
|
|
197
|
+
block = ReceiverWriterCall.define_method_block(node)
|
|
198
|
+
remember(block.location, class_name) if block
|
|
199
|
+
end
|
|
200
|
+
|
|
114
201
|
# Issue #2079 — `attr_writer :x` / `attr_accessor :x` in a class body writes `@x` with whatever the caller hands.
|
|
115
202
|
def record_writer_declaration(node, owner)
|
|
116
203
|
return unless owner && WRITER_DECLARATIONS.include?(node.name) && self_receiver?(node)
|
|
@@ -128,7 +215,8 @@ module Rigor
|
|
|
128
215
|
|
|
129
216
|
def visit_call(node, owner)
|
|
130
217
|
record_writer_declaration(node, owner)
|
|
131
|
-
|
|
218
|
+
record_string_eval(node, owner)
|
|
219
|
+
remember_host(node.block, owner) if define_method_block?(node)
|
|
132
220
|
if self_receiver?(node) || own_singleton_class_call?(node)
|
|
133
221
|
return if @instance_ranges.empty? || !(own_singleton_class_call?(node) || open_world?(node))
|
|
134
222
|
|
|
@@ -139,12 +227,19 @@ module Rigor
|
|
|
139
227
|
end
|
|
140
228
|
end
|
|
141
229
|
|
|
142
|
-
# `class << self` inside an instance `def` opens the instance's singleton class.
|
|
230
|
+
# `class << self` inside an instance `def` opens the instance's singleton class. Its body's `self` is that
|
|
231
|
+
# singleton class, so an `@x` write there is the singleton's own ivar (#2490), whatever the expression.
|
|
143
232
|
def visit_singleton_class(node)
|
|
144
|
-
return
|
|
233
|
+
return if @instance_ranges.empty?
|
|
145
234
|
|
|
146
235
|
instance = instance_owner(node)
|
|
147
|
-
|
|
236
|
+
return unless instance
|
|
237
|
+
|
|
238
|
+
record_class(instance) if node.expression.is_a?(Prism::SelfNode)
|
|
239
|
+
return unless node.body
|
|
240
|
+
|
|
241
|
+
remember(node.body.location, nil)
|
|
242
|
+
remember_reentry(node, instance)
|
|
148
243
|
end
|
|
149
244
|
|
|
150
245
|
def record_class(owner) = @census << [:ivar_set_class, owner_name(owner)].freeze
|
|
@@ -5,7 +5,8 @@ require "prism"
|
|
|
5
5
|
require_relative "../type"
|
|
6
6
|
require_relative "mutation_widening"
|
|
7
7
|
require_relative "key_presence_guard/scan"
|
|
8
|
-
require_relative "../
|
|
8
|
+
require_relative "../typing_state"
|
|
9
|
+
require_relative "../memo_audit"
|
|
9
10
|
|
|
10
11
|
module Rigor
|
|
11
12
|
module Inference
|
|
@@ -58,9 +59,22 @@ module Rigor
|
|
|
58
59
|
] + MutationWidening::SHAPE_MUTATORS.to_a
|
|
59
60
|
).to_set.freeze
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
# ADR-129 WD1 — the walk {.with_guards} installs is a Frame, and {.record}, which rewrites its `guard_ends` in
|
|
63
|
+
# place, is a Resource touch that also takes a fresh id ({TypingState::State#rewrite}), so a read of the walk's
|
|
64
|
+
# guards is fixed while the id is. The walk's `defs`, `frozen_constants` and `memo` are extensional caches of
|
|
65
|
+
# `root`, filled in place without a touch.
|
|
66
|
+
WALK = TypingState.frame(
|
|
67
|
+
:key_presence_walk,
|
|
68
|
+
contents: :resource, read_exemption: :id_determined,
|
|
69
|
+
reason: "installed per guarded re-walk; each recorded guard rewrites its `guard_ends`",
|
|
70
|
+
witness: "spec/fixtures/typing_state_witnesses/key_presence_walk.rb"
|
|
71
|
+
)
|
|
72
|
+
REWALK = TypingState.memo(
|
|
73
|
+
:key_presence_rewalk,
|
|
74
|
+
run_scoped: true, audit: "key presence rewalk",
|
|
75
|
+
reason: "the last file's guarded index, keyed by the file's root"
|
|
76
|
+
)
|
|
77
|
+
private_constant :WALK, :REWALK
|
|
64
78
|
|
|
65
79
|
# The re-walk's state: the file's root, the end offset of each recorded guard, and the per-file scans.
|
|
66
80
|
Walk = Struct.new(:root, :guard_ends, :defs, :frozen_constants, :memo) do
|
|
@@ -88,25 +102,21 @@ module Rigor
|
|
|
88
102
|
|
|
89
103
|
# Runs the block with guards recorded and read, for the re-walk of `root`. Thread-local, so it holds under a
|
|
90
104
|
# Ractor worker too.
|
|
91
|
-
def with_guards(root)
|
|
92
|
-
|
|
93
|
-
Thread.current[STATE_KEY] = Walk.new(root, {}, nil, nil, {})
|
|
94
|
-
yield
|
|
95
|
-
ensure
|
|
96
|
-
Thread.current[STATE_KEY] = previous
|
|
105
|
+
def with_guards(root, &)
|
|
106
|
+
TypingState.current.with(WALK, Walk.new(root, {}, nil, nil, {}), &)
|
|
97
107
|
end
|
|
98
108
|
|
|
99
109
|
# Runs the block with guards off again: a callee's return summary, memoised for the whole run, must be the one
|
|
100
110
|
# the file's analysis reads.
|
|
101
|
-
def without_guards
|
|
102
|
-
|
|
103
|
-
Thread.current[STATE_KEY] = nil
|
|
104
|
-
yield
|
|
105
|
-
ensure
|
|
106
|
-
Thread.current[STATE_KEY] = previous
|
|
111
|
+
def without_guards(&)
|
|
112
|
+
TypingState.current.with(WALK, nil, &)
|
|
107
113
|
end
|
|
108
114
|
|
|
109
|
-
def active? = !
|
|
115
|
+
def active? = !TypingState.current[WALK].nil?
|
|
116
|
+
|
|
117
|
+
# {.active?}, read only to decide whether to turn the walk off for the extent that follows ({.without_guards}):
|
|
118
|
+
# that extent reads it off either way, so the key audit does not count the read (ADR-129 WD1).
|
|
119
|
+
def forcing_off? = !TypingState.current.forcing_read(WALK).nil?
|
|
110
120
|
|
|
111
121
|
# Whether `call.possible-nil-receiver` should withhold its report on `call_node`, whose receiver the file's
|
|
112
122
|
# analysis types `receiver_type`: true only when the guarded re-walk types that receiver as exactly
|
|
@@ -130,19 +140,33 @@ module Rigor
|
|
|
130
140
|
# The guarded scope index of `root`, built once per file (the cache holds the last file only), or nil when the
|
|
131
141
|
# file holds no guard-shaped call.
|
|
132
142
|
def rewalk(root, scope_index)
|
|
133
|
-
|
|
134
|
-
|
|
143
|
+
return guarded_index(root, scope_index) if MemoAudit.enabled? && MemoAudit.suspended?
|
|
144
|
+
|
|
145
|
+
state = TypingState.current
|
|
146
|
+
cached = state[REWALK]
|
|
147
|
+
if cached&.first.equal?(root)
|
|
148
|
+
# A re-walk that raised is stored as nil, and its audit recomputation answers nil the same way.
|
|
149
|
+
return MemoAudit.check("key presence rewalk", cached.last, root) do
|
|
150
|
+
guarded_index(root, scope_index)
|
|
151
|
+
rescue StandardError
|
|
152
|
+
nil
|
|
153
|
+
end
|
|
154
|
+
end
|
|
135
155
|
|
|
136
156
|
# Cached before the walk, so a re-walk that raises is attempted once per file and then answers nil.
|
|
137
|
-
|
|
157
|
+
state[REWALK] = [root, nil]
|
|
158
|
+
index = MemoAudit.key_audit("key presence rewalk") { guarded_index(root, scope_index) }
|
|
159
|
+
state[REWALK] = [root, index]
|
|
160
|
+
index
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def guarded_index(root, scope_index)
|
|
138
164
|
return nil unless guard_shaped_call?(root)
|
|
139
165
|
|
|
140
166
|
base = scope_index[root]
|
|
141
|
-
|
|
167
|
+
base && with_guards(root) do
|
|
142
168
|
ScopeIndexer.index(root, default_scope: base.with_isolated_side_tables)
|
|
143
169
|
end
|
|
144
|
-
Thread.current[CACHE_KEY] = [root, index]
|
|
145
|
-
index
|
|
146
170
|
end
|
|
147
171
|
|
|
148
172
|
def guard_shaped_call?(node)
|
|
@@ -206,7 +230,8 @@ module Rigor
|
|
|
206
230
|
# The truthy edge of `receiver.key?(key)` with a non-literal key, inside the re-walk: `scope` with the guard
|
|
207
231
|
# recorded, or nil when the guard does not apply.
|
|
208
232
|
def record(call_node, scope)
|
|
209
|
-
|
|
233
|
+
state = TypingState.current
|
|
234
|
+
walk = state[WALK]
|
|
210
235
|
return nil if walk.nil?
|
|
211
236
|
|
|
212
237
|
args = call_node.arguments&.arguments
|
|
@@ -217,6 +242,7 @@ module Rigor
|
|
|
217
242
|
return nil unless receiver_type?(scope.type_of(call_node.receiver))
|
|
218
243
|
return nil unless Scan.eligible?(walk, call_node, guard)
|
|
219
244
|
|
|
245
|
+
state.rewrite(WALK)
|
|
220
246
|
walk.guard_ends[guard] = call_node.location.end_offset
|
|
221
247
|
scope.with_indexed_narrowing(*guard, PRESENT)
|
|
222
248
|
end
|
|
@@ -226,7 +252,7 @@ module Rigor
|
|
|
226
252
|
# read may have changed it ({.intervening?}), and no value of the receiver can be `nil` (the read cannot tell a
|
|
227
253
|
# value's own `nil` from the miss). Nil when no guard applies or nothing would change.
|
|
228
254
|
def guarded_read(node, type, scope)
|
|
229
|
-
walk =
|
|
255
|
+
walk = TypingState.current[WALK]
|
|
230
256
|
guard = walk && held_guard(node, scope)
|
|
231
257
|
return nil if guard.nil? || Scan.intervening?(walk, guard, node)
|
|
232
258
|
|