rigortype 0.1.17 → 0.1.19

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.
Files changed (125) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +159 -222
  3. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +24 -1
  4. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
  5. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +29 -0
  6. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
  7. data/lib/rigor/analysis/check_rules/rule_walk.rb +213 -0
  8. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +24 -1
  9. data/lib/rigor/analysis/check_rules.rb +275 -44
  10. data/lib/rigor/analysis/diagnostic.rb +8 -0
  11. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +581 -0
  12. data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
  13. data/lib/rigor/analysis/runner/project_pre_passes.rb +321 -0
  14. data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
  15. data/lib/rigor/analysis/runner.rb +207 -1200
  16. data/lib/rigor/analysis/worker_session.rb +60 -11
  17. data/lib/rigor/bleeding_edge.rb +123 -0
  18. data/lib/rigor/cache/descriptor.rb +86 -8
  19. data/lib/rigor/cache/incremental_snapshot.rb +10 -4
  20. data/lib/rigor/cache/rbs_cache_producer.rb +5 -1
  21. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  22. data/lib/rigor/cache/store.rb +46 -13
  23. data/lib/rigor/cli/annotate_command.rb +100 -15
  24. data/lib/rigor/cli/check_command.rb +708 -0
  25. data/lib/rigor/cli/ci_detector.rb +94 -0
  26. data/lib/rigor/cli/diagnostic_formats.rb +345 -0
  27. data/lib/rigor/cli/plugins_command.rb +2 -4
  28. data/lib/rigor/cli/plugins_renderer.rb +0 -2
  29. data/lib/rigor/cli/prism_colorizer.rb +10 -3
  30. data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
  31. data/lib/rigor/cli/trace_command.rb +143 -0
  32. data/lib/rigor/cli/trace_renderer.rb +310 -0
  33. data/lib/rigor/cli/triage_command.rb +6 -3
  34. data/lib/rigor/cli/triage_renderer.rb +15 -1
  35. data/lib/rigor/cli.rb +21 -612
  36. data/lib/rigor/configuration/severity_profile.rb +13 -1
  37. data/lib/rigor/configuration.rb +66 -7
  38. data/lib/rigor/environment/rbs_loader.rb +78 -68
  39. data/lib/rigor/environment.rb +1 -1
  40. data/lib/rigor/inference/acceptance.rb +10 -0
  41. data/lib/rigor/inference/body_fixpoint.rb +89 -0
  42. data/lib/rigor/inference/budget_trace.rb +29 -2
  43. data/lib/rigor/inference/expression_typer.rb +1080 -105
  44. data/lib/rigor/inference/flow_tracer.rb +180 -0
  45. data/lib/rigor/inference/macro_block_self_type.rb +11 -12
  46. data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
  47. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +54 -14
  48. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
  49. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
  50. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
  51. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +148 -10
  52. data/lib/rigor/inference/method_dispatcher.rb +187 -55
  53. data/lib/rigor/inference/method_parameter_binder.rb +56 -2
  54. data/lib/rigor/inference/multi_target_binder.rb +46 -3
  55. data/lib/rigor/inference/mutation_widening.rb +142 -0
  56. data/lib/rigor/inference/narrowing.rb +330 -37
  57. data/lib/rigor/inference/scope_indexer.rb +770 -39
  58. data/lib/rigor/inference/statement_evaluator.rb +998 -68
  59. data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
  60. data/lib/rigor/plugin/additional_initializer.rb +61 -38
  61. data/lib/rigor/plugin/base.rb +517 -120
  62. data/lib/rigor/plugin/macro/block_as_method.rb +22 -21
  63. data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
  64. data/lib/rigor/plugin/macro.rb +2 -3
  65. data/lib/rigor/plugin/manifest.rb +4 -24
  66. data/lib/rigor/plugin/node_rule_walk.rb +192 -0
  67. data/lib/rigor/plugin/registry.rb +264 -35
  68. data/lib/rigor/plugin.rb +1 -0
  69. data/lib/rigor/rbs_extended/conformance_checker.rb +86 -1
  70. data/lib/rigor/scope/discovery_index.rb +60 -0
  71. data/lib/rigor/scope.rb +199 -204
  72. data/lib/rigor/sig_gen/generator.rb +8 -0
  73. data/lib/rigor/sig_gen/observation_collector.rb +6 -6
  74. data/lib/rigor/source/literals.rb +14 -0
  75. data/lib/rigor/triage/catalogue.rb +4 -19
  76. data/lib/rigor/triage.rb +69 -1
  77. data/lib/rigor/type/combinator.rb +34 -0
  78. data/lib/rigor/version.rb +1 -1
  79. data/lib/rigor.rb +0 -1
  80. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +13 -29
  81. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
  82. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
  83. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +27 -90
  84. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
  85. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
  86. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +90 -51
  87. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
  88. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -29
  89. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
  90. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
  91. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +11 -40
  92. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
  93. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +1 -1
  94. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
  95. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +21 -34
  96. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +11 -18
  97. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
  98. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +12 -2
  99. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  100. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +37 -31
  101. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
  102. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
  103. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
  104. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +8 -29
  105. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
  106. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
  107. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +108 -36
  108. data/sig/rigor/analysis/fact_store.rbs +3 -0
  109. data/sig/rigor/environment.rbs +0 -2
  110. data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
  111. data/sig/rigor/inference.rbs +5 -0
  112. data/sig/rigor/plugin/base.rbs +6 -4
  113. data/sig/rigor/plugin/manifest.rbs +1 -2
  114. data/sig/rigor/scope.rbs +50 -29
  115. data/sig/rigor/source.rbs +1 -0
  116. data/sig/rigor/type.rbs +1 -0
  117. data/sig/rigor.rbs +1 -1
  118. data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
  119. data/skills/rigor-ci-setup/SKILL.md +319 -0
  120. data/skills/rigor-plugin-author/SKILL.md +6 -4
  121. data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
  122. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
  123. metadata +21 -3
  124. data/lib/rigor/cache/rbs_instance_definitions.rb +0 -66
  125. data/lib/rigor/plugin/macro/external_file.rb +0 -143
