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.
- checksums.yaml +4 -4
- data/README.md +159 -222
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +24 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +29 -0
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +213 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +24 -1
- data/lib/rigor/analysis/check_rules.rb +275 -44
- data/lib/rigor/analysis/diagnostic.rb +8 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +581 -0
- data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +321 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
- data/lib/rigor/analysis/runner.rb +207 -1200
- data/lib/rigor/analysis/worker_session.rb +60 -11
- data/lib/rigor/bleeding_edge.rb +123 -0
- data/lib/rigor/cache/descriptor.rb +86 -8
- data/lib/rigor/cache/incremental_snapshot.rb +10 -4
- data/lib/rigor/cache/rbs_cache_producer.rb +5 -1
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +46 -13
- data/lib/rigor/cli/annotate_command.rb +100 -15
- data/lib/rigor/cli/check_command.rb +708 -0
- data/lib/rigor/cli/ci_detector.rb +94 -0
- data/lib/rigor/cli/diagnostic_formats.rb +345 -0
- data/lib/rigor/cli/plugins_command.rb +2 -4
- data/lib/rigor/cli/plugins_renderer.rb +0 -2
- data/lib/rigor/cli/prism_colorizer.rb +10 -3
- data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
- data/lib/rigor/cli/trace_command.rb +143 -0
- data/lib/rigor/cli/trace_renderer.rb +310 -0
- data/lib/rigor/cli/triage_command.rb +6 -3
- data/lib/rigor/cli/triage_renderer.rb +15 -1
- data/lib/rigor/cli.rb +21 -612
- data/lib/rigor/configuration/severity_profile.rb +13 -1
- data/lib/rigor/configuration.rb +66 -7
- data/lib/rigor/environment/rbs_loader.rb +78 -68
- data/lib/rigor/environment.rb +1 -1
- data/lib/rigor/inference/acceptance.rb +10 -0
- data/lib/rigor/inference/body_fixpoint.rb +89 -0
- data/lib/rigor/inference/budget_trace.rb +29 -2
- data/lib/rigor/inference/expression_typer.rb +1080 -105
- data/lib/rigor/inference/flow_tracer.rb +180 -0
- data/lib/rigor/inference/macro_block_self_type.rb +11 -12
- data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +54 -14
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +148 -10
- data/lib/rigor/inference/method_dispatcher.rb +187 -55
- data/lib/rigor/inference/method_parameter_binder.rb +56 -2
- data/lib/rigor/inference/multi_target_binder.rb +46 -3
- data/lib/rigor/inference/mutation_widening.rb +142 -0
- data/lib/rigor/inference/narrowing.rb +330 -37
- data/lib/rigor/inference/scope_indexer.rb +770 -39
- data/lib/rigor/inference/statement_evaluator.rb +998 -68
- data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
- data/lib/rigor/plugin/additional_initializer.rb +61 -38
- data/lib/rigor/plugin/base.rb +517 -120
- data/lib/rigor/plugin/macro/block_as_method.rb +22 -21
- data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
- data/lib/rigor/plugin/macro.rb +2 -3
- data/lib/rigor/plugin/manifest.rb +4 -24
- data/lib/rigor/plugin/node_rule_walk.rb +192 -0
- data/lib/rigor/plugin/registry.rb +264 -35
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/conformance_checker.rb +86 -1
- data/lib/rigor/scope/discovery_index.rb +60 -0
- data/lib/rigor/scope.rb +199 -204
- data/lib/rigor/sig_gen/generator.rb +8 -0
- data/lib/rigor/sig_gen/observation_collector.rb +6 -6
- data/lib/rigor/source/literals.rb +14 -0
- data/lib/rigor/triage/catalogue.rb +4 -19
- data/lib/rigor/triage.rb +69 -1
- data/lib/rigor/type/combinator.rb +34 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +0 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +13 -29
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +27 -90
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +90 -51
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -29
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +11 -40
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +21 -34
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +11 -18
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +12 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +37 -31
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +8 -29
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +108 -36
- data/sig/rigor/analysis/fact_store.rbs +3 -0
- data/sig/rigor/environment.rbs +0 -2
- data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
- data/sig/rigor/inference.rbs +5 -0
- data/sig/rigor/plugin/base.rbs +6 -4
- data/sig/rigor/plugin/manifest.rbs +1 -2
- data/sig/rigor/scope.rbs +50 -29
- data/sig/rigor/source.rbs +1 -0
- data/sig/rigor/type.rbs +1 -0
- data/sig/rigor.rbs +1 -1
- data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
- data/skills/rigor-ci-setup/SKILL.md +319 -0
- data/skills/rigor-plugin-author/SKILL.md +6 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
- metadata +21 -3
- data/lib/rigor/cache/rbs_instance_definitions.rb +0 -66
- 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
|
data/lib/rigor/configuration.rb
CHANGED
|
@@ -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
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
|
|
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
|
-
#
|
|
563
|
-
#
|
|
564
|
-
#
|
|
565
|
-
#
|
|
566
|
-
#
|
|
567
|
-
#
|
|
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] =
|
|
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
|
-
#
|
|
640
|
-
#
|
|
641
|
-
#
|
|
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] =
|
|
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
|
|
766
|
-
#
|
|
767
|
-
#
|
|
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
|
|
775
|
-
#
|
|
776
|
-
#
|
|
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
|
-
#
|
|
973
|
-
#
|
|
974
|
-
#
|
|
975
|
-
#
|
|
976
|
-
#
|
|
977
|
-
#
|
|
978
|
-
#
|
|
979
|
-
|
|
980
|
-
|
|
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] ||=
|
|
985
|
-
|
|
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] ||=
|
|
996
|
-
|
|
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
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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
|
data/lib/rigor/environment.rb
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
|
|
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
|