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
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module TypeOfMemo
|
|
5
|
+
# The counters and mismatch samples of this process, behind a mutex: process counters like {MemoAudit}'s, never
|
|
6
|
+
# read back into a type (ADR-129 WD5).
|
|
7
|
+
class Stats
|
|
8
|
+
def initialize
|
|
9
|
+
@mutex = Mutex.new
|
|
10
|
+
@counts = Hash.new { |hash, phase| hash[phase] = Hash.new(0) }
|
|
11
|
+
@stale = Hash.new { |hash, phase| hash[phase] = Hash.new(0) }
|
|
12
|
+
@samples = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def count(phase, counter) = @mutex.synchronize { @counts[phase][counter] += 1 }
|
|
16
|
+
|
|
17
|
+
def stale_miss(phase, names) = @mutex.synchronize { @stale[phase][names] += 1 }
|
|
18
|
+
|
|
19
|
+
def sample(limit)
|
|
20
|
+
@mutex.synchronize { @samples << yield if @samples.size < limit }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def counts = @mutex.synchronize { copy(@counts) }
|
|
24
|
+
|
|
25
|
+
def stale = @mutex.synchronize { copy(@stale) }
|
|
26
|
+
|
|
27
|
+
def samples = @mutex.synchronize { @samples.dup }
|
|
28
|
+
|
|
29
|
+
def reset
|
|
30
|
+
@mutex.synchronize do
|
|
31
|
+
@counts.clear
|
|
32
|
+
@stale.clear
|
|
33
|
+
@samples.clear
|
|
34
|
+
end
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def copy(by_phase) = by_phase.to_h { |phase, counters| [phase, counters.dup] }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "prism"
|
|
5
|
+
require_relative "typing_state"
|
|
6
|
+
require_relative "memo_audit"
|
|
7
|
+
require_relative "type_of_memo/stats"
|
|
8
|
+
require_relative "type_of_memo/serving"
|
|
9
|
+
|
|
10
|
+
module Rigor
|
|
11
|
+
# ADR-129 stage 3 — the `ExpressionTyper#type_of` result memo in **shadow** form (WD5's G1, stage-3 form). It keys a
|
|
12
|
+
# computation on `(scope identity, node identity, state id)` (WD2), opens a journal capture around it, and stores
|
|
13
|
+
# what an unrefused computation read and wrote (WD3). It never serves anything: on a would-be hit, an entry whose read
|
|
14
|
+
# slice still holds, production computes as usual inside a priors window, and G1 checks afterwards that
|
|
15
|
+
# - the type equals the entry's;
|
|
16
|
+
# - the live tables equal their pre-computation state with the entry's write slice replayed;
|
|
17
|
+
# - the live computation touched no Resource.
|
|
18
|
+
# A read slice that no longer holds is a miss, not a mismatch, counted by the slots whose reads went stale, which
|
|
19
|
+
# measures what the journal costs in hits (#2853 for the set-only Tables). Output is production's by construction.
|
|
20
|
+
#
|
|
21
|
+
# Like {MemoAudit}, a process switch (`RIGOR_TYPE_OF_SHADOW=1`, read once at load, or {enable!}), a single boolean
|
|
22
|
+
# check when off. With `RIGOR_TYPE_OF_SHADOW_REPORT=path`, each process appends one JSON line of its counters and
|
|
23
|
+
# mismatch samples to `path` when it exits. The memo is shut while a tracer is attached or a typer carries
|
|
24
|
+
# instance state (`@tracer`, `@operand_types`: WD1's bypass), and leaf nodes are never stored (WD3).
|
|
25
|
+
module TypeOfMemo
|
|
26
|
+
# ADR-129 WD4 — the per-file table, a Memo by kind: entries keyed on the state id, which carries every Frame
|
|
27
|
+
# (WD2), and checked by G1 rather than by `MemoAudit.check` (its hits recompute in production already).
|
|
28
|
+
TABLE = TypingState.memo(
|
|
29
|
+
:type_of_memo,
|
|
30
|
+
run_scoped: true,
|
|
31
|
+
reason: "the type_of memo's per-file entries, keyed on scope, node and state id; G1 checks every would-be hit"
|
|
32
|
+
)
|
|
33
|
+
private_constant :TABLE
|
|
34
|
+
|
|
35
|
+
# WD3 — never stored: their typing is a read the memo could only repeat.
|
|
36
|
+
LEAVES = %w[
|
|
37
|
+
LocalVariableReadNode InstanceVariableReadNode ClassVariableReadNode GlobalVariableReadNode SelfNode
|
|
38
|
+
IntegerNode FloatNode RationalNode ImaginaryNode StringNode SymbolNode NilNode TrueNode FalseNode
|
|
39
|
+
RegularExpressionNode SourceFileNode SourceLineNode SourceEncodingNode ConstantReadNode ConstantPathNode
|
|
40
|
+
NumberedReferenceReadNode BackReferenceReadNode
|
|
41
|
+
].to_h { |name| [Prism.const_get(name), true] }.freeze
|
|
42
|
+
private_constant :LEAVES
|
|
43
|
+
|
|
44
|
+
# The Tables whose reads only record a filing a later reader never consumes (#2853).
|
|
45
|
+
SET_ONLY = %i[dependency_reads self_call_misses].freeze
|
|
46
|
+
|
|
47
|
+
SAMPLE_LIMIT = 50
|
|
48
|
+
private_constant :SAMPLE_LIMIT
|
|
49
|
+
|
|
50
|
+
# One file's entries, `scope => node => state id => entry`, and the phase its analysis is in.
|
|
51
|
+
class Table
|
|
52
|
+
attr_accessor :phase
|
|
53
|
+
attr_reader :entries
|
|
54
|
+
|
|
55
|
+
# Whether the table keys on the ELIDE id ({TypingState::ElideId}) rather than the state id.
|
|
56
|
+
attr_reader :elide
|
|
57
|
+
|
|
58
|
+
# Recomputations of G1's stage-4 form open on this table ({TypeOfMemo.enable!} with `:serve`), during which the
|
|
59
|
+
# memo is suspended.
|
|
60
|
+
attr_accessor :suspended
|
|
61
|
+
|
|
62
|
+
def initialize(elide: false, phase: :other)
|
|
63
|
+
@entries = {}.compare_by_identity
|
|
64
|
+
@phase = phase
|
|
65
|
+
@elide = elide
|
|
66
|
+
@suspended = 0
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
@enabled = %w[1 true].include?(ENV["RIGOR_TYPE_OF_SHADOW"].to_s.downcase)
|
|
71
|
+
@stats = Stats.new
|
|
72
|
+
|
|
73
|
+
class << self
|
|
74
|
+
include Serving
|
|
75
|
+
|
|
76
|
+
def enabled? = @enabled
|
|
77
|
+
|
|
78
|
+
# Turns the shadow on. `serve: true` is G1's stage-4 form (ADR-129 WD5), for specs and audit tools only: a
|
|
79
|
+
# would-be hit is recomputed with the memo suspended, the recomputation's Table writes are undone, and the entry
|
|
80
|
+
# is served (validated, its reads merged upward, its writes replayed), so output depends on the memo.
|
|
81
|
+
def enable!(serve: false) = (@enabled = serve ? :serve : true)
|
|
82
|
+
|
|
83
|
+
def disable! = (@enabled = false)
|
|
84
|
+
|
|
85
|
+
# `{phase => {counter => n}}`, the counters since the last {reset}.
|
|
86
|
+
def counts = @stats.counts
|
|
87
|
+
|
|
88
|
+
# `{phase => {slot names => n}}`: the would-be hits whose read slice did not hold, by the slots that went stale.
|
|
89
|
+
def stale = @stats.stale
|
|
90
|
+
|
|
91
|
+
# The first mismatches seen since the last {reset}, each a Hash.
|
|
92
|
+
def samples = @stats.samples
|
|
93
|
+
|
|
94
|
+
# The number of mismatches since the last {reset}, all phases.
|
|
95
|
+
def mismatches = counts.sum { |_, counters| counters[:mismatch] }
|
|
96
|
+
|
|
97
|
+
def reset = @stats.reset
|
|
98
|
+
|
|
99
|
+
# A file's analysis starts: a fresh table, in the main pass. A no-op when off. With
|
|
100
|
+
# `RIGOR_TYPE_OF_SHADOW_ELIDE=1` (ADR-129 WD2's ELIDE, measured in stage 3), the table keys on the thread's ELIDE
|
|
101
|
+
# id, restarted here. The state is extended at the first file's start: every frame open then closes after that
|
|
102
|
+
# file ends (the per-file analysis is nested in it), so none exits through the plain `State#with`, which would
|
|
103
|
+
# not restore the ELIDE id.
|
|
104
|
+
def begin_file
|
|
105
|
+
return unless @enabled && Ractor.main?
|
|
106
|
+
|
|
107
|
+
elide = %w[1 true].include?(ENV["RIGOR_TYPE_OF_SHADOW_ELIDE"].to_s.downcase)
|
|
108
|
+
table = Table.new(elide: elide, phase: :main)
|
|
109
|
+
state = TypingState.current
|
|
110
|
+
if elide
|
|
111
|
+
state.extend(TypingState::ElideId) unless state.is_a?(TypingState::ElideId)
|
|
112
|
+
state.restart_elide_id
|
|
113
|
+
end
|
|
114
|
+
state[TABLE] = table
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# The file's analysis moved on to `phase` (`:rule` once the main pass has indexed it).
|
|
119
|
+
def phase!(phase)
|
|
120
|
+
return unless @enabled
|
|
121
|
+
|
|
122
|
+
table = TypingState.current[TABLE]
|
|
123
|
+
table.phase = phase if table
|
|
124
|
+
nil
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# A file's analysis ended: its entries go (WD4: the table is per file).
|
|
128
|
+
def end_file
|
|
129
|
+
return unless @enabled
|
|
130
|
+
|
|
131
|
+
state = TypingState.current
|
|
132
|
+
state[TABLE] = nil
|
|
133
|
+
nil
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# The block's value, the type `node` has in `scope`, computed by production under G1's shadow. Typing outside a
|
|
137
|
+
# file's analysis ({begin_file} .. {end_file}: a pre-pass, a language-server query, a non-main Ractor) has no
|
|
138
|
+
# table and is not shadowed.
|
|
139
|
+
# `callee` is whether the typing runs in a callee body (the ADR-84 guard stack is not empty).
|
|
140
|
+
def shadow(state, scope, node, callee: false, &)
|
|
141
|
+
return yield if LEAVES[node.class]
|
|
142
|
+
|
|
143
|
+
table = state[TABLE]
|
|
144
|
+
return yield if table.nil? || table.suspended.positive?
|
|
145
|
+
|
|
146
|
+
# G1′: the memo must not run while a `memo_shut_while_installed` Frame is installed.
|
|
147
|
+
MemoAudit.note_memo_lookup(state) if MemoAudit.enabled?
|
|
148
|
+
|
|
149
|
+
phase = callee ? :callee : table.phase
|
|
150
|
+
by_id = (table.entries[scope] ||= {}.compare_by_identity)[node] ||= {}
|
|
151
|
+
id = table.elide ? state.elide_id : state.id
|
|
152
|
+
entry = by_id[id]
|
|
153
|
+
count(phase, :lookups)
|
|
154
|
+
if entry && stale_names(state, phase, entry).nil?
|
|
155
|
+
return serve_hit(state, table, phase, scope, node, entry, &) if @enabled == :serve
|
|
156
|
+
|
|
157
|
+
return check_hit(state, phase, scope, node, entry, &)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
compute_and_store(state, phase, by_id, id, &)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
# A miss: compute under a capture and store an unrefused, unraised result (WD3).
|
|
166
|
+
def compute_and_store(state, phase, by_id, id)
|
|
167
|
+
state.open_capture
|
|
168
|
+
returned = false
|
|
169
|
+
begin
|
|
170
|
+
type = yield
|
|
171
|
+
returned = true
|
|
172
|
+
ensure
|
|
173
|
+
refused, records = state.close_capture
|
|
174
|
+
count(phase, :raised) unless returned
|
|
175
|
+
end
|
|
176
|
+
if refused
|
|
177
|
+
count(phase, :refused)
|
|
178
|
+
else
|
|
179
|
+
reads, writes = state.slices(records)
|
|
180
|
+
by_id[id] = reads.empty? && writes.empty? ? type : [type, reads, writes].freeze
|
|
181
|
+
count(phase, :stored)
|
|
182
|
+
end
|
|
183
|
+
type
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# nil when the entry's read slice holds; else counts the stale slots and answers their names.
|
|
187
|
+
def stale_names(state, phase, entry)
|
|
188
|
+
return nil unless entry.is_a?(Array)
|
|
189
|
+
|
|
190
|
+
reads = entry[1]
|
|
191
|
+
return nil if state.stale_read(reads).nil?
|
|
192
|
+
|
|
193
|
+
names = state.stale_slots(reads).map { |slot| TypingState.slots[slot].name }.sort.freeze
|
|
194
|
+
count(phase, :stale)
|
|
195
|
+
count(phase, :stale_set_only) if (names - SET_ONLY).empty?
|
|
196
|
+
@stats.stale_miss(phase, names.join(","))
|
|
197
|
+
names
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# A would-be hit: production computes, and G1 compares it with what serving the entry would have done.
|
|
201
|
+
def check_hit(state, phase, scope, node, entry)
|
|
202
|
+
stored, writes = entry.is_a?(Array) ? [entry[0], entry[2]] : [entry, EMPTY]
|
|
203
|
+
count(phase, :checked)
|
|
204
|
+
state.open_capture
|
|
205
|
+
state.open_priors
|
|
206
|
+
begin
|
|
207
|
+
fresh = yield
|
|
208
|
+
ensure
|
|
209
|
+
priors = state.close_priors
|
|
210
|
+
refused, = state.close_capture
|
|
211
|
+
end
|
|
212
|
+
judge(state, phase, scope, node, stored, fresh, writes, priors, refused)
|
|
213
|
+
fresh
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
EMPTY = [].freeze
|
|
217
|
+
private_constant :EMPTY
|
|
218
|
+
|
|
219
|
+
# G1's verdict on one would-be hit. An error in the comparison itself is counted as a mismatch rather than raised:
|
|
220
|
+
# the typing path's rescues would turn a raise into a different answer.
|
|
221
|
+
def judge(state, phase, scope, node, stored, fresh, writes, priors, refused) # rubocop:disable Metrics/ParameterLists
|
|
222
|
+
compare(state, phase, scope, node, stored, fresh, writes, priors, refused)
|
|
223
|
+
rescue StandardError => e
|
|
224
|
+
count(phase, :mismatch)
|
|
225
|
+
count(phase, :mismatch_judge_error)
|
|
226
|
+
sample(phase, scope, node, [:judge_error, "#{e.class}: #{e.message[0, 120]}"], stored, fresh, [])
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def compare(state, phase, scope, node, stored, fresh, writes, priors, refused) # rubocop:disable Metrics/ParameterLists
|
|
230
|
+
kinds = []
|
|
231
|
+
kinds << :type unless same?(fresh, stored)
|
|
232
|
+
kinds << :resource if refused
|
|
233
|
+
diffs = table_diffs(state, writes, priors)
|
|
234
|
+
kinds << :table unless diffs.empty?
|
|
235
|
+
return if kinds.empty?
|
|
236
|
+
|
|
237
|
+
report(phase, scope, node, kinds, stored, fresh, diffs)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def report(phase, scope, node, kinds, stored, fresh, diffs)
|
|
241
|
+
count(phase, :mismatch)
|
|
242
|
+
kinds.each { |kind| count(phase, :"mismatch_#{kind}") if kind.is_a?(Symbol) }
|
|
243
|
+
sample(phase, scope, node, kinds, stored, fresh, diffs)
|
|
244
|
+
nil
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# The entries where the live tables differ from their pre-computation state with `writes` replayed: each key
|
|
248
|
+
# the entry wrote must hold its last written value, and each key only the live computation wrote must hold
|
|
249
|
+
# the value it replaced.
|
|
250
|
+
def table_diffs(state, writes, priors)
|
|
251
|
+
expected = TypingState::KeyIndex.new
|
|
252
|
+
diffs = []
|
|
253
|
+
# The last write of a key is what a replay leaves there: walk backwards and take each key once.
|
|
254
|
+
writes.each_slice(3).reverse_each do |slot, key, value|
|
|
255
|
+
next if expected.key?(slot, key)
|
|
256
|
+
|
|
257
|
+
expected.store(slot, key, value)
|
|
258
|
+
now = state.entry(slot, key)
|
|
259
|
+
diffs << [slot, key, value, now] unless same?(value, now)
|
|
260
|
+
end
|
|
261
|
+
priors.each_slice(3) do |slot, key, prior|
|
|
262
|
+
next if expected.key?(slot, key)
|
|
263
|
+
|
|
264
|
+
now = state.entry(slot, key)
|
|
265
|
+
diffs << [slot, key, prior, now] unless same?(prior, now)
|
|
266
|
+
end
|
|
267
|
+
diffs.uniq { |slot, key, _, _| [slot, key.is_a?(Array) ? key.map(&:__id__) : key.__id__] }
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Entry values compare by identity, or by `==` element-wise, a scope (which the one-slot caches record) by
|
|
271
|
+
# identity alone: `Scope#==` ignores fields typing reads.
|
|
272
|
+
#
|
|
273
|
+
# `==` ignores a Hash's order, which output shows: a type also compares its rendering
|
|
274
|
+
# (`HashShape#==` compares its pairs as a Hash), and a Hash its entries in order.
|
|
275
|
+
def same?(left, right)
|
|
276
|
+
return true if left.equal?(right)
|
|
277
|
+
return false if left.is_a?(Scope) || right.is_a?(Scope)
|
|
278
|
+
return ordered_same?(left.to_a, right.to_a) if left.is_a?(Hash) && right.is_a?(Hash)
|
|
279
|
+
return ordered_same?(left, right) if left.is_a?(Array) && right.is_a?(Array)
|
|
280
|
+
return false unless left == right
|
|
281
|
+
|
|
282
|
+
!left.respond_to?(:describe) || !right.respond_to?(:describe) || left.describe(:short) == right.describe(:short)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def ordered_same?(left, right)
|
|
286
|
+
left.size == right.size && left.each_index.all? { |index| same?(left[index], right[index]) }
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def count(phase, counter) = @stats.count(phase, counter)
|
|
290
|
+
|
|
291
|
+
def sample(phase, scope, node, kinds, stored, fresh, diffs)
|
|
292
|
+
@stats.sample(SAMPLE_LIMIT) do
|
|
293
|
+
{
|
|
294
|
+
phase: phase, kinds: kinds, path: scope.source_path.to_s, line: node.location.start_line,
|
|
295
|
+
node: node.class.name.delete_prefix("Prism::"),
|
|
296
|
+
stored: describe(stored), fresh: describe(fresh),
|
|
297
|
+
diffs: diffs.first(4).map do |slot, key, want, now|
|
|
298
|
+
[TypingState.slots[slot].name, describe(key), describe(want), describe(now)]
|
|
299
|
+
end
|
|
300
|
+
}
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def describe(value)
|
|
305
|
+
return "ABSENT" if TypingState::ABSENT.equal?(value)
|
|
306
|
+
return value.describe(:short).to_s[0, 160] if value.respond_to?(:describe)
|
|
307
|
+
|
|
308
|
+
value.inspect[0, 160]
|
|
309
|
+
rescue StandardError => e
|
|
310
|
+
"<describe failed: #{e.class}>"
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
public
|
|
314
|
+
|
|
315
|
+
# A forked child starts: the counters it inherited are the parent's, which the parent reports.
|
|
316
|
+
def forked
|
|
317
|
+
reset if @enabled
|
|
318
|
+
nil
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Appends this process's report to `RIGOR_TYPE_OF_SHADOW_REPORT`, when set (one JSON line). Runs at exit, and a
|
|
322
|
+
# forked worker calls it before its `exit!`, which skips `at_exit`.
|
|
323
|
+
def write_report
|
|
324
|
+
path = ENV.fetch("RIGOR_TYPE_OF_SHADOW_REPORT", nil)
|
|
325
|
+
return unless @enabled && path
|
|
326
|
+
|
|
327
|
+
line = JSON.generate(pid: Process.pid, counts: counts, stale: stale, samples: samples)
|
|
328
|
+
File.open(path, "a") { |file| file.puts(line) }
|
|
329
|
+
rescue StandardError => e
|
|
330
|
+
# A report that cannot be written must not keep a forked worker from its `exit!`, nor change the exit status.
|
|
331
|
+
warn("rigor: RIGOR_TYPE_OF_SHADOW_REPORT not written: #{e.class}: #{e.message}")
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
at_exit { send(:write_report) }
|
|
336
|
+
end
|
|
337
|
+
end
|