@@ -130,14 +130,26 @@ module Rigor
130
130
  # Keys are canonical rule ids; values are
131
131
  # {VALID_SEVERITIES} symbols. Family-wildcard keys
132
132
  # (`call`) match every rule under that prefix.
133
+ # @param bleeding_edge_overrides [Hash{String => Symbol}] the
134
+ # severity map imposed by the active ADR-50 § WD2 bleeding-edge
135
+ # features ({Rigor::BleedingEdge.severity_overrides_for}).
136
+ # Consulted *below* the user's own `overrides` (so an explicit
137
+ # `severity_overrides:` entry, exact or family wildcard, always
138
+ # wins) and *above* the profile table. Exact rule ids only — the
139
+ # overlay never carries family wildcards. Empty while the
140
+ # overlay is unpopulated, so the default leaves resolution
141
+ # bit-for-bit unchanged.
133
142
  # @return [Symbol] the resolved severity. Returns `:off` to
134
143
  # mean "drop the diagnostic entirely".
135
- def resolve(rule:, authored_severity:, profile: DEFAULT_PROFILE, overrides: {})
144
+ def resolve(rule:, authored_severity:, profile: DEFAULT_PROFILE, overrides: {}, bleeding_edge_overrides: {})
136
145
  return authored_severity if rule.nil?
137
146
 
138
147
  override = overrides[rule] || family_override(rule, overrides)
139
148
  return override.to_sym if override
140
149
 
150
+ bleeding = bleeding_edge_overrides[rule]
151
+ return bleeding.to_sym if bleeding
152
+
141
153
  profile_table = PROFILES[profile] || PROFILES.fetch(DEFAULT_PROFILE)
142
154
  profile_table.fetch(rule, authored_severity)
143
155
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "yaml"
4
4
 
5
+ require_relative "bleeding_edge"
5
6
  require_relative "configuration/dependencies"
6
7
  require_relative "configuration/severity_profile"
7
8
 
@@ -70,12 +71,15 @@ module Rigor
70
71
  "fold_platform_specific_paths" => false,
