Class: Rigor::Type::Refined

Inherits:
Object
  • Object
show all
Includes:
AcceptanceRouter, ValueSemantics
Defined in:
lib/rigor/type/refined.rb

Overview

‘Refined[base, predicate_id]` — predicate-subset half of the OQ3 refinement-carrier strategy ([ADR-3](docs/adr/3-type-representation.md), Working Decision Option C). Sibling of `Type::Difference`, which carries the point-removal half.

lowercase-string = Refined[Nominal[String], :lowercase]
uppercase-string = Refined[Nominal[String], :uppercase]
numeric-string   = Refined[Nominal[String], :numeric]

The carrier wraps a base type and a ‘predicate_id` Symbol drawn from PREDICATES. The recogniser is invoked at constant-fold and acceptance time over a `Constant<base>` value; for non-Constant receivers the carrier is a marker the catalog tier consults to project `String#downcase` / `String#upcase` (etc.) into the matching refinement.

Display routes through CANONICAL_NAMES: registered ‘(base_class_name, predicate_id)` pairs print in their kebab-case spelling (`lowercase-string`); unregistered combinations fall back to the `base & predicate?` operator form per [`type-operators.md`](docs/type-specification/type-operators.md).

Construction MUST go through ‘Type::Combinator.refined` / the per-name factories (`Combinator.lowercase_string`, `Combinator.uppercase_string`, `Combinator.numeric_string`). Direct `.new` is an internal escape hatch for tests and combinator’s own implementation.

Constant Summary collapse

PREDICATES =
{
  lowercase: ->(v) { v.is_a?(String) && v == v.downcase },
  not_lowercase: ->(v) { v.is_a?(String) && v != v.downcase },
  uppercase: ->(v) { v.is_a?(String) && v == v.upcase },
  not_uppercase: ->(v) { v.is_a?(String) && v != v.upcase },
  numeric: ->(v) { ruby_numeric_literal?(v) },
  not_numeric: ->(v) { v.is_a?(String) && !ruby_numeric_literal?(v) },
  decimal_int: ->(v) { v.is_a?(String) && DECIMAL_INT_STRING_PATTERN.match?(v) },
  octal_int: ->(v) { v.is_a?(String) && OCTAL_INT_STRING_PATTERN.match?(v) },
  hex_int: ->(v) { v.is_a?(String) && HEX_INT_STRING_PATTERN.match?(v) },
  # `literal-string` is a flow-tracked predicate, not a value-
  # level predicate: a String is literal-string when it is
  # known to come from a source-code literal (or composition
  # of literals). Every concrete `Constant<String>` is
  # already literal by construction, so the inspection
  # recogniser returns true for any String — the property is
  # really tracked in the flow analysis (interpolation,
  # concatenation, RBS::Extended `return: literal-string`)
  # rather than recovered by inspecting an arbitrary string.
  literal_string: ->(v) { v.is_a?(String) }
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from ValueSemantics

included

Methods included from AcceptanceRouter

#accepts

Constructor Details

#initialize(base, predicate_id) ⇒ Refined

Returns a new instance of Refined.

Raises:

  • (ArgumentError)


43
44
45
46
47
48
49
# File 'lib/rigor/type/refined.rb', line 43

def initialize(base, predicate_id)
  raise ArgumentError, "predicate_id must be a Symbol" unless predicate_id.is_a?(Symbol)

  @base = base
  @predicate_id = predicate_id
  freeze
end

Instance Attribute Details

#baseObject (readonly)

Returns the value of attribute base.



41
42
43
# File 'lib/rigor/type/refined.rb', line 41

def base
  @base
end

#predicate_idObject (readonly)

Returns the value of attribute predicate_id.



41
42
43
# File 'lib/rigor/type/refined.rb', line 41

def predicate_id
  @predicate_id
end

Class Method Details

.ruby_numeric_literal?(value) ⇒ Boolean

Returns true when ‘value` is a String that is a single, complete Ruby numeric literal. Total over arbitrary input — never raises (Prism reports malformed input through `errors`, it does not throw).

Parameters:

  • value (Object)

    typically a ‘Constant#value`

Returns:

  • (Boolean)

    true when ‘value` is a String that is a single, complete Ruby numeric literal. Total over arbitrary input — never raises (Prism reports malformed input through `errors`, it does not throw).



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/rigor/type/refined.rb', line 176

def self.ruby_numeric_literal?(value)
  return false unless value.is_a?(String)
  return false if value.empty?
  # A numeric literal carries no whitespace; reject any
  # leading / trailing / interior space so the *whole* string
  # must be the literal (Prism would otherwise accept a
  # trailing-space `"1 "`).
  return false if value.match?(/\s/)
  return false unless value.match?(NUMERIC_LITERAL_PREFIX)

  result = Prism.parse(value)
  return false unless result.errors.empty?

  body = result.value.statements&.body
  return false unless body && body.size == 1

  node = body.first
  NUMERIC_LITERAL_NODES.any? { |klass| node.is_a?(klass) }
end

Instance Method Details

#botObject



68
69
70
# File 'lib/rigor/type/refined.rb', line 68

def bot
  Trinary.no
end

#complement_predicate_idSymbol?

Returns the registered complement predicate id, or nil when no pair is registered for this predicate.

Returns:

  • (Symbol, nil)

    the registered complement predicate id, or nil when no pair is registered for this predicate.



269
270
271
# File 'lib/rigor/type/refined.rb', line 269

def complement_predicate_id
  COMPLEMENT_PAIRS[predicate_id]
end

#describe(verbosity = :short) ⇒ Object



51
52
53
54
55
56
# File 'lib/rigor/type/refined.rb', line 51

def describe(verbosity = :short)
  named = canonical_name
  return named if named

  "#{base.describe(verbosity)} & #{predicate_id}?"
end

#dynamicObject



72
73
74
# File 'lib/rigor/type/refined.rb', line 72

def dynamic
  base.respond_to?(:dynamic) ? base.dynamic : Trinary.no
end

#erase_to_rbsObject

Erases to the base nominal: every refinement MUST erase to its base per [‘rbs-erasure.md`](docs/type-specification/rbs-erasure.md).



60
61
62
# File 'lib/rigor/type/refined.rb', line 60

def erase_to_rbs
  base.erase_to_rbs
end

#inspectObject



82
83
84
# File 'lib/rigor/type/refined.rb', line 82

def inspect
  "#<Rigor::Type::Refined #{describe(:short)}>"
end

#matches?(value) ⇒ Boolean

Recognises a Ruby value against this carrier’s predicate. The trinary return is intentional: ‘true` / `false` when the predicate registry decides, `nil` when the predicate is unknown to the registry, so callers (today Inference::Acceptance) can fall through to gradual-mode `:maybe`. rubocop:disable Style/ReturnNilInPredicateMethodDefinition

Returns:

  • (Boolean)


93
94
95
96
97
98
# File 'lib/rigor/type/refined.rb', line 93

def matches?(value)
  recogniser = PREDICATES[predicate_id]
  return nil if recogniser.nil?

  !!recogniser.call(value)
end

#topObject



64
65
66
# File 'lib/rigor/type/refined.rb', line 64

def top
  Trinary.no
end