plumb 0.0.18 → 0.2.0.beta.2
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 +887 -64
- data/bench/compare_dry_schema.rb +79 -0
- data/bench/compare_dry_types.rb +37 -0
- data/bench/compare_parametric_schema.rb +2 -80
- data/bench/dry_schema_hash.rb +103 -0
- data/bench/dry_types_hash.rb +125 -0
- data/bench/json_schema_profile.rb +107 -0
- data/bench/plumb_hash.rb +17 -11
- data/bench/results_allocations.rb +137 -0
- data/bench/sample_data.rb +78 -0
- data/examples/command_objects.rb +1 -1
- data/examples/concurrent_downloads.rb +16 -9
- data/examples/event_registry.rb +6 -1
- data/examples/weekdays.rb +1 -1
- data/lib/plumb/and.rb +63 -6
- data/lib/plumb/any_class.rb +12 -2
- data/lib/plumb/array_class.rb +133 -25
- data/lib/plumb/attribute_value_match.rb +41 -1
- data/lib/plumb/attributes.rb +59 -19
- data/lib/plumb/codec.rb +886 -0
- data/lib/plumb/composable.rb +451 -39
- data/lib/plumb/conjunction.rb +50 -0
- data/lib/plumb/constraint.rb +234 -0
- data/lib/plumb/covariant_fusion.rb +46 -0
- data/lib/plumb/decorator.rb +12 -22
- data/lib/plumb/deferred.rb +13 -5
- data/lib/plumb/disjunction.rb +112 -0
- data/lib/plumb/encoder.rb +207 -0
- data/lib/plumb/function.rb +347 -0
- data/lib/plumb/hash_class.rb +339 -32
- data/lib/plumb/hash_map.rb +58 -14
- data/lib/plumb/implementation.rb +247 -0
- data/lib/plumb/interface_class.rb +21 -2
- data/lib/plumb/intersection.rb +47 -0
- data/lib/plumb/json_schema_visitor.rb +255 -36
- data/lib/plumb/key.rb +63 -13
- data/lib/plumb/mermaid_visitor.rb +129 -0
- data/lib/plumb/metadata.rb +10 -1
- data/lib/plumb/metadata_visitor.rb +36 -34
- data/lib/plumb/never_class.rb +38 -0
- data/lib/plumb/node_mapper.rb +97 -0
- data/lib/plumb/not.rb +34 -2
- data/lib/plumb/optimizer.rb +444 -0
- data/lib/plumb/or.rb +26 -29
- data/lib/plumb/pipeline.rb +99 -11
- data/lib/plumb/policy.rb +17 -4
- data/lib/plumb/range_class.rb +46 -0
- data/lib/plumb/relation.rb +57 -0
- data/lib/plumb/result.rb +55 -23
- data/lib/plumb/semantic_matcher.rb +393 -0
- data/lib/plumb/static_class.rb +20 -1
- data/lib/plumb/stream_class.rb +28 -6
- data/lib/plumb/subtyping.rb +461 -0
- data/lib/plumb/tagged_hash.rb +45 -4
- data/lib/plumb/tuple_class.rb +21 -4
- data/lib/plumb/type_cache.rb +41 -0
- data/lib/plumb/type_registry.rb +71 -0
- data/lib/plumb/typed_step.rb +67 -0
- data/lib/plumb/types.rb +44 -43
- data/lib/plumb/union.rb +30 -0
- data/lib/plumb/value_class.rb +20 -1
- data/lib/plumb/version.rb +1 -1
- data/lib/plumb/visitor_handlers.rb +20 -4
- data/lib/plumb.rb +90 -3
- metadata +30 -8
- data/lib/plumb/build.rb +0 -22
- data/lib/plumb/match_class.rb +0 -42
- data/lib/plumb/schema.rb +0 -195
- data/lib/plumb/step.rb +0 -27
- data/lib/plumb/transform.rb +0 -26
data/lib/plumb/policy.rb
CHANGED
|
@@ -14,6 +14,9 @@ module Plumb
|
|
|
14
14
|
# @param policy_name [Symbol]
|
|
15
15
|
# @param arg [Object, nil] the argument to the policy, if any.
|
|
16
16
|
# @param step [Step] the step composition wrapped by this policy.
|
|
17
|
+
# @see Plumb::NodeMapper
|
|
18
|
+
def with_children(children) = self.class.new(policy_name, arg, children.first)
|
|
19
|
+
|
|
17
20
|
def initialize(policy_name, arg, step)
|
|
18
21
|
@policy_name = policy_name
|
|
19
22
|
@arg = arg
|
|
@@ -22,15 +25,25 @@ module Plumb
|
|
|
22
25
|
freeze
|
|
23
26
|
end
|
|
24
27
|
|
|
28
|
+
# Policy equality includes the wrapped step; otherwise same-named policies on
|
|
29
|
+
# disjoint types would compare equal and one could be reduced away.
|
|
30
|
+
# @param other [Object]
|
|
31
|
+
# @return [Boolean]
|
|
25
32
|
def ==(other)
|
|
26
|
-
other.
|
|
33
|
+
other.instance_of?(self.class) &&
|
|
27
34
|
policy_name == other.policy_name &&
|
|
28
|
-
arg == other.arg
|
|
35
|
+
arg == other.arg &&
|
|
36
|
+
children == other.children
|
|
29
37
|
end
|
|
30
38
|
|
|
39
|
+
# A Policy is a transparent wrapper: it delegates type-flow to the wrapped
|
|
40
|
+
# step.
|
|
41
|
+
def input_type = @step.input_type
|
|
42
|
+
def output_type = @step.output_type
|
|
43
|
+
|
|
31
44
|
# The standard Step interface.
|
|
32
|
-
# @param result [Result
|
|
33
|
-
# @return [Result
|
|
45
|
+
# @param result [Result]
|
|
46
|
+
# @return [Result]
|
|
34
47
|
def call(result) = @step.call(result)
|
|
35
48
|
|
|
36
49
|
private def _inspect = @step.inspect
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'plumb/composable'
|
|
4
|
+
|
|
5
|
+
module Plumb
|
|
6
|
+
class RangeClass
|
|
7
|
+
include Composable
|
|
8
|
+
|
|
9
|
+
attr_reader :children
|
|
10
|
+
|
|
11
|
+
def initialize(member_type = Types::Any)
|
|
12
|
+
@member_type = Composable.wrap(member_type)
|
|
13
|
+
@children = [@member_type].freeze
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def of(member_type)
|
|
18
|
+
self.class.new(member_type)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
alias [] of
|
|
22
|
+
|
|
23
|
+
# #call only validates the Range's endpoints against the member type — it
|
|
24
|
+
# never coerces or rebuilds the Range — so a Range type always returns its
|
|
25
|
+
# input unchanged, regardless of member. This lets `#|` absorb a narrower
|
|
26
|
+
# branch (`Range[Integer] | Range[1..10]` -> `Range[Integer]`) and `#>>`
|
|
27
|
+
# drop a redundant re-check.
|
|
28
|
+
def value_preserving? = true
|
|
29
|
+
|
|
30
|
+
def call(result)
|
|
31
|
+
return result.invalid!(errors: 'must be a Range') unless result.value.is_a?(::Range)
|
|
32
|
+
|
|
33
|
+
range = result.value
|
|
34
|
+
[range.begin, range.end].compact.each do |endpoint|
|
|
35
|
+
r = @member_type.resolve(endpoint)
|
|
36
|
+
return result.invalid!(errors: r.errors) unless r.valid?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
result
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def _inspect = %(Range[#{@member_type}])
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plumb
|
|
4
|
+
# Internal three-valued result for propositions the type algebra attempts to
|
|
5
|
+
# prove. Unknown is distinct from false so callers cannot turn missing
|
|
6
|
+
# knowledge into an unsafe rewrite.
|
|
7
|
+
class Relation
|
|
8
|
+
class << self
|
|
9
|
+
# Converts a known predicate result to a proof.
|
|
10
|
+
# @param value [Object]
|
|
11
|
+
# @return [Relation]
|
|
12
|
+
def from(value) = value ? PROVEN : DISPROVEN
|
|
13
|
+
|
|
14
|
+
# Combines proofs for a logical conjunction.
|
|
15
|
+
# @param relations [Enumerable<Relation>]
|
|
16
|
+
# @return [Relation]
|
|
17
|
+
def all(relations)
|
|
18
|
+
unknown = false
|
|
19
|
+
relations.each do |relation|
|
|
20
|
+
return DISPROVEN if relation.disproven?
|
|
21
|
+
|
|
22
|
+
unknown ||= relation.unknown?
|
|
23
|
+
end
|
|
24
|
+
unknown ? UNKNOWN : PROVEN
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Combines proofs for a logical disjunction.
|
|
28
|
+
# @param relations [Enumerable<Relation>]
|
|
29
|
+
# @return [Relation]
|
|
30
|
+
def any(relations)
|
|
31
|
+
unknown = false
|
|
32
|
+
relations.each do |relation|
|
|
33
|
+
return PROVEN if relation.proven?
|
|
34
|
+
|
|
35
|
+
unknown ||= relation.unknown?
|
|
36
|
+
end
|
|
37
|
+
unknown ? UNKNOWN : DISPROVEN
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private :new
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @return [Boolean]
|
|
44
|
+
def proven? = equal?(PROVEN)
|
|
45
|
+
|
|
46
|
+
# @return [Boolean]
|
|
47
|
+
def disproven? = equal?(DISPROVEN)
|
|
48
|
+
|
|
49
|
+
# @return [Boolean]
|
|
50
|
+
def unknown? = equal?(UNKNOWN)
|
|
51
|
+
|
|
52
|
+
PROVEN = new.freeze
|
|
53
|
+
DISPROVEN = new.freeze
|
|
54
|
+
UNKNOWN = new.freeze
|
|
55
|
+
end
|
|
56
|
+
private_constant :Relation
|
|
57
|
+
end
|
data/lib/plumb/result.rb
CHANGED
|
@@ -1,14 +1,38 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Plumb
|
|
4
|
+
# The value + validity + errors triple that flows through a composition.
|
|
5
|
+
#
|
|
6
|
+
# There are two ways to derive one result from another:
|
|
7
|
+
#
|
|
8
|
+
# - #valid / #invalid ALLOCATE a fresh Result and leave the receiver
|
|
9
|
+
# untouched. This is the safe, functional form: use it in custom types, and
|
|
10
|
+
# anywhere a result may outlive the call or be captured (Streams close over
|
|
11
|
+
# it lazily; concurrent Arrays hand results between threads). This is the
|
|
12
|
+
# public API for user-defined steps.
|
|
13
|
+
#
|
|
14
|
+
# - #valid! / #invalid! FLIP THE RECEIVER in place and return it (no
|
|
15
|
+
# allocation). Plumb's own built-in steps use these on the hot path to
|
|
16
|
+
# avoid a Result per node — but only where the input cursor is theirs to
|
|
17
|
+
# mutate: never on a shared/constant Result, never when the result escapes
|
|
18
|
+
# into a closure or another thread. The one fork point (Or) snapshots the
|
|
19
|
+
# value and #resets before retrying its second branch.
|
|
20
|
+
#
|
|
21
|
+
# So built-ins stay allocation-lean while user code (and the concurrency- and
|
|
22
|
+
# laziness-sensitive built-ins) keep the footgun-free copying semantics, at the
|
|
23
|
+
# cost of a few extra allocations there.
|
|
24
|
+
#
|
|
25
|
+
# Valid and Invalid were once separate subclasses; collapsing them into one
|
|
26
|
+
# class with a boolean is what makes the in-place valid<->invalid flip possible
|
|
27
|
+
# (Ruby can't change an object's class).
|
|
4
28
|
class Result
|
|
5
29
|
class << self
|
|
6
|
-
def valid(value)
|
|
7
|
-
|
|
30
|
+
def valid(value = nil)
|
|
31
|
+
new(value, valid: true)
|
|
8
32
|
end
|
|
9
33
|
|
|
10
34
|
def invalid(value = nil, errors: nil)
|
|
11
|
-
|
|
35
|
+
new(value, valid: false, errors:)
|
|
12
36
|
end
|
|
13
37
|
|
|
14
38
|
def wrap(value)
|
|
@@ -20,24 +44,20 @@ module Plumb
|
|
|
20
44
|
|
|
21
45
|
attr_reader :value, :errors
|
|
22
46
|
|
|
23
|
-
def initialize(value, errors: nil)
|
|
47
|
+
def initialize(value, valid: true, errors: nil)
|
|
24
48
|
@value = value
|
|
49
|
+
@valid = valid
|
|
25
50
|
@errors = errors
|
|
26
51
|
end
|
|
27
52
|
|
|
28
|
-
def valid? =
|
|
29
|
-
def invalid? =
|
|
53
|
+
def valid? = @valid
|
|
54
|
+
def invalid? = !@valid
|
|
30
55
|
|
|
31
56
|
def inspect
|
|
32
|
-
%(<#{self.class}##{object_id} value:#{value.inspect} errors:#{errors.inspect}>)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def reset(val)
|
|
36
|
-
@value = val
|
|
37
|
-
@errors = nil
|
|
38
|
-
self
|
|
57
|
+
%(<#{self.class}##{object_id} valid:#{@valid} value:#{value.inspect} errors:#{errors.inspect}>)
|
|
39
58
|
end
|
|
40
59
|
|
|
60
|
+
# Allocating, copy-returning form. Safe everywhere: the receiver is untouched.
|
|
41
61
|
def valid(val = value)
|
|
42
62
|
Result.valid(val)
|
|
43
63
|
end
|
|
@@ -46,19 +66,31 @@ module Plumb
|
|
|
46
66
|
Result.invalid(val, errors:)
|
|
47
67
|
end
|
|
48
68
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
69
|
+
# In-place form: flip THIS result and return self. No allocation. Only for
|
|
70
|
+
# code that owns the cursor (see the class docstring).
|
|
71
|
+
def valid!(val = value)
|
|
72
|
+
@value = val
|
|
73
|
+
@valid = true
|
|
74
|
+
@errors = nil
|
|
75
|
+
self
|
|
53
76
|
end
|
|
54
77
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
78
|
+
def invalid!(val = value, errors: nil)
|
|
79
|
+
@value = val
|
|
80
|
+
@valid = false
|
|
81
|
+
@errors = errors
|
|
82
|
+
self
|
|
83
|
+
end
|
|
58
84
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
85
|
+
# Reset the cursor to a fresh VALID state carrying `val`, in place. The
|
|
86
|
+
# mutating scratch-reuse primitive behind the HashClass/ArrayClass element
|
|
87
|
+
# loops (same as #valid!(val)).
|
|
88
|
+
def reset(val)
|
|
89
|
+
valid!(val)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def map(callable)
|
|
93
|
+
@valid ? callable.call(self) : self
|
|
62
94
|
end
|
|
63
95
|
end
|
|
64
96
|
end
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
5
|
+
module Plumb
|
|
6
|
+
# Internal semantic form of Ruby `#===` matchers. Raw matchers are normalized
|
|
7
|
+
# once at a Constraint boundary; nodes then collaborate polymorphically.
|
|
8
|
+
module SemanticMatcher
|
|
9
|
+
class Merge
|
|
10
|
+
attr_reader :node
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def merged(node) = new(:merged, node)
|
|
14
|
+
|
|
15
|
+
private :new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(state, node = nil)
|
|
19
|
+
@state = state
|
|
20
|
+
@node = node
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def merged? = @state == :merged
|
|
25
|
+
def empty? = equal?(EMPTY)
|
|
26
|
+
def unknown? = equal?(UNKNOWN)
|
|
27
|
+
|
|
28
|
+
EMPTY = new(:empty)
|
|
29
|
+
UNKNOWN = new(:unknown)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class Node
|
|
33
|
+
attr_reader :raw
|
|
34
|
+
|
|
35
|
+
def initialize(raw)
|
|
36
|
+
@raw = raw
|
|
37
|
+
freeze
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def kind = :opaque
|
|
41
|
+
def singleton? = false
|
|
42
|
+
def nominal? = false
|
|
43
|
+
def matcher_domain = []
|
|
44
|
+
def value_domain = raw.class
|
|
45
|
+
def error_message = "Must match #{raw.inspect}"
|
|
46
|
+
def matches_value(_candidate) = Relation::UNKNOWN
|
|
47
|
+
|
|
48
|
+
def equivalent?(other)
|
|
49
|
+
other.instance_of?(self.class) && raw == other.raw
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def subset_of(other)
|
|
53
|
+
return Relation::PROVEN if equivalent?(other)
|
|
54
|
+
|
|
55
|
+
other.subset_from_opaque(self)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def subset_from_opaque(_other) = Relation::UNKNOWN
|
|
59
|
+
def subset_from_nominal(_other) = Relation::UNKNOWN
|
|
60
|
+
def subset_from_literal(_other) = Relation::UNKNOWN
|
|
61
|
+
def subset_from_text_literal(other) = subset_from_literal(other)
|
|
62
|
+
def subset_from_numeric_literal(other) = subset_from_literal(other)
|
|
63
|
+
def subset_from_finite_set(_other) = Relation::UNKNOWN
|
|
64
|
+
def subset_from_range(_other) = Relation::UNKNOWN
|
|
65
|
+
def subset_from_pattern(_other) = Relation::UNKNOWN
|
|
66
|
+
|
|
67
|
+
def overlap(other)
|
|
68
|
+
return Relation::PROVEN if equivalent?(other)
|
|
69
|
+
|
|
70
|
+
other.overlap_with_opaque(self)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def overlap_with_opaque(_other) = Relation::UNKNOWN
|
|
74
|
+
def overlap_with_nominal(_other) = Relation::UNKNOWN
|
|
75
|
+
def overlap_with_literal(_other) = Relation::UNKNOWN
|
|
76
|
+
def overlap_with_text_literal(other) = overlap_with_literal(other)
|
|
77
|
+
def overlap_with_numeric_literal(other) = overlap_with_literal(other)
|
|
78
|
+
def overlap_with_finite_set(_other) = Relation::UNKNOWN
|
|
79
|
+
def overlap_with_range(_other) = Relation::UNKNOWN
|
|
80
|
+
def overlap_with_pattern(_other) = Relation::UNKNOWN
|
|
81
|
+
|
|
82
|
+
def intersect(_other) = Merge::UNKNOWN
|
|
83
|
+
def intersect_with_finite_set(_other) = Merge::UNKNOWN
|
|
84
|
+
def intersect_with_range(_other) = Merge::UNKNOWN
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
class Opaque < Node
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
class CollectionMatcher < Opaque
|
|
91
|
+
def kind = :collection_matcher
|
|
92
|
+
def error_message = "Must be equal to #{raw}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
class Literal < Node
|
|
96
|
+
def kind = :literal
|
|
97
|
+
def singleton? = true
|
|
98
|
+
def matcher_domain = [value_domain]
|
|
99
|
+
def matches_value(candidate) = Relation.from(raw === candidate)
|
|
100
|
+
def error_message = "Must be equal to #{raw}"
|
|
101
|
+
|
|
102
|
+
def subset_of(other)
|
|
103
|
+
return Relation::PROVEN if equivalent?(other)
|
|
104
|
+
|
|
105
|
+
other.subset_from_literal(self)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def subset_from_literal(_other) = Relation::DISPROVEN
|
|
109
|
+
|
|
110
|
+
def overlap(other)
|
|
111
|
+
return Relation::PROVEN if equivalent?(other)
|
|
112
|
+
|
|
113
|
+
other.overlap_with_literal(self)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def overlap_with_literal(_other) = Relation::DISPROVEN
|
|
117
|
+
def overlap_with_nominal(other) = other.overlap_with_literal(self)
|
|
118
|
+
def overlap_with_range(other) = other.overlap_with_literal(self)
|
|
119
|
+
def overlap_with_pattern(other) = other.overlap_with_literal(self)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
class TextLiteral < Literal
|
|
123
|
+
def kind = :text_literal
|
|
124
|
+
|
|
125
|
+
def subset_of(other)
|
|
126
|
+
return Relation::PROVEN if equivalent?(other)
|
|
127
|
+
|
|
128
|
+
other.subset_from_text_literal(self)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def overlap(other)
|
|
132
|
+
return Relation::PROVEN if equivalent?(other)
|
|
133
|
+
|
|
134
|
+
other.overlap_with_text_literal(self)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
class NumericLiteral < Literal
|
|
139
|
+
def kind = :numeric_literal
|
|
140
|
+
def value_domain = ::Numeric
|
|
141
|
+
|
|
142
|
+
def subset_of(other)
|
|
143
|
+
return Relation::PROVEN if equivalent?(other)
|
|
144
|
+
|
|
145
|
+
other.subset_from_numeric_literal(self)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def overlap(other)
|
|
149
|
+
return Relation::PROVEN if equivalent?(other)
|
|
150
|
+
|
|
151
|
+
other.overlap_with_numeric_literal(self)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
class Nominal < Node
|
|
156
|
+
def kind = :nominal
|
|
157
|
+
def nominal? = true
|
|
158
|
+
def matcher_domain = raw.is_a?(::Class) ? [raw] : []
|
|
159
|
+
def matches_value(candidate) = Relation.from(raw === candidate)
|
|
160
|
+
|
|
161
|
+
def error_message
|
|
162
|
+
raw.is_a?(::Class) ? "Must be a #{raw}" : super
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def subset_of(other)
|
|
166
|
+
return Relation::PROVEN if equivalent?(other)
|
|
167
|
+
|
|
168
|
+
other.subset_from_nominal(self)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def subset_from_nominal(other) = Relation.from((other.raw <= raw) == true)
|
|
172
|
+
def subset_from_literal(other) = matches_value(other.raw)
|
|
173
|
+
def subset_from_numeric_literal(_other) = SemanticMatcher.wrap(::Numeric).subset_of(self)
|
|
174
|
+
def subset_from_finite_set(other) = other.members_match(self)
|
|
175
|
+
def subset_from_range(other) = other.endpoints_within(self)
|
|
176
|
+
|
|
177
|
+
def subset_from_pattern(other)
|
|
178
|
+
Relation.all(other.matcher_domain.map { |domain| SemanticMatcher.wrap(domain).subset_of(self) })
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def overlap(other)
|
|
182
|
+
return Relation::PROVEN if equivalent?(other)
|
|
183
|
+
|
|
184
|
+
other.overlap_with_nominal(self)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def overlap_with_nominal(other)
|
|
188
|
+
return Relation::PROVEN if subset_from_nominal(other).proven? || other.subset_from_nominal(self).proven?
|
|
189
|
+
return Relation::DISPROVEN if raw.is_a?(::Class) && other.raw.is_a?(::Class)
|
|
190
|
+
|
|
191
|
+
Relation::UNKNOWN # unrelated mixins may share an implementing class
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def overlap_with_literal(other) = Relation.from(other.raw.is_a?(raw))
|
|
195
|
+
def overlap_with_range(other) = other.overlap_with_nominal(self)
|
|
196
|
+
def overlap_with_pattern(other) = other.overlap_with_nominal(self)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
class FiniteSet < Node
|
|
200
|
+
def kind = :finite_set
|
|
201
|
+
def matcher_domain = raw.map(&:class).uniq
|
|
202
|
+
def matches_value(candidate) = Relation.from(raw === candidate)
|
|
203
|
+
|
|
204
|
+
def subset_of(other)
|
|
205
|
+
return Relation::PROVEN if equivalent?(other)
|
|
206
|
+
|
|
207
|
+
other.subset_from_finite_set(self)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def subset_from_literal(other) = matches_value(other.raw)
|
|
211
|
+
def subset_from_numeric_literal(_other) = Relation::DISPROVEN
|
|
212
|
+
def subset_from_finite_set(other) = Relation.from(other.raw.subset?(raw))
|
|
213
|
+
|
|
214
|
+
def members_match(other)
|
|
215
|
+
Relation.all(raw.map { |member| other.matches_value(member) })
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def overlap(other)
|
|
219
|
+
return Relation::PROVEN if equivalent?(other)
|
|
220
|
+
|
|
221
|
+
other.overlap_with_finite_set(self)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def overlap_with_finite_set(other)
|
|
225
|
+
Relation.from(!(raw & other.raw).empty?)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def intersect(other) = other.intersect_with_finite_set(self)
|
|
229
|
+
|
|
230
|
+
def intersect_with_finite_set(other)
|
|
231
|
+
merged = raw & other.raw
|
|
232
|
+
merged.empty? ? Merge::EMPTY : Merge.merged(self.class.new(merged))
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
class Interval < Node
|
|
237
|
+
def kind = :range
|
|
238
|
+
|
|
239
|
+
def matcher_domain
|
|
240
|
+
endpoint = raw.begin || raw.end
|
|
241
|
+
endpoint.nil? ? [] : [SemanticMatcher.value_domain(endpoint)]
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def matches_value(candidate) = Relation.from(raw === candidate)
|
|
245
|
+
def error_message = "Must be within #{raw}"
|
|
246
|
+
|
|
247
|
+
def subset_of(other)
|
|
248
|
+
return Relation::PROVEN if equivalent?(other)
|
|
249
|
+
|
|
250
|
+
other.subset_from_range(self)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def subset_from_literal(other) = matches_value(other.raw)
|
|
254
|
+
def subset_from_finite_set(other) = other.members_match(self)
|
|
255
|
+
|
|
256
|
+
def subset_from_range(other)
|
|
257
|
+
lo = if raw.begin.nil?
|
|
258
|
+
Relation::PROVEN
|
|
259
|
+
elsif other.raw.begin.nil?
|
|
260
|
+
Relation::DISPROVEN
|
|
261
|
+
else
|
|
262
|
+
Relation.from(raw.cover?(other.raw.begin))
|
|
263
|
+
end
|
|
264
|
+
Relation.all([lo, other.end_within(self)])
|
|
265
|
+
rescue ::ArgumentError, ::TypeError
|
|
266
|
+
Relation::UNKNOWN
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def endpoints_within(nominal)
|
|
270
|
+
endpoints = [raw.begin, raw.end].compact
|
|
271
|
+
return Relation::UNKNOWN if endpoints.empty?
|
|
272
|
+
|
|
273
|
+
Relation.all(endpoints.map { |endpoint| nominal.matches_value(endpoint) })
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def end_within(outer)
|
|
277
|
+
return Relation::PROVEN if outer.raw.end.nil?
|
|
278
|
+
return Relation::DISPROVEN if raw.end.nil?
|
|
279
|
+
|
|
280
|
+
cmp = raw.end <=> outer.raw.end
|
|
281
|
+
return Relation::UNKNOWN if cmp.nil?
|
|
282
|
+
|
|
283
|
+
Relation.from(!raw.exclude_end? && outer.raw.exclude_end? ? cmp.negative? : cmp <= 0)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def overlap(other)
|
|
287
|
+
return Relation::PROVEN if equivalent?(other)
|
|
288
|
+
|
|
289
|
+
other.overlap_with_range(self)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def overlap_with_nominal(other)
|
|
293
|
+
endpoint = raw.begin || raw.end
|
|
294
|
+
endpoint.nil? ? Relation::UNKNOWN : other.matches_value(endpoint)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def overlap_with_literal(other) = matches_value(other.raw)
|
|
298
|
+
|
|
299
|
+
def overlap_with_range(other)
|
|
300
|
+
merged = intersect_with_range(other)
|
|
301
|
+
return Relation::DISPROVEN if merged.empty?
|
|
302
|
+
return Relation::PROVEN if merged.merged?
|
|
303
|
+
|
|
304
|
+
Relation::UNKNOWN
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def intersect(other) = other.intersect_with_range(self)
|
|
308
|
+
|
|
309
|
+
def intersect_with_range(other)
|
|
310
|
+
ab = raw.begin
|
|
311
|
+
bb = other.raw.begin
|
|
312
|
+
new_begin = ab.nil? ? bb : (bb.nil? ? ab : (ab >= bb ? ab : bb))
|
|
313
|
+
ae = raw.end
|
|
314
|
+
be = other.raw.end
|
|
315
|
+
new_end, new_exclude =
|
|
316
|
+
if ae.nil? then [be, other.raw.exclude_end?]
|
|
317
|
+
elsif be.nil? then [ae, raw.exclude_end?]
|
|
318
|
+
elsif ae < be then [ae, raw.exclude_end?]
|
|
319
|
+
elsif be < ae then [be, other.raw.exclude_end?]
|
|
320
|
+
else [ae, raw.exclude_end? || other.raw.exclude_end?]
|
|
321
|
+
end
|
|
322
|
+
unless new_begin.nil? || new_end.nil?
|
|
323
|
+
return Merge::EMPTY if new_begin > new_end
|
|
324
|
+
return Merge::EMPTY if new_begin == new_end && new_exclude
|
|
325
|
+
end
|
|
326
|
+
Merge.merged(self.class.new(::Range.new(new_begin, new_end, new_exclude)))
|
|
327
|
+
rescue ::ArgumentError, ::TypeError
|
|
328
|
+
Merge::UNKNOWN
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
class Pattern < Node
|
|
333
|
+
def kind = :pattern
|
|
334
|
+
def matcher_domain = [::String, ::Symbol]
|
|
335
|
+
def matches_value(candidate) = Relation.from(raw === candidate)
|
|
336
|
+
|
|
337
|
+
def subset_of(other)
|
|
338
|
+
return Relation::PROVEN if equivalent?(other)
|
|
339
|
+
|
|
340
|
+
other.subset_from_pattern(self)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def subset_from_text_literal(other) = matches_value(other.raw.to_s)
|
|
344
|
+
|
|
345
|
+
def overlap(other)
|
|
346
|
+
return Relation::PROVEN if equivalent?(other)
|
|
347
|
+
|
|
348
|
+
other.overlap_with_pattern(self)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def overlap_with_nominal(other)
|
|
352
|
+
Relation.any(matcher_domain.map { |domain| SemanticMatcher.wrap(domain).overlap(other) })
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def overlap_with_literal(other)
|
|
356
|
+
return Relation::UNKNOWN unless other.is_a?(TextLiteral)
|
|
357
|
+
|
|
358
|
+
matches_value(other.raw.to_s)
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
module_function
|
|
363
|
+
|
|
364
|
+
# The only raw-class normalization switch. Core algebra receives nodes.
|
|
365
|
+
def wrap(raw)
|
|
366
|
+
return raw if raw.is_a?(Node)
|
|
367
|
+
|
|
368
|
+
case raw
|
|
369
|
+
when ::Module then Nominal.new(raw)
|
|
370
|
+
when ::Range then Interval.new(raw)
|
|
371
|
+
when ::Set then FiniteSet.new(raw)
|
|
372
|
+
when ::Regexp then Pattern.new(raw)
|
|
373
|
+
when ::Numeric then NumericLiteral.new(raw)
|
|
374
|
+
when ::String, ::Symbol then TextLiteral.new(raw)
|
|
375
|
+
when ::TrueClass, ::FalseClass, ::NilClass then Literal.new(raw)
|
|
376
|
+
when ::Array, ::Hash then CollectionMatcher.new(raw)
|
|
377
|
+
else Opaque.new(raw)
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def value_domain(raw) = wrap(raw).value_domain
|
|
382
|
+
def matcher_domain(raw) = wrap(raw).matcher_domain
|
|
383
|
+
def singleton?(raw) = wrap(raw).singleton?
|
|
384
|
+
def nominal?(raw) = wrap(raw).nominal?
|
|
385
|
+
def error_message(raw) = wrap(raw).error_message
|
|
386
|
+
|
|
387
|
+
def merge(left, right)
|
|
388
|
+
wrap(left).intersect(wrap(right))
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
end
|
|
392
|
+
private_constant :SemanticMatcher
|
|
393
|
+
end
|
data/lib/plumb/static_class.rb
CHANGED
|
@@ -20,8 +20,27 @@ module Plumb
|
|
|
20
20
|
self.class.new(value)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
# A static step ignores its input and always emits a fixed value. Its input
|
|
24
|
+
# is therefore Any — it accepts anything, so it opts out of #>> checks on
|
|
25
|
+
# the input side (eg. as the right of `Undefined >> Static[x]`). Its output
|
|
26
|
+
# is the value itself (the default #output_type — this atomic node wraps
|
|
27
|
+
# the value), so a successor that could never accept that value is caught
|
|
28
|
+
# at composition time, eg. `Types::Static['foo'] >> Types::Integer` raises.
|
|
29
|
+
def input_type = Types::Any
|
|
30
|
+
|
|
31
|
+
# Tests the concrete produced value directly. Atomic matcher logic is unsuitable
|
|
32
|
+
# here because it models matched values and widens numeric equality domains.
|
|
33
|
+
# @param other [Composable]
|
|
34
|
+
# @return [Boolean]
|
|
35
|
+
def subtype_of?(other)
|
|
36
|
+
return true if self == other
|
|
37
|
+
return super if @value.is_a?(Composable)
|
|
38
|
+
|
|
39
|
+
other === @value
|
|
40
|
+
end
|
|
41
|
+
|
|
23
42
|
def call(result)
|
|
24
|
-
result.valid(@value)
|
|
43
|
+
result.valid!(@value)
|
|
25
44
|
end
|
|
26
45
|
|
|
27
46
|
private
|