71
72
  "cache" => {
72
73
  "path" => ".rigor/cache",
73
- # LRU eviction cap in bytes. nil (the default) disables eviction;
74
- # the cache grows until the user runs `rigor check --clear-cache`.
75
- # Set to a positive integer (e.g. 536870912 for 512 MB) to keep the
76
- # cache bounded the least-recently-used entries are removed at the
77
- # end of each run when the total exceeds this limit.
78
- "max_bytes" => nil
74
+ # LRU eviction cap in bytes (ADR-54 WD3). The least-recently-used
75
+ # entries are removed at the end of a run when the total exceeds
76
+ # this limit. The 256 MB default exists to reap orphans entries
77
+ # whose content key nothing references any more (an rbs gem bump,
78
+ # a signature change) and that no run would otherwise ever delete;
79
+ # a full active per-project set is ~2 MB, so the cap never touches
80
+ # live entries. Set explicitly to `null` to disable eviction
81
+ # (pre-WD3 behaviour: the cache grows until `--clear-cache`).
82
+ "max_bytes" => 268_435_456
79
83
  },
80
84
  "plugins_io" => {
81
85
  "network" => "disabled",
@@ -84,6 +88,15 @@ module Rigor
84
88
  },
85
89
  "severity_profile" => "balanced",
86
90
  "severity_overrides" => {},
