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,1094 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
# ADR-129 WD1–WD3 — the one owner of the mutable state typing reads and writes.
|
|
5
|
+
#
|
|
6
|
+
# Every piece of that state is a **slot** declared here with one of four kinds, where its owner names it
|
|
7
|
+
# (`RETURN_SINK = TypingState.frame(:return_sink, …)`), and a deny-by-default static spec
|
|
8
|
+
# (`spec/rigor/typing_state_closure_spec.rb`, ADR-129's G3) rejects any other state in a file it
|
|
9
|
+
# covers. The kind decides how a future `type_of` memo treats the slot, so the memo needs no knowledge of any
|
|
10
|
+
# particular one:
|
|
11
|
+
#
|
|
12
|
+
# - **Frame** ({.frame}): an input installed for a dynamic extent with {State#with}, restored exactly on the way
|
|
13
|
+
# out, and frozen while installed. A frame whose installed object is a container mutated in place (a jump sink,
|
|
14
|
+
# the key-presence walk) declares `contents: :resource`: the installation is the Frame, and each mutation of
|
|
15
|
+
# the contents is a Resource touch ({State#touch}, {State#rewrite}). The installed value may also hold
|
|
16
|
+
# extensional caches its owner fills in place: those are Memos by kind and take no touch.
|
|
17
|
+
# - **Table** ({.table}): a keyed store typing writes and may read back. Its reads and writes are logged to the
|
|
18
|
+
# journal ({State#log_read}, {State#log_write}) while a capture is open. A table that answers what reading
|
|
19
|
+
# ABSENT would have computed declares `read_through: true` (WD1). A table whose stores are held elsewhere, one per
|
|
20
|
+
# owner (the advisory tables each file's scopes share, an installed recorder), declares `bound: true` and logs
|
|
21
|
+
# through {State#log_bound_read} / {State#log_bound_write}, which name the store beside the key.
|
|
22
|
+
# - **Resource** ({.resource}): state consumed or rewritten in place, or read as a delta. A write refuses every
|
|
23
|
+
# open capture (the `any` rule, WD3); so does a read, unless the slot declares one of WD3's two read exemptions
|
|
24
|
+
# (`:id_determined`, `:delta_internal`).
|
|
25
|
+
# - **Memo** ({.memo}): an extensional cache keyed on all its inputs. Unlogged.
|
|
26
|
+
#
|
|
27
|
+
# Typing also reaches state that is not a slot value: the instance variables of the objects a scope holds. ADR-129's
|
|
28
|
+
# G4 (`spec/rigor/typing_state_reachability_spec.rb`) walks everything reachable from a scope and the slot values,
|
|
29
|
+
# and accepts a mutable object only where a declaration names it: an extensional cache an object fills in place
|
|
30
|
+
# ({.memo_field}), or the store of a bound Table ({.store_field}).
|
|
31
|
+
#
|
|
32
|
+
# The values live in one {State} per thread (per fiber, as `Thread.current[]` is), cached by the typer and the
|
|
33
|
+
# evaluator, so a slot read is an ivar and an array index. Each slot constant is the slot's integer index, which
|
|
34
|
+
# keeps the declaration Ractor-shareable and the read an `Array#[]`.
|
|
35
|
+
#
|
|
36
|
+
# Stage 1 (ADR-129 WD6) adds no caching: no capture is opened outside the specs, so the journal stays empty and
|
|
37
|
+
# every touch is a size check.
|
|
38
|
+
module TypingState
|
|
39
|
+
KINDS = %i[frame table resource memo].freeze
|
|
40
|
+
READ_EXEMPTIONS = %i[id_determined delta_internal].freeze
|
|
41
|
+
# ADR-129 G1′ — the four things a Frame declares about how typing meets it, exactly one each ({.frame}).
|
|
42
|
+
G1_KINDS = %i[witness typing_unreachable typing_never_reads memo_shut_while_installed].freeze
|
|
43
|
+
|
|
44
|
+
# One declared slot: its name, kind and index, why it is the kind it is, and its kind's options.
|
|
45
|
+
# - `run_scoped`: a Table, Memo or Resource whose contents belong to one analysis run, emptied by
|
|
46
|
+
# {.release_run_scoped} when the run ends ({RunScopedSlots.release}).
|
|
47
|
+
# - `read_exemption`: a Resource, or a `contents: :resource` Frame's contents, whose reads do not refuse (WD3).
|
|
48
|
+
# - `contents`: `:resource` for a Frame whose installed container is mutated in place, `:table` for one whose
|
|
49
|
+
# installed container is a store of a bound Table, whose entries are journaled rather than touched.
|
|
50
|
+
# - `read_through`: a Table that is a read-through cache of the computation's own value (WD1).
|
|
51
|
+
# - `bound`: a Table whose stores are not the slot's value but objects its owner holds, several at once (a store
|
|
52
|
+
# per file's scopes, per installed recorder); its journal records key on `[store, key]`.
|
|
53
|
+
# - `audit`: a Memo's `MemoAudit.check` labels, the hit sites that recompute and compare (WD1, G2).
|
|
54
|
+
# - `memos`: a Frame's members or keys that hold Memos its owner fills in place (G4 accepts them).
|
|
55
|
+
# - `computed`: a Table filled by a computation (the ADR-84 return memo), the `MemoAudit.key_audit` label of its
|
|
56
|
+
# miss (WD1's key audit, G2).
|
|
57
|
+
# - `entries`: a Table's entry protocol ({StoreEntries}), how the journal reads and writes one of its entries.
|
|
58
|
+
# - `declaration`: a Frame's G1′ declaration (ADR-129 WD5), a frozen `[kind, argument]` pair: `[:witness, path]` (a
|
|
59
|
+
# repo-relative fixture on which G1 must fail with the slot ignored), or `[:typing_unreachable, true]`,
|
|
60
|
+
# `[:typing_never_reads, true]` or `[:memo_shut_while_installed, true]` ({FrameAudit} asserts the first two under
|
|
61
|
+
# `RIGOR_MEMO_AUDIT`).
|
|
62
|
+
Slot = Data.define(:name, :kind, :index, :reason, :run_scoped, :read_exemption, :contents, :read_through, :bound,
|
|
63
|
+
:audit, :memos, :computed, :entries, :declaration)
|
|
64
|
+
|
|
65
|
+
# A field declared outside the slots: `kind` is `:memo` ({.memo_field}) or `:store` ({.store_field}, whose `slot`
|
|
66
|
+
# names the bound Table).
|
|
67
|
+
Field = Data.define(:owner, :ivar, :kind, :slot, :reason)
|
|
68
|
+
|
|
69
|
+
# A Table read of a key the table does not hold logs this rather than nil, so a stored nil and a miss differ.
|
|
70
|
+
ABSENT = Object.new.freeze
|
|
71
|
+
|
|
72
|
+
# ADR-129 WD3 — a Table's entry protocol: how the journal reaches one entry of a store, to validate a logged read
|
|
73
|
+
# ({State#stale_read}), replay a logged write ({State#replay}) and note the value a write replaces
|
|
74
|
+
# ({State#open_priors}). A Table names its protocol as `entries:`; this one is the default, for a store that answers
|
|
75
|
+
# `fetch(key, default)` and `[]=` (a Hash, or a recorder's accumulator built to that shape). A protocol module
|
|
76
|
+
# answers:
|
|
77
|
+
# - `entry(store, key)`: the value at `key`, or {ABSENT}, for a nil store too (an unbound table not created yet);
|
|
78
|
+
# - `put(store, key, value)`: stores `value` at `key` the way the table's own writer does;
|
|
79
|
+
# - `delete(store, key)`: removes the entry at `key`, undoing a first write (G1's stage-4 form);
|
|
80
|
+
# - `container(state, slot)`: the store an unbound table's replay writes into, the one `state` holds in `slot` or
|
|
81
|
+
# one it creates there the way the table's writer does.
|
|
82
|
+
#
|
|
83
|
+
# Records index their stores by identity and their keys by `eql?`, which is identity for the Prism nodes most
|
|
84
|
+
# tables key on (`Prism::Node` defines neither `==` nor `hash`) and what each store's own lookup uses for the rest.
|
|
85
|
+
module StoreEntries
|
|
86
|
+
module_function
|
|
87
|
+
|
|
88
|
+
def entry(store, key) = store.nil? ? ABSENT : store.fetch(key, ABSENT)
|
|
89
|
+
|
|
90
|
+
def put(store, key, value)
|
|
91
|
+
store[key] = value
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def delete(store, key)
|
|
95
|
+
store.delete(key)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def container(state, slot) = state[slot] || (state[slot] = {})
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# The entry protocol of a Table whose slot holds an `ObjectSpace::WeakKeyMap` ({State#weak_key_map}), which has no
|
|
102
|
+
# `fetch`.
|
|
103
|
+
module WeakKeyMapEntries
|
|
104
|
+
module_function
|
|
105
|
+
|
|
106
|
+
def entry(store, key) = store&.key?(key) ? store[key] : ABSENT
|
|
107
|
+
|
|
108
|
+
def put(store, key, value)
|
|
109
|
+
store[key] = value
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def delete(store, key)
|
|
113
|
+
store&.delete(key)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def container(state, slot) = state.weak_key_map(slot)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# The read and write slices of a computation that logged nothing.
|
|
120
|
+
EMPTY_SLICES = [[].freeze, [].freeze].freeze
|
|
121
|
+
|
|
122
|
+
# A read slice's fourth element when the read is not a read-through cache's ABSENT read of a key the computation
|
|
123
|
+
# then wrote; a write's prior when the writer did not pass one ({State#log_write}).
|
|
124
|
+
NONE = Object.new.freeze
|
|
125
|
+
private_constant :NONE
|
|
126
|
+
|
|
127
|
+
STATE_KEY = :__rigor_typing_state__
|
|
128
|
+
private_constant :STATE_KEY
|
|
129
|
+
|
|
130
|
+
# The registry and the run-scoped indices, each replaced by a frozen copy on every declaration so a non-main
|
|
131
|
+
# Ractor can read them. Declarations run at require time, on the main Ractor.
|
|
132
|
+
@slots = [].freeze
|
|
133
|
+
@run_scoped = [].freeze
|
|
134
|
+
@fields = [].freeze
|
|
135
|
+
|
|
136
|
+
class << self
|
|
137
|
+
# Declares a Frame slot and returns its index. A `contents: :resource` frame may declare a read exemption for
|
|
138
|
+
# reads of its contents. `memos` names the members (a Data or Struct value) or keys (a Hash value) of the
|
|
139
|
+
# installed value that hold extensional caches its owner fills in place, Memos by kind, which `reason` explains.
|
|
140
|
+
#
|
|
141
|
+
# A Frame declares exactly one of ADR-129 G1′'s four things about how typing meets it (WD5):
|
|
142
|
+
# - `witness:` a repo-relative fixture path, a program whose typing installs the Frame and reads it inside
|
|
143
|
+
# `type_of`, and on which G1 must fail with the slot ignored;
|
|
144
|
+
# - `typing_unreachable: true`: no `type_of` runs while the Frame is installed, which audit mode asserts;
|
|
145
|
+
# - `typing_never_reads: true`: typing wraps no read of the Frame, only code outside `type_of` reads it, which
|
|
146
|
+
# audit mode asserts;
|
|
147
|
+
# - `memo_shut_while_installed: true`: the `type_of` memo never runs while the Frame is installed (the tracer
|
|
148
|
+
# switches).
|
|
149
|
+
def frame(name, reason:, contents: nil, read_exemption: nil, memos: nil, witness: nil, # rubocop:disable Metrics/ParameterLists
|
|
150
|
+
typing_unreachable: false, typing_never_reads: false, memo_shut_while_installed: false)
|
|
151
|
+
check_unclaimed(name)
|
|
152
|
+
unless [nil, :resource, :table].include?(contents)
|
|
153
|
+
raise ArgumentError, "#{name}: contents must be :resource, :table or nil"
|
|
154
|
+
end
|
|
155
|
+
if read_exemption && contents != :resource
|
|
156
|
+
raise ArgumentError, "#{name}: only a contents: :resource frame declares a read exemption"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
check_read_exemption(name, read_exemption)
|
|
160
|
+
declare(name, :frame, reason, contents: contents, read_exemption: read_exemption,
|
|
161
|
+
memos: memos && Array(memos).freeze,
|
|
162
|
+
declaration: g1_declaration(name, witness, typing_unreachable, typing_never_reads,
|
|
163
|
+
memo_shut_while_installed))
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Declares a Table slot and returns its index. A `bound` table's stores live with their owners, so the slot holds
|
|
167
|
+
# nothing a run could release. A table a computation fills on a miss names the miss's `MemoAudit.key_audit` label
|
|
168
|
+
# as `computed`. `entries` names the table's entry protocol ({StoreEntries}, the default).
|
|
169
|
+
def table(name, reason:, run_scoped: false, read_through: false, bound: false, computed: nil,
|
|
170
|
+
entries: StoreEntries)
|
|
171
|
+
raise ArgumentError, "#{name}: a bound table holds no run-scoped value" if bound && run_scoped
|
|
172
|
+
unless %i[entry put delete container].all? { |method| entries.respond_to?(method) }
|
|
173
|
+
raise ArgumentError, "#{name}: entries must answer entry, put, delete and container"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
declare(name, :table, reason, run_scoped: run_scoped, read_through: read_through, bound: bound,
|
|
177
|
+
computed: computed, entries: entries)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Declares a Resource slot and returns its index.
|
|
181
|
+
def resource(name, reason:, read_exemption: nil, run_scoped: false)
|
|
182
|
+
check_read_exemption(name, read_exemption)
|
|
183
|
+
declare(name, :resource, reason, read_exemption: read_exemption, run_scoped: run_scoped)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Declares a Memo slot and returns its index. `audit` names the `MemoAudit.check` labels its hits go through;
|
|
187
|
+
# a spec (`spec/rigor/typing_state_memo_audit_spec.rb`) holds every Memo to them, or to its stage-2 ratchet.
|
|
188
|
+
def memo(name, reason:, run_scoped: false, audit: nil)
|
|
189
|
+
labels = Array(audit)
|
|
190
|
+
declare(name, :memo, reason, run_scoped: run_scoped, audit: labels.empty? ? nil : labels)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Declares that `owner`'s instances fill each of `ivars` in place, or assign it lazily, as an extensional cache
|
|
194
|
+
# keyed on all its inputs: a Memo that lives outside a slot. `reason` says why it is extensional. G4 accepts
|
|
195
|
+
# what such a field holds without walking it, and accepts an unfrozen `owner` whose other fields keep their
|
|
196
|
+
# objects.
|
|
197
|
+
def memo_field(owner, *ivars, reason:)
|
|
198
|
+
raise ArgumentError, "#{owner}: memo_field names at least one field" if ivars.empty?
|
|
199
|
+
|
|
200
|
+
ivars.each { |ivar| declare_field(owner, ivar, :memo, reason, nil) }
|
|
201
|
+
nil
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Declares that `owner`'s instance variable `ivar` holds a store of the bound Table `slot` (an index from
|
|
205
|
+
# {.table} with `bound: true`), whose entries are journaled through the slot.
|
|
206
|
+
def store_field(owner, ivar, slot)
|
|
207
|
+
declared = @slots.fetch(slot)
|
|
208
|
+
raise ArgumentError, "#{declared.name}: only a bound table has stores in fields" unless declared.bound
|
|
209
|
+
|
|
210
|
+
declare_field(owner, ivar, :store, declared.reason, declared.name)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Every declared slot, in declaration order (a slot's index is its position).
|
|
214
|
+
attr_reader :slots
|
|
215
|
+
|
|
216
|
+
# The indices of the Frames that declare the G1′ kind `kind` ({G1_KINDS}), for {FrameAudit}.
|
|
217
|
+
def g1_slots(kind) = @slots.filter_map { |slot| slot.index if slot.declaration&.first == kind }.freeze
|
|
218
|
+
|
|
219
|
+
# Every field declared with {.memo_field} or {.store_field}, as `Field` values.
|
|
220
|
+
attr_reader :fields
|
|
221
|
+
|
|
222
|
+
# The slot declared as `name`. For specs and diagnostics; the hot path reads by index.
|
|
223
|
+
def slot(name)
|
|
224
|
+
@slots.find { |slot| slot.name == name } || raise(KeyError, "no typing-state slot #{name.inspect}")
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# The calling thread's state, created on first use. The `is_a?` test types the answer as a {State} for Rigor's
|
|
228
|
+
# own analysis of its callers, where a bare `Thread.current[]` read is `Dynamic`.
|
|
229
|
+
def current
|
|
230
|
+
state = Thread.current[STATE_KEY]
|
|
231
|
+
return state if state.is_a?(State)
|
|
232
|
+
|
|
233
|
+
Thread.current[STATE_KEY] = State.new
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Empties every run-scoped slot on the calling thread, the TypingState half of {RunScopedSlots.release}.
|
|
237
|
+
def release_run_scoped
|
|
238
|
+
state = Thread.current[STATE_KEY]
|
|
239
|
+
state&.release(@run_scoped)
|
|
240
|
+
nil
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
private
|
|
244
|
+
|
|
245
|
+
def check_unclaimed(name)
|
|
246
|
+
return unless @slots.any? { |slot| slot.name == name }
|
|
247
|
+
|
|
248
|
+
raise ArgumentError, "typing-state slot #{name.inspect} is already declared"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def g1_declaration(name, witness, typing_unreachable, typing_never_reads, memo_shut_while_installed)
|
|
252
|
+
declared = []
|
|
253
|
+
declared << [:witness, witness] unless witness.nil?
|
|
254
|
+
declared << [:typing_unreachable, true] if typing_unreachable
|
|
255
|
+
declared << [:typing_never_reads, true] if typing_never_reads
|
|
256
|
+
declared << [:memo_shut_while_installed, true] if memo_shut_while_installed
|
|
257
|
+
unless declared.size == 1
|
|
258
|
+
raise ArgumentError, "#{name}: a Frame declares exactly one of #{G1_KINDS.inspect} (ADR-129 G1′), " \
|
|
259
|
+
"not #{declared.map(&:first).inspect}"
|
|
260
|
+
end
|
|
261
|
+
if witness && !(witness.is_a?(String) && witness.end_with?(".rb") && !witness.start_with?("/"))
|
|
262
|
+
raise ArgumentError, "#{name}: witness is a repo-relative path to a .rb fixture"
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
declared.first.freeze
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def check_read_exemption(name, read_exemption)
|
|
269
|
+
return if read_exemption.nil? || READ_EXEMPTIONS.include?(read_exemption)
|
|
270
|
+
|
|
271
|
+
raise ArgumentError, "#{name}: read_exemption must be one of #{READ_EXEMPTIONS.inspect} or nil"
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def declare_field(owner, ivar, kind, reason, slot_name)
|
|
275
|
+
raise ArgumentError, "#{owner}##{ivar}: a field is an instance variable" unless ivar.to_s.start_with?("@")
|
|
276
|
+
raise ArgumentError, "#{owner}##{ivar}: a field names why it is its kind" if reason.to_s.strip.empty?
|
|
277
|
+
if @fields.any? { |field| field.owner.equal?(owner) && field.ivar == ivar }
|
|
278
|
+
raise ArgumentError, "#{owner}##{ivar} is already declared"
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
field = Field.new(owner: owner, ivar: ivar, kind: kind, slot: slot_name, reason: reason.dup.freeze)
|
|
282
|
+
@fields = Ractor.make_shareable([*@fields, field])
|
|
283
|
+
nil
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def declare(name, kind, reason, run_scoped: false, read_exemption: nil, contents: nil, read_through: false, # rubocop:disable Metrics/ParameterLists
|
|
287
|
+
bound: false, audit: nil, memos: nil, computed: nil, entries: nil, declaration: nil)
|
|
288
|
+
check_unclaimed(name)
|
|
289
|
+
raise ArgumentError, "#{name}: a slot names why it is its kind" if reason.to_s.strip.empty?
|
|
290
|
+
|
|
291
|
+
index = @slots.size
|
|
292
|
+
slot = Slot.new(name: name, kind: kind, index: index, reason: reason.dup.freeze, run_scoped: run_scoped,
|
|
293
|
+
read_exemption: read_exemption, contents: contents, read_through: read_through, bound: bound,
|
|
294
|
+
audit: audit, memos: memos, computed: computed, entries: entries, declaration: declaration)
|
|
295
|
+
@slots = Ractor.make_shareable([*@slots, slot])
|
|
296
|
+
@run_scoped = [*@run_scoped, index].freeze if run_scoped
|
|
297
|
+
index
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# ADR-129 WD3 — a {State}'s reach into the Table entries its journal names: reading one ({#entry}), splitting a
|
|
302
|
+
# capture's records into the slices an entry keeps ({#slices}), validating a read slice ({#stale_read}), replaying a
|
|
303
|
+
# write slice ({#replay}), and the priors windows that note what each write replaced ({#open_priors}).
|
|
304
|
+
module Slices
|
|
305
|
+
# The value the Table `slot` holds at `key`, a journal record's key (`[store, key]` for a bound table), or
|
|
306
|
+
# {ABSENT}.
|
|
307
|
+
def entry(slot, key)
|
|
308
|
+
declared = TypingState.slots[slot]
|
|
309
|
+
return declared.entries.entry(key[0], key[1]) if declared.bound
|
|
310
|
+
|
|
311
|
+
declared.entries.entry(@values[slot], key)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Splits a capture's records ({#close_capture}) into the slices a stored entry keeps (WD3), as a frozen pair:
|
|
315
|
+
# - the read slice, four entries per read `(slot, key, observed, own)`: every read of a key the computation had
|
|
316
|
+
# not written yet (a read of its own write is internal). `own` is the value the computation last wrote at that
|
|
317
|
+
# key when the table is a read-through cache and the read observed {ABSENT}, else an internal marker;
|
|
318
|
+
# - the write slice, three entries per write `(slot, key, value)`, in order.
|
|
319
|
+
def slices(records)
|
|
320
|
+
return EMPTY_SLICES if records.empty?
|
|
321
|
+
|
|
322
|
+
reads = []
|
|
323
|
+
writes = []
|
|
324
|
+
written = nil
|
|
325
|
+
index = 0
|
|
326
|
+
while index < records.size
|
|
327
|
+
slot = records[index + 1]
|
|
328
|
+
key = records[index + 2]
|
|
329
|
+
value = records[index + 3]
|
|
330
|
+
if records[index] == :write
|
|
331
|
+
writes.push(slot, key, value)
|
|
332
|
+
(written ||= KeyIndex.new).store(slot, key, value)
|
|
333
|
+
elsif written.nil? || !written.key?(slot, key)
|
|
334
|
+
reads.push(slot, key, value, NONE)
|
|
335
|
+
end
|
|
336
|
+
index += 4
|
|
337
|
+
end
|
|
338
|
+
attach_own_writes(reads, written) if written
|
|
339
|
+
[reads.freeze, writes.freeze].freeze
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# The slot of the first read in `reads` (a read slice, {#slices}) that no longer holds: the table holds a value
|
|
343
|
+
# other than the one observed, by identity, {ABSENT} included, unless the table is a read-through cache, the
|
|
344
|
+
# read observed {ABSENT} and the table now holds what the computation itself wrote there. nil when all hold.
|
|
345
|
+
def stale_read(reads)
|
|
346
|
+
index = 0
|
|
347
|
+
while index < reads.size
|
|
348
|
+
return reads[index] unless read_holds?(reads, index)
|
|
349
|
+
|
|
350
|
+
index += 4
|
|
351
|
+
end
|
|
352
|
+
nil
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Every slot with a read in `reads` that no longer holds ({#stale_read}), each once, in first-failure order.
|
|
356
|
+
def stale_slots(reads)
|
|
357
|
+
slots = []
|
|
358
|
+
index = 0
|
|
359
|
+
while index < reads.size
|
|
360
|
+
slot = reads[index]
|
|
361
|
+
slots << slot unless slots.include?(slot) || read_holds?(reads, index)
|
|
362
|
+
index += 4
|
|
363
|
+
end
|
|
364
|
+
slots
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Replays `writes` (a write slice, {#slices}) in order, through each table's entry protocol, journaling each write
|
|
368
|
+
# as its writer would (WD3: the replay is itself journaled).
|
|
369
|
+
def replay(writes)
|
|
370
|
+
index = 0
|
|
371
|
+
while index < writes.size
|
|
372
|
+
slot = writes[index]
|
|
373
|
+
key = writes[index + 1]
|
|
374
|
+
value = writes[index + 2]
|
|
375
|
+
note_prior(slot, key, NONE) if @priors
|
|
376
|
+
put(slot, key, value)
|
|
377
|
+
@journal.push(:write, slot, key, value) unless @marks.empty?
|
|
378
|
+
index += 3
|
|
379
|
+
end
|
|
380
|
+
nil
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# Opens a priors window: until {#close_priors}, the first write of each Table entry notes the value it replaced.
|
|
384
|
+
# Windows nest, and a write is noted in every open window that has not seen its key. A window opens only inside
|
|
385
|
+
# a capture, since some writers log only while one is open, and closes before that capture does.
|
|
386
|
+
def open_priors
|
|
387
|
+
raise ArgumentError, "a priors window opens inside a capture" unless capturing?
|
|
388
|
+
|
|
389
|
+
(@priors ||= []) << [KeyIndex.new, []]
|
|
390
|
+
nil
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Closes the innermost priors window and answers what it noted, three entries per entry `(slot, key, prior)` in
|
|
394
|
+
# first-write order, {ABSENT} for an entry the table did not hold.
|
|
395
|
+
def close_priors
|
|
396
|
+
_, noted = @priors.pop
|
|
397
|
+
@priors = nil if @priors.empty?
|
|
398
|
+
noted.freeze
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# The journal's length now, a mark for {#truncate_journal}.
|
|
402
|
+
def journal_size = @journal.size
|
|
403
|
+
|
|
404
|
+
# Drops the journal's records past `mark`, which no capture opened since `mark` is still open to own (G1's
|
|
405
|
+
# stage-4 form drops a recomputation's records, which the served entry's replace).
|
|
406
|
+
def truncate_journal(mark)
|
|
407
|
+
@journal.slice!(mark..)
|
|
408
|
+
nil
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Appends `reads` (a read slice, {#slices}) to the journal as the reads they record, so every open capture
|
|
412
|
+
# inherits them: a served hit's upward merge (WD3 step 2).
|
|
413
|
+
def merge_reads(reads)
|
|
414
|
+
return if @marks.empty?
|
|
415
|
+
|
|
416
|
+
index = 0
|
|
417
|
+
while index < reads.size
|
|
418
|
+
@journal.push(:read, reads[index], reads[index + 1], reads[index + 2])
|
|
419
|
+
index += 4
|
|
420
|
+
end
|
|
421
|
+
nil
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Puts back, last first, the values `priors` ({#close_priors}) noted, removing an entry noted {ABSENT}: undoes
|
|
425
|
+
# the writes of the computation the window was open around.
|
|
426
|
+
def undo(priors)
|
|
427
|
+
index = priors.size - 3
|
|
428
|
+
while index >= 0
|
|
429
|
+
slot = priors[index]
|
|
430
|
+
key = priors[index + 1]
|
|
431
|
+
prior = priors[index + 2]
|
|
432
|
+
ABSENT.equal?(prior) ? remove(slot, key) : put(slot, key, prior)
|
|
433
|
+
index -= 3
|
|
434
|
+
end
|
|
435
|
+
nil
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
private
|
|
439
|
+
|
|
440
|
+
# Stores `value` at `key` (a journal record's key) in the Table `slot`, creating an unbound table's store.
|
|
441
|
+
def put(slot, key, value)
|
|
442
|
+
declared = TypingState.slots[slot]
|
|
443
|
+
return declared.entries.put(key[0], key[1], value) if declared.bound
|
|
444
|
+
|
|
445
|
+
entries = declared.entries
|
|
446
|
+
entries.put(entries.container(self, slot), key, value)
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# Removes the entry at `key` (a journal record's key) from the Table `slot`.
|
|
450
|
+
def remove(slot, key)
|
|
451
|
+
declared = TypingState.slots[slot]
|
|
452
|
+
return declared.entries.delete(key[0], key[1]) if declared.bound
|
|
453
|
+
|
|
454
|
+
declared.entries.delete(@values[slot], key)
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def read_holds?(reads, index)
|
|
458
|
+
current = entry(reads[index], reads[index + 1])
|
|
459
|
+
return true if current.equal?(reads[index + 2])
|
|
460
|
+
|
|
461
|
+
own = reads[index + 3]
|
|
462
|
+
!NONE.equal?(own) && current.equal?(own)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# Gives each read-through table's ABSENT read in `reads` the value the computation last wrote at its key.
|
|
466
|
+
def attach_own_writes(reads, written)
|
|
467
|
+
index = 0
|
|
468
|
+
while index < reads.size
|
|
469
|
+
slot = reads[index]
|
|
470
|
+
key = reads[index + 1]
|
|
471
|
+
if ABSENT.equal?(reads[index + 2]) && TypingState.slots[slot].read_through && written.key?(slot, key)
|
|
472
|
+
reads[index + 3] = written.fetch(slot, key)
|
|
473
|
+
end
|
|
474
|
+
index += 4
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Notes in every open priors window that has not seen `key` the value `slot` holds there now, or `prior` when the
|
|
479
|
+
# writer passed it.
|
|
480
|
+
def note_prior(slot, key, prior)
|
|
481
|
+
@priors.each do |seen, noted|
|
|
482
|
+
next if seen.key?(slot, key)
|
|
483
|
+
|
|
484
|
+
prior = entry(slot, key) if NONE.equal?(prior)
|
|
485
|
+
seen.store(slot, key, true)
|
|
486
|
+
noted.push(slot, key, prior)
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# One thread's typing state: the slot values, the state id (WD2), and the journal (WD3).
|
|
492
|
+
#
|
|
493
|
+
# **The state id** is an integer that names the installed Frame values. Every install takes a fresh one, and the
|
|
494
|
+
# frame restores the saved one on the way out. Because Frame values are frozen, equal ids mean equal frames: no
|
|
495
|
+
# interning or hashing. An id-determined Resource (WD3) keeps that property: the guard stack's push and pop each
|
|
496
|
+
# take a fresh id ({#push}, {#pop}), and a rewrite in place ({#rewrite}) also moves the **epoch**, so a frame
|
|
497
|
+
# that saw one exits to a fresh id rather than to its saved one, which named the contents before the rewrite.
|
|
498
|
+
#
|
|
499
|
+
# **The journal** is one flat array of Table reads and writes, four entries per record (`:read` or `:write`, the
|
|
500
|
+
# slot, the key, the value or {ABSENT}). A capture is a start mark in it plus a refusal flag. Records are kept
|
|
501
|
+
# only while a capture is open, and the journal empties when the outermost capture closes.
|
|
502
|
+
class State
|
|
503
|
+
include Slices
|
|
504
|
+
|
|
505
|
+
attr_reader :id
|
|
506
|
+
|
|
507
|
+
def initialize
|
|
508
|
+
@values = []
|
|
509
|
+
@id = 0
|
|
510
|
+
@last_id = 0
|
|
511
|
+
@epoch = 0
|
|
512
|
+
@journal = []
|
|
513
|
+
@marks = []
|
|
514
|
+
@refused = []
|
|
515
|
+
@priors = nil
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# A Frame or Memo read, or a Table's container.
|
|
519
|
+
def [](slot) = @values[slot]
|
|
520
|
+
|
|
521
|
+
# A Frame read only to decide whether to install it at a constant for the extent that follows (the return memo
|
|
522
|
+
# turning the key-presence walk off): whichever way it goes, that extent reads the constant, so the key audit
|
|
523
|
+
# ({KeyAudit}) does not note it. Any other read goes through {#[]}.
|
|
524
|
+
def forcing_read(slot) = @values[slot]
|
|
525
|
+
|
|
526
|
+
# A Frame read as a term of a memo key (ADR-129 WD1's key audit): the value {#[]} answers. Under the key audit
|
|
527
|
+
# ({KeyAudit}) the read is also noted, so the audit compares what the key carries with what its miss read.
|
|
528
|
+
def key_frame(slot) = @values[slot]
|
|
529
|
+
|
|
530
|
+
# The state id read as a term of a memo key: it names every installed Frame value (WD2), so a key carrying it
|
|
531
|
+
# carries every Frame.
|
|
532
|
+
def key_state_id = @id
|
|
533
|
+
|
|
534
|
+
# A Memo write, or a Table's container (an entry's own write is {#log_write}).
|
|
535
|
+
def []=(slot, value)
|
|
536
|
+
@values[slot] = value
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
# Runs the block with `value` installed in the Frame `slot`, under a fresh state id, and restores both.
|
|
540
|
+
def with(slot, value)
|
|
541
|
+
values = @values
|
|
542
|
+
previous = values[slot]
|
|
543
|
+
saved_id = @id
|
|
544
|
+
saved_epoch = @epoch
|
|
545
|
+
values[slot] = value
|
|
546
|
+
@id = (@last_id += 1)
|
|
547
|
+
begin
|
|
548
|
+
yield
|
|
549
|
+
ensure
|
|
550
|
+
values[slot] = previous
|
|
551
|
+
@id = @epoch == saved_epoch ? saved_id : (@last_id += 1)
|
|
552
|
+
end
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
# {#with} for two Frames installed together, under one fresh id.
|
|
556
|
+
def with_pair(slot, value, other, other_value)
|
|
557
|
+
values = @values
|
|
558
|
+
previous = values[slot]
|
|
559
|
+
other_previous = values[other]
|
|
560
|
+
saved_id = @id
|
|
561
|
+
saved_epoch = @epoch
|
|
562
|
+
values[slot] = value
|
|
563
|
+
values[other] = other_value
|
|
564
|
+
@id = (@last_id += 1)
|
|
565
|
+
begin
|
|
566
|
+
yield
|
|
567
|
+
ensure
|
|
568
|
+
values[slot] = previous
|
|
569
|
+
values[other] = other_previous
|
|
570
|
+
@id = @epoch == saved_epoch ? saved_id : (@last_id += 1)
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
# {#with} for nil installed in each Frame of `slots` (a frozen Array), under one fresh id.
|
|
575
|
+
def without(slots)
|
|
576
|
+
values = @values
|
|
577
|
+
previous = slots.map { |slot| values[slot] }
|
|
578
|
+
saved_id = @id
|
|
579
|
+
saved_epoch = @epoch
|
|
580
|
+
slots.each { |slot| values[slot] = nil }
|
|
581
|
+
@id = (@last_id += 1)
|
|
582
|
+
begin
|
|
583
|
+
yield
|
|
584
|
+
ensure
|
|
585
|
+
index = 0
|
|
586
|
+
while index < slots.size
|
|
587
|
+
values[slots[index]] = previous[index]
|
|
588
|
+
index += 1
|
|
589
|
+
end
|
|
590
|
+
@id = @epoch == saved_epoch ? saved_id : (@last_id += 1)
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
# A Resource write, a read the slot does not exempt, or a write to a `contents: :resource` Frame's container:
|
|
595
|
+
# refuses every open capture (the `any` rule, WD3).
|
|
596
|
+
def touch(_slot) = refuse_captures
|
|
597
|
+
|
|
598
|
+
# Refuses every open capture: a touch, or a miss that wrote a Table ({#memo_miss}).
|
|
599
|
+
def refuse_captures
|
|
600
|
+
refused = @refused
|
|
601
|
+
refused.fill(true) unless refused.empty?
|
|
602
|
+
nil
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
# A Resource read the slot does not exempt.
|
|
606
|
+
def read(slot)
|
|
607
|
+
touch(slot)
|
|
608
|
+
@values[slot]
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
# A Resource write.
|
|
612
|
+
def write(slot, value)
|
|
613
|
+
touch(slot)
|
|
614
|
+
@values[slot] = value
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
# The Resource container in `slot`, created by the block on first use: a read the slot does not exempt, and a
|
|
618
|
+
# write when it creates. Callers mutate the container in place after this touch.
|
|
619
|
+
def acquire(slot)
|
|
620
|
+
touch(slot)
|
|
621
|
+
values = @values
|
|
622
|
+
values[slot] || (values[slot] = yield)
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
# Appends `value` to the append-only Resource log in `slot`, creating it on first use: a write.
|
|
626
|
+
def append(slot, value)
|
|
627
|
+
touch(slot)
|
|
628
|
+
(@values[slot] ||= []) << value
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
# The id-determined Resource stack in `slot`, created empty on first use. Reading it is exempt (WD3: its value
|
|
632
|
+
# is fixed while the id is), and creating it changes no contents; {#push} and {#pop} are its writes.
|
|
633
|
+
def stack(slot) = @values[slot] ||= []
|
|
634
|
+
|
|
635
|
+
# Pushes `value` onto the id-determined Resource stack in `slot`, creating it on first use: a write, under a
|
|
636
|
+
# fresh id.
|
|
637
|
+
def push(slot, value)
|
|
638
|
+
touch(slot)
|
|
639
|
+
@id = (@last_id += 1)
|
|
640
|
+
(@values[slot] ||= []) << value
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
# Pops the id-determined Resource stack in `slot`: a write, under a fresh id. The stack holds what it held
|
|
644
|
+
# before the matching {#push}, so the epoch stays.
|
|
645
|
+
def pop(slot)
|
|
646
|
+
touch(slot)
|
|
647
|
+
@id = (@last_id += 1)
|
|
648
|
+
@values[slot].pop
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
# The contents of the id-determined `slot` (a Resource, or a `contents: :resource` Frame's container) were
|
|
652
|
+
# rewritten in place: a write, under a fresh id, and a new epoch, so no frame open now restores an id that
|
|
653
|
+
# named the old contents.
|
|
654
|
+
def rewrite(slot)
|
|
655
|
+
touch(slot)
|
|
656
|
+
@epoch += 1
|
|
657
|
+
@id = (@last_id += 1)
|
|
658
|
+
nil
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
# The `ObjectSpace::WeakKeyMap` a Table or Memo `slot` holds, created on first use.
|
|
662
|
+
def weak_key_map(slot) = @values[slot] ||= ObjectSpace::WeakKeyMap.new
|
|
663
|
+
|
|
664
|
+
# Logs a Table read of `key` in `slot` that observed `value` ({ABSENT} for a miss), while a capture is open.
|
|
665
|
+
def log_read(slot, key, value)
|
|
666
|
+
@journal.push(:read, slot, key, value) unless @marks.empty?
|
|
667
|
+
nil
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
# Logs a Table write of `value` at `key` in `slot`, while a capture is open. A writer logs before it stores, so
|
|
671
|
+
# while a priors window is open ({#open_priors}) the value the write replaces is still there to note; a writer
|
|
672
|
+
# that knows it (a set-only add, whose prior is {ABSENT}) passes it as `prior` and may log after.
|
|
673
|
+
def log_write(slot, key, value, prior = NONE)
|
|
674
|
+
note_prior(slot, key, prior) if @priors
|
|
675
|
+
@journal.push(:write, slot, key, value) unless @marks.empty?
|
|
676
|
+
nil
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
# Logs a read of `key` in `store`, one of the bound Table `slot`'s stores, that observed `value` ({ABSENT} for a
|
|
680
|
+
# miss), while a capture is open. The record's key is the frozen pair `[store, key]`, built only then, so a run
|
|
681
|
+
# with no capture open allocates nothing.
|
|
682
|
+
def log_bound_read(slot, store, key, value)
|
|
683
|
+
@journal.push(:read, slot, [store, key].freeze, value) unless @marks.empty?
|
|
684
|
+
nil
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
# Logs a write of `value` at `key` in `store`, one of the bound Table `slot`'s stores, while a capture is open,
|
|
688
|
+
# before the store changes or with the replaced value as `prior` ({#log_write}).
|
|
689
|
+
def log_bound_write(slot, store, key, value, prior = NONE)
|
|
690
|
+
return if @marks.empty?
|
|
691
|
+
|
|
692
|
+
pair = [store, key].freeze
|
|
693
|
+
note_prior(slot, pair, prior) if @priors
|
|
694
|
+
@journal.push(:write, slot, pair, value)
|
|
695
|
+
nil
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
# Opens a capture over the journal from here; the innermost open capture closes first.
|
|
699
|
+
def open_capture
|
|
700
|
+
@marks << @journal.size
|
|
701
|
+
@refused << false
|
|
702
|
+
nil
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
# Closes the innermost capture and returns `[refused, records]`: whether a touch refused it, and the journal
|
|
706
|
+
# records logged while it was open, nested captures' included, as a frozen flat array.
|
|
707
|
+
def close_capture
|
|
708
|
+
mark = @marks.pop
|
|
709
|
+
refused = @refused.pop
|
|
710
|
+
records = @journal[mark..].freeze
|
|
711
|
+
@journal.clear if @marks.empty?
|
|
712
|
+
[refused, records]
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
# Whether a capture is open.
|
|
716
|
+
def capturing? = !@marks.empty?
|
|
717
|
+
|
|
718
|
+
# ADR-129 WD1's two Memo rules: runs a miss computation and refuses every open capture when, while a capture was
|
|
719
|
+
# open, it wrote what a later hit of an enclosing entry would replay stale:
|
|
720
|
+
# - a Memo's miss (`own` nil) that wrote an entry of any Table: the Memo, now filled, no longer makes the write,
|
|
721
|
+
# and a newer typing may have overwritten it;
|
|
722
|
+
# - a read-through Table's miss (`own`, its slot) that wrote its own table and another one: reading back its
|
|
723
|
+
# own write validates (WD1's read-through rule) while the other write may have been overwritten (the inject
|
|
724
|
+
# record's miss writes `dynamic_origins` causes). A miss that did not fill its own table leaves its ABSENT
|
|
725
|
+
# read to validate strictly, so its other writes replay only while nothing has changed.
|
|
726
|
+
# The refusal is made on the way out, a raise included, since the journal keeps what a raising computation wrote.
|
|
727
|
+
def memo_miss(own = nil)
|
|
728
|
+
return yield if @marks.empty?
|
|
729
|
+
|
|
730
|
+
mark = @journal.size
|
|
731
|
+
begin
|
|
732
|
+
yield
|
|
733
|
+
ensure
|
|
734
|
+
refuse_captures if wrote_since?(mark, own)
|
|
735
|
+
end
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
# Whether a priors window is open ({#open_priors}).
|
|
739
|
+
def noting_priors? = !@priors.nil?
|
|
740
|
+
|
|
741
|
+
# Whether the journal holds, past `mark`, a write to a Table other than `own` and, when `own` is a slot, a write
|
|
742
|
+
# to `own` too. The journal empties when the outermost capture closes, which a miss computation cannot do to a
|
|
743
|
+
# capture open at its start.
|
|
744
|
+
def wrote_since?(mark, own)
|
|
745
|
+
journal = @journal
|
|
746
|
+
other = false
|
|
747
|
+
filled = own.nil?
|
|
748
|
+
index = mark
|
|
749
|
+
while index < journal.size
|
|
750
|
+
if journal[index] == :write
|
|
751
|
+
journal[index + 1].equal?(own) ? (filled = true) : (other = true)
|
|
752
|
+
return true if other && filled
|
|
753
|
+
end
|
|
754
|
+
index += 4
|
|
755
|
+
end
|
|
756
|
+
false
|
|
757
|
+
end
|
|
758
|
+
private :wrote_since?
|
|
759
|
+
|
|
760
|
+
# Empties the slots at `indices` ({TypingState.release_run_scoped}).
|
|
761
|
+
def release(indices)
|
|
762
|
+
values = @values
|
|
763
|
+
indices.each { |index| values[index] = nil }
|
|
764
|
+
nil
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
# The names of the slots holding a value other than nil or an empty collection: what a finished run left
|
|
768
|
+
# behind. For specs.
|
|
769
|
+
def holding
|
|
770
|
+
TypingState.slots.filter_map do |slot|
|
|
771
|
+
value = @values[slot.index]
|
|
772
|
+
slot.name unless value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
773
|
+
end
|
|
774
|
+
end
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
# A set of Table entries named by journal record keys, with a value each: the stores of a bound table by
|
|
778
|
+
# identity (`[store, key]` pairs are never hashed whole, since `Array#hash` would hash a Hash store by its
|
|
779
|
+
# contents), the keys by `eql?` ({StoreEntries}).
|
|
780
|
+
class KeyIndex
|
|
781
|
+
def initialize
|
|
782
|
+
@tables = {}
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
def key?(slot, key)
|
|
786
|
+
keys = keys_for(slot, key, false)
|
|
787
|
+
!keys.nil? && keys.key?(entry_key(slot, key))
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
def fetch(slot, key) = keys_for(slot, key, false).fetch(entry_key(slot, key))
|
|
791
|
+
|
|
792
|
+
def store(slot, key, value)
|
|
793
|
+
keys_for(slot, key, true)[entry_key(slot, key)] = value
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
private
|
|
797
|
+
|
|
798
|
+
def entry_key(slot, key) = TypingState.slots[slot].bound ? key[1] : key
|
|
799
|
+
|
|
800
|
+
def keys_for(slot, key, create)
|
|
801
|
+
tables = @tables
|
|
802
|
+
if TypingState.slots[slot].bound
|
|
803
|
+
stores = tables[slot] || (create ? (tables[slot] = {}.compare_by_identity) : nil)
|
|
804
|
+
return nil if stores.nil?
|
|
805
|
+
|
|
806
|
+
stores[key[0]] || (create ? (stores[key[0]] = {}) : nil)
|
|
807
|
+
else
|
|
808
|
+
tables[slot] || (create ? (tables[slot] = {}) : nil)
|
|
809
|
+
end
|
|
810
|
+
end
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
# ADR-129 G1′'s audit of the Frame declarations, on one {State}: `MemoAudit` extends a thread's state with this
|
|
814
|
+
# module the first time an audited run enters `ExpressionTyper#type_of` ({MemoAudit.enter_type_of}), so a run with
|
|
815
|
+
# the audit off reads a slot through the plain {State#[]}. It asserts three of the four declarations, each as a
|
|
816
|
+
# {MemoAudit::Mismatch} on the thread's list (not raised):
|
|
817
|
+
# - `typing_unreachable`: a `type_of` entered while such a Frame holds a value;
|
|
818
|
+
# - `typing_never_reads`: a read of such a Frame ({State#[]}, {State#key_frame}, {State#forcing_read}) while a
|
|
819
|
+
# `type_of` is running;
|
|
820
|
+
# - `memo_shut_while_installed`: the `type_of` memo's shadow ({TypeOfMemo.shadow}) reaching its table while such a
|
|
821
|
+
# Frame holds a value ({MemoAudit.note_memo_lookup}).
|
|
822
|
+
#
|
|
823
|
+
# **What this module hooks.** The three Frame accessors are the reads a typed answer can come through: {State#[]},
|
|
824
|
+
# {State#key_frame} and {State#forcing_read}. A new accessor that reads `@values` of a Frame must hook here too, or
|
|
825
|
+
# a `typing_never_reads` Frame can be read past the audit. The declared slot lists are a snapshot taken when the
|
|
826
|
+
# state is extended: every Frame is declared at require time, before any audited `type_of`, so a Frame declared
|
|
827
|
+
# later (a spec's probe) is not asserted.
|
|
828
|
+
module FrameAudit
|
|
829
|
+
# How many `type_of`s are running on this thread, for specs that observe the reads inside them.
|
|
830
|
+
attr_reader :type_of_depth
|
|
831
|
+
|
|
832
|
+
def self.extended(state) = state.send(:start_frame_audit)
|
|
833
|
+
|
|
834
|
+
def [](slot)
|
|
835
|
+
note_typing_read(slot) if @type_of_depth.positive?
|
|
836
|
+
super
|
|
837
|
+
end
|
|
838
|
+
|
|
839
|
+
def key_frame(slot)
|
|
840
|
+
note_typing_read(slot) if @type_of_depth.positive?
|
|
841
|
+
super
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
def forcing_read(slot)
|
|
845
|
+
note_typing_read(slot) if @type_of_depth.positive?
|
|
846
|
+
super
|
|
847
|
+
end
|
|
848
|
+
|
|
849
|
+
# A `type_of` begins: fails on an installed `typing_unreachable` Frame, and opens the extent in which reads of a
|
|
850
|
+
# `typing_never_reads` Frame fail.
|
|
851
|
+
def enter_type_of
|
|
852
|
+
@unreachable.each do |slot|
|
|
853
|
+
next if @values[slot].nil?
|
|
854
|
+
|
|
855
|
+
MemoAudit.report_frame_declaration(
|
|
856
|
+
self, "frame audit: type_of ran while the typing_unreachable Frame #{TypingState.slots[slot].name} " \
|
|
857
|
+
"is installed"
|
|
858
|
+
)
|
|
859
|
+
end
|
|
860
|
+
@type_of_depth += 1
|
|
861
|
+
end
|
|
862
|
+
|
|
863
|
+
# The `type_of` memo's shadow reached its table: fails on an installed `memo_shut_while_installed` Frame.
|
|
864
|
+
def note_memo_lookup
|
|
865
|
+
@memo_shut.each do |slot|
|
|
866
|
+
next if @values[slot].nil?
|
|
867
|
+
|
|
868
|
+
MemoAudit.report_frame_declaration(
|
|
869
|
+
self, "frame audit: the type_of memo ran while the memo_shut_while_installed Frame " \
|
|
870
|
+
"#{TypingState.slots[slot].name} is installed"
|
|
871
|
+
)
|
|
872
|
+
end
|
|
873
|
+
nil
|
|
874
|
+
end
|
|
875
|
+
|
|
876
|
+
# The matching `type_of` ends.
|
|
877
|
+
def leave_type_of
|
|
878
|
+
@type_of_depth -= 1
|
|
879
|
+
end
|
|
880
|
+
|
|
881
|
+
private
|
|
882
|
+
|
|
883
|
+
def start_frame_audit
|
|
884
|
+
@type_of_depth = 0
|
|
885
|
+
@unreachable = TypingState.g1_slots(:typing_unreachable)
|
|
886
|
+
@never_reads = TypingState.g1_slots(:typing_never_reads)
|
|
887
|
+
@memo_shut = TypingState.g1_slots(:memo_shut_while_installed)
|
|
888
|
+
end
|
|
889
|
+
|
|
890
|
+
def note_typing_read(slot)
|
|
891
|
+
return unless @never_reads.include?(slot)
|
|
892
|
+
|
|
893
|
+
MemoAudit.report_frame_declaration(
|
|
894
|
+
self, "frame audit: the typing_never_reads Frame #{TypingState.slots[slot].name} was read inside type_of"
|
|
895
|
+
)
|
|
896
|
+
end
|
|
897
|
+
end
|
|
898
|
+
|
|
899
|
+
# ADR-129 WD2's ELIDE, measured by stage 3's shadow only: a second id that, unlike the state id, stays put when an
|
|
900
|
+
# install puts back the value the slot already holds (`equal?`), so two frames that differ only by such an install
|
|
901
|
+
# share it. Every other install, every id-determined Resource change and every rewrite moves it exactly as the
|
|
902
|
+
# state id moves, so equal ELIDE ids still name equal Frame values. `TypeOfMemo` extends a thread's state with this
|
|
903
|
+
# module when the measurement is on and keys on {#elide_id}; the state id, which other memos key on, is unchanged.
|
|
904
|
+
module ElideId
|
|
905
|
+
attr_reader :elide_id
|
|
906
|
+
|
|
907
|
+
def self.extended(state) = state.restart_elide_id
|
|
908
|
+
|
|
909
|
+
# A fresh ELIDE id, naming the Frame values installed now.
|
|
910
|
+
def restart_elide_id
|
|
911
|
+
@last_elide_id = (@last_elide_id || 0) + 1
|
|
912
|
+
@elide_id = @last_elide_id
|
|
913
|
+
nil
|
|
914
|
+
end
|
|
915
|
+
|
|
916
|
+
def with(slot, value)
|
|
917
|
+
values = @values
|
|
918
|
+
elide_install(values[slot].equal?(value)) { super }
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
def with_pair(slot, value, other, other_value)
|
|
922
|
+
values = @values
|
|
923
|
+
elide_install(values[slot].equal?(value) && values[other].equal?(other_value)) { super }
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
def without(slots)
|
|
927
|
+
values = @values
|
|
928
|
+
elide_install(slots.all? { |slot| values[slot].nil? }) { super }
|
|
929
|
+
end
|
|
930
|
+
|
|
931
|
+
def push(slot, value)
|
|
932
|
+
result = super
|
|
933
|
+
@elide_id = (@last_elide_id += 1)
|
|
934
|
+
result
|
|
935
|
+
end
|
|
936
|
+
|
|
937
|
+
def pop(slot)
|
|
938
|
+
result = super
|
|
939
|
+
@elide_id = (@last_elide_id += 1)
|
|
940
|
+
result
|
|
941
|
+
end
|
|
942
|
+
|
|
943
|
+
def rewrite(slot)
|
|
944
|
+
super
|
|
945
|
+
@elide_id = (@last_elide_id += 1)
|
|
946
|
+
nil
|
|
947
|
+
end
|
|
948
|
+
|
|
949
|
+
private
|
|
950
|
+
|
|
951
|
+
# Runs the block, an install, under a fresh ELIDE id unless the install puts back what the slots hold, and
|
|
952
|
+
# restores the id on the way out unless an id-determined Resource was rewritten inside it.
|
|
953
|
+
def elide_install(same)
|
|
954
|
+
saved = @elide_id
|
|
955
|
+
saved_epoch = @epoch
|
|
956
|
+
@elide_id = (@last_elide_id += 1) unless same
|
|
957
|
+
begin
|
|
958
|
+
yield
|
|
959
|
+
ensure
|
|
960
|
+
@elide_id = @epoch == saved_epoch ? saved : (@last_elide_id += 1)
|
|
961
|
+
end
|
|
962
|
+
end
|
|
963
|
+
end
|
|
964
|
+
|
|
965
|
+
# ADR-129 WD1's key audit, on one {State}: `MemoAudit` extends a thread's state with this module the first time an
|
|
966
|
+
# audited run reaches a memo miss, so a run with the audit off reads a slot through the plain {State#[]}.
|
|
967
|
+
#
|
|
968
|
+
# While a miss computation runs ({#open_key_audit} .. {#close_key_audit}, nested for a miss inside a miss) every
|
|
969
|
+
# Frame read is noted on each open audit, except a read of a Frame installed since that audit opened: the
|
|
970
|
+
# computation installed it itself, so it is internal (the return memo's `with_return_sink`). A read an inner audit
|
|
971
|
+
# declares a known gap reaches the audits around it as that gap, named through the inner audit's label: the outer
|
|
972
|
+
# value is computed from the inner one. {#collect_key_terms} notes the Frames a key builder reads through
|
|
973
|
+
# {State#key_frame} and {State#key_state_id}.
|
|
974
|
+
module KeyAudit
|
|
975
|
+
# The key term {State#key_state_id} notes: the id names every installed Frame.
|
|
976
|
+
STATE_ID = :state_id
|
|
977
|
+
|
|
978
|
+
def self.extended(state) = state.send(:start_key_audit)
|
|
979
|
+
|
|
980
|
+
def [](slot)
|
|
981
|
+
note_frame_read(slot) unless @key_audits.empty?
|
|
982
|
+
super
|
|
983
|
+
end
|
|
984
|
+
|
|
985
|
+
def key_frame(slot)
|
|
986
|
+
@key_terms&.push(slot)
|
|
987
|
+
note_frame_read(slot) unless @key_audits.empty?
|
|
988
|
+
super
|
|
989
|
+
end
|
|
990
|
+
|
|
991
|
+
def key_state_id
|
|
992
|
+
@key_terms&.push(STATE_ID)
|
|
993
|
+
super
|
|
994
|
+
end
|
|
995
|
+
|
|
996
|
+
def with(slot, value)
|
|
997
|
+
@key_installs.push(slot)
|
|
998
|
+
begin
|
|
999
|
+
super
|
|
1000
|
+
ensure
|
|
1001
|
+
@key_installs.pop
|
|
1002
|
+
end
|
|
1003
|
+
end
|
|
1004
|
+
|
|
1005
|
+
def with_pair(slot, value, other, other_value)
|
|
1006
|
+
installs = @key_installs
|
|
1007
|
+
installs.push(slot, other)
|
|
1008
|
+
begin
|
|
1009
|
+
super
|
|
1010
|
+
ensure
|
|
1011
|
+
installs.pop(2)
|
|
1012
|
+
end
|
|
1013
|
+
end
|
|
1014
|
+
|
|
1015
|
+
def without(slots)
|
|
1016
|
+
installs = @key_installs
|
|
1017
|
+
installs.concat(slots)
|
|
1018
|
+
begin
|
|
1019
|
+
super
|
|
1020
|
+
ensure
|
|
1021
|
+
installs.pop(slots.size)
|
|
1022
|
+
end
|
|
1023
|
+
end
|
|
1024
|
+
|
|
1025
|
+
# Runs the block, a key builder, and remembers the key it answers with the Frame terms it read, for the
|
|
1026
|
+
# {#key_terms_for} of the miss that follows. Answers the key.
|
|
1027
|
+
def collect_key_terms
|
|
1028
|
+
saved = @key_terms
|
|
1029
|
+
terms = @key_terms = []
|
|
1030
|
+
key = yield
|
|
1031
|
+
@last_key = [key, terms.uniq.freeze]
|
|
1032
|
+
key
|
|
1033
|
+
ensure
|
|
1034
|
+
@key_terms = saved
|
|
1035
|
+
end
|
|
1036
|
+
|
|
1037
|
+
# The Frame terms (slot indices, or {STATE_ID}) the key builder of `key` read, or an empty list when `key` is not
|
|
1038
|
+
# the last key {#collect_key_terms} built (a memo whose key reads no Frame passes nil).
|
|
1039
|
+
def key_terms_for(key)
|
|
1040
|
+
last = @last_key
|
|
1041
|
+
last && !key.nil? && last[0].equal?(key) ? last[1] : EMPTY_TERMS
|
|
1042
|
+
end
|
|
1043
|
+
|
|
1044
|
+
# Opens the audit of the miss computation `label` names, whose site declares `gaps` (slot name => reason).
|
|
1045
|
+
def open_key_audit(label, gaps)
|
|
1046
|
+
@key_audits.push([@key_installs.size, {}, label, gaps])
|
|
1047
|
+
nil
|
|
1048
|
+
end
|
|
1049
|
+
|
|
1050
|
+
# Closes the innermost audit and answers what its computation read from outside it: `slot => true` for a read of
|
|
1051
|
+
# its own, or `slot => [inner label, reason]` for one an audit nested in it declared a gap.
|
|
1052
|
+
def close_key_audit
|
|
1053
|
+
@key_audits.pop[1]
|
|
1054
|
+
end
|
|
1055
|
+
|
|
1056
|
+
EMPTY_TERMS = [].freeze
|
|
1057
|
+
private_constant :EMPTY_TERMS
|
|
1058
|
+
|
|
1059
|
+
private
|
|
1060
|
+
|
|
1061
|
+
def start_key_audit
|
|
1062
|
+
@key_audits = []
|
|
1063
|
+
@key_installs = []
|
|
1064
|
+
@key_terms = nil
|
|
1065
|
+
@last_key = nil
|
|
1066
|
+
end
|
|
1067
|
+
|
|
1068
|
+
# Innermost audit first: an audit whose computation installed the slot, and every audit around it, read it
|
|
1069
|
+
# internally.
|
|
1070
|
+
def note_frame_read(slot)
|
|
1071
|
+
declared = TypingState.slots[slot]
|
|
1072
|
+
return unless declared.kind == :frame
|
|
1073
|
+
|
|
1074
|
+
at = @key_installs.rindex(slot)
|
|
1075
|
+
audits = @key_audits
|
|
1076
|
+
gap = nil
|
|
1077
|
+
index = audits.size - 1
|
|
1078
|
+
while index >= 0
|
|
1079
|
+
mark, reads, label, gaps = audits[index]
|
|
1080
|
+
break if at && at >= mark
|
|
1081
|
+
|
|
1082
|
+
if gap
|
|
1083
|
+
reads[slot] ||= gap
|
|
1084
|
+
else
|
|
1085
|
+
reads[slot] = true
|
|
1086
|
+
reason = gaps[declared.name]
|
|
1087
|
+
gap = [label, reason].freeze if reason
|
|
1088
|
+
end
|
|
1089
|
+
index -= 1
|
|
1090
|
+
end
|
|
1091
|
+
end
|
|
1092
|
+
end
|
|
1093
|
+
end
|
|
1094
|
+
end
|