rigortype 0.0.8 → 0.1.0
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 +234 -22
- data/data/builtins/ruby_core/encoding.yml +210 -0
- data/data/builtins/ruby_core/exception.yml +641 -0
- data/data/builtins/ruby_core/numeric.yml +3 -2
- data/data/builtins/ruby_core/proc.yml +731 -0
- data/data/builtins/ruby_core/random.yml +166 -0
- data/data/builtins/ruby_core/re.yml +689 -0
- data/data/builtins/ruby_core/struct.yml +449 -0
- data/lib/rigor/analysis/check_rules.rb +228 -40
- data/lib/rigor/analysis/diagnostic.rb +15 -1
- data/lib/rigor/analysis/runner.rb +199 -4
- data/lib/rigor/builtins/imported_refinements.rb +6 -1
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +63 -0
- data/lib/rigor/cache/rbs_class_type_param_names.rb +60 -0
- data/lib/rigor/cache/rbs_constant_table.rb +15 -51
- data/lib/rigor/cache/rbs_descriptor.rb +55 -0
- data/lib/rigor/cache/rbs_environment.rb +52 -0
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +40 -0
- data/lib/rigor/cache/rbs_instance_definitions.rb +79 -0
- data/lib/rigor/cache/rbs_known_class_names.rb +43 -0
- data/lib/rigor/cache/store.rb +81 -15
- data/lib/rigor/cli.rb +45 -7
- data/lib/rigor/configuration/severity_profile.rb +109 -0
- data/lib/rigor/configuration.rb +110 -6
- data/lib/rigor/environment/rbs_hierarchy.rb +18 -5
- data/lib/rigor/environment/rbs_loader.rb +220 -32
- data/lib/rigor/environment.rb +11 -2
- data/lib/rigor/flow_contribution/conflict.rb +81 -0
- data/lib/rigor/flow_contribution/element.rb +53 -0
- data/lib/rigor/flow_contribution/fact.rb +88 -0
- data/lib/rigor/flow_contribution/merge_result.rb +67 -0
- data/lib/rigor/flow_contribution/merger.rb +275 -0
- data/lib/rigor/flow_contribution.rb +179 -0
- data/lib/rigor/inference/block_parameter_binder.rb +15 -0
- data/lib/rigor/inference/builtins/encoding_catalog.rb +67 -0
- data/lib/rigor/inference/builtins/exception_catalog.rb +92 -0
- data/lib/rigor/inference/builtins/proc_catalog.rb +122 -0
- data/lib/rigor/inference/builtins/random_catalog.rb +58 -0
- data/lib/rigor/inference/builtins/re_catalog.rb +81 -0
- data/lib/rigor/inference/builtins/struct_catalog.rb +55 -0
- data/lib/rigor/inference/expression_typer.rb +110 -6
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +16 -1
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +173 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +21 -1
- data/lib/rigor/inference/method_dispatcher.rb +2 -0
- data/lib/rigor/inference/multi_target_binder.rb +2 -0
- data/lib/rigor/inference/narrowing.rb +134 -144
- data/lib/rigor/inference/scope_indexer.rb +75 -1
- data/lib/rigor/inference/statement_evaluator.rb +380 -40
- data/lib/rigor/plugin/access_denied_error.rb +24 -0
- data/lib/rigor/plugin/base.rb +241 -0
- data/lib/rigor/plugin/io_boundary.rb +102 -0
- data/lib/rigor/plugin/load_error.rb +23 -0
- data/lib/rigor/plugin/loader.rb +191 -0
- data/lib/rigor/plugin/manifest.rb +134 -0
- data/lib/rigor/plugin/registry.rb +50 -0
- data/lib/rigor/plugin/services.rb +65 -0
- data/lib/rigor/plugin/trust_policy.rb +99 -0
- data/lib/rigor/plugin.rb +61 -0
- data/lib/rigor/rbs_extended.rb +103 -0
- data/lib/rigor/reflection.rb +2 -2
- data/lib/rigor/type/combinator.rb +72 -0
- data/lib/rigor/type/refined.rb +50 -2
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +13 -0
- data/sig/rigor/environment.rbs +7 -1
- data/sig/rigor/inference.rbs +1 -0
- data/sig/rigor/rbs_extended.rbs +2 -0
- data/sig/rigor/scope.rbs +1 -0
- data/sig/rigor/type.rbs +7 -0
- data/sig/rigor.rbs +3 -1
- metadata +38 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "method_catalog"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module Builtins
|
|
8
|
+
# `Proc` / `Method` / `UnboundMethod` catalog. Singleton —
|
|
9
|
+
# load once, consult during dispatch.
|
|
10
|
+
#
|
|
11
|
+
# The three callable carriers are imported together because
|
|
12
|
+
# `Init_Proc` registers them in a single C init block. They
|
|
13
|
+
# share the same fundamental hazard at the catalog tier:
|
|
14
|
+
# most of their public methods invoke the wrapped Ruby code
|
|
15
|
+
# (the proc body, the bound method's receiver, …) and that
|
|
16
|
+
# code can do anything — read mutable state, call I/O, return
|
|
17
|
+
# different values on successive calls. The static C-body
|
|
18
|
+
# classifier marks these `:leaf` because the C functions
|
|
19
|
+
# themselves do not call `rb_funcall*` / `rb_yield` directly
|
|
20
|
+
# (they delegate through the VM's optimised call paths and
|
|
21
|
+
# method-entry table), but folding any of them at compile
|
|
22
|
+
# time would freeze a value the runtime never actually
|
|
23
|
+
# produces twice.
|
|
24
|
+
#
|
|
25
|
+
# The blocklist below errs aggressively on the side of
|
|
26
|
+
# caution: a hypothetical future `Constant<Proc>` /
|
|
27
|
+
# `Constant<Method>` / `Constant<UnboundMethod>` carrier
|
|
28
|
+
# would have very little to gain from these folds and a
|
|
29
|
+
# great deal to lose if user code ran behind the analyzer's
|
|
30
|
+
# back. Reflective readers (`#arity`, `#parameters`,
|
|
31
|
+
# `#source_location`, `#name`, `#owner`, `#receiver`) remain
|
|
32
|
+
# foldable; the RBS tier still resolves return types for
|
|
33
|
+
# the blocklisted methods so callers do not lose precision.
|
|
34
|
+
PROC_CATALOG = MethodCatalog.new(
|
|
35
|
+
path: File.expand_path(
|
|
36
|
+
"../../../../data/builtins/ruby_core/proc.yml",
|
|
37
|
+
__dir__
|
|
38
|
+
),
|
|
39
|
+
mutating_selectors: {
|
|
40
|
+
"Proc" => Set[
|
|
41
|
+
# `#call` / `#[]` / `#===` / `#yield` invoke the proc
|
|
42
|
+
# body. The C body routes through
|
|
43
|
+
# `OPTIMIZED_METHOD_TYPE_CALL` (a VM fast path the
|
|
44
|
+
# classifier cannot see into); the proc body can do
|
|
45
|
+
# anything — read globals, mutate captured locals,
|
|
46
|
+
# raise. MUST decline to fold.
|
|
47
|
+
:call,
|
|
48
|
+
:[],
|
|
49
|
+
:===,
|
|
50
|
+
:yield,
|
|
51
|
+
# `#curry` / `#<<` / `#>>` allocate a fresh `Proc`
|
|
52
|
+
# that closes over the receiver (and, for `<<` /
|
|
53
|
+
# `>>`, over the argument). Folding would freeze a
|
|
54
|
+
# specific `Proc` instance whose identity the runtime
|
|
55
|
+
# never actually produces (object_id differs every
|
|
56
|
+
# call), so the catalog tier declines.
|
|
57
|
+
:curry,
|
|
58
|
+
:<<,
|
|
59
|
+
:>>,
|
|
60
|
+
# `#to_proc` returns `self` for `Proc` (cheap), but
|
|
61
|
+
# blocking it keeps the rule shape uniform across the
|
|
62
|
+
# three callable carriers (Method#to_proc allocates a
|
|
63
|
+
# fresh `Proc`).
|
|
64
|
+
:to_proc,
|
|
65
|
+
# Identity-based equality and hashing: `#hash` is
|
|
66
|
+
# derived from the underlying ISeq pointer; `#==` /
|
|
67
|
+
# `#eql?` compare ISeq + binding. Folding to a
|
|
68
|
+
# `Constant<Integer>` / `Constant<bool>` would freeze
|
|
69
|
+
# an answer that depends on memory layout. Defensive.
|
|
70
|
+
:hash,
|
|
71
|
+
:==,
|
|
72
|
+
:eql?,
|
|
73
|
+
# `initialize_copy` is blocklisted by convention so a
|
|
74
|
+
# hypothetical future `Constant<Proc>` carrier cannot
|
|
75
|
+
# fold an aliasing copy through the catalog.
|
|
76
|
+
:initialize_copy
|
|
77
|
+
],
|
|
78
|
+
"Method" => Set[
|
|
79
|
+
# `#call` / `#[]` / `#===` invoke the bound method.
|
|
80
|
+
# Same hazard as `Proc#call`: arbitrary user code,
|
|
81
|
+
# arbitrary side effects.
|
|
82
|
+
:call,
|
|
83
|
+
:[],
|
|
84
|
+
:===,
|
|
85
|
+
# `#curry` / `#<<` / `#>>` allocate a fresh `Proc`
|
|
86
|
+
# that closes over the bound method.
|
|
87
|
+
:curry,
|
|
88
|
+
:<<,
|
|
89
|
+
:>>,
|
|
90
|
+
# `#to_proc` allocates a fresh `Proc` wrapping the
|
|
91
|
+
# bound method — folding would freeze its object_id.
|
|
92
|
+
# The classifier already marks it `:block_dependent`,
|
|
93
|
+
# but the explicit entry keeps the intent obvious.
|
|
94
|
+
:to_proc,
|
|
95
|
+
# `#unbind` allocates a fresh `UnboundMethod` whose
|
|
96
|
+
# identity differs every call.
|
|
97
|
+
:unbind,
|
|
98
|
+
# Identity-based equality and hashing.
|
|
99
|
+
:hash,
|
|
100
|
+
:==,
|
|
101
|
+
:eql?,
|
|
102
|
+
# `initialize_copy` is blocklisted by convention.
|
|
103
|
+
:initialize_copy
|
|
104
|
+
],
|
|
105
|
+
"UnboundMethod" => Set[
|
|
106
|
+
# `#bind` allocates a fresh `Method` whose object_id
|
|
107
|
+
# differs every call; `#bind_call` invokes the bound
|
|
108
|
+
# method (already classified `:block_dependent`).
|
|
109
|
+
:bind,
|
|
110
|
+
:bind_call,
|
|
111
|
+
# Identity-based equality and hashing.
|
|
112
|
+
:hash,
|
|
113
|
+
:==,
|
|
114
|
+
:eql?,
|
|
115
|
+
# `initialize_copy` is blocklisted by convention.
|
|
116
|
+
:initialize_copy
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "method_catalog"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module Builtins
|
|
8
|
+
# `Random` catalog. Singleton — load once, consult during
|
|
9
|
+
# dispatch.
|
|
10
|
+
#
|
|
11
|
+
# The static classifier marks most Random methods `:leaf`
|
|
12
|
+
# because their C bodies do not call `rb_funcall*` /
|
|
13
|
+
# `rb_yield` / `rb_check_frozen` directly. Random is the
|
|
14
|
+
# canonical case where that heuristic under-counts: every
|
|
15
|
+
# call to `#rand` / `#bytes` / `Random.rand` / `Random.bytes`
|
|
16
|
+
# advances the receiver's Mersenne-Twister state through a
|
|
17
|
+
# helper (`rand_random` -> `random_real` / `random_ulong_limited`),
|
|
18
|
+
# so folding any of them statically is unsound.
|
|
19
|
+
# `Random.new_seed` and `Random.urandom` are non-deterministic
|
|
20
|
+
# (different output every call); even though they are
|
|
21
|
+
# functionally pure they would produce a misleading constant
|
|
22
|
+
# at fold time. The whole class is conservative-by-default
|
|
23
|
+
# at the catalog tier; precision flows through the RBS layer.
|
|
24
|
+
RANDOM_CATALOG = MethodCatalog.new(
|
|
25
|
+
path: File.expand_path(
|
|
26
|
+
"../../../../data/builtins/ruby_core/random.yml",
|
|
27
|
+
__dir__
|
|
28
|
+
),
|
|
29
|
+
mutating_selectors: {
|
|
30
|
+
"Random" => Set[
|
|
31
|
+
# `rand_random` -> `random_real` / `random_ulong_limited`
|
|
32
|
+
# advance the MT state on the receiver (instance #rand)
|
|
33
|
+
# and on `Random::DEFAULT` (singleton .rand). The
|
|
34
|
+
# classifier misses the indirect mutator.
|
|
35
|
+
:rand,
|
|
36
|
+
# `random_bytes` / `random_s_bytes` consume MT output
|
|
37
|
+
# the same way #rand does — every call mutates the
|
|
38
|
+
# underlying generator.
|
|
39
|
+
:bytes,
|
|
40
|
+
# Non-deterministic: each call produces a fresh seed
|
|
41
|
+
# via `with_random_seed` reading platform entropy. Folding
|
|
42
|
+
# to a constant would freeze a value that the runtime
|
|
43
|
+
# never actually returns twice.
|
|
44
|
+
:new_seed,
|
|
45
|
+
# Non-deterministic: reads from platform CSPRNG (e.g.
|
|
46
|
+
# /dev/urandom). Folding is unsound for the same reason
|
|
47
|
+
# as `new_seed`.
|
|
48
|
+
:urandom,
|
|
49
|
+
# `initialize_copy` is blocklisted by convention so a
|
|
50
|
+
# hypothetical future `Constant<Random>` carrier
|
|
51
|
+
# cannot fold an aliasing copy through the catalog.
|
|
52
|
+
:initialize_copy
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "method_catalog"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module Builtins
|
|
8
|
+
# `Regexp` / `MatchData` catalog. Singleton — load once,
|
|
9
|
+
# consult during dispatch.
|
|
10
|
+
#
|
|
11
|
+
# `Init_Regexp` in `references/ruby/re.c` registers BOTH
|
|
12
|
+
# classes in a single C init block, so the catalog carries
|
|
13
|
+
# both — `Regexp` (the pattern carrier) plus `MatchData`
|
|
14
|
+
# (the result-of-match carrier produced by `Regexp#match` /
|
|
15
|
+
# `String#match` and consulted via `$~`). The catalog wiring
|
|
16
|
+
# therefore mostly governs:
|
|
17
|
+
#
|
|
18
|
+
# 1. The reader surface on each class (`Regexp#source`,
|
|
19
|
+
# `Regexp#options`, `Regexp#casefold?`, `MatchData#size`,
|
|
20
|
+
# `MatchData#captures`, etc.) — RBS-declared returns are
|
|
21
|
+
# preserved through dispatch.
|
|
22
|
+
# 2. The blocklist below, which keeps methods that touch
|
|
23
|
+
# process-global state (the `$~` backref) from being
|
|
24
|
+
# folded. Regexp matching is observably stateful:
|
|
25
|
+
# `Regexp#=~`, `#===` and `#~` all call `rb_backref_set`
|
|
26
|
+
# (writing `$~` and the `$1..$N` / `$&` / `` $` `` / `$'`
|
|
27
|
+
# aliases). A constant-fold that dropped those calls
|
|
28
|
+
# would silently change the visible state of the program,
|
|
29
|
+
# so they MUST decline through to the RBS tier.
|
|
30
|
+
#
|
|
31
|
+
# `Regexp.last_match` and `Regexp.timeout` / `Regexp.timeout=`
|
|
32
|
+
# are class-level (singleton) methods that also touch
|
|
33
|
+
# process-global state, but the dispatcher's catalog lookup
|
|
34
|
+
# only consults `:instance` entries today — class-method calls
|
|
35
|
+
# on a `Singleton` receiver type take the `meta_*` path in
|
|
36
|
+
# `MethodDispatcher` rather than walking `CATALOG_BY_CLASS` —
|
|
37
|
+
# so listing them here would be dead code. Their RBS-tier
|
|
38
|
+
# signatures already widen the answer enough to keep the
|
|
39
|
+
# behaviour sound; revisit if the dispatcher ever grows a
|
|
40
|
+
# singleton-aware catalog path.
|
|
41
|
+
REGEXP_CATALOG = MethodCatalog.new(
|
|
42
|
+
path: File.expand_path(
|
|
43
|
+
"../../../../data/builtins/ruby_core/re.yml",
|
|
44
|
+
__dir__
|
|
45
|
+
),
|
|
46
|
+
mutating_selectors: {
|
|
47
|
+
"Regexp" => Set[
|
|
48
|
+
# Defensive: aliasing-copy semantics already covered
|
|
49
|
+
# by the `:mutates_self` classifier, listed here for
|
|
50
|
+
# symmetry with String / Array / Hash / Range / Set.
|
|
51
|
+
:initialize_copy,
|
|
52
|
+
# `=~`, `===`, `~` all run `rb_reg_search` (or call
|
|
53
|
+
# `rb_backref_set(Qnil)` directly) — every successful
|
|
54
|
+
# OR failing match writes `$~` and the
|
|
55
|
+
# `$1..$N` / `$&` / `` $` `` / `$'` aliases. Folding
|
|
56
|
+
# would discard the visible side effect.
|
|
57
|
+
:=~,
|
|
58
|
+
:"===",
|
|
59
|
+
:~,
|
|
60
|
+
# `match` is already `:block_dependent` (the C body
|
|
61
|
+
# yields), but it ALSO writes `$~` regardless of the
|
|
62
|
+
# block. Listed here so a future extractor that
|
|
63
|
+
# reclassifies it as `:leaf` (because the yield is
|
|
64
|
+
# behind a helper) does not silently fold it.
|
|
65
|
+
:match
|
|
66
|
+
],
|
|
67
|
+
"MatchData" => Set[
|
|
68
|
+
# Defensive entry mirroring the other catalogs.
|
|
69
|
+
# `match_init_copy` is already `:leaf` per the
|
|
70
|
+
# extractor (it copies the regs slot in place but
|
|
71
|
+
# uses no helper the C-body regex flags as a
|
|
72
|
+
# mutator); blocked so a future
|
|
73
|
+
# `Constant<MatchData>` carrier never folds an
|
|
74
|
+
# aliasing copy through the catalog.
|
|
75
|
+
:initialize_copy
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "method_catalog"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module Builtins
|
|
8
|
+
# `Struct` catalog. Singleton — load once, consult during
|
|
9
|
+
# dispatch.
|
|
10
|
+
#
|
|
11
|
+
# `Struct` is a meta-class: `Struct.new(*members)` returns a
|
|
12
|
+
# fresh anonymous subclass — never a `Struct` value. Today
|
|
13
|
+
# Rigor never produces a `Constant<Struct>` carrier (a literal
|
|
14
|
+
# struct instance), so the catalog is defensive: it documents
|
|
15
|
+
# the shape and forbids unsafe folds in case a future tier
|
|
16
|
+
# learns to lift literal struct instances into the value
|
|
17
|
+
# lattice.
|
|
18
|
+
#
|
|
19
|
+
# Subclasses define their own writers (`name=`) at class-build
|
|
20
|
+
# time, so per-instance member accessors do not appear in this
|
|
21
|
+
# YAML — only the generic `[]` / `[]=` pair on the base class.
|
|
22
|
+
# `[]=` is already classified `:mutates_self`; `[]` reads a
|
|
23
|
+
# member but the answer depends on the subclass's member
|
|
24
|
+
# definition, which the catalog does not see, so we blocklist
|
|
25
|
+
# it defensively.
|
|
26
|
+
STRUCT_CATALOG = MethodCatalog.new(
|
|
27
|
+
path: File.expand_path(
|
|
28
|
+
"../../../../data/builtins/ruby_core/struct.yml",
|
|
29
|
+
__dir__
|
|
30
|
+
),
|
|
31
|
+
mutating_selectors: {
|
|
32
|
+
"Struct" => Set[
|
|
33
|
+
# Defensive: aliasing-copy semantics on a hypothetical
|
|
34
|
+
# `Constant<Struct>` carrier. Convention across the
|
|
35
|
+
# other catalogs (Range, Random, Pathname).
|
|
36
|
+
:initialize_copy,
|
|
37
|
+
# `rb_struct_hash` mixes member values via
|
|
38
|
+
# `rb_hash` -> `rb_funcall(:hash, ...)`. The classifier
|
|
39
|
+
# sees no direct dispatch because the recursion goes
|
|
40
|
+
# through `rb_hash` (a helper), but the answer depends
|
|
41
|
+
# on the member values' `#hash` — user-redefinable.
|
|
42
|
+
# Block to avoid folding a hash that would diverge
|
|
43
|
+
# from the runtime once a member overrides `#hash`.
|
|
44
|
+
:hash,
|
|
45
|
+
# `rb_struct_aref` reads a member by name or index; the
|
|
46
|
+
# answer depends on the subclass's member layout, which
|
|
47
|
+
# the catalog does not carry. Folding without knowing
|
|
48
|
+
# the layout would be unsound.
|
|
49
|
+
:[]
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -65,6 +65,7 @@ module Rigor
|
|
|
65
65
|
Prism::NilNode => :type_of_nil,
|
|
66
66
|
# Locals
|
|
67
67
|
Prism::LocalVariableReadNode => :local_read,
|
|
68
|
+
Prism::ItLocalVariableReadNode => :it_read,
|
|
68
69
|
Prism::LocalVariableWriteNode => :type_of_assignment_write,
|
|
69
70
|
# Containers and pass-throughs
|
|
70
71
|
Prism::ArrayNode => :array_type_for,
|
|
@@ -171,9 +172,11 @@ module Rigor
|
|
|
171
172
|
Prism::WhileNode => :type_of_loop,
|
|
172
173
|
Prism::UntilNode => :type_of_loop,
|
|
173
174
|
Prism::ForNode => :type_of_dynamic_top,
|
|
174
|
-
Prism::DefinedNode => :
|
|
175
|
-
Prism::
|
|
176
|
-
Prism::
|
|
175
|
+
Prism::DefinedNode => :type_of_defined,
|
|
176
|
+
Prism::NumberedReferenceReadNode => :type_of_string_or_nil,
|
|
177
|
+
Prism::BackReferenceReadNode => :type_of_string_or_nil,
|
|
178
|
+
Prism::MatchPredicateNode => :type_of_match_predicate,
|
|
179
|
+
Prism::MatchRequiredNode => :type_of_match_required,
|
|
177
180
|
Prism::MatchWriteNode => :type_of_dynamic_top,
|
|
178
181
|
# Literal containers
|
|
179
182
|
Prism::LambdaNode => :type_of_lambda,
|
|
@@ -298,6 +301,45 @@ module Rigor
|
|
|
298
301
|
dynamic_top
|
|
299
302
|
end
|
|
300
303
|
|
|
304
|
+
# `defined?(expr)` returns `String | nil` per Ruby semantics —
|
|
305
|
+
# a description of the expression's category (`"local-variable"`,
|
|
306
|
+
# `"method"`, ...) when defined, or `nil` when not. The argument
|
|
307
|
+
# is not evaluated (it is statically inspected by the runtime),
|
|
308
|
+
# so the typer does not recurse into it.
|
|
309
|
+
def type_of_defined(_node)
|
|
310
|
+
Type::Combinator.union(
|
|
311
|
+
Type::Combinator.nominal_of("String"),
|
|
312
|
+
Type::Combinator.constant_of(nil)
|
|
313
|
+
)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# `$1`, `$&`, `$'`, `$+`, `$\`` — the regex back-reference and
|
|
317
|
+
# numbered-capture globals each carry `String | nil`. They share
|
|
318
|
+
# the typer because the typing rule is identical regardless of
|
|
319
|
+
# which back-reference shape Prism emitted.
|
|
320
|
+
def type_of_string_or_nil(_node)
|
|
321
|
+
Type::Combinator.union(
|
|
322
|
+
Type::Combinator.nominal_of("String"),
|
|
323
|
+
Type::Combinator.constant_of(nil)
|
|
324
|
+
)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# `expr in pattern` — pattern-match predicate. Returns `true`
|
|
328
|
+
# when the pattern matches, `false` otherwise.
|
|
329
|
+
def type_of_match_predicate(_node)
|
|
330
|
+
Type::Combinator.union(
|
|
331
|
+
Type::Combinator.constant_of(true),
|
|
332
|
+
Type::Combinator.constant_of(false)
|
|
333
|
+
)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# `expr => pattern` — one-line pattern-match assertion. Raises
|
|
337
|
+
# `NoMatchingPatternError` on mismatch; on success the expression
|
|
338
|
+
# itself evaluates to `nil`.
|
|
339
|
+
def type_of_match_required(_node)
|
|
340
|
+
Type::Combinator.constant_of(nil)
|
|
341
|
+
end
|
|
342
|
+
|
|
301
343
|
# The expression `Foo` evaluates to the *class object* `Foo`, not
|
|
302
344
|
# an instance. From Slice 4 phase 2b on we therefore type a
|
|
303
345
|
# bare-constant reference as `Singleton[Foo]`; method dispatch on
|
|
@@ -472,10 +514,35 @@ module Rigor
|
|
|
472
514
|
[keys, values]
|
|
473
515
|
end
|
|
474
516
|
|
|
475
|
-
|
|
517
|
+
# An interpolated string `"#{a}b#{c}"` is `literal-string`
|
|
518
|
+
# when every part contributes literal-bearing material:
|
|
519
|
+
# plain text segments are literal by construction, embedded
|
|
520
|
+
# expressions count when their type is itself literal-string-
|
|
521
|
+
# compatible (a `Constant<String>`, the `literal-string`
|
|
522
|
+
# carrier, an `Intersection` containing it, or a `Union`
|
|
523
|
+
# whose members all qualify). Otherwise the result widens to
|
|
524
|
+
# plain `Nominal[String]` as before.
|
|
525
|
+
def type_of_interpolated_string(node)
|
|
526
|
+
return Type::Combinator.literal_string if interpolation_parts_literal?(node.parts)
|
|
527
|
+
|
|
476
528
|
Type::Combinator.nominal_of(String)
|
|
477
529
|
end
|
|
478
530
|
|
|
531
|
+
def interpolation_parts_literal?(parts)
|
|
532
|
+
parts.all? { |part| interpolation_part_literal?(part) }
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def interpolation_part_literal?(part)
|
|
536
|
+
case part
|
|
537
|
+
when Prism::StringNode
|
|
538
|
+
true
|
|
539
|
+
when Prism::EmbeddedStatementsNode, Prism::EmbeddedVariableNode
|
|
540
|
+
Type::Combinator.literal_string_compatible?(type_of(part))
|
|
541
|
+
else
|
|
542
|
+
false
|
|
543
|
+
end
|
|
544
|
+
end
|
|
545
|
+
|
|
479
546
|
def type_of_interpolated_symbol(_node)
|
|
480
547
|
Type::Combinator.nominal_of(Symbol)
|
|
481
548
|
end
|
|
@@ -698,9 +765,38 @@ module Rigor
|
|
|
698
765
|
def type_of_range(node)
|
|
699
766
|
left_static, left = static_range_endpoint(node.left)
|
|
700
767
|
right_static, right = static_range_endpoint(node.right)
|
|
701
|
-
return Type::Combinator.
|
|
768
|
+
return Type::Combinator.constant_of(Range.new(left, right, node.exclude_end?)) if left_static && right_static
|
|
769
|
+
|
|
770
|
+
nominal_range_for_endpoints(node.left, node.right)
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
# Derives `Nominal[Range, [T]]` from the endpoint expression
|
|
774
|
+
# types when at least one endpoint is statically typeable. The
|
|
775
|
+
# element parameter is the union of the endpoint types (lifted
|
|
776
|
+
# from `Constant<v>` to `Nominal<v.class>` so the carrier matches
|
|
777
|
+
# what `Range#each` would yield). Falls back to bare
|
|
778
|
+
# `Nominal[Range]` when no endpoint contributes a typable shape.
|
|
779
|
+
def nominal_range_for_endpoints(left_node, right_node)
|
|
780
|
+
endpoints = [left_node, right_node].compact.map { |n| range_endpoint_element_type(n) }
|
|
781
|
+
endpoints.reject! { |t| t.equal?(Type::Combinator.untyped) }
|
|
782
|
+
return Type::Combinator.nominal_of("Range") if endpoints.empty?
|
|
783
|
+
|
|
784
|
+
Type::Combinator.nominal_of("Range", type_args: [Type::Combinator.union(*endpoints)])
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
def range_endpoint_element_type(node)
|
|
788
|
+
type = type_of(node)
|
|
789
|
+
case type
|
|
790
|
+
when Type::Constant
|
|
791
|
+
value = type.value
|
|
792
|
+
return Type::Combinator.untyped if value.nil?
|
|
702
793
|
|
|
703
|
-
|
|
794
|
+
Type::Combinator.nominal_of(value.class.name)
|
|
795
|
+
when Type::IntegerRange
|
|
796
|
+
Type::Combinator.nominal_of("Integer")
|
|
797
|
+
else
|
|
798
|
+
type
|
|
799
|
+
end
|
|
704
800
|
end
|
|
705
801
|
|
|
706
802
|
# v0.0.7 — non-interpolated regex literals lift to
|
|
@@ -721,6 +817,7 @@ module Rigor
|
|
|
721
817
|
def static_range_endpoint(node)
|
|
722
818
|
return [true, nil] if node.nil?
|
|
723
819
|
return [true, node.value] if node.is_a?(Prism::IntegerNode)
|
|
820
|
+
return [true, node.unescaped] if node.is_a?(Prism::StringNode) && node.respond_to?(:unescaped)
|
|
724
821
|
|
|
725
822
|
[false, nil]
|
|
726
823
|
end
|
|
@@ -780,6 +877,13 @@ module Rigor
|
|
|
780
877
|
scope.local(node.name) || dynamic_top
|
|
781
878
|
end
|
|
782
879
|
|
|
880
|
+
# `it` (Ruby 3.4) — `ItLocalVariableReadNode` carries no `name`
|
|
881
|
+
# field; the implicit name is always `:it`, matching the binding
|
|
882
|
+
# `BlockParameterBinder` installs for `Prism::ItParametersNode`.
|
|
883
|
+
def it_read(_node)
|
|
884
|
+
scope.local(:it) || dynamic_top
|
|
885
|
+
end
|
|
886
|
+
|
|
783
887
|
# Slice 5 phase 1 upgrades array literals to `Tuple[T1..Tn]`
|
|
784
888
|
# when every element is a non-splat value. Splatted entries
|
|
785
889
|
# (`[*xs, 1]`) preserve the Slice 4 phase 2d behavior: we union
|
|
@@ -14,6 +14,12 @@ require_relative "../builtins/enumerable_catalog"
|
|
|
14
14
|
require_relative "../builtins/rational_catalog"
|
|
15
15
|
require_relative "../builtins/complex_catalog"
|
|
16
16
|
require_relative "../builtins/pathname_catalog"
|
|
17
|
+
require_relative "../builtins/random_catalog"
|
|
18
|
+
require_relative "../builtins/struct_catalog"
|
|
19
|
+
require_relative "../builtins/encoding_catalog"
|
|
20
|
+
require_relative "../builtins/re_catalog"
|
|
21
|
+
require_relative "../builtins/proc_catalog"
|
|
22
|
+
require_relative "../builtins/exception_catalog"
|
|
17
23
|
|
|
18
24
|
module Rigor
|
|
19
25
|
module Inference
|
|
@@ -1077,7 +1083,16 @@ module Rigor
|
|
|
1077
1083
|
[Date, [Builtins::DATE_CATALOG, "Date"]],
|
|
1078
1084
|
[Rational, [Builtins::RATIONAL_CATALOG, "Rational"]],
|
|
1079
1085
|
[Complex, [Builtins::COMPLEX_CATALOG, "Complex"]],
|
|
1080
|
-
[Pathname, [Builtins::PATHNAME_CATALOG, "Pathname"]]
|
|
1086
|
+
[Pathname, [Builtins::PATHNAME_CATALOG, "Pathname"]],
|
|
1087
|
+
[Random, [Builtins::RANDOM_CATALOG, "Random"]],
|
|
1088
|
+
[Struct, [Builtins::STRUCT_CATALOG, "Struct"]],
|
|
1089
|
+
[Encoding, [Builtins::ENCODING_CATALOG, "Encoding"]],
|
|
1090
|
+
[Regexp, [Builtins::REGEXP_CATALOG, "Regexp"]],
|
|
1091
|
+
[MatchData, [Builtins::REGEXP_CATALOG, "MatchData"]],
|
|
1092
|
+
[Proc, [Builtins::PROC_CATALOG, "Proc"]],
|
|
1093
|
+
[Method, [Builtins::PROC_CATALOG, "Method"]],
|
|
1094
|
+
[UnboundMethod, [Builtins::PROC_CATALOG, "UnboundMethod"]],
|
|
1095
|
+
[Exception, [Builtins::EXCEPTION_CATALOG, "Exception"]]
|
|
1081
1096
|
].freeze
|
|
1082
1097
|
private_constant :CATALOG_BY_CLASS
|
|
1083
1098
|
|