Module: Rigor::Builtins::RegexRefinement
- Defined in:
- lib/rigor/builtins/regex_refinement.rb
Overview
Maps a curated table of canonical regex sub-patterns onto the imported refinement
carriers Rigor already ships (decimal-int-string, hex-int-string,
octal-int-string, lowercase-string, uppercase-string, numeric-string). See
docs/type-specification/imported-built-in-types.md for the registry the refinements
come from and docs/ROADMAP.md § "v0.1.1 — Planned" Track 1 slice 1 for the binding
scope of this recogniser.
The intended consumer is Inference::Narrowing.analyse_match_write: given if /(?<year>\d+)/ =~ str; year; end, the v0.1.0 baseline narrows year to plain String;
v0.1.1 introspects the regex source and narrows further to decimal-int-string
whenever the named-capture body matches one of the rows in RULES.
Recognised body shapes (each row admits the + quantifier and the bounded {n} /
{n,m} forms with n >= 1):
- `\d`, `[0-9]` -> decimal-int-string
- `\h` -> hex-int-string
- `[0-9a-fA-F]` -> hex-int-string
- `[0-9a-f]`, `[0-9A-F]` -> hex-int-string
- `[0-7]` -> octal-int-string
- `[a-z]` -> lowercase-string
- `[A-Z]` -> uppercase-string
- `[[:digit:]]` -> numeric-string
Anything outside the table returns nil so the calling narrowing site falls back to
its previous behaviour (plain String). Arbitrary regex semantic equivalence is
undecidable, so the table is intentionally a small audited set of canonical shapes
rather than a general equivalence checker.
Class Method Summary collapse
-
.for_capture_body(body) ⇒ Rigor::Type?
The matching imported refinement carrier, or
nilifbodyis not a recognised shape. -
.for_whole_pattern(source) ⇒ Rigor::Type?
Whole-receiver regime (#164).
-
.valid_bounds?(body) ⇒ Boolean
Filters the bounded-quantifier forms to ones whose lower bound is at least 1 and whose upper bound (if any) is at least the lower bound.
Class Method Details
.for_capture_body(body) ⇒ Rigor::Type?
Returns the matching imported refinement carrier, or nil if
body is not a recognised shape.
71 72 73 74 75 76 77 78 79 |
# File 'lib/rigor/builtins/regex_refinement.rb', line 71 def for_capture_body(body) return nil if body.nil? || body.empty? rule = RULES.find { |pattern, _| pattern.match?(body) } return nil if rule.nil? return nil unless valid_bounds?(body) Type::Combinator.public_send(rule.last) end |
.for_whole_pattern(source) ⇒ Rigor::Type?
Whole-receiver regime (#164). Where for_capture_body narrows a named-capture body —
sound without anchors because the capture boundary is itself the anchor — this maps a
WHOLE regex source (as from Regexp#source or a literal RegularExpressionNode)
matched against an entire string via String#match? / =~. The predicate proves a
property of the whole string only when the pattern is fully anchored, so a required
leading \A and trailing \z are load-bearing: both must be present, and the inner
body must be one of the curated single-char-class shapes for_capture_body already
audits.
Deliberately rejected (each admits a string the carrier would misdescribe):
- bare / one-sided anchoring (`\d+`, `\A\d+`, `\d+\z`) — matches a substring, not the
whole string;
- `\Z` (capital) — anchors before an optional trailing newline, so `"12\n"` matches
`/\A\d+\Z/` yet is not a decimal-int-string;
- line anchors `^` / `$` — match per-line, so `"12\nabc"` matches `/^\d+$/`;
- any inner body outside the curated table (`\w+`, alternation, multi-class).
Extended (//x) mode is the caller's concern — the char-class table cannot see the
free-whitespace/#-comment flag from the source alone, so consumers bail on it before
calling here.
105 106 107 108 109 110 111 112 113 |
# File 'lib/rigor/builtins/regex_refinement.rb', line 105 def for_whole_pattern(source) return nil if source.nil? return nil unless source.start_with?('\A') && source.end_with?('\z') inner = source[2...-2] return nil if inner.nil? || inner.empty? for_capture_body(inner) end |
.valid_bounds?(body) ⇒ Boolean
Filters the bounded-quantifier forms to ones whose lower bound is at least 1 and
whose upper bound (if any) is at least the lower bound. Without this, \d{0,5} would
be accepted even though it admits the empty string, which is not a valid
decimal-int-string.
119 120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/rigor/builtins/regex_refinement.rb', line 119 def valid_bounds?(body) m = BOUND_RE.match(body) return true if m.nil? low = Integer(m[1]) return false if low < 1 high = m[2] && Integer(m[2]) return true if high.nil? low <= high end |