rigortype 0.3.1 → 0.3.3
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 +10 -10
- data/data/core_overlay/pathname.rbs +5 -0
- data/data/core_overlay/resolv.rbs +31 -0
- data/data/core_overlay/string_scanner.rbs +5 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +10 -6
- data/data/vendored_gem_sigs/bundler/bundler.rbs +39 -27
- data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +6 -0
- data/data/vendored_gem_sigs/prism/prism_supplement.rbs +7 -0
- data/data/vendored_gem_sigs/racc/racc.rbs +67 -0
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +20 -9
- data/docs/manual/02-cli-reference.md +71 -11
- data/docs/manual/03-configuration.md +16 -1
- data/docs/manual/04-diagnostics.md +8 -0
- data/docs/manual/12-caching.md +17 -5
- data/docs/manual/15-type-protection-coverage.md +122 -1
- data/docs/manual/plugins/rigor-rbs-inline.md +22 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +8 -0
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +53 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +7 -0
- data/lib/rigor/analysis/check_rules.rb +107 -28
- data/lib/rigor/analysis/incremental_session.rb +156 -11
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +14 -0
- data/lib/rigor/analysis/rule_catalog.rb +5 -1
- data/lib/rigor/analysis/run_cache_key.rb +17 -1
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +201 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +28 -10
- data/lib/rigor/analysis/runner/pool_coordinator.rb +7 -10
- data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -1
- data/lib/rigor/analysis/runner.rb +78 -7
- data/lib/rigor/analysis/severity_stamp.rb +2 -1
- data/lib/rigor/bleeding_edge.rb +132 -7
- data/lib/rigor/cache/descriptor.rb +6 -1
- data/lib/rigor/cache/engine_source.rb +162 -0
- data/lib/rigor/cache/incremental_snapshot.rb +87 -18
- data/lib/rigor/cli/check_command.rb +49 -28
- data/lib/rigor/cli/coverage_command.rb +15 -2
- data/lib/rigor/cli/coverage_mutation.rb +242 -11
- data/lib/rigor/cli/fused_protection_renderer.rb +10 -0
- data/lib/rigor/cli/fused_protection_report.rb +12 -3
- data/lib/rigor/cli/mutation_fork_scan.rb +64 -0
- data/lib/rigor/cli/mutation_protection_renderer.rb +12 -0
- data/lib/rigor/cli/mutation_protection_report.rb +26 -5
- data/lib/rigor/cli/protection_renderer.rb +13 -0
- data/lib/rigor/cli/protection_report.rb +11 -3
- data/lib/rigor/cli/show_bleedingedge_command.rb +17 -4
- data/lib/rigor/cli/sig_gen_command.rb +5 -4
- data/lib/rigor/configuration.rb +45 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +2 -1
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +434 -75
- data/lib/rigor/environment.rb +28 -13
- data/lib/rigor/inference/anonymous_meta_class.rb +81 -0
- data/lib/rigor/inference/expression_typer.rb +62 -4
- data/lib/rigor/inference/fork_map.rb +47 -4
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +50 -2
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +35 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +158 -12
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +64 -5
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +234 -11
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +124 -1
- data/lib/rigor/inference/method_dispatcher.rb +41 -10
- data/lib/rigor/inference/mutation_widening.rb +30 -25
- data/lib/rigor/inference/narrowing.rb +3 -1
- data/lib/rigor/inference/optimistic_origin.rb +123 -0
- data/lib/rigor/inference/protection_scanner.rb +18 -2
- data/lib/rigor/inference/receiver_alias.rb +57 -0
- data/lib/rigor/inference/scope_indexer.rb +197 -55
- data/lib/rigor/inference/singleton_object_constant.rb +63 -0
- data/lib/rigor/inference/statement_evaluator.rb +55 -4
- data/lib/rigor/language_server/buffer_table.rb +22 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +203 -4
- data/lib/rigor/language_server/project_context.rb +45 -0
- data/lib/rigor/language_server/publish_batcher.rb +82 -0
- data/lib/rigor/language_server/server.rb +20 -1
- data/lib/rigor/language_server.rb +1 -0
- data/lib/rigor/plugin/node_rule_walk.rb +6 -0
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
- data/lib/rigor/protection/closure_kill_oracle.rb +190 -0
- data/lib/rigor/protection/dependency_closure.rb +59 -0
- data/lib/rigor/protection/diagnostic_oracle.rb +12 -8
- data/lib/rigor/protection/discovery_seed.rb +116 -0
- data/lib/rigor/protection/kill_signature.rb +31 -0
- data/lib/rigor/protection/mutation_cache.rb +355 -0
- data/lib/rigor/protection/mutation_scanner.rb +55 -11
- data/lib/rigor/protection/mutator.rb +26 -4
- data/lib/rigor/runtime/jit.rb +63 -1
- data/lib/rigor/scope/discovery_index.rb +9 -0
- data/lib/rigor/scope.rb +114 -10
- data/lib/rigor/sig_gen/generator.rb +130 -31
- data/lib/rigor/sig_gen/layout_index.rb +6 -0
- data/lib/rigor/sig_gen/meta_class_shape.rb +84 -0
- data/lib/rigor/sig_gen/renderer.rb +20 -1
- data/lib/rigor/sig_gen/write_result.rb +6 -4
- data/lib/rigor/sig_gen/writer.rb +12 -0
- data/lib/rigor/sig_gen.rb +1 -0
- data/lib/rigor/source/node_walker.rb +12 -0
- data/lib/rigor/type/anonymous_class_name.rb +40 -0
- data/lib/rigor/type/nominal.rb +5 -2
- data/lib/rigor/type/singleton.rb +4 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +6 -6
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/result_shape.rb +146 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +177 -12
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +74 -7
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +257 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +69 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +87 -11
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +34 -4
- data/sig/rigor/inference/void_origin.rbs +6 -3
- data/sig/rigor/scope.rbs +12 -0
- data/sig/rigor.rbs +4 -0
- metadata +21 -1
data/lib/rigor/environment.rb
CHANGED
|
@@ -399,23 +399,38 @@ module Rigor
|
|
|
399
399
|
) { invoke_synthesizer_safely(callable, path) || "" }
|
|
400
400
|
end
|
|
401
401
|
|
|
402
|
-
# ADR-32 WD6 — route a synthesizer return value through the per-run
|
|
402
|
+
# ADR-32 WD6 / WD12 — route a synthesizer return value through the per-run reporter. The
|
|
403
403
|
# synthesizer's contract (declared in
|
|
404
|
-
# `plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb`) admits
|
|
405
|
-
# - `String` (non-empty)
|
|
406
|
-
# - `nil` / `""`
|
|
407
|
-
# - `[:error, message]`
|
|
404
|
+
# `plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb`) admits four return shapes:
|
|
405
|
+
# - `String` (non-empty) → successful RBS source
|
|
406
|
+
# - `nil` / `""` → no contribution
|
|
407
|
+
# - `[:error, message]` → parse failed (WD6)
|
|
408
|
+
# - `[:ok, source, [message…]]` → synthesis SUCCEEDED, but an annotation was parsed and not
|
|
409
|
+
# honoured (WD12)
|
|
408
410
|
# The error tuple is converted into a reporter entry + treated as "no contribution" so the analysis
|
|
409
|
-
# pipeline continues.
|
|
411
|
+
# pipeline continues. The WD12 tuple keeps its `source` — the file's other annotations are good, and
|
|
412
|
+
# dropping them to report one omission would be a strictly worse trade. Reporter is `nil` for callers
|
|
413
|
+
# that don't care (legacy Environment.new, tests).
|
|
414
|
+
#
|
|
415
|
+
# A cached entry written before WD12 is a bare `String`, which falls through the `is_a?(Array)` guard
|
|
416
|
+
# unchanged, so no cache-descriptor bump is needed for the new shape.
|
|
410
417
|
def interpret_synthesizer_outcome(outcome, plugin, path, reporter)
|
|
411
|
-
return outcome unless outcome.is_a?(Array)
|
|
418
|
+
return outcome unless outcome.is_a?(Array)
|
|
419
|
+
|
|
420
|
+
case outcome[0]
|
|
421
|
+
when :error
|
|
422
|
+
record_synthesis_entry(reporter, plugin, path, outcome[1], :failed)
|
|
423
|
+
nil
|
|
424
|
+
when :ok
|
|
425
|
+
Array(outcome[2]).each { |m| record_synthesis_entry(reporter, plugin, path, m, :not_honoured) }
|
|
426
|
+
outcome[1]
|
|
427
|
+
else
|
|
428
|
+
outcome
|
|
429
|
+
end
|
|
430
|
+
end
|
|
412
431
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
path: path,
|
|
416
|
-
message: outcome[1].to_s
|
|
417
|
-
)
|
|
418
|
-
nil
|
|
432
|
+
def record_synthesis_entry(reporter, plugin, path, message, kind)
|
|
433
|
+
reporter&.record(plugin_id: plugin.manifest.id, path: path, message: message.to_s, kind: kind)
|
|
419
434
|
end
|
|
420
435
|
|
|
421
436
|
SYNTHESIZER_CACHE_PRODUCER_ID = "plugin.source_rbs_synthesizer"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../type/anonymous_class_name"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Inference
|
|
9
|
+
# #319 — identity for the class a `Class.new do ... end` / `Module.new do ... end` creates away from
|
|
10
|
+
# constant-write position.
|
|
11
|
+
#
|
|
12
|
+
# Ruby evaluates such a block with `self` bound to the freshly created class (`class_eval` semantics): a
|
|
13
|
+
# `def` inside it defines an instance method on that class and `attr_reader` runs as a class-level macro.
|
|
14
|
+
# `ScopeIndexer` has always walked that body as a class body — but only through its
|
|
15
|
+
# `Prism::ConstantWriteNode` branch, because the constant supplied the name the discovered-method tables
|
|
16
|
+
# are keyed by. At every other position (`klass = Class.new do ... end`, a bare expression, a method
|
|
17
|
+
# return) there was no name, so the body fell through to the enclosing — usually top-level — scope. Two
|
|
18
|
+
# false positives followed on correct code: `attr_reader` became a `call.unresolved-toplevel` warning
|
|
19
|
+
# (`Scope#toplevel?` is `self_type.nil?`), and the class itself typed as the bare `Singleton[Object]`
|
|
20
|
+
# `MethodDispatcher#class_new_lift` hands back, taking `Object`'s zero-arity `new` with it.
|
|
21
|
+
#
|
|
22
|
+
# This module supplies the missing name: a per-call-site synthetic one, spelled so that no Ruby constant
|
|
23
|
+
# path can collide with it in the discovery tables, and stable across the index pass and the dispatch
|
|
24
|
+
# pass because both derive it from the same `(call node, source path)` pair.
|
|
25
|
+
module AnonymousMetaClass
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
# The class-creating meta calls whose block body is a class body. `Struct.new` / `Data.define` are here
|
|
29
|
+
# because the body semantics are identical — the block is `class_eval`'d on the generated subclass — not
|
|
30
|
+
# because their anonymous *value* is modelled: those keep the `StructClass` / `DataClass` carriers
|
|
31
|
+
# {MethodDispatcher::StructFolding} / {MethodDispatcher::DataFolding} build for them.
|
|
32
|
+
META_NEW_SELECTORS = { Class: :new, Module: :new, Struct: :new, Data: :define }.freeze
|
|
33
|
+
|
|
34
|
+
# The receiver constant name (`:Class`, `:Module`, `:Struct`, `:Data`) of a class-creating meta call that
|
|
35
|
+
# carries a literal block, or nil for anything else. The receiver MUST be the bare constant (or its
|
|
36
|
+
# `::`-rooted spelling) — a call through a variable or another call's return has no statically known
|
|
37
|
+
# identity, exactly as `ScopeIndexer#meta_constant_receiver?` requires.
|
|
38
|
+
#
|
|
39
|
+
# Argument shapes are deliberately NOT inspected: `Class.new(Base) { ... }` and
|
|
40
|
+
# `Data.define(*members) { ... }` evaluate their block as a class body whatever the arguments are, and a
|
|
41
|
+
# narrower gate here would leave the body in top-level scope again.
|
|
42
|
+
def block_form_receiver(node)
|
|
43
|
+
return nil unless node.is_a?(Prism::CallNode)
|
|
44
|
+
return nil unless node.block.is_a?(Prism::BlockNode)
|
|
45
|
+
|
|
46
|
+
receiver_name = literal_constant_name(node.receiver)
|
|
47
|
+
return nil if receiver_name.nil?
|
|
48
|
+
return nil unless META_NEW_SELECTORS[receiver_name] == node.name
|
|
49
|
+
|
|
50
|
+
receiver_name
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The synthetic class name for `node`'s block body, or nil when `node` is not a recognised block form.
|
|
54
|
+
# {Type::AnonymousClassName} owns the spelling; this decides which call sites earn one and what the key
|
|
55
|
+
# holds.
|
|
56
|
+
#
|
|
57
|
+
# `source_path` is part of the key so two files whose anonymous classes happen to share a line and column
|
|
58
|
+
# do not merge their method sets when the per-file tables are folded into the cross-file seed. It is
|
|
59
|
+
# omitted when the caller has no path (unit-level probes over a bare `Prism` program) — passed through
|
|
60
|
+
# verbatim rather than defaulted, so every pass over the same file derives the same name.
|
|
61
|
+
def name_for(node, source_path = nil)
|
|
62
|
+
receiver_name = block_form_receiver(node)
|
|
63
|
+
return nil if receiver_name.nil?
|
|
64
|
+
|
|
65
|
+
location = node.location
|
|
66
|
+
key = [source_path, location.start_line, location.start_column].compact.join(":")
|
|
67
|
+
Type::AnonymousClassName.build(receiver_name, key)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The bare-constant receiver's name (`Class`, `::Class`), or nil for every other receiver shape.
|
|
71
|
+
def literal_constant_name(node)
|
|
72
|
+
case node
|
|
73
|
+
when Prism::ConstantReadNode
|
|
74
|
+
node.name
|
|
75
|
+
when Prism::ConstantPathNode
|
|
76
|
+
node.name if node.parent.nil?
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -19,6 +19,8 @@ require_relative "indexed_narrowing"
|
|
|
19
19
|
require_relative "macro_block_self_type"
|
|
20
20
|
require_relative "method_dispatcher"
|
|
21
21
|
require_relative "narrowing"
|
|
22
|
+
require_relative "singleton_object_constant"
|
|
23
|
+
require_relative "optimistic_origin"
|
|
22
24
|
require_relative "struct_fold_safety"
|
|
23
25
|
|
|
24
26
|
module Rigor
|
|
@@ -609,6 +611,13 @@ module Rigor
|
|
|
609
611
|
elide_or_union(node.predicate, else_type, then_type)
|
|
610
612
|
end
|
|
611
613
|
|
|
614
|
+
# Issue #286 — the effective optimistic-nil-free cause of an expression. {OptimisticOrigin.resolve} owns
|
|
615
|
+
# the judgment, shared verbatim with `StatementEvaluator#optimistic_origin_for` and the
|
|
616
|
+
# `flow.always-truthy-condition` collector.
|
|
617
|
+
def optimistic_origin_for(node)
|
|
618
|
+
OptimisticOrigin.resolve(node, scope)
|
|
619
|
+
end
|
|
620
|
+
|
|
612
621
|
def if_else_type(subsequent)
|
|
613
622
|
return Type::Combinator.constant_of(nil) if subsequent.nil?
|
|
614
623
|
|
|
@@ -632,6 +641,9 @@ module Rigor
|
|
|
632
641
|
# `Constant[false]` fold one branch; `Union[true, false]`, `Dynamic[T]`, and `Top` keep both branches live.
|
|
633
642
|
def constant_predicate_polarity(predicate)
|
|
634
643
|
return nil if predicate.nil?
|
|
644
|
+
# ADR-101 — decline on an optimistically nil-free carrier; see
|
|
645
|
+
# `StatementEvaluator#optimistic_carrier?` for why the gate is here and not in `Narrowing`.
|
|
646
|
+
return nil unless optimistic_origin_for(predicate).nil?
|
|
635
647
|
|
|
636
648
|
Narrowing.predicate_certainty(type_of(predicate))
|
|
637
649
|
end
|
|
@@ -648,7 +660,7 @@ module Rigor
|
|
|
648
660
|
# fallback is preserved.
|
|
649
661
|
def type_of_and_or(node)
|
|
650
662
|
left_type = type_of(node.left)
|
|
651
|
-
polarity =
|
|
663
|
+
polarity = left_operand_polarity(node.left, left_type)
|
|
652
664
|
return short_circuit_for(node, left_type, polarity) if polarity
|
|
653
665
|
|
|
654
666
|
# The left operand only flows through on the edge that short-circuits: `a || b` yields `a` solely
|
|
@@ -674,6 +686,19 @@ module Rigor
|
|
|
674
686
|
end
|
|
675
687
|
end
|
|
676
688
|
|
|
689
|
+
# Issue #313 — the node-aware wrapper the `&&` / `||` short-circuit reads. The spec's exclusion binds
|
|
690
|
+
# this gate as much as it binds `flow.always-truthy-condition`, and a `Constant`-only gate is not by
|
|
691
|
+
# itself enough to honour it: a literal hash whose values share one type reads as a lone `Constant`
|
|
692
|
+
# (`UNIFORM[key]` → `Constant[1]`), so the gate would judge the left operand of `UNIFORM[key] || key`
|
|
693
|
+
# provably truthy and discard the author's fallback — the counter-example the spec names verbatim.
|
|
694
|
+
# Declining returns the union of both operands, which is what `StatementEvaluator#eval_and_or` produces
|
|
695
|
+
# anyway, so the two `&&` / `||` typers stay in agreement.
|
|
696
|
+
def left_operand_polarity(left_node, left_type)
|
|
697
|
+
return nil unless optimistic_origin_for(left_node).nil?
|
|
698
|
+
|
|
699
|
+
constant_value_polarity(left_type)
|
|
700
|
+
end
|
|
701
|
+
|
|
677
702
|
# Returns `:truthy` / `:falsey` for a `Type::Constant`, nil otherwise. Mirrors
|
|
678
703
|
# `constant_predicate_polarity` but operates on a typed value (already-type-of'd) rather than a Prism
|
|
679
704
|
# node, so the same predicate analysis can be reused in both contexts.
|
|
@@ -1077,8 +1102,12 @@ module Rigor
|
|
|
1077
1102
|
# through `Enumerable#select` / `Object#select` and the caller observes `Array[Elem]` instead of the
|
|
1078
1103
|
# helper's actual return type. The check fires only for `node.receiver.nil?` (true implicit self), so
|
|
1079
1104
|
# explicit-receiver dispatch is unaffected.
|
|
1105
|
+
#
|
|
1106
|
+
# Issue #316 — the lookup goes through the confidence-gated `Scope#bindable_top_level_def_for`, not the
|
|
1107
|
+
# raw table: inside a block whose `self` is unmodelled, a top-level `def` from ANOTHER file is not
|
|
1108
|
+
# evidence about which method the call reaches, so the bind is declined and the call widens.
|
|
1080
1109
|
def try_local_def_dispatch(node, receiver, arg_types)
|
|
1081
|
-
local_def = node.receiver.nil? ? scope.
|
|
1110
|
+
local_def = node.receiver.nil? ? scope.bindable_top_level_def_for(node.name) : nil
|
|
1082
1111
|
return nil unless local_def
|
|
1083
1112
|
|
|
1084
1113
|
local_inference = infer_top_level_user_method(local_def, receiver, arg_types)
|
|
@@ -1144,7 +1173,7 @@ module Rigor
|
|
|
1144
1173
|
# `module_function` body, re-type that body with the call args bound. Sits after the RBS dispatch
|
|
1145
1174
|
# tier, so foreign / RBS-known singletons (`Math.sqrt`) keep their catalog answer; only
|
|
1146
1175
|
# project-defined singleton methods reach here.
|
|
1147
|
-
singleton_inference =
|
|
1176
|
+
singleton_inference = try_project_singleton_inference(receiver, node, arg_types)
|
|
1148
1177
|
return singleton_inference if singleton_inference
|
|
1149
1178
|
|
|
1150
1179
|
# Dynamic-origin propagation: when the receiver is Dynamic[T] and no positive rule resolves the call,
|
|
@@ -1321,6 +1350,31 @@ module Rigor
|
|
|
1321
1350
|
nil
|
|
1322
1351
|
end
|
|
1323
1352
|
|
|
1353
|
+
# The project-side singleton-method band: a `Foo.bar` call resolved against a `class << …` / `def self.…`
|
|
1354
|
+
# body the project itself wrote. Which of the two tiers applies is decided by the receiver carrier —
|
|
1355
|
+
# `Singleton[Foo]` when the constant names a class or module, `Nominal[…]` when it holds an ordinary
|
|
1356
|
+
# object (#320) — so the two are mutually exclusive and consulting both is one resolution attempt.
|
|
1357
|
+
def try_project_singleton_inference(receiver, call_node, arg_types)
|
|
1358
|
+
try_singleton_method_inference(receiver, call_node, arg_types) ||
|
|
1359
|
+
try_singleton_object_constant_inference(receiver, call_node, arg_types)
|
|
1360
|
+
end
|
|
1361
|
+
|
|
1362
|
+
# #320 — resolves a call whose receiver is a constant holding an ordinary object with a `class << Const`
|
|
1363
|
+
# singleton body, re-typing that body with the call's argument types bound. `self` inside the body IS
|
|
1364
|
+
# that object, so the receiver carrier is passed through unchanged. Own-constant only, and only for a
|
|
1365
|
+
# name the project actually recorded — a miss degrades to today's `Dynamic[top]`, never a false
|
|
1366
|
+
# resolution. `Singleton` receivers never reach here: {#try_singleton_method_inference} owns them.
|
|
1367
|
+
def try_singleton_object_constant_inference(receiver, call_node, arg_types)
|
|
1368
|
+
return nil unless receiver.is_a?(Type::Nominal)
|
|
1369
|
+
|
|
1370
|
+
def_node = SingletonObjectConstant.def_node_for(call_node, receiver, call_node.name, scope)
|
|
1371
|
+
return nil if def_node.nil?
|
|
1372
|
+
|
|
1373
|
+
infer_user_method_return(def_node, receiver, arg_types)
|
|
1374
|
+
rescue StandardError
|
|
1375
|
+
nil
|
|
1376
|
+
end
|
|
1377
|
+
|
|
1324
1378
|
# ADR-24 slice 2 — resolves `method_name` against `class_name`'s own `def`s, then walks the user-class
|
|
1325
1379
|
# ancestor chain: included / prepended modules (transitive) and the superclass chain. RBS-known
|
|
1326
1380
|
# ancestors are NOT walked here — the `MethodDispatcher` RBS tier runs before
|
|
@@ -2429,7 +2483,11 @@ module Rigor
|
|
|
2429
2483
|
case block_arg
|
|
2430
2484
|
when Prism::BlockNode
|
|
2431
2485
|
bindings = BlockParameterBinder.new(expected_param_types: expected).bind(block_arg)
|
|
2432
|
-
|
|
2486
|
+
# Issue #316 — mirrors `StatementEvaluator#build_block_entry_scope`: the block body's `self` is the
|
|
2487
|
+
# yielding method's business, so the return-typing pass must see the same unmodelled-self mark.
|
|
2488
|
+
block_scope = bindings.reduce(scope.entering_opaque_block) do |acc, (name, type)|
|
|
2489
|
+
acc.with_local(name, type)
|
|
2490
|
+
end
|
|
2433
2491
|
block_scope = block_scope.with_self_type(narrowed_self_type) if narrowed_self_type
|
|
2434
2492
|
type_block_body(block_arg, block_scope)
|
|
2435
2493
|
when Prism::BlockArgumentNode
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "tmpdir"
|
|
4
4
|
|
|
5
|
+
require_relative "../runtime/jit"
|
|
6
|
+
|
|
5
7
|
module Rigor
|
|
6
8
|
module Inference
|
|
7
9
|
# Generic fork-over-slices map for the embarrassingly-parallel per-file passes of the
|
|
@@ -35,6 +37,27 @@ module Rigor
|
|
|
35
37
|
worker_count > 1 && Process.respond_to?(:fork)
|
|
36
38
|
end
|
|
37
39
|
|
|
40
|
+
# Where a forked child may put process-private scratch files. `nil` on the parent and on the sequential
|
|
41
|
+
# path, meaning "you are not in a child — use `Dir.tmpdir`".
|
|
42
|
+
#
|
|
43
|
+
# A child ends at {.run_worker}'s `exit!`, which skips `at_exit` AND object finalizers, so a `Tempfile`
|
|
44
|
+
# a child creates is never reclaimed by the mechanism that reclaims the parent's (issue #330 —
|
|
45
|
+
# `Protection::ClosureKillOracle`'s per-process mutant file leaked one file per worker per
|
|
46
|
+
# `rigor coverage --protection --mutation` run). Answering with the directory the parent already removes
|
|
47
|
+
# once every child has been collected makes the child's scratch state the parent's problem, which is the
|
|
48
|
+
# only place it can be solved: the names are random, so nothing outside the child could find them.
|
|
49
|
+
#
|
|
50
|
+
# The directory check is the belt to {.run_worker}'s braces: the offer is only worth taking while the
|
|
51
|
+
# parent still has the directory open, and answering `nil` once it is gone degrades to `Dir.tmpdir`
|
|
52
|
+
# instead of raising `Errno::ENOENT` somewhere far from here.
|
|
53
|
+
#
|
|
54
|
+
# @return [String, nil]
|
|
55
|
+
def child_scratch_dir
|
|
56
|
+
return nil unless @child_scratch_dir && File.directory?(@child_scratch_dir)
|
|
57
|
+
|
|
58
|
+
@child_scratch_dir
|
|
59
|
+
end
|
|
60
|
+
|
|
38
61
|
# Forks `worker_count` children over contiguous slices, waits for each, and returns the Marshal'd
|
|
39
62
|
# per-slice payloads in slice order. A child that exits abnormally has its slice re-run in-process.
|
|
40
63
|
def fork_map(items, worker_count, block)
|
|
@@ -55,11 +78,31 @@ module Rigor
|
|
|
55
78
|
# Child-process body: run the block over the slice, Marshal the result to `out_path`, and `exit!`
|
|
56
79
|
# (skipping `at_exit` / stdio flush — the payload is durable on disk). Any failure exits non-zero so
|
|
57
80
|
# the parent re-runs the slice in-process.
|
|
81
|
+
# The `ensure` is not dead code even though `exit!` never unwinds: {.child_scratch_dir} is module state,
|
|
82
|
+
# and a caller that drives this body in-process with `exit!` stubbed (the deferred-YJIT spec does exactly
|
|
83
|
+
# that — a spy in the parent cannot observe a real child) would otherwise leave the offer standing for
|
|
84
|
+
# the rest of the process, pointing at a directory its `Dir.mktmpdir` block has since removed.
|
|
58
85
|
def run_worker(slice, block, out_path)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
86
|
+
status = begin
|
|
87
|
+
# Re-arm deferred YJIT in the child, exactly as the check fork pool does
|
|
88
|
+
# ({Analysis::Runner::PoolCoordinator#run_fork_worker}). The parent's deadline thread does not
|
|
89
|
+
# survive `fork`, so without this a worker runs its whole slice un-JITted while the parent JITs the
|
|
90
|
+
# tail it no longer runs — the pool was a *pessimization* on long runs (`coverage --protection
|
|
91
|
+
# --mutation lib/rigor/analysis`: 37s sequential against 67s at eight workers). The child picks up
|
|
92
|
+
# what is left of the parent's window rather than a fresh one; {Runtime::Jit.rearm_after_fork}
|
|
93
|
+
# carries the mechanism.
|
|
94
|
+
Runtime::Jit.rearm_after_fork
|
|
95
|
+
# Published before the block runs, so anything it lazily creates for this process alone lands in the
|
|
96
|
+
# parent's tmpdir and dies with it. See {.child_scratch_dir}.
|
|
97
|
+
@child_scratch_dir = File.dirname(out_path)
|
|
98
|
+
File.binwrite(out_path, Marshal.dump(block.call(slice)))
|
|
99
|
+
0
|
|
100
|
+
rescue StandardError
|
|
101
|
+
1
|
|
102
|
+
ensure
|
|
103
|
+
@child_scratch_dir = nil
|
|
104
|
+
end
|
|
105
|
+
exit!(status)
|
|
63
106
|
end
|
|
64
107
|
|
|
65
108
|
# Waits for every child, placing each successful payload at its slice index; a child that exited
|
|
@@ -117,7 +117,16 @@ module Rigor
|
|
|
117
117
|
:succ, :pred, :next, :abs, :magnitude,
|
|
118
118
|
:bit_length, :to_s, :to_i, :to_int, :to_f,
|
|
119
119
|
:floor, :ceil, :round, :truncate, :chr,
|
|
120
|
-
:inspect, :-@, :+@, :~, :to_r, :to_c
|
|
120
|
+
:inspect, :-@, :+@, :~, :to_r, :to_c,
|
|
121
|
+
# `rationalize` (no-arg form) is the Integer sibling of the already-folded
|
|
122
|
+
# `Float#rationalize` / `Rational#rationalize` — pure, returns `Rational(self, 1)`. The
|
|
123
|
+
# optional-`eps` binary form is NOT mirrored here: unlike `Rational#rationalize` (which reaches
|
|
124
|
+
# `NUMERIC_BINARY`'s catalog fallback via `numeric.yml`'s `nurat_rationalize` leaf entry),
|
|
125
|
+
# `numeric.yml` carries no `rationalize` entry for Integer/Float, so the 1-arg call declines to
|
|
126
|
+
# the RBS tier exactly as it does today — this only closes the 0-arg gap.
|
|
127
|
+
# `abs2` (`self * self`, always a non-negative Integer) is the Integer sibling of the
|
|
128
|
+
# already-folded `Rational#abs2` / `Complex#abs2`.
|
|
129
|
+
:rationalize, :abs2
|
|
121
130
|
].freeze
|
|
122
131
|
FLOAT_UNARY = Set[
|
|
123
132
|
:zero?, :positive?, :negative?, :nonzero?,
|
|
@@ -135,7 +144,12 @@ module Rigor
|
|
|
135
144
|
# `self >= 0`, `Math::PI` for `self < 0`. Pure sign test, deterministic; a NaN receiver yields NaN
|
|
136
145
|
# which `foldable_constant_value?` declines.
|
|
137
146
|
:arg, :angle, :phase,
|
|
138
|
-
:inspect, :-@,
|
|
147
|
+
:inspect, :-@, :+@,
|
|
148
|
+
# `abs2` (`self * self`) is the Float sibling of the already-folded `Rational#abs2` /
|
|
149
|
+
# `Complex#abs2` / (now) `Integer#abs2`. A non-finite receiver (`Infinity`, `NaN`) still folds
|
|
150
|
+
# via `foldable_constant_value?`'s existing NaN guard — `Infinity.abs2` is `Infinity`
|
|
151
|
+
# (foldable), `NaN.abs2` is `NaN` (declined, same as every other NaN-producing Float unary op).
|
|
152
|
+
:abs2
|
|
139
153
|
].freeze
|
|
140
154
|
STRING_UNARY = Set[
|
|
141
155
|
:upcase, :downcase, :capitalize, :swapcase,
|
|
@@ -195,6 +209,11 @@ module Rigor
|
|
|
195
209
|
# `flow.always-truthy-condition`.
|
|
196
210
|
REFLECTIVE_SEND_METHODS = %i[public_send send __send__].to_set.freeze
|
|
197
211
|
|
|
212
|
+
# The self-returners, the `Type::Constant` counterpart of `ShapeDispatch`'s `:shape_self` handlers
|
|
213
|
+
# (ADR-76 WD2 / ADR-78 WD3). Each returns the receiver at run time, so the value-pinned carrier
|
|
214
|
+
# survives the call.
|
|
215
|
+
CONSTANT_SELF_RETURNERS = %i[freeze itself dup clone].to_set.freeze
|
|
216
|
+
|
|
198
217
|
# @return [Rigor::Type::Constant, Rigor::Type::Union, Rigor::Type::IntegerRange, nil]
|
|
199
218
|
def try_dispatch(context)
|
|
200
219
|
receiver = context.receiver
|
|
@@ -205,6 +224,9 @@ module Rigor
|
|
|
205
224
|
first_arg = args.first
|
|
206
225
|
return nil unless first_arg.is_a?(Type::Constant) && first_arg.value.is_a?(Symbol)
|
|
207
226
|
end
|
|
227
|
+
self_return = try_fold_self_return(receiver, method_name, args)
|
|
228
|
+
return self_return if self_return
|
|
229
|
+
|
|
208
230
|
# v0.0.7 — `String#%` against a `Tuple` / `HashShape` argument runs Ruby's format-string engine
|
|
209
231
|
# when both sides are statically constant. The standard `numeric_set_of` path bails on Tuple /
|
|
210
232
|
# HashShape arguments because they are not scalar-Constant carriers, so the special-case sits
|
|
@@ -221,6 +243,32 @@ module Rigor
|
|
|
221
243
|
dispatch_by_arity(receiver_set, method_name, arg_sets)
|
|
222
244
|
end
|
|
223
245
|
|
|
246
|
+
# `freeze` / `itself` / `dup` / `clone` on a value-pinned receiver return the receiver carrier.
|
|
247
|
+
#
|
|
248
|
+
# This sits AHEAD of the catalogue path and deliberately bypasses its purity gate, because the gate
|
|
249
|
+
# asks a question that does not apply. `String#freeze` is classified `purity: mutates_self`
|
|
250
|
+
# (`data/builtins/ruby_core/string.yml`) — correctly, since the C body sets `FL_FREEZE` — and the
|
|
251
|
+
# gate then holds it back from the pure-invoke path. But nothing here is invoked: the receiver type
|
|
252
|
+
# is returned unchanged, and freezing mutates an object's flags, not its value. `Type::Constant`
|
|
253
|
+
# pins a value, not an identity, so the mutation it performs is invisible to the carrier.
|
|
254
|
+
#
|
|
255
|
+
# The gap this closes is `FOO = "bar".freeze`, the standard way to write a String constant in
|
|
256
|
+
# Ruby, which lost its value while `%w[a b].freeze` (a Tuple, via `:shape_self`) and `"bar".-@`
|
|
257
|
+
# (classified `purity: leaf`) both kept theirs. Integer / Float / Symbol had the same hole for a
|
|
258
|
+
# different reason — no `Object`/`Kernel` catalogue owns their self-returners at all.
|
|
259
|
+
#
|
|
260
|
+
# `dup` / `clone` return a NEW object at run time, so the carrier is right about the value and
|
|
261
|
+
# silent about the identity — the same trade `shape_self` already makes, and the same one the
|
|
262
|
+
# existing `"x".dup` fold (which reaches the invoke path, `dup` being `leaf`) already makes.
|
|
263
|
+
# A `clone(freeze: false)` carries an argument and declines here.
|
|
264
|
+
def try_fold_self_return(receiver, method_name, args)
|
|
265
|
+
return nil unless args.empty?
|
|
266
|
+
return nil unless CONSTANT_SELF_RETURNERS.include?(method_name)
|
|
267
|
+
return nil unless receiver.is_a?(Type::Constant)
|
|
268
|
+
|
|
269
|
+
receiver
|
|
270
|
+
end
|
|
271
|
+
|
|
224
272
|
# `Constant<String> % …` — runs the actual `String#%` operation when both sides are statically
|
|
225
273
|
# known. The argument may be:
|
|
226
274
|
# - A `Type::Constant` whose value is a scalar (Integer / Float / String / Symbol). Already handled
|
|
@@ -116,7 +116,7 @@ module Rigor
|
|
|
116
116
|
map = member_map_for_new(members, context)
|
|
117
117
|
return degraded_instance(class_name) if map.nil?
|
|
118
118
|
|
|
119
|
-
Type::Combinator.data_instance_of(members: map, class_name: class_name)
|
|
119
|
+
Type::Combinator.data_instance_of(members: widen_unowned_emptiness(map), class_name: class_name)
|
|
120
120
|
end
|
|
121
121
|
|
|
122
122
|
# Builds the member -> type map from the call's arguments, honouring the keyword vs positional
|
|
@@ -4,6 +4,7 @@ require "prism"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../../type"
|
|
6
6
|
require_relative "../../source/node_children"
|
|
7
|
+
require_relative "../mutation_widening"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
module Inference
|
|
@@ -18,6 +19,39 @@ module Rigor
|
|
|
18
19
|
# Both folders `extend` this module so the projections resolve as their own module functions
|
|
19
20
|
# (matching their `module_function` style).
|
|
20
21
|
module MemberShapeProjection
|
|
22
|
+
# A value object's member holds a **reference** to whatever the constructor was handed, and an
|
|
23
|
+
# EMPTY container literal's emptiness is a fact about that argument at the instant of
|
|
24
|
+
# construction — never a fact about the member. The caller keeps its own alias, and "construct
|
|
25
|
+
# empty, then fill" (`r = R.new([], []); xs.each { |x| r.items << x }`) is the dominant Ruby
|
|
26
|
+
# shape, so of every literal a member map can pin, emptiness is the one falsified most often.
|
|
27
|
+
#
|
|
28
|
+
# It is also the only one whose reads fold to `nil` — `.first` / `.last` / `[]` on an empty
|
|
29
|
+
# container — and `nil` is the receiver type that fires `call.undefined-method` on correct code
|
|
30
|
+
# (issue #293). A non-empty literal's element evidence, by contrast, survives an append and its
|
|
31
|
+
# reads never fold to `nil`, so it is kept intact.
|
|
32
|
+
#
|
|
33
|
+
# So an empty `Tuple` / closed empty `HashShape` is recorded widened to its bare nominal —
|
|
34
|
+
# the same carrier, through the same helpers, that {Inference::MutationWidening} installs when
|
|
35
|
+
# it retracts a mutated local's literal shape.
|
|
36
|
+
def widen_unowned_emptiness(member_map)
|
|
37
|
+
return member_map if member_map.nil?
|
|
38
|
+
|
|
39
|
+
member_map.transform_values { |type| widened_empty_container(type) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def widened_empty_container(type)
|
|
43
|
+
case type
|
|
44
|
+
when Type::Tuple
|
|
45
|
+
type.elements.empty? ? MutationWidening.widen_tuple(type) : type
|
|
46
|
+
when Type::HashShape
|
|
47
|
+
# An OPEN shape claims nothing about the keys it does not list — a read of an unlisted key
|
|
48
|
+
# is already `untyped`, not `nil` — so only the closed literal `{}` carries the emptiness.
|
|
49
|
+
type.closed? && type.pairs.empty? ? MutationWidening.widen_hash_shape(type) : type
|
|
50
|
+
else
|
|
51
|
+
type
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
21
55
|
# A Data/Struct subclass body can redefine a member's synthesised reader (`def x`); when it does,
|
|
22
56
|
# `inst.x` runs that `def`, not the member, so folding the read would be unsound. A real `def` node
|
|
23
57
|
# under the class name is the discriminator (the synthesised reader has none), so an entry in the
|
|
@@ -109,7 +143,7 @@ module Rigor
|
|
|
109
143
|
return nil unless shape.optional_keys.empty?
|
|
110
144
|
return nil unless shape.pairs.keys.all? { |key| instance.members.key?(key) }
|
|
111
145
|
|
|
112
|
-
merged = instance.members.merge(shape.pairs)
|
|
146
|
+
merged = widen_unowned_emptiness(instance.members.merge(shape.pairs))
|
|
113
147
|
yield(merged, instance.class_name)
|
|
114
148
|
end
|
|
115
149
|
end
|