rigortype 0.0.8 → 0.0.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 +195 -21
- 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/runner.rb +19 -3
- 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 +53 -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_known_class_names.rb +43 -0
- data/lib/rigor/cache/store.rb +79 -15
- data/lib/rigor/cli.rb +36 -4
- data/lib/rigor/environment/rbs_hierarchy.rb +18 -5
- data/lib/rigor/environment/rbs_loader.rb +137 -25
- data/lib/rigor/environment.rb +11 -2
- data/lib/rigor/flow_contribution.rb +128 -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 +26 -1
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +16 -1
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +87 -0
- data/lib/rigor/inference/method_dispatcher.rb +2 -0
- data/lib/rigor/inference/narrowing.rb +29 -14
- data/lib/rigor/rbs_extended.rb +55 -0
- 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 +6 -0
- data/sig/rigor.rbs +3 -1
- metadata +21 -1
|
@@ -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
|
|
@@ -472,10 +472,35 @@ module Rigor
|
|
|
472
472
|
[keys, values]
|
|
473
473
|
end
|
|
474
474
|
|
|
475
|
-
|
|
475
|
+
# An interpolated string `"#{a}b#{c}"` is `literal-string`
|
|
476
|
+
# when every part contributes literal-bearing material:
|
|
477
|
+
# plain text segments are literal by construction, embedded
|
|
478
|
+
# expressions count when their type is itself literal-string-
|
|
479
|
+
# compatible (a `Constant<String>`, the `literal-string`
|
|
480
|
+
# carrier, an `Intersection` containing it, or a `Union`
|
|
481
|
+
# whose members all qualify). Otherwise the result widens to
|
|
482
|
+
# plain `Nominal[String]` as before.
|
|
483
|
+
def type_of_interpolated_string(node)
|
|
484
|
+
return Type::Combinator.literal_string if interpolation_parts_literal?(node.parts)
|
|
485
|
+
|
|
476
486
|
Type::Combinator.nominal_of(String)
|
|
477
487
|
end
|
|
478
488
|
|
|
489
|
+
def interpolation_parts_literal?(parts)
|
|
490
|
+
parts.all? { |part| interpolation_part_literal?(part) }
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def interpolation_part_literal?(part)
|
|
494
|
+
case part
|
|
495
|
+
when Prism::StringNode
|
|
496
|
+
true
|
|
497
|
+
when Prism::EmbeddedStatementsNode, Prism::EmbeddedVariableNode
|
|
498
|
+
Type::Combinator.literal_string_compatible?(type_of(part))
|
|
499
|
+
else
|
|
500
|
+
false
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
479
504
|
def type_of_interpolated_symbol(_node)
|
|
480
505
|
Type::Combinator.nominal_of(Symbol)
|
|
481
506
|
end
|
|
@@ -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
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module MethodDispatcher
|
|
8
|
+
# Dispatcher tier that lifts string-composition results into
|
|
9
|
+
# the `literal-string` carrier when every operand is itself
|
|
10
|
+
# literal-bearing. Sits between {ConstantFolding} (which
|
|
11
|
+
# handles all-Constant cases) and {ShapeDispatch}; runs only
|
|
12
|
+
# for `String#+` / `String#*` / `String#<<` / `String#concat`
|
|
13
|
+
# calls whose inputs the ConstantFolding tier could not fold
|
|
14
|
+
# to a precise `Constant<String>` (e.g. one operand is
|
|
15
|
+
# `literal-string` rather than `Constant<String>`, or the
|
|
16
|
+
# multiplication exceeds the constant-fold size cap).
|
|
17
|
+
#
|
|
18
|
+
# Result rule:
|
|
19
|
+
#
|
|
20
|
+
# - `+`, `<<`, `concat`: receiver and argument MUST both be
|
|
21
|
+
# `Type::Combinator.literal_string_compatible?`. The result
|
|
22
|
+
# is `literal-string`. `<<` and `concat` mutate the
|
|
23
|
+
# receiver at runtime; the analyzer does not track that
|
|
24
|
+
# mutation against the local's binding, but the call's
|
|
25
|
+
# *return value* is the receiver itself, and the receiver
|
|
26
|
+
# stays literal-bearing because every appended slice was
|
|
27
|
+
# literal-bearing too.
|
|
28
|
+
# - `*`: receiver MUST be literal-bearing; argument MUST be
|
|
29
|
+
# integer-typed. The result is `literal-string`.
|
|
30
|
+
#
|
|
31
|
+
# Other receiver / argument shapes decline so the next tier
|
|
32
|
+
# (ShapeDispatch / FileFolding / RbsDispatch) takes over and
|
|
33
|
+
# the call site widens to the RBS-declared `Nominal[String]`
|
|
34
|
+
# as before.
|
|
35
|
+
module LiteralStringFolding
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
CONCAT_METHODS = %i[+ << concat].freeze
|
|
39
|
+
private_constant :CONCAT_METHODS
|
|
40
|
+
|
|
41
|
+
def try_dispatch(receiver:, method_name:, args:, **)
|
|
42
|
+
return nil unless Type::Combinator.literal_string_compatible?(receiver)
|
|
43
|
+
return nil unless args.size == 1
|
|
44
|
+
|
|
45
|
+
if CONCAT_METHODS.include?(method_name)
|
|
46
|
+
fold_concat(args.first)
|
|
47
|
+
elsif method_name == :*
|
|
48
|
+
fold_repeat(args.first)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def fold_concat(arg_type)
|
|
53
|
+
return nil unless Type::Combinator.literal_string_compatible?(arg_type)
|
|
54
|
+
|
|
55
|
+
Type::Combinator.literal_string
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def fold_repeat(arg_type)
|
|
59
|
+
return nil unless integer_typed?(arg_type)
|
|
60
|
+
return nil if known_negative_integer?(arg_type)
|
|
61
|
+
|
|
62
|
+
Type::Combinator.literal_string
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def integer_typed?(type)
|
|
66
|
+
case type
|
|
67
|
+
when Type::Constant then type.value.is_a?(Integer)
|
|
68
|
+
when Type::Nominal then type.class_name == "Integer"
|
|
69
|
+
when Type::IntegerRange then true
|
|
70
|
+
else false
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# `String#*` raises ArgumentError on a negative multiplier, so a
|
|
75
|
+
# `Constant<-1>` argument is not a valid lift target. Decline so
|
|
76
|
+
# the call site keeps the existing nil-result behaviour rather
|
|
77
|
+
# than promising a `literal-string` value that could never
|
|
78
|
+
# exist at runtime.
|
|
79
|
+
def known_negative_integer?(type)
|
|
80
|
+
type.is_a?(Type::Constant) && type.value.is_a?(Integer) && type.value.negative?
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private_class_method :fold_concat, :fold_repeat, :integer_typed?
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../reflection"
|
|
4
4
|
require_relative "../type"
|
|
5
5
|
require_relative "method_dispatcher/constant_folding"
|
|
6
|
+
require_relative "method_dispatcher/literal_string_folding"
|
|
6
7
|
require_relative "method_dispatcher/shape_dispatch"
|
|
7
8
|
require_relative "method_dispatcher/rbs_dispatch"
|
|
8
9
|
require_relative "method_dispatcher/iterator_dispatch"
|
|
@@ -101,6 +102,7 @@ module Rigor
|
|
|
101
102
|
return meta_result if meta_result
|
|
102
103
|
|
|
103
104
|
ConstantFolding.try_fold(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
105
|
+
LiteralStringFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
104
106
|
ShapeDispatch.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
105
107
|
FileFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
106
108
|
KernelDispatch.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
@@ -539,26 +539,41 @@ module Rigor
|
|
|
539
539
|
# downstream narrowing knows the refinement subset is
|
|
540
540
|
# excluded.
|
|
541
541
|
#
|
|
542
|
-
#
|
|
543
|
-
#
|
|
544
|
-
#
|
|
545
|
-
#
|
|
546
|
-
#
|
|
547
|
-
#
|
|
542
|
+
# v0.0.9 — when the predicate has a registered
|
|
543
|
+
# complement (see {Type::Refined::COMPLEMENT_PAIRS}) and
|
|
544
|
+
# the part is exactly the refinement's base, the
|
|
545
|
+
# narrowing returns `Refined[base, complement_predicate]`
|
|
546
|
+
# instead of `Difference[base, refined]`. This is the
|
|
547
|
+
# `~T` symmetry the spec promises: `~lowercase-string`
|
|
548
|
+
# narrows `String` to `non-lowercase-string` rather than
|
|
549
|
+
# `Difference[String, lowercase-string]`.
|
|
550
|
+
#
|
|
551
|
+
# Predicates without a registered complement still fall
|
|
552
|
+
# back to the imprecise but sound `Difference[part,
|
|
553
|
+
# refined]` carrier so behaviour is unchanged for
|
|
554
|
+
# untouched call sites.
|
|
548
555
|
def complement_refined(current_type, refined)
|
|
549
|
-
|
|
556
|
+
complement = registered_complement_for(refined)
|
|
550
557
|
parts = current_type.is_a?(Type::Union) ? current_type.members : [current_type]
|
|
558
|
+
survivors = parts.filter_map { |part| complement_refined_part(part, refined, complement) }
|
|
559
|
+
return current_type if survivors.empty?
|
|
560
|
+
|
|
561
|
+
Type::Combinator.union(*survivors)
|
|
562
|
+
end
|
|
551
563
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
564
|
+
def complement_refined_part(part, refined, complement)
|
|
565
|
+
return nil if part == refined
|
|
566
|
+
return part if base_disjoint?(refined.base, part)
|
|
567
|
+
return complement if complement && part == refined.base
|
|
555
568
|
|
|
556
|
-
|
|
557
|
-
|
|
569
|
+
Type::Combinator.difference(part, refined)
|
|
570
|
+
end
|
|
558
571
|
|
|
559
|
-
|
|
572
|
+
def registered_complement_for(refined)
|
|
573
|
+
complement_id = refined.complement_predicate_id
|
|
574
|
+
return nil if complement_id.nil?
|
|
560
575
|
|
|
561
|
-
Type::Combinator.
|
|
576
|
+
Type::Combinator.refined(refined.base, complement_id)
|
|
562
577
|
end
|
|
563
578
|
|
|
564
579
|
def falsey_value?(value)
|
data/lib/rigor/rbs_extended.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "type"
|
|
4
4
|
require_relative "builtins/imported_refinements"
|
|
5
|
+
require_relative "flow_contribution"
|
|
5
6
|
|
|
6
7
|
module Rigor
|
|
7
8
|
# Slice 7 phase 15 — first-preview reader for the
|
|
@@ -410,5 +411,59 @@ module Rigor
|
|
|
410
411
|
|
|
411
412
|
ParamOverride.new(param_name: match[:param].to_sym, type: type)
|
|
412
413
|
end
|
|
414
|
+
|
|
415
|
+
# The shared {Rigor::FlowContribution::Provenance} for every
|
|
416
|
+
# bundle this module produces. `source_family: :rbs_extended`
|
|
417
|
+
# so consumers (today the documentation surface; v0.1.0 the
|
|
418
|
+
# plugin contribution merger) can attribute facts back to the
|
|
419
|
+
# RBS::Extended layer.
|
|
420
|
+
RBS_EXTENDED_PROVENANCE = FlowContribution::Provenance.new(
|
|
421
|
+
source_family: :rbs_extended,
|
|
422
|
+
plugin_id: nil,
|
|
423
|
+
node: nil,
|
|
424
|
+
descriptor: nil
|
|
425
|
+
).freeze
|
|
426
|
+
|
|
427
|
+
# Rolls up every recognised RBS::Extended directive on
|
|
428
|
+
# `method_def` into a single {Rigor::FlowContribution}:
|
|
429
|
+
#
|
|
430
|
+
# - `predicate-if-true` → `truthy_facts` (`PredicateEffect`s)
|
|
431
|
+
# - `predicate-if-false` → `falsey_facts` (`PredicateEffect`s)
|
|
432
|
+
# - `assert*` → `post_return_facts` (`AssertEffect`s)
|
|
433
|
+
# - `return:` override → `return_type` (`Rigor::Type`)
|
|
434
|
+
#
|
|
435
|
+
# Param overrides are intentionally NOT included — they refine
|
|
436
|
+
# the call's signature contract rather than its flow facts and
|
|
437
|
+
# do not fit ADR-2 § "Flow Contribution Bundle" slot semantics.
|
|
438
|
+
# Callers that care about parameter contracts keep using
|
|
439
|
+
# {.read_param_type_overrides} / {.param_type_override_map}.
|
|
440
|
+
#
|
|
441
|
+
# Returns `nil` when the method carries no recognised
|
|
442
|
+
# contribution directives (callers can skip the merge step
|
|
443
|
+
# without iterating an empty bundle).
|
|
444
|
+
def read_flow_contribution(method_def)
|
|
445
|
+
return nil if method_def.nil?
|
|
446
|
+
|
|
447
|
+
predicate_effects = read_predicate_effects(method_def)
|
|
448
|
+
assert_effects = read_assert_effects(method_def)
|
|
449
|
+
return_override = read_return_type_override(method_def)
|
|
450
|
+
return nil if predicate_effects.empty? && assert_effects.empty? && return_override.nil?
|
|
451
|
+
|
|
452
|
+
build_flow_contribution(predicate_effects, assert_effects, return_override)
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def build_flow_contribution(predicate_effects, assert_effects, return_override)
|
|
456
|
+
FlowContribution.new(
|
|
457
|
+
return_type: return_override,
|
|
458
|
+
truthy_facts: nilable_slot(predicate_effects.select(&:truthy_only?)),
|
|
459
|
+
falsey_facts: nilable_slot(predicate_effects.select(&:falsey_only?)),
|
|
460
|
+
post_return_facts: nilable_slot(assert_effects),
|
|
461
|
+
provenance: RBS_EXTENDED_PROVENANCE
|
|
462
|
+
)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def nilable_slot(facts)
|
|
466
|
+
facts.empty? ? nil : facts
|
|
467
|
+
end
|
|
413
468
|
end
|
|
414
469
|
end
|
|
@@ -149,14 +149,40 @@ module Rigor
|
|
|
149
149
|
Refined.new(nominal_of("String"), :lowercase)
|
|
150
150
|
end
|
|
151
151
|
|
|
152
|
+
# Complement of `lowercase-string`: a `String` with at least
|
|
153
|
+
# one non-lowercase character (i.e. `v != v.downcase`).
|
|
154
|
+
# Registered as the paired complement of
|
|
155
|
+
# `:lowercase` in {Refined::COMPLEMENT_PAIRS} so
|
|
156
|
+
# `~lowercase-string` narrows to this carrier instead of
|
|
157
|
+
# falling back to `Difference[String, lowercase-string]`.
|
|
158
|
+
def non_lowercase_string
|
|
159
|
+
Refined.new(nominal_of("String"), :not_lowercase)
|
|
160
|
+
end
|
|
161
|
+
|
|
152
162
|
def uppercase_string
|
|
153
163
|
Refined.new(nominal_of("String"), :uppercase)
|
|
154
164
|
end
|
|
155
165
|
|
|
166
|
+
# Complement of `uppercase-string`: a `String` with at least
|
|
167
|
+
# one non-uppercase character. Paired with `:uppercase` in
|
|
168
|
+
# {Refined::COMPLEMENT_PAIRS}.
|
|
169
|
+
def non_uppercase_string
|
|
170
|
+
Refined.new(nominal_of("String"), :not_uppercase)
|
|
171
|
+
end
|
|
172
|
+
|
|
156
173
|
def numeric_string
|
|
157
174
|
Refined.new(nominal_of("String"), :numeric)
|
|
158
175
|
end
|
|
159
176
|
|
|
177
|
+
# Complement of `numeric-string`: a `String` that is not
|
|
178
|
+
# accepted by Rigor's Ruby numeric-string predicate
|
|
179
|
+
# (contains at least one non-digit, has a malformed numeric
|
|
180
|
+
# form, etc.). Paired with `:numeric` in
|
|
181
|
+
# {Refined::COMPLEMENT_PAIRS}.
|
|
182
|
+
def non_numeric_string
|
|
183
|
+
Refined.new(nominal_of("String"), :not_numeric)
|
|
184
|
+
end
|
|
185
|
+
|
|
160
186
|
def decimal_int_string
|
|
161
187
|
Refined.new(nominal_of("String"), :decimal_int)
|
|
162
188
|
end
|
|
@@ -169,6 +195,52 @@ module Rigor
|
|
|
169
195
|
Refined.new(nominal_of("String"), :hex_int)
|
|
170
196
|
end
|
|
171
197
|
|
|
198
|
+
# `literal-string` — a `String` that is statically known to
|
|
199
|
+
# come from a source-code literal (or a composition of
|
|
200
|
+
# literals). v0.0.9 tracks this flow through interpolation
|
|
201
|
+
# `"#{...}"`, leaving propagation through `+` / `<<` to a
|
|
202
|
+
# later slice. Every `Constant<String>` is implicitly
|
|
203
|
+
# literal-string-compatible; the carrier exists for cases
|
|
204
|
+
# where the concrete value is unknown but literal-ness has
|
|
205
|
+
# been established (an RBS::Extended `return: literal-string`
|
|
206
|
+
# annotation, or interpolation over literal-bearing parts).
|
|
207
|
+
def literal_string
|
|
208
|
+
Refined.new(nominal_of("String"), :literal_string)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# `non-empty-literal-string` = `non-empty-string ∩ literal-string`.
|
|
212
|
+
# Composes the point-removal half (`Difference[String, ""]`)
|
|
213
|
+
# with the predicate-subset half. Both members erase to
|
|
214
|
+
# `String`.
|
|
215
|
+
def non_empty_literal_string
|
|
216
|
+
intersection(non_empty_string, literal_string)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Recognises the carriers that participate in literal-string
|
|
220
|
+
# flow tracking: any `Constant<String>` (constants are literal
|
|
221
|
+
# by construction), the `literal-string` Refined carrier, an
|
|
222
|
+
# `Intersection` containing `literal-string`, or a `Union`
|
|
223
|
+
# whose every member qualifies. Used by
|
|
224
|
+
# `ExpressionTyper#type_of_interpolated_string` and the
|
|
225
|
+
# `LiteralStringFolding` dispatcher tier so propagation
|
|
226
|
+
# through interpolation and `+`/`*` composition stays
|
|
227
|
+
# consistent.
|
|
228
|
+
def literal_string_compatible?(type)
|
|
229
|
+
case type
|
|
230
|
+
when Constant then type.value.is_a?(String)
|
|
231
|
+
when Refined then literal_string_carrier?(type)
|
|
232
|
+
when Intersection then type.members.any? { |m| literal_string_compatible?(m) }
|
|
233
|
+
when Union then type.members.all? { |m| literal_string_compatible?(m) }
|
|
234
|
+
else false
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def literal_string_carrier?(refined)
|
|
239
|
+
refined.predicate_id == :literal_string &&
|
|
240
|
+
refined.base.is_a?(Nominal) &&
|
|
241
|
+
refined.base.class_name == "String"
|
|
242
|
+
end
|
|
243
|
+
|
|
172
244
|
# Normalised intersection. Flattens nested Intersections,
|
|
173
245
|
# drops `Top` members, collapses to `Bot` if any member is
|
|
174
246
|
# `Bot`, deduplicates structurally-equal members, sorts the
|
data/lib/rigor/type/refined.rb
CHANGED
|
@@ -141,11 +141,24 @@ module Rigor
|
|
|
141
141
|
|
|
142
142
|
PREDICATES = {
|
|
143
143
|
lowercase: ->(v) { v.is_a?(String) && v == v.downcase },
|
|
144
|
+
not_lowercase: ->(v) { v.is_a?(String) && v != v.downcase },
|
|
144
145
|
uppercase: ->(v) { v.is_a?(String) && v == v.upcase },
|
|
146
|
+
not_uppercase: ->(v) { v.is_a?(String) && v != v.upcase },
|
|
145
147
|
numeric: ->(v) { v.is_a?(String) && NUMERIC_STRING_PATTERN.match?(v) },
|
|
148
|
+
not_numeric: ->(v) { v.is_a?(String) && !NUMERIC_STRING_PATTERN.match?(v) },
|
|
146
149
|
decimal_int: ->(v) { v.is_a?(String) && DECIMAL_INT_STRING_PATTERN.match?(v) },
|
|
147
150
|
octal_int: ->(v) { v.is_a?(String) && OCTAL_INT_STRING_PATTERN.match?(v) },
|
|
148
|
-
hex_int: ->(v) { v.is_a?(String) && HEX_INT_STRING_PATTERN.match?(v) }
|
|
151
|
+
hex_int: ->(v) { v.is_a?(String) && HEX_INT_STRING_PATTERN.match?(v) },
|
|
152
|
+
# `literal-string` is a flow-tracked predicate, not a value-
|
|
153
|
+
# level predicate: a String is literal-string when it is
|
|
154
|
+
# known to come from a source-code literal (or composition
|
|
155
|
+
# of literals). Every concrete `Constant<String>` is
|
|
156
|
+
# already literal by construction, so the inspection
|
|
157
|
+
# recogniser returns true for any String — the property is
|
|
158
|
+
# really tracked in the flow analysis (interpolation,
|
|
159
|
+
# concatenation, RBS::Extended `return: literal-string`)
|
|
160
|
+
# rather than recovered by inspecting an arbitrary string.
|
|
161
|
+
literal_string: ->(v) { v.is_a?(String) }
|
|
149
162
|
}.freeze
|
|
150
163
|
|
|
151
164
|
# Maps `[base_class_name, predicate_id]` pairs to their
|
|
@@ -154,14 +167,49 @@ module Rigor
|
|
|
154
167
|
# to the operator form.
|
|
155
168
|
CANONICAL_NAMES = {
|
|
156
169
|
["String", :lowercase] => "lowercase-string",
|
|
170
|
+
["String", :not_lowercase] => "non-lowercase-string",
|
|
157
171
|
["String", :uppercase] => "uppercase-string",
|
|
172
|
+
["String", :not_uppercase] => "non-uppercase-string",
|
|
158
173
|
["String", :numeric] => "numeric-string",
|
|
174
|
+
["String", :not_numeric] => "non-numeric-string",
|
|
159
175
|
["String", :decimal_int] => "decimal-int-string",
|
|
160
176
|
["String", :octal_int] => "octal-int-string",
|
|
161
|
-
["String", :hex_int] => "hex-int-string"
|
|
177
|
+
["String", :hex_int] => "hex-int-string",
|
|
178
|
+
["String", :literal_string] => "literal-string"
|
|
162
179
|
}.freeze
|
|
163
180
|
private_constant :CANONICAL_NAMES
|
|
164
181
|
|
|
182
|
+
# Bidirectional `predicate_id ↔ complement_predicate_id`
|
|
183
|
+
# registry. `~Refined[base, p]` narrows to
|
|
184
|
+
# `Refined[base, COMPLEMENT_PAIRS[p]]` when the part is the
|
|
185
|
+
# refinement's base — the precise carrier the spec promises
|
|
186
|
+
# under the `~T` operator. Predicates without a registered
|
|
187
|
+
# complement fall back to the imprecise but sound
|
|
188
|
+
# `Difference[part, refined]` carrier from the existing
|
|
189
|
+
# narrowing rule.
|
|
190
|
+
#
|
|
191
|
+
# Adding a new pair here is an additive change: register the
|
|
192
|
+
# complement predicate in {PREDICATES}, give it a kebab-case
|
|
193
|
+
# canonical name in {CANONICAL_NAMES}, and add the bidirectional
|
|
194
|
+
# entry below. No call site needs to know about the new pair —
|
|
195
|
+
# `complement_refined` consults this map and routes through
|
|
196
|
+
# the registered complement automatically.
|
|
197
|
+
COMPLEMENT_PAIRS = {
|
|
198
|
+
lowercase: :not_lowercase,
|
|
199
|
+
not_lowercase: :lowercase,
|
|
200
|
+
uppercase: :not_uppercase,
|
|
201
|
+
not_uppercase: :uppercase,
|
|
202
|
+
numeric: :not_numeric,
|
|
203
|
+
not_numeric: :numeric
|
|
204
|
+
}.freeze
|
|
205
|
+
private_constant :COMPLEMENT_PAIRS
|
|
206
|
+
|
|
207
|
+
# @return [Symbol, nil] the registered complement predicate
|
|
208
|
+
# id, or nil when no pair is registered for this predicate.
|
|
209
|
+
def complement_predicate_id
|
|
210
|
+
COMPLEMENT_PAIRS[predicate_id]
|
|
211
|
+
end
|
|
212
|
+
|
|
165
213
|
private
|
|
166
214
|
|
|
167
215
|
def canonical_name
|
data/lib/rigor/version.rb
CHANGED