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
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require "rbs"
|
|
4
4
|
|
|
5
5
|
require_relative "../type"
|
|
6
|
+
require_relative "../typing_state"
|
|
7
|
+
require_relative "../memo_audit"
|
|
6
8
|
|
|
7
9
|
module Rigor
|
|
8
10
|
module Inference
|
|
@@ -36,8 +38,9 @@ module Rigor
|
|
|
36
38
|
# `untyped`. `Type::Intersection` stays out of this path: it is the same-base refinement composite
|
|
37
39
|
# carrier, and the dispatch tiers do not accept it as a receiver.
|
|
38
40
|
#
|
|
39
|
-
# Interface types
|
|
40
|
-
#
|
|
41
|
+
# Interface types degrade to `Dynamic[Top]`, except an interface in the `Rigor::Plugin` namespace, which
|
|
42
|
+
# translates to `Dynamic[Type::Interface]` (ADR-130 WD1/WD4): `untyped` to every rule, and resolvable only by a
|
|
43
|
+
# call of a member it declares (`RbsDispatch`).
|
|
41
44
|
#
|
|
42
45
|
# The optional `self_type:` and `instance_type:` arguments are the Rigor counterparts of RBS's
|
|
43
46
|
# `self` and `instance` tokens:
|
|
@@ -78,7 +81,7 @@ module Rigor
|
|
|
78
81
|
RBS::Types::Alias => :translate_alias,
|
|
79
82
|
RBS::Types::Intersection => :translate_intersection,
|
|
80
83
|
RBS::Types::Variable => :translate_variable,
|
|
81
|
-
RBS::Types::Interface => :
|
|
84
|
+
RBS::Types::Interface => :translate_interface
|
|
82
85
|
}.freeze
|
|
83
86
|
private_constant :TRANSLATORS
|
|
84
87
|
|
|
@@ -106,14 +109,12 @@ module Rigor
|
|
|
106
109
|
# repeat with one frozen String.
|
|
107
110
|
@relative_names = {}
|
|
108
111
|
|
|
109
|
-
#
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
# scratch; every repeat now shares one Union.
|
|
116
|
-
@closed_alias_translations = ObjectSpace::WeakKeyMap.new
|
|
112
|
+
# ADR-130 WD2 — the erasure baseline's flag, installed by {.with_interface_erasure}.
|
|
113
|
+
INTERFACE_ERASURE = TypingState.frame(
|
|
114
|
+
:interface_erasure, reason: "a flag installed around an erasure-baseline run (the parity harness)",
|
|
115
|
+
witness: "spec/fixtures/typing_state_witnesses/interface_erasure.rb"
|
|
116
|
+
)
|
|
117
|
+
private_constant :INTERFACE_ERASURE
|
|
117
118
|
|
|
118
119
|
class << self
|
|
119
120
|
# @param self_type — substitute for `Bases::Self`.
|
|
@@ -130,6 +131,20 @@ module Rigor
|
|
|
130
131
|
translate_in(rbs_type, Context.new(self_type, instance_type, type_vars, alias_expander, 0))
|
|
131
132
|
end
|
|
132
133
|
|
|
134
|
+
# ADR-130 WD2 — the erasure baseline. Inside the block every RBS interface translates to `untyped`, as it did
|
|
135
|
+
# before ADR-130, whatever carrier the default engine has; the behavioural parity gate
|
|
136
|
+
# (`spec/integration/interface_parity_spec.rb`) runs its baseline here, so the baseline never comes from the
|
|
137
|
+
# default engine. {translate_interface} reads {interface_erasure?}, and {translate_alias} bypasses the loader's
|
|
138
|
+
# closed-alias memo under it, so a translation made in one mode is never served in the other (ADR-130 WD4). The
|
|
139
|
+
# switch lives in the calling thread's (and fiber's) `TypingState`, so a run under it is sequential: a pooled
|
|
140
|
+
# run raises (`PoolCoordinator`).
|
|
141
|
+
def with_interface_erasure(&)
|
|
142
|
+
MemoAudit.check_run_level_install(:interface_erasure)
|
|
143
|
+
TypingState.current.with(INTERFACE_ERASURE, true, &)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def interface_erasure? = TypingState.current[INTERFACE_ERASURE] == true
|
|
147
|
+
|
|
133
148
|
private
|
|
134
149
|
|
|
135
150
|
def translate_in(rbs_type, context)
|
|
@@ -151,6 +166,28 @@ module Rigor
|
|
|
151
166
|
Type::Combinator.untyped
|
|
152
167
|
end
|
|
153
168
|
|
|
169
|
+
# ADR-130 WD4 — the namespace whose interfaces carry the facet: `::Rigor::Plugin::_Scope` and its siblings.
|
|
170
|
+
PLUGIN_INTERFACE_PATH = %i[Rigor Plugin].freeze
|
|
171
|
+
private_constant :PLUGIN_INTERFACE_PATH
|
|
172
|
+
|
|
173
|
+
# ADR-130 WD1/WD4 — a `Rigor::Plugin` interface is `Dynamic[Interface]`; every other interface, rbs core's
|
|
174
|
+
# conversion aliases (`int`, `string`, ...) included, is `untyped` as before. Which one is a pure function of
|
|
175
|
+
# the `TypeName` (its namespace path, read without allocating), so a closed alias translates alike whichever
|
|
176
|
+
# file or worker reaches it first. Under {with_interface_erasure} every interface is `untyped`.
|
|
177
|
+
def translate_interface(rbs_type, context)
|
|
178
|
+
name = rbs_type.name
|
|
179
|
+
return Type::Combinator.untyped unless name.namespace.path == PLUGIN_INTERFACE_PATH
|
|
180
|
+
return Type::Combinator.untyped if interface_erasure?
|
|
181
|
+
|
|
182
|
+
carrier =
|
|
183
|
+
if rbs_type.args.empty?
|
|
184
|
+
Type::Interface.new(relative_name(name))
|
|
185
|
+
else
|
|
186
|
+
Type::Interface.new(relative_name(name), rbs_type.args.map { |arg| translate_in(arg, context) })
|
|
187
|
+
end
|
|
188
|
+
Type::Combinator.dynamic(carrier)
|
|
189
|
+
end
|
|
190
|
+
|
|
154
191
|
def translate_nil(_rbs_type, _context)
|
|
155
192
|
Type::Combinator.constant_of(nil)
|
|
156
193
|
end
|
|
@@ -251,18 +288,34 @@ module Rigor
|
|
|
251
288
|
|
|
252
289
|
expanded = expander.expand_type_alias(rbs_type)
|
|
253
290
|
return Type::Combinator.untyped if expanded.nil?
|
|
291
|
+
|
|
254
292
|
# A nested expansion is translated under its parent's budget, and `rigor trace` records every
|
|
255
|
-
# union merge as it happens, so neither consults the memo.
|
|
256
|
-
|
|
293
|
+
# union merge as it happens, so neither consults the memo. Nor does the erasure baseline (ADR-130 WD4): the
|
|
294
|
+
# memo holds the default engine's translations, where a `Rigor::Plugin` interface keeps its facet.
|
|
295
|
+
memo = closed_translations_of(expander)
|
|
296
|
+
unless memo && context.alias_depth.zero? && !FlowTracer.active? && !interface_erasure?
|
|
297
|
+
return translate_in(expanded, context.deeper)
|
|
298
|
+
end
|
|
257
299
|
|
|
258
|
-
cached =
|
|
300
|
+
cached = memo[expanded]
|
|
259
301
|
return cached if cached
|
|
260
302
|
|
|
261
303
|
translated = translate_in(expanded, context.deeper)
|
|
262
|
-
|
|
304
|
+
memo[expanded] = translated if closed_type?(expanded)
|
|
263
305
|
translated
|
|
264
306
|
end
|
|
265
307
|
|
|
308
|
+
# #775 — the translated form of a CLOSED alias expansion, keyed by the expansion, lives with the expander.
|
|
309
|
+
# Closed means no `self`, `instance` or type variable anywhere inside, so the caller's context cannot change
|
|
310
|
+
# the answer; at expansion depth 0 the budget cannot either. But a nested alias inside the expansion is
|
|
311
|
+
# expanded through the expander (`type a = b | Integer` holds `b`), so the translation reads the expander's
|
|
312
|
+
# environment: a memo shared by every expander answered a second loader, whose `b` differs, with the first
|
|
313
|
+
# loader's translation (equal expansions are equal keys). The expander owns its memo, so it dies with it. An
|
|
314
|
+
# expander that offers none (anything answering only `expand_type_alias`) is translated afresh each time.
|
|
315
|
+
def closed_translations_of(expander)
|
|
316
|
+
expander.closed_alias_translations if expander.respond_to?(:closed_alias_translations)
|
|
317
|
+
end
|
|
318
|
+
|
|
266
319
|
# True when nothing in `rbs_type`'s tree reads the translation context: no `self`, no `instance`,
|
|
267
320
|
# no type variable. Every rbs type answers `each_type` (the leaves through `EmptyEachType`).
|
|
268
321
|
def closed_type?(rbs_type)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
# Issue #2488 — a writer a class gains from a call on an explicit receiver outside its body: `E.attr_writer :w`,
|
|
8
|
+
# `E.send(:attr_accessor, :y)`, `E.define_method(:set) { @v = … }` (and `define_method` through `send`). Ruby made
|
|
9
|
+
# `attr_*` and `define_method` public in 2.5, so each is the receiver class's own declaration, exactly as the same
|
|
10
|
+
# call in a `E.class_eval { … }` block is. Both ivar censuses read the calls through here: the per-file class-ivar
|
|
11
|
+
# walk ({ScopeIndexer}) and the cross-file {IvarWriterCensus}. The receiver's name is the caller's to resolve, the
|
|
12
|
+
# same way each resolves a `class_eval` receiver.
|
|
13
|
+
module ReceiverWriterCall
|
|
14
|
+
SEND_CALLS = %i[send __send__ public_send].to_set.freeze
|
|
15
|
+
WRITER_MACROS = %i[attr_writer attr_accessor].to_set.freeze
|
|
16
|
+
# The names a direct call spells; a `send` reaches any of them through its literal first argument.
|
|
17
|
+
CALLS = (WRITER_MACROS | SEND_CALLS | %i[define_method]).freeze
|
|
18
|
+
NO_ARGUMENTS = [].freeze
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# The receiver of `node` when it is one of these calls on a receiver other than `self`, else nil.
|
|
23
|
+
def receiver(node)
|
|
24
|
+
return nil unless node.is_a?(Prism::CallNode) && CALLS.include?(node.name)
|
|
25
|
+
|
|
26
|
+
recv = node.receiver
|
|
27
|
+
recv unless recv.nil? || recv.is_a?(Prism::SelfNode)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The ivar names an explicit-receiver `attr_writer` / `attr_accessor` declares (`[:@w]`), or an empty Array.
|
|
31
|
+
def writer_ivars(node)
|
|
32
|
+
name, arguments = called(node)
|
|
33
|
+
return NO_ARGUMENTS unless WRITER_MACROS.include?(name)
|
|
34
|
+
|
|
35
|
+
arguments.filter_map do |argument|
|
|
36
|
+
text = literal_name(argument)
|
|
37
|
+
:"@#{text}" if text
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The literal block an explicit-receiver `define_method` installs as an instance method, or nil.
|
|
42
|
+
def define_method_block(node)
|
|
43
|
+
name, = called(node)
|
|
44
|
+
block = node.block
|
|
45
|
+
block if name == :define_method && block.is_a?(Prism::BlockNode)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The method `node` calls and its arguments, seen through a `send` whose first argument spells the name.
|
|
49
|
+
def called(node)
|
|
50
|
+
return [nil, NO_ARGUMENTS] if receiver(node).nil?
|
|
51
|
+
|
|
52
|
+
arguments = node.arguments&.arguments || NO_ARGUMENTS
|
|
53
|
+
return [node.name, arguments] unless SEND_CALLS.include?(node.name)
|
|
54
|
+
|
|
55
|
+
sent = literal_name(arguments.first)
|
|
56
|
+
sent ? [sent, arguments.drop(1)] : [nil, NO_ARGUMENTS]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def literal_name(argument)
|
|
60
|
+
return unless argument.is_a?(Prism::SymbolNode) || argument.is_a?(Prism::StringNode)
|
|
61
|
+
|
|
62
|
+
text = argument.unescaped
|
|
63
|
+
text.to_sym if text.valid_encoding?
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "../source/node_walker"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Inference
|
|
8
|
+
# Which of a def's parameters its own recursion rebinds (issue #3115). The bot-collapse retry of
|
|
9
|
+
# `ExpressionTyper#fixpoint_user_method_return` types the body once for every call the recursion makes, and a
|
|
10
|
+
# recursive call binds a parameter to whatever it passes: `tog(n: n - 1, f: !f)` gives `f` a value the
|
|
11
|
+
# outer call never held. A parameter is KEPT only when every call to the def's own name inside the body
|
|
12
|
+
# passes it the same parameter read unchanged (`cfg(n - 1, mode)` keeps `mode`), and the body never
|
|
13
|
+
# assigns it; every other parameter is rebound, and the retry binds it `Dynamic[top]`.
|
|
14
|
+
#
|
|
15
|
+
# The test is syntactic and reads only the def. A call counts by its method name alone, whatever its
|
|
16
|
+
# receiver (`child.depth(n + 1)` re-enters the same recursion guard), so a same-named method of another
|
|
17
|
+
# class is read as a recursive call too, which only rebinds more. The caller supplies the def's other
|
|
18
|
+
# names (`alias`), since the def does not carry them. A call to ANOTHER method that recurses back (mutual
|
|
19
|
+
# recursion) is not followed: its parameters are judged by the def's own calls alone.
|
|
20
|
+
#
|
|
21
|
+
# Two shapes the test cannot read rebind more instead. A body that calls a method by a computed name
|
|
22
|
+
# ({REFLECTIVE_CALLS}) may recurse with any arguments, so every parameter is rebound ({ALL}). A name a
|
|
23
|
+
# block or lambda in the body declares (a parameter, a block-local, `it`) is rebound, because a read of
|
|
24
|
+
# it inside that block is the block's variable, not the parameter.
|
|
25
|
+
module RecursionRebinding
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
LOCAL_WRITE_NODES = Set[
|
|
29
|
+
Prism::LocalVariableWriteNode, Prism::LocalVariableOperatorWriteNode, Prism::LocalVariableOrWriteNode,
|
|
30
|
+
Prism::LocalVariableAndWriteNode, Prism::LocalVariableTargetNode
|
|
31
|
+
].freeze
|
|
32
|
+
private_constant :LOCAL_WRITE_NODES
|
|
33
|
+
|
|
34
|
+
# The calls that reach a method by a name the scan cannot read.
|
|
35
|
+
REFLECTIVE_CALLS = Set[:send, :__send__, :public_send, :method, :instance_method, :define_method].freeze
|
|
36
|
+
private_constant :REFLECTIVE_CALLS
|
|
37
|
+
|
|
38
|
+
# {.rebound_parameters}'s answer when every parameter is rebound.
|
|
39
|
+
ALL = :all
|
|
40
|
+
|
|
41
|
+
# The names the body's recursive calls, its blocks or its own assignments rebind, as a Set, or {ALL}. The
|
|
42
|
+
# Set may hold a name that is no parameter (any assigned local); the caller looks up parameters only.
|
|
43
|
+
# `names` is every name the def answers to: its own and its aliases.
|
|
44
|
+
def rebound_parameters(def_node, names = Set[def_node.name])
|
|
45
|
+
params = def_node.parameters
|
|
46
|
+
return Set.new unless params.is_a?(Prism::ParametersNode)
|
|
47
|
+
|
|
48
|
+
rebound = Set.new
|
|
49
|
+
catch(ALL) { collect(def_node.body, names, params, rebound) } == ALL ? ALL : rebound
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Every method name the body calls, for the caller to ask which of them are this def's aliases.
|
|
53
|
+
def called_names(node, names = Set.new)
|
|
54
|
+
return names if node.nil?
|
|
55
|
+
|
|
56
|
+
names << node.name if node.is_a?(Prism::CallNode)
|
|
57
|
+
node.rigor_each_child { |child| called_names(child, names) }
|
|
58
|
+
names
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def collect(node, names, params, rebound)
|
|
62
|
+
return if node.nil?
|
|
63
|
+
|
|
64
|
+
case node
|
|
65
|
+
when *LOCAL_WRITE_NODES then rebound << node.name
|
|
66
|
+
when Prism::BlockNode, Prism::LambdaNode then note_block(node, rebound)
|
|
67
|
+
when Prism::CallNode
|
|
68
|
+
throw ALL, ALL if REFLECTIVE_CALLS.include?(node.name)
|
|
69
|
+
|
|
70
|
+
note_call(node, params, rebound) if names.include?(node.name)
|
|
71
|
+
end
|
|
72
|
+
node.rigor_each_child { |child| collect(child, names, params, rebound) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# A block's `locals` are its parameters, its block-locals and the locals it first assigns; `it` is not
|
|
76
|
+
# among them.
|
|
77
|
+
def note_block(block, rebound)
|
|
78
|
+
rebound.merge(block.locals)
|
|
79
|
+
rebound << :it if block.parameters.is_a?(Prism::ItParametersNode)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def note_call(call, params, rebound)
|
|
83
|
+
args = call.arguments&.arguments || []
|
|
84
|
+
keywords = args.last if args.last.is_a?(Prism::KeywordHashNode)
|
|
85
|
+
positional = keywords ? args[0...-1] : args
|
|
86
|
+
forwarding = args.any?(Prism::ForwardingArgumentsNode)
|
|
87
|
+
note_positionals(params, positional, rebound)
|
|
88
|
+
note_keywords(params, keywords, forwarding, rebound)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Positionals correspond by index up to the first `*splat` (or `...`), which may reach every slot from
|
|
92
|
+
# its own on. The rest parameter is kept only by `*rest` in its own place as the last positional.
|
|
93
|
+
def note_positionals(params, positional, rebound)
|
|
94
|
+
slots = params.requireds + params.optionals
|
|
95
|
+
cut = positional.index { |arg| arg.is_a?(Prism::SplatNode) || arg.is_a?(Prism::ForwardingArgumentsNode) }
|
|
96
|
+
cut ||= positional.size
|
|
97
|
+
slots.each_with_index do |param, index|
|
|
98
|
+
next if param.is_a?(Prism::MultiTargetNode)
|
|
99
|
+
|
|
100
|
+
rebound << param.name unless index < cut && reads?(positional[index], param.name)
|
|
101
|
+
end
|
|
102
|
+
note_rest(params.rest, positional[slots.size..] || [], cut >= slots.size, rebound)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def note_rest(rest, tail, aligned, rebound)
|
|
106
|
+
return unless rest.is_a?(Prism::RestParameterNode) && rest.name
|
|
107
|
+
|
|
108
|
+
splat = tail.first
|
|
109
|
+
kept = aligned && tail.size == 1 && splat.is_a?(Prism::SplatNode) && reads?(splat.expression, rest.name)
|
|
110
|
+
rebound << rest.name unless kept
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Keywords correspond by name. A keyword the call omits falls back to its default, so it is rebound. A
|
|
114
|
+
# `**splat` other than the def's own `**rest`, a non-Symbol key or `...` may carry any keyword, so each of
|
|
115
|
+
# them rebinds every keyword. `**rest` is kept by `**rest` alone, with no key beside it that it would collect.
|
|
116
|
+
def note_keywords(params, keywords, forwarding, rebound)
|
|
117
|
+
names = params.keywords.map { |param| param.name.to_s.delete_suffix(":").to_sym }
|
|
118
|
+
rest_name = keyword_rest_name(params)
|
|
119
|
+
passed, splats, opaque = keyword_entries(keywords)
|
|
120
|
+
opaque ||= forwarding || splats.any? { |value| !reads?(value, rest_name) }
|
|
121
|
+
return rebound.merge(names, [rest_name].compact) if opaque
|
|
122
|
+
|
|
123
|
+
names.each { |name| rebound << name unless reads?(passed[name], name) }
|
|
124
|
+
rebound << rest_name if rest_name && !(splats.size == 1 && (passed.keys - names).empty?)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def keyword_rest_name(params)
|
|
128
|
+
rest = params.keyword_rest
|
|
129
|
+
rest.name if rest.is_a?(Prism::KeywordRestParameterNode)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# The call's keyword hash as `[{ name => value node }, [**splat value nodes], opaque]`, `opaque` when
|
|
133
|
+
# a key is not a plain Symbol.
|
|
134
|
+
def keyword_entries(keywords)
|
|
135
|
+
passed = {}
|
|
136
|
+
splats = []
|
|
137
|
+
opaque = false
|
|
138
|
+
(keywords&.elements || []).each do |element|
|
|
139
|
+
if element.is_a?(Prism::AssocSplatNode)
|
|
140
|
+
splats << element.value
|
|
141
|
+
elsif element.key.is_a?(Prism::SymbolNode) && element.key.unescaped
|
|
142
|
+
passed[element.key.unescaped.to_sym] = element.value
|
|
143
|
+
else
|
|
144
|
+
opaque = true
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
[passed, splats, opaque]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Whether `node` is a bare read of the local `name` (`n`, or the `n:` shorthand's implicit read).
|
|
151
|
+
def reads?(node, name)
|
|
152
|
+
node = node.value if node.is_a?(Prism::ImplicitNode)
|
|
153
|
+
!name.nil? && node.is_a?(Prism::LocalVariableReadNode) && node.name == name
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../../
|
|
3
|
+
require_relative "../../typing_state"
|
|
4
|
+
require_relative "../../memo_audit"
|
|
4
5
|
|
|
5
6
|
module Rigor
|
|
6
7
|
module Inference
|
|
@@ -18,9 +19,16 @@ module Rigor
|
|
|
18
19
|
# the name itself, and no constant write of it. Any other shape answers false, as does a class a
|
|
19
20
|
# `K = Class.new(...)` write names.
|
|
20
21
|
module ConstructedClass
|
|
21
|
-
|
|
22
|
+
# ADR-129 WD1 — a run-scoped Memo keyed on the run-generation token and the environment, whose tables every
|
|
23
|
+
# answer reads and which are fixed for a run.
|
|
24
|
+
MEMO = TypingState.memo(
|
|
25
|
+
:constructed_class,
|
|
26
|
+
run_scoped: true,
|
|
27
|
+
audit: ["constructed class verdict", "constructed class segments", "constructed class names"],
|
|
28
|
+
reason: "verdicts keyed on the run-generation token and environment identities"
|
|
29
|
+
)
|
|
22
30
|
EMPTY = [].freeze
|
|
23
|
-
private_constant :
|
|
31
|
+
private_constant :MEMO, :EMPTY
|
|
24
32
|
|
|
25
33
|
module_function
|
|
26
34
|
|
|
@@ -36,9 +44,19 @@ module Rigor
|
|
|
36
44
|
return false if named.empty?
|
|
37
45
|
|
|
38
46
|
by_class = (table[:verdicts][class_name] ||= {})
|
|
39
|
-
|
|
47
|
+
if by_class.key?(named)
|
|
48
|
+
return MemoAudit.check("constructed class verdict", by_class[named], class_name, named) do
|
|
49
|
+
apart_verdict(scope, named, class_name, memo(scope))
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
by_class[named] = MemoAudit.key_audit("constructed class verdict") do
|
|
54
|
+
apart_verdict(scope, named, class_name, table)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
40
57
|
|
|
41
|
-
|
|
58
|
+
def apart_verdict(scope, named, class_name, table)
|
|
59
|
+
named.all? { |constant| core_class_apart?(scope, constant, class_name.to_s, table) }
|
|
42
60
|
end
|
|
43
61
|
|
|
44
62
|
def core_class_apart?(scope, constant, class_name, table)
|
|
@@ -68,7 +86,18 @@ module Rigor
|
|
|
68
86
|
|
|
69
87
|
# The last segment of every class or module the project declares, reopenings included.
|
|
70
88
|
def declared_segments(scope, table)
|
|
71
|
-
table[:segments]
|
|
89
|
+
segments = table[:segments]
|
|
90
|
+
if segments.nil?
|
|
91
|
+
return table[:segments] = MemoAudit.key_audit("constructed class segments") do
|
|
92
|
+
compute_declared_segments(scope)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
MemoAudit.check("constructed class segments", segments, :segments) { compute_declared_segments(scope) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def compute_declared_segments(scope)
|
|
100
|
+
declared_names(scope).each_key.to_set { |key| key.to_s.split("::").last }.freeze
|
|
72
101
|
end
|
|
73
102
|
|
|
74
103
|
# The constants `name`'s superclass expression names, or an empty Array; memoised per envelope bucket,
|
|
@@ -78,15 +107,18 @@ module Rigor
|
|
|
78
107
|
return EMPTY if bucket.nil?
|
|
79
108
|
|
|
80
109
|
names = table[:names]
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
110
|
+
if names.key?(bucket)
|
|
111
|
+
return MemoAudit.check("constructed class names", names[bucket], name) { bucket_constructed_names(bucket) }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
names[bucket] = MemoAudit.key_audit("constructed class names") { bucket_constructed_names(bucket) }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def bucket_constructed_names(bucket)
|
|
118
|
+
return EMPTY if bucket.key?(Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK)
|
|
119
|
+
|
|
120
|
+
kind = Scope::DiscoveryIndex::ENVELOPE_CONSTRUCTED_KIND
|
|
121
|
+
bucket.each_key.filter_map { |key| key[1] if key.is_a?(Array) && key[0] == kind }.freeze
|
|
90
122
|
end
|
|
91
123
|
|
|
92
124
|
# The run-wide table of declared classes: the seeded `discovered_class_sources`, or a single-file run's
|
|
@@ -100,15 +132,21 @@ module Rigor
|
|
|
100
132
|
# `discovered_def_nodes`, as `ExpressionTyper` falls back) and the environment: every table read here is
|
|
101
133
|
# fixed for a run, so each answer is computed once a run, and a new run replaces the slot.
|
|
102
134
|
def memo(scope)
|
|
103
|
-
|
|
135
|
+
return new_table if MemoAudit.enabled? && MemoAudit.suspended?
|
|
136
|
+
|
|
137
|
+
state = TypingState.current
|
|
138
|
+
slot = state[MEMO]
|
|
104
139
|
generation = scope.run_generation || scope.discovered_def_nodes
|
|
105
140
|
environment = scope.environment
|
|
106
141
|
unless slot && slot[0].equal?(generation) && slot[1].equal?(environment)
|
|
107
|
-
slot = [generation, environment,
|
|
108
|
-
|
|
142
|
+
slot = [generation, environment, new_table]
|
|
143
|
+
state[MEMO] = slot
|
|
109
144
|
end
|
|
110
145
|
slot[2]
|
|
111
146
|
end
|
|
147
|
+
|
|
148
|
+
# Empty tables: a new slot's, and a throwaway set while an audit recomputation bypasses the memo.
|
|
149
|
+
def new_table = { names: {}.compare_by_identity, verdicts: {} }
|
|
112
150
|
end
|
|
113
151
|
end
|
|
114
152
|
end
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../builtins/cruby_definers"
|
|
4
4
|
require_relative "constructed_class"
|
|
5
|
-
require_relative "../../
|
|
5
|
+
require_relative "../../typing_state"
|
|
6
|
+
require_relative "../../memo_audit"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
module Inference
|
|
@@ -307,30 +308,54 @@ module Rigor
|
|
|
307
308
|
|
|
308
309
|
def unknown_superclass_classes(scope, class_name)
|
|
309
310
|
Analysis::DependencyRecorder.read_name(:subclass_of, "*") if Analysis::DependencyRecorder.active?
|
|
310
|
-
|
|
311
|
-
all = table[:unknown] ||= begin
|
|
312
|
-
mark = Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK
|
|
313
|
-
computed = scope.discovered_superclasses.filter_map { |child, parent| child if parent.nil? }
|
|
314
|
-
meta = scope.discovered_parameter_envelopes.filter_map { |name, bucket| name if bucket.key?(mark) }
|
|
315
|
-
(computed + meta).uniq.freeze
|
|
316
|
-
end
|
|
311
|
+
all = all_unknown_superclass_classes(scope)
|
|
317
312
|
name = class_name.to_s
|
|
318
313
|
all.include?(name) ? all - [name] : all
|
|
319
314
|
end
|
|
320
315
|
|
|
321
|
-
|
|
322
|
-
|
|
316
|
+
def all_unknown_superclass_classes(scope)
|
|
317
|
+
table = subclass_memo(scope)
|
|
318
|
+
stored = table[:unknown]
|
|
319
|
+
if stored.nil?
|
|
320
|
+
return table[:unknown] = MemoAudit.key_audit("own method unknown superclasses") do
|
|
321
|
+
compute_unknown_superclass_classes(scope)
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
MemoAudit.check("own method unknown superclasses", stored, :unknown) do
|
|
326
|
+
compute_unknown_superclass_classes(scope)
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def compute_unknown_superclass_classes(scope)
|
|
331
|
+
mark = Scope::DiscoveryIndex::ENVELOPE_META_SUPERCLASS_MARK
|
|
332
|
+
computed = scope.discovered_superclasses.filter_map { |child, parent| child if parent.nil? }
|
|
333
|
+
meta = scope.discovered_parameter_envelopes.filter_map { |name, bucket| name if bucket.key?(mark) }
|
|
334
|
+
(computed + meta).uniq.freeze
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# ADR-129 WD1 — a run-scoped Memo keyed on the identities of the discovery index and environment, the only
|
|
338
|
+
# inputs of the buckets it holds.
|
|
339
|
+
SUBCLASS_MEMO = TypingState.memo(
|
|
340
|
+
:own_method_subclasses,
|
|
341
|
+
run_scoped: true, audit: ["own method unknown superclasses", "own method children"],
|
|
342
|
+
reason: "subclass buckets keyed on the discovery index and environment identities"
|
|
343
|
+
)
|
|
344
|
+
private_constant :SUBCLASS_MEMO
|
|
323
345
|
|
|
324
346
|
# One thread-local slot keyed on the scope's discovery index (a frozen `Data`) and environment, the only
|
|
325
347
|
# inputs `children_by_parent` and the unknown-superclass list read; neither files a dependency edge
|
|
326
348
|
# (`each_subclass` files its own `subclass_of:` edges on every call), so a hit drops none.
|
|
327
349
|
def subclass_memo(scope)
|
|
328
|
-
|
|
350
|
+
return {} if MemoAudit.enabled? && MemoAudit.suspended?
|
|
351
|
+
|
|
352
|
+
state = TypingState.current
|
|
353
|
+
slot = state[SUBCLASS_MEMO]
|
|
329
354
|
discovery = scope.discovery
|
|
330
355
|
environment = scope.environment
|
|
331
356
|
unless slot && slot[0].equal?(discovery) && slot[1].equal?(environment)
|
|
332
357
|
slot = [discovery, environment, {}]
|
|
333
|
-
|
|
358
|
+
state[SUBCLASS_MEMO] = slot
|
|
334
359
|
end
|
|
335
360
|
slot[2]
|
|
336
361
|
end
|
|
@@ -342,11 +367,20 @@ module Rigor
|
|
|
342
367
|
end
|
|
343
368
|
|
|
344
369
|
def children_by_parent(scope)
|
|
345
|
-
subclass_memo(scope)
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
370
|
+
table = subclass_memo(scope)
|
|
371
|
+
stored = table[:children]
|
|
372
|
+
if stored.nil?
|
|
373
|
+
return table[:children] = MemoAudit.key_audit("own method children") { compute_children_by_parent(scope) }
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
MemoAudit.check("own method children", stored, :children) { compute_children_by_parent(scope) }
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def compute_children_by_parent(scope)
|
|
380
|
+
scope.discovered_superclasses.each_with_object({}) do |(child, raw_parent), children|
|
|
381
|
+
parent = scope.ancestor_name_candidates(child, raw_parent).find { |name| known_class?(scope, name) }
|
|
382
|
+
(children[parent] ||= []) << child if parent
|
|
383
|
+
end.each_value(&:freeze).freeze
|
|
350
384
|
end
|
|
351
385
|
|
|
352
386
|
def known_class?(scope, name)
|
|
@@ -126,6 +126,19 @@ module Rigor
|
|
|
126
126
|
out
|
|
127
127
|
end
|
|
128
128
|
|
|
129
|
+
# ADR-121 WD7 — a names-wildcard row counts as a refinement of every name. True when a row `list` puts in
|
|
130
|
+
# effect names `method_name` (or the wildcard) for ANY refined class — which class the row refines is not
|
|
131
|
+
# decided here — or `list` carries {InEffectRefinements::UNKNOWN} and any row names it. {ExpressionTyper}
|
|
132
|
+
# asks it for a `&:name` block, whose receiver is not known; {SelfPreservingBlock} for the yielding method
|
|
133
|
+
# (issue #2909).
|
|
134
|
+
def refined_anywhere?(scope, method_name, list)
|
|
135
|
+
refinements = scope.discovered_refinements
|
|
136
|
+
return true if list.include?(InEffectRefinements::UNKNOWN) &&
|
|
137
|
+
refinements.any? { |_refined, methods| methods.key?(method_name) || methods.key?(wildcard) }
|
|
138
|
+
|
|
139
|
+
!targets(refinements, method_name, list).nil? || !targets(refinements, wildcard, list).nil?
|
|
140
|
+
end
|
|
141
|
+
|
|
129
142
|
# The refinement table records every name a `refine` argument's spelling can denote (`refine String` inside
|
|
130
143
|
# `module M` records `M::String` and `String`), which only withholds a check. To type, a key must be the class
|
|
131
144
|
# Ruby's lexical lookup resolves: one the project or RBS knows, with no longer spelling of the same last segment
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "../type"
|
|
6
|
+
require_relative "../typing_state"
|
|
6
7
|
|
|
7
8
|
module Rigor
|
|
8
9
|
module Inference
|
|
@@ -26,8 +27,13 @@ module Rigor
|
|
|
26
27
|
IVAR_NAME = /\A@[A-Za-z_]\w*\z/
|
|
27
28
|
private_constant :IVAR_NAME
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
# ADR-129 WD1 — a Frame: the depth is installed for a region and restored on the way out.
|
|
31
|
+
REPEATING = TypingState.frame(
|
|
32
|
+
:reflective_ivar_write_repeating,
|
|
33
|
+
reason: "the repeat depth of `for` bodies and loop predicates, installed per region",
|
|
34
|
+
witness: "spec/fixtures/typing_state_witnesses/reflective_ivar_write_repeating.rb"
|
|
35
|
+
)
|
|
36
|
+
private_constant :REPEATING
|
|
31
37
|
|
|
32
38
|
module_function
|
|
33
39
|
|
|
@@ -69,18 +75,20 @@ module Rigor
|
|
|
69
75
|
# rebind fixpoint, and a predicate runs before every iteration but is walked once, so a literal-name write there
|
|
70
76
|
# must not replace the binding with one pass's value ({.apply}). A `while` / `until` body does not enter it: its
|
|
71
77
|
# fixpoint widens the ivar (issue #1930).
|
|
72
|
-
def in_repeating_body
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
def in_repeating_body(&)
|
|
79
|
+
state = TypingState.current
|
|
80
|
+
state.with(REPEATING, (state[REPEATING] || 0) + 1, &)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Runs the block at repeat depth zero, restoring the depth on exit. A user-method body re-typed for its return
|
|
84
|
+
# starts from its own scope and reads its own write, so a caller's `for` body does not repeat it
|
|
85
|
+
# ({ExpressionTyper.with_call_site_frame}).
|
|
86
|
+
def outside_repeating_body(&)
|
|
87
|
+
TypingState.current.with(REPEATING, 0, &)
|
|
80
88
|
end
|
|
81
89
|
|
|
82
90
|
def repeating_body?
|
|
83
|
-
(
|
|
91
|
+
(TypingState.current[REPEATING] || 0).positive?
|
|
84
92
|
end
|
|
85
93
|
|
|
86
94
|
# `scope` after `node` runs. A literal name binds that ivar to the value's type (yielded), as `@n = v` does.
|