rigortype 0.1.8 → 0.1.9
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 +186 -513
- data/lib/rigor/analysis/check_rules.rb +20 -0
- data/lib/rigor/cli/annotate_command.rb +224 -0
- data/lib/rigor/cli/baseline_command.rb +36 -16
- data/lib/rigor/cli/prism_colorizer.rb +111 -0
- data/lib/rigor/cli.rb +62 -4
- data/lib/rigor/environment.rb +9 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +17 -1
- data/lib/rigor/inference/builtins/time_catalog.rb +10 -1
- data/lib/rigor/inference/expression_typer.rb +165 -6
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +109 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +173 -10
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +53 -1
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +149 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +20 -1
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +316 -2
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +126 -0
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +56 -0
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +148 -1
- data/lib/rigor/inference/method_parameter_binder.rb +67 -10
- data/lib/rigor/inference/narrowing.rb +29 -10
- data/lib/rigor/inference/statement_evaluator.rb +3 -1
- data/lib/rigor/plugin/base.rb +39 -0
- data/lib/rigor/plugin/loader.rb +22 -1
- data/lib/rigor/plugin/manifest.rb +73 -10
- data/lib/rigor/plugin/protocol_contract.rb +185 -0
- data/lib/rigor/plugin/registry.rb +66 -0
- data/lib/rigor/triage/catalogue.rb +2 -2
- data/lib/rigor/type/constant.rb +29 -2
- data/lib/rigor/version.rb +1 -1
- metadata +11 -1
|
@@ -1056,6 +1056,9 @@ module Rigor
|
|
|
1056
1056
|
per_element = try_per_element_block_fold(node, receiver)
|
|
1057
1057
|
return per_element if per_element
|
|
1058
1058
|
|
|
1059
|
+
hash_transform = try_hash_shape_block_fold(node, receiver)
|
|
1060
|
+
return hash_transform if hash_transform
|
|
1061
|
+
|
|
1059
1062
|
result = MethodDispatcher.dispatch(
|
|
1060
1063
|
receiver_type: receiver,
|
|
1061
1064
|
method_name: node.name,
|
|
@@ -1455,10 +1458,17 @@ module Rigor
|
|
|
1455
1458
|
# the dispatch chain untouched.
|
|
1456
1459
|
PER_ELEMENT_TUPLE_METHODS = Set[
|
|
1457
1460
|
:map, :collect, :filter_map, :flat_map,
|
|
1461
|
+
:select, :filter, :reject,
|
|
1458
1462
|
:find, :detect, :find_index, :index
|
|
1459
1463
|
].freeze
|
|
1460
1464
|
private_constant :PER_ELEMENT_TUPLE_METHODS
|
|
1461
1465
|
|
|
1466
|
+
HASH_SHAPE_TRANSFORM_METHODS = Set[
|
|
1467
|
+
:transform_keys, :transform_keys!,
|
|
1468
|
+
:transform_values, :transform_values!
|
|
1469
|
+
].freeze
|
|
1470
|
+
private_constant :HASH_SHAPE_TRANSFORM_METHODS
|
|
1471
|
+
|
|
1462
1472
|
# Cardinality cap for per-element block fold over
|
|
1463
1473
|
# finite-bound `Constant<Range>` receivers. Walking
|
|
1464
1474
|
# `(1..1_000_000).map { … }` element-wise would balloon
|
|
@@ -1475,15 +1485,50 @@ module Rigor
|
|
|
1475
1485
|
element_types = per_element_elements_of(receiver_type)
|
|
1476
1486
|
return nil if element_types.nil? || element_types.empty?
|
|
1477
1487
|
|
|
1478
|
-
|
|
1479
|
-
return nil
|
|
1488
|
+
per_position = per_element_block_results(call_node.block, element_types)
|
|
1489
|
+
return nil if per_position.nil? || per_position.any?(&:nil?)
|
|
1480
1490
|
|
|
1481
|
-
per_position
|
|
1482
|
-
|
|
1491
|
+
assemble_per_element_result(call_node.name, per_position, element_types)
|
|
1492
|
+
end
|
|
1493
|
+
|
|
1494
|
+
# Evaluates the call's block once per receiver element.
|
|
1495
|
+
# Two block shapes are supported:
|
|
1496
|
+
#
|
|
1497
|
+
# - `Prism::BlockNode` — a full `do … end` / `{ … }` block;
|
|
1498
|
+
# the body is re-typed per position with the element
|
|
1499
|
+
# bound to the block parameter.
|
|
1500
|
+
# - `Prism::BlockArgumentNode` wrapping a `SymbolNode` —
|
|
1501
|
+
# the `&:predicate` shorthand; the symbol is dispatched
|
|
1502
|
+
# as a zero-arg method on each element type.
|
|
1503
|
+
#
|
|
1504
|
+
# Any other shape (`&proc_local`, `&method(:foo)`, no
|
|
1505
|
+
# block) returns `nil` so the fold declines.
|
|
1506
|
+
def per_element_block_results(block, element_types)
|
|
1507
|
+
case block
|
|
1508
|
+
when Prism::BlockNode
|
|
1509
|
+
element_types.map { |element_type| type_block_body_with_param(block, [element_type]) }
|
|
1510
|
+
when Prism::BlockArgumentNode
|
|
1511
|
+
per_element_symbol_results(block, element_types)
|
|
1483
1512
|
end
|
|
1484
|
-
|
|
1513
|
+
end
|
|
1485
1514
|
|
|
1486
|
-
|
|
1515
|
+
def per_element_symbol_results(block_arg, element_types)
|
|
1516
|
+
expression = block_arg.expression
|
|
1517
|
+
return nil unless expression.is_a?(Prism::SymbolNode)
|
|
1518
|
+
|
|
1519
|
+
method_name = expression.unescaped.to_sym
|
|
1520
|
+
element_types.map do |element_type|
|
|
1521
|
+
MethodDispatcher.dispatch(
|
|
1522
|
+
receiver_type: element_type,
|
|
1523
|
+
method_name: method_name,
|
|
1524
|
+
arg_types: [],
|
|
1525
|
+
block_type: nil,
|
|
1526
|
+
environment: scope.environment,
|
|
1527
|
+
scope: scope
|
|
1528
|
+
)
|
|
1529
|
+
end
|
|
1530
|
+
rescue StandardError
|
|
1531
|
+
nil
|
|
1487
1532
|
end
|
|
1488
1533
|
|
|
1489
1534
|
# Returns the per-position element types for a finite,
|
|
@@ -1532,11 +1577,37 @@ module Rigor
|
|
|
1532
1577
|
when :map, :collect then Type::Combinator.tuple_of(*per_position)
|
|
1533
1578
|
when :filter_map then assemble_filter_map_result(per_position)
|
|
1534
1579
|
when :flat_map then assemble_flat_map_result(per_position)
|
|
1580
|
+
when :select, :filter
|
|
1581
|
+
assemble_filter_result(per_position, element_types, keep_on_truthy: true)
|
|
1582
|
+
when :reject
|
|
1583
|
+
assemble_filter_result(per_position, element_types, keep_on_truthy: false)
|
|
1535
1584
|
when :find, :detect then assemble_find_result(per_position, element_types)
|
|
1536
1585
|
when :find_index, :index then assemble_find_index_result(per_position)
|
|
1537
1586
|
end
|
|
1538
1587
|
end
|
|
1539
1588
|
|
|
1589
|
+
# `select` / `filter` / `reject`: keeps each receiver
|
|
1590
|
+
# element whose per-position predicate result folds to a
|
|
1591
|
+
# decisive `Constant` — Ruby-truthy for `select` / `filter`,
|
|
1592
|
+
# Ruby-falsey for `reject`. The surviving elements assemble
|
|
1593
|
+
# into a `Tuple`, strictly tighter than the RBS-projected
|
|
1594
|
+
# `Array[Elem]`.
|
|
1595
|
+
#
|
|
1596
|
+
# Folds tightly only when EVERY position is a `Constant`:
|
|
1597
|
+
# a single non-`Constant` position leaves the result
|
|
1598
|
+
# cardinality unknown (the element might or might not
|
|
1599
|
+
# survive), so the dispatcher declines and the RBS tier
|
|
1600
|
+
# widens to `Array[Elem]`. `[].select` style empty results
|
|
1601
|
+
# are sound — an empty `Tuple` is the empty-array carrier.
|
|
1602
|
+
def assemble_filter_result(per_position, element_types, keep_on_truthy:)
|
|
1603
|
+
return nil unless per_position.all?(Type::Constant)
|
|
1604
|
+
|
|
1605
|
+
kept = element_types.each_index.filter_map do |index|
|
|
1606
|
+
element_types[index] if truthy_constant?(per_position[index]) == keep_on_truthy
|
|
1607
|
+
end
|
|
1608
|
+
Type::Combinator.tuple_of(*kept)
|
|
1609
|
+
end
|
|
1610
|
+
|
|
1540
1611
|
# `filter_map` folds tightly only when every per-position
|
|
1541
1612
|
# result is a `Constant`: positions whose value is `nil`
|
|
1542
1613
|
# or `false` drop, the rest survive in declaration order.
|
|
@@ -1611,6 +1682,94 @@ module Rigor
|
|
|
1611
1682
|
type.is_a?(Type::Constant) && type.value && type.value != false
|
|
1612
1683
|
end
|
|
1613
1684
|
|
|
1685
|
+
# Per-pair block fold for `HashShape#transform_keys` and
|
|
1686
|
+
# `HashShape#transform_values` (and their bang variants).
|
|
1687
|
+
#
|
|
1688
|
+
# When the receiver is a closed `HashShape` with no optional
|
|
1689
|
+
# keys, applies the call's block (a `Prism::BlockNode` or
|
|
1690
|
+
# `Prism::BlockArgumentNode`) to each key/value pair
|
|
1691
|
+
# independently and assembles a new `HashShape`:
|
|
1692
|
+
#
|
|
1693
|
+
# - `transform_values` / `transform_values!`: re-types
|
|
1694
|
+
# each VALUE by binding it to the block parameter; keys
|
|
1695
|
+
# are preserved unchanged.
|
|
1696
|
+
# - `transform_keys` / `transform_keys!`: re-types each
|
|
1697
|
+
# KEY by wrapping it in `Constant[k]` and passing it to
|
|
1698
|
+
# the block; values are preserved unchanged. The result
|
|
1699
|
+
# key must be a `Constant[Symbol | String]` — otherwise
|
|
1700
|
+
# the tier declines (the new key cannot be used as a
|
|
1701
|
+
# static HashShape index). Collisions (two old keys
|
|
1702
|
+
# mapping to the same new key) also decline.
|
|
1703
|
+
#
|
|
1704
|
+
# Returns `nil` on any decline so the dispatcher falls
|
|
1705
|
+
# through to `RbsDispatch` and gets the widened `Hash[K, V]`
|
|
1706
|
+
# answer.
|
|
1707
|
+
def try_hash_shape_block_fold(call_node, receiver_type)
|
|
1708
|
+
return nil unless HASH_SHAPE_TRANSFORM_METHODS.include?(call_node.name)
|
|
1709
|
+
return nil unless receiver_type.is_a?(Type::HashShape)
|
|
1710
|
+
return nil unless receiver_type.closed?
|
|
1711
|
+
return nil unless receiver_type.optional_keys.empty?
|
|
1712
|
+
|
|
1713
|
+
block_arg = call_node.block
|
|
1714
|
+
return nil if block_arg.nil?
|
|
1715
|
+
|
|
1716
|
+
if %i[transform_values transform_values!].include?(call_node.name)
|
|
1717
|
+
fold_hash_shape_transform_values(receiver_type, block_arg)
|
|
1718
|
+
else
|
|
1719
|
+
fold_hash_shape_transform_keys(receiver_type, block_arg)
|
|
1720
|
+
end
|
|
1721
|
+
end
|
|
1722
|
+
|
|
1723
|
+
def fold_hash_shape_transform_values(shape, block_arg)
|
|
1724
|
+
new_pairs = {}
|
|
1725
|
+
shape.pairs.each do |key, value|
|
|
1726
|
+
new_value = apply_hash_block(block_arg, value)
|
|
1727
|
+
return nil if new_value.nil?
|
|
1728
|
+
|
|
1729
|
+
new_pairs[key] = new_value
|
|
1730
|
+
end
|
|
1731
|
+
Type::Combinator.hash_shape_of(new_pairs)
|
|
1732
|
+
end
|
|
1733
|
+
|
|
1734
|
+
def fold_hash_shape_transform_keys(shape, block_arg)
|
|
1735
|
+
new_pairs = {}
|
|
1736
|
+
shape.pairs.each do |key, value|
|
|
1737
|
+
key_type = Type::Combinator.constant_of(key)
|
|
1738
|
+
new_key_type = apply_hash_block(block_arg, key_type)
|
|
1739
|
+
return nil unless new_key_type.is_a?(Type::Constant)
|
|
1740
|
+
|
|
1741
|
+
new_key = new_key_type.value
|
|
1742
|
+
return nil unless new_key.is_a?(Symbol) || new_key.is_a?(String)
|
|
1743
|
+
return nil if new_pairs.key?(new_key)
|
|
1744
|
+
|
|
1745
|
+
new_pairs[new_key] = value
|
|
1746
|
+
end
|
|
1747
|
+
Type::Combinator.hash_shape_of(new_pairs)
|
|
1748
|
+
end
|
|
1749
|
+
|
|
1750
|
+
# Applies a single-argument block (either a full BlockNode
|
|
1751
|
+
# or a `&:symbol` BlockArgumentNode) to `param_type` and
|
|
1752
|
+
# returns the resulting type, or `nil` on failure.
|
|
1753
|
+
def apply_hash_block(block_arg, param_type)
|
|
1754
|
+
case block_arg
|
|
1755
|
+
when Prism::BlockNode
|
|
1756
|
+
type_block_body_with_param(block_arg, [param_type])
|
|
1757
|
+
when Prism::BlockArgumentNode
|
|
1758
|
+
expression = block_arg.expression
|
|
1759
|
+
return nil unless expression.is_a?(Prism::SymbolNode)
|
|
1760
|
+
|
|
1761
|
+
MethodDispatcher.dispatch(
|
|
1762
|
+
receiver_type: param_type,
|
|
1763
|
+
method_name: expression.unescaped.to_sym,
|
|
1764
|
+
arg_types: [],
|
|
1765
|
+
block_type: nil,
|
|
1766
|
+
environment: scope.environment,
|
|
1767
|
+
call_node: block_arg,
|
|
1768
|
+
scope: scope
|
|
1769
|
+
)
|
|
1770
|
+
end
|
|
1771
|
+
end
|
|
1772
|
+
|
|
1614
1773
|
def type_block_body_with_param(block_node, expected_param_types)
|
|
1615
1774
|
bindings = BlockParameterBinder.new(expected_param_types: expected_param_types).bind(block_node)
|
|
1616
1775
|
block_scope = bindings.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi/util"
|
|
4
|
+
require_relative "../../type"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Inference
|
|
8
|
+
module MethodDispatcher
|
|
9
|
+
# Folds `CGI` module-function calls on statically known
|
|
10
|
+
# string constants.
|
|
11
|
+
#
|
|
12
|
+
# `CGI.escapeHTML` / `CGI.unescapeHTML` and related methods
|
|
13
|
+
# are pure, deterministic functions over their string inputs.
|
|
14
|
+
# When the argument is a `Constant[String]`, the analyzer can
|
|
15
|
+
# evaluate the call at inference time and return the concrete
|
|
16
|
+
# `Constant[String]` result.
|
|
17
|
+
#
|
|
18
|
+
# === Supported methods
|
|
19
|
+
#
|
|
20
|
+
# * `escapeHTML(str)` / `escape_html(str)` / `h(str)` —
|
|
21
|
+
# HTML-escape. Returns `Constant[String]`.
|
|
22
|
+
# * `unescapeHTML(str)` / `unescape_html(str)` —
|
|
23
|
+
# HTML-unescape. Returns `Constant[String]`.
|
|
24
|
+
# * `escape(str)` / `unescape(str)` —
|
|
25
|
+
# URL-encode / decode (`application/x-www-form-urlencoded`).
|
|
26
|
+
# Returns `Constant[String]`.
|
|
27
|
+
# * `escapeURIComponent(str)` / `escape_uri_component(str)`,
|
|
28
|
+
# `unescapeURIComponent(str)` / `unescape_uri_component(str)` —
|
|
29
|
+
# URI-component percent-encode / decode. Returns `Constant[String]`.
|
|
30
|
+
# * `escapeElement(str, *elements)` / `escape_element(str, *elements)`,
|
|
31
|
+
# `unescapeElement(str, *elements)` / `unescape_element(str, *elements)` —
|
|
32
|
+
# element-level escape / unescape (first arg is the string,
|
|
33
|
+
# remaining args are element names). Returns `Constant[String]`.
|
|
34
|
+
#
|
|
35
|
+
# === Non-constant / unsupported cases
|
|
36
|
+
#
|
|
37
|
+
# Returns `nil` (deferring to the next dispatcher tier) when:
|
|
38
|
+
# - the receiver is not `Singleton[CGI]`,
|
|
39
|
+
# - the first argument is not a `Constant[String]`,
|
|
40
|
+
# - the method is not in the supported set.
|
|
41
|
+
module CGIFolding
|
|
42
|
+
CGI_HTML_ESCAPE_METHODS = Set[:escapeHTML, :escape_html, :h].freeze
|
|
43
|
+
CGI_HTML_UNESCAPE_METHODS = Set[:unescapeHTML, :unescape_html].freeze
|
|
44
|
+
CGI_URL_ESCAPE_METHODS = Set[:escape, :unescape].freeze
|
|
45
|
+
CGI_URI_ESCAPE_METHODS = Set[
|
|
46
|
+
:escapeURIComponent, :escape_uri_component,
|
|
47
|
+
:unescapeURIComponent, :unescape_uri_component
|
|
48
|
+
].freeze
|
|
49
|
+
CGI_ELEMENT_ESCAPE_METHODS = Set[
|
|
50
|
+
:escapeElement, :escape_element,
|
|
51
|
+
:unescapeElement, :unescape_element
|
|
52
|
+
].freeze
|
|
53
|
+
CGI_ALL_ESCAPE_METHODS = (
|
|
54
|
+
CGI_HTML_ESCAPE_METHODS | CGI_HTML_UNESCAPE_METHODS |
|
|
55
|
+
CGI_URL_ESCAPE_METHODS | CGI_URI_ESCAPE_METHODS |
|
|
56
|
+
CGI_ELEMENT_ESCAPE_METHODS
|
|
57
|
+
).freeze
|
|
58
|
+
|
|
59
|
+
private_constant :CGI_HTML_ESCAPE_METHODS, :CGI_HTML_UNESCAPE_METHODS,
|
|
60
|
+
:CGI_URL_ESCAPE_METHODS, :CGI_URI_ESCAPE_METHODS,
|
|
61
|
+
:CGI_ELEMENT_ESCAPE_METHODS, :CGI_ALL_ESCAPE_METHODS
|
|
62
|
+
|
|
63
|
+
module_function
|
|
64
|
+
|
|
65
|
+
# @return [Rigor::Type, nil] folded result, or nil to defer.
|
|
66
|
+
def try_dispatch(receiver:, method_name:, args:)
|
|
67
|
+
return nil unless dispatch_target?(receiver)
|
|
68
|
+
return nil unless CGI_ALL_ESCAPE_METHODS.include?(method_name)
|
|
69
|
+
|
|
70
|
+
fold_cgi_call(method_name, args)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def dispatch_target?(receiver)
|
|
74
|
+
receiver.is_a?(Type::Singleton) && receiver.class_name == "CGI"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def fold_cgi_call(method_name, args)
|
|
78
|
+
return nil if args.empty?
|
|
79
|
+
return nil unless args.first.is_a?(Type::Constant) && args.first.value.is_a?(String)
|
|
80
|
+
|
|
81
|
+
str = args.first.value
|
|
82
|
+
|
|
83
|
+
if CGI_ELEMENT_ESCAPE_METHODS.include?(method_name)
|
|
84
|
+
fold_cgi_element(method_name, str, args.drop(1))
|
|
85
|
+
else
|
|
86
|
+
Type::Combinator.constant_of(CGI.public_send(method_name, str))
|
|
87
|
+
end
|
|
88
|
+
rescue StandardError
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# `CGI.escapeElement(str, "elem1", "elem2", ...)` — element-
|
|
93
|
+
# level escape / unescape. The remaining args after the first
|
|
94
|
+
# must be `Constant[String]` element names.
|
|
95
|
+
def fold_cgi_element(method_name, str, element_args)
|
|
96
|
+
elements = element_args.map do |arg|
|
|
97
|
+
return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(String)
|
|
98
|
+
|
|
99
|
+
arg.value
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
Type::Combinator.constant_of(CGI.public_send(method_name, str, *elements))
|
|
103
|
+
rescue StandardError
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -48,11 +48,21 @@ module Rigor
|
|
|
48
48
|
module ConstantFolding # rubocop:disable Metrics/ModuleLength
|
|
49
49
|
module_function
|
|
50
50
|
|
|
51
|
-
NUMERIC_BINARY = Set[
|
|
52
|
-
|
|
51
|
+
NUMERIC_BINARY = Set[
|
|
52
|
+
:+, :-, :*, :/, :%, :**, :&, :|, :^, :<<, :>>,
|
|
53
|
+
:<, :<=, :>, :>=, :==, :!=, :<=>,
|
|
54
|
+
:gcd, :lcm, :fdiv
|
|
55
|
+
].freeze
|
|
56
|
+
STRING_BINARY = Set[
|
|
57
|
+
:+, :*, :==, :!=, :<, :<=, :>, :>=, :<=>,
|
|
58
|
+
:start_with?, :end_with?, :include?,
|
|
59
|
+
:delete_prefix, :delete_suffix,
|
|
60
|
+
:match?, :index, :rindex, :center, :ljust, :rjust
|
|
61
|
+
].freeze
|
|
53
62
|
SYMBOL_BINARY = Set[:==, :!=, :<=>, :<, :<=, :>, :>=].freeze
|
|
54
|
-
BOOL_BINARY = Set[:&, :|, :^, :==,
|
|
63
|
+
BOOL_BINARY = Set[:&, :|, :^, :==, :!=, :===].freeze
|
|
55
64
|
NIL_BINARY = Set[:==, :!=].freeze
|
|
65
|
+
RATIONAL_BINARY = Set[:div, :modulo, :%, :remainder, :fdiv].freeze
|
|
56
66
|
|
|
57
67
|
# v0.0.3 C — pure unary catalogue. Each method must:
|
|
58
68
|
# - take zero arguments,
|
|
@@ -74,20 +84,23 @@ module Rigor
|
|
|
74
84
|
:odd?, :even?, :zero?, :positive?, :negative?,
|
|
75
85
|
:succ, :pred, :next, :abs, :magnitude,
|
|
76
86
|
:bit_length, :to_s, :to_i, :to_int, :to_f,
|
|
87
|
+
:floor, :ceil, :round, :truncate, :chr,
|
|
77
88
|
:inspect, :hash, :-@, :+@, :~
|
|
78
89
|
].freeze
|
|
79
90
|
FLOAT_UNARY = Set[
|
|
80
91
|
:zero?, :positive?, :negative?,
|
|
81
92
|
:nan?, :finite?, :infinite?,
|
|
82
93
|
:abs, :magnitude, :floor, :ceil, :round, :truncate,
|
|
94
|
+
:next_float, :prev_float,
|
|
83
95
|
:to_s, :to_i, :to_int, :to_f,
|
|
84
96
|
:inspect, :hash, :-@, :+@
|
|
85
97
|
].freeze
|
|
86
98
|
STRING_UNARY = Set[
|
|
87
99
|
:upcase, :downcase, :capitalize, :swapcase,
|
|
88
100
|
:reverse, :length, :size, :bytesize,
|
|
89
|
-
:empty?, :strip, :lstrip, :rstrip, :chomp,
|
|
101
|
+
:empty?, :strip, :lstrip, :rstrip, :chomp, :chop,
|
|
90
102
|
:to_s, :to_str, :to_sym, :intern,
|
|
103
|
+
:to_i, :to_f, :ord, :chr, :hex, :oct, :succ, :next,
|
|
91
104
|
:inspect, :hash
|
|
92
105
|
].freeze
|
|
93
106
|
SYMBOL_UNARY = Set[
|
|
@@ -97,6 +110,11 @@ module Rigor
|
|
|
97
110
|
].freeze
|
|
98
111
|
BOOL_UNARY = Set[:!, :to_s, :inspect, :hash, :&, :|, :^].freeze
|
|
99
112
|
NIL_UNARY = Set[:nil?, :!, :to_s, :to_a, :to_h, :inspect, :hash].freeze
|
|
113
|
+
RATIONAL_UNARY = Set[
|
|
114
|
+
:zero?, :integer?, :real, :abs2,
|
|
115
|
+
:conj, :conjugate, :nonzero?
|
|
116
|
+
].freeze
|
|
117
|
+
COMPLEX_UNARY = Set[:zero?, :nonzero?].freeze
|
|
100
118
|
|
|
101
119
|
STRING_FOLD_BYTE_LIMIT = 4096
|
|
102
120
|
|
|
@@ -283,6 +301,18 @@ module Rigor
|
|
|
283
301
|
pathname_lift = try_fold_pathname_unary(receiver_values, method_name)
|
|
284
302
|
return pathname_lift if pathname_lift
|
|
285
303
|
|
|
304
|
+
regexp_lift = try_fold_regexp_array_unary(receiver_values, method_name)
|
|
305
|
+
return regexp_lift if regexp_lift
|
|
306
|
+
|
|
307
|
+
set_lift = try_fold_set_array_unary(receiver_values, method_name)
|
|
308
|
+
return set_lift if set_lift
|
|
309
|
+
|
|
310
|
+
integer_lift = try_fold_integer_array_unary(receiver_values, method_name)
|
|
311
|
+
return integer_lift if integer_lift
|
|
312
|
+
|
|
313
|
+
numeric_lift = try_fold_numeric_array_unary(receiver_values, method_name)
|
|
314
|
+
return numeric_lift if numeric_lift
|
|
315
|
+
|
|
286
316
|
# Type-level allow check on every receiver. If one member's
|
|
287
317
|
# type does not have the method in its allow list (e.g.
|
|
288
318
|
# `Union[String, nil].nil?` — `:nil?` is not in
|
|
@@ -309,7 +339,7 @@ module Rigor
|
|
|
309
339
|
# Only fires on a single-receiver Range with finite integer
|
|
310
340
|
# endpoints; mixed unions fall through so the existing
|
|
311
341
|
# union-of-Constants path keeps the rest of the arms.
|
|
312
|
-
RANGE_FOLD_METHODS = Set[:to_a, :first, :last, :min, :max, :count, :size, :length].freeze
|
|
342
|
+
RANGE_FOLD_METHODS = Set[:to_a, :first, :last, :min, :max, :count, :size, :length, :entries, :minmax].freeze
|
|
313
343
|
RANGE_TO_A_LIMIT = 16
|
|
314
344
|
private_constant :RANGE_FOLD_METHODS, :RANGE_TO_A_LIMIT
|
|
315
345
|
|
|
@@ -326,10 +356,11 @@ module Rigor
|
|
|
326
356
|
|
|
327
357
|
def range_constant_unary(range, method_name)
|
|
328
358
|
case method_name
|
|
329
|
-
when :to_a then range_to_a_tuple(range)
|
|
359
|
+
when :to_a, :entries then range_to_a_tuple(range)
|
|
330
360
|
when :first, :min then range_endpoint_constant(range, :first)
|
|
331
361
|
when :last, :max then range_endpoint_constant(range, :last)
|
|
332
362
|
when :count, :size, :length then Type::Combinator.constant_of(range.to_a.size)
|
|
363
|
+
when :minmax then range_minmax_tuple(range)
|
|
333
364
|
end
|
|
334
365
|
end
|
|
335
366
|
|
|
@@ -348,6 +379,21 @@ module Rigor
|
|
|
348
379
|
Type::Combinator.constant_of(edge == :first ? values.first : values.last)
|
|
349
380
|
end
|
|
350
381
|
|
|
382
|
+
def range_minmax_tuple(range)
|
|
383
|
+
values = range.to_a
|
|
384
|
+
if values.empty?
|
|
385
|
+
return Type::Combinator.tuple_of(
|
|
386
|
+
Type::Combinator.constant_of(nil),
|
|
387
|
+
Type::Combinator.constant_of(nil)
|
|
388
|
+
)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
Type::Combinator.tuple_of(
|
|
392
|
+
Type::Combinator.constant_of(values.first),
|
|
393
|
+
Type::Combinator.constant_of(values.last)
|
|
394
|
+
)
|
|
395
|
+
end
|
|
396
|
+
|
|
351
397
|
def try_fold_binary_set(receiver_values, method_name, arg_values)
|
|
352
398
|
string_lift = try_fold_string_array_binary(receiver_values, method_name, arg_values)
|
|
353
399
|
return string_lift if string_lift
|
|
@@ -377,6 +423,30 @@ module Rigor
|
|
|
377
423
|
:STRING_ARRAY_BINARY_METHODS,
|
|
378
424
|
:STRING_ARRAY_LIFT_LIMIT
|
|
379
425
|
|
|
426
|
+
# `Constant<Regexp>#names` returns an Array of capture-group name
|
|
427
|
+
# strings. Lifted to a Tuple so downstream narrowing can project
|
|
428
|
+
# per-element types. The catalog classifies the C body as `:leaf`
|
|
429
|
+
# so it is safe to evaluate at fold time; no `$~` side effect.
|
|
430
|
+
REGEXP_ARRAY_UNARY_METHODS = Set[:names].freeze
|
|
431
|
+
private_constant :REGEXP_ARRAY_UNARY_METHODS
|
|
432
|
+
|
|
433
|
+
# `Constant<Set>#to_a` returns an Array of the set's elements.
|
|
434
|
+
# Ruby 3.2+ Set is C-implemented with a Hash as its backing store,
|
|
435
|
+
# so element ordering is deterministic (insertion order).
|
|
436
|
+
# The catalog marks `to_a` as `:dispatch` (it calls through to the
|
|
437
|
+
# internal hash), so this dedicated handler bypasses the catalog gate.
|
|
438
|
+
SET_ARRAY_UNARY_METHODS = Set[:to_a, :entries].freeze
|
|
439
|
+
private_constant :SET_ARRAY_UNARY_METHODS
|
|
440
|
+
|
|
441
|
+
# `Constant<Integer>#digits` returns the base-10 (or base-n with
|
|
442
|
+
# an argument — only the no-arg form is folded here) place
|
|
443
|
+
# values as a little-endian Array of Integers. Lifted to a
|
|
444
|
+
# Tuple so downstream rules see the precise per-position type.
|
|
445
|
+
# `digits` raises `Math::DomainError` on a negative receiver,
|
|
446
|
+
# so the negative case bails to the RBS tier.
|
|
447
|
+
INTEGER_ARRAY_UNARY_METHODS = Set[:digits].freeze
|
|
448
|
+
private_constant :INTEGER_ARRAY_UNARY_METHODS
|
|
449
|
+
|
|
380
450
|
# v0.0.7 — `Constant<Pathname>` delegates to a curated set
|
|
381
451
|
# of pure path-manipulation methods. Pathname is immutable
|
|
382
452
|
# in Ruby (per its docstring) and the catalog classifies
|
|
@@ -446,6 +516,83 @@ module Rigor
|
|
|
446
516
|
nil
|
|
447
517
|
end
|
|
448
518
|
|
|
519
|
+
# `Constant<Regexp>#names` — lift the Array[String] of named-capture
|
|
520
|
+
# group names to a Tuple[Constant[String]…]. Safe to evaluate at fold
|
|
521
|
+
# time: the C body reads only the regexp's internal names table,
|
|
522
|
+
# writes no global state, and always returns an Array of frozen Strings.
|
|
523
|
+
def try_fold_regexp_array_unary(receiver_values, method_name)
|
|
524
|
+
return nil unless REGEXP_ARRAY_UNARY_METHODS.include?(method_name)
|
|
525
|
+
return nil unless receiver_values.size == 1
|
|
526
|
+
|
|
527
|
+
receiver = receiver_values.first
|
|
528
|
+
return nil unless receiver.is_a?(Regexp)
|
|
529
|
+
|
|
530
|
+
lift_array_result(receiver.public_send(method_name))
|
|
531
|
+
rescue StandardError
|
|
532
|
+
nil
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# `Constant<Set>#to_a` / `#entries` — lift the Array of set elements
|
|
536
|
+
# to a Tuple[Constant[…]…] when every element is a foldable scalar.
|
|
537
|
+
# Ruby 3.2+ Set is C-implemented; element order is deterministic
|
|
538
|
+
# (insertion order), so the result is stable across invocations.
|
|
539
|
+
def try_fold_set_array_unary(receiver_values, method_name)
|
|
540
|
+
return nil unless SET_ARRAY_UNARY_METHODS.include?(method_name)
|
|
541
|
+
return nil unless receiver_values.size == 1
|
|
542
|
+
|
|
543
|
+
receiver = receiver_values.first
|
|
544
|
+
return nil unless receiver.is_a?(::Set)
|
|
545
|
+
|
|
546
|
+
lift_array_result(receiver.to_a)
|
|
547
|
+
rescue StandardError
|
|
548
|
+
nil
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
# `Constant<Integer>#digits` — lift the Array of base-10 place
|
|
552
|
+
# values to a Tuple[Constant[Integer]…]. Safe to evaluate at
|
|
553
|
+
# fold time: the C body is pure arithmetic. Negative receivers
|
|
554
|
+
# raise `Math::DomainError`; the fold declines so the RBS tier
|
|
555
|
+
# answers with `Array[Integer]`.
|
|
556
|
+
def try_fold_integer_array_unary(receiver_values, method_name)
|
|
557
|
+
return nil unless INTEGER_ARRAY_UNARY_METHODS.include?(method_name)
|
|
558
|
+
return nil unless receiver_values.size == 1
|
|
559
|
+
|
|
560
|
+
receiver = receiver_values.first
|
|
561
|
+
return nil unless receiver.is_a?(Integer)
|
|
562
|
+
return nil if receiver.negative?
|
|
563
|
+
|
|
564
|
+
lift_array_result(receiver.digits)
|
|
565
|
+
rescue StandardError
|
|
566
|
+
nil
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
# `Constant<Complex>#rect` / `#rectangular` — lifts `[real, imaginary]`
|
|
570
|
+
# to `Tuple[Constant[re], Constant[im]]`. Both components are always
|
|
571
|
+
# numeric (Integer or Float for literal complexes), so they satisfy
|
|
572
|
+
# `foldable_constant_value?`.
|
|
573
|
+
#
|
|
574
|
+
# `Constant<Complex>#polar` — lifts `[abs, arg]` to
|
|
575
|
+
# `Tuple[Constant[Float], Constant[Float]]`. Evaluated at fold time
|
|
576
|
+
# via `Complex#polar` (which calls `Math.hypot` and `Math.atan2`).
|
|
577
|
+
# Deterministic: reads only the receiver's real and imaginary parts.
|
|
578
|
+
#
|
|
579
|
+
# Rational receivers also support `rect` / `rectangular` / `polar`:
|
|
580
|
+
# `Rational(r,1).rect` → `[r, 0]`, `Rational(r,1).polar` → `[abs, arg]`.
|
|
581
|
+
NUMERIC_ARRAY_UNARY_METHODS = Set[:rect, :rectangular, :polar].freeze
|
|
582
|
+
private_constant :NUMERIC_ARRAY_UNARY_METHODS
|
|
583
|
+
|
|
584
|
+
def try_fold_numeric_array_unary(receiver_values, method_name)
|
|
585
|
+
return nil unless NUMERIC_ARRAY_UNARY_METHODS.include?(method_name)
|
|
586
|
+
return nil unless receiver_values.size == 1
|
|
587
|
+
|
|
588
|
+
receiver = receiver_values.first
|
|
589
|
+
return nil unless receiver.is_a?(Complex) || receiver.is_a?(Rational)
|
|
590
|
+
|
|
591
|
+
lift_array_result(receiver.public_send(method_name))
|
|
592
|
+
rescue StandardError
|
|
593
|
+
nil
|
|
594
|
+
end
|
|
595
|
+
|
|
449
596
|
# `Constant<String>#split(arg)` / `#scan(arg)` — lift the
|
|
450
597
|
# Array result to a Tuple when both sides are statically
|
|
451
598
|
# known and the cardinality fits.
|
|
@@ -1101,6 +1248,8 @@ module Rigor
|
|
|
1101
1248
|
when Symbol then SYMBOL_UNARY
|
|
1102
1249
|
when true, false then BOOL_UNARY
|
|
1103
1250
|
when nil then NIL_UNARY
|
|
1251
|
+
when Rational then RATIONAL_UNARY
|
|
1252
|
+
when Complex then COMPLEX_UNARY
|
|
1104
1253
|
else Set.new
|
|
1105
1254
|
end
|
|
1106
1255
|
end
|
|
@@ -1126,11 +1275,15 @@ module Rigor
|
|
|
1126
1275
|
# have their own shape carriers; this method picks
|
|
1127
1276
|
# the conservative envelope of "values that already
|
|
1128
1277
|
# round-trip through `Type::Combinator.constant_of`".
|
|
1278
|
+
FOLDABLE_CONSTANT_CLASSES = [
|
|
1279
|
+
Integer, Float, Rational, Complex, String, Symbol,
|
|
1280
|
+
Regexp, Pathname, ::Set, Date, Time,
|
|
1281
|
+
TrueClass, FalseClass, NilClass
|
|
1282
|
+
].freeze
|
|
1283
|
+
private_constant :FOLDABLE_CONSTANT_CLASSES
|
|
1284
|
+
|
|
1129
1285
|
def foldable_constant_value?(value)
|
|
1130
|
-
|
|
1131
|
-
when Integer, Float, Rational, Complex, String, Symbol, Regexp, Pathname, true, false, nil then true
|
|
1132
|
-
else false
|
|
1133
|
-
end
|
|
1286
|
+
FOLDABLE_CONSTANT_CLASSES.any? { |klass| value.is_a?(klass) }
|
|
1134
1287
|
end
|
|
1135
1288
|
|
|
1136
1289
|
def safe?(receiver_value, method_name, arg_value)
|
|
@@ -1150,6 +1303,7 @@ module Rigor
|
|
|
1150
1303
|
when Symbol then SYMBOL_BINARY
|
|
1151
1304
|
when true, false then BOOL_BINARY
|
|
1152
1305
|
when nil then NIL_BINARY
|
|
1306
|
+
when Rational then RATIONAL_BINARY
|
|
1153
1307
|
else Set.new
|
|
1154
1308
|
end
|
|
1155
1309
|
end
|
|
@@ -1169,10 +1323,19 @@ module Rigor
|
|
|
1169
1323
|
case method_name
|
|
1170
1324
|
when :+ then string_concat_blow_up?(receiver_value, arg_value)
|
|
1171
1325
|
when :* then string_repeat_blow_up?(receiver_value, arg_value)
|
|
1326
|
+
when :center, :ljust, :rjust then string_pad_blow_up?(arg_value)
|
|
1172
1327
|
else false
|
|
1173
1328
|
end
|
|
1174
1329
|
end
|
|
1175
1330
|
|
|
1331
|
+
# `"x".center(width)` / `#ljust` / `#rjust` produce a string
|
|
1332
|
+
# of `max(width, len)` characters. A literal `width` far
|
|
1333
|
+
# larger than the receiver would materialise a huge Constant;
|
|
1334
|
+
# cap it at the same byte limit the concat / repeat paths use.
|
|
1335
|
+
def string_pad_blow_up?(arg_value)
|
|
1336
|
+
arg_value.is_a?(Integer) && arg_value > STRING_FOLD_BYTE_LIMIT
|
|
1337
|
+
end
|
|
1338
|
+
|
|
1176
1339
|
def string_concat_blow_up?(receiver_value, arg_value)
|
|
1177
1340
|
arg_value.is_a?(String) &&
|
|
1178
1341
|
receiver_value.bytesize + arg_value.bytesize > STRING_FOLD_BYTE_LIMIT
|
|
@@ -57,11 +57,63 @@ module Rigor
|
|
|
57
57
|
return nil if receiver.nil?
|
|
58
58
|
return try_array(args) if method_name == :Array
|
|
59
59
|
return try_numeric_constructor(method_name, args) if NUMERIC_CONSTRUCTORS.key?(method_name)
|
|
60
|
-
return
|
|
60
|
+
return try_integer(args) if method_name == :Integer
|
|
61
|
+
return try_float(args) if method_name == :Float
|
|
61
62
|
|
|
62
63
|
nil
|
|
63
64
|
end
|
|
64
65
|
|
|
66
|
+
# `Kernel#Integer(arg)` / `Integer(arg, base)`. Two folding
|
|
67
|
+
# paths, tried in order:
|
|
68
|
+
#
|
|
69
|
+
# 1. A `Refined[String, predicate]` argument whose predicate
|
|
70
|
+
# is a digit-only carrier narrows to `non-negative-int`
|
|
71
|
+
# (see {try_integer_from_refinement}).
|
|
72
|
+
# 2. A `Constant` String or Numeric argument — optionally
|
|
73
|
+
# with a `Constant[Integer]` base — runs the actual
|
|
74
|
+
# `Integer()` conversion and lifts the result to
|
|
75
|
+
# `Constant[Integer]`.
|
|
76
|
+
def try_integer(args)
|
|
77
|
+
refined = try_integer_from_refinement(args)
|
|
78
|
+
return refined if refined
|
|
79
|
+
|
|
80
|
+
try_integer_constant(args)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Constant-folding path for `Integer()`. A non-parseable
|
|
84
|
+
# string raises `ArgumentError` (or `TypeError` for a base
|
|
85
|
+
# against a non-string) at fold time; the handler declines
|
|
86
|
+
# so the RBS tier answers with the widened `Integer`.
|
|
87
|
+
def try_integer_constant(args)
|
|
88
|
+
return nil unless [1, 2].include?(args.size)
|
|
89
|
+
return nil unless args.all?(Type::Constant)
|
|
90
|
+
|
|
91
|
+
values = args.map(&:value)
|
|
92
|
+
return nil unless values[0].is_a?(String) || values[0].is_a?(Numeric)
|
|
93
|
+
return nil if values.size == 2 && !values[1].is_a?(Integer)
|
|
94
|
+
|
|
95
|
+
Type::Combinator.constant_of(Integer(*values))
|
|
96
|
+
rescue ArgumentError, TypeError
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# `Kernel#Float(arg)` — folds a `Constant` String or Numeric
|
|
101
|
+
# argument to `Constant[Float]`. A non-parseable string
|
|
102
|
+
# raises `ArgumentError` at fold time; the handler declines.
|
|
103
|
+
def try_float(args)
|
|
104
|
+
return nil unless args.size == 1
|
|
105
|
+
|
|
106
|
+
arg = args.first
|
|
107
|
+
return nil unless arg.is_a?(Type::Constant)
|
|
108
|
+
|
|
109
|
+
value = arg.value
|
|
110
|
+
return nil unless value.is_a?(String) || value.is_a?(Numeric)
|
|
111
|
+
|
|
112
|
+
Type::Combinator.constant_of(Float(value))
|
|
113
|
+
rescue ArgumentError, TypeError
|
|
114
|
+
nil
|
|
115
|
+
end
|
|
116
|
+
|
|
65
117
|
# `Kernel#Integer(s)` over a `Refined[String, predicate]`
|
|
66
118
|
# whose predicate is in {INTEGER_REFINEMENT_PREDICATES}.
|
|
67
119
|
# Mirrors the `String#to_i` projection in `ShapeDispatch`
|