rigortype 0.0.1
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 +7 -0
- data/LICENSE +373 -0
- data/README.md +152 -0
- data/exe/rigor +9 -0
- data/lib/rigor/analysis/check_rules.rb +503 -0
- data/lib/rigor/analysis/diagnostic.rb +35 -0
- data/lib/rigor/analysis/fact_store.rb +133 -0
- data/lib/rigor/analysis/result.rb +29 -0
- data/lib/rigor/analysis/runner.rb +119 -0
- data/lib/rigor/ast/type_node.rb +41 -0
- data/lib/rigor/ast.rb +22 -0
- data/lib/rigor/cli/type_of_command.rb +160 -0
- data/lib/rigor/cli/type_of_renderer.rb +88 -0
- data/lib/rigor/cli/type_scan_command.rb +160 -0
- data/lib/rigor/cli/type_scan_renderer.rb +165 -0
- data/lib/rigor/cli/type_scan_report.rb +32 -0
- data/lib/rigor/cli.rb +195 -0
- data/lib/rigor/configuration.rb +49 -0
- data/lib/rigor/environment/class_registry.rb +141 -0
- data/lib/rigor/environment/rbs_hierarchy.rb +64 -0
- data/lib/rigor/environment/rbs_loader.rb +244 -0
- data/lib/rigor/environment.rb +177 -0
- data/lib/rigor/inference/acceptance.rb +444 -0
- data/lib/rigor/inference/block_parameter_binder.rb +198 -0
- data/lib/rigor/inference/closure_escape_analyzer.rb +191 -0
- data/lib/rigor/inference/coverage_scanner.rb +85 -0
- data/lib/rigor/inference/expression_typer.rb +831 -0
- data/lib/rigor/inference/fallback.rb +35 -0
- data/lib/rigor/inference/fallback_tracer.rb +64 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +102 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +169 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +421 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +336 -0
- data/lib/rigor/inference/method_dispatcher.rb +213 -0
- data/lib/rigor/inference/method_parameter_binder.rb +257 -0
- data/lib/rigor/inference/multi_target_binder.rb +143 -0
- data/lib/rigor/inference/narrowing.rb +1008 -0
- data/lib/rigor/inference/rbs_type_translator.rb +219 -0
- data/lib/rigor/inference/scope_indexer.rb +468 -0
- data/lib/rigor/inference/statement_evaluator.rb +1017 -0
- data/lib/rigor/rbs_extended.rb +98 -0
- data/lib/rigor/scope.rb +340 -0
- data/lib/rigor/source/node_locator.rb +104 -0
- data/lib/rigor/source/node_walker.rb +37 -0
- data/lib/rigor/source.rb +15 -0
- data/lib/rigor/testing.rb +65 -0
- data/lib/rigor/trinary.rb +108 -0
- data/lib/rigor/type/accepts_result.rb +109 -0
- data/lib/rigor/type/bot.rb +57 -0
- data/lib/rigor/type/combinator.rb +148 -0
- data/lib/rigor/type/constant.rb +90 -0
- data/lib/rigor/type/dynamic.rb +60 -0
- data/lib/rigor/type/hash_shape.rb +246 -0
- data/lib/rigor/type/nominal.rb +83 -0
- data/lib/rigor/type/singleton.rb +65 -0
- data/lib/rigor/type/top.rb +56 -0
- data/lib/rigor/type/tuple.rb +84 -0
- data/lib/rigor/type/union.rb +65 -0
- data/lib/rigor/type.rb +23 -0
- data/lib/rigor/version.rb +5 -0
- data/lib/rigor.rb +29 -0
- data/sig/rigor/analysis/fact_store.rbs +51 -0
- data/sig/rigor/ast.rbs +11 -0
- data/sig/rigor/environment.rbs +59 -0
- data/sig/rigor/inference.rbs +151 -0
- data/sig/rigor/rbs_extended.rbs +22 -0
- data/sig/rigor/scope.rbs +49 -0
- data/sig/rigor/source.rbs +20 -0
- data/sig/rigor/testing.rbs +9 -0
- data/sig/rigor/trinary.rbs +29 -0
- data/sig/rigor/type.rbs +171 -0
- data/sig/rigor.rbs +70 -0
- metadata +260 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Inference
|
|
5
|
+
FALLBACK_FAMILIES = %i[prism virtual].freeze
|
|
6
|
+
private_constant :FALLBACK_FAMILIES
|
|
7
|
+
|
|
8
|
+
# Immutable value object recorded by the typer whenever Scope#type_of
|
|
9
|
+
# falls back to Dynamic[Top] for a node it does not yet recognise. The
|
|
10
|
+
# contract for emitting these events lives in
|
|
11
|
+
# docs/internal-spec/inference-engine.md (Fail-Soft Policy).
|
|
12
|
+
#
|
|
13
|
+
# Fields:
|
|
14
|
+
# - node_class: the Ruby class of the node that triggered the
|
|
15
|
+
# fallback (e.g. Prism::CallNode, or a Rigor::AST::Node subclass).
|
|
16
|
+
# - location: the Prism source location for real Prism nodes, or
|
|
17
|
+
# nil for synthetic nodes.
|
|
18
|
+
# - family: :prism for real Prism nodes, :virtual for nodes
|
|
19
|
+
# that include Rigor::AST::Node.
|
|
20
|
+
# - inner_type: the Rigor::Type returned to the caller (currently
|
|
21
|
+
# always Dynamic[Top]; later slices may carry richer fallback
|
|
22
|
+
# types).
|
|
23
|
+
Fallback = Data.define(:node_class, :location, :family, :inner_type) do
|
|
24
|
+
def initialize(node_class:, location:, family:, inner_type:)
|
|
25
|
+
raise ArgumentError, "node_class must be a Class, got #{node_class.class}" unless node_class.is_a?(Class)
|
|
26
|
+
|
|
27
|
+
unless FALLBACK_FAMILIES.include?(family)
|
|
28
|
+
raise ArgumentError, "family must be one of #{FALLBACK_FAMILIES.inspect}, got #{family.inspect}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
super
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "fallback"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
# Mutable observer that accumulates Rigor::Inference::Fallback events
|
|
8
|
+
# emitted by the type-inference engine. Pass an instance to
|
|
9
|
+
# Rigor::Scope#type_of(node, tracer: ...) to record every fail-soft
|
|
10
|
+
# fallback. The tracer MUST NOT change the return value of type_of;
|
|
11
|
+
# see docs/internal-spec/inference-engine.md (Fail-Soft Policy).
|
|
12
|
+
#
|
|
13
|
+
# Future slices may add additional record_* methods (e.g.
|
|
14
|
+
# record_dispatch_miss for Slice 3, record_budget_cutoff for Slice 5);
|
|
15
|
+
# the namespaced method names exist so a single tracer can collect
|
|
16
|
+
# multiple event families without confusing them.
|
|
17
|
+
class FallbackTracer
|
|
18
|
+
def initialize
|
|
19
|
+
@events = []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def events
|
|
23
|
+
@events.dup.freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def record_fallback(event)
|
|
27
|
+
raise ArgumentError, "expected Rigor::Inference::Fallback, got #{event.class}" unless event.is_a?(Fallback)
|
|
28
|
+
|
|
29
|
+
@events << event
|
|
30
|
+
self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def empty?
|
|
34
|
+
@events.empty?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def size
|
|
38
|
+
@events.size
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def each(&block)
|
|
42
|
+
return @events.each unless block
|
|
43
|
+
|
|
44
|
+
@events.each(&block)
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
include Enumerable
|
|
49
|
+
|
|
50
|
+
def kinds
|
|
51
|
+
@events.map(&:node_class).uniq
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def families
|
|
55
|
+
@events.map(&:family).uniq
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def clear
|
|
59
|
+
@events.clear
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module MethodDispatcher
|
|
8
|
+
# Slice 2 rule book that folds binary operations on `Rigor::Type::Constant`
|
|
9
|
+
# receivers into another `Constant` whenever:
|
|
10
|
+
#
|
|
11
|
+
# * the receiver is a recognised scalar literal,
|
|
12
|
+
# * exactly one argument is supplied and it is also a `Constant`,
|
|
13
|
+
# * the method name is in the curated whitelist for the receiver's class,
|
|
14
|
+
# * the operation cannot accidentally explode the analyzer (we cap
|
|
15
|
+
# string-fold output at `STRING_FOLD_BYTE_LIMIT` bytes), and
|
|
16
|
+
# * the actual Ruby invocation does not raise.
|
|
17
|
+
#
|
|
18
|
+
# Anything else returns `nil`, signalling "no rule matched" so the
|
|
19
|
+
# caller (`ExpressionTyper`) falls back to `Dynamic[Top]` and records a
|
|
20
|
+
# fail-soft event. Slice 4 (RBS-backed) layers another dispatch tier
|
|
21
|
+
# behind this rule book, but the constant-folding semantics defined
|
|
22
|
+
# here MUST NOT regress: any value reachable by literal arithmetic at
|
|
23
|
+
# parse time is meant to be foldable independent of RBS data.
|
|
24
|
+
module ConstantFolding
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
NUMERIC_BINARY = Set[:+, :-, :*, :/, :%, :<, :<=, :>, :>=, :==, :!=, :<=>].freeze
|
|
28
|
+
STRING_BINARY = Set[:+, :*, :==, :!=, :<, :<=, :>, :>=, :<=>].freeze
|
|
29
|
+
SYMBOL_BINARY = Set[:==, :!=, :<=>, :<, :<=, :>, :>=].freeze
|
|
30
|
+
BOOL_BINARY = Set[:&, :|, :^, :==, :!=].freeze
|
|
31
|
+
NIL_BINARY = Set[:==, :!=].freeze
|
|
32
|
+
|
|
33
|
+
STRING_FOLD_BYTE_LIMIT = 4096
|
|
34
|
+
|
|
35
|
+
# @return [Rigor::Type::Constant, nil]
|
|
36
|
+
def try_fold(receiver:, method_name:, args:)
|
|
37
|
+
return nil unless receiver.is_a?(Type::Constant)
|
|
38
|
+
return nil if args.size != 1
|
|
39
|
+
|
|
40
|
+
arg = args.first
|
|
41
|
+
return nil unless arg.is_a?(Type::Constant)
|
|
42
|
+
return nil unless safe?(receiver.value, method_name, arg.value)
|
|
43
|
+
|
|
44
|
+
Type::Combinator.constant_of(receiver.value.public_send(method_name, arg.value))
|
|
45
|
+
rescue StandardError
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def safe?(receiver_value, method_name, arg_value)
|
|
50
|
+
ops = ops_for(receiver_value)
|
|
51
|
+
return false unless ops.include?(method_name)
|
|
52
|
+
return false if integer_division_by_zero?(receiver_value, method_name, arg_value)
|
|
53
|
+
return false if string_blow_up?(receiver_value, method_name, arg_value)
|
|
54
|
+
|
|
55
|
+
true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def ops_for(receiver_value)
|
|
59
|
+
case receiver_value
|
|
60
|
+
when Integer, Float then NUMERIC_BINARY
|
|
61
|
+
when String then STRING_BINARY
|
|
62
|
+
when Symbol then SYMBOL_BINARY
|
|
63
|
+
when true, false then BOOL_BINARY
|
|
64
|
+
when nil then NIL_BINARY
|
|
65
|
+
else Set.new
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Integer / 0 and Integer % 0 raise; Float / 0 and Float / 0.0 return
|
|
70
|
+
# Float::INFINITY or NaN, which are valid `Constant[Float]` values.
|
|
71
|
+
def integer_division_by_zero?(receiver_value, method_name, arg_value)
|
|
72
|
+
return false unless %i[/ %].include?(method_name)
|
|
73
|
+
return false unless receiver_value.is_a?(Integer)
|
|
74
|
+
|
|
75
|
+
arg_value.is_a?(Integer) && arg_value.zero?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def string_blow_up?(receiver_value, method_name, arg_value)
|
|
79
|
+
return false unless receiver_value.is_a?(String)
|
|
80
|
+
|
|
81
|
+
case method_name
|
|
82
|
+
when :+ then string_concat_blow_up?(receiver_value, arg_value)
|
|
83
|
+
when :* then string_repeat_blow_up?(receiver_value, arg_value)
|
|
84
|
+
else false
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def string_concat_blow_up?(receiver_value, arg_value)
|
|
89
|
+
arg_value.is_a?(String) &&
|
|
90
|
+
receiver_value.bytesize + arg_value.bytesize > STRING_FOLD_BYTE_LIMIT
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def string_repeat_blow_up?(receiver_value, arg_value)
|
|
94
|
+
return false unless arg_value.is_a?(Integer)
|
|
95
|
+
return true if arg_value.negative?
|
|
96
|
+
|
|
97
|
+
receiver_value.bytesize * arg_value > STRING_FOLD_BYTE_LIMIT
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
require_relative "../acceptance"
|
|
5
|
+
require_relative "../rbs_type_translator"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Inference
|
|
9
|
+
module MethodDispatcher
|
|
10
|
+
# Picks the RBS overload that should answer a call given the
|
|
11
|
+
# caller's actual argument types. Slice 4 phase 2c shape:
|
|
12
|
+
#
|
|
13
|
+
# 1. Filter overloads by positional arity (required, optional and
|
|
14
|
+
# rest_positionals are honored; required_keywords disqualify the
|
|
15
|
+
# overload because we do not yet thread keyword args through
|
|
16
|
+
# `call_arg_types`).
|
|
17
|
+
# 2. Within the arity-matching overloads, accept the first one
|
|
18
|
+
# whose every (param, arg) pair returns a `yes` or `maybe`
|
|
19
|
+
# answer from `Rigor::Type#accepts(arg, mode: :gradual)`.
|
|
20
|
+
# 3. If no overload matches, fall back to `method_types.first`
|
|
21
|
+
# so existing call sites keep their phase 1 / 2b behavior.
|
|
22
|
+
# This preserves the fail-soft invariant of the dispatcher.
|
|
23
|
+
#
|
|
24
|
+
# The selector is intentionally agnostic about the dispatch kind
|
|
25
|
+
# (instance vs singleton). Both kinds share the same arity and
|
|
26
|
+
# acceptance shape; the difference is only in which `Definition`
|
|
27
|
+
# the caller fetched.
|
|
28
|
+
module OverloadSelector
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
# @param method_definition [RBS::Definition::Method]
|
|
32
|
+
# @param arg_types [Array<Rigor::Type>] caller-provided types in
|
|
33
|
+
# positional order. Empty when there are no arguments.
|
|
34
|
+
# @param self_type [Rigor::Type] substitute for `Bases::Self`.
|
|
35
|
+
# @param instance_type [Rigor::Type] substitute for `Bases::Instance`.
|
|
36
|
+
# @param type_vars [Hash{Symbol => Rigor::Type}] substitution map
|
|
37
|
+
# for class-level type variables (Slice 4 phase 2d). The
|
|
38
|
+
# selector threads it through to {RbsTypeTranslator} so
|
|
39
|
+
# parameter types like `::Array[Elem]` substitute Elem before
|
|
40
|
+
# the accepts check, instead of degrading the param to
|
|
41
|
+
# `Array[Dynamic[Top]]`.
|
|
42
|
+
# @param block_required [Boolean] when `true`, only overloads
|
|
43
|
+
# that declare a block clause are considered (Slice 6 phase C
|
|
44
|
+
# sub-phase 1). The fallback also prefers a block-bearing
|
|
45
|
+
# overload over `method_types.first`. When `false` (the
|
|
46
|
+
# Slice 4 phase 2c default) the selector behaves exactly as
|
|
47
|
+
# before: `find` over arity-compatible overloads, falling
|
|
48
|
+
# back to the first declaration.
|
|
49
|
+
# @return [RBS::MethodType, nil] the chosen overload, or nil
|
|
50
|
+
# when the definition has no method types at all.
|
|
51
|
+
# rubocop:disable Metrics/ParameterLists
|
|
52
|
+
def select(method_definition, arg_types:, self_type:, instance_type:, type_vars: {}, block_required: false)
|
|
53
|
+
overloads = method_definition.method_types
|
|
54
|
+
return nil if overloads.empty?
|
|
55
|
+
|
|
56
|
+
match = find_matching_overload(
|
|
57
|
+
overloads,
|
|
58
|
+
arg_types: arg_types,
|
|
59
|
+
self_type: self_type,
|
|
60
|
+
instance_type: instance_type,
|
|
61
|
+
type_vars: type_vars,
|
|
62
|
+
block_required: block_required
|
|
63
|
+
)
|
|
64
|
+
return match if match
|
|
65
|
+
return overloads.find { |mt| overload_has_block?(mt) } if block_required
|
|
66
|
+
|
|
67
|
+
overloads.first
|
|
68
|
+
end
|
|
69
|
+
# rubocop:enable Metrics/ParameterLists
|
|
70
|
+
|
|
71
|
+
def overload_has_block?(method_type)
|
|
72
|
+
method_type.respond_to?(:block) && method_type.block
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class << self
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# rubocop:disable Metrics/ParameterLists
|
|
79
|
+
def find_matching_overload(overloads, arg_types:, self_type:, instance_type:, type_vars:, block_required:)
|
|
80
|
+
overloads.find do |method_type|
|
|
81
|
+
next false if block_required && !OverloadSelector.overload_has_block?(method_type)
|
|
82
|
+
|
|
83
|
+
matches?(
|
|
84
|
+
method_type,
|
|
85
|
+
arg_types,
|
|
86
|
+
self_type: self_type,
|
|
87
|
+
instance_type: instance_type,
|
|
88
|
+
type_vars: type_vars
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
# rubocop:enable Metrics/ParameterLists
|
|
93
|
+
|
|
94
|
+
def matches?(method_type, arg_types, self_type:, instance_type:, type_vars:)
|
|
95
|
+
return false if method_type.respond_to?(:type_params) && rejects_keyword_required?(method_type)
|
|
96
|
+
|
|
97
|
+
fun = method_type.type
|
|
98
|
+
return false unless arity_compatible?(fun, arg_types.size)
|
|
99
|
+
|
|
100
|
+
params = positional_params_for(fun, arg_types.size)
|
|
101
|
+
params.zip(arg_types).all? do |param, arg|
|
|
102
|
+
accepts_param?(
|
|
103
|
+
param,
|
|
104
|
+
arg,
|
|
105
|
+
self_type: self_type,
|
|
106
|
+
instance_type: instance_type,
|
|
107
|
+
type_vars: type_vars
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Slice 4 phase 2c does not pass keyword arguments through the
|
|
113
|
+
# call site (caller passes only positional `arg_types`). An
|
|
114
|
+
# overload that requires keywords is therefore not a viable
|
|
115
|
+
# candidate; we skip it instead of forcing a fallback.
|
|
116
|
+
def rejects_keyword_required?(method_type)
|
|
117
|
+
fun = method_type.type
|
|
118
|
+
return false unless fun.respond_to?(:required_keywords)
|
|
119
|
+
|
|
120
|
+
!fun.required_keywords.empty?
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def arity_compatible?(fun, actual_count)
|
|
124
|
+
min_arity = fun.required_positionals.size + fun.trailing_positionals.size
|
|
125
|
+
return false if actual_count < min_arity
|
|
126
|
+
|
|
127
|
+
return true if fun.rest_positionals
|
|
128
|
+
|
|
129
|
+
max_arity = min_arity + fun.optional_positionals.size
|
|
130
|
+
actual_count <= max_arity
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Builds the list of formal parameter declarations to compare
|
|
134
|
+
# against the actual arguments, in positional order: required
|
|
135
|
+
# first, then as many optionals as needed, then trailing
|
|
136
|
+
# required. Rest_positionals consumes the remainder; we
|
|
137
|
+
# repeat its single declaration for each absorbed argument.
|
|
138
|
+
def positional_params_for(fun, actual_count)
|
|
139
|
+
required = fun.required_positionals
|
|
140
|
+
optional = fun.optional_positionals
|
|
141
|
+
rest = fun.rest_positionals
|
|
142
|
+
trailing = fun.trailing_positionals
|
|
143
|
+
|
|
144
|
+
head = required.dup
|
|
145
|
+
optional_needed = [actual_count - head.size - trailing.size, 0].max
|
|
146
|
+
head.concat(optional.first(optional_needed))
|
|
147
|
+
|
|
148
|
+
absorbed_by_rest = actual_count - head.size - trailing.size
|
|
149
|
+
head.concat([rest] * absorbed_by_rest) if rest && absorbed_by_rest.positive?
|
|
150
|
+
|
|
151
|
+
head.concat(trailing)
|
|
152
|
+
head
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def accepts_param?(param, arg, self_type:, instance_type:, type_vars:)
|
|
156
|
+
param_type = RbsTypeTranslator.translate(
|
|
157
|
+
param.type,
|
|
158
|
+
self_type: self_type,
|
|
159
|
+
instance_type: instance_type,
|
|
160
|
+
type_vars: type_vars
|
|
161
|
+
)
|
|
162
|
+
result = param_type.accepts(arg, mode: :gradual)
|
|
163
|
+
result.yes? || result.maybe?
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|