91
+ # ADR-50 § WD2 — bleeding-edge overlay opt-in. Selects which of
92
+ # the *next major's* queued changes ({Rigor::BleedingEdge}) this
93
+ # project adopts early. Orthogonal to `severity_profile:`. Accepts
94
+ # `false` (default — adopt none), `true` (adopt the whole
95
+ # overlay), a list of feature ids (adopt only those), or
96
+ # `{ all: true, except: [ids] }` (adopt all but the named). The
97
+ # overlay is empty today, so every form is currently a no-op; it
98
+ # becomes live when the first discipline is queued for a major.
99
+ "bleeding_edge" => false,
87
100
  "dependencies" => {
88
101
  "source_inference" => [],
89
102
  "budget_per_gem" => Configuration::Dependencies::DEFAULT_BUDGET_PER_GEM
@@ -178,6 +191,7 @@ module Rigor
178
191
  :plugins_io_network, :plugins_io_allowed_paths,
179
192
  :plugins_io_allowed_url_hosts,
180
193
  :severity_profile, :severity_overrides,
194
+ :bleeding_edge, :bleeding_edge_severity_overrides,
181
195
  :dependencies, :parallel_workers,
182
196
  :bundler_bundle_path, :bundler_auto_detect, :bundler_lockfile,
183
197
  :rbs_collection_lockfile, :rbs_collection_auto_detect,
@@ -352,6 +366,10 @@ module Rigor
352
366
  @severity_overrides = coerce_severity_overrides(
353
367
  data.fetch("severity_overrides", DEFAULTS.fetch("severity_overrides"))
354
368
  )
369
+ @bleeding_edge = coerce_bleeding_edge(
370
+ data.fetch("bleeding_edge", DEFAULTS.fetch("bleeding_edge"))
371
+ )
372
+ @bleeding_edge_severity_overrides = BleedingEdge.severity_overrides_for(@bleeding_edge)
355
373
  @dependencies = Dependencies.from_h(
356
374
  data.fetch("dependencies", DEFAULTS.fetch("dependencies"))
357
375
  )
@@ -380,7 +398,7 @@ module Rigor
380
398
  end
381
399
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
382
400
 
383
- def to_h # rubocop:disable Metrics/MethodLength
401
+ def to_h # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
384
402
  {
385
403
  "target_ruby" => target_ruby,
386
404
  "paths" => paths,
@@ -402,6 +420,7 @@ module Rigor
402
420
  },
403
421
  "severity_profile" => severity_profile.to_s,
404
422
  "severity_overrides" => severity_overrides.to_h { |k, v| [k, v.to_s] },
423
+ "bleeding_edge" => bleeding_edge_to_h,
405
424
  "dependencies" => dependencies.to_h,
406
425
  "parallel" => {
407
426
  "workers" => parallel_workers
@@ -563,5 +582,45 @@ module Rigor
563
582
  [k.to_s, sym]
564
583
  end.freeze
565
584
  end
585
+
586
+ # ADR-50 § WD2 — normalizes the `bleeding_edge:` selector to a
587
+ # canonical `{ "mode" => … }` hash (interpreted by
588
+ # {Rigor::BleedingEdge}). Validates *shape* only; membership against
589
+ # the overlay is intentionally NOT checked here (an unknown id stays
590
+ # inert, like an unknown `severity_overrides:` rule). Deep-frozen so
591
+ # the Configuration stays `Ractor.shareable?`.
592
+ def coerce_bleeding_edge(value)
593
+ case value
594
+ when nil, false then { "mode" => "none" }
595
+ when true then { "mode" => "all" }
596
+ when Array then { "mode" => "list", "ids" => value.map(&:to_s).freeze }
597
+ when Hash then coerce_bleeding_edge_hash(value)
598
+ else
599
+ raise ArgumentError,
600
+ "bleeding_edge must be true, false, a list of feature ids, " \
601
+ "or { all: true, except: [...] }, got #{value.inspect}"
602
+ end.freeze
603
+ end
604
+
605
+ def coerce_bleeding_edge_hash(value)
606
+ hash = value.to_h { |k, v| [k.to_s, v] }
607
+ if hash.fetch("all", false) == true
608
+ { "mode" => "all", "except" => Array(hash["except"]).map(&:to_s).freeze }
609
+ else
610
+ { "mode" => "none" }
611
+ end
612
+ end
613
+
614
+ # Renders the normalized selector back into the user-facing
615
+ # `bleeding_edge:` form for `#to_h` round-trips.
616
+ def bleeding_edge_to_h
617
+ case bleeding_edge["mode"]
618
+ when "all"
619
+ except = bleeding_edge["except"] || []
620
+ except.empty? || { "all" => true, "except" => except }
621
+ when "list" then bleeding_edge["ids"] || []
622
+ else false
623
+ end
624
+ end
566
625
  end
567
626
  end
@@ -88,6 +88,15 @@ module Rigor
88
88
  vendored_gem_sig_paths.each do |path|
89
89
  rbs_loader.add(path: path) if path.directory?
90
90
  end
91
+ # Rigor-owned core overlay — loaded LAST so an upstream
92
+ # declaration always wins on conflict; these reopenings only
93
+ # fill genuine holes (e.g. `Numeric#to_f`/`to_i`/`to_r`, which
94
+ # upstream RBS declares on the concrete subclasses but not on
95
+ # the abstract `Numeric` that Rigor's arithmetic-chain widening
96
+ # produces).
97
+ core_overlay_sig_paths.each do |path|
98
+ rbs_loader.add(path: path) if path.directory?
99
+ end
91
100
  env = RBS::Environment.from_loader(rbs_loader)
92
101
  add_virtual_rbs(env, virtual_rbs)
93
102
  synthesize_missing_namespaces(env)
@@ -298,6 +307,22 @@ module Rigor
298
307
  ).freeze
299
308
  private_constant :VENDORED_GEM_SIGS_ROOT
300
309
 
310
+ # Rigor-owned core-overlay RBS (`data/core_overlay/`). Reopens
311
+ # Ruby core classes to add methods upstream `ruby/rbs` omits but
312
+ # which every concrete value answers at runtime — loaded last so
313
+ # upstream always wins on conflict. Public so the cache descriptor
314
+ # can digest these files into the env-blob key.
315
+ CORE_OVERLAY_SIGS_ROOT = File.expand_path(
316
+ "../../../data/core_overlay",
317
+ __dir__
318
+ ).freeze
319
+
320
+ def core_overlay_sig_paths
321
+ return [] unless File.directory?(CORE_OVERLAY_SIGS_ROOT)
322
+
323
+ [Pathname(CORE_OVERLAY_SIGS_ROOT)]
324
+ end
325
+
301
326
  def vendored_gem_sig_paths
302
327
  return [] unless File.directory?(VENDORED_GEM_SIGS_ROOT)
303
328
 
@@ -559,29 +584,17 @@ module Rigor
559
584
  # definition cannot be built (RBS may raise on broken hierarchies;
560
585
  # we fail-soft and return nil so the caller can fall back).
561
586
  #
562
- # When `cache_store` is set, the loader fetches the per-class
563
- # definition through {Cache::RbsInstanceDefinitions.fetch} so
564
- # subsequent runs (and other loaders sharing the same Store)
565
- # skip the `RBS::DefinitionBuilder.build_instance` step.
566
- # In-memory `@instance_definition_cache` keeps the per-process
567
- # short-circuit on top.
587
+ # Built on demand from the (possibly cache-loaded) env; the
588
+ # in-memory `@instance_definition_cache` keeps the per-process
589
+ # short-circuit. ADR-54 WD1 retired the definitions disk blob:
590
+ # given a cached env, `Marshal.load`-ing every definition was
591
+ # measurably slower (and allocation-heavier) than rebuilding
592
+ # the ones a run actually touches.
568
593
  def instance_definition(class_name)
569
594
  key = class_name.to_s
570
595
  return @instance_definition_cache[key] if @instance_definition_cache.key?(key)
571
596
 
572
- @instance_definition_cache[key] = if cache_store
573
- cached_instance_definition(class_name)
574
- else
575
- build_instance_definition(class_name)
576
- end
577
- end
578
-
579
- # Public uncached accessor used by the cache producer
580
- # ({Rigor::Cache::RbsInstanceDefinitions}). Avoids the
581
- # `private_method_called` round-trip a `loader.send(...)`
582
- # callsite would require.
583
- def uncached_instance_definition(class_name)
584
- build_instance_definition(class_name)
597
+ @instance_definition_cache[key] = build_instance_definition(class_name)
585
598
  end
586
599
 
587
600
  # @return [RBS::Definition::Method, nil]
@@ -636,24 +649,14 @@ module Rigor
636
649
  # those inherited from `Class` and `Module` for class types.
637
650
  # Returns nil for unknown names and on RBS build errors (fail-soft).
638
651
  #
639
- # When `cache_store` is set, the loader fetches the per-class
640
- # singleton definition through
641
- # {Cache::RbsSingletonDefinitions.fetch}; the same caching
642
- # discipline as {#instance_definition}.
652
+ # Built on demand from the env with a per-process memo; the
653
+ # same on-demand discipline as {#instance_definition} (ADR-54
654
+ # WD1).
643
655
  def singleton_definition(class_name)
644
656
  key = class_name.to_s
645
657
  return @singleton_definition_cache[key] if @singleton_definition_cache.key?(key)
646
658
 
647
- @singleton_definition_cache[key] = if cache_store
648
- cached_singleton_definition(class_name)
649
- else
650
- build_singleton_definition(class_name)
651
- end
652
- end
653
-
654
- # Public uncached accessor used by the cache producer.
655
- def uncached_singleton_definition(class_name)
656
- build_singleton_definition(class_name)
659
+ @singleton_definition_cache[key] = build_singleton_definition(class_name)
657
660
  end
658
661
 
659
662
  # @return [RBS::Definition::Method, nil] the class method on
@@ -762,18 +765,20 @@ module Rigor
762
765
  end
763
766
 
764
767
  # ADR-15 Phase 4b.x — eagerly drives every cached
765
- # producer so a subsequent worker Ractor can serve all
766
- # of its RBS queries from the Marshal blob on disk
767
- # without ever calling `RBS::EnvironmentLoader.new`.
768
+ # producer (plus the eager definitions tables, computed
769
+ # from the cached env since ADR-54 WD1) so a subsequent
770
+ # worker Ractor can serve all of its RBS queries without
771
+ # ever calling `RBS::EnvironmentLoader.new`.
768
772
  # The loader path that calls `EnvironmentLoader.new`
769
773
  # transitively reads a chain of non-`Ractor.shareable?`
770
774
  # module constants
771
775
  # (`RBS::EnvironmentLoader::DEFAULT_CORE_ROOT`,
772
776
  # `RBS::Repository::DEFAULT_STDLIB_ROOT`,
773
777
  # `Gem::Requirement::DefaultRequirement`, …) and trips
774
- # `Ractor::IsolationError`. Pre-warming the cache on
775
- # the main Ractor and letting workers consult ONLY the
776
- # Marshal-loaded blob sidesteps the whole chain.
778
+ # `Ractor::IsolationError`. Pre-warming on the main
779
+ # Ractor env blob loaded, derived tables built — keeps
780
+ # workers off that chain (`RBS::DefinitionBuilder` over
781
+ # an already-built env does not touch it).
777
782
  #
778
783
  # No-op when `cache_store` is nil — without a Store the
779
784
  # worker has no choice but to build env via the loader,
@@ -793,6 +798,20 @@ module Rigor
793
798
  self
794
799
  end
795
800
 
801
+ # ADR-54 WD4 — the shared cache descriptor for every RBS-derived
802
+ # producer consulting this loader. Building it digests every
803
+ # `.rbs` file under `signature_paths` + the vendored gem sigs,
804
+ # and the result is identical across producers, so one build is
805
+ # memoised per loader (on `@state`, alongside `:env` — the env
806
+ # itself is loader-lifetime-memoised, so this adds no new
807
+ # staleness class).
808
+ def rbs_cache_descriptor
809
+ @state[:rbs_cache_descriptor] ||= begin
810
+ require_relative "../cache/rbs_descriptor"
811
+ Cache::RbsDescriptor.build(self)
812
+ end
813
+ end
814
+
796
815
  # ADR-15 Phase 2b — return the loader's read-only
797
816
  # query surface as a frozen, `Ractor.shareable?`
798
817
  # {Reflection} value object. Built lazily on first
@@ -969,43 +988,34 @@ module Rigor
969
988
  Cache::RbsEnvironment.fetch(loader: self, store: cache_store)
970
989
  end
971
990
 
972
- # Per-process Hash<String, RBS::Definition> for the instance
973
- # side. Loaded once on first miss through the
974
- # {Cache::RbsInstanceDefinitions} producer (single Marshal
975
- # blob); subsequent calls are pure Hash lookups. Cold runs
976
- # build every known class once and persist; warm runs (and
977
- # other loaders sharing the same Store) skip the
978
- # `RBS::DefinitionBuilder.build_instance` work entirely.
979
- def cached_instance_definition(class_name)
980
- instance_definitions_table[normalise_class_key(class_name)]
981
- end
982
-
991
+ # Full `Hash<String, RBS::Definition>` tables for the
992
+ # {#prewarm} / {#reflection} consumers (ADR-15 Phase 2b/4b.x),
993
+ # which need every definition materialised up front. Built
994
+ # from the (cached) env via `RBS::DefinitionBuilder` ADR-54
995
+ # WD1 retired the disk blobs these used to `Marshal.load`
996
+ # (building all definitions from a cached env is faster), so
997
+ # the eager-table cost is now a compute, not a load. Keys stay
998
+ # in `RBS::TypeName#to_s` form (top-level prefixed `"::Hash"`)
999
+ # — the shape {Environment::Reflection} documents.
983
1000
  def instance_definitions_table
984
- @state[:instance_definitions_table] ||= begin
985
- require_relative "../cache/rbs_instance_definitions"
986
- fetch_or_compute_producer(Cache::RbsInstanceDefinitions)
1001
+ @state[:instance_definitions_table] ||= build_definitions_table do |name|
1002
+ build_instance_definition(name)
987
1003
  end
988
1004
  end
989
1005
 
990
- def cached_singleton_definition(class_name)
991
- singleton_definitions_table[normalise_class_key(class_name)]
992
- end
993
-
994
1006
  def singleton_definitions_table
995
- @state[:singleton_definitions_table] ||= begin
996
- require_relative "../cache/rbs_instance_definitions"
997
- fetch_or_compute_producer(Cache::RbsSingletonDefinitions)
1007
+ @state[:singleton_definitions_table] ||= build_definitions_table do |name|
1008
+ build_singleton_definition(name)
998
1009
  end
999
1010
  end
1000
1011
 
1001
- # The cache producers persist class names in
1002
- # `RBS::TypeName#to_s` form (top-level prefixed
1003
- # `"::Hash"`); plain-name lookups (`"Hash"`) normalise
1004
- # before the Hash query so callers stay agnostic to the
1005
- # prefix.
1006
- def normalise_class_key(class_name)
1007
- s = class_name.to_s
1008
- s.start_with?("::") ? s : "::#{s}"
1012
+ def build_definitions_table
1013
+ table = {}
1014
+ each_known_class_name do |name|
1015
+ definition = yield(name)
1016
+ table[name] = definition if definition
1017
+ end
1018
+ table
1009
1019
  end
1010
1020
 
1011
1021
  def builder
@@ -77,7 +77,7 @@ module Rigor
77
77
  # @param plugin_registry [Rigor::Plugin::Registry, nil] v0.1.1
78
78
  # Track 2 slice 7. The per-run plugin registry the
79
79
  # inference engine consults at call sites for plugin
80
- # `#flow_contribution_for` overrides. When nil (the
80
+ # `dynamic_return` rules. When nil (the
81
81
  # default), no plugin-level return-type contribution
82
82
  # participates — useful for tests, the `Environment.default`
83
83
  # facade, and analyses that don't load plugins.
@@ -243,6 +243,12 @@ module Rigor
243
243
  when Type::HashShape
244
244
  accepts(self_type, project_hash_shape_to_nominal(other_type), mode: mode)
245
245
  .with_reason("projected HashShape to Nominal[Hash]")
246
+ when Type::DataInstance
247
+ # ADR-48: a class-tagged member instance is exactly one value of
248
+ # its tagging class, so it projects to that class's nominal (the
249
+ # anonymous local-bound form projects to `Data` itself).
250
+ accepts(self_type, project_data_instance_to_nominal(other_type), mode: mode)
251
+ .with_reason("projected DataInstance to Nominal[#{other_type.class_name || 'Data'}]")
246
252
  when Type::Difference, Type::Refined
247
253
  # A refinement carrier's value set is a subset of its
248
254
  # base. So if `self` (Nominal) accepts the base, it
@@ -376,6 +382,10 @@ module Rigor
376
382
  end
377
383
  end
378
384
 
385
+ def project_data_instance_to_nominal(instance)
386
+ Type::Combinator.nominal_of(instance.class_name || "Data")
387
+ end
388
+
379
389
  def project_hash_shape_to_nominal(shape)
380
390
  return Type::Combinator.nominal_of(Hash) if shape.pairs.empty?
381
391
 
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../type"
4
+ require_relative "budget_trace"
5
+
6
+ module Rigor
7
+ module Inference
8
+ # ADR-56 WD3 — the single capped-fixpoint mechanism shared by the
9
+ # non-escaping block captured-local write-back (slice A) and the
10
+ # loop-body fixpoint (slice B). Computes the continuation binding of a
11
+ # set of locals that a body (a block body or a loop body) may rebind
12
+ # across an unknown number (0..N) of iterations.
13
+ #
14
+ # The body may run zero times, so the seed (pre-state) binding is kept
15
+ # as a join constituent throughout; it may compound (`a = [a]`), so the
16
+ # join is iterated to a fixed point under a hard cap (ADR-41 WD4). On
17
+ # the final permitted iteration value-pinned constituents are widened to
18
+ # their nominal base to force convergence; a local that still moves is
19
+ # collapsed to `Dynamic[top]` — the established escaping-block floor —
20
+ # and a {BudgetTrace::BLOCK_WRITEBACK_CAP} hit is recorded.
21
+ #
22
+ # The mechanism is parameterized over an `evaluate_body` callable so
23
+ # slice B can reuse it: given the current per-name bindings it returns
24
+ # the per-name exit bindings produced by one body evaluation from those
25
+ # bindings (names the body leaves unwritten in a given pass simply do
26
+ # not appear in the returned hash).
27
+ module BodyFixpoint
28
+ # One body evaluation per iteration; ADR-55's shape (cap 3).
29
+ CAP = 3
30
+
31
+ module_function
32
+
33
+ # @param names [Array<Symbol>] the outer locals the body can rebind.
34
+ # @param seed_bindings [Hash{Symbol=>Type}] pre-state binding per name.
35
+ # @param widen [#call] value-pinned widener (Constant -> Nominal).
36
+ # @param evaluate_body [#call] `bindings -> exit_bindings` — evaluates
37
+ # the body once from `bindings` (the per-name current assumption) and
38
+ # returns the per-name exit binding it produced.
39
+ # @return [Hash{Symbol=>Type}] the continuation binding per name.
40
+ def converge(names:, seed_bindings:, widen:, evaluate_body:)
41
+ return {} if names.empty?
42
+
43
+ # Running assumption per name; seeded with the pre-state binding,
44
+ # which stays a join constituent throughout (0-iteration soundness).
45
+ assumption = seed_bindings.dup
46
+
47
+ (0...CAP).each do |iteration|
48
+ last_iteration = iteration == CAP - 1
49
+ exit_bindings = evaluate_body.call(assumption)
50
+
51
+ stable = true
52
+ names.each do |name|
53
+ exit_type = exit_bindings[name]
54
+ next if exit_type.nil? # body did not write it this pass
55
+
56
+ if last_iteration
57
+ # On the final permitted iteration widen BOTH the running
58
+ # assumption and the fresh exit type to their nominal bases
59
+ # before joining: Rigor's `union` keeps `Constant[1]` and
60
+ # `Nominal[Integer]` as distinct members, so an accumulator
61
+ # (`+=`/`*=`) producing a fresh constant per pass would never
62
+ # converge without collapsing both sides first. If the join
63
+ # is still wider than the widened assumption (structural
64
+ # compounding, `a = [a]`), the local floors to `Dynamic[top]`.
65
+ base = widen.call(assumption[name])
66
+ joined = Type::Combinator.union(base, widen.call(exit_type))
67
+ if joined == base
68
+ assumption[name] = joined
69
+ else
70
+ BudgetTrace.hit(BudgetTrace::BLOCK_WRITEBACK_CAP)
71
+ assumption[name] = Type::Combinator.untyped
72
+ end
73
+ else
74
+ joined = Type::Combinator.union(assumption[name], exit_type)
75
+ next if joined == assumption[name]
76
+
77
+ stable = false
78
+ assumption[name] = joined
79
+ end
80
+ end
81
+
82
+ break if stable
83
+ end
84
+
85
+ assumption
86
+ end
87
+ end
88
+ end
89
+ end
@@ -20,6 +20,12 @@ module Rigor
20
20
  # hit the 100-node BFS cap and gave up resolving the self-call.
21
21
  # - {HKT_FUEL_EXHAUSTED} — `HktReducer` ran out of its reduction
22
22
  # fuel budget and unwound to `app.bound`.
23
+ # - {RECURSION_UNROLL_FUEL} — the constant-arg recursion unroll
24
+ # (ADR-55 slice 1) exhausted its per-entry fuel and fell back to
25
+ # the plain `(receiver, method)` guard (in-cycle call → `Dynamic[top]`).
26
+ # - {RECURSION_FIXPOINT_CAP} — the fixpoint return-summary iteration
27
+ # (ADR-55 slice 2) hit its 3-evaluation cap without converging and
28
+ # collapsed the summary to `untyped` (today's behaviour).
23
29
  #
24
30
  # Enabled only when `RIGOR_BUDGET_TRACE` is set (to any non-empty
25
31
  # value) in the environment, or via {enable!} in tests. When
@@ -34,8 +40,29 @@ module Rigor
34
40
  RECURSION_GUARD = :recursion_guard
35
41
  ANCESTOR_WALK_LIMIT = :ancestor_walk_limit
36
42
  HKT_FUEL_EXHAUSTED = :hkt_fuel_exhausted
37
-
38
- CATEGORIES = [RECURSION_GUARD, ANCESTOR_WALK_LIMIT, HKT_FUEL_EXHAUSTED].freeze
43
+ # `ExpressionTyper#infer_user_method_return` exhausted its
44
+ # constant-arg unroll fuel (ADR-55 slice 1) and fell back to the
45
+ # plain `(receiver, method)` recursion guard — i.e. the in-cycle
46
+ # call widened to `Dynamic[top]` exactly as it does without the
47
+ # unroll.
48
+ RECURSION_UNROLL_FUEL = :recursion_unroll_fuel
49
+ # `ExpressionTyper#infer_user_method_return` ran the fixpoint
50
+ # return-summary iteration (ADR-55 slice 2) to its 3-evaluation cap
51
+ # without reaching convergence and collapsed the summary to
52
+ # `untyped` — the in-cycle result widens to `Dynamic[top]` exactly
53
+ # as it does without the fixpoint.
54
+ RECURSION_FIXPOINT_CAP = :recursion_fixpoint_cap
55
+ # `BodyFixpoint#converge` (ADR-56 slice A — non-escaping block
56
+ # captured-local write-back) ran its 3-evaluation cap without the
57
+ # written local's join converging and collapsed that local to
58
+ # `Dynamic[top]` (the escaping-block floor). Shared by slice B's
59
+ # loop-body fixpoint.
60
+ BLOCK_WRITEBACK_CAP = :block_writeback_cap
61
+
62
+ CATEGORIES = [
63
+ RECURSION_GUARD, ANCESTOR_WALK_LIMIT, HKT_FUEL_EXHAUSTED, RECURSION_UNROLL_FUEL,
64
+ RECURSION_FIXPOINT_CAP, BLOCK_WRITEBACK_CAP
65
+ ].freeze
39
66
 
40
67
  # Distribution (histogram) categories — read-only observations of
41
68
  # a value's size at a site, used to choose budget defaults from an