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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +887 -64
  3. data/bench/compare_dry_schema.rb +79 -0
  4. data/bench/compare_dry_types.rb +37 -0
  5. data/bench/compare_parametric_schema.rb +2 -80
  6. data/bench/dry_schema_hash.rb +103 -0
  7. data/bench/dry_types_hash.rb +125 -0
  8. data/bench/json_schema_profile.rb +107 -0
  9. data/bench/plumb_hash.rb +17 -11
  10. data/bench/results_allocations.rb +137 -0
  11. data/bench/sample_data.rb +78 -0
  12. data/examples/command_objects.rb +1 -1
  13. data/examples/concurrent_downloads.rb +16 -9
  14. data/examples/event_registry.rb +6 -1
  15. data/examples/weekdays.rb +1 -1
  16. data/lib/plumb/and.rb +63 -6
  17. data/lib/plumb/any_class.rb +12 -2
  18. data/lib/plumb/array_class.rb +133 -25
  19. data/lib/plumb/attribute_value_match.rb +41 -1
  20. data/lib/plumb/attributes.rb +59 -19
  21. data/lib/plumb/codec.rb +886 -0
  22. data/lib/plumb/composable.rb +451 -39
  23. data/lib/plumb/conjunction.rb +50 -0
  24. data/lib/plumb/constraint.rb +234 -0
  25. data/lib/plumb/covariant_fusion.rb +46 -0
  26. data/lib/plumb/decorator.rb +12 -22
  27. data/lib/plumb/deferred.rb +13 -5
  28. data/lib/plumb/disjunction.rb +112 -0
  29. data/lib/plumb/encoder.rb +207 -0
  30. data/lib/plumb/function.rb +347 -0
  31. data/lib/plumb/hash_class.rb +339 -32
  32. data/lib/plumb/hash_map.rb +58 -14
  33. data/lib/plumb/implementation.rb +247 -0
  34. data/lib/plumb/interface_class.rb +21 -2
  35. data/lib/plumb/intersection.rb +47 -0
  36. data/lib/plumb/json_schema_visitor.rb +255 -36
  37. data/lib/plumb/key.rb +63 -13
  38. data/lib/plumb/mermaid_visitor.rb +129 -0
  39. data/lib/plumb/metadata.rb +10 -1
  40. data/lib/plumb/metadata_visitor.rb +36 -34
  41. data/lib/plumb/never_class.rb +38 -0
  42. data/lib/plumb/node_mapper.rb +97 -0
  43. data/lib/plumb/not.rb +34 -2
  44. data/lib/plumb/optimizer.rb +444 -0
  45. data/lib/plumb/or.rb +26 -29
  46. data/lib/plumb/pipeline.rb +99 -11
  47. data/lib/plumb/policy.rb +17 -4
  48. data/lib/plumb/range_class.rb +46 -0
  49. data/lib/plumb/relation.rb +57 -0
  50. data/lib/plumb/result.rb +55 -23
  51. data/lib/plumb/semantic_matcher.rb +393 -0
  52. data/lib/plumb/static_class.rb +20 -1
  53. data/lib/plumb/stream_class.rb +28 -6
  54. data/lib/plumb/subtyping.rb +461 -0
  55. data/lib/plumb/tagged_hash.rb +45 -4
  56. data/lib/plumb/tuple_class.rb +21 -4
  57. data/lib/plumb/type_cache.rb +41 -0
  58. data/lib/plumb/type_registry.rb +71 -0
  59. data/lib/plumb/typed_step.rb +67 -0
  60. data/lib/plumb/types.rb +44 -43
  61. data/lib/plumb/union.rb +30 -0
  62. data/lib/plumb/value_class.rb +20 -1
  63. data/lib/plumb/version.rb +1 -1
  64. data/lib/plumb/visitor_handlers.rb +20 -4
  65. data/lib/plumb.rb +90 -3
  66. metadata +30 -8
  67. data/lib/plumb/build.rb +0 -22
  68. data/lib/plumb/match_class.rb +0 -42
  69. data/lib/plumb/schema.rb +0 -195
  70. data/lib/plumb/step.rb +0 -27
  71. 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.is_a?(self.class) &&
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::Valid]
33
- # @return [Result::Valid, Result::Invalid]
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
- Valid.new(value)
30
+ def valid(value = nil)
31
+ new(value, valid: true)
8
32
  end
9
33
 
10
34
  def invalid(value = nil, errors: nil)
11
- Invalid.new(value, errors:)
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? = true
29
- def invalid? = false
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
- class Valid < self
50
- def map(callable)
51
- callable.call(self)
52
- end
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
- class Invalid < self
56
- def valid? = false
57
- def invalid? = true
78
+ def invalid!(val = value, errors: nil)
79
+ @value = val
80
+ @valid = false
81
+ @errors = errors
82
+ self
83
+ end
58
84
 
59
- def map(_)
60
- self
61
- end
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
@@ -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