Class: Plumb::Constraint

Inherits:
Object
  • Object
show all
Includes:
Composable
Defined in:
lib/plumb/constraint.rb

Constant Summary collapse

EMPTY =

Sentinel returned by merge_matchers/intersect_ranges when two same-kind matchers have a PROVABLY-EMPTY overlap (disjoint Ranges, empty Set intersection). Distinct from nil, which means "not the same knowable kind, or an incomputable overlap — leave the two matchers stacked". Callers map EMPTY to Types::Never, so Integer[0..5][10..] == Integer[0..5] & Integer[10..]

Types::Never (see .narrow and Subtyping.intersect_constraints).

::Object.new
LITERAL_MATCHERS =

Matcher kinds whose #=== is just #== — they match exactly ONE value. Two distinct ones are therefore provably disjoint, and as a #>> consumer one accepts only its own value. Deliberately excludes Module/Range/Regexp/ Set/proc: their #=== is a type, membership or pattern test that matches many values (and Set#=== is #include? on Ruby >= 3.1).

[::String, ::Symbol, ::Numeric, ::TrueClass, ::FalseClass, ::NilClass].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Composable

#&, #/, #>>, #[], #absorb_input, #absorb_output, #as_node, #build, #check, #defer, #fusable_step?, #fuse_with, #generate, included, #invalid, #invoke, #match, #metadata, #not, #output_type, #pipeline, #policy, resolve_operand, #static, #to_json_schema, #to_mermaid, #to_plumb_type, #to_s, #transform, #value, #where, #with, wrap, #|

Methods included from Callable

#parse, #resolve

Constructor Details

#initialize(matcher = Undefined, base: nil, error: nil, label: nil) ⇒ Constraint

Returns a new instance of Constraint.

