// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// A compiled regular expression over bytes: the `Bytes` counterpart of
/// `@string.Regex`, with its own byte-specialized parser and engine.
pub struct BytesRegex {
priv pat : @re.Pattern
priv mut re : @re.Regex?
}
///|
/// Compiles a regex pattern string into a `BytesRegex`.
///
/// The syntax is that of `@string.Regex`, read over bytes:
///
/// - An atom is one byte, and `.` matches any byte, including newline.
/// - `\xHH` is the byte `0xHH`; it may appear in character classes and
/// ranges, as in `[\x80-\xff]`.
/// - The pattern itself is ASCII: a non-ASCII character, `\uXXXX` and
/// `\u{X...}` are errors. Write a byte above `0x7F` as `\xHH`.
/// - `(?i: ... )`, `\b`, `\B` and the POSIX classes such as `[[:alpha:]]`
/// only know ASCII. Every byte above `0x7F` is a non-word byte.
/// - Negated classes such as `[^a]` match every other byte, including bytes
/// above `0x7F`.
///
/// Everything else, from quantifiers and groups to anchors and error
/// positions, is as in `@string.Regex`. Positions are byte offsets.
///
/// Raises when `pattern` is not a valid bytes regex pattern.
///
/// Example:
///
/// ```mbt check
/// test {
/// let regex = @bytes.BytesRegex("\\xff+(?[[:digit:]]+)")
/// guard regex.execute(b"a\xff\xff42") is Some(m) else { fail("Expected match") }
/// inspect(m.content(), content="b\"\\xff\\xff42\"")
/// inspect(m.named_group("rest").unwrap(), content="b\"42\"")
/// }
/// ```
pub fn BytesRegex::BytesRegex(pattern : StringView) -> BytesRegex raise {
let pat = @regex_parser.parse(pattern)
{ pat, re: None, }
}
///|
pub impl @debug.Debug for BytesRegex with fn to_repr(self) {
@debug.Repr::opaque_("BytesRegex", Repr(self.pat))
}
///|
/// Result of one successful match produced by `BytesRegex::execute`.
struct MatchResult {
input : BytesView
group_names : ReadOnlyArray[String?]
result : @re.MatchResult
} derive(@debug.Debug)
///|
/// Return match `before` view.
pub fn MatchResult::before(self : MatchResult) -> BytesView {
self.input[:self.result.start(0)]
}
///|
/// Return match `after` view.
pub fn MatchResult::after(self : MatchResult) -> BytesView {
self.input[self.result.end(0):]
}
///|
/// Return match `content` view.
pub fn MatchResult::content(self : MatchResult) -> BytesView {
self.input[self.result.start(0):self.result.end(0)]
}
///|
/// Returns a capture group; group `0` is the entire match.
/// Returns `None` for an out-of-range index or a group that did not participate.
pub fn MatchResult::group(
self : MatchResult,
group_index : Int32,
) -> BytesView? {
let start = self.result.start(group_index)
let end = self.result.end(group_index)
guard start >= 0 && end >= 0 else { None }
Some(self.input[start:end])
}
///|
/// Access capture `named_group` information.
pub fn MatchResult::named_group(
self : MatchResult,
name : String,
) -> BytesView? {
match self.group_names.search(Some(name)) {
None => None
Some(index) => self.group(index)
}
}
///|
/// The value of a `reb"..."` literal: the compiler checks the pattern at
/// compile time and builds the regex at run time with this function.
///
/// DO NOT call this function even in this package.
#internal(internal, "not intended for public use")
#doc(hidden)
pub fn BytesRegex::internal_from_string(pat : String) -> BytesRegex {
try! BytesRegex(pat)
}
///|
/// Concatenates two regexes in sequence.
///
/// Example:
///
/// ```mbt check
/// test {
/// let regex = @bytes.BytesRegex("ab") + BytesRegex("\\xff")
/// guard regex.execute(b"xab\xff") is Some(m) else { fail("Expected match") }
/// inspect(m.content(), content="b\"ab\\xff\"")
/// }
/// ```
// The compiler does not know `%bytes_regex.seq` yet and runs the body;
// drop the `#warnings` once a released compiler knows it.
#intrinsic("%bytes_regex.seq")
#warnings("-unknown_intrinsic")
pub impl Add for BytesRegex with fn add(self, other) -> BytesRegex {
{ pat: @re.seq([self.pat, other.pat]), re: None, }
}
///|
/// Builds an alternation that matches either regex.
///
/// Example:
///
/// ```mbt check
/// test {
/// let regex = @bytes.BytesRegex("^GET$") | BytesRegex("^POST$")
/// inspect(regex.execute(b"POST") is Some(_), content="true")
/// inspect(regex.execute(b"PUT") is Some(_), content="false")
/// }
/// ```
// The compiler does not know `%bytes_regex.alt` yet and runs the body;
// drop the `#warnings` once a released compiler knows it.
#intrinsic("%bytes_regex.alt")
#warnings("-unknown_intrinsic")
pub impl BitOr for BytesRegex with fn lor(self, other) -> BytesRegex {
{ pat: @re.alt([self.pat, other.pat]), re: None, }
}
///|
fn BytesRegex::re(self : BytesRegex) -> @re.Regex {
match self.re {
Some(re) => re
None => {
let re = @re.compile(self.pat)
self.re = Some(re)
re
}
}
}
///|
/// Search `input` for the leftmost-first match, as `@string.Regex::execute`
/// does for strings.
///
/// Search starts at `last_index` (default `0`), a byte offset that must
/// satisfy `0 <= last_index <= input.length()`. The returned match, when
/// present, starts at or after that index.
///
/// `last_index` only controls where searching starts. It does **not** change
/// anchor semantics: `^` still matches only the beginning of `input` and `$`
/// only its end, and `\b` looks at the byte before `last_index`.
///
/// Returns `None` when there is no match from `last_index` to the end of
/// `input`.
///
/// Example:
///
/// ```mbt check
/// test {
/// let regex = @bytes.BytesRegex("[[:digit:]]+")
/// let input = b"a12b34"
/// guard regex.execute(input) is Some(first) else {
/// fail("Expected first match")
/// }
/// inspect(first.content(), content="b\"12\"")
/// let next = first.before().length() + first.content().length()
/// guard regex.execute(input, last_index=next) is Some(second) else {
/// fail("Expected second match")
/// }
/// inspect(second.content(), content="b\"34\"")
/// let anchored = @bytes.BytesRegex("^ab$")
/// inspect(anchored.execute(b"xaby", last_index=1) is Some(_), content="false")
/// }
/// ```
pub fn BytesRegex::execute(
self : BytesRegex,
input : BytesView,
last_index~ : Int32 = 0,
) -> MatchResult? {
let re = self.re()
match re.execute(input, last_index) {
None => None
Some(result) => Some({ input, group_names: re.group_names(), result, })
}
}
///|
/// DO NOT call this function even in this package.
///
/// The offsets of the first match in `input`, without allocating a result:
/// group `i` is `input[offsets[2 * i]:offsets[2 * i + 1]]`, and `-1` marks a
/// group that did not participate. `self` keeps the array and overwrites it,
/// so what is needed must be read before the next search with `self`. For the
/// code `lexmatch` compiles to.
#doc(hidden)
pub fn BytesRegex::internal_search(
self : BytesRegex,
input : BytesView,
) -> FixedArray[Int32]? {
self.re().search(input, 0)
}