rigortype 0.0.3 → 0.0.5
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 +215 -117
- data/data/builtins/ruby_core/comparable.yml +87 -0
- data/data/builtins/ruby_core/complex.yml +505 -0
- data/data/builtins/ruby_core/date.yml +1737 -0
- data/data/builtins/ruby_core/enumerable.yml +557 -0
- data/data/builtins/ruby_core/file.yml +9 -0
- data/data/builtins/ruby_core/hash.yml +936 -0
- data/data/builtins/ruby_core/range.yml +389 -0
- data/data/builtins/ruby_core/rational.yml +365 -0
- data/data/builtins/ruby_core/set.yml +594 -0
- data/data/builtins/ruby_core/string.yml +9 -0
- data/data/builtins/ruby_core/time.yml +752 -0
- data/lib/rigor/analysis/check_rules.rb +11 -3
- data/lib/rigor/builtins/imported_refinements.rb +192 -10
- data/lib/rigor/cli.rb +1 -1
- data/lib/rigor/inference/acceptance.rb +181 -12
- data/lib/rigor/inference/builtins/comparable_catalog.rb +27 -0
- data/lib/rigor/inference/builtins/complex_catalog.rb +41 -0
- data/lib/rigor/inference/builtins/date_catalog.rb +98 -0
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +27 -0
- data/lib/rigor/inference/builtins/hash_catalog.rb +40 -0
- data/lib/rigor/inference/builtins/range_catalog.rb +46 -0
- data/lib/rigor/inference/builtins/rational_catalog.rb +38 -0
- data/lib/rigor/inference/builtins/set_catalog.rb +54 -0
- data/lib/rigor/inference/builtins/time_catalog.rb +64 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +145 -11
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +202 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +95 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +23 -7
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +135 -6
- data/lib/rigor/inference/method_dispatcher.rb +3 -1
- data/lib/rigor/inference/method_parameter_binder.rb +29 -4
- data/lib/rigor/inference/narrowing.rb +211 -0
- data/lib/rigor/inference/scope_indexer.rb +87 -11
- data/lib/rigor/inference/statement_evaluator.rb +6 -0
- data/lib/rigor/rbs_extended.rb +170 -14
- data/lib/rigor/type/combinator.rb +90 -0
- data/lib/rigor/type/integer_range.rb +4 -2
- data/lib/rigor/type/intersection.rb +135 -0
- data/lib/rigor/type/refined.rb +174 -0
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +4 -6
- data/sig/rigor/inference.rbs +2 -1
- data/sig/rigor/rbs_extended.rbs +11 -0
- data/sig/rigor/type.rbs +75 -35
- metadata +22 -1
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# `Date` and `DateTime` live in CRuby's bundled `date` gem, which
|
|
4
|
+
# is stdlib rather than core — so the constants are not visible
|
|
5
|
+
# until `date` is required. The dispatcher's `CATALOG_BY_CLASS`
|
|
6
|
+
# table references `Date` and `DateTime` at load time, so requiring
|
|
7
|
+
# the gem here (alongside the loader file that exports the catalog)
|
|
8
|
+
# keeps the wiring self-contained: a consumer that pulls in the
|
|
9
|
+
# constant-folding rule book gets the Date constants for free.
|
|
10
|
+
require "date"
|
|
11
|
+
|
|
12
|
+
require_relative "method_catalog"
|
|
13
|
+
|
|
14
|
+
module Rigor
|
|
15
|
+
module Inference
|
|
16
|
+
module Builtins
|
|
17
|
+
# `Date` / `DateTime` catalog. Singleton — load once, consult
|
|
18
|
+
# during dispatch.
|
|
19
|
+
#
|
|
20
|
+
# `Date` and `DateTime` both come from CRuby's bundled `date`
|
|
21
|
+
# gem (`references/ruby/ext/date/date_core.c`). A single
|
|
22
|
+
# `Init_date_core` function registers them, so the catalog
|
|
23
|
+
# carries both classes — `Date` plus the `DateTime` subclass
|
|
24
|
+
# whose own Init block extends with `hour` / `min` /
|
|
25
|
+
# `strftime` / `iso8601` etc. The Ruby-side prelude
|
|
26
|
+
# (`lib/date.rb`) only contributes `Date#infinite?` and the
|
|
27
|
+
# nested `Date::Infinity` class; the bulk of the surface is
|
|
28
|
+
# in C.
|
|
29
|
+
#
|
|
30
|
+
# Date / DateTime receivers are not lifted to a `Constant`
|
|
31
|
+
# carrier today (there is no Date literal node — the closest
|
|
32
|
+
# is `Date.today` / `Date.parse(...)`, which produce
|
|
33
|
+
# `Nominal[Date]`). The catalog wiring therefore mostly
|
|
34
|
+
# governs:
|
|
35
|
+
#
|
|
36
|
+
# 1. The Integer-typed reader surface (`#year`, `#month`,
|
|
37
|
+
# `#day`, `#wday`, `#hour`, `#min`, `#sec`) — RBS-declared
|
|
38
|
+
# `Integer` is preserved through dispatch.
|
|
39
|
+
# 2. The blocklist below, which keeps mutator-style methods
|
|
40
|
+
# that the C-body classifier already flagged
|
|
41
|
+
# (`mutates_self`) from being missed by a future
|
|
42
|
+
# `Constant<Date>` carrier, plus a defensive
|
|
43
|
+
# `:initialize_copy` entry for symmetry with the other
|
|
44
|
+
# catalogs.
|
|
45
|
+
#
|
|
46
|
+
# The non-bang `#next_day` / `#prev_day` / `#next_month` /
|
|
47
|
+
# `#prev_month` / `#next_year` / `#prev_year` / `#>>` / `#<<`
|
|
48
|
+
# selectors all RETURN brand-new `Date` objects rather than
|
|
49
|
+
# mutating the receiver — they intentionally stay
|
|
50
|
+
# catalog-eligible. The two real mutators
|
|
51
|
+
# (`#initialize_copy`, `#marshal_load`) are already classified
|
|
52
|
+
# `:mutates_self` by the C-body regex, so they fall out of
|
|
53
|
+
# `MethodCatalog#safe_for_folding?` without an explicit
|
|
54
|
+
# blocklist entry; the entries below are defense-in-depth
|
|
55
|
+
# against indirect mutators the regex might miss in a future
|
|
56
|
+
# CRuby bump.
|
|
57
|
+
DATE_CATALOG = MethodCatalog.new(
|
|
58
|
+
path: File.expand_path(
|
|
59
|
+
"../../../../data/builtins/ruby_core/date.yml",
|
|
60
|
+
__dir__
|
|
61
|
+
),
|
|
62
|
+
mutating_selectors: {
|
|
63
|
+
"Date" => Set[
|
|
64
|
+
# `d_lite_initialize_copy` is already classed
|
|
65
|
+
# `:mutates_self` by the regex (it calls
|
|
66
|
+
# `rb_check_frozen` and rewrites the receiver's
|
|
67
|
+
# internal `dat` slots). Listed here for symmetry with
|
|
68
|
+
# String / Array / Range / Set / Time and to keep the
|
|
69
|
+
# blocklist self-documenting.
|
|
70
|
+
:initialize_copy,
|
|
71
|
+
# `d_lite_fill` is a `#ifndef NDEBUG` debug method that
|
|
72
|
+
# warms the receiver's cached `simple` / `complex`
|
|
73
|
+
# fields via the `get_s_*` / `get_c_*` macros. The
|
|
74
|
+
# macros perform in-place writes on the receiver's
|
|
75
|
+
# internal `dat` struct but use no helper the C-body
|
|
76
|
+
# regex recognises, so the classifier mis-flags it
|
|
77
|
+
# `:leaf`. Blocked so a future `Constant<Date>` carrier
|
|
78
|
+
# never folds it.
|
|
79
|
+
:fill
|
|
80
|
+
],
|
|
81
|
+
"DateTime" => Set[
|
|
82
|
+
# `DateTime` inherits the bulk of its surface from
|
|
83
|
+
# `Date`. The dedicated DateTime-side methods are all
|
|
84
|
+
# readers (`hour`, `min`, …) plus formatting
|
|
85
|
+
# converters (`strftime`, `iso8601`, …); none mutate
|
|
86
|
+
# the receiver. The single defensive entry mirrors the
|
|
87
|
+
# Date side so that the inherited
|
|
88
|
+
# `Date#initialize_copy` (registered against
|
|
89
|
+
# `cDateTime` through subclassing) cannot fold through
|
|
90
|
+
# the catalog if a future `Constant<DateTime>` carrier
|
|
91
|
+
# ever lands.
|
|
92
|
+
:initialize_copy
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "method_catalog"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module Builtins
|
|
8
|
+
# `Enumerable` module catalog. Singleton — load once.
|
|
9
|
+
#
|
|
10
|
+
# `Enumerable` is a Ruby module, not a class, so the
|
|
11
|
+
# catalog is NOT routed through
|
|
12
|
+
# `MethodDispatcher::ConstantFolding::CATALOG_BY_CLASS`
|
|
13
|
+
# (which dispatches on the receiver's concrete class).
|
|
14
|
+
# The data is consumed by future include-aware lookup —
|
|
15
|
+
# see `docs/CURRENT_WORK.md` for the planned slice.
|
|
16
|
+
ENUMERABLE_CATALOG = MethodCatalog.new(
|
|
17
|
+
path: File.expand_path(
|
|
18
|
+
"../../../../data/builtins/ruby_core/enumerable.yml",
|
|
19
|
+
__dir__
|
|
20
|
+
),
|
|
21
|
+
mutating_selectors: {
|
|
22
|
+
"Enumerable" => Set[]
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "method_catalog"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module Builtins
|
|
8
|
+
# `Hash` catalog. Singleton — load once, consult during dispatch.
|
|
9
|
+
#
|
|
10
|
+
# Hash mirrors Array's mutation pattern: nearly every iteration
|
|
11
|
+
# method yields through `rb_hash_foreach` plus a per-pair static
|
|
12
|
+
# callback (`each_value_i`, `keep_if_i`, …), and the C-body
|
|
13
|
+
# classifier does not follow into the callback so it lands as
|
|
14
|
+
# `:leaf` despite being block-dependent. The blocklist below
|
|
15
|
+
# captures every false-positive `:leaf` we have spotted in the
|
|
16
|
+
# generated YAML — bias toward conservatism so a missed fold is
|
|
17
|
+
# acceptable but a folded mutator/yielder is not.
|
|
18
|
+
HASH_CATALOG = MethodCatalog.new(
|
|
19
|
+
path: File.expand_path(
|
|
20
|
+
"../../../../data/builtins/ruby_core/hash.yml",
|
|
21
|
+
__dir__
|
|
22
|
+
),
|
|
23
|
+
mutating_selectors: {
|
|
24
|
+
"Hash" => Set[
|
|
25
|
+
# Block-dependent iteration — yields via `rb_hash_foreach`
|
|
26
|
+
# plus a per-pair callback that the regex classifier does
|
|
27
|
+
# not follow:
|
|
28
|
+
:each, :each_pair, :each_key, :each_value,
|
|
29
|
+
:select, :filter, :reject,
|
|
30
|
+
:transform_values,
|
|
31
|
+
# Block-dependent merge — `rb_hash_merge` delegates into
|
|
32
|
+
# `rb_hash_update`, which yields per conflict when a block
|
|
33
|
+
# is given:
|
|
34
|
+
:merge
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "method_catalog"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module Builtins
|
|
8
|
+
# `Range` catalog. Singleton — load once, consult during
|
|
9
|
+
# dispatch.
|
|
10
|
+
#
|
|
11
|
+
# Range is largely immutable: `begin`, `end`, and `excl` are
|
|
12
|
+
# set at construction by `range_initialize` and never mutated
|
|
13
|
+
# afterwards. The blocklist below therefore stays small. The
|
|
14
|
+
# entries we DO need are the iteration methods whose C body
|
|
15
|
+
# routes through a helper the block/yield regex does not
|
|
16
|
+
# recognise, so the classifier mis-flags them as `:leaf`
|
|
17
|
+
# despite yielding to a block.
|
|
18
|
+
RANGE_CATALOG = MethodCatalog.new(
|
|
19
|
+
path: File.expand_path(
|
|
20
|
+
"../../../../data/builtins/ruby_core/range.yml",
|
|
21
|
+
__dir__
|
|
22
|
+
),
|
|
23
|
+
mutating_selectors: {
|
|
24
|
+
"Range" => Set[
|
|
25
|
+
# `range_initialize` / `range_initialize_copy` write
|
|
26
|
+
# `begin`/`end`/`excl` slots on the receiver; classed
|
|
27
|
+
# `:leaf` because the writes go through the struct
|
|
28
|
+
# accessor not `rb_check_frozen`. Blocked for symmetry
|
|
29
|
+
# with String / Array.
|
|
30
|
+
:initialize, :initialize_copy,
|
|
31
|
+
# `range_reverse_each` yields to its block via
|
|
32
|
+
# `range_each_func` -> caller's block; the regex
|
|
33
|
+
# classifier follows direct `rb_yield*` calls only.
|
|
34
|
+
:reverse_each,
|
|
35
|
+
# `range_percent_step` returns an Enumerator unless a
|
|
36
|
+
# block is supplied, in which case it yields. Treated
|
|
37
|
+
# as block-dependent so the fold tier never invokes it
|
|
38
|
+
# against a literal Range and tries to materialise an
|
|
39
|
+
# Enumerator into a Constant.
|
|
40
|
+
:%
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "method_catalog"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module Builtins
|
|
8
|
+
# `Rational` catalog. Singleton — load once, consult during
|
|
9
|
+
# dispatch.
|
|
10
|
+
#
|
|
11
|
+
# Rational is fully immutable: numerator / denominator slots
|
|
12
|
+
# are written once during `nurat_s_new_internal` and the C
|
|
13
|
+
# body never reaches for `rb_check_frozen`. Every catalog
|
|
14
|
+
# entry classifies cleanly (`:leaf`, `:leaf_when_numeric`,
|
|
15
|
+
# or `:dispatch` for the two methods that delegate into
|
|
16
|
+
# user-redefinable `==` / `Float()` — `nurat_eqeq_p` and
|
|
17
|
+
# `nurat_fdiv`). Bang-suffixed mutators do not exist on
|
|
18
|
+
# Rational.
|
|
19
|
+
#
|
|
20
|
+
# The blocklist therefore stays minimal. `initialize_copy`
|
|
21
|
+
# is added defensively (mirrors Range / Set) so a
|
|
22
|
+
# hypothetical future `Constant<Rational>` carrier cannot
|
|
23
|
+
# fold an aliasing copy through the catalog and surface a
|
|
24
|
+
# shared mutable handle.
|
|
25
|
+
RATIONAL_CATALOG = MethodCatalog.new(
|
|
26
|
+
path: File.expand_path(
|
|
27
|
+
"../../../../data/builtins/ruby_core/rational.yml",
|
|
28
|
+
__dir__
|
|
29
|
+
),
|
|
30
|
+
mutating_selectors: {
|
|
31
|
+
"Rational" => Set[
|
|
32
|
+
:initialize_copy
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "method_catalog"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module Builtins
|
|
8
|
+
# `Set` catalog. Singleton — load once, consult during dispatch.
|
|
9
|
+
#
|
|
10
|
+
# Set was rewritten in C and folded into CRuby for Ruby 3.2+;
|
|
11
|
+
# the reference branch (`ruby_4_0`) ships the implementation in
|
|
12
|
+
# `references/ruby/set.c` with `Init_Set` registering every
|
|
13
|
+
# method directly. There is no `set.rb` prelude — the trailing
|
|
14
|
+
# `rb_provide("set.rb")` makes `require "set"` a no-op against
|
|
15
|
+
# the built-in.
|
|
16
|
+
#
|
|
17
|
+
# The blocklist below catches the catalog `:leaf` entries the
|
|
18
|
+
# C-body classifier mis-attributes. Set's iteration helpers
|
|
19
|
+
# (`set_iter`, `RETURN_SIZED_ENUMERATOR`) and its identity-
|
|
20
|
+
# mode and reset paths drive into helpers the regex classifier
|
|
21
|
+
# does not yet recognise as block-yielding or mutating.
|
|
22
|
+
SET_CATALOG = MethodCatalog.new(
|
|
23
|
+
path: File.expand_path(
|
|
24
|
+
"../../../../data/builtins/ruby_core/set.yml",
|
|
25
|
+
__dir__
|
|
26
|
+
),
|
|
27
|
+
mutating_selectors: {
|
|
28
|
+
"Set" => Set[
|
|
29
|
+
# Indirect mutators classified `:leaf` because the C
|
|
30
|
+
# classifier did not follow the helper functions:
|
|
31
|
+
#
|
|
32
|
+
# - `initialize_copy` calls `set_copy` to overwrite the
|
|
33
|
+
# receiver's table.
|
|
34
|
+
# - `compare_by_identity` swaps the internal hash type
|
|
35
|
+
# via `set_reset_table_with_type`.
|
|
36
|
+
# - `reset` rebuilds the internal table to dedup after
|
|
37
|
+
# element mutation.
|
|
38
|
+
:initialize_copy, :compare_by_identity, :reset,
|
|
39
|
+
# Block-dependent methods classified `:leaf` because the
|
|
40
|
+
# C body uses `set_iter` / `RETURN_SIZED_ENUMERATOR`
|
|
41
|
+
# rather than calling `rb_yield` directly:
|
|
42
|
+
:each, :classify, :divide,
|
|
43
|
+
# `disjoint?` delegates into `set_i_intersect`, which
|
|
44
|
+
# for non-Set enumerables uses `rb_funcall(other,
|
|
45
|
+
# :any?, ...)` — that is user-redefinable dispatch the
|
|
46
|
+
# classifier missed because the call site is in a
|
|
47
|
+
# sibling function.
|
|
48
|
+
:disjoint?
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "method_catalog"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module Builtins
|
|
8
|
+
# `Time` catalog. Singleton — load once, consult during dispatch.
|
|
9
|
+
#
|
|
10
|
+
# Time is a pure-C built-in: the Init block in
|
|
11
|
+
# `references/ruby/time.c` registers the bulk of the surface,
|
|
12
|
+
# and the Ruby-side prelude `references/ruby/timev.rb`
|
|
13
|
+
# contributes the class-side constructors (`Time.now`,
|
|
14
|
+
# `Time.at`, `Time.new`) through Primitive cexpr stubs.
|
|
15
|
+
#
|
|
16
|
+
# Time receivers are not lifted to a `Constant` carrier today
|
|
17
|
+
# (there is no `Time` literal node — the closest is
|
|
18
|
+
# `Time.now` / `Time.new(...)`, which produce `Nominal[Time]`).
|
|
19
|
+
# The catalog wiring therefore mostly governs:
|
|
20
|
+
#
|
|
21
|
+
# 1. The size-projection-equivalent reader surface (`#year`,
|
|
22
|
+
# `#month`, `#hour`, `#sec`, `#wday`, …) — RBS-declared
|
|
23
|
+
# `Integer` is preserved through dispatch.
|
|
24
|
+
# 2. The blocklist below, which keeps the indirect-mutator
|
|
25
|
+
# methods that the C-body classifier mis-flagged as
|
|
26
|
+
# `:leaf` from ever folding through a hypothetical future
|
|
27
|
+
# `Constant<Time>` carrier.
|
|
28
|
+
#
|
|
29
|
+
# The blocklist captures the false-positive `:leaf` entries
|
|
30
|
+
# whose helper functions the regex classifier did not
|
|
31
|
+
# recognise as mutators.
|
|
32
|
+
TIME_CATALOG = MethodCatalog.new(
|
|
33
|
+
path: File.expand_path(
|
|
34
|
+
"../../../../data/builtins/ruby_core/time.yml",
|
|
35
|
+
__dir__
|
|
36
|
+
),
|
|
37
|
+
mutating_selectors: {
|
|
38
|
+
"Time" => Set[
|
|
39
|
+
# `time_init_copy` writes the `timew` and `vtm` slots on
|
|
40
|
+
# the receiver via `time_set_timew` / `time_set_vtm`.
|
|
41
|
+
# Classed `:leaf` because those setters are not in the
|
|
42
|
+
# mutator regex's helper list. Blocked for symmetry with
|
|
43
|
+
# String / Array / Range / Set initialize_copy entries.
|
|
44
|
+
:initialize_copy,
|
|
45
|
+
# `time_localtime_m` -> `time_localtime` calls
|
|
46
|
+
# `time_modify(time)` to mark the receiver mutable
|
|
47
|
+
# before rewriting its `vtm` cache and `tzmode`. The
|
|
48
|
+
# docstring is explicit ("converts time to local time
|
|
49
|
+
# in place"). The C-body classifier mis-flagged it as
|
|
50
|
+
# `:leaf` because `time_modify` is not in its mutator
|
|
51
|
+
# regex.
|
|
52
|
+
:localtime,
|
|
53
|
+
# `time_gmtime` (registered as both `gmtime` and `utc`
|
|
54
|
+
# against `rb_cTime`) follows the same in-place pattern
|
|
55
|
+
# as `time_localtime`: `time_modify(time)` then a
|
|
56
|
+
# `time_set_vtm` write and `TZMODE_SET_UTC`. Both
|
|
57
|
+
# selectors share the cfunc, so both must be blocked.
|
|
58
|
+
:gmtime, :utc
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -4,6 +4,15 @@ require_relative "../../type"
|
|
|
4
4
|
require_relative "../builtins/numeric_catalog"
|
|
5
5
|
require_relative "../builtins/string_catalog"
|
|
6
6
|
require_relative "../builtins/array_catalog"
|
|
7
|
+
require_relative "../builtins/hash_catalog"
|
|
8
|
+
require_relative "../builtins/range_catalog"
|
|
9
|
+
require_relative "../builtins/set_catalog"
|
|
10
|
+
require_relative "../builtins/time_catalog"
|
|
11
|
+
require_relative "../builtins/date_catalog"
|
|
12
|
+
require_relative "../builtins/comparable_catalog"
|
|
13
|
+
require_relative "../builtins/enumerable_catalog"
|
|
14
|
+
require_relative "../builtins/rational_catalog"
|
|
15
|
+
require_relative "../builtins/complex_catalog"
|
|
7
16
|
|
|
8
17
|
module Rigor
|
|
9
18
|
module Inference
|
|
@@ -105,9 +114,14 @@ module Rigor
|
|
|
105
114
|
arg_sets = args.map { |a| numeric_set_of(a) }
|
|
106
115
|
return nil if arg_sets.any?(&:nil?)
|
|
107
116
|
|
|
108
|
-
|
|
117
|
+
dispatch_by_arity(receiver_set, method_name, arg_sets)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def dispatch_by_arity(receiver_set, method_name, arg_sets)
|
|
121
|
+
case arg_sets.size
|
|
109
122
|
when 0 then try_fold_unary(receiver_set, method_name)
|
|
110
123
|
when 1 then try_fold_binary(receiver_set, method_name, arg_sets.first)
|
|
124
|
+
when 2 then try_fold_ternary(receiver_set, method_name, arg_sets)
|
|
111
125
|
end
|
|
112
126
|
end
|
|
113
127
|
|
|
@@ -216,6 +230,38 @@ module Rigor
|
|
|
216
230
|
build_constant_type(results, source: receiver_values + arg_values)
|
|
217
231
|
end
|
|
218
232
|
|
|
233
|
+
# 2-arg fold dispatch. Used by `Comparable#between?(min, max)`,
|
|
234
|
+
# `Comparable#clamp(min, max)`, and `Integer#pow(exp, mod)` —
|
|
235
|
+
# methods the catalog classifies `:leaf` but that the prior
|
|
236
|
+
# 0/1-arg switch could not reach. Range receivers/args are
|
|
237
|
+
# held back: a precise 2-arg range fold (e.g.
|
|
238
|
+
# `int<0,10>.between?(0, 10)` → `Constant[true]`) is a
|
|
239
|
+
# follow-up; for now any IntegerRange operand bails to the
|
|
240
|
+
# RBS tier.
|
|
241
|
+
def try_fold_ternary(receiver_set, method_name, arg_sets)
|
|
242
|
+
return nil if receiver_set.is_a?(Type::IntegerRange)
|
|
243
|
+
return nil if arg_sets.any?(Type::IntegerRange)
|
|
244
|
+
|
|
245
|
+
try_fold_ternary_set(receiver_set, method_name, arg_sets)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def try_fold_ternary_set(receiver_values, method_name, arg_sets)
|
|
249
|
+
total = receiver_values.size * arg_sets[0].size * arg_sets[1].size
|
|
250
|
+
return nil if total > UNION_FOLD_INPUT_LIMIT
|
|
251
|
+
return nil unless receiver_values.all? { |rv| ternary_method_allowed?(rv, method_name) }
|
|
252
|
+
|
|
253
|
+
results = ternary_cartesian(receiver_values, method_name, arg_sets)
|
|
254
|
+
build_constant_type(results, source: receiver_values + arg_sets.flatten)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def ternary_cartesian(receiver_values, method_name, arg_sets)
|
|
258
|
+
receiver_values.flat_map do |rv|
|
|
259
|
+
arg_sets[0].flat_map do |av0|
|
|
260
|
+
arg_sets[1].flat_map { |av1| invoke_ternary(rv, method_name, av0, av1) || [] }
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
219
265
|
def unary_method_allowed?(receiver_value, method_name)
|
|
220
266
|
unary_ops_for(receiver_value).include?(method_name) ||
|
|
221
267
|
catalog_allows?(receiver_value, method_name)
|
|
@@ -226,6 +272,13 @@ module Rigor
|
|
|
226
272
|
catalog_allows?(receiver_value, method_name)
|
|
227
273
|
end
|
|
228
274
|
|
|
275
|
+
# 2-arg methods have no hand-rolled allow list; the catalog
|
|
276
|
+
# is the sole gate. Adding a per-class arity-2 set is reserved
|
|
277
|
+
# for future cases that need it.
|
|
278
|
+
def ternary_method_allowed?(receiver_value, method_name)
|
|
279
|
+
catalog_allows?(receiver_value, method_name)
|
|
280
|
+
end
|
|
281
|
+
|
|
229
282
|
# Builds a Constant or Union[Constant…] from a flat list of
|
|
230
283
|
# Ruby values. When the deduped set exceeds
|
|
231
284
|
# `UNION_FOLD_OUTPUT_LIMIT` and every result is an Integer,
|
|
@@ -594,6 +647,19 @@ module Rigor
|
|
|
594
647
|
nil
|
|
595
648
|
end
|
|
596
649
|
|
|
650
|
+
# Returns `[value]` on success, `nil` to signal "skip this triple".
|
|
651
|
+
# Mirrors `invoke_binary` but for the 2-argument shape; the wrap
|
|
652
|
+
# convention lets callers `flat_map` without losing
|
|
653
|
+
# legitimate `false`/`nil` folds.
|
|
654
|
+
def invoke_ternary(receiver_value, method_name, av0, av1)
|
|
655
|
+
return nil unless ternary_method_allowed?(receiver_value, method_name)
|
|
656
|
+
|
|
657
|
+
result = receiver_value.public_send(method_name, av0, av1)
|
|
658
|
+
foldable_constant_value?(result) ? [result] : nil
|
|
659
|
+
rescue StandardError
|
|
660
|
+
nil
|
|
661
|
+
end
|
|
662
|
+
|
|
597
663
|
# Returns `[value]` on success, `nil` to signal "skip". See
|
|
598
664
|
# `invoke_binary` for why we wrap.
|
|
599
665
|
def invoke_unary(receiver_value, method_name)
|
|
@@ -619,24 +685,92 @@ module Rigor
|
|
|
619
685
|
# implementation does not call back into user-redefinable
|
|
620
686
|
# Ruby methods, so executing them on a literal Integer/Float
|
|
621
687
|
# is safe regardless of monkey-patching.
|
|
688
|
+
#
|
|
689
|
+
# Resolution order:
|
|
690
|
+
#
|
|
691
|
+
# 1. Primary class catalog (e.g. NumericCatalog for an
|
|
692
|
+
# Integer receiver). When the catalog has an entry —
|
|
693
|
+
# even one classified `:dispatch` — that answer wins.
|
|
694
|
+
# The class's direct `rb_define_method` registration is
|
|
695
|
+
# authoritative; we MUST NOT fall through to a module
|
|
696
|
+
# catalog and risk over-folding.
|
|
697
|
+
# 2. Module catalogs (Comparable, Enumerable, …) that the
|
|
698
|
+
# receiver's class includes by ancestry. Reached only
|
|
699
|
+
# when the primary catalog has NO entry for the method
|
|
700
|
+
# — typically because the method is inherited purely
|
|
701
|
+
# through `include Comparable` / `include Enumerable`
|
|
702
|
+
# (e.g. `Integer#between?` / `Integer#clamp` are not in
|
|
703
|
+
# numeric.yml because the Init block does not
|
|
704
|
+
# `rb_define_method` them on Integer).
|
|
622
705
|
def catalog_allows?(receiver_value, method_name)
|
|
623
706
|
catalog, class_name = catalog_for(receiver_value)
|
|
624
|
-
return
|
|
707
|
+
return catalog.safe_for_folding?(class_name, method_name) if catalog&.method_entry(class_name, method_name)
|
|
708
|
+
|
|
709
|
+
module_catalogs_for(receiver_value).any? do |mod_catalog, mod_name|
|
|
710
|
+
mod_catalog.method_entry(mod_name, method_name) &&
|
|
711
|
+
mod_catalog.safe_for_folding?(mod_name, method_name)
|
|
712
|
+
end
|
|
713
|
+
end
|
|
625
714
|
|
|
626
|
-
|
|
715
|
+
# `(Module, catalog, class_name)` triples consulted as a
|
|
716
|
+
# fallthrough when the primary class catalog has no entry.
|
|
717
|
+
# Each triple's Module is matched against the receiver
|
|
718
|
+
# class's ancestor chain at lookup time; the catalog
|
|
719
|
+
# corresponds to the module-mode YAML at
|
|
720
|
+
# `data/builtins/ruby_core/<topic>.yml`.
|
|
721
|
+
MODULE_CATALOGS = [
|
|
722
|
+
[Comparable, Builtins::COMPARABLE_CATALOG, "Comparable"],
|
|
723
|
+
[Enumerable, Builtins::ENUMERABLE_CATALOG, "Enumerable"]
|
|
724
|
+
].freeze
|
|
725
|
+
private_constant :MODULE_CATALOGS
|
|
726
|
+
|
|
727
|
+
# Returns the `(catalog, class_name)` pairs for every
|
|
728
|
+
# registered module that is in the receiver's ancestor
|
|
729
|
+
# chain. The receiver's class's `Module#ancestors` is
|
|
730
|
+
# cached by Ruby; the `Set` membership check is cheap.
|
|
731
|
+
def module_catalogs_for(receiver_value)
|
|
732
|
+
ancestors = Set.new(receiver_value.class.ancestors)
|
|
733
|
+
MODULE_CATALOGS.filter_map do |mod, catalog, class_name|
|
|
734
|
+
[catalog, class_name] if ancestors.include?(mod)
|
|
735
|
+
end
|
|
627
736
|
end
|
|
628
737
|
|
|
738
|
+
# `(catalog, class_name)` per receiver class. The class_name
|
|
739
|
+
# is what each catalog's RBS-rooted entries are keyed by.
|
|
740
|
+
# `catalog_for` walks this table in declaration order so
|
|
741
|
+
# subclasses (Symbol < String) hit their dedicated entry
|
|
742
|
+
# before any base-class fallback would, and adding a new
|
|
743
|
+
# class is a one-line addition rather than another `when`
|
|
744
|
+
# arm on a growing case statement.
|
|
745
|
+
# Subclass-before-superclass ordering: `DateTime < Date`,
|
|
746
|
+
# so the `DateTime` row MUST come before the `Date` row.
|
|
747
|
+
# Otherwise a `DateTime` receiver would match the `Date`
|
|
748
|
+
# arm first and the catalog would consult the Date entry
|
|
749
|
+
# in `DATE_CATALOG` for the wrong class.
|
|
750
|
+
CATALOG_BY_CLASS = [
|
|
751
|
+
[Integer, [Builtins::NumericCatalog, "Integer"]],
|
|
752
|
+
[Float, [Builtins::NumericCatalog, "Float"]],
|
|
753
|
+
[String, [Builtins::STRING_CATALOG, "String"]],
|
|
754
|
+
[Symbol, [Builtins::STRING_CATALOG, "Symbol"]],
|
|
755
|
+
[Array, [Builtins::ARRAY_CATALOG, "Array"]],
|
|
756
|
+
[Hash, [Builtins::HASH_CATALOG, "Hash"]],
|
|
757
|
+
[Range, [Builtins::RANGE_CATALOG, "Range"]],
|
|
758
|
+
[::Set, [Builtins::SET_CATALOG, "Set"]],
|
|
759
|
+
[Time, [Builtins::TIME_CATALOG, "Time"]],
|
|
760
|
+
[DateTime, [Builtins::DATE_CATALOG, "DateTime"]],
|
|
761
|
+
[Date, [Builtins::DATE_CATALOG, "Date"]],
|
|
762
|
+
[Rational, [Builtins::RATIONAL_CATALOG, "Rational"]],
|
|
763
|
+
[Complex, [Builtins::COMPLEX_CATALOG, "Complex"]]
|
|
764
|
+
].freeze
|
|
765
|
+
private_constant :CATALOG_BY_CLASS
|
|
766
|
+
|
|
629
767
|
# Returns `[catalog, class_name]` for receivers we have a
|
|
630
|
-
# catalog for; nil otherwise.
|
|
631
|
-
# catalog's RBS-rooted entries are keyed by.
|
|
768
|
+
# catalog for; nil otherwise.
|
|
632
769
|
def catalog_for(receiver_value)
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
when Float then [Builtins::NumericCatalog, "Float"]
|
|
636
|
-
when String then [Builtins::STRING_CATALOG, "String"]
|
|
637
|
-
when Symbol then [Builtins::STRING_CATALOG, "Symbol"]
|
|
638
|
-
when Array then [Builtins::ARRAY_CATALOG, "Array"]
|
|
770
|
+
CATALOG_BY_CLASS.each do |klass, entry|
|
|
771
|
+
return entry if receiver_value.is_a?(klass)
|
|
639
772
|
end
|
|
773
|
+
nil
|
|
640
774
|
end
|
|
641
775
|
|
|
642
776
|
def unary_ops_for(receiver_value)
|