///|
const INFO_NAMECOUNT : UInt = 17
///|
const INFO_NAMEENTRYSIZE : UInt = 18
///|
const INFO_NAMETABLE : UInt = 19
///|
const INFO_NEWLINE : UInt = 20
///|
const NEWLINE_CR : UInt = 1
///|
const NEWLINE_LF : UInt = 2
///|
const NEWLINE_CRLF : UInt = 3
///|
const NEWLINE_ANY : UInt = 4
///|
const NEWLINE_ANYCRLF : UInt = 5
///|
const NEWLINE_NUL : UInt = 6
///|
const SUBSTITUTE_GLOBAL : UInt = 0x00000100U
///|
const SUBSTITUTE_OVERFLOW_LENGTH : UInt = 0x00001000U
///|
const ERROR_NOMATCH : Int = -1
///|
const NOTEMPTY_ATSTART : UInt = 0x00000008U
///|
const ANCHORED : UInt = 0x80000000U
///|
priv enum Pcre2Newline {
CR = 1
LF = 2
CRLF = 3
Any = 4
AnyCRLF = 5
Nul = 6
}
///|
fn Pcre2Newline::of_uint(value : UInt) -> Pcre2Newline {
match value {
NEWLINE_CR => Pcre2Newline::CR
NEWLINE_LF => Pcre2Newline::LF
NEWLINE_CRLF => Pcre2Newline::CRLF
NEWLINE_ANY => Pcre2Newline::Any
NEWLINE_ANYCRLF => Pcre2Newline::AnyCRLF
NEWLINE_NUL | _ => Pcre2Newline::Nul
}
}
///|
fn Code::newline(self : Code) -> Pcre2Newline raise Pcre2Error {
let newline = Ref::new(0U)
let result = pcre2_pattern_info_16_uint(self.0, INFO_NEWLINE, newline)
if result != 0 {
raise Pcre2Error::Code(result)
}
Pcre2Newline::of_uint(newline.val)
}
///|
fn Code::name_count(self : Code) -> UInt raise Pcre2Error {
let name_count = Ref::new(0U)
let result = pcre2_pattern_info_16_uint(self.0, INFO_NAMECOUNT, name_count)
if result != 0 {
raise Pcre2Error::Code(result)
}
name_count.val
}
///|
fn Code::name_table(self : Code) -> @c.Pointer[UInt16] raise Pcre2Error {
let name_table = Ref::new(@c.Pointer::null())
let result = pcre2_pattern_info_16_uint16_pointer(
self.0,
INFO_NAMETABLE,
name_table,
)
if result != 0 {
raise Pcre2Error::Code(result)
}
name_table.val
}
///|
fn Code::name_entry_size(self : Code) -> UInt raise Pcre2Error {
let name_entry_size = Ref::new(0U)
let result = pcre2_pattern_info_16_uint(
self.0,
INFO_NAMEENTRYSIZE,
name_entry_size,
)
if result != 0 {
raise Pcre2Error::Code(result)
}
name_entry_size.val
}
///|
priv type Pcre2Code
///|
fn Pcre2Code::is_null(self : Pcre2Code) -> Bool {
@pointer.unsafe_borrow(self, ptr => ptr.is_null())
}
///|
#external
priv type Pcre2CompileContext
///|
fn Pcre2CompileContext::null() -> Pcre2CompileContext {
@c.Pointer::unsafe_into(@c.Pointer::null())
}
///|
#external
priv type Pcre2GeneralContext
///|
fn Pcre2GeneralContext::null() -> Pcre2GeneralContext {
@c.Pointer::unsafe_into(@c.Pointer::null())
}
///|
priv type Pcre2MatchData
///|
#external
priv type Pcre2MatchContext
///|
fn Pcre2MatchContext::null() -> Pcre2MatchContext {
@c.Pointer::unsafe_into(@c.Pointer::null())
}
///|
type Pcre2Size = UInt64
///|
#owned(buffer)
extern "c" fn pcre2_get_error_message_16(
error_code : Int,
buffer : String,
buffer_length : Pcre2Size,
) -> Int = "moonbit_pcre2_get_error_message_16"
///|
#owned(pattern, error_code, error_offset)
extern "c" fn pcre2_compile_16(
pattern : String,
length : Pcre2Size,
options : UInt,
error_code : Ref[Int],
error_offset : Ref[Pcre2Size],
context : Pcre2CompileContext,
) -> Pcre2Code = "moonbit_pcre2_compile_16"
///|
#owned(code)
extern "c" fn pcre2_match_data_create_from_pattern_16(
code : Pcre2Code,
context : Pcre2GeneralContext,
) -> Pcre2MatchData = "moonbit_pcre2_match_data_create_from_pattern_16"
///|
#owned(match_data)
extern "c" fn pcre2_get_ovector_pointer_16(
match_data : Pcre2MatchData,
) -> @c.Pointer[Pcre2Size] = "moonbit_pcre2_get_ovector_pointer_16"
///|
#owned(code, subject, match_data, replacement, output_buffer, output_length)
extern "c" fn pcre2_substitute_16(
code : Pcre2Code,
subject : String,
subject_offset : Pcre2Size,
subject_length : Pcre2Size,
start_offset : Pcre2Size,
options : UInt,
match_data : Pcre2MatchData,
match_context : Pcre2MatchContext,
replacement : String,
replacement_offset : Pcre2Size,
replacement_length : Pcre2Size,
output_buffer : String,
output_length : Ref[Pcre2Size],
) -> Int = "moonbit_pcre2_substitute_16"
///|
#owned(match_data)
extern "c" fn pcre2_get_startchar_16(match_data : Pcre2MatchData) -> Pcre2Size = "moonbit_pcre2_get_startchar_16"
///|
#owned(code, subject, match_data, context)
extern "c" fn pcre2_match_16(
code : Pcre2Code,
subject : String,
length : Pcre2Size,
start_offset : Pcre2Size,
options : UInt,
match_data : Pcre2MatchData,
context : Pcre2MatchContext,
) -> Int = "moonbit_pcre2_match_16"
///|
#owned(code, where_)
extern "c" fn pcre2_pattern_info_16_uint(
code : Pcre2Code,
what : UInt,
where_ : Ref[UInt],
) -> Int = "moonbit_pcre2_pattern_info_16_uint32"
///|
#owned(code, where_)
extern "c" fn pcre2_pattern_info_16_uint16_pointer(
code : Pcre2Code,
what : UInt,
where_ : Ref[@c.Pointer[UInt16]],
) -> Int = "moonbit_pcre2_pattern_info_16_uint16_pointer"
///|
struct Code(Pcre2Code)
///|
pub fn compile(pattern : String) -> Code raise Pcre2Error {
let error_code = Ref::new(0)
let error_offset = Ref::new((0 : Pcre2Size))
let code = pcre2_compile_16(
pattern,
pattern.length().to_uint64(),
0,
error_code,
error_offset,
Pcre2CompileContext::null(),
)
if code.is_null() {
if error_code.val > 0 {
raise Pcre2Error::Compile(pattern, error_code.val, error_offset.val)
} else {
raise Pcre2Error::Code(error_code.val)
}
} else {
code
}
}
///|
struct Match {
subject : StringView
code_unit_offsets : FixedArray[Int]
name_table : Map[String, Int]
}
///|
trait MatchIndex {
get(Match, Self) -> StringView?
}
///|
pub impl MatchIndex for Int with get(self : Match, index : Int) -> StringView? {
if index < 0 || index * 2 + 1 >= self.code_unit_offsets.length() {
return None
}
let start = self.code_unit_offsets[index * 2]
let end = self.code_unit_offsets[index * 2 + 1]
Some(self.subject.view(start_offset=start, end_offset=end))
}
///|
pub impl MatchIndex for String with get(self : Match, index : String) -> StringView? {
guard self.name_table.get(index) is Some(index) else { None }
let start = self.code_unit_offsets[index * 2]
let end = self.code_unit_offsets[index * 2 + 1]
Some(self.subject.view(start_offset=start, end_offset=end))
}
///|
pub impl MatchIndex for StringView with get(self : Match, index : StringView) -> StringView? {
guard self.name_table.get(index.to_string()) is Some(index) else { None }
let start = self.code_unit_offsets[index * 2]
let end = self.code_unit_offsets[index * 2 + 1]
Some(self.subject.view(start_offset=start, end_offset=end))
}
///|
pub fn[Index : MatchIndex] Match::op_get(
self : Match,
index : Index,
) -> StringView {
Index::get(self, index).unwrap()
}
///|
pub fn[Index : MatchIndex] Match::get(
self : Match,
index : Index,
) -> StringView? {
Index::get(self, index)
}
///|
pub fn Match::groups(self : Match) -> Iter2[Int, StringView] {
let mut i = 0
let total = self.code_unit_offsets.length() / 2
Iter2::new(fn() {
if i < total {
let start = self.code_unit_offsets[2 * i]
let end = self.code_unit_offsets[2 * i + 1]
let index = i
i += 1
Some((index, self.subject.view(start_offset=start, end_offset=end)))
} else {
None
}
})
}
///|
pub fn Match::named_groups(self : Match) -> Iter2[String, StringView] {
let iter = self.name_table.iter2()
Iter2::new(fn() {
guard iter.next() is Some((name, index)) else { None }
let start = self.code_unit_offsets[2 * index]
let end = self.code_unit_offsets[2 * index + 1]
Some((name, self.subject.view(start_offset=start, end_offset=end)))
})
}
///|
pub suberror Pcre2Error {
LookaroundBSK
Compile(String, Int, Pcre2Size)
Code(Int)
}
///|
pub impl Show for Pcre2Error with output(self : Pcre2Error, logger : &Logger) -> Unit {
fn write_error_message(code : Int, logger : &Logger) -> Unit {
let message = String::make(256, '\u{00}')
let length = message.length().to_uint64()
let length = pcre2_get_error_message_16(code, message, length)
if length < 0 {
logger.write_string("Unknown error")
} else {
logger.write_string([..message.view(end_offset=length)])
}
}
match self {
LookaroundBSK => logger.write_string("Lookaround BSK")
Compile(pattern, code, offset) => {
logger.write_string("Compilation error @ \{offset}: ")
write_error_message(code, logger)
logger.write_char('\n')
let start = @cmp.maximum(0, offset.to_int() - 10)
let end = @cmp.minimum(pattern.length(), offset.to_int() + 10)
logger.write_substring(pattern, start, end)
for _ in 0.. write_error_message(code, logger)
}
}
///|
fn array_to_string(array : FixedArray[UInt16]) -> String = "%identity"
///|
struct Matches {
code : Code
subject : String
subject_length : Pcre2Size
subject_start_offset : Pcre2Size
mut match_data : Pcre2MatchData?
crlf_is_newline : Bool
mut ovector : @c.Pointer[Pcre2Size]
name_count : Int
name_table : @c.Pointer[UInt16]
name_entry_size : Int
}
///|
/// Retrieves the next match from the iterator, advancing the internal state to
/// find subsequent matches in the subject string.
///
/// This method implements the core pattern matching logic, handling various edge
/// cases including empty matches, lookaround assertions with `\K`, and proper
/// Unicode character boundary handling. It maintains internal state to ensure
/// all matches are found in sequence without duplicates or infinite loops.
///
/// Parameters:
///
/// * `self` : The `Matches` iterator containing the compiled pattern, subject
/// string, and matching state.
///
/// Returns `Some(Match)` containing the next match with capturing groups and
/// named groups, or `None` if no more matches are found.
///
/// Throws `Pcre2Error` in the following cases:
///
/// * `Pcre2Error::Code(Int)` for general PCRE2 matching errors
/// * `Pcre2Error::Unicode(UnicodeError)` for Unicode-related errors during
/// character offset conversion
/// * `Pcre2Error::LookaroundBSK` when encountering problematic lookaround
/// assertions with `\K`
///
/// Examples:
///
/// ```moonbit nocheck
/// let pattern = @pcre2.compile("\\d+")
/// let matches = pattern.matches("abc123def456")
/// let first = matches.next().unwrap()
/// inspect(first[0], content="123")
/// let second = matches.next().unwrap()
/// inspect(second[0], content="456")
/// assert_true(matches.next() is None)
/// ```
///
/// ```moonbit nocheck
/// let pattern = @pcre2.compile("a*")
/// let matches = pattern.matches("aabaa")
/// let first = matches.next().unwrap()
/// inspect(first[0], content="aa")
/// let second = matches.next().unwrap()
/// inspect(second[0], content="") // empty match between 'b' and 'a'
/// ```
///
/// ```moonbit nocheck
/// let pattern = @pcre2.compile("(?P\\d+)")
/// let matches = pattern.matches("number: 42")
/// let match_result = matches.next().unwrap()
/// inspect(match_result["num"], content="42")
/// inspect(match_result[0], content="42")
/// ```
pub fn Matches::next(self : Matches) -> Match? raise Pcre2Error {
guard self.match_data is Some(match_data) else { return None }
fn next() -> Match? raise Pcre2Error {
let rc = if self.ovector.is_null() {
let rc = pcre2_match_16(
self.code.0,
self.subject,
self.subject_length,
self.subject_start_offset,
0,
match_data,
Pcre2MatchContext::null(),
)
if rc == ERROR_NOMATCH {
return None
}
self.ovector = pcre2_get_ovector_pointer_16(match_data)
rc
} else {
let mut options : UInt = 0 // Normally no options
let mut start_offset = self.ovector[1] // Start at end of previous match
// If the previous match was for an empty string, we are finished if we are
// at the end of the subject. Otherwise, arrange to run another match at the
// same point to see if a non-empty match can be found.
if self.ovector[0] == self.ovector[1] {
if self.ovector[0] == self.subject_length {
return None
}
options = NOTEMPTY_ATSTART | ANCHORED
} else {
// If the previous match was not an empty string, there is one tricky case to
// consider. If a pattern contains \K within a lookbehind assertion at the
// start, the end of the matched string can be at the offset where the match
// started. Without special action, this leads to a loop that keeps on matching
// the same substring. We must detect this case and arrange to move the start on
// by one character. The pcre2_get_startchar() function returns the starting
// offset that was passed to pcre2_match_16().
let startchar = pcre2_get_startchar_16(match_data)
if start_offset <= startchar {
if startchar >= self.subject_length {
return None
}
start_offset = startchar + 1
}
}
let rc = pcre2_match_16(
self.code.0,
self.subject,
self.subject_length,
start_offset,
options,
match_data,
Pcre2MatchContext::null(),
)
// This time, a result of NOMATCH isn't an error. If the value in "options"
// is zero, it just means we have found all possible matches, so the loop ends.
// Otherwise, it means we have failed to find a non-empty-string match at a
// point where there was a previous empty-string match. In this case, we do
// what Perl does: advance the matching position by one character, and
// continue. We do this by setting the "end of previous match" offset, because
// that is picked up at the top of the loop as the point at which to start
// again.
// There are two complications: (a) When CRLF is a valid newline sequence, and
// the current position is just before it, advance by an extra byte. (b)
// Otherwise we must ensure that we skip an entire UTF character if we are in
// UTF mode.
if rc == ERROR_NOMATCH {
if options == 0U {
return None // All matches found
}
self.ovector[1] = start_offset + 1 // Advance one code unit
if self.crlf_is_newline && // If CRLF is a newline & we are at CRLF
start_offset < self.subject_length - 1 &&
self.subject.code_unit_at(start_offset.to_int()) == 13 &&
self.subject.code_unit_at(start_offset.to_int() + 1) == 10 {
self.ovector[1] += 1 // Advance by one more code unit
}
// NOTE(translator): The original code test if the UTF8 flag is set. However,
// we are using UTF16, so we can skip the check.
return self.next() // Go round the loop again
}
rc
}
// Other matching errors are not recoverable.
if rc < 0 {
raise Pcre2Error::Code(rc)
}
// The match succeeded, but the output vector wasn't big enough. This
// should not happen.
if rc == 0 {
abort("Ovector was not big enough")
}
// We must guard against patterns such as /(?=.\K)/ that use \K in an
// assertion to set the start of a match later than its end. In this
// demonstration program, we just detect this case and give up.
if self.ovector[0] > self.ovector[1] {
raise Pcre2Error::LookaroundBSK
}
// As before, show substrings stored in the output vector by number, and
// then also any named substrings.
// NOTE(translator): The pcre2demo.c prints the matches to the console. We
// return the match here instead.
let matched = Match::{
subject: self.subject,
code_unit_offsets: FixedArray::make(2 * rc, 0),
name_table: {},
}
for i in 0.. {
self.match_data = None
raise error
}
}
}
///|
/// Creates an iterator over all matches of the pattern in a subject string. Each
/// match contains both numbered and named capturing groups.
///
/// Parameters:
///
/// * `self` : A compiled regular expression pattern.
/// * `subject` : The string to search for matches.
///
/// Returns a `Matches` iterator that yields each match in sequence. Each match
/// contains information about the matched substring and any capturing groups.
///
/// Throws `Pcre2Error` if an error occurs during pattern matching.
///
/// Example:
///
/// ```moonbit nocheck
/// let pattern = @pcre2.compile("[0-9]+")
/// let matches = pattern.matches("abc123def456")
/// let first = matches.next().unwrap()
/// inspect(first[0], content="123")
/// let second = matches.next().unwrap()
/// inspect(second[0], content="456")
/// assert_true(matches.next() is None)
/// ```
pub fn Code::matches(
self : Code,
subject : StringView,
) -> Matches raise Pcre2Error {
// The following code has been adapted from the pcre2demo.c file in the PCRE2
// library.
let match_data = pcre2_match_data_create_from_pattern_16(
self.0,
Pcre2GeneralContext::null(),
)
let subject_start_offset = subject.start_offset().to_uint64()
let subject_length = subject_start_offset + subject.length().to_uint64()
let crlf_is_newline = match self.newline() {
Any | CRLF | AnyCRLF => true
CR | LF | Nul => false
}
let ovector = @c.Pointer::null()
let name_count = self.name_count().reinterpret_as_int()
let name_table = self.name_table()
let name_entry_size = self.name_entry_size().reinterpret_as_int()
return Matches::{
code: self,
subject: subject.data(),
subject_length,
subject_start_offset,
match_data: Some(match_data),
crlf_is_newline,
ovector,
name_count,
name_table,
name_entry_size,
}
}
///|
pub fn Code::substitute(
self : Code,
subject : StringView,
replacement : StringView,
global? : Bool = false,
) -> StringView raise {
let match_data = pcre2_match_data_create_from_pattern_16(
self.0,
Pcre2GeneralContext::null(),
)
let subject_length = subject.length().to_uint64()
let replacement_length = replacement.length().to_uint64()
let output_length : Ref[UInt64] = Ref::new(subject.length().to_uint64() * 2)
while true {
let output = String::make(output_length.val.to_int(), '\u{00}')
let mut options = SUBSTITUTE_OVERFLOW_LENGTH
if global {
options = options | SUBSTITUTE_GLOBAL
}
let start_offset = 0UL
let rc = pcre2_substitute_16(
self.0,
subject.data(),
subject.start_offset().to_uint64(),
subject_length,
start_offset,
options,
match_data,
Pcre2MatchContext::null(),
replacement.data(),
replacement.start_offset().to_uint64(),
replacement_length,
output,
output_length,
)
if rc < 0 {
raise Pcre2Error::Code(rc)
}
break output.view(end_offset=output_length.val.to_int())
} nobreak {
abort("unreachable")
}
}
///|
const CONFIG_JIT : UInt = 1
///|
#owned(where_)
extern "c" fn pcre2_config_16_int(what : UInt, where_ : Ref[Int]) -> Int = "moonbit_pcre2_config_16_int"
///|
pub fn config_jit() -> Bool raise {
let val = Ref::new(0)
let rc = pcre2_config_16_int(CONFIG_JIT, val)
if rc != 0 {
raise Pcre2Error::Code(rc)
}
val.val is 1
}
///|
#owned(code)
extern "c" fn pcre2_jit_compile(code : Pcre2Code, options : UInt) -> Int = "moonbit_pcre2_jit_compile_16"
///|
const PCRE2_JIT_COMPLETE : UInt = 0x00000001U
///|
const PCRE2_JIT_PARTIAL_SOFT : UInt = 0x00000002U
///|
const PCRE2_JIT_PARTIAL_HARD : UInt = 0x00000004U
///|
pub fn Code::jit_compile(
code : Code,
complete? : Bool = false,
partial_soft? : Bool = false,
partial_hard? : Bool = false,
) -> Unit raise Pcre2Error {
let mut options : UInt = 0
if complete {
options = options | PCRE2_JIT_COMPLETE
}
if partial_soft {
options = options | PCRE2_JIT_PARTIAL_SOFT
}
if partial_hard {
options = options | PCRE2_JIT_PARTIAL_HARD
}
let rc = pcre2_jit_compile(code.0, options)
if rc != 0 {
raise Pcre2Error::Code(rc)
}
}