rigortype 0.2.9 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/data/builtins/ruby_core/array.yml +416 -392
- data/data/builtins/ruby_core/file.yml +42 -42
- data/data/builtins/ruby_core/hash.yml +302 -302
- data/data/builtins/ruby_core/io.yml +191 -191
- data/data/builtins/ruby_core/numeric.yml +321 -366
- data/data/builtins/ruby_core/proc.yml +124 -124
- data/data/builtins/ruby_core/range.yml +21 -21
- data/data/builtins/ruby_core/rational.yml +39 -39
- data/data/builtins/ruby_core/re.yml +65 -65
- data/data/builtins/ruby_core/set.yml +106 -106
- data/data/builtins/ruby_core/struct.yml +14 -14
- data/data/core_overlay/string_scanner.rbs +6 -5
- data/docs/handbook/01-getting-started.md +22 -34
- data/docs/handbook/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +19 -10
- data/docs/handbook/07-rbs-and-extended.md +76 -101
- data/docs/handbook/08-understanding-errors.md +114 -246
- data/docs/handbook/09-plugins.md +54 -144
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/README.md +5 -3
- data/docs/handbook/appendix-liskov.md +4 -2
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/handbook/appendix-phpstan.md +14 -7
- data/docs/handbook/appendix-steep.md +4 -2
- data/docs/handbook/appendix-type-theory.md +3 -1
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +98 -4
- data/docs/manual/02-cli-reference.md +81 -12
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +59 -4
- data/docs/manual/06-baseline.md +35 -1
- data/docs/manual/07-plugins.md +4 -4
- data/docs/manual/08-skills.md +6 -1
- data/docs/manual/09-editor-integration.md +7 -6
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- data/docs/manual/plugins/rigor-actioncable.md +32 -0
- data/docs/manual/plugins/rigor-devise.md +4 -2
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +112 -0
- data/lib/rigor/analysis/check_rules.rb +581 -98
- data/lib/rigor/analysis/dependency_recorder.rb +93 -9
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +456 -51
- data/lib/rigor/analysis/path_expansion.rb +42 -0
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
- data/lib/rigor/analysis/rule_catalog.rb +226 -1
- data/lib/rigor/analysis/run_cache_key.rb +78 -0
- data/lib/rigor/analysis/run_cache_probe.rb +72 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
- data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
- data/lib/rigor/analysis/runner.rb +344 -68
- data/lib/rigor/analysis/severity_stamp.rb +43 -0
- data/lib/rigor/analysis/worker_session.rb +30 -2
- data/lib/rigor/bleeding_edge.rb +37 -8
- data/lib/rigor/builtins/regex_refinement.rb +36 -1
- data/lib/rigor/cache/descriptor.rb +88 -28
- data/lib/rigor/cache/file_digest.rb +158 -0
- data/lib/rigor/cache/incremental_snapshot.rb +41 -5
- data/lib/rigor/cache/rbs_cache_producer.rb +11 -1
- data/lib/rigor/cache/rbs_descriptor.rb +56 -10
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +38 -0
- data/lib/rigor/cache/store.rb +132 -27
- data/lib/rigor/ci_detector.rb +90 -0
- data/lib/rigor/cli/annotate_command.rb +15 -9
- data/lib/rigor/cli/baseline_command.rb +4 -1
- data/lib/rigor/cli/check_command.rb +223 -32
- data/lib/rigor/cli/check_invocation.rb +84 -0
- data/lib/rigor/cli/coverage_command.rb +6 -1
- data/lib/rigor/cli/docs_command.rb +0 -30
- data/lib/rigor/cli/doctor_command.rb +169 -31
- data/lib/rigor/cli/lsp_command.rb +5 -0
- data/lib/rigor/cli/mcp_command.rb +5 -0
- data/lib/rigor/cli/plugins_command.rb +36 -6
- data/lib/rigor/cli/plugins_renderer.rb +44 -12
- data/lib/rigor/cli/probe_environment.rb +85 -0
- data/lib/rigor/cli/sig_gen_command.rb +36 -9
- data/lib/rigor/cli/skill_command.rb +22 -33
- data/lib/rigor/cli/skill_deep_probe.rb +172 -0
- data/lib/rigor/cli/skill_describe.rb +75 -9
- data/lib/rigor/cli/trace_command.rb +5 -6
- data/lib/rigor/cli/type_of_command.rb +9 -8
- data/lib/rigor/cli/type_scan_command.rb +8 -8
- data/lib/rigor/cli.rb +15 -4
- data/lib/rigor/config_audit.rb +48 -2
- data/lib/rigor/configuration/severity_profile.rb +31 -0
- data/lib/rigor/configuration.rb +156 -8
- data/lib/rigor/environment/default_libraries.rb +36 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +29 -28
- data/lib/rigor/inference/budget_trace.rb +77 -2
- data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
- data/lib/rigor/inference/def_handle.rb +23 -0
- data/lib/rigor/inference/def_node_resolver.rb +90 -0
- data/lib/rigor/inference/def_return_typer.rb +2 -1
- data/lib/rigor/inference/expression_typer.rb +361 -113
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +17 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
- data/lib/rigor/inference/method_dispatcher.rb +57 -1
- data/lib/rigor/inference/mutation_widening.rb +42 -8
- data/lib/rigor/inference/narrowing.rb +127 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +15 -0
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +8 -1
- data/lib/rigor/inference/scope_indexer.rb +451 -36
- data/lib/rigor/inference/statement_evaluator.rb +64 -15
- data/lib/rigor/inference/struct_fold_safety.rb +48 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
- data/lib/rigor/inference/void_origin.rb +25 -0
- data/lib/rigor/inference/void_tail_summary.rb +220 -0
- data/lib/rigor/language_server/buffer_resolution.rb +6 -3
- data/lib/rigor/language_server/buffer_table.rb +46 -6
- data/lib/rigor/language_server/completion_provider.rb +2 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -0
- data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
- data/lib/rigor/language_server/folding_range_provider.rb +2 -1
- data/lib/rigor/language_server/incremental_sync.rb +159 -0
- data/lib/rigor/language_server/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/server.rb +19 -9
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/language_server.rb +1 -0
- data/lib/rigor/plugin/base.rb +38 -24
- data/lib/rigor/plugin/inflector.rb +12 -3
- data/lib/rigor/plugin/io_boundary.rb +5 -1
- data/lib/rigor/plugin/isolation.rb +81 -11
- data/lib/rigor/plugin/load_error.rb +10 -1
- data/lib/rigor/plugin/loader.rb +96 -14
- data/lib/rigor/plugin/node_rule_walk.rb +5 -3
- data/lib/rigor/plugin/registry.rb +32 -23
- data/lib/rigor/plugin.rb +26 -0
- data/lib/rigor/protection/mutator.rb +3 -2
- data/lib/rigor/reflection.rb +64 -0
- data/lib/rigor/runtime/jit.rb +128 -0
- data/lib/rigor/scope/discovery_index.rb +12 -2
- data/lib/rigor/scope.rb +122 -28
- data/lib/rigor/sig_gen/classification.rb +5 -1
- data/lib/rigor/sig_gen/generator.rb +38 -6
- data/lib/rigor/sig_gen/observation_collector.rb +4 -3
- data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
- data/lib/rigor/sig_gen/renderer.rb +10 -0
- data/lib/rigor/sig_gen/write_result.rb +9 -4
- data/lib/rigor/sig_gen/writer.rb +207 -49
- data/lib/rigor/source/node_children.rb +116 -0
- data/lib/rigor/source/node_locator.rb +3 -1
- data/lib/rigor/source/node_walker.rb +4 -2
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/difference.rb +28 -24
- data/lib/rigor/type/hash_shape.rb +34 -10
- data/lib/rigor/version.rb +1 -1
- data/lib/rigortype.rb +24 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +51 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +3 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
- data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +7 -5
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
- data/sig/prism_node_children.rbs +9 -0
- data/sig/rigor/cache.rbs +6 -0
- data/sig/rigor/inference/void_origin.rbs +18 -0
- data/sig/rigor/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +6 -6
- data/sig/rigor/reflection.rbs +1 -0
- data/sig/rigor/scope.rbs +12 -1
- data/skills/rigor-ci-setup/SKILL.md +10 -0
- data/skills/rigor-editor-setup/SKILL.md +11 -0
- data/skills/rigor-next-steps/SKILL.md +23 -2
- data/skills/rigor-plugin-author/SKILL.md +3 -3
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
- data/skills/rigor-plugin-review/SKILL.md +2 -2
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
- data/skills/rigor-project-init/SKILL.md +29 -3
- data/skills/rigor-project-init/references/01-detect.md +1 -1
- data/skills/rigor-project-init/references/02-configure.md +39 -5
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
- data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
- metadata +48 -26
- data/lib/rigor/cli/ci_detector.rb +0 -89
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "../type"
|
|
6
|
+
require_relative "../reflection"
|
|
6
7
|
require_relative "../ast"
|
|
7
8
|
require_relative "../source/constant_path"
|
|
9
|
+
require_relative "../source/node_children"
|
|
8
10
|
require_relative "../analysis/self_call_resolution_recorder"
|
|
9
11
|
require_relative "block_parameter_binder"
|
|
10
12
|
require_relative "body_fixpoint"
|
|
@@ -209,6 +211,11 @@ module Rigor
|
|
|
209
211
|
}.freeze
|
|
210
212
|
private_constant :PRISM_DISPATCH
|
|
211
213
|
|
|
214
|
+
# Sentinel distinguishing "the key is not a value-pinned literal" from the legitimate literal hash
|
|
215
|
+
# keys `nil` and `false` (see {#static_hash_key}).
|
|
216
|
+
NO_STATIC_HASH_KEY = Object.new.freeze
|
|
217
|
+
private_constant :NO_STATIC_HASH_KEY
|
|
218
|
+
|
|
212
219
|
def initialize(scope:, tracer: nil)
|
|
213
220
|
@scope = scope
|
|
214
221
|
@tracer = tracer
|
|
@@ -244,6 +251,29 @@ module Rigor
|
|
|
244
251
|
fallback_for(node, family: :prism)
|
|
245
252
|
end
|
|
246
253
|
|
|
254
|
+
# ADR-89 WD2 — the return type of `def_node` called with `receiver` / `arg_types`, computed exactly as a
|
|
255
|
+
# resolved in-body dispatch does (through the ADR-84 memo, so it is cheap and yields FINAL values only).
|
|
256
|
+
# Public entry point for the incremental session's return-summary re-evaluation: it re-drives a
|
|
257
|
+
# declaration-stable changed callee at each previously-observed call key to prove its return is
|
|
258
|
+
# unchanged before skipping the callee's symbol dependents. nil for an abstract / bodyless def, or when
|
|
259
|
+
# the memo refuses a transient result — the caller treats either as "not provably stable" (keeps the
|
|
260
|
+
# dependents), the conservative direction.
|
|
261
|
+
def return_type_for(def_node, receiver, arg_types)
|
|
262
|
+
infer_user_method_return(def_node, receiver, arg_types)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# ADR-89 WD2 — the current run's return memo bucket as `{ def_node => [MemoEntry, …] }` (only entries
|
|
266
|
+
# that carry a call descriptor, i.e. every stored entry). Read by the incremental session right after a
|
|
267
|
+
# recording run to harvest each analyzed callee's observed call keys → return descriptors. Returns an
|
|
268
|
+
# empty hash when no bucket exists (a run that memoised nothing). Class method: the bucket lives on
|
|
269
|
+
# `Thread.current`, independent of any one `ExpressionTyper` instance.
|
|
270
|
+
def self.harvest_return_memo
|
|
271
|
+
slot = Thread.current[RETURN_MEMO_KEY]
|
|
272
|
+
return {} if slot.nil?
|
|
273
|
+
|
|
274
|
+
slot[1].transform_values(&:values)
|
|
275
|
+
end
|
|
276
|
+
|
|
247
277
|
private
|
|
248
278
|
|
|
249
279
|
attr_reader :scope, :tracer
|
|
@@ -413,65 +443,21 @@ module Rigor
|
|
|
413
443
|
inner
|
|
414
444
|
end
|
|
415
445
|
|
|
416
|
-
#
|
|
417
|
-
#
|
|
418
|
-
#
|
|
419
|
-
#
|
|
420
|
-
# the caller decides whether to fall back.
|
|
446
|
+
# Resolves a constant reference through Ruby's lexical constant lookup. Delegates to the shared
|
|
447
|
+
# `Reflection.resolve_constant_type` owner so the same walk (registry singleton, discovered class,
|
|
448
|
+
# in-source value, RBS constant, across the peeled `::` prefix candidates) is reused by
|
|
449
|
+
# `Inference::Narrowing`'s `Constant[Regexp]` match-operand recognition. Returns the matched
|
|
450
|
+
# `Rigor::Type` or nil; the caller decides whether to fall back.
|
|
421
451
|
def resolve_constant_name(name)
|
|
422
|
-
|
|
423
|
-
discovered = scope.discovered_classes
|
|
424
|
-
in_source = scope.in_source_constants
|
|
425
|
-
lexical_constant_candidates(name).each do |candidate|
|
|
426
|
-
singleton = env.singleton_for_name(candidate)
|
|
427
|
-
return singleton if singleton
|
|
428
|
-
|
|
429
|
-
in_source_class = discovered[candidate]
|
|
430
|
-
return in_source_class if in_source_class
|
|
431
|
-
|
|
432
|
-
# In-source value-bearing constants take precedence over RBS constant decls because user code is
|
|
433
|
-
# the authoritative source for its own constants.
|
|
434
|
-
in_source_value = in_source[candidate]
|
|
435
|
-
return in_source_value if in_source_value
|
|
436
|
-
|
|
437
|
-
value = env.constant_for_name(candidate)
|
|
438
|
-
return value if value
|
|
439
|
-
end
|
|
440
|
-
nil
|
|
441
|
-
end
|
|
442
|
-
|
|
443
|
-
# The candidate qualified names to try, in Ruby's lexical order: most-qualified first (the surrounding
|
|
444
|
-
# class path joined to `name`), then progressively less-qualified, then the bare `name`. Top-level
|
|
445
|
-
# scopes (no `self_type`) yield only `[name]`, preserving the pre-walk behaviour.
|
|
446
|
-
def lexical_constant_candidates(name)
|
|
447
|
-
prefix = enclosing_class_path
|
|
448
|
-
candidates = []
|
|
449
|
-
while prefix && !prefix.empty?
|
|
450
|
-
candidates << "#{prefix}::#{name}"
|
|
451
|
-
# Strip the last `::` segment without `rpartition`'s throwaway 3-element array + extra substrings
|
|
452
|
-
# (this loop is the sole caller of the `String#rpartition` allocation seen in the profile): `rindex`
|
|
453
|
-
# + slice gives the same prefix, or nil.
|
|
454
|
-
idx = prefix.rindex("::")
|
|
455
|
-
prefix = idx ? prefix[0, idx] : nil
|
|
456
|
-
end
|
|
457
|
-
candidates << name
|
|
458
|
-
candidates
|
|
459
|
-
end
|
|
460
|
-
|
|
461
|
-
# Pulls the enclosing qualified class name out of `scope.self_type` when one is set. `Nominal[T]` and
|
|
462
|
-
# `Singleton[T]` both expose `class_name`. Returns nil when no class context is available (top-level).
|
|
463
|
-
def enclosing_class_path
|
|
464
|
-
st = scope.self_type
|
|
465
|
-
case st
|
|
466
|
-
when Type::Nominal, Type::Singleton then st.class_name
|
|
467
|
-
end
|
|
452
|
+
Reflection.resolve_constant_type(name, scope: scope)
|
|
468
453
|
end
|
|
469
454
|
|
|
470
455
|
# Slice 5 phase 1 upgrades hash literals to `HashShape{...}` when every entry is a static `AssocNode`
|
|
471
|
-
# whose key is a
|
|
472
|
-
#
|
|
473
|
-
#
|
|
474
|
-
#
|
|
456
|
+
# whose key is a value-pinned scalar literal — Symbol, plain String, Integer, Float, `true`, `false`,
|
|
457
|
+
# or `nil` (covering `{ a: 1, "b" => 2 }` and `{ 1 => 2, 1.0 => 4 }` alike) — falling back to the
|
|
458
|
+
# generic `Hash[K, V]` form otherwise. Splatted entries (`{ **other }`) and dynamic keys widen to the
|
|
459
|
+
# underlying `Hash[K, V]` form by unioning the types each entry exposes; when no concrete pair
|
|
460
|
+
# survives we fall back to the raw `Hash` so callers stay backward compatible.
|
|
475
461
|
def type_of_hash(node)
|
|
476
462
|
elements = node.respond_to?(:elements) ? node.elements : []
|
|
477
463
|
# v0.0.7 — `{}` resolves to the empty `HashShape{}` carrier rather than `Nominal[Hash]`, mirroring the
|
|
@@ -491,16 +477,22 @@ module Rigor
|
|
|
491
477
|
)
|
|
492
478
|
end
|
|
493
479
|
|
|
494
|
-
# Builds `HashShape{...}` when every entry is an `AssocNode` whose key is a
|
|
495
|
-
# literal
|
|
480
|
+
# Builds `HashShape{...}` when every entry is an `AssocNode` whose key is a value-pinned scalar
|
|
481
|
+
# literal (Symbol, plain String, Integer, Float, true, false, nil). Returns nil otherwise so the
|
|
482
|
+
# caller falls back to the generic shape.
|
|
483
|
+
#
|
|
484
|
+
# Duplicate keys are LAST-WINS, matching the runtime (`{ a: 1, a: 2 }` keeps `a: 2`; Ruby itself
|
|
485
|
+
# only warns under `-w`). Key identity is Ruby `Hash` `eql?` identity because `pairs` is a native
|
|
486
|
+
# Hash — `1` and `1.0` stay distinct entries, `1.0` and `1.00` collide. The plain re-assignment
|
|
487
|
+
# also reproduces the runtime's ordering: the key keeps its FIRST insertion position while the
|
|
488
|
+
# value comes from the LAST occurrence.
|
|
496
489
|
def static_hash_shape_for(elements)
|
|
497
490
|
pairs = {}
|
|
498
491
|
elements.each do |entry|
|
|
499
492
|
return nil unless entry.is_a?(Prism::AssocNode)
|
|
500
493
|
|
|
501
494
|
key = static_hash_key(entry.key)
|
|
502
|
-
return nil if key.
|
|
503
|
-
return nil if pairs.key?(key)
|
|
495
|
+
return nil if key.equal?(NO_STATIC_HASH_KEY)
|
|
504
496
|
|
|
505
497
|
pairs[key] = type_of(entry.value)
|
|
506
498
|
end
|
|
@@ -509,16 +501,22 @@ module Rigor
|
|
|
509
501
|
Type::Combinator.hash_shape_of(pairs)
|
|
510
502
|
end
|
|
511
503
|
|
|
512
|
-
# Returns the
|
|
513
|
-
#
|
|
514
|
-
#
|
|
504
|
+
# Returns the value-pinned scalar literal carried by a hash key node, or {NO_STATIC_HASH_KEY} when
|
|
505
|
+
# the key is dynamic (a computed expression, an interpolated string — SymbolNode#value /
|
|
506
|
+
# StringNode#unescaped are nil under interpolation — a constant, a local, …). `nil` / `false` are
|
|
507
|
+
# real key values here, hence the sentinel rather than a nil return.
|
|
515
508
|
def static_hash_key(node)
|
|
516
509
|
case node
|
|
517
510
|
when Prism::SymbolNode
|
|
518
511
|
raw = node.value
|
|
519
|
-
raw
|
|
512
|
+
raw.nil? ? NO_STATIC_HASH_KEY : raw.to_sym
|
|
520
513
|
when Prism::StringNode
|
|
521
|
-
node.unescaped
|
|
514
|
+
node.unescaped || NO_STATIC_HASH_KEY
|
|
515
|
+
when Prism::IntegerNode, Prism::FloatNode then node.value
|
|
516
|
+
when Prism::TrueNode then true
|
|
517
|
+
when Prism::FalseNode then false
|
|
518
|
+
when Prism::NilNode then nil
|
|
519
|
+
else NO_STATIC_HASH_KEY
|
|
522
520
|
end
|
|
523
521
|
end
|
|
524
522
|
|
|
@@ -1609,18 +1607,106 @@ module Rigor
|
|
|
1609
1607
|
# args change folds (`factorial(5)` vs `factorial(6)`), so a coarser key would serve a stale fold. The
|
|
1610
1608
|
# third unsafe dimension — the ADR-55 recursion machinery (unroll fuel / fixpoint Kleene
|
|
1611
1609
|
# assumption / WD1 clamp) producing a TRANSIENT result rather than a final return — is excluded
|
|
1612
|
-
#
|
|
1613
|
-
#
|
|
1614
|
-
#
|
|
1610
|
+
# post-hoc (ADR-84 WD3): candidacy only requires that the plain signature not already be on the
|
|
1611
|
+
# recursion guard stack (a frame inside its own cycle returns a Kleene iterate by construction), and
|
|
1612
|
+
# the store gate brackets the compute over the transient-event depth log
|
|
1613
|
+
# (TRANSIENT_EVENT_DEPTHS_KEY), refusing only when an event referenced a frame BELOW the bracket's
|
|
1614
|
+
# entry depth — proof the ancestor context (an in-flight ancestor summary, the shared unroll fuel, a
|
|
1615
|
+
# possibly-ancestor-caused clamp) influenced the result. Events at-or-above the entry depth are the
|
|
1616
|
+
# compute's own deterministic machinery — a recursive method's own converged fixpoint stores from a
|
|
1617
|
+
# top-of-stack entry (standalone by construction: fuel reseeds, summary tables cleared at the
|
|
1618
|
+
# previous drain) — and a nested compute whose bracket saw no below-entry event ran as if standalone
|
|
1619
|
+
# (fuel consumed without exhausting is invisible; every summary read fires a logged guard event). A
|
|
1620
|
+
# memo HIT while an unroll is merely in flight elsewhere on the stack is legal (stored values are
|
|
1621
|
+
# final and context-free). This replaced the blanket unroll-in-flight candidacy exclusion, which
|
|
1622
|
+
# refused 82% of mail's body evaluations for results that were overwhelmingly final (PR #79
|
|
1623
|
+
# counters; ADR-84).
|
|
1615
1624
|
#
|
|
1616
|
-
#
|
|
1617
|
-
#
|
|
1618
|
-
#
|
|
1619
|
-
#
|
|
1620
|
-
#
|
|
1625
|
+
# Bucket scope (ADR-84 WD2): ONE bucket per analysis run, keyed by the identity of the run-generation
|
|
1626
|
+
# token `Analysis::Runner#run_analysis` mints and seeds through `scope.discovery` — hits cross
|
|
1627
|
+
# consumer-file boundaries within a run, and a re-run in the same process (LSP re-check, ADR-62 warm
|
|
1628
|
+
# loop) rolls the bucket over. Scopes without a runner seed (single-file probes) fall back to the
|
|
1629
|
+
# per-file merged `discovered_def_nodes` identity — exactly the pre-WD2 per-file scope. Inside the
|
|
1630
|
+
# bucket, entries key on the `def_node`'s object identity (the identity Hash holds the node strongly,
|
|
1631
|
+
# so a collected parse can never recycle an object id into a stale hit) and then on the
|
|
1632
|
+
# `[receiver, *args]` descriptor tuple; a callee may appear under at most two node identities per run
|
|
1633
|
+
# (the project-index parse every OTHER file resolves through, plus the defining file's own analysis
|
|
1634
|
+
# parse). Only the current generation's bucket is retained (single slot) — a rolled-over run's entries
|
|
1635
|
+
# become garbage instead of accumulating across LSP runs. `ExpressionTyper` is rebuilt per
|
|
1636
|
+
# `Scope#type_of`, so the store lives on `Thread.current`; fork-pool workers are separate processes,
|
|
1637
|
+
# so it never crosses a project boundary (ADR-84 WD5).
|
|
1621
1638
|
RETURN_MEMO_KEY = :__rigor_user_method_return_memo__
|
|
1622
1639
|
private_constant :RETURN_MEMO_KEY
|
|
1623
1640
|
|
|
1641
|
+
# ADR-84 WD2 — one memo entry: the final inferred return plus, when the entry was computed under
|
|
1642
|
+
# ADR-46 dependency recording, the frozen `Analysis::DependencyRecorder::ReadSet` its body walk
|
|
1643
|
+
# produced (nil on non-recording runs — recording is decided per run and the bucket never outlives a
|
|
1644
|
+
# run, so a recording run only ever hits entries that carry a read-set; pinned by
|
|
1645
|
+
# return_memo_recording_spec's rollover example). ADR-89 WD2 additionally carries the CALL DESCRIPTOR
|
|
1646
|
+
# (`receiver` + `arg_types`) so the incremental session can harvest each observed call key and persist
|
|
1647
|
+
# it as a return-summary — a recheck re-evaluates the (declaration-stable) callee at those keys and,
|
|
1648
|
+
# when every return is unchanged, skips its symbol dependents. Carrying two frozen type refs is
|
|
1649
|
+
# negligible on the memo-store path (already in scope) and never enters memo equality (entries are
|
|
1650
|
+
# keyed by `memo_key`, retrieved by `.result` / `.read_set`).
|
|
1651
|
+
MemoEntry = Data.define(:result, :read_set, :receiver, :arg_types)
|
|
1652
|
+
private_constant :MemoEntry
|
|
1653
|
+
|
|
1654
|
+
# ADR-84 WD3 — thread-local transient-machinery event log, appended by `note_transient_fallback` at
|
|
1655
|
+
# EVERY site where the ADR-55 recursion machinery substitutes transient state for a plain body
|
|
1656
|
+
# evaluation. Each entry is the STACK POSITION (guard-stack index) of the frame whose in-flight state
|
|
1657
|
+
# the event referenced — the consulted owner for a guard hit, 0 for the whole-stack resources (shared
|
|
1658
|
+
# unroll fuel; conservatively the clamp), the owner's own position for its cap collapse.
|
|
1659
|
+
# `consult_and_store_return_memo` brackets a candidate's compute over the log and refuses to store
|
|
1660
|
+
# only when an event referenced a frame BELOW the bracket's entry depth: such an event proves the
|
|
1661
|
+
# ancestor context influenced the result, while events at-or-above the entry depth are the compute's
|
|
1662
|
+
# OWN deterministic machinery (a recursive method's own converged fixpoint, a sub-cycle that opened
|
|
1663
|
+
# and closed inside the bracket) which a standalone recompute reproduces event-for-event. A
|
|
1664
|
+
# top-of-stack compute (entry depth 0) can never see a below-entry event — it is standalone by
|
|
1665
|
+
# construction (fuel reseeds on an empty stack, the summary tables were cleared at the previous
|
|
1666
|
+
# drain). Cleared with the other per-outermost-entry tables when the guard stack drains. LOAD-BEARING:
|
|
1667
|
+
# unlike `BudgetTrace` counters the log must be maintained on every run, not only under
|
|
1668
|
+
# RIGOR_BUDGET_TRACE.
|
|
1669
|
+
#
|
|
1670
|
+
# Audit table (ADR-84 WD3) — every early-return / fallback in the recursion machinery, each either
|
|
1671
|
+
# LOGGED (routes through `note_transient_fallback` with the referenced frame's position) or PROVABLY
|
|
1672
|
+
# FINAL (a deterministic pure function of the frame's own inputs, so a standalone recompute
|
|
1673
|
+
# reproduces it):
|
|
1674
|
+
#
|
|
1675
|
+
# compute_user_method_return
|
|
1676
|
+
# - in-cycle re-entry (`stack.include?(signature)` → `consult_summary`) LOGGED at the consulted
|
|
1677
|
+
# owner's stack position (the outermost frame carrying the plain signature — the seeder whose
|
|
1678
|
+
# in-flight Kleene iterate the re-entry returns)
|
|
1679
|
+
# evaluate_guarded_user_method_body
|
|
1680
|
+
# - non-outermost plain evaluation, clamp pass-through PROVABLY FINAL (plain
|
|
1681
|
+
# body eval; nested transient events log at their own sites)
|
|
1682
|
+
# - `clamp_unroll_result` clamp branch (ADR-55 WD1 → `untyped`) LOGGED at 0 (the
|
|
1683
|
+
# would-have-been-guarded match may be an ancestor frame; position not threaded — conservative)
|
|
1684
|
+
# fixpoint_user_method_return
|
|
1685
|
+
# - `degrade_entangled_fixpoint` (foreign in-flight consult) LOGGED at the degrading
|
|
1686
|
+
# owner's own position (the foreign consult that caused it was already logged at the ancestor's
|
|
1687
|
+
# position by the guard site, which is what taints the enclosing brackets)
|
|
1688
|
+
# - "summary never consulted" early return (body did not recurse) PROVABLY FINAL
|
|
1689
|
+
# - `resolve_bot_collapse` (widened re-run / explicit-return floor) PROVABLY FINAL in itself
|
|
1690
|
+
# (deterministic); transient events inside the re-run log at their own sites
|
|
1691
|
+
# - `fixpoint_step` convergence (`joined == assumption`) converged value; any
|
|
1692
|
+
# consults the iterations performed were already LOGGED at the guard site
|
|
1693
|
+
# - `fixpoint_step` cap collapse (`RECURSION_FIXPOINT_CAP` → `untyped`) LOGGED at the owner's own
|
|
1694
|
+
# position (per-owner constant cap — deterministic for every enclosing bracket; the iterates'
|
|
1695
|
+
# context-dependence, if any, logs separately at its own sites)
|
|
1696
|
+
# unroll helpers
|
|
1697
|
+
# - `unroll_fuel_remaining` exhaustion (extended key denied → plain) LOGGED at 0 (fuel is ONE
|
|
1698
|
+
# shared resource seeded at stack bottom, so a nested frame's unroll budget depends on what ran
|
|
1699
|
+
# before it — the hazard the blanket exclusion used to over-approximate)
|
|
1700
|
+
# - `constant_argument_value_key` / `pinned_value_descriptor` nil PROVABLY FINAL (pure
|
|
1701
|
+
# functions of `arg_types`)
|
|
1702
|
+
#
|
|
1703
|
+
# Adding a fallback? Route it through `note_transient_fallback` unless it is provably final in the
|
|
1704
|
+
# sense above — position 0 is always a sound (conservative) choice; spec/rigor/inference/
|
|
1705
|
+
# return_memo_taint_spec.rb pins this (no raw `BudgetTrace.hit` on the recursion categories outside
|
|
1706
|
+
# the helper).
|
|
1707
|
+
TRANSIENT_EVENT_DEPTHS_KEY = :__rigor_user_method_transient_event_depths__
|
|
1708
|
+
private_constant :TRANSIENT_EVENT_DEPTHS_KEY
|
|
1709
|
+
|
|
1624
1710
|
# Per-inference recursion context threaded through the guard / fixpoint helpers (ADR-55 slice 2).
|
|
1625
1711
|
# Bundles the call descriptor (`receiver`, `arg_types`, `plain_signature`), the thread-local summary
|
|
1626
1712
|
# table, and the WD1 clamp flag so the helpers stay within the parameter-list budget. `def_node` is
|
|
@@ -1647,7 +1733,7 @@ module Rigor
|
|
|
1647
1733
|
RECURSION_VALUE_SIZE_CAP = 64
|
|
1648
1734
|
private_constant :RECURSION_VALUE_SIZE_CAP
|
|
1649
1735
|
|
|
1650
|
-
def infer_user_method_return(def_node, receiver, arg_types)
|
|
1736
|
+
def infer_user_method_return(def_node, receiver, arg_types)
|
|
1651
1737
|
return nil if def_node.body.nil?
|
|
1652
1738
|
|
|
1653
1739
|
body_scope = build_user_method_body_scope(def_node, receiver, arg_types)
|
|
@@ -1664,6 +1750,9 @@ module Rigor
|
|
|
1664
1750
|
stack = (Thread.current[INFERENCE_GUARD_KEY] ||= [])
|
|
1665
1751
|
summaries = (Thread.current[INFERENCE_SUMMARY_KEY] ||= {})
|
|
1666
1752
|
|
|
1753
|
+
# WD0 (RIGOR_BUDGET_TRACE) — one entry into user-method return inference. No-op when disabled.
|
|
1754
|
+
BudgetTrace.hit(BudgetTrace::MEMO_ENTRIES)
|
|
1755
|
+
|
|
1667
1756
|
# ADR-57 follow-up — return memo. The inferred return is a pure function of `(def_node, receiver,
|
|
1668
1757
|
# arg_types)` and the frozen discovery index whenever the computation does NOT depend on a transient
|
|
1669
1758
|
# ADR-55 Kleene assumption (an in-flight fixpoint summary). Two structural preconditions decide
|
|
@@ -1679,25 +1768,104 @@ module Rigor
|
|
|
1679
1768
|
# call, re-walking the shared sub-readers combinatorially (~932k body evaluations for ~20 tiny
|
|
1680
1769
|
# methods). The computation itself lives in `compute_user_method_return`.
|
|
1681
1770
|
unless memo_candidate?(stack, plain_signature)
|
|
1771
|
+
trace_memo_refusal(stack, plain_signature)
|
|
1682
1772
|
return compute_user_method_return(def_node, body_scope, stack, summaries,
|
|
1683
1773
|
receiver, arg_types, plain_signature)
|
|
1684
1774
|
end
|
|
1685
1775
|
|
|
1686
|
-
|
|
1687
|
-
|
|
1776
|
+
# INVARIANT (ADR-46 recording soundness, ADR-84 WD2) — the deep cross-file dependency edges (the
|
|
1777
|
+
# reads a callee body's dispatches perform through the instrumented `Scope` accessors) are recorded,
|
|
1778
|
+
# per consumer, only as a side effect of evaluating that body, and a memo hit serves the return
|
|
1779
|
+
# WITHOUT re-evaluating it. Since the bucket is run-scoped, hits CROSS consumer-file boundaries, so
|
|
1780
|
+
# every cross-file hit is PAIRED WITH CACHE-AND-REPLAY of the callee's read-set: under recording,
|
|
1781
|
+
# the first evaluation of a key captures the recorder events of its body walk
|
|
1782
|
+
# (`DependencyRecorder.capture` — observe-and-forward, so the first consumer's own record is
|
|
1783
|
+
# untouched) onto the entry, and a hit replays that set into the current consumer's accumulator
|
|
1784
|
+
# (`DependencyRecorder.replay`, which re-applies the per-consumer self-read filter). A memo hit is
|
|
1785
|
+
# thereby edge-equivalent to a fresh body evaluation for EVERY consumer. The naive alternative —
|
|
1786
|
+
# bypassing the memo while the recorder is active — measured >200x wall on analyzer-shaped files
|
|
1787
|
+
# (ActiveStorage video_analyzer.rb's subtree, 0.43s -> >90s; PR #79) and stays rejected. Pinned by
|
|
1788
|
+
# spec/rigor/inference/return_memo_recording_spec.rb (cross-file replay completeness) and
|
|
1789
|
+
# dependency_recorder_spec.rb's transitive deep-edge example.
|
|
1790
|
+
consult_and_store_return_memo(def_node, body_scope, stack, summaries,
|
|
1791
|
+
receiver, arg_types, plain_signature)
|
|
1792
|
+
end
|
|
1793
|
+
|
|
1794
|
+
# The candidate-frame memo path: consult the current run generation's bucket, and on a miss compute
|
|
1795
|
+
# and store a FINAL result. Reached only when `memo_candidate?` held (see `infer_user_method_return`).
|
|
1796
|
+
# The store gate (ADR-84 WD3): a result is stored when the ADR-55 fixpoint consult counter did not
|
|
1797
|
+
# move across the compute (the WD0 `MEMO_REFUSE_CONSULT_TAINTED` non-store) AND no transient-machinery
|
|
1798
|
+
# event during the bracket referenced a stack frame BELOW the bracket's entry depth (see
|
|
1799
|
+
# TRANSIENT_EVENT_DEPTHS_KEY). Below-entry events — an ancestor's in-flight Kleene iterate read by a
|
|
1800
|
+
# guard hit, a shared-fuel exhaustion, a possibly-ancestor-caused WD1 clamp — mean the ancestor
|
|
1801
|
+
# context influenced `result`, which a standalone recompute would not reproduce; at-or-above-entry
|
|
1802
|
+
# events are the compute's own deterministic machinery (its own converged fixpoint, sub-cycles that
|
|
1803
|
+
# opened and closed inside the bracket) and do not block the store. A top-of-stack compute (entry
|
|
1804
|
+
# depth 0) is standalone by construction. A hit under ADR-46 recording replays the entry's captured
|
|
1805
|
+
# read-set into the current consumer (see the INVARIANT comment at the call site).
|
|
1806
|
+
def consult_and_store_return_memo(def_node, body_scope, stack, summaries,
|
|
1807
|
+
receiver, arg_types, plain_signature)
|
|
1808
|
+
per_def = (return_memo_bucket[def_node] ||= {})
|
|
1809
|
+
memo_key = [receiver.describe(:short),
|
|
1688
1810
|
arg_types.map { |type| type.describe(:short) }]
|
|
1689
|
-
|
|
1811
|
+
if (entry = per_def[memo_key])
|
|
1812
|
+
BudgetTrace.hit(BudgetTrace::MEMO_HITS)
|
|
1813
|
+
Analysis::DependencyRecorder.replay(entry.read_set) if Analysis::DependencyRecorder.active?
|
|
1814
|
+
return entry.result
|
|
1815
|
+
end
|
|
1816
|
+
BudgetTrace.hit(BudgetTrace::MEMO_MISSES)
|
|
1817
|
+
trace_distinct_memo_key(plain_signature, def_node, memo_key)
|
|
1818
|
+
|
|
1819
|
+
entry_depth = stack.size
|
|
1820
|
+
event_mark = transient_event_mark
|
|
1821
|
+
consults_before = summary_consult_count
|
|
1822
|
+
result, read_set = compute_with_read_capture(def_node, body_scope, stack, summaries,
|
|
1823
|
+
receiver, arg_types, plain_signature)
|
|
1824
|
+
|
|
1825
|
+
if summary_consult_count != consults_before
|
|
1826
|
+
BudgetTrace.hit(BudgetTrace::MEMO_REFUSE_CONSULT_TAINTED)
|
|
1827
|
+
elsif context_tainted?(event_mark, entry_depth)
|
|
1828
|
+
BudgetTrace.hit(BudgetTrace::MEMO_REFUSE_TRANSIENT)
|
|
1829
|
+
else
|
|
1830
|
+
per_def[memo_key] = MemoEntry.new(result: result, read_set: read_set,
|
|
1831
|
+
receiver: receiver, arg_types: arg_types)
|
|
1832
|
+
end
|
|
1833
|
+
result
|
|
1834
|
+
end
|
|
1690
1835
|
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
consults_after = Thread.current[SUMMARY_CONSULT_COUNTER_KEY] || 0
|
|
1836
|
+
def transient_event_mark
|
|
1837
|
+
Thread.current[TRANSIENT_EVENT_DEPTHS_KEY]&.size || 0
|
|
1838
|
+
end
|
|
1695
1839
|
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1840
|
+
def summary_consult_count
|
|
1841
|
+
Thread.current[SUMMARY_CONSULT_COUNTER_KEY] || 0
|
|
1842
|
+
end
|
|
1843
|
+
|
|
1844
|
+
# ADR-84 WD3 — true when a transient-machinery event logged during the bracket (entries past
|
|
1845
|
+
# `event_mark`) referenced a frame below `entry_depth`. The log is cleared when the guard stack drains
|
|
1846
|
+
# (only possible mid-bracket for a top-of-stack compute, whose events are deterministic anyway), so a
|
|
1847
|
+
# missing / shorter log reads as untainted.
|
|
1848
|
+
def context_tainted?(event_mark, entry_depth)
|
|
1849
|
+
log = Thread.current[TRANSIENT_EVENT_DEPTHS_KEY]
|
|
1850
|
+
return false if log.nil? || log.size <= event_mark
|
|
1851
|
+
|
|
1852
|
+
log[event_mark..].any? { |depth| depth < entry_depth }
|
|
1853
|
+
end
|
|
1854
|
+
|
|
1855
|
+
# ADR-84 WD2 — the memo-miss compute, wrapped in a `DependencyRecorder.capture` window when ADR-46
|
|
1856
|
+
# recording is active so the entry can carry its replayable read-set. Returns `[result, read_set]`
|
|
1857
|
+
# (`read_set` nil when not recording).
|
|
1858
|
+
def compute_with_read_capture(def_node, body_scope, stack, summaries,
|
|
1859
|
+
receiver, arg_types, plain_signature)
|
|
1860
|
+
unless Analysis::DependencyRecorder.active?
|
|
1861
|
+
return [compute_user_method_return(def_node, body_scope, stack, summaries,
|
|
1862
|
+
receiver, arg_types, plain_signature), nil]
|
|
1863
|
+
end
|
|
1864
|
+
|
|
1865
|
+
Analysis::DependencyRecorder.capture do
|
|
1866
|
+
compute_user_method_return(def_node, body_scope, stack, summaries,
|
|
1867
|
+
receiver, arg_types, plain_signature)
|
|
1868
|
+
end
|
|
1701
1869
|
end
|
|
1702
1870
|
|
|
1703
1871
|
# The ADR-55 recursion-guard + value-unroll + fixpoint body of user-method return inference, factored
|
|
@@ -1705,6 +1873,7 @@ module Rigor
|
|
|
1705
1873
|
# unchanged from pre-memo behaviour).
|
|
1706
1874
|
def compute_user_method_return(def_node, body_scope, stack, summaries,
|
|
1707
1875
|
receiver, arg_types, plain_signature)
|
|
1876
|
+
trace_body_eval(plain_signature)
|
|
1708
1877
|
# ADR-55 slice 1: when every bound argument is value-pinned, extend the guard key with a stable
|
|
1709
1878
|
# descriptor of the argument *values* so distinct constant frames may recurse (e.g. `factorial(5)`
|
|
1710
1879
|
# folds to `Constant[120]`). Distinct constant frames are bounded by `RECURSION_UNROLL_FUEL` per
|
|
@@ -1716,7 +1885,10 @@ module Rigor
|
|
|
1716
1885
|
signature = [plain_signature, value_key] if extended
|
|
1717
1886
|
|
|
1718
1887
|
if stack.include?(signature)
|
|
1719
|
-
|
|
1888
|
+
# ADR-84 WD3 — the referenced frame is the consulted owner: the OUTERMOST frame carrying this
|
|
1889
|
+
# plain signature (the seeder whose in-flight iterate the re-entry returns).
|
|
1890
|
+
note_transient_fallback(BudgetTrace::RECURSION_GUARD,
|
|
1891
|
+
stack.index { |frame| plain_part(frame) == plain_signature } || 0)
|
|
1720
1892
|
# ADR-55 slice 2: in-cycle re-entries return the current assumed summary (Kleene iterate, seeded
|
|
1721
1893
|
# `bot`) instead of bare `untyped`. The fixpoint loop below seeds the entry on the outermost frame;
|
|
1722
1894
|
# if a re-entry beats it here the entry already exists. The WD4 composition: slice 1's clamp/fuel
|
|
@@ -1742,30 +1914,85 @@ module Rigor
|
|
|
1742
1914
|
evaluate_guarded_user_method_body(def_node, body_scope, stack, signature, context)
|
|
1743
1915
|
end
|
|
1744
1916
|
|
|
1745
|
-
# True when this frame's result is a candidate for the return memo: the structural
|
|
1746
|
-
# stable across the body walk, that
|
|
1747
|
-
#
|
|
1748
|
-
#
|
|
1749
|
-
#
|
|
1750
|
-
#
|
|
1751
|
-
#
|
|
1752
|
-
#
|
|
1753
|
-
#
|
|
1917
|
+
# True when this frame's result is a candidate for the return memo: the one structural precondition,
|
|
1918
|
+
# stable across the body walk, that is necessary (but not sufficient) for a FINAL result — this plain
|
|
1919
|
+
# signature must not itself be on the recursion guard stack (else we are inside its own cycle,
|
|
1920
|
+
# returning a Kleene iterate by construction). Sufficiency is decided post-hoc in
|
|
1921
|
+
# `consult_and_store_return_memo` by the two bracket counters (fixpoint consults + ADR-84 WD3
|
|
1922
|
+
# transient-machinery events) — so unlike the prior form this deliberately does NOT refuse while a
|
|
1923
|
+
# constant-arg unroll is in flight: a nested frame whose compute finishes without a single transient
|
|
1924
|
+
# event ran exactly as it would standalone (fuel consumption without exhaustion is invisible), and
|
|
1925
|
+
# the blanket exclusion refused 82% of mail's body evaluations for such final results (ADR-84).
|
|
1754
1926
|
def memo_candidate?(stack, plain_signature)
|
|
1755
|
-
|
|
1756
|
-
# unroll has begun iff the thread-local is set and the stack is non-empty (the `ensure` in
|
|
1757
|
-
# `evaluate_guarded_user_method_body` clears it at stack-empty).
|
|
1758
|
-
unroll_idle = stack.empty? || Thread.current[INFERENCE_UNROLL_FUEL_KEY].nil?
|
|
1759
|
-
unroll_idle &&
|
|
1760
|
-
stack.none? { |frame| plain_part(frame) == plain_signature }
|
|
1927
|
+
stack.none? { |frame| plain_part(frame) == plain_signature }
|
|
1761
1928
|
end
|
|
1762
1929
|
|
|
1763
|
-
# Run-scoped return-memo bucket
|
|
1764
|
-
#
|
|
1765
|
-
#
|
|
1930
|
+
# Run-scoped return-memo bucket (ADR-84 WD2): a single retained slot keyed by the identity of the
|
|
1931
|
+
# run-generation token the runner seeds (`Scope#run_generation`), falling back to the per-file merged
|
|
1932
|
+
# `def_nodes` table identity for runner-less scopes (single-file probes) — the pre-WD2 per-file
|
|
1933
|
+
# scope. A generation mismatch drops the previous bucket whole, so a re-run in one process (LSP,
|
|
1934
|
+
# ADR-62 warm loop) can neither hit stale entries nor accumulate them. The bucket maps `def_node`
|
|
1935
|
+
# (object identity, held strongly — see RETURN_MEMO_KEY) to its per-descriptor entries.
|
|
1766
1936
|
def return_memo_bucket
|
|
1767
|
-
|
|
1768
|
-
|
|
1937
|
+
generation = scope.run_generation || scope.discovered_def_nodes
|
|
1938
|
+
slot = Thread.current[RETURN_MEMO_KEY]
|
|
1939
|
+
unless slot && slot[0].equal?(generation)
|
|
1940
|
+
slot = [generation, {}.compare_by_identity]
|
|
1941
|
+
Thread.current[RETURN_MEMO_KEY] = slot
|
|
1942
|
+
end
|
|
1943
|
+
slot[1]
|
|
1944
|
+
end
|
|
1945
|
+
|
|
1946
|
+
# WD0 (RIGOR_BUDGET_TRACE) trace helpers. Each guards on `BudgetTrace.enabled?` before doing any work
|
|
1947
|
+
# beyond a single boolean check, so a normal run pays nothing (no signature-label string built, no
|
|
1948
|
+
# stack scan). Attributes a non-candidate frame's non-store to on-stack vs unroll-in-flight; since
|
|
1949
|
+
# ADR-84 WD3 reduced candidacy to the on-stack check alone, the unroll-in-flight branch is
|
|
1950
|
+
# structurally unreachable and is kept only so the printed counter table still shows the category
|
|
1951
|
+
# (pinned ~0 — the ADR-84 WD3 gate).
|
|
1952
|
+
def trace_memo_refusal(stack, plain_signature)
|
|
1953
|
+
return unless BudgetTrace.enabled?
|
|
1954
|
+
|
|
1955
|
+
if stack.any? { |frame| plain_part(frame) == plain_signature }
|
|
1956
|
+
BudgetTrace.hit(BudgetTrace::MEMO_REFUSE_ON_STACK)
|
|
1957
|
+
else
|
|
1958
|
+
BudgetTrace.hit(BudgetTrace::MEMO_REFUSE_UNROLL)
|
|
1959
|
+
end
|
|
1960
|
+
end
|
|
1961
|
+
|
|
1962
|
+
# The observed member is the full `(def-node identity, receiver, args)` entry key, so the per-signature
|
|
1963
|
+
# count also surfaces the ADR-84 WD2 dual-parse duplication (project-index parse vs the defining
|
|
1964
|
+
# file's own parse — at most 2 node identities per def per run).
|
|
1965
|
+
def trace_distinct_memo_key(plain_signature, def_node, memo_key)
|
|
1966
|
+
return unless BudgetTrace.enabled?
|
|
1967
|
+
|
|
1968
|
+
BudgetTrace.observe_distinct(
|
|
1969
|
+
BudgetTrace::MEMO_DISTINCT_KEY_BY_SIGNATURE, signature_label(plain_signature),
|
|
1970
|
+
[def_node.object_id, memo_key]
|
|
1971
|
+
)
|
|
1972
|
+
end
|
|
1973
|
+
|
|
1974
|
+
def trace_body_eval(plain_signature)
|
|
1975
|
+
return unless BudgetTrace.enabled?
|
|
1976
|
+
|
|
1977
|
+
BudgetTrace.hit(BudgetTrace::MEMO_BODY_EVALS)
|
|
1978
|
+
BudgetTrace.observe(BudgetTrace::MEMO_BODY_EVAL_BY_SIGNATURE, signature_label(plain_signature))
|
|
1979
|
+
end
|
|
1980
|
+
|
|
1981
|
+
# `"Receiver#method"` label for a `[receiver_descriptor, method_name]` plain signature — the bucket key
|
|
1982
|
+
# for the WD0 per-signature distributions. Built only under `BudgetTrace.enabled?`.
|
|
1983
|
+
def signature_label(plain_signature)
|
|
1984
|
+
"#{plain_signature[0]}##{plain_signature[1]}"
|
|
1985
|
+
end
|
|
1986
|
+
|
|
1987
|
+
# ADR-84 WD3 — the single choke point every transient-machinery fallback routes through: appends the
|
|
1988
|
+
# referenced frame's stack position to the load-bearing thread-local event log (regardless of
|
|
1989
|
+
# RIGOR_BUDGET_TRACE) and forwards the category to `BudgetTrace`. The taint spec pins that no
|
|
1990
|
+
# recursion-machinery `BudgetTrace.hit` exists outside this helper, so a future fallback cannot
|
|
1991
|
+
# silently join unlogged (see the audit table at TRANSIENT_EVENT_DEPTHS_KEY; `context_depth` 0 is
|
|
1992
|
+
# always a sound conservative choice).
|
|
1993
|
+
def note_transient_fallback(category, context_depth)
|
|
1994
|
+
(Thread.current[TRANSIENT_EVENT_DEPTHS_KEY] ||= []) << context_depth
|
|
1995
|
+
BudgetTrace.hit(category)
|
|
1769
1996
|
end
|
|
1770
1997
|
|
|
1771
1998
|
# Pushes the recursion-guard frame, evaluates the body (the outermost frame for a plain signature runs
|
|
@@ -1790,6 +2017,7 @@ module Rigor
|
|
|
1790
2017
|
Thread.current[INFERENCE_UNROLL_FUEL_KEY] = nil
|
|
1791
2018
|
Thread.current[INFERENCE_SUMMARY_KEY] = nil
|
|
1792
2019
|
Thread.current[SUMMARY_CONSULT_DEPTHS_KEY] = nil
|
|
2020
|
+
Thread.current[TRANSIENT_EVENT_DEPTHS_KEY] = nil
|
|
1793
2021
|
end
|
|
1794
2022
|
end
|
|
1795
2023
|
end
|
|
@@ -1879,12 +2107,21 @@ module Rigor
|
|
|
1879
2107
|
# soundness fix, 2026-06-12), parking `untyped` in the assumption so any consumer that still reads
|
|
1880
2108
|
# this signature's summary sees the floor, not the stale `bot` seed.
|
|
1881
2109
|
def degrade_entangled_fixpoint(summaries, plain_signature)
|
|
1882
|
-
|
|
2110
|
+
# ADR-84 WD3 — logged at the degrading owner's own position: the foreign consult that caused the
|
|
2111
|
+
# entanglement was already logged at the ANCESTOR's position by the guard site.
|
|
2112
|
+
note_transient_fallback(BudgetTrace::RECURSION_GUARD, own_guard_frame_position)
|
|
1883
2113
|
scope.record_dynamic_origin(@typing_node, DynamicOrigin::ANALYZER_BUDGET_CUTOFF) if @typing_node
|
|
1884
2114
|
summaries[plain_signature][:assumption] = Type::Combinator.untyped
|
|
1885
2115
|
Type::Combinator.untyped
|
|
1886
2116
|
end
|
|
1887
2117
|
|
|
2118
|
+
# The stack position of the currently-evaluating owner frame (the guard stack's top) — the
|
|
2119
|
+
# self-referential `context_depth` for events that are deterministic per owner (ADR-84 WD3).
|
|
2120
|
+
def own_guard_frame_position
|
|
2121
|
+
size = Thread.current[INFERENCE_GUARD_KEY]&.size || 0
|
|
2122
|
+
size.positive? ? size - 1 : 0
|
|
2123
|
+
end
|
|
2124
|
+
|
|
1888
2125
|
# One Kleene-iteration step of the fixpoint loop. Joins `computed` into the running assumption
|
|
1889
2126
|
# (widening value-pinned constituents on the final permitted iteration to force convergence) and
|
|
1890
2127
|
# either returns a final type — convergence, or the capped `untyped` collapse — or `:continue` to
|
|
@@ -1900,9 +2137,9 @@ module Rigor
|
|
|
1900
2137
|
return candidate if joined == assumption
|
|
1901
2138
|
|
|
1902
2139
|
if last_iteration
|
|
1903
|
-
# Out of iterations and still unstable — collapse to today's
|
|
1904
|
-
#
|
|
1905
|
-
|
|
2140
|
+
# Out of iterations and still unstable — collapse to today's widening behaviour. ADR-84 WD3: the
|
|
2141
|
+
# cap is a per-owner constant, so the event references the owner's own frame.
|
|
2142
|
+
note_transient_fallback(BudgetTrace::RECURSION_FIXPOINT_CAP, own_guard_frame_position)
|
|
1906
2143
|
scope.record_dynamic_origin(@typing_node, DynamicOrigin::ANALYZER_BUDGET_CUTOFF) if @typing_node
|
|
1907
2144
|
summaries[plain_signature][:assumption] = Type::Combinator.untyped
|
|
1908
2145
|
return Type::Combinator.untyped
|
|
@@ -1962,7 +2199,14 @@ module Rigor
|
|
|
1962
2199
|
return false if RETURN_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
|
|
1963
2200
|
return true if node.is_a?(Prism::ReturnNode)
|
|
1964
2201
|
|
|
1965
|
-
|
|
2202
|
+
found = false
|
|
2203
|
+
node.rigor_each_child do |child|
|
|
2204
|
+
next unless body_has_explicit_return?(child)
|
|
2205
|
+
|
|
2206
|
+
found = true
|
|
2207
|
+
break
|
|
2208
|
+
end
|
|
2209
|
+
found
|
|
1966
2210
|
end
|
|
1967
2211
|
|
|
1968
2212
|
# Returns the current assumed summary for `plain_signature`, recording that it was consulted (so the
|
|
@@ -1985,7 +2229,9 @@ module Rigor
|
|
|
1985
2229
|
def clamp_unroll_result(type, would_have_been_guarded)
|
|
1986
2230
|
return type unless would_have_been_guarded && !fully_value_pinned?(type)
|
|
1987
2231
|
|
|
1988
|
-
|
|
2232
|
+
# ADR-84 WD3 — position 0 (conservative): the would-have-been-guarded match may be an ancestor
|
|
2233
|
+
# frame, and the matched position is not threaded through RecursionContext.
|
|
2234
|
+
note_transient_fallback(BudgetTrace::RECURSION_GUARD, 0)
|
|
1989
2235
|
scope.record_dynamic_origin(@typing_node, DynamicOrigin::ANALYZER_BUDGET_CUTOFF) if @typing_node
|
|
1990
2236
|
# ADR-55 WD1 clamp: a guarded extended frame whose body is non-pinned must be byte-identical to the
|
|
1991
2237
|
# plain guard's `untyped`. This path deliberately does NOT route to the in-progress fixpoint summary:
|
|
@@ -2016,7 +2262,9 @@ module Rigor
|
|
|
2016
2262
|
if remaining.positive?
|
|
2017
2263
|
Thread.current[INFERENCE_UNROLL_FUEL_KEY] = remaining - 1
|
|
2018
2264
|
else
|
|
2019
|
-
|
|
2265
|
+
# ADR-84 WD3 — position 0: fuel is one shared resource seeded at the stack bottom, so its
|
|
2266
|
+
# exhaustion is context-dependent for every nested bracket.
|
|
2267
|
+
note_transient_fallback(BudgetTrace::RECURSION_UNROLL_FUEL, 0)
|
|
2020
2268
|
end
|
|
2021
2269
|
remaining
|
|
2022
2270
|
end
|