Parameters:

  • matcher (#===) (defaults to: Undefined)

    the value/type/predicate to match against

  • base (Composable, nil) (defaults to: nil)

    the type this matcher refines. nil for a root type matcher (eg. Types::Integer is Constraint(::Integer)); set for a refinement built via #[]/#match/#check (eg. Integer[1..10] is Constraint(1..10, base: Types::Integer)). Carrying the base lets a matcher answer subtyping locally — it is a subtype of whatever its base is — so no And wrapper (or transparency flag) is needed to preserve the base type.

Raises:



42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/plumb/constraint.rb', line 42

def initialize(matcher = Undefined, base: nil, error: nil, label: nil)
  raise ParseError, 'matcher must respond to #===' unless matcher.respond_to?(:===)

  raise_if_incompatible!(base, matcher) if base

  @matcher = matcher
  @matcher_node = SemanticMatcher.wrap(matcher)
  @base = base
  @error = error.nil? ? build_error(matcher) : (error % matcher)
  @label = matcher.is_a?(Class) ? matcher.inspect : "Constraint(#{label || @matcher.inspect})"
  @children = [matcher].freeze
  freeze
end

Instance Attribute Details

#baseObject (readonly)

Returns the value of attribute base.



33
34
35
# File 'lib/plumb/constraint.rb', line 33

def base
  @base
end

#childrenObject (readonly)

Returns the value of attribute children.



33
34
35
# File 'lib/plumb/constraint.rb', line 33

def children
  @children
end

#matcherObject (readonly)

Returns the value of attribute matcher.



33
34
35
# File 'lib/plumb/constraint.rb', line 33

def matcher
  @matcher
end

Class Method Details

.literal_matcher?(matcher) ⇒ Boolean

The matcher-level form of #literal? for normalization callers that do not yet have a Constraint. Prefer #literal? on an existing Constraint.

Parameters:

  • matcher (#===)

Returns:

  • (Boolean)

    whether matcher matches a single value by equality



31
# File 'lib/plumb/constraint.rb', line 31

def self.literal_matcher?(matcher) = SemanticMatcher.singleton?(matcher)

.merge_matchers(a, b) ⇒ Object

Intersection of two knowable matchers of the same kind. Returns the merged matcher on a non-empty overlap, EMPTY when the overlap is provably empty (disjoint Ranges / empty Set intersection — callers reduce it to Never), or nil to not merge (different kinds, or an incomputable Range overlap — leave the two matchers stacked). Public because AttributeValueMatch narrowing reuses it (see Subtyping) — an attribute constraint intersects its Range/Set values just like a Constraint.



79
80
81
82
83
84
85
# File 'lib/plumb/constraint.rb', line 79

def self.merge_matchers(a, b)
  merged = SemanticMatcher.merge(a, b)
  return EMPTY if merged.empty?
  return merged.node.raw if merged.merged?

  nil
end

.narrow(base, matcher) ⇒ Object

Smart refinement constructor: builds Constraint.new(matcher, base:), but when both base (a Constraint) and matcher are the same kind of knowable matcher (Ranges or Sets), it INTERSECTS them into one over base's own base rather than stacking two checks — so Integer[0..100][10..] is Integer[10..100], Integer[Set[1,2,3]][Set[2,3,4]] is Integer[Set[2,3]], and Integer[0..100] >> Integer[0..] reduces to Integer[0..100]. Other matchers stack as before.



63
64
65
66
67
68
69
70
# File 'lib/plumb/constraint.rb', line 63

def self.narrow(base, matcher)
  if base.is_a?(Constraint)
    merged = merge_matchers(base.matcher, matcher)
    return Types::Never if merged.equal?(EMPTY) # provably-empty overlap ⇒ bottom
    return narrow(base.base, merged) if merged # recurse: base.base may be a type gate
  end
  new(matcher, base:)
end

Instance Method Details

#==(other) ⇒ Boolean

Compares matchers and bases. Fresh Procs lack structural equality, so Proc matchers use their caller-supplied label as stable identity.

Parameters:

  • other (Object)

Returns:

  • (Boolean)


175
176
177
178
179
180
181
182
183
# File 'lib/plumb/constraint.rb', line 175

def ==(other)
  return false unless other.instance_of?(self.class) && other.base == base

  if matcher.is_a?(::Proc)
    other.matcher.is_a?(::Proc) && other.label == label
  else
    other.matcher == matcher
  end
end

#accepted_typeComposable

A converting base runs before the matcher, so this constraint consumes what the base accepts. Pure refinements accept themselves.

Returns:



112
113
114
115
116
# File 'lib/plumb/constraint.rb', line 112

def accepted_type
  return self unless @base && !Plumb::Subtyping.value_preserving?(@base)

  Plumb::Subtyping.accepted_type(@base)
end

#call(result) ⇒ Object



185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/plumb/constraint.rb', line 185

def call(result)
  # Only the base can invalidate the incoming result (callers — map/Or/resolve
  # — always pass a valid one), so the `valid?` short-circuit is only needed
  # when there IS a base. Root constraints skip straight to the matcher check,
  # like a flat leaf. Reading @base directly (not the #base reader) also skips
  # a dispatch on this hot path.
  if @base
    result = @base.call(result)
    return result unless result.valid?
  end

  @matcher === result.value ? result : result.invalid!(errors: @error)
end

#idempotent?Boolean

Returns whether the base and matcher are safe to deduplicate.

Returns:

  • (Boolean)

    whether the base and matcher are safe to deduplicate



107
# File 'lib/plumb/constraint.rb', line 107

def idempotent? = @base.nil? || @base.idempotent?

#input_typeObject

As the consumer of a left >> self chain:

- a refinement (has a base), a Class/Module gate, or a literal matcher
accepts exactly what it validates — itself — so `Integer >>
Integer[1..10]` and `Any[5] >> Any[6]` are correctly rejected (narrow
with `#[]` instead), and the default `#accepted_type` (its resolved
input) is `self`;
- a bare pattern matcher (regex/range/set/proc with no base) narrows
arbitrary input over a domain it cannot name, so it reports Any and opts
out of the check.


96
97
98
99
100
# File 'lib/plumb/constraint.rb', line 96

def input_type
  return self if base || @matcher_node.nominal? || literal?

  Types::Any
end

#literal?Boolean

Whether this constraint matches a SINGLE value, by equality — so two distinct ones are provably disjoint (see Subtyping#literal_value).

Returns:

  • (Boolean)


104
# File 'lib/plumb/constraint.rb', line 104

def literal? = @matcher_node.singleton?

#subtype_identityComposable

Projects a matcher over a conversion onto the narrowed output type. Without this, subtype checks would treat the node as consuming the value it produces.

Returns:



121
122
123
124
125
126
# File 'lib/plumb/constraint.rb', line 121

def subtype_identity
  return self if @base.nil? || Plumb::Subtyping.value_preserving?(@base)

  produced = Plumb::Subtyping.resolved_output(@base)
  produced.equal?(@base) ? self : Constraint.narrow(produced, @matcher)
end

#subtype_of?(other) ⇒ Boolean

Structural subtyping. A matcher describes the set base ∩ {x | matcher === x} (base = everything when nil). self <= other when self's set is contained in each of other's conjuncts: within other's base (if any) AND within other's matcher. Against a non-matcher type, a refinement is a subtype of whatever its base is (like AttributeValueMatch).

Returns:

  • (Boolean)


138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/plumb/constraint.rb', line 138

def subtype_of?(other)
  return true if self == other

  if other.is_a?(Constraint)
    within_base = other.base.nil? || Plumb::Subtyping.subtype?(self, other.base)
    return true if within_base && within_matcher?(other.matcher)

    # `self` is `base ∩ {matcher}` — a subset of `base` — so whenever the
    # base alone is a subtype of `other`, so is `self`. This is the same
    # fallback the non-Constraint branch below uses; it belongs here too
    # because base types (`Types::String`, `Types::Date`) ARE Constraints,
    # and `within_matcher?` can't peel a transparent base (a Metadata/Policy
    # wrapper, eg. `String.metadata(...).present`) to see the guarantee.
    base ? Plumb::Subtyping.subtype?(base, other) : false
  elsif base
    Plumb::Subtyping.subtype?(base, other)
  else
    false
  end
end

#value_preserving?Boolean

A constraint may wrap a converting base; treating it as a pure filter would let reductions discard that conversion.

Returns:

  • (Boolean)

    whether the base and matcher preserve the input value



131
# File 'lib/plumb/constraint.rb', line 131

def value_preserving? = @base.nil? || Plumb::Subtyping.value_preserving?